Introduction to Machine Learning · Neural networks lab

Two Pixels Over

Can you build a network that still reads a digit after it moves?

A hackathon in three tracks, then six problems on everything since the first neuron, all scored on one live board.

Today, in six questions

A 2, moved up two pixelsOne of the 600 held-out digits, read by the 32-unit network from the neural networks lecture

As written
the network reads 2
with more than 99% confidence
Moved up two pixels
the network reads 6
with more than 99% confidence

The same ink, the same shape, and the network is just as sure of the wrong answer. This is not one unlucky digit: on the 600 held-out digits, the network reads 91% as written and 24% once each is moved two pixels up, down, left or right. That is the failure the lecture ended on. Today you fix it.

The rules

  1. Three sets of the 3,000 MNIST digits at 16 by 16 pixels, the same for everyone: 1,900 to train on, 500 to validate (to choose between settings), and 600 to test, kept hidden until the end.
  2. As written and moved. The validation and test digits come twice: as written, and each moved two pixels up, down, left or right, in a direction drawn once, in advance.
  3. Accuracy is the share of digits read correctly. The board ranks on the lower of the two validation accuracies: a network that reads moved digits by forgetting how to read unmoved ones has not solved anything.
  4. Never train on a validation or test digit, moved or not. The test set is opened once, at the end: if your validation accuracy was luck, that is where it shows.
How it runsWork in twos or threes. Everyone runs the baseline in track 1 first (5 minutes); then each team picks a track. Every run prints its accuracy on the training digits and on both validation sets.
To beatThe lecture network: validation accuracy 0.888 as written, 0.262 moved, so 0.262 on the board, with 8,554 weights.
The boardOne of you creates your team and gets a four-letter code; the others join with it. Every run of a challenge cell then posts itself under your team, and its best run counts.

The board: shifted digitsTracks 1 and 2: every team's best validation accuracy, and the settings that produced it

Loading the board…

The board: Beirut, one day aheadTrack 3: every team's best forecast error on 2023 to 2025, in degrees

Loading the board…

Part 2 of 6

Three tracks

Where do you start, and what can you change?

Pick a trackEach one changes a different thing: the network, the data it sees, or the question

Track 1 · everyoneIn the browser

scikit-learn's network, run on the next two slides. Change the width, the depth, the penalty on large weights, and whether the network also sees shifted copies of its training digits.

No installation. Each run takes seconds to half a minute.

Track 2 · a step upA convolutional network

Replace the fully connected layer with convolutions and pooling, then train it on shifted batches. Count what each change costs in weights.

In numpy on the slides, or PyTorch in Colab. Seconds on a CPU.

Track 3 · a different questionBeirut's weather

A recurrent network forecasts tomorrow's maximum temperature in Beirut from the last 14 days. Can it beat "tomorrow is like today", and a straight line?

For teams that finish track 2, or want time series.

Track 1: the baselinePress Run: the lecture's network, trained here, scored on both tests


from sklearn.neural_network import MLPClassifier

net = MLPClassifier(hidden_layer_sizes=(32,), max_iter=500, random_state=0)
net.fit(X_train, y_train)                 # the 1,900 training digits, 256 pixels each
score(net, 'the lecture network')         # both tests, and the lower of the two
    

You should see accuracy 1.000 on the training digits, and 0.888 as written, 0.262 moved on the validation digits: it has learned its 1,900 digits, not to read moved ones. A hidden first cell loads the digits and builds the three sets (X_train, X_val, X_val_shift, and the hidden test digits), shift(X, dx, dy) and score(net).

Track 1: change one thing at a timeEdit the settings and run; every run goes on the board with its settings


from sklearn.neural_network import MLPClassifier

HIDDEN = (32,)      # hidden layer widths: try (128,), or two layers (64, 32)
ALPHA = 0.0001      # the penalty on large weights (weight decay): try 0.001 or 0.01
SHIFTS = []         # moves (dx, dy) to copy every training digit with, e.g.
                    # [(2, 0), (-2, 0), (0, 2), (0, -2), (1, 0), (-1, 0), (0, 1), (0, -1)]
UPDATES = 5000      # the most weight updates training may make, each on a batch of 200 digits

