Adding a Nonlinearity

So far we have a general procedure for optimizing the parameters of a function, and we have tried it out on a very boring function: a simple linear classifier. A linear classifier is very constrained in terms of what it can do. To make it a bit more complex (and able to handle more tasks), we need to add something nonlinear between two linear classifiers—this is what gives us a neural network.

Here is the entire definition of a basic neural network:

In [ ]:

  1. def simple_net(xb):
  2. res = xb@w1 + b1
  3. res = res.max(tensor(0.0))
  4. res = res@w2 + b2
  5. return res

That’s it! All we have in simple_net is two linear classifiers with a max function between them.

Here, w1 and w2 are weight tensors, and b1 and b2 are bias tensors; that is, parameters that are initially randomly initialized, just like we did in the previous section:

In [ ]:

  1. w1 = init_params((28*28,30))
  2. b1 = init_params(30)
  3. w2 = init_params((30,1))
  4. b2 = init_params(1)

The key point about this is that w1 has 30 output activations (which means that w2 must have 30 input activations, so they match). That means that the first layer can construct 30 different features, each representing some different mix of pixels. You can change that 30 to anything you like, to make the model more or less complex.

That little function res.max(tensor(0.0)) is called a rectified linear unit, also known as ReLU. We think we can all agree that rectified linear unit sounds pretty fancy and complicated… But actually, there’s nothing more to it than res.max(tensor(0.0))—in other words, replace every negative number with a zero. This tiny function is also available in PyTorch as F.relu:

In [ ]:

  1. plot_function(F.relu)

Adding a Nonlinearity - 图1

J: There is an enormous amount of jargon in deep learning, including terms like rectified linear unit. The vast vast majority of this jargon is no more complicated than can be implemented in a short line of code, as we saw in this example. The reality is that for academics to get their papers published they need to make them sound as impressive and sophisticated as possible. One of the ways that they do that is to introduce jargon. Unfortunately, this has the result that the field ends up becoming far more intimidating and difficult to get into than it should be. You do have to learn the jargon, because otherwise papers and tutorials are not going to mean much to you. But that doesn’t mean you have to find the jargon intimidating. Just remember, when you come across a word or phrase that you haven’t seen before, it will almost certainly turn out to be referring to a very simple concept.

The basic idea is that by using more linear layers, we can have our model do more computation, and therefore model more complex functions. But there’s no point just putting one linear layer directly after another one, because when we multiply things together and then add them up multiple times, that could be replaced by multiplying different things together and adding them up just once! That is to say, a series of any number of linear layers in a row can be replaced with a single linear layer with a different set of parameters.

But if we put a nonlinear function between them, such as max, then this is no longer true. Now each linear layer is actually somewhat decoupled from the other ones, and can do its own useful work. The max function is particularly interesting, because it operates as a simple if statement.

S: Mathematically, we say the composition of two linear functions is another linear function. So, we can stack as many linear classifiers as we want on top of each other, and without nonlinear functions between them, it will just be the same as one linear classifier.

Amazingly enough, it can be mathematically proven that this little function can solve any computable problem to an arbitrarily high level of accuracy, if you can find the right parameters for w1 and w2 and if you make these matrices big enough. For any arbitrarily wiggly function, we can approximate it as a bunch of lines joined together; to make it closer to the wiggly function, we just have to use shorter lines. This is known as the universal approximation theorem. The three lines of code that we have here are known as layers. The first and third are known as linear layers, and the second line of code is known variously as a nonlinearity, or activation function.

Just like in the previous section, we can replace this code with something a bit simpler, by taking advantage of PyTorch:

In [ ]:

  1. simple_net = nn.Sequential(
  2. nn.Linear(28*28,30),
  3. nn.ReLU(),
  4. nn.Linear(30,1)
  5. )

nn.Sequential creates a module that will call each of the listed layers or functions in turn.

nn.ReLU is a PyTorch module that does exactly the same thing as the F.relu function. Most functions that can appear in a model also have identical forms that are modules. Generally, it’s just a case of replacing F with nn and changing the capitalization. When using nn.Sequential, PyTorch requires us to use the module version. Since modules are classes, we have to instantiate them, which is why you see nn.ReLU() in this example.

Because nn.Sequential is a module, we can get its parameters, which will return a list of all the parameters of all the modules it contains. Let’s try it out! As this is a deeper model, we’ll use a lower learning rate and a few more epochs.

In [ ]:

  1. learn = Learner(dls, simple_net, opt_func=SGD,
  2. loss_func=mnist_loss, metrics=batch_accuracy)

In [ ]:

  1. #hide_output
  2. learn.fit(40, 0.1)
