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: Convolutional NN on [Yale Face Database B](http://vision.ucsd.edu/~iskwak/ExtYaleDatabase/ExtYaleB.html) with [torch](https://pytorch.org/)

Trying to recognize faces under variable lighting.

This is face recognition as a classification problem: given a cropped photo, which of the 38 people is it? It follows the same recipe as the MNIST convolutional network, with bigger images (192×168192 \times 168), more classes, far fewer examples per class (about 58 training photos per person), and the dramatic lighting changes we saw while exploring the data.

 

Namespace for parameters.

The same settings Namespace as in the linear model, where it’s explained.

 

Organize the Face data

We’ll use the torchvision ImageFolder to automatically make a dataset from the face data folder. Then we’ll randomly split the dataset into 90% training and 10% test using random_split. The split is random but seeded, with its own generator, so that it’s the same photos in each set on every run, and results can be compared from run to run.

One subtlety: random_split makes trainFaces and testFaces views of the same yaleData, so they share its transform, including the random ColorJitter. That means the test images are also randomly re-jittered every time they’re loaded, so the test accuracy will wobble a little from epoch to epoch for reasons that have nothing to do with the model. A cleaner setup would load the data twice, with jitter for training and without it for testing, and split both the same way.

 

Explore the data a bit.

trainFaces and testFaces are both subsets of the yaleData Dataset. They implement __len__ and __getitem__, so look at what some of the elements are.

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

 

Make the DataLoaders that we’ll use.

Define the Network

Use Convolutional Layers to transform the original image into a collection of features that linear layers can use to discriminate among the classes.

To keep all the layers’ dimensions in agreement, take a look at the prime factorizations of your image dimesions.

  • 168 = 2 x 2 x 2 x 3 x 7

  • 192 = 2 x 2 x 2 x 2 x 2 x 2 x 3

Tracing the shapes: a 3×33 \times 3 convolution with padding=2 actually grows each dimension by 2, and padding=1 keeps it the same; each max pool halves it (rounding down). So 192×168192 \times 168 becomes 194×170→97×85→99×87→49×43→49×43→24×21194 \times 170 \to 97 \times 85 \to 99 \times 87 \to 49 \times 43 \to 49 \times 43 \to 24 \times 21, and the 64 feature maps of 24×2124 \times 21 flatten to 64⋅24⋅21=3225664 \cdot 24 \cdot 21 = 32256 inputs for fc1.

The network returns raw class scores (logits), not probabilities. F.cross_entropy applies the softmax itself.

A cautionary tale. An earlier version of this network ended with output = torch.sigmoid(x), and you’ll find that line commented out at the end of forward. It looks harmless, and the network still trains and runs without any error. But cross-entropy applies a softmax to whatever it’s given, pk=esk/∑jesjp_k = e^{s_k} / \sum_j e^{s_j}, and a sigmoid has already squashed every score sks_k into (0,1)(0, 1). With 38 classes, the best the correct class can then do is p=e/(e+37)≈0.07p = e / (e + 37) \approx 0.07, so the loss can never fall below −ln⁡0.07≈2.7-\ln 0.07 \approx 2.7, and the gradients that should push the right answer up stay weak. One extra line quietly ruins the outcome. For how softmax and cross-entropy work together, see the softmax classifier notes from Stanford’s CS231n.

Training and Test functions.

train and test are the same loops we wrote out and walked through step by step in the linear model. Every network in this chapter trains and tests the same way, so rather than repeat them in each notebook, they now live in torch_utils and are imported at the top, just as arr_info moved to matrix_utils after the numpy tutorial. Two small differences from the inline version: they take the loss function as an argument (loss_fn), and they return the losses and accuracy so that we can plot the training curves. The file is short; open it and read it once.

This network returns raw scores, so its loss is F.cross_entropy, which applies the softmax for us.

 

Instantiate the model and count parameters

Expected output (what the cell above should show, if you can’t run it)
...
Train set: Average loss: 0.0217 (1.603 sec)
Test set: Average loss: 0.1040, Accuracy: 241/245 (98%)
Train Epoch: 19 [0/2207 (0%)]	Loss: 0.035926
Train Epoch: 19 [960/2207 (43%)]	Loss: 0.000376
Train Epoch: 19 [1920/2207 (87%)]	Loss: 0.000286
Train set: Average loss: 0.0216 (1.598 sec)
Test set: Average loss: 0.0869, Accuracy: 242/245 (99%)
Train Epoch: 20 [0/2207 (0%)]	Loss: 0.000431
Train Epoch: 20 [960/2207 (43%)]	Loss: 0.002463
Train Epoch: 20 [1920/2207 (87%)]	Loss: 0.001132
Train set: Average loss: 0.0217 (1.604 sec)
Test set: Average loss: 0.0871, Accuracy: 242/245 (99%)
Expected output (what the cell above should show, if you can’t run it)
expected output

Losses (left axis) and test accuracy (right axis, dashed) by epoch. With only about 2,200 training photos, watch for the two loss curves separating: training loss that keeps falling while test loss levels off or rises is the signature of overfitting, memorizing the training photos rather than learning what distinguishes faces. That’s what happens here: within a few epochs the training loss falls to nearly zero, while the test loss levels off (around 0.1 to 0.15) and test accuracy plateaus near 98%. (Small differences remain from run to run, from the random ColorJitter and the GPU’s arithmetic.) The dropout layers defined in Net are commented out in forward; try enabling them and compare.

 

Let’s get the output of the model for all the test data.

The test_loader isn’t shuffled, but we’ll still keep track of the targets alongside the outputs, to know what the answer should have been. In fact, let’s just keep all of it.

In the below cell we loop over all the test data, push it through the model and store the resulting classifier outputs. The expression model(data.to(device)) sends the data to the GPU (where the model resides) and applies the model to it. The sequence that happens after, .cpu().detach().numpy(), takes the resulting outputs and brings it back to the cpu memory space in the form of a numpy array.

A notes on pretty-printing arrays.

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

Look at which test faces were misclassified. Nearly all of them are photos with the most extreme lighting: half in shadow, or nearly black. In those images little of the face is visible at all. Considering how differently the same person can look across these photos, it’s remarkable how well a small network does with fewer than 60 examples per person.