Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

stough 202-

Preview: Linear [MNIST](https://github.com/pytorch/examples/tree/master/mnist) with [torch](https://pytorch.org/)

Simple torch model with a single Linear (or fully connected) layer from MNIST images to digit classification. This is simplified from the example just to see if torch learns a pretty simple per pixel prior.

A fully-connected layer with N inputs and M outputs solves a simple linear equation: y=xAT+by = x A^T + b, with a total of NxM + M parameters to learn.

Additional resources:

Define the Network

The simplest possible network, simple linear.

The input is a flattened 28×2828 \times 28 image, a vector x\mathbf{x} of 784 pixel values. nn.Linear(784, 10) computes 10 numbers from it, s=Wx+b\mathbf{s} = W\mathbf{x} + \mathbf{b}, one score per digit class. Each score is a weighted sum of all the pixels, so each row of WW is a 784-pixel “template” that the image is compared against with a dot product. The highest score wins.

log_softmax turns the scores into log-probabilities: softmax(s)k=esk/∑jesj\text{softmax}(\mathbf{s})_k = e^{s_k} / \sum_j e^{s_j} is positive and sums to 1 over the classes, and we take its log for numerical convenience.

Training and Test functions.

train makes one pass (an epoch) through the training data, a batch of 64 images at a time. For each batch:

  1. optimizer.zero_grad(): clear the gradients left over from the previous batch.

  2. output = model(data): the forward pass, computing the log-probabilities for each image.

  3. loss = F.nll_loss(output, target): the negative log-likelihood, −log⁡p(correct class)-\log p(\text{correct class}), averaged over the batch. It’s near 0 when the model confidently picks the right digit and large when it’s confidently wrong. (log_softmax followed by nll_loss is the standard cross-entropy loss.)

  4. loss.backward(): torch’s automatic differentiation computes the gradient of the loss with respect to every weight and bias.

  5. optimizer.step(): nudge every parameter a little in the direction that lowers the loss.

test runs the model on the test set without computing gradients, and reports the average loss and the fraction of images whose highest-probability class is correct.

These two functions are boilerplate: every network in this chapter is trained and tested exactly the same way, whatever its architecture. So this is the only notebook where we’ll write them out. Later notebooks import them from torch_utils, the same way arr_info moved to matrix_utils after we wrote it in the numpy tutorial. The shared versions differ in two small ways: they take the loss function as an argument, and they return the losses and accuracy so the later notebooks can plot their training curves.

Namespace to replace the argparse business

This code is adapted from PyTorch’s MNIST example, a command-line script whose settings came from argparse. A Namespace holds the same settings, so the rest of the code can still say args.batch_size and so on.

Organize the MNIST data

The initial Dataset object returned implements __getitem__() and __len__(), and that’s it pretty much. Below I collect this data into numpy arrays and use a custom class that inherits from Dataset.

 

Make torch DataLoaders for our custom dataset.

After the below code executes, train_loader and test_loader are torch DataLoader objects that we iterate through to get batches of (N,784) data and (N,) target outputs.

 

Instantiate and train the torch model.

Expected output (what the cell above should show, if you can’t run it)
...
Train Epoch: 4 [0/60000 (0%)]	Loss: 0.476218
Train Epoch: 4 [16000/60000 (27%)]	Loss: 0.132038
Train Epoch: 4 [32000/60000 (53%)]	Loss: 0.410379
Train Epoch: 4 [48000/60000 (80%)]	Loss: 0.415302
Train set: Average loss: 0.2801
Test set: Average loss: 0.2780, Accuracy: 9238/10000 (92%)
Train Epoch: 5 [0/60000 (0%)]	Loss: 0.205844
Train Epoch: 5 [16000/60000 (27%)]	Loss: 0.208537
Train Epoch: 5 [32000/60000 (53%)]	Loss: 0.221882
Train Epoch: 5 [48000/60000 (80%)]	Loss: 0.276989
Train set: Average loss: 0.2762
Test set: Average loss: 0.2734, Accuracy: 9250/10000 (92%)

Adadelta is a variant of gradient descent that adapts its step size for each parameter, and StepLR multiplies the learning rate by gamma = 0.7 after each epoch, so the steps get smaller as training settles down.

Watch the test accuracy: this one linear layer, with 7,850 parameters, gets to around 92% correct after a few epochs. That’s much better than chance (10%), and it shows that pixel templates capture a lot. But it still misses about 1 in 12 digits, and it stops improving quickly. A single template per class can’t represent all the different ways people write a digit.

 

View the weights that were learned.

Recall that we learned a single fully connected layer, so something like 784x10 (plus 10 for bias). Let’s see what these per-pixel weightings look like.

Expected output (what the cell above should show, if you can’t run it)
expected output

Each panel is one row of WW, reshaped back into a 28×2828 \times 28 image: the template for digits 0 to 4 (top) and 5 to 9 (bottom). The colormap is centered so that white is a weight of zero. Red pixels are positive weights, where ink raises that digit’s score; blue pixels are negative weights, where ink lowers it.

These aren’t pictures of digits. Much of what the model learned is negative evidence. The 0 template’s strongest feature is the blue blob in the middle, since ink in the center means it’s not a zero, with only a faint red ring around it. The 1 template is a red vertical stroke flanked by blue, where a wider digit would put ink. The model didn’t learn what each digit looks like so much as what distinguishes it from the other nine.

 

Summary

  1. A linear classifier is template matching. Each class score is the dot product of the image with one learned 784-pixel template, plus a bias. That’s the same idea as the digit recognition through averages exercise, except that here training chooses the templates.

  2. Learned templates encode differences. Because training is rewarded only for picking the right class, the templates aren’t pictures of digits. As the weight maps show, much of what they hold is negative evidence: where ink rules a digit out.

  3. One template per class is the limit. The model reaches about 92% and stops improving. A single template can’t cover all the ways people write a digit, and each weight is tied to a fixed pixel position, so the same digit shifted by a couple of pixels looks like a different pattern.

  4. Next: local features. A convolutional network instead learns small filters that are applied at every position of the image, and combines their responses in later layers. It gets to about 99%.