Not part of the fourteen lessons, and not an outline either — this one runs. A real, tiny, trainable model: 104 numbers, zero installs, that learns to match colors purely from examples. Copy it into a terminal and watch it happen.
Lesson 2 drew a line between the model's structure and the paint on its floor. In code terms: a model's structure — the actual program that defines it — is usually small. What's large is the trained weights sitting inside that structure, sometimes hundreds of gigabytes of numbers. A real one weighs that much. This one doesn't, on purpose.
Lesson 6's arithmetic gave the model about a hundred parameters — a five-slot floor, stacked a few layers deep. This is that exact model, written out in runnable Python, doing real work: given one color, it predicts three others that go with it.
Color theory has a real, named scheme for this: pick any color, and three more colors evenly spaced around the color wheel — 90°, 180°, and 270° around — will read as a matching set. That's not a guess; it's a known formula, the same one design tools use.
The model is never given that formula. It only ever sees pairs — this color, in; these three colors, out — thousands of times, generated fresh at random, and has to find the pattern on its own. Nothing here is a lookup table. Feed it a color it has never seen, and it still has to generalize, the same way Lesson 7's model had to generalize instead of memorize.
Three inputs (a color's red, green, and blue), two hidden layers five wide — the same width as the very first board in this course — and nine outputs: three matching colors, three channels each.
Count every weight and every bias and it comes to 104 parameters — not picked to hit a round number, just what falls out of that shape.
This is the exact same network as the runnable script further down this page, ported line for line into JavaScript, training from scratch in your browser right now — nothing sent to a server, nothing pre-computed. Once it's done, pick any color and watch it predict fresh.
$ training in your browser...
Training — 104 parameters, 30,000 examples...
No imports beyond Python's own standard library. Save this as color_board.py and run it with python3 color_board.py — or download it directly.
#!/usr/bin/env python3
"""
color_board.py -- a hundred-parameter model that learns color harmony.
Give it a color, it predicts three others that go with it -- a tetradic
("square") color scheme: four colors evenly spaced around the color wheel,
90 degrees apart. Nothing here is hard-coded. The network never sees the
rotation formula directly -- it only ever sees (base color) -> (three
matching colors) examples, thousands of them, and has to discover the
pattern on its own.
Zero installs. colorsys and random are both part of core Python.
"""
import colorsys
import random
INPUT_SIZE = 3 # a color: red, green, blue
HIDDEN_SIZE = 5 # same width as the toy board from the course
OUTPUT_SIZE = 9 # three matching colors, three channels each
def rand_weight():
return random.uniform(-0.5, 0.5)
def new_layer(n_in, n_out):
weights = [[rand_weight() for _ in range(n_in)] for _ in range(n_out)]
biases = [rand_weight() for _ in range(n_out)]
return weights, biases
w1, b1 = new_layer(INPUT_SIZE, HIDDEN_SIZE)
w2, b2 = new_layer(HIDDEN_SIZE, HIDDEN_SIZE)
w3, b3 = new_layer(HIDDEN_SIZE, OUTPUT_SIZE)
def param_count():
return (
len(w1) * len(w1[0]) + len(b1) +
len(w2) * len(w2[0]) + len(b2) +
len(w3) * len(w3[0]) + len(b3)
)
def sigmoid(x):
if x < -60:
return 0.0
if x > 60:
return 1.0
return 1 / (1 + pow(2.718281828, -x))
def sigmoid_deriv(y):
return y * (1 - y)
def forward(x):
h1 = [sigmoid(sum(w1[j][i] * x[i] for i in range(INPUT_SIZE)) + b1[j]) for j in range(HIDDEN_SIZE)]
h2 = [sigmoid(sum(w2[j][i] * h1[i] for i in range(HIDDEN_SIZE)) + b2[j]) for j in range(HIDDEN_SIZE)]
out = [sigmoid(sum(w3[j][i] * h2[i] for i in range(HIDDEN_SIZE)) + b3[j]) for j in range(OUTPUT_SIZE)]
return h1, h2, out
def train_step(x, target, lr):
h1, h2, out = forward(x)
out_error = [out[j] - target[j] for j in range(OUTPUT_SIZE)]
out_delta = [out_error[j] * sigmoid_deriv(out[j]) for j in range(OUTPUT_SIZE)]
h2_error = [sum(out_delta[j] * w3[j][i] for j in range(OUTPUT_SIZE)) for i in range(HIDDEN_SIZE)]
h2_delta = [h2_error[i] * sigmoid_deriv(h2[i]) for i in range(HIDDEN_SIZE)]
h1_error = [sum(h2_delta[j] * w2[j][i] for j in range(HIDDEN_SIZE)) for i in range(HIDDEN_SIZE)]
h1_delta = [h1_error[i] * sigmoid_deriv(h1[i]) for i in range(HIDDEN_SIZE)]
for j in range(OUTPUT_SIZE):
for i in range(HIDDEN_SIZE):
w3[j][i] -= lr * out_delta[j] * h2[i]
b3[j] -= lr * out_delta[j]
for j in range(HIDDEN_SIZE):
for i in range(HIDDEN_SIZE):
w2[j][i] -= lr * h2_delta[j] * h1[i]
b2[j] -= lr * h2_delta[j]
for j in range(HIDDEN_SIZE):
for i in range(INPUT_SIZE):
w1[j][i] -= lr * h1_delta[j] * x[i]
b1[j] -= lr * h1_delta[j]
return sum(e * e for e in out_error) / len(out_error)
def tetradic_match(r, g, b):
h, l, s = colorsys.rgb_to_hls(r, g, b)
matches = []
for turn in (0.25, 0.50, 0.75):
h2 = (h + turn) % 1.0
r2, g2, b2 = colorsys.hls_to_rgb(h2, l, s)
matches.extend([r2, g2, b2])
return matches
def random_color():
return [random.random(), random.random(), random.random()]
def training_example():
base = random_color()
target = tetradic_match(*base)
return base, target
def train(steps=30000, lr=0.5):
for step in range(steps):
x, target = training_example()
loss = train_step(x, target, lr)
if step % 3000 == 0:
print(f" step {step:>6} loss {loss:.5f}")
def hex_to_rgb(hexcolor):
hexcolor = hexcolor.strip().lstrip("#")
r = int(hexcolor[0:2], 16) / 255
g = int(hexcolor[2:4], 16) / 255
b = int(hexcolor[4:6], 16) / 255
return r, g, b
def swatch(r, g, b, label=""):
R, G, B = int(r * 255), int(g * 255), int(b * 255)
block = f"\033[48;2;{R};{G};{B}m \033[0m"
return f"{block} {label:<11} #{R:02x}{G:02x}{B:02x}"
def show_prediction(hexcolor):
r, g, b = hex_to_rgb(hexcolor)
_, _, out = forward([r, g, b])
print(swatch(r, g, b, "your color"))
for i, name in enumerate(["+90 deg", "+180 deg", "+270 deg"]):
rr, gg, bb = out[i * 3:i * 3 + 3]
print(swatch(rr, gg, bb, f"match {name}"))
if __name__ == "__main__":
print(f"A model with {param_count()} parameters. Nothing more.")
print("Training on random colors, no rules ever told -- only examples shown...")
train()
print("\nDone training.\n")
for test in ["ff6b35", "1f6f66", "b8862f", "202020"]:
print(f"base: #{test}")
show_prediction(test)
print()
It trains in under a second — thirty thousand random colors is nothing for a hundred parameters. Then it asks for a color and prints its three matches as real colored blocks, right in your terminal.
One real run, unedited — asked to match #ff6b35, a warm orange:
$ python3 color_board.py
A model with 104 parameters. Nothing more.
Training on random colors, no rules ever told -- only examples shown...
step 0 loss 0.12280
step 9000 loss 0.00364
step 18000 loss 0.00840
step 27000 loss 0.00479
Done training.
base: #ff6b35
Compare that against the exact formula for the same color — green, blue, magenta, evenly rotated — and the model is genuinely close, not identical. That gap is real: a hundred parameters, trained on random examples, gets you clearly rotated, roughly right, not a perfect colorimeter. It also has one known blind spot worth trying yourself: feed it a near-grey color, where the true hue is barely defined at all, and watch it hedge — a small, clear look at where a small model's confidence actually runs out.
Every term in this page — model, weights, layers, training, generalizing instead of memorizing — comes straight from the fourteen lessons. If any of it felt unfamiliar, that's where it's built up from the ground.