AI for Beginners · Pro

A hundred-parameter model that learns color

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.

CourseAI for Beginners
TierPro — runnable code
NeedsPython 3, nothing else
Before the code

The code was never the large part

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.

What it actually learns

Tetradic color harmony — four colors, 90° apart

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.

The shape of the model

3 in, two layers of 5, 9 out — 104 parameters

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.

Input3 — R, G, B
Hidden
Hidden
Output9 — 3 colors

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.

No install required

Try it yourself, right here

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...

#ff6b35
your color
match +90°
match +180°
match +270°
Copy, paste, run

The whole thing, top to bottom

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()
What you'll see

Untrained noise, then a real palette

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
your color · #ff6b35
+90° · #72ec3f
+180° · #2ab8e3
+270° · #c12be0

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.

Back to the course

Where this came from

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.

AI for Beginners — start from Lesson 1 →

Sign in to orqo

Choose how you'd like to continue.

More ways to sign in are on the way.