epochtrain_lossvalid_lossbatch_accuracytime
00.3058280.3996630.50834100:00
10.1429600.2257020.80765500:00
20.0795160.1135190.91952900:00
30.0523910.0767920.94308100:00
40.0397960.0600830.95633000:00
50.0333680.0507130.96369000:00
60.0296800.0447970.96565300:00
70.0272900.0407290.96810600:00
80.0255680.0377710.96859700:00
90.0242330.0355080.97055900:00
100.0231490.0337140.97203100:00
110.0222420.0322430.97252200:00
120.0214680.0310060.97350300:00
130.0207960.0299440.97448500:00
140.0202070.0290160.97546600:00
150.0196830.0281960.97644800:00
160.0192150.0274630.97644800:00
170.0187910.0268060.97693800:00
180.0184050.0262120.97792000:00
190.0180510.0256710.97792000:00
200.0177250.0251790.97792000:00
210.0174220.0247280.97841000:00
220.0171410.0243130.97890100:00
230.0168780.0239320.97939200:00
240.0166320.0235800.97988200:00
250.0164000.0232540.97988200:00
260.0161810.0229520.97988200:00
270.0159750.0226720.98086400:00
280.0157790.0224110.98086400:00
290.0155930.0221680.98184500:00
300.0154170.0219410.98184500:00
310.0152490.0217280.98184500:00
320.0150880.0215290.98184500:00
330.0149350.0213410.98184500:00
340.0147880.0211640.98184500:00
350.0146470.0209980.98233600:00
360.0145120.0208400.98282600:00
370.0143820.0206910.98282600:00
380.0142570.0205500.98282600:00
390.0141360.0204150.98282600:00

We’re not showing the 40 lines of output here to save room; the training process is recorded in learn.recorder, with the table of output stored in the values attribute, so we can plot the accuracy over training as:

In [ ]:

  1. plt.plot(L(learn.recorder.values).itemgot(2));

Adding a Nonlinearity - 图2

And we can view the final accuracy:

In [ ]:

  1. learn.recorder.values[-1][2]

Out[ ]:

  1. 0.982826292514801

At this point we have something that is rather magical:

  1. A function that can solve any problem to any level of accuracy (the neural network) given the correct set of parameters
  2. A way to find the best set of parameters for any function (stochastic gradient descent)

This is why deep learning can do things which seem rather magical, such fantastic things. Believing that this combination of simple techniques can really solve any problem is one of the biggest steps that we find many students have to take. It seems too good to be true—surely things should be more difficult and complicated than this? Our recommendation: try it out! We just tried it on the MNIST dataset and you have seen the results. And since we are doing everything from scratch ourselves (except for calculating the gradients) you know that there is no special magic hiding behind the scenes.

Going Deeper

There is no need to stop at just two linear layers. We can add as many as we want, as long as we add a nonlinearity between each pair of linear layers. As you will learn, however, the deeper the model gets, the harder it is to optimize the parameters in practice. Later in this book you will learn about some simple but brilliantly effective techniques for training deeper models.

We already know that a single nonlinearity with two linear layers is enough to approximate any function. So why would we use deeper models? The reason is performance. With a deeper model (that is, one with more layers) we do not need to use as many parameters; it turns out that we can use smaller matrices with more layers, and get better results than we would get with larger matrices, and few layers.

That means that we can train the model more quickly, and it will take up less memory. In the 1990s researchers were so focused on the universal approximation theorem that very few were experimenting with more than one nonlinearity. This theoretical but not practical foundation held back the field for years. Some researchers, however, did experiment with deep models, and eventually were able to show that these models could perform much better in practice. Eventually, theoretical results were developed which showed why this happens. Today, it is extremely unusual to find anybody using a neural network with just one nonlinearity.

Here what happens when we train an 18-layer model using the same approach we saw in <>:

In [ ]:

  1. dls = ImageDataLoaders.from_folder(path)
  2. learn = cnn_learner(dls, resnet18, pretrained=False,
  3. loss_func=F.cross_entropy, metrics=accuracy)
  4. learn.fit_one_cycle(1, 0.1)
epochtrain_lossvalid_lossaccuracytime
00.0820890.0095780.99705600:11

Nearly 100% accuracy! That’s a big difference compared to our simple neural net. But as you’ll learn in the remainder of this book, there are just a few little tricks you need to use to get such great results from scratch yourself. You already know the key foundational pieces. (Of course, even once you know all the tricks, you’ll nearly always want to work with the pre-built classes provided by PyTorch and fastai, because they save you having to think about all the little details yourself.)