Implementing an MLP from scratch

Implementing an MLP from scratch

30 min read

Backpropagation hands over the gradient of a whole network in a single backward pass, and the previous lesson derived it without skipping a single step. What it produced, though, was a list of numbers on a screen. A printed gradient moves no weight on its own: it's an instruction (which way is down, and how much of it falls to each parameter), and somebody still has to carry that out, a thousand times over, before a network goes from answering anything at all to answering well. That loop hasn't been written in any cell of this course yet. Its four pieces have, spread across four lessons of this block, and putting them together also settles a debt open since the very beginning: the eight weights of the review neuron and the seven that separate the XOR (exclusive or) phrases were written by me, by looking at the data alone.

The place to check all this has been chosen since then. The lesson on XOR and hidden layers proved that no single line leaves it's good and not bad on one side and the other two phrases on the other, then solved all four with a hidden layer whose seven numbers I picked by hand. If the loop actually works, the network has to find weights that get all four right (not necessarily mine) starting from numbers that mean nothing at all. And if it doesn't work, you'll notice immediately: with four examples and two coordinates, there's nowhere left for a fault to hide.

The four pieces aren't loose ends. They fall into an order, and that order closes back on itself.

Four boxes placed at the corners of a rectangle and joined by arrows that turn clockwise. Top left, forward pass; top right, loss; bottom right, gradient; bottom left, weight update. The first three arrows are grey; the fourth, the one that goes up from the update to the forward pass and closes the circle, is green and has the words and again written beside it.
Training adds no new operation, it adds a loop. Each box was derived in its own lesson and none of them changes here; the only thing this lesson writes for the first time is the arrow that hands control back to the start.

From the example to the batch: the backward pass, stacked by rows

The previous lesson derived the backward pass for one example, in column vectors. The forward pass has been written for a whole batch since the lesson on the forward pass: one example per row in X∈RB×d0\mathbf{X} \in \mathbb{R}^{B \times d_0}, and from there H(l)∈RB×dl\mathbf{H}^{(l)} \in \mathbb{R}^{B \times d_l} layer by layer. The backward pass still needs that same treatment, and it comes down to two changes: one of shape, one of size.

The shape change is stacking. Call Δ(l)∈RB×dl\boldsymbol{\Delta}^{(l)} \in \mathbb{R}^{B \times d_l} the batch error at layer ll: row ii is the δ(l)\boldsymbol{\delta}^{(l)} of example ii, laid on its side, exactly the way row ii of H(l)\mathbf{H}^{(l)} is its activation laid on its side. Capital for the batch, lower case for the example, this block's usual convention. The output layer enters exactly as before, only now with BB rows at once:

Δ(L)=Y^−Y∈RB×dL.\boldsymbol{\Delta}^{(L)} = \hat{\mathbf{Y}} - \mathbf{Y} \in \mathbb{R}^{B \times d_L}.

And the recurrence transposes whole. Take the previous lesson's version, for example ii, δi(l)=((W(l+1))⊤δi(l+1))⊙φ′(zi(l))\boldsymbol{\delta}^{(l)}_i = \left(\left(\mathbf{W}^{(l+1)}\right)^{\top}\boldsymbol{\delta}^{(l+1)}_i\right) \odot \varphi^{\prime}\left(\mathbf{z}^{(l)}_i\right), and transpose it. Transposing a product reverses the order of its factors, and the Hadamard product works coordinate by coordinate, so it doesn't even notice:

(δi(l))⊤=((δi(l+1))⊤W(l+1))⊙(φ′(zi(l)))⊤,\left(\boldsymbol{\delta}^{(l)}_i\right)^{\top} = \left(\left(\boldsymbol{\delta}^{(l+1)}_i\right)^{\top}\mathbf{W}^{(l+1)}\right) \odot \left(\varphi^{\prime}\left(\mathbf{z}^{(l)}_i\right)\right)^{\top},

which is row ii of what you're after, written using rows ii of Δ(l+1)\boldsymbol{\Delta}^{(l+1)} and of Z(l)\mathbf{Z}^{(l)}. Stack all BB of them, and you get

Δ(l)=(Δ(l+1)W(l+1))⊙φ′(Z(l)),\boldsymbol{\Delta}^{(l)} = \left(\boldsymbol{\Delta}^{(l+1)}\mathbf{W}^{(l+1)}\right) \odot \varphi^{\prime}\left(\mathbf{Z}^{(l)}\right),

