This lesson builds on lessons 01–03. New here? Start at lesson 01, or carry on and load the finished pieces with one click.
A knob sits at w = 3 and its partial slope there is 4. One gradient-descent step with lr = 0.1: where does w land?
3 − 0.1 · 4 = 2.6: against the slope.
A program learns to predict a plant's height from its days of sunlight. After training, its line has w = 0.8. Who chose 0.8?
The programmer, who knew the growth rule and typed it in
If the rule were known, there would be nothing to learn and no training to run.
One plant: the program copied the growth rate of the plant most like the new one
No single example is copied: 0.8 is fitted to all the plants at once.
Nobody: it is the value that made the line agree best with the measured plants
The programmer chose the shape, a line, and how to score it; the measurements chose 0.8.
The data chose it, through training. That is the whole idea of lesson 01, on a new surface.
Yes or no
gradient_descent tunes the knobs of any function by feeling the slope of its loss, one nudge per knob.
So far the function drew a line through numbers. Many questions want a yes or a no instead: should a gate open when both of its sensors fire? A neuron answers questions like that. It multiplies each input by a weight, adds up the products and a bias, and says yes when the total is above 0.
A neuron answers yes when w1x1+w2x2+b is above 0, for inputs x1 and x2 that are each 0 or 1. Which of these rules can no setting of w1, w2 and b produce?
AND: yes only when both inputs are 1
w1=w2=2 and b = −3 does it: only 2 + 2 − 3 is above 0. One straight line cuts the corner (1, 1) off from the other three.
XOR: yes when exactly one input is 1
The yes corners, (0, 1) and (1, 0), sit on one diagonal and the no corners on the other. No straight line puts both yeses on one side and both nos on the other.
None: with the right numbers one neuron can produce any of these rules
One neuron's yes-region is everything on one side of a straight line, and XOR's two yes corners can't be fenced off by one line.
XOR. A single neuron splits its inputs with one straight line. AND and OR each need one cut; XOR needs two, and lesson 05 makes them.
A neuron with that hard answer, 1 when the total is above 0 and 0 otherwise, is called a perceptron. Hand one to gradient_descent and ask it to learn AND.
A step neuron gets AND wrong on two of four inputs. Measure its slopes, then let gradient_descent try for 100 steps.
from ember.basics import numerical_derivative, gradient_descent, mse
AND = [((0, 0), 0), ((0, 1), 0), ((1, 0), 0), ((1, 1), 1)]
def step(z):
return 1.0 if z > 0 else 0.0
def loss(knobs):
w1, w2, b = knobs
return mse([t for _, t in AND], [step(w1 * x1 + w2 * x2 + b) for (x1, x2), _ in AND])
knobs = [0.3, -0.2, 0.1]
print(f"loss {loss(knobs):.2f}")
for i, name in enumerate(["w1", "w2", "b"]):
def along(v, i=i):
moved = list(knobs)
moved[i] = v
return loss(moved)
print(f"measured slope for {name}: {numerical_derivative(along, knobs[i]):.2f}")
after, _ = gradient_descent(loss, knobs, 1.0, 100)
print(f"after 100 steps: knobs {[round(k, 2) for k in after]}, loss {loss(after):.2f}")
loss 0.50
measured slope for w1: 0.00
measured slope for w2: 0.00
measured slope for b: 0.00
after 100 steps: knobs [0.3, -0.2, 0.1], loss 0.50
The loss is 0.50, all three measured slopes are 0.00, and after 100 steps the knobs have not moved. A nudge of a millionth never flips an answer, so the loss never changes, so there is no slope to follow.
A smooth step
Multiplying two lists pair by pair and adding up the products is called a dot product. The weights (2, −1) and the inputs (1, 0) give 2 · 1 + (−1) · 0 = 2. A neuron's weighted sum is the dot product of its weights and its inputs.
The neuron's total is a weighted sum of its inputs plus the bias, z=∑iwixi+b. For x = (1, 0), w = (2, −1) and b = −0.5: z = 2 · 1 + (−1) · 0 − 0.5 = 1.5.
The step function is flat everywhere except at its jump, so its measured slope is 0 almost everywhere, and gradient descent has nothing to follow. The neuron needs a step with a slope: one that rises smoothly from 0 to 1. Its ingredient is a new number, e, raised to a power.
The number e is about 2.718, and e0=1 while e−1≈0.368: ex shrinks towards 0 as x goes negative and grows fast as x goes positive.
The smooth step is sigmoid(z)=1+e−z1 which at z = 1.5 is 1 / (1 + 0.2231) = 0.8176, because e−1.5≈0.2231.
sigmoid is 0.5 at z = 0, close to 1 for large z and close to 0 for very negative z. Read an output above 0.5 as yes. Every small change of z now changes the output a little, so the loss has a slope to feel. The function a neuron applies to its total, here sigmoid, is called its activation.
Using e−1≈0.368, what is sigmoid(1)?
1 / (1 + 0.368) = 1 / 1.368, about 0.731: a fairly confident yes.
What: write sigmoid(z), and neuron(xs, ws, b), which returns sigmoid of the weighted sum of the inputs plus b.
Why: without a smooth step the neuron has no slope, and nothing can train it. train_neuron in the next lab tunes this neuron, lesson 12's Neuron is the same shape built from Values, and lesson 23 compares sigmoid with other kinks.
Your ember has a neuron.
sigmoid needs e raised to the power -z. Which function in the math module computes e to a power?
sigmoid is the page's formula, with math.exp(-z) for e to the power -z. neuron pairs each weight with its input, adds up the products, adds b, and passes the total through sigmoid.
Nearly the code: sigmoid returns 1 / (1 + math.exp(-z)); neuron returns sigmoid(sum(w * x for w, x in zip(ws, xs)) + ...)
sigmoid(0) is exactly 0.5, halfway between off and on.
sigmoid(2) is about 0.8808 and sigmoid(-2) about 0.1192.
sigmoid(-z) is 1 - sigmoid(z).
Even at z = 30 and z = -30 the output is inside (0, 1).
neuron(xs, ws, b) is sigmoid(w . x + b).
With all-zero inputs the neuron's output is sigmoid(b).
What: write train_neuron(data, lr, steps): data is a list of (inputs, target) pairs; fit the neuron's weights and bias with gradient_descent on the mean squared error of its outputs, and return (ws, b).
Why: without it a neuron's weights are numbers you pick by hand. It is the first time your ember learns a decision from examples, and lesson 05 needs its failure on XOR.
Your ember learns AND and OR from four examples each.
gradient_descent tunes one flat list of knobs. How do you pack the weights and the bias into one list, and unpack them inside the loss?
Write loss(knobs): ws = knobs[:-1] and b = knobs[-1]; return the mse of the targets against neuron(x, ws, b) for every example. Start from zeros: one per input, plus one for the bias.
Nearly the code: knobs, _ = gradient_descent(loss, [0.0] * (len(data[0][0]) + 1), lr, steps); then return knobs[:-1], knobs[-1].
With steps=0 the knobs stay at zero: ([0, 0], 0).
One step at lr=1 on AND leaves the weights at 0 and moves the bias to -0.125.
Trained with the defaults, one neuron gets all four AND cases right.
Trained with the defaults, one neuron gets all four OR cases right.
No single neuron gets all four XOR cases right, so training must leave one wrong.
Train on AND, OR and XOR, and draw the AND neuron's boundary.
import hx
from ember.basics import neuron, train_neuron
AND = [((0, 0), 0), ((0, 1), 0), ((1, 0), 0), ((1, 1), 1)]
OR = [((0, 0), 0), ((0, 1), 1), ((1, 0), 1), ((1, 1), 1)]
XOR = [((0, 0), 0), ((0, 1), 1), ((1, 0), 1), ((1, 1), 0)]
trained = {}
for name, data in [("AND", AND), ("OR", OR), ("XOR", XOR)]:
ws, b = train_neuron(data, 10.0, 300)
trained[name] = (ws, b)
print(f"{name:3}: " + " ".join(f"{x} -> {neuron(x, ws, b):.2f}" for x, _ in data))
ws, b = trained["AND"]
grid = {"x0": -0.5, "x1": 1.5, "y0": -0.5, "y1": 1.5, "nx": 21, "ny": 21}
values = [[neuron((-0.5 + 0.1 * i, -0.5 + 0.1 * j), ws, b) for i in range(21)] for j in range(21)]
points = [{"x": x1, "y": x2, "c": t} for (x1, x2), t in AND]
hx.plot("boundary", {"grid": grid, "values": values, "points": points})