X_more = np.vstack([X_train] + [shift(X_train, dx, dy) for dx, dy in SHIFTS])
y_more = np.tile(y_train, len(SHIFTS) + 1)
passes = max(1, UPDATES * 200 // len(y_more))   # passes over the data that make that many updates:
                                                # 526 over the 1,900 digits, 58 over them and 8 copies
                                                # (it stops sooner once the loss stops improving)
net = MLPClassifier(hidden_layer_sizes=HIDDEN, alpha=ALPHA, max_iter=passes, random_state=0)
net.fit(X_more, y_more)
score(net, f'{HIDDEN}, alpha {ALPHA}, {len(SHIFTS)} shifted copies')
    

Which helps the moved accuracy more: a wider network, or the shifted copies? The copies are moved training digits, never validation or test digits. With copies a run takes up to a minute here.

Track 2: a convolutional networkPyTorch, in a notebook you run in Colab or Jupyter


def make_cnn():
    return nn.Sequential(
        nn.Conv2d(1, 8, kernel_size=3, padding=1),   # 8 maps, 16 by 16
        nn.ReLU(), nn.MaxPool2d(2),                  # 8 by 8
        nn.Conv2d(8, 16, kernel_size=3, padding=1),  # 16 maps
        nn.ReLU(), nn.MaxPool2d(2),                  # 4 by 4
        nn.Flatten(), nn.Linear(16 * 4 * 4, 10))

cnn = train(make_cnn())                  # 30 epochs, Adam
score(cnn, 'CNN')
cnn_aug = train(make_cnn(), augment=True)  # every batch moved
score(cnn_aug, 'CNN on shifted batches')
        

The notebook builds the same split, moved digits and score as the slides, trains the lecture network in PyTorch, then this network, then lets you change it.

Colab needs a free Google account; the notebook fetches the digits from this site, and a CPU is enough: about ten seconds per network. The next slide runs a numpy version right here.

Before you believe a difference, run a second seed.

Track 2, here: the same network in numpyEvery layer written out, trained in this page in seconds; press Run, then change the four settings


FILTERS = 8         # kernels in the first convolution: try 4 or 16
EPOCHS = 4          # passes over the 1,900 training digits
LR = 0.01           # Adam's step size
AUGMENT = False     # True: every batch is moved by up to 2 pixels before the network sees it

net = [Conv(1, FILTERS, 3), ReLU(), MaxPool(),      # FILTERS maps of 16 by 16, pooled to 8 by 8
       Conv(FILTERS, 16, 3), ReLU(), MaxPool(),     # 16 maps, pooled to 4 by 4
       Flatten(), Dense(16 * 4 * 4, 10)]            # one score per digit
# one convolution only: [Conv(1, FILTERS, 3), ReLU(), MaxPool(), Flatten(), Dense(FILTERS * 8 * 8, 10)]

history = train(net, EPOCHS, LR, augment=AUGMENT)
score_cnn(net, f'CNN, {FILTERS} kernels, {EPOCHS} epochs' + (', moved batches' if AUGMENT else ''))
show(net, history)
    
Run the cell: the accuracies, the training loss, the validation error, the kernels and the feature maps appear here.

Press Run: about 5 seconds. You should see validation accuracy 0.944 as written and 0.604 moved, with 3,818 weights, against 0.262 moved for the lecture network; with AUGMENT = True, 0.922 and 0.886. The layers are written in numpy in a hidden first cell, the same convolution, pooling and backward pass as the slides.

Track 3: Beirut, one day aheadDaily maximum temperature 2015 to 2025, trained on 2015 to 2022, tested on 2023 to 2025

A different question, so a different score: the error of the forecast in degrees, on 1,096 test days the model never saw. Lower is better.

The baselineSay tomorrow will be like today: 2.21 °C. A linear model on the last 14 days does better. Can a recurrent network, reading the days in order, do better still?

Or the next slide: a recurrent network in numpy, in this page.

Then add the night minimum, the rain, the season. Does the network still win when both read the same inputs?


class Forecaster(nn.Module):
    def __init__(self, n_in, hidden=16, kind='LSTM'):
        super().__init__()
        self.rnn = getattr(nn, kind)(n_in, hidden,
                                     batch_first=True)
        self.out = nn.Linear(hidden, 1)
    def forward(self, x):          # x: (days, 14, inputs)
        h, _ = self.rnn(x)         # the state after each day
        return self.out(h[:, -1]).squeeze(-1)
            

Data: ERA5 reanalysis for Beirut via the Open-Meteo archive API, CC-BY 4.0.

Track 3, here: a recurrent network in numpyA tanh cell read over 14 days, trained by backpropagation through time in this page


INPUTS = ['tmax']    # what the network reads each day: try ['tmax', 'tmin', 'rain', 'season']
HIDDEN = 16          # numbers in the hidden state
EPOCHS = 30          # passes over the 2,908 training days
LR = 0.003           # Adam's step size: 0.01 trains faster and noisier
SEED = 0             # the starting weights and the order of the days: try 1

win, Xn = make_windows(INPUTS)                 # every 14-day window, standardized
model = RNN(Xn.shape[2], HIDDEN, seed=SEED)
history = fit(model, Xn, EPOCHS, LR, seed=SEED)
report(model, Xn, win, history, f'RNN {HIDDEN}, {"+".join(INPUTS)}, {EPOCHS} epochs')
    
Run the cell: the three test errors, the training curve and a forecast of early 2024 appear here.

Press Run: about 2 seconds. It trains on 2015 to 2021 and reports the error on the validation year, 2022: 2.25 °C for tomorrow = today, 2.13 for the linear model and 2.14 for the recurrent network; with all four inputs, 1.95 for both. SEED = 1 gives 2.12: a difference of 0.02 is noise. 2023 to 2025 is the test, opened at the end.

Part 3 of 6

When it will not train

What do you check when the numbers look wrong?

Symptoms you may meet todayEach one happened on these digits, with the network of the lecture or the notebook's CNN

What you seeThe usual cause
The loss sits near 2.30 and accuracy near 10%: one digit for every imageThe learning rate is too large (3.0 with plain SGD does it), or inputs and labels are out of step
Accuracy 39% where the same network gets 91%opt.zero_grad() is missing, so gradients pile up across batches
Expected 3D (unbatched) or 4D (batched) input to conv2d, but got input of size: [32, 256]A convolution wants images: xb.view(-1, 1, 16, 16), batch, channels, height, width
100% on the training digits, far less on held-out onesOverfitting: fewer epochs, a penalty on weights, or more (or shifted) data
A test accuracy of 1.000 on the moved digitsThe test digits reached training. Part 4 shows how easily

A checklist for when the loss will not go downFrom the backpropagation session, in the order to try it

  1. Check the loss before training. With ten classes it should be near \(\ln 10 \approx 2.30\). Much larger means the starting weights are too big.
  2. Print the shapes after every layer for one batch. Most errors in a new network are shape errors.
  3. Overfit ten examples. A healthy network reaches zero training loss on ten digits in a few hundred steps. If it cannot, the bug is in the code, not the data.
  4. Sweep the learning rate by factors of ten before touching anything else.
  5. Change one thing at a time, and run two seeds before believing a difference.
  6. Plot training and held-out loss together. Both high: underfitting. Training falling while held-out climbs: overfitting.

Part 4 of 6

An assistant in the lab

How do you use one without fooling yourself?

An assistant is a fast colleague who is sometimes confidently wrongUse one today, the way you would use that colleague

Ask it to

Explain an error message, line by line.

Propose one change, and say why it should help the shifted score.

Write the code for one idea you already have: a third convolution, a shift per digit.

Explain every line it gave you.

Check it by

Running it, every time.

Printing shapes and a few predictions.

Comparing with the numbers you know: 0.888 and 0.262 on validation for the baseline.

Asking it, and yourself, how the number could be wrong.

Never

Paste code you have not run and read.

Let it near the held-out digits.

Report a number you cannot reproduce, or explain.

The assistant's perfect scoreAsked to make the network robust to shifts, an assistant suggested this. Run it.


from sklearn.neural_network import MLPClassifier

# The sets, as the hidden first cell builds them for everyone:
test = np.arange(len(y)) % 5 == 0          # every fifth digit: the 600 test digits
X_test, y_test = X[test], y[test]
X_shift = moved(X_test, 0)                 # the same 600 test digits, each moved two pixels
# (and from the other 2,400: X_train, 1,900 to train on; X_val and X_val_shift, 500 to choose with)

# The assistant: "Train on shifted examples so the network learns that position does not matter:"
X_more = np.vstack([X_train, X_shift])
y_more = np.concatenate([y_train, y_test])
net = MLPClassifier(hidden_layer_sizes=(32,), max_iter=500, random_state=0)
net.fit(X_more, y_more)
score(net, "the assistant's network", show_test=True)   # this once, the test accuracy too
    

Validation says 0.898 and 0.650: better on moved digits than the baseline's 0.262, nothing strange. Test says 1.000 on the moved digits. The advice sounds right, and the code runs. Read where X_shift comes from: which line is the problem, what does 1.000 measure, and why did validation not see it?

Part 5 of 6

What worked

Which change bought the most?

Our runs, against yoursTest accuracy of our PyTorch runs, trained on all 2,400: the lower bar of each row, the moved digits, is the one that counts

test digits as writtenmoved two pixels

Width alone bought nothing. Shifted copies did most of the work, for every architecture: they show the network what to ignore. The convolution did the rest, with a ninth of the weights of the wide network. And the moved 2 from the start? The lecture network reads a 6; with shifted copies, or as a CNN, a 2.

Beirut: what a recurrent network boughtOur runs: test error of tomorrow's maximum on the 1,096 days of 2023 to 2025, in °C, lower is better

The LSTM beat the straight line by a tenth of a degree on maxima alone, and a straight line that reads the night minimum, the rain and the season does as well as it. Most of what the past two weeks can say about tomorrow, a linear model already hears. The rest of the error is weather that is not in the file: a front arriving from the sea, a dust storm from the east.

What produced each result: shifted digitsBefore you crown a team, read its settings: more epochs, more data, or a better idea?

Loading the runs…

A team that won by training longer has not found a better network: run the others for as long before you compare. Then reveal the test set, and see who kept their place.

What produced each result: BeirutThe same question for the forecasts: which settings bought the lower error?

Loading the runs…

Part 6 of 6

Six problems

Can you do by hand what the network does?

Six problems, ten points eachCounting weights, a backward pass, two training curves, a convolution and a recurrent cell, by hand

  1. Answer each part in its box and press Check. Numbers can be typed as 0.25, 25%, 1/4 or 1,184.
  2. Wrong is not final. Try again: each wrong try takes a point off that part (on a multiple-choice part, the wrong letters together cost all of it).
  3. Stuck? A hint costs a point. A question to the tutor costs a point; it explains, and it will not give an answer.
  4. Every part right and the worked solution appears under the problem.
TeamsYour lab team, on one laptop or several: each member joins it with the code shown on the creator's screen. The board counts the best member of each team on each problem.
The boardIn class, the points of every team appeared on a board after the last problem.

Problems 3 and 4 read Figures A and B; every figure sits on its problem's slide.

Problem 1: what a convolutional network costsThe track 2 network, on a 16 by 16 digit

16×16×1 16×16×8 8×8×8 8×8×16 4×4×16 conv 3x3 8 maps pool 2x2 conv 3x3 16 maps pool 2x2 flatten: 256 numbers linear 10 scores Each convolution: padding 1, then a ReLU

How PyTorch counts. Conv2d(c_in, c_out, k, padding=p) has c_out filters, each a c_in by k by k block of weights, plus one bias per filter; padding=p adds p rows and columns of zeros on every side. Linear(n_in, n_out) has n_out by n_in weights plus one bias per output. Count weights and biases: by this rule the lecture network, 256 to 32 to 10, has 8,554.

(a) Weights and biases in the first convolution, Conv2d(1, 8, 3, padding=1)
(b) Weights and biases in the second convolution, Conv2d(8, 16, 3, padding=1)
(c) Weights and biases in the last layer, Linear(16*4*4, 10)
(d) Weights and biases in the whole network: the sum of (a), (b) and (c)
(e) With padding=0 in the first convolution instead: how many rows does each map have after it and the first pool? (The maps are square.)

Problem 2: one backward pass, by handOne input, one hidden ReLU unit, one output, a squared error

x = 2 × w₁ ReLU × w₂ (ŷ − y)² z h ŷ L y = 1 w₁ = 0.5 w₂ = −1 forward: left to right backward: the chain rule, right to left, from dL/dL = 1

\(z = w_1 x\), \(h = \mathrm{ReLU}(z)\), \(\hat y = w_2 h\), \(L = (\hat y - y)^2\), on the example \(x = 2\), \(y = 1\).

(a) The loss \(L\)
(b) \(\partial L / \partial w_2\)
(c) \(\partial L / \partial w_1\)
(d) \(w_1\) after one step of gradient descent with learning rate 0.1 (both weights step)
(e) \(\partial L / \partial w_1\) on the same example, after that step

Problem 3: reading a training curve64 hidden units trained on only 100 digits: the loss on those 100, and on 600 held-out digits, after each epoch

Figure A. The network reads all 100 of its training digits by the end.

(a) The epoch at which you would stop training
(b) The held-out loss at epoch 60, to two decimals
(c) What is happening?
(d) Which change will not lower the held-out loss at the end?

Problem 4: three learning ratesPlain SGD on the 2,400 training digits: same network, same starting weights, learning rates 0.01, 0.1 and 3.0 in some order

Figure B. The thin grey line, marked uniform guess, is the loss of a network that cannot tell the digits apart: it gives all ten the same probability.

(a) Which run has learning rate 3.0?
(b) Which run has learning rate 0.01?
(c) The value of the grey line, to two decimals. (The cross-entropy loss on one image is \(-\ln\) of the probability the network gives its correct digit.)
(d) The accuracy, as a fraction, of a network that answers the same digit for every image, when the ten digits are equally common

Problem 5: a convolution, a ReLU and a pool, by handA 6 by 6 digit, a 3 by 3 kernel, no bias, no padding, stride 1, as PyTorch computes it (no flip)

the image, 6 x 6 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 1 1 1 0 yellow: the window for output row 1, column 1 the kernel K −1 0 1 −1 0 1 −1 0 1 out = sum of K times the window then slide one pixel, and repeat

Rows and columns of the output count from 1, top left.

(a) The output map is square: how many rows does it have? (How many positions can the 3 by 3 window take down the image?)
(b) The output at row 2, column 2
(c) The output at row 1, column 3
(d) After a ReLU and a 2 by 2 max pool: the top-left value of the pooled map
(e) Move the image one pixel right (zeros enter on the left) and do it again: the top-left pooled value now

Problem 6: a recurrent network's weights and memoryTrack 3: Beirut's maximum temperature tomorrow, from the last 14 days

cell x₁ day 1 W cell x₂ day 2 W cell x₃ day 3 W cell x₁₄ day 14 W U U h, 16 numbers linear tomorrow One cell, the same W and U every day; the state h carries the past An LSTM cell holds four such blocks, one per gate, and a second state c

How PyTorch counts. nn.RNN(n_in, H) reads n_in numbers each day into a hidden state of H numbers, and computes \(h_t = \tanh(W x_t + b + U h_{t-1} + b')\): \(W\) is \(H \times n_{in}\), \(U\) is \(H \times H\), and there are two bias vectors \(b, b'\) of \(H\) each. nn.LSTM(n_in, H) has four such blocks, one per gate (input, forget, cell, output), each with its own \(W\), \(U\), \(b\) and \(b'\). Count weights and biases.

(a) Weights and biases in nn.RNN(1, 16): a recurrent cell that reads 1 number per day (that day's maximum) into a hidden state of 16 numbers
(b) Weights and biases in nn.LSTM(1, 16): an LSTM cell with the same input and hidden state sizes
(c) Weights and biases in the readout, Linear(16, 1), which turns the last hidden state into tomorrow's forecast
(d) Along the LSTM cell, the gradient 30 days back is multiplied by the forget gates. If every one is 0.9: by what factor? (two significant figures)
(e) Test errors: tomorrow = today 2.21 °C, the LSTM 1.96 °C. The LSTM is lower by what fraction of 2.21? (two decimals)

The board: the six problemsPoints per problem for every team, as they are earned

Loading the board…

What to take away

  1. A fully connected network gives every pixel its own weight, so a two-pixel move turns 91% into 24%, with the same confidence.
  2. An assumption the network needs can be built into the architecture (a convolution shares its kernel across positions) or shown in the data (shifted copies). On these digits, showing it did most of the work, and the two together did best with the fewest weights.
  3. On Beirut's temperatures, the recurrent network bought a tenth of a degree over a straight line. A better model of the wrong inputs has a floor.
  4. Debug in order: the starting loss, the shapes, ten examples, the learning rate, one change at a time.
  5. A number too good to be true is a bug until shown otherwise; an assistant makes such bugs as easily as it makes working code.