where what's worth noticing is what disappeared: the transpose. That's neither an oversight nor a different convention. The forward pass gained a transpose when it moved to rows (W(l)h\mathbf{W}^{(l)}\mathbf{h} became H(W(l))⊤\mathbf{H}\left(\mathbf{W}^{(l)}\right)^{\top}), and the backward pass loses it for the exact same reason, since it started from a matrix that was already transposed. Both times, it's the same product read with the examples sitting in rows.

The size change is the 1/B1/B, and it comes straight from how the loss is defined. L\mathcal{L} is the mean of the ℓ\ell across the BB examples, and differentiating a mean means taking the mean of the derivatives, so the gradient of the batch is the mean of the BB gradients the previous lesson would have handed you one at a time. That mean turns out to be another matrix product:

∇W(l)L=1B∑i=1Bδi(l)(hi(l−1))⊤=1B(Δ(l))⊤H(l−1)∈Rdl×dl−1,∇b(l)L=1B(Δ(l))⊤1B,\nabla_{\mathbf{W}^{(l)}}\mathcal{L} = \frac{1}{B}\sum_{i=1}^{B}\boldsymbol{\delta}^{(l)}_i\left(\mathbf{h}^{(l-1)}_i\right)^{\top} = \frac{1}{B}\left(\boldsymbol{\Delta}^{(l)}\right)^{\top}\mathbf{H}^{(l-1)} \in \mathbb{R}^{d_l \times d_{l-1}}, \qquad \nabla_{\mathbf{b}^{(l)}}\mathcal{L} = \frac{1}{B}\left(\boldsymbol{\Delta}^{(l)}\right)^{\top}\mathbf{1}_B,

with 1B∈RB\mathbf{1}_B \in \mathbb{R}^{B} the vector of ones from the lesson on the forward pass. The sum over examples the previous lesson wrote out by hand is exactly the index the matrix product runs over and discards, which is why the result has the shape of W(l)\mathbf{W}^{(l)} and carries no memory of BB at all.

With that in hand, the descent rule from the lesson on gradient descent doesn't change by a single letter: θt+1=θt−η ∇θL(θt)\theta_{t+1} = \theta_t - \eta\,\nabla_{\theta}\mathcal{L}(\theta_t), applied to each matrix and each bias vector on its own. One concession, worth saying out loud: every step here looks at the entire training set (the labelled examples the network has, in this case the four phrases), exactly as that lesson did with the ten reviews. Splitting it into pieces and taking one step per piece only becomes necessary once the set grows large, and this one has all of four rows.

Everything the four-phrase network needs is on the table now: the smallest multilayer perceptron (MLP) worth trying it on, with two inputs, four hidden neurons using tanh⁡\tanh and one output neuron with a sigmoid, that is, L=2L = 2 and d0,d1,d2=2,4,1d_0, d_1, d_2 = 2, 4, 1. Its forward pass takes one line per layer,

H(1)=tanh⁡(X(W(1))⊤+1B(b(1))⊤)∈R4×4,Y^=σ(H(1)(W(2))⊤+1B(b(2))⊤)∈R4×1,\mathbf{H}^{(1)} = \tanh\left(\mathbf{X}\left(\mathbf{W}^{(1)}\right)^{\top} + \mathbf{1}_B\left(\mathbf{b}^{(1)}\right)^{\top}\right) \in \mathbb{R}^{4 \times 4}, \qquad \hat{\mathbf{Y}} = \sigma\left(\mathbf{H}^{(1)}\left(\mathbf{W}^{(2)}\right)^{\top} + \mathbf{1}_B\left(\mathbf{b}^{(2)}\right)^{\top}\right) \in \mathbb{R}^{4 \times 1},

with X∈R4×2\mathbf{X} \in \mathbb{R}^{4 \times 2} and B=4B = 4, and its backward pass is the three formulas above with L=2L = 2. Twelve weights and five biases: seventeen numbers that all need to go somewhere before the very first step.

Four identical neurons are a single neuron

Deciding where those seventeen numbers start is the one call no earlier formula makes, and the comfortable answer, all of them zero, fails outright. Follow the arithmetic through with that same network and seventeen zeros. Then Z(1)=0\mathbf{Z}^{(1)} = \mathbf{0}, and since tanh⁡(0)=0\tanh(0) = 0, also H(1)=0\mathbf{H}^{(1)} = \mathbf{0}; from there Y^=σ(0)\hat{\mathbf{Y}} = \sigma(\mathbf{0}), which is 1/21/2 for all four phrases. Feed that into the two gradients above:

∇W(2)L=1B(Δ(2))⊤H(1)=0,Δ(1)=(Δ(2)W(2))⊙φ′(Z(1))=0,\nabla_{\mathbf{W}^{(2)}}\mathcal{L} = \frac{1}{B}\left(\boldsymbol{\Delta}^{(2)}\right)^{\top}\mathbf{H}^{(1)} = \mathbf{0}, \qquad \boldsymbol{\Delta}^{(1)} = \left(\boldsymbol{\Delta}^{(2)}\mathbf{W}^{(2)}\right) \odot \varphi^{\prime}\left(\mathbf{Z}^{(1)}\right) = \mathbf{0},

the first because H(1)\mathbf{H}^{(1)} is null and the second because W(2)\mathbf{W}^{(2)} is. With Δ(1)=0\boldsymbol{\Delta}^{(1)} = \mathbf{0}, the two gradients of the first layer vanish too, so the step leaves the network exactly where it was, and the next one finds the same starting point all over again. The hidden layer will never move.

And zero isn't really the problem, it's a particular case of something much wider. Take two hidden neurons jj and kk that start out with the same input weights, the same bias and the same output weight. The forward pass gives them the same pre-activation and the same activation, zj(1)=zk(1)z^{(1)}_j = z^{(1)}_k and hj(1)=hk(1)h^{(1)}_j = h^{(1)}_k; the backward pass makes them receive the same error, δj(1)=δk(1)\delta^{(1)}_j = \delta^{(1)}_k; and with the same error and the same activation, all three of their gradients coincide:

(∇W(1)L)jm=(∇W(1)L)km    for all m,∇bj(1)L=∇bk(1)L,∇W1j(2)L=∇W1k(2)L.\left(\nabla_{\mathbf{W}^{(1)}}\mathcal{L}\right)_{jm} = \left(\nabla_{\mathbf{W}^{(1)}}\mathcal{L}\right)_{km} \;\; \text{for all } m, \qquad \nabla_{b^{(1)}_j}\mathcal{L} = \nabla_{b^{(1)}_k}\mathcal{L}, \qquad \nabla_{\mathbf{W}^{(2)}_{1j}}\mathcal{L} = \nabla_{\mathbf{W}^{(2)}_{1k}}\mathcal{L}.

Subtracting equal quantities from equal quantities preserves the equality, so the two neurons come out of the step exactly as identical as they went in. And out of the next one after that.

The same plane twice, with the four phrases at the corners of a square: the two green dots at minus 1 comma 1 and at 1 comma minus 1, and the two hollow ones at minus 1 comma minus 1 and at 1 comma 1. In the panel on the left, titled four different rows, four green lines cross the square in four directions and cut across one another. In the one on the right, titled four identical rows, there is a single line, thicker and marked times four, which splits the square into two halves; the right-hand half is shaded green and has yes written on it, the left-hand one has no, and each of the two halves keeps one green dot and one hollow dot.
A hidden neuron is a line in the plane, and a layer of four neurons is four lines. With all four rows identical, the four lines collapse into one, and no single line can leave the two green phrases on one side and the other two on the other.

That single line is the very one from the lesson on XOR and hidden layers, which proved that none of them work: the hidden layer effectively has the width of one neuron, no matter how loudly d1d_1 claims four.

Seen from the seventeen numbers rather than from the plane, the equality turns into a trap shaped like a subspace. A configuration with all four neurons identical has only five real numbers dressed up as seventeen: one row of W(1)\mathbf{W}^{(1)}, one bias, one weight of W(2)\mathbf{W}^{(2)}, and b(2)b^{(2)}. Together they form a five-dimensional subspace sitting inside the seventeen, and the gradient always comes out tangent to it.

A contour map of a loss, stretched out horizontally. Through the centre, from side to side, runs a horizontal line labelled four identical rows; on it, seven dots come in from the right and bunch closer together until they stop at the centre, which is marked with a circle and labelled saddle point. Just above and just below that centre are two elongated green patches, each with a green dot in the middle labelled minimum, and two short arrows leave the centre upwards and downwards, perpendicular to the line, one towards each patch. Neither of the two arrows points along the line. The left edge has, written vertically, the rows come apart.
The contours belong to a toy loss I drew myself so the shape would be visible. The gradient has no vertical component along the line of identical rows, so the descent travels the whole length of it and stops at the saddle point, with its only exits pointing exactly where it cannot go.

Nothing in the algorithm ever breaks that equality on its own, because everything it does is deterministic: the break has to come from outside, and the cheapest source of "different" is a random draw. Choosing those seventeen starting numbers is the initialisation, the one point in training where chance actually enters. Its role isn't quite what it looks like: it contributes no information whatsoever about the phrases, it only pulls some neurons apart from others so the gradient can push them down different paths. How large those numbers are matters too (tiny weights end up looking too much like the case above), and the third cell measures exactly that. Which scale to pick in general depends on the depth of the network, and it comes back in Block 3, on recurrent networks, once the problem stops being a minor inconvenience and becomes the whole subject.

The loop in NumPy, and the loss falling

The first cell turns the formulas above into NumPy: the forward pass, the loss and the backward pass, with no loop yet. Run it and look at the column of shapes, then the last line.

import numpy as np

# The four XOR phrases: x1 is +1 if "not" appears and x2 is +1 if the
# evaluative word is "good". The label is 1 if the phrase speaks well.
X = np.array([[-1., 1.], [-1., -1.], [1., 1.], [1., -1.]])
Y = np.array([[1.], [0.], [0.], [1.]])
B, d0, d1, d2 = 4, 2, 4, 1

rng = np.random.default_rng(0) # fixed seed: you will see these numbers
W1, b1 = rng.normal(size=(d1, d0)), np.zeros(d1)
W2, b2 = rng.normal(size=(d2, d1)), np.zeros(d2)


def forward(X, W1, b1, W2, b2):
H1 = np.tanh(X @ W1.T + b1) # (B, d1)
return H1, 1.0 / (1.0 + np.exp(-(H1 @ W2.T + b2)))


def loss(Y_hat, Y):
p = np.clip(Y_hat, 1e-12, 1.0 - 1e-12) # the logarithm does not take zero
return -np.mean(Y * np.log(p) + (1.0 - Y) * np.log(1.0 - p))


def backward(X, Y, H1, Y_hat, W2):
D2 = Y_hat - Y # (B, d2): the output enters with no factor
D1 = (D2 @ W2) * (1.0 - H1 ** 2) # transport and mask: (B, d1)
return (D1.T @ X) / B, D1.sum(0) / B, (D2.T @ H1) / B, D2.sum(0) / B


H1, Y_hat = forward(X, W1, b1, W2, b2)
gW1, gb1, gW2, gb2 = backward(X, Y, H1, Y_hat, W2)
for name, A in [("X", X), ("H(1)", H1), ("Ŷ", Y_hat),
("∇W(1)", gW1), ("∇b(1)", gb1), ("∇W(2)", gW2)]:
print("%-6s %s" % (name, A.shape))
print("initial loss: %.4f" % loss(Y_hat, Y))

# The 1/B in plain sight: the batch gradient is the mean of the four one at a time.
one_at_a_time = np.zeros((d1, d0))
for i in range(B):
d2_i = Y_hat[i] - Y[i]
d1_i = (W2.T @ d2_i) * (1.0 - H1[i] ** 2) # the same δ, as a column
one_at_a_time += np.outer(d1_i, X[i]) / B
print("\n∇W(1) of the batch =", np.round(gW1, 4).tolist())
print("difference from the mean of the four outer products: %.1e"
% np.abs(one_at_a_time - gW1).max())
numpy

The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.

The column of shapes holds the whole division of labour: the 44 examples show up in X\mathbf{X}, in H(1)\mathbf{H}^{(1)} and in Y^\hat{\mathbf{Y}}, and in none of the three gradients, which come out with the shape of their own parameters instead. The last line checks the 1/B1/B and returns zero: the four outer products from the previous lesson, averaged by hand, match the matrix product above entry for entry.

The second cell puts that into a loop and repeats it two thousand times, with η=0.5\eta = 0.5. Run it and watch the loss fall.

import numpy as np

phrases = ["it's good", "it's bad", "not good", "not bad"]
X = np.array([[-1., 1.], [-1., -1.], [1., 1.], [1., -1.]])
Y = np.array([[1.], [0.], [0.], [1.]])
B, eta, steps = 4, 0.5, 2000

rng = np.random.default_rng(0) # the same network as the previous cell
W1, b1 = rng.normal(size=(4, 2)), np.zeros(4)
W2, b2 = rng.normal(size=(1, 4)), np.zeros(1)

for t in range(steps + 1):
H1 = np.tanh(X @ W1.T + b1) # forward pass
Y_hat = 1.0 / (1.0 + np.exp(-(H1 @ W2.T + b2)))
if t in (0, 25, 50, 100, 200, 500, 1000, 2000):
p = np.clip(Y_hat, 1e-12, 1.0 - 1e-12)
print("step %4d loss %.4f correct %d of 4"
% (t, -np.mean(Y * np.log(p) + (1.0 - Y) * np.log(1.0 - p)),
int(((Y_hat >= 0.5) == (Y == 1.0)).sum())))
if t == steps:
break
D2 = Y_hat - Y # backward pass
D1 = (D2 @ W2) * (1.0 - H1 ** 2)
W1 -= eta * (D1.T @ X) / B # and descent
b1 -= eta * D1.sum(0) / B
W2 -= eta * (D2.T @ H1) / B
b2 -= eta * D2.sum(0) / B

print()
print("neuron w1 w2 bias output weight")
for j in range(4):
print(" %d %+5.2f %+5.2f %+5.2f %+5.2f"
% (j + 1, W1[j, 0], W1[j, 1], b1[j], W2[0, j]))
print()
contribution = W2[0] * H1 # what each neuron puts into z(2)
for phrase, a in zip(phrases, contribution):
print("%-9s neurons 2 and 4: %+5.2f neurons 1 and 3: %+5.2f"
% (phrase, a[1] + a[3], a[0] + a[2]))
numpy

The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.

The loss starts at 0.72600.7260 and ends at 0.00110.0011, and all four correct answers show up before step 2525. The debt is paid off: nobody told the network that not flips the sign of whatever follows, and the weights it found aren't even the ones I wrote in the lesson on XOR and hidden layers.

They're better than that: they're the same ones, rediscovered independently. Look at neurons 22 and 44. Their weights point in almost exactly the same direction ((1.83, 1.77)(1.83,\ 1.77) and (1.79, 1.85)(1.79,\ 1.85), both close to x1+x2x_1 + x_2), their biases are opposite (−1.80-1.80 and +1.80+1.80), and so are their output weights (−3.98-3.98 and +3.93+3.93). That's the strip from the lesson on XOR and hidden layers: two parallel lines and a subtraction between them. The last table confirms it: that pair contributes +7.5+7.5 to the two positive phrases and only +0.2+0.2 to the other two. And the pair 11 and 33 does the same on the other diagonal with the sign reversed, subtracting 6.76.7 exactly where the first pair stays quiet.

What's left is deciding when to stop, and it's the flimsiest of the three decisions by far. Here I stop at a round number of steps because the curve has flattened out; I could equally well have stopped once the loss stopped falling by a thousandth, or once the norm of the gradient dropped below some threshold, and all three are rules of thumb. Correct-answer counts are no use here: by step 2525 there are already four of them, and there they stay, while the loss keeps falling by a factor of three hundred afterwards. And all three rules share a limit none of them can fix, because all three only ever look at these same four phrases.

The third cell goes back to the start of the loop and changes exactly one thing: where the weights come from. Six scales at the same seed, and below them, four genuinely identical neurons.

import numpy as np

X = np.array([[-1., 1.], [-1., -1.], [1., 1.], [1., -1.]])
Y = np.array([[1.], [0.], [0.], [1.]])
B, eta, steps = 4, 0.5, 1000


def train(scale, identical=False):
rng = np.random.default_rng(0) # the same seed in all of them
W1, W2 = rng.normal(size=(4, 2)) * scale, rng.normal(size=(1, 4)) * scale
if identical: # four copies of one and the same neuron
W1, W2 = np.repeat(W1[:1], 4, axis=0), np.full((1, 4), W2[0, 0])
b1, b2 = np.zeros(4), np.zeros(1)
curve = []
for t in range(steps + 1):
H1 = np.tanh(X @ W1.T + b1)
Y_hat = 1.0 / (1.0 + np.exp(-(H1 @ W2.T + b2)))
p = np.clip(Y_hat, 1e-12, 1.0 - 1e-12)
curve.append(-np.mean(Y * np.log(p) + (1.0 - Y) * np.log(1.0 - p)))
if t == steps:
break
D2 = Y_hat - Y
D1 = (D2 @ W2) * (1.0 - H1 ** 2)
W1 -= eta * (D1.T @ X) / B
b1 -= eta * D1.sum(0) / B
W2 -= eta * (D2.T @ H1) / B
b2 -= eta * D2.sum(0) / B
return curve, int(((Y_hat >= 0.5) == (Y == 1.0)).sum()), W1


print("%7s %8s %8s %8s %9s %s"
% ("scale", "step 0", "step 50", "step 200", "step 1000", "correct"))
for scale in [0.0, 0.01, 0.1, 0.5, 1.0, 10.0]:
curve, correct, _ = train(scale)
print("%7.2f %8.4f %8.4f %8.4f %9.4f %d of 4"
% (scale, curve[0], curve[50], curve[200], curve[1000], correct))

# And symmetry with no zeros: four copies of one neuron, with normal weights.
curve, correct, W1 = train(1.0, identical=True)
print()
print("four identical neurons: loss %.4f -> %.4f, %d of 4"
% (curve[0], curve[-1], correct))
print("W(1) after 1000 steps:", np.round(W1, 4).tolist())
print("are the four rows still the same one?", bool(np.allclose(W1, W1[0])))
numpy

The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.

The first two rows of the table never budge from 0.69310.6931, which is log⁡2\log 2: the network answers 0.50.5 to all four phrases at the start and is still answering the same thing a thousand steps later. The second of the two is the one that actually teaches something, since its scale is 0.010.01 rather than zero: the four neurons really are different from each other. What happens instead is that each layer receives a gradient multiplied by the other one's scale, and with two small factors stacked together, everything advances hundreds of times more slowly: that network isn't stuck at all, it still reaches the four phrases, only around step fifty thousand instead. At the other end, the scale 1010 starts at 4.734.73 (confidently wrong) and recovers anyway: with only two layers that comes cheap, and once depth increases, it stops being cheap at all.

The last three lines show symmetry with no zeros anywhere in sight. All four neurons start with proper normal weights, only equal to one another, and a thousand steps later they're still the same row repeated four times over. The loss has fallen from 0.72400.7240 to 0.69310.6931, which is exactly what a network that answers the same thing to everything achieves: four neurons working as one, and one neuron cannot do the job.

Test your intuition

Two questions (why zero freezes the network, and what can actually be read off a loss curve), plus a challenge that is the whole loop.

The network in this lesson starts with the four weights of W(2)\mathbf{W}^{(2)} and the eight of W(1)\mathbf{W}^{(1)} set to zero, and a thousand steps later it is exactly the same. Why?

Tick everything that is true about the loss curves the second and third cells print.

Select every correct option. This is graded all-or-nothing: there is no partial credit.

Write the whole loop for this lesson's network: d0d_0 inputs, a hidden layer of d1d_1 neurons with tanh⁡\tanh and one output with the sigmoid, over a batch of BB examples.

  • gradients(X, Y, W1, b1, W2, b2) returns the four gradients of L\mathcal{L} in the order (gW1, gb1, gW2, gb2), each one with the shape of the parameter it differentiates.
  • train(X, Y, W1, b1, W2, b2, eta, steps) takes steps steps of descent and returns (curve, W1, b1, W2, b2), where curve holds steps + 1 losses: one before each step and one at the end.

train may not write into the matrices it receives.

The first run downloads the Python interpreter (~15 MB); after that it stays in the browser cache. This challenge is much easier to solve on a physical keyboard: on a phone, read it and come back later.


The network gets the four phrases it has seen right, and there is no fifth to check it against. That isn't a defect of the training, it's that here getting it right and genuinely learning are indistinguishable, since with only four examples, memorising the answer and understanding the rule produce exactly the same numbers. Nothing this loop prints says what the network would do with not that good, and the loop has no way of ever finding out.

There's also a debt this whole business with the weights has left sitting in plain sight. I invented the two input coordinates myself: whether not appears, and whether the evaluative word is good or bad, are two questions somebody had to formulate after reading the four phrases, and an arbitrary piece of text doesn't arrive already packaged into two numbers. It did arrive, though, as the bag of words from the previous block. Mounting this same loop on a few hundred reviews represented that way, measuring accuracy for the first time on reviews the network has never seen, and finding out what's left of it, is the next lesson, the sentiment classification project.

Was this helpful?
Further reading2 sources · 1 paper, 1 interactive

Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.

  • Understanding the difficulty of training deep feedforward neural networks
    paperGlorot and Bengio, 2010AISTATS 2010EN

    The paper this lesson points at when it defers "which scale" to Block 3: it sets the initialisation scale from each layer's width so the signal neither saturates nor dies out. It does not treat symmetry; this lesson already has.

  • A Neural Network Playground
    interactiveSmilkov and Carterplayground.tensorflow.orgEN

    The loop you wrote above, with knobs: move the learning rate, the layers, the neurons and the activation, and watch the loss fall. It works on points in the plane rather than text, and brings in regularisation from outside this course.