If you are training a binary classifier, chances are you are using binary cross-entropy / log loss as your loss function.
Have you ever thought about what exactly does it mean to use this loss function? The thing is, given the ease of use of today’s libraries and frameworks, it is very easy to overlook the true meaning of the loss function used.
I was looking for a blog post that would explain the concepts behind binary cross-entropy / log loss in a visually clear and concise manner, so I could show it to my students at Data Science Retreat. Since I could not find any that would fit my purpose, I took the task of writing it myself 🙂
Now, let’s assign some colors to our points: red and green. These are our labels.
Figure 1: the data
So, our classification problem is quite straightforward: given our feature x, we need to predict its label: red or green.
Since this is a binary classification, we can also pose this problem as: “is the point green” or, even better, “what is the probability of the point being green”? Ideally, green points would have a probability of 1.0 (of being green), while red points would have a probability of 0.0 (of being green).
In this setting, green points belong to the positive class (YES, they are green), while red points belong to the negative class (NO, they are not green).
If we fit a model to perform this classification, it will predict a probability of being green to each one of our points. Given what we know about the color of the points, how can we evaluate how good (or bad) are the predicted probabilities? This is the whole purpose of the loss function! It should return high values for bad predictions and low values for good predictions.
For a binary classification like our example, the typical loss function is the binary cross-entropy / log loss.
Loss Function: Binary Cross-Entropy / Log Loss
If you look this loss function up, this is what you’ll find:
Binary Cross-Entropy / Log Loss
where y is the label (1forgreen points and 0forred points) and p(y) is the predicted probability of the point being green for all N points.
Reading this formula, it tells you that, for each green point (y=1), it adds log(p(y)) to the loss, that is, the log probability of it being green. Conversely, it adds log(1-p(y)), that is, the log probability of it being red, for each red point (y=0). Not necessarily difficult, sure, but no so intuitive too…
Besides, what does entropy have to do with all this? Why are we taking log of probabilities in the first place? These are valid questions and I hope to answer them on the “Show me the math” section below.
But, before going into more formulas, let me show you a visual representation of the formula above…
Computing the Loss — the visual way
First, let’s split the points according to their classes, positive or negative, like the figure below:
Figure 2: splitting the data!
Now, let’s train a Logistic Regression to classify our points. The fitted regression is a sigmoid curve representing the probability of a point being green for any given x . It looks like this:
Figure 3: fitting a Logistic Regression
Then, for all points belonging to the positive class (green), what are the predicted probabilities given by our classifier? These are the green barsunder the sigmoid curve, at the x coordinates corresponding to the points.
Figure 4: probabilities of classifying points in the POSITIVE class correctly
OK, so far, so good! What about the points in the negative class? Remember, the green barsunder the sigmoid curve represent the probability of a given point being green. So, what is the probability of a given point being red? The red bars ABOVEthe sigmoid curve, of course 🙂
Figure 5: probabilities of classifying points in the NEGATIVE class correctly
Putting it all together, we end up with something like this:
Figure 6: all probabilities put together!
The bars represent the predicted probabilities associated with the corresponding true class of each point!
OK, we have the predicted probabilities… time to evaluate them by computing the binary cross-entropy / log loss!
These probabilities are all we need, so, let’s get rid of the x axis and bring the bars next to each other:
Figure 7: probabilities of all points
Well, the hanging bars don’t make much sense anymore, so let’s reposition them:
Figure 8: probabilities of all points — much better 🙂
Since we’re trying to compute a loss, we need to penalize bad predictions, right? If the probability associated with the true class is 1.0, we need its loss to be zero. Conversely, if that probability islow, say, 0.01, we need its loss to be HUGE!
It turns out, taking the (negative) log of the probability suits us well enough for this purpose (since the log of values between 0.0 and 1.0 is negative, we take the negative log to obtain a positive value for the loss).
Actually, the reason we use log for this comes from the definition of cross-entropy, please check the “Show me the math” section below for more details.
The plot below gives us a clear picture —as the predicted probability of the true class gets closer to zero, the loss increases exponentially:
Figure 9: Log Loss for different probabilities
Fair enough! Let’s take the (negative) log of the probabilities — these are the corresponding losses of each and every point.
Finally, we compute the mean of all these losses.
Figure 10: finally, the loss!
Voilà! We have successfully computed the binary cross-entropy / log loss of this toy example. It is 0.3329!
Show me the code
If you want to double check the value we found, just run the code below and see for yourself 🙂
Show me the math (really?!)
Jokes aside, this post is not intended to be very mathematically inclined… but for those of you, my readers, looking to understand the role of entropy, logarithms in all this, here we go 🙂
If you want to go deeper into information theory, including all these concepts — entropy, cross-entropy and much, much more — check Chris Olah’s post out, it is incredibly detailed!
Let’s start with the distribution of our points. Since y represents the classes of our points (we have 3 red points and 7 green points), this is what its distribution, let’s call it q(y), looks like:
Figure 11: q(y), the distribution of our points
Entropy is a measure of the uncertainty associated with a given distribution q(y).
What if all our points were green? What would be the uncertainty of that distribution? ZERO, right? After all, there would be no doubt about the color of a point: it is always green! So, entropy is zero!
On the other hand, what if we knew exactly half of the points were green and the other half, red? That’s the worst case scenario, right? We would have absolutely no edge on guessing the color of a point: it is totally random! For that case, entropy is given by the formula below (we have two classes (colors)— red or green — hence, 2):
Entropy for a half-half distribution
For every other case in between, we can compute the entropy of a distribution, like our q(y), using the formula below, where C is the number of classes:
So, if we know the true distribution of a random variable, we can compute its entropy. But, if that’s the case, why bother training a classifier in the first place? After all, we KNOW the true distribution…
But, what if we DON’T? Can we try to approximate the true distribution with some other distribution, say, p(y)? Sure we can! 🙂
Let’s assume our pointsfollow this other distribution p(y). But we know they are actually coming from the true (unknown) distribution q(y), right?
If we compute entropy like this, we are actually computing the cross-entropy between both distributions:
If we, somewhat miraculously, match p(y) to q(y) perfectly, the computed values for both cross-entropy and entropy will match as well.
Since this is likely never happening, cross-entropy will have a BIGGER value than the entropy computed on the true distribution.
Cross-Entropy minus Entropy
It turns out, this difference between cross-entropy and entropy has a name…
The Kullback-Leibler Divergence,or “KL Divergence” for short, is a measure of dissimilarity between two distributions:
This means that, the closer p(y) gets to q(y), the lower the divergence and, consequently, the cross-entropy, will be.
So, we need to find a good p(y) to use… but, this is what our classifier should do, isn’t it?! And indeed it does! It looks for the best possiblep(y), which is the one that minimizes the cross-entropy.
During its training, the classifier uses each of the N points in its training set to compute the cross-entropy loss, effectively fitting the distribution p(y)! Since the probability of each point is 1/N, cross-entropy is given by:
Cross-Entropy —point by point
Remember Figures 6 to 10 above? We need to compute the cross-entropy on top of the probabilities associated with the true class of each point. It means using the green bars for the points in the positive class (y=1) and the red hanging bars for the points in the negativeclass (y=0) or, mathematically speaking:
Mathematical expression corresponding to Figure 10 🙂
The final step is to compute the average of all points in both classes, positive and negative:
Binary Cross-Entropy — computed over positive and negative classes
Finally, with a little bit of manipulation, we can take any point, either from the positive or negative classes, under the same formula:
Binary Cross-Entropy — the usual formula
Voilà! We got back to the original formula for binary cross-entropy / log loss 🙂
I truly hope this post was able shine some new light on a concept that is quite often taken for granted, that of binary cross-entropy as loss function. Moreover, I also hope it served to show you a little bit how Machine Learning and Information Theory are linked together.
If you have any thoughts, comments or questions, please leave a comment below or contact me on Twitter.
This is the second post of my series on hyper-parameters. In this post, I will show you the importance of properly initializing the weights of your deep neural network. We will start with a naive initialization scheme and work out its issues, like the vanishing / exploding gradients, till we (re)discover two popular initialization schemes: Xavier / Glorot and He.
I am assuming you’re already familiar with some key concepts (Z-values, activation functions and its gradients) which I covered on my first post of this series.
The plots illustrating this post were generated using my package, DeepReplay, which you can find on GitHub and learn more about it on this post.
On my quest to have a deeper understanding of the effects of each and every one of the different hyper-parameters on training a deep neural network, it is time to investigate the weight initializers.
If you have ever searched for this particular topic, you’ve likely ran into some common initialization schemes:
Xavier / Glorot
If you dug a little bit deeper, you’ve likely also found out that one should use Xavier / Glorot initialization if the activation function is a Tanh, and that He initialization is the recommended one if the activation function is a ReLU.
By the way, just to clear something out: Xavier Glorot, along with Yoshua Bengio, are the authors of the “Understanding the difficulty of training deep feedforward neural networks” paper, which outlines the initialization scheme that takes either first (Xavier) or last (Glorot) name of its first author. So, sometimes this scheme will be referred to as Xavier initialization, and some other times (like in Keras), it will be referred to as Glorot initialization. Don’t be confused by this, as I was the first time I learned about this topic.
Having cleared that out, I ask you once again: have you ever wondered what exactly is going on under the hood? Why is initialization so important? What is the difference between the initialization schemes? I mean, not only their different definitions for what the variance should be, but the overall effect of using one or the other while training a deep neural network!
Before diving into it, I want to give credit where it is due: the plots were heavily inspired by Andre Perunicic’s awesome post on this very same topic.
OK, NOW let’s dive into it!
Make sure you’re using Keras 2.2.0 or newer—older versions had an issue, generating sets of weights with variance lower than expected!
In this post, I will use a model with 5 hidden layers with 100 units each, and a single unit output layer as in a typical binary classification task (that is, using a sigmoid as activation function and binary cross-entropy as loss). I will refer to this model as the BLOCK model, as it has consecutive layers of same size. I used the following code to build my model:
Model builder function
This is the architecture of the BLOCK model, regardless of the activation function and/or initializer I will be using to build the plots.
The inputs are 1,000 random points drawn from a 10-dimensional ball (this seems fancier than it actually is, you can think of it as a dataset with 1,000 samples with 10 features each) such that the samples have zero mean and unit standard deviation.
In this dataset, points situated within half of the radius of the ball are labeled as negative cases (0), while the remaining points are labeled positive cases (1).
Loading 10-dimensional ball dataset using DeepReplay
Naive Initialization Scheme
At the very beginning, there was a sigmoid activation function and randomly initialized weights. And training was hard, convergence was slow, and results were not good.
Back then, the usual procedure was to draw random values from a Normal distribution (zero mean, unit standard deviation) and multiply them by a small number, say 0.01. The result would be a set of weights with a standard deviation of approximately 0.01. And this led to some problems…
Before going a bit deeper (just a bit, I promise!) into the mathematical reason why this was a bad initialization scheme, let me show what would be the result of using it in the BLOCK model with sigmoid activation function:
Code to build the plots! Just change the initializer and have fun! 🙂
Figure 1. BLOCK model using sigmoid and naive initialization — don’t try this at home!
This does NOT look good, right? Can you spot everything that is going bad?
Both Z-values (remember, these are the outputs BEFORE applying the activation function) and Activations are within a narrow range;
Gradients are pretty much zero;
And what is the deal with the weird distributions on the left column?!
Unfortunately, this network is likely not learning much anytime soon, should we choose to try and train it. Why? Because its gradients VANISHED!
And we have not even started the train process yet! This is Epoch 0! What happened so far, then? Let’s see:
Weights were initialized using the naive scheme (upper right subplot)
1,000 samples were used in a forward pass through the network and generated Z-values (upper left subplot) and Activations (lower left subplot) for all the layers (output layer was not included in the plot)
The loss was computed against the true labels and backpropagated through the network, generating the gradients for all layers (lower right subplot)
That is it! One single pass through the network!
Next, we should update the weights accordingly and repeat the process, right? But, wait… if gradients are pretty much zero, the updated weights are going to be pretty much the same, right?
What does it mean? It means that our network is borderline useless, as it cannot LEARN anything (that is, update its weights to perform the proposed classification task) in a reasonable amount of time.
Welcome to an extreme case of vanishing gradients!
You may be thinking: “yeah, sure, the standard deviation was too low, no way it could work like that”. So, how about trying different values, say, 10x or 100x bigger?
Standard deviation 10x bigger = 0.10
Figure 2. BLOCK model using 10x bigger standard deviation
OK, this looks a bit better… Z-values and Activations are within a decent range, gradients for the second to last hidden layers are showing some improvement, but still vanishing towards the initial layers.
Maybe going even BIGGER can fix the gradients, let’s see…
Standard deviation 100x bigger = 1.00
Figure 3. BLOCK model using 100x bigger standard deviation
OK, seems like we had some progress in the vanishing gradients problem, as the ranges of all layers got more similar to each other. Yay! But… we ruined the Z-values and Activations altogether… Z-values now exhibit a too wide range, forcing the Activations pretty much into binary mode.
Trying a different Activation Function
If you read my first post on hyper-parameters, you’ll recall that a Sigmoid activation function has this fundamental issue of being centered around 0.5. So, let’s keep following the evolution path of neural networks and use a Tanh activation function instead!
Figure 4. BLOCK model using Tanh and naive initialization
Replacing a Sigmoid for a Tanh activation function while keeping the naive initialization scheme with small random values from a Normal distribution led us to yet another vanishing gradients situation (it may notlook like it, after all, they aresimilar along all the layers, but check the scale, gradients vanished in the last layer already!), accompanied by vanishing Z-values and vanishing Activations also (just to be clear, these two are not real terms)! Definitely, not the way to go!
Let’s go all the way to using a BIG standard deviation and see how it goes (yes, I am saving the best for last…).
Figure 5. BLOCK model using Tanh and a BIG standard deviation
In this setup, we can observe the exploding gradients problem, for a change. See how gradient values grow bigger and bigger as we backpropagate from the last to the first hidden layer? Besides, just like it happened when using a Sigmoid activation function, Z-values have a too wide range and Activations are collapsing into either zero or one for the most part. Again, not good!
And, as promised, the winner is… Tanh with a standard deviation of 0.10!
Figure 6. BLOCK model looking good!
Why is this one the winner? Let’s check its features:
First, gradients are reasonably similaralong all the layers (and within a decent scale— about 20x smaller than the weights)
Second, Z-values are within a decent range (-1, 1) and are reasonably similar along all the layers (though some shrinkage is noticeable)
Third, Activations have not collapsed into binary mode, and are reasonably similar along all the layers (again, with some shrinkage)
If you haven’t noticed yet, being similar along all the layers is a big deal! Putting it simply, it means we can stack yet another layer at the end of the network and expect a similar distribution of Z-values, Activations and, of course, gradients. We definitely do NOT like collapsing, vanishing or exploding behaviors in our network, no, Sir!
But, being similar along all the layers is the effect, not the cause… as you probably guessed already, the key is the standard deviation of the weights!
So, we need an initialization scheme that uses the best possible standard deviation for drawing random weights! Enter Xavier Glorot and Yoshua Bengio…
Xavier / Glorot Initialization Scheme
Glorot and Bengio devised an initialization scheme that tries to keep all the winning features listed , that is, gradients, Z-values and Activationssimilar along all the layers. Another way of putting it: keeping variance similar along all the layers.
How did they pull this one out, you ask? We’ll see that in a moment, we just need to do a reallybrief recap on a fundamental property of the variance.
Really Brief Recap
Let’s say we have x values (either inputs or activation values from a previous layer) and W weights. The variance of the product of two independent variables is given by the formula below:
Then, let’s assume both x and W have zero mean. The expression above turns into a simple product of both variances of x and W.
There are two important points to make here:
The inputs should have zero mean for this to hold in the first layer, so always scale and center your inputs!
Sigmoid activation function poses a problem for this, as the activation values will have a mean of 0.5, NOTzero! For more details on how to compensate for this, please check this post.
Given the point #2, it only makes sense to stick with Tanh, right? So, that is exactly what we’ll do! Now, it is time to apply this knowledge to a tiny example, so we arrive (hopefully!) at the same conclusion as Glorot and Bengio.
Figure 7. Two hidden layers of a network
This example is made of two hidden layers, X and Y, fully connected (I am throwing usual conventions to the wind to keep mathematical notation to a bare minimum!).
We only care about the weights connecting these two layers and, guess what, the variances of both activations and gradients.
For the activations, we need to go through a forward pass in the network. For the gradients, we need to backpropagate.
Figure 8. Tanh activation function
And, for the sake of keeping the math simple, we will assume that the activation function is linear (instead of a Tanh) in the equations, meaning that the activation values are the same as Z-values.
Although this may seem a bit of a stretch, Figure 8 shows us that a Tanh is roughly linear in the interval [-1, 1], so the results should hold, at least in this interval.
Figure 9. Forward Pass
So, instead of using a vectorized approach, we’ll be singling out ONE unit, y1, and work the math for it.
Figure 9 provides a clear-cut picture of the parts involved, namely:
three units in the previous layer (fan-in)
weights (w11, w21 and w31)
the unit we want to compute variance for, y1
Assuming that x and W are independent and identically distributed, we can work out some simple math for the variance of y1:
Remember the brief recap on variance? Time to put it for good use!
OK, almost there! Remember, our goal is to keep variance similar along all the layers. In other words, we should aim for making the variance of x the same as the variance of y.
For our single unit, y1, this can be accomplished by choosing the variance of its connecting weights to be:
And, generalizing for all the connecting weights between hidden layers X and Y, we have:
By the way, this is the variance to be used if we’re drawing random weights from a Normaldistribution!
What if we want to use a Uniform distribution? We just have to compute the (symmetric) lower and upper limits, as shown below:
Are we done?! Not yet… don’t forget the backpropagation, we also want to keep the gradients similar along all the layers (its variance, to be more precise).
Figure 10. Backward Pass
Backward Pass (Backpropagation)
Again, let’s single out ONE unit, x1, for the backward pass.
Figure 10 provides a clear-cut picture of the parts involved, namely:
five units in the following layer (fan-out)
weights (w11, w12, w13, w14 and w15)
the unit we want to compute the variance of the gradients with respect to it, x1
Basically, we will make the same assumptions and follow the same steps as in the forward pass. For the variance of the gradients with respect to x1, we can work out the math the same way:
Using what we learned on the brief recap once again:
And, to keep the variance of gradients similar along all the layers, we find the needed variance of its connecting weights to be:
OK, we have gone a long way already! The inverse of the “fan in” gives us the desired variance of the weights for the forward pass, while the inverse of the “fan out” gives us the desired variance of the (same!) weights for the backpropagation.
But… what if “fan in” and “fan out” have VERY different values?
Reconciling Forward and Backward Passes
Can’t decide which one to choose, “fan in” or “fan out”, for calculating the variance of your network weights? No problem, just take the average!
So, we finally arrive at the expression for the variance of the weights, as in Glorot and Bengio, to be used with a Normal distribution:
And, for the Uniform distribution, we compute the limits accordingly:
Congratulations! You (re)discovered the Xavier / Glorot initialization scheme!
But, there is still one small detail, should you choose a Normal distribution to draw the weights from…
Truncated Normal and Keras’ Variance Scaling
When it comes to the weights of a neural network, we want them to be neatly distributed around zero and, even more than that, we don’t want any outliers! So, we truncate it!
What does it mean to truncate it? Just get rid of any values farther than twice the standard deviation! So, if you use a standard deviation of 0.1, a truncated normal distribution will have absolutely no values below -0.2 or above 0.2 (as in the left plot of Figure 11).
The thing is, once you cut out the tails of the normal distribution, the remaining values have a slightly lower standard deviation… 0.87962566103423978 of the original value, to be precise.
In Keras, before version 2.2.0, this difference in a truncated normal distribution was not taken into account in the Variance Scaling initializer, which is the base for Glorot and He initializers. So, it is possible that, in deeper models, initializers based on uniform distributions would have performed better than its normal counterparts, which suffered from a slowly shrinking variance layer after layer…
As of today, this is not an issue anymore, and we can observe the effect of compensating for the truncation in the right plot of Figure 11, where the distribution of the Variance Scaling initializer is clearly wider.
Figure 11. Truncated normal and Keras’ Variance Scaling
Some plots, PLEASE!
Thank you very much for bearing with me through the more mathematical parts. Your patience is going to be rewarded with plots galore!
Let’s see how the Glorot initializer (as it is called in Keras) performs, using both Normal and Uniform distributions.
It looks like we have two winners!
Do you remember our previous winner, the BLOCK model using the naive initialization scheme with a standard deviation of 0.1 in Figure 6? The results are incredibly similar, right?
Well, it turns out that a standard deviation of 0.1 we used there is exactly the right value, according to the Glorot initialization scheme, when we have “fan in” and “fan out” equal to 100. It was not using a truncated normal distribution, though…
So, this initialization scheme solves our issues with vanishing and exploding gradients… but does it work with a different activation function other than the Tanh? Let’s see…
Rectified Linear Unit (ReLU) Activation Function
Can we stick with the same initialization scheme and use a ReLU as activation function instead?
Luckily, everything we got while (re)discovering the Glorot initialization scheme still holds. There is only one tiny adjustment we need to make… multiply the variance of the weights by 2! Really, that’s all that it takes!
Figure 15. ReLU activation function
Simple enough, right? But, WHY?
The reason is also pretty straightforward: the ReLU turns half of the Z-values (the negative ones) into zeros, effectively removing about half of the variance. So, we need to double the variance of the weights to compensate for it.
Since we know that the Glorot initialization scheme preserves variance (1), how to compensate for the variance halving effect of the ReLU (2)? The result (3), as expected, is doubling the variance.
So, the expression for the variance of the weights, as in He et al., to be used with a Normal distribution is:
And, for the Uniform distribution, we compute the limits accordingly:
Congratulations! You (re)discovered the He initialization scheme!
But… what about the backpropagation? Shouldn’t we use the average of both “fans” once again? Actually, there is no need for it. He et al. showed in their paper that, for common network designs, if the initialization scheme scales the activation values during the forward pass, it does the trick for the backpropagation as well! Moreover, it works both ways, so we could even use “fan out” instead of “fan in”.
Now, it is time for more plots!
Figure 16. BLOCK model with ReLU and He Uniform initializer
Again, two more winners! When it comes to the distributions of Z-values, they look remarkably similar along all the layers! As for the gradients, they look a bit more “vanishy” now than when we used the Tanh/Glorot duo… Does this mean that Tanh/Glorot is better than ReLU/He? We know this is not true…
But, then, why its gradients did not look so good on Figure 16? Well, once again, don’t forget to look at the scale! Even though the variance of the gradients decreases as we backpropagate through the network, its values are nowherenearvanished (if you remember Figure 4, it was the other way around — the variance was similar along the layers, but it scale was around 0.0000001!).
So, we need not only a similar variance along all the layers, but also a proper scale for the gradients. The scale is quite important, as it will, together with the learning rate, define how fast the weights are going to be updated. If the gradients are way too small, the learning (that is, the update of the weights) will be extremely slow.
How small is too small, you ask? As always, it depends… on the magnitude of the weights. So, too small is not an absolute measure, but a relative one.
If we compute the ratio between the variance of the gradients and the variance of the corresponding weights (or its standard deviations, for that matter), we can roughly compare the learning speed of different initialization schemes and its underlying distributions (assuming a constant learning rate).
So, it is time for the…
Showdown — Normal vs Uniform and Glorot vs He!
To be honest, GlorotvsHe actually means TanhvsReLU and we all know the answer to this match (spoiler alert!): ReLU wins!
And what about NormalvsUniform? Let’s check the plot below:
Figure 17. How big are the gradients, after all?
And the winner is… Uniform! It is clear that, at least for our particular BLOCK model and inputs, using an Uniform distribution yields relatively biggergradients than using a Normal distribution.
Moreover, as expected, using a ReLU yields relatively bigger gradients than using a Tanh. For our particular example, this doesn’t hold true for the first layer because its “fan in” is only 10 (the dimension of the inputs). Should we use 100-dimension inputs, the gradients for a ReLU would have been bigger for that layer as well.
And, even though it may seem like gradients are kinda “vanishing” when using a ReLU, just take a look at the tiny purple bar to the very right of Figure 17… I slipped the naively initialized and Sigmoid activated network into the plot to highlight how badtrue vanishing gradients are 🙂 And, if the plot still cannot convince you, I throw in also the corresponding table:
Ratio: standard deviation of gradients over standard deviation of weights
In summary, for a ReLU activated network, the He initialization scheme using an Uniform distribution is a pretty good choice 😉
There are many, many more ways to analyze the effects of choosing a particular initialization scheme… we could try different network architectures (like “funnel” or “hourglass” shaped), deeper networks, changing the distribution of the labels (and, therefore, the loss)… I tried LOTS of combinations, and He/Uniform always outperformed the other initialization schemes, but this post is too long already!
This was a looong post, especially for a topic so taken for granted as weightinitializers! But I felt that, for one to really appreciate its importance, one should follow the steps and bump into the issues that led to the development of the schemes used nowadays.
Even though, as a practitioner, you know the “right” combinations to use for initializing your network, I really hope this post was able to give you some insights into what is really happening and, most importantly, whythat particular combination is the “right” one 🙂
If you have any thoughts, comments or questions, please leave a comment below or contact me on Twitter.
https://aideepdive.com/wp-content/uploads/2019/02/weight_init-1000x423.jpg4231000Danielhttps://aideepdive.com/wp-content/uploads/2019/02/AIDD-LOGO-final.svgDaniel2018-12-03 13:30:502019-02-13 21:00:53Hyper-parameters in Action! Weight Initializers
In my previous post, I invited you to wonder what exactly is going on under the hood when you train a neural network. Then I investigated the role of activation functions, illustrating the effect they have on the feature spaceusing plots and animations.
Now, I invite you to play an active role on the investigation!
It turns out these plots and animations drew quite some attention. So I decided to organize my code and structure it into a proper Python package, so you can plot and animate your own Deep Learning models!
How do they look like, you ask? Well, if you haven’t checked the original post yet, here it is a quick peek at it:
This is what animating with DeepReplay looks like 🙂
So, without further ado, I present you… DeepReplay!
The package is called DeepReplay because this is exactly what it allows you to do: REPLAY the process of training your Deep Learning Model, plotting and animating several aspects of it.
The process is simple enough, consisting of five steps:
It all starts with creating an instance of a callback!
Then, business as usual: build and train your model.
Next, load the collected data into Replay.
Finally, create a figure and attach the visualizations to it.
The callback takes, as arguments, the model inputs (X and y), as well as the filename and group name where you want to store the collected training data.
Two things to keep in mind:
For toy datasets, it is fine to use the same X and y as in your model fitting. These are the examples that will be plot —so, you can choose a random subset of your dataset to keep computation times reasonable, if you are using a bigger dataset.
The data is stored in a HDF5 file, and you can use the same fileseveral times over, but never the same group! If you try running it twice using the same group name, you will get an error.
2. Build and train your model
Like I said, business as usual, nothing to see here… just don’t forget to add your callback instance to the list of callbacks when fitting!
[gist id=”86591c9796731c21f920e01ed2376b23″ /]
3. Load collected data into Replay
So, the part that gives the whole thing its name… time to replay it!
It should be straightforward enough: create an instance of Replay, providing the filename and the group name you chose in Step 1.
[gist id=”019637d6d041fdbd269db9a78a2311b6″ /]
4. Create a figure and attach visualizations to it
This is the step where things get interesting, actually. Just use Matplotlib to create a figure, as simple as the one in the example, or as complex as subplot2grid allows you to make it, and start attaching visualizations from your Replay object to the figure.
[gist id=”ba49bdca40a2abaa68af39922e78a556″ /]
The example above builds a feature space based on the output of the layer named, suggestively, hidden.
But there are five types of visualizations available:
Feature Space: plot representing the twisted and turned feature space, corresponding to the output of a hidden layer (only 2-unit hidden layers supported for now), including grid lines for 2-dimensional inputs;
Decision Boundary: plot of a 2-D grid representing the original feature space, together with the decision boundary (only 2-dimensional inputs supported for now);
Probability Histogram: two histograms of the resulting classification probabilities for the inputs, one for each class, corresponding to the model output (only binary classification supported for now);
Loss and Metric: line plot for both the loss and a chosen metric, computed over all the inputs you passed as arguments to the callback;
Loss Histogram: histogram of the losses computed over all the inputs you passed as arguments to the callback (only binary cross-entropy loss supported for now).
5. Plot and/or animate it!
For this example, with a singlevisualization, you can use its plot and animate methods directly. These methods will return, respectively, a figure and an animation, which you can then save to a file.
[gist id=”83ef91da63de149f5a58f6e428ab37f3″ /]
If you decide to go with multiple simultaneous visualizations, there are two helper methods that return composed plots and animations, respectively: compose_plots and compose_animations.
To illustrate these methods, here is a gist that comes from the “canonical” example I used in my original post. There are four visualizations and five plots (Probability Histogram has two plots, for negative and positive cases).
The animated GIF at the beginning of this post is actually the result of this composed animation!
[gist id=”6ad78608f5ae7ebe2c31f84f9b001625″ /]
At this point, you probably noticed that the two coolest visualizations, Feature Space and Decision Boundary, are limited to two dimensions.
I plan on adding support for visualizations in three dimensions also, but most of datasets and models have either more inputs or hidden layers with many more units.
So, these are the options you have:
2D inputs, 2-unit hidden layer: Feature Space with optional grid (check the Activation Functions example);
3D+ inputs, 2-unit hidden layer: Feature Space, but no grid;
2D inputs, hidden layer with 3+ units: Decision Boundary with optional grid (check the Circles example);
nothing is two dimensional: well… there is always a workaround, right?
Working around multidimensionality
What do we want to achieve? Since we can only do 2-dimensional plots, we want 2-dimensional outputs — simple enough.
How to get 2-dimensional outputs? Adding an extra hidden layer with two units, of course! OK, I know this is suboptimal, as it is actually modifying the model (did I mention this is a workaround?!). We can then use the outputs of this extra layer for plotting.
You can check either the Moons or the UCI Spambase notebooks, for examples on adding an extra hidden layer and plotting it.
NOTE: The following part is a bit more advanced, it delves deeper into the reasoning behind adding the extra hidden layer and what it represents. Proceed at your own risk 🙂
What are we doing with the model, anyway? By adding an extra hidden layer, we can think of our model as having two components: an encoder and a decoder. Let’s dive just a bit deeper into those:
Encoder: the encoder goes from the inputs all the way to our extra hidden layer. Let’s consider its 2-dimensional output as features and call them f1 and f2.
Decoder: the decoder, in this case, is just a plain and simple logistic regression, which takes two inputs, say, f1 and f2, and outputs a classification probability.
Let me try to make it more clear with a network diagram:
Encoder / Decoder after adding an extra hidden layer
What do we have here? A 9-dimensional input, an original hidden layer with 5 units, an extra hidden layer with two units, its corresponding two outputs (features) and a single unit output layer.
So, what happens with the inputs along the way? Let’s see:
Inputs (x1 through x9) are fed into the encoder part of the model.
The original hidden layer twists and turns the inputs. The outputs of the hidden layer can also be thought of as features (these would be the outputs of units h1 through h5 in the diagram), but these are assumed to be n-dimensional and therefore not suited for plotting. So far, business as usual.
Then comes the extra hidden layer. Its weights matrix has shape (n, 2) (in the diagram, n = 5 and we can count 10 arrows between h and e nodes). If we assume a linear activation function, this layer is actually performing an affine transformation, mapping points from a n-dimensional to a 2-dimensional feature space. These are our features, f1 and f2, the output of the encoder part.
Since we assumed a linear activation function for the extra hidden layer, f1 and f2 are going to be directly fed to the decoder(output layer), that is, to a single unit with a sigmoidactivation function. This is a plain and simple logistic regression.
What does it all mean? It means that our model is also learning a latent space with two latent factors (f1 and f2) now! Fancy, uh?! Don’t get intimidated by the fanciness of these terms, though… it basically means the model learned to best compress the information to only two features, given the task at hand — a binary classification.
This is the basic underlying principle of auto-encoders, the major difference being the fact that the auto-encoder’s task is to reconstruct its inputs, not classify them in any way.
I hope this post enticed you to try DeepReplay out 🙂
If you come up with nice and cool visualizations for different datasets, or using different network architectures or hyper-parameters, please share it on the comments section. I am considering starting a Gallery page, if there is enough interest in it.
For more information about the DeepReplay package, like installation, documentation, examples and notebooks (which you can play with using Google Colab), please go to my GitHub repository:
Deep Learning is all about hyper-parameters! Maybe this is an exaggeration, but having a sound understanding of the effects of different hyper-parameters on training a deep neural network is definitely going to make your life easier.
While studying Deep Learning, you’re likely to find lots of information on the importance of properly setting the network’s hyper-parameters: activation functions, weight initializer, optimizer, learning rate, mini-batch size, and the network architecture itself, like the number of hidden layers and the number of units in each layer.
So, you learn all the best practices, you set up your network, define the hyper-parameters (or just use its default values), start training and monitor the progress of your model’s losses and metrics.
Perhaps the experiment doesn’t go so well as you’d expect, so you iterate over it, tweaking the network, until you find out the set of values that will do the trick for your particular problem.
Looking for a deeper understanding (no pun intended!)
Have you ever wondered what exactly is going on under the hood? I did, and it turns out that some simple experiments may shed quite some light on this matter.
Take activation functions, for instance, the topic of this post. You and I know that the role of activation functions is to introduce a non-linearity, otherwise the whole neural network could be simply replaced by a corresponding affine transformation (that is, a linear transformation, such as rotating, scaling or skewing, followed by a translation), no matter how deep the network is.
A neural network having only linearactivations (that is, no activation!) would have a hard time handling even a quite simple classification problem like this (each line has 1,000 points, generated for x values equally spaced between -1.0 and 1.0):
If the only thing a network can do is to perform an affine transformation, this is likely what it would be able to come up with as a solution:
Clearly, this is not even close! Some examples of much better solutions are:
These are three fine examples of what non-linear activation functions bring to the table! Can you guess which one of the images corresponds to a ReLU?
Non-linear boundaries (or are they?)
How does these non-linear boundaries come to be? Well, the actual role of the non-linearity is to twist and turn the feature space so much so that the boundary turns out to be… LINEAR!
OK, things are getting more interesting now (at least, I thought so first time I laid my eyes on it in this awesome Chris Olah’s blog post, from which I drew my inspiration to write this). So, let’s investigate it further!
Next step is to build the simplest possible neural network to tackle this particular classification problem. There are two dimensions in our feature space (x1 and x2), and the network has a single hidden layer with two units, so we preserve the number of dimensions when it comes to the outputs of the hidden layer (z1 and z2).
Up to this point, we are still on the realm of affine transformations… so, it is time for a non-linear activation function, represented by the Greek letter sigma, resulting in the activation values (a1 and a2) for the hidden layer.
These activation values represented the twisted and turned feature space I referred to in the first paragraph of this section. This is a preview of what it looks like, when using a sigmoid as activation function:
As promised, the boundary is LINEAR! By the way, the plot above corresponds to the left-most solution with a non-linear boundary on the original feature space (Figure 3).
Neural network’s basic math recap
Just to make sure you and I are on the same page, I am showing you below four representations of the very basic matrix arithmetic performed by the neural network up to the hidden layer, BEFORE applying the activation function (that is, just an affine transformation such as xW + b)
Time to apply the activation function, represented by the Greek letter sigma on the network diagram.
Voilà! We went from the inputs to the activation values of the hidden layer!
Implementing the network in Keras
For the implementation of this simple network, I used Keras Sequential model API. Apart from distinct activation functions, every model trained used the very same hyper-parameters:
weight initializers: Glorot (Xavier) normal (hidden layer) and random normal (output layer);
optimizer: Stochastic Gradient Descent (SGD);
learning rate: 0.05;
mini-batch size: 16;
number of hidden layers: 1;
number of units (in the hidden layer): 2.
Given that this is a binary classification task, the output layer has a single unit with a sigmoidactivation function and the loss is given by binary cross-entropy.
Code: simple neural network with a single 2-unit hidden layer
Activation functions in action!
Now, for the juicy part — visualizing the twisted and turned feature space as the network trains, using a different activation function each time: sigmoid, tanh and ReLU.
In addition to showing changes in the feature space, the animations also contain:
histograms of predicted probabilities for both negative (blue line) and positive cases (green line), with misclassified cases shown in red bars (using threshold = 0.5);
line plots of accuracy and average loss;
histogram of losses for every element in the dataset.
Let’s start with the most traditional of the activation functions, the sigmoid, even though, nowadays, its usage is pretty much limited to the output layer in classification tasks.
As you can see in Figure 6, a sigmoidactivation function “squashes” the inputs values into the range (0, 1) (same range probabilities can take, the reason why it is used in the output layer for classification tasks). Also, remember that the activation values of any given layer are the inputs of the following layer and, given the range for the sigmoid, the activation values are going to be centered around 0.5, instead of zero (as it usually is the case for normalized inputs).
It is also possible to verify that its gradient peak value is 0.25 (for z = 0) and that it gets already close to zero as |z| reaches a value of 5.
So, how does using a sigmoidactivation function work for this simple network? Let’s take a look at the animation:
There are a couple of observations to be made:
epochs 15–40: it is noticeable the typical sigmoid “ squashing” happening on the horizontal axis;
epochs 40–65: the loss stays at a plateau, and there is a “widening” of the transformed feature space on the vertical axis;
epoch 65: at this point, negative cases (blue line) are all correctly classified, even though its associated probabilities still are distributed up to 0.5; while the positive cases on the edges are still misclassified;
epochs 65–100: the aforementioned “widening” becomes more and more intense, up to the point pretty much all feature space is covered again, while the loss falls steadily;
epoch 103: thanks to the “widening”, all positive cases are now lying within the proper boundary, although some still have probabilities barely above the 0.5 threshold;
epoch 100–150: there is now some “squashing” happening on the vertical axis as well, the loss falls a bit more to what seems to be a new plateau and, except for a few of the positive edge cases, the network is pretty confident on its predictions.
So, the sigmoid activation function succeeds in separating both lines, but the loss declines slowly, while staying at plateaus for a significant portion of the training time.
Can we do better with a different activation function?
The tanhactivation function was the evolution of the sigmoid, as it outputs values with a zero mean, differently from its predecessor.
As you can see in Figure 7, the tanhactivation function “squashes” the input values into the range (-1, 1). Therefore, being centered at zero, the activation values are already (somewhat) normalized inputs for the next layer.
Regarding the gradient, it has a much bigger peak value of 1.0 (again, for z = 0), but its decrease is even faster, approaching zero to values of |z| as low as 3. This is the underlying cause to what is referred to as the problem of vanishing gradients, which causes the training of the network to be progressively slower.
Now, for the corresponding animation, using tanh as activation function:
There are a couple of observations to be made:
epochs 10–40: there is a tanh “ squashing” happening on the horizontal axis, though it less pronounced, while the loss stays at a plateau;
epochs 40–55: there is still no improvement in the loss, but there is a “widening” of the transformed feature space on the vertical axis;
epoch 55: at this point, negative cases (blue line) are all correctly classified, even though its associated probabilities still are distributed up to 0.5; while the positive cases on the edges are still misclassified;
epochs 55–65: the aforementioned “widening” quickly reaches the point where pretty much all feature space is covered again, while the loss falls abruptly;
epoch 69: thanks to the “widening”, all positive cases are now lying within the proper boundary, although some still have probabilities barely above the 0.5 threshold;
epochs 65–90: there is now some “squashing” happening on the vertical axis as well, the loss keeps falling until reaching a new plateau and the network exhibits a high level of confidence for all predictions;
epochs 90–150: only small improvements in the predicted probabilities happen at this point.
OK, it seems a bit better… the tanhactivation function reached a correct classification for all cases faster, with the loss also declining faster (when declining, that is), but it also spends a lot of time in plateaus.
What if we get rid of all the “squashing”?
Rectified Linear Units, or ReLUs for short, are the commonplace choice of activation function these days. A ReLUaddresses the problem of vanishing gradients so common in its two predecessors, while also being the fastest to compute gradients for.
As you can see in Figure 8, the ReLU is a totally different beast: it does not “squash” the values into a range — it simply preserves positive values and turns all negative values into zero.
The upside of using a ReLU is that its gradient is either 1 (for positive values) or 0 (for negative values) — no more vanishing gradients! This pattern leads to a faster convergence of the network.
On the other hand, this behavior can lead to what it is called a “dead neuron”, that is, a neuron whose inputs are consistently negative and, therefore, always has an activation value of zero.
Time for the last of the animations, which is quite different from the previous two, thanks to the absence of “squashing” in the ReLUactivation function:
ReLU in action!
There are a couple of observations to be made:
epochs 0–10: the loss falls steadily from the very beginning
epoch 10: at this point, negative cases (blue line) are all correctly classified, even though its associated probabilities still are distributed up to 0.5; while the positive cases on the edges are still misclassified;
epochs 10–60: loss falls until reaching a plateau, all cases are already correctly classified since epoch 52, and the network already exhibits a high level of confidence for all predictions;
epochs 60–150: only small improvements in the predicted probabilities happen at this point.
Well, no wonder the ReLUs are the de facto standard for activation functions nowadays. The loss kept falling steadily from the beginning and only plateaued at a level close to zero, reaching correct classification for all cases in about 75% the time it took tanh to do it.
The animations are cool (ok, I am biased, I made them!), but not very handy to compare the overall effect of each and every different activation function on the feature space. So, to make it easier for you to compare them, there they are, side by side:
What about side-by-side accuracy and loss curves, so I can also compare the training speeds? Sure, here we go:
The example I used to illustrate this post is almost as simple as it could possibly be, and the patterns depicted in the animations are intended to give you just a general idea of the underlying mechanics of each one of the activation functions.
Besides, I got “lucky” with my initialization of the weights (maybe using 42 as seed is a good omen?!) and all three networks learned to classify correctly all the cases within 150 epochs of training. It turns out, training is VERYsensitive to the initialization, but this is a topic for a future post.
Nonetheless, I truly hope this post and its animations can give you some insights and maybe even some “a-ha!” moments while learning about this fascinating topic that is Deep Learning.
https://aideepdive.com/wp-content/uploads/2019/02/sigmoid_evolution_big-1030x206.jpg2061030Danielhttps://aideepdive.com/wp-content/uploads/2019/02/AIDD-LOGO-final.svgDaniel2018-12-02 00:11:352019-02-13 21:04:39Hyper-parameters in Action! Activation Functions