Drawing a digit from noise

Ask a small trained network for a handwritten digit and watch it turn random noise into one, step by step, in your browser.

A small network trained by this site on 60,000 handwritten digits draws a new 28 × 28 digit from random noise in 20 steps. The left picture is the drawing so far; the right is the network’s guess, at that step, of what the finished picture will be. Everything runs in your browser from a 1.3 MB download from this site. The advanced lab Generating a digit runs the same network with all of its settings.

Drawing a digit from noise

A network trained on handwritten digits turns random noise into the digit you ask for, removing a little noise at each of 20 steps.

Starting noise
Its guess of the finished picture

The picture along the way, by step

Draw a digit
You asked fornothing yet
Starting noise#1
A digit recognizer reads it asn/a
Time on this devicen/a

Challenges

0 of 3 complete

  1. Keep one starting noise and draw 3 different digits from it, each read as the digit you asked for. (not yet)
  2. Draw the same digit from 3 different starting noises, each read as that digit. (not yet)
  3. Press Any digit and find a drawing the recognizer cannot read with confidence (no digit gets 70%). (not yet)

Try this

  • Press 3 and watch both pictures. Which appears first in the guess: the rough shape or the fine strokes?
  • Without changing the starting noise, press 1, then 8.
  • Press New starting noise, then 3 again.
  • Press Any digit on a few different starting noises.
Noise to a picture in a few linesPython with NumPy
import numpy as np

# A 5 x 5 picture of a plus sign, on the model's scale: 1 is ink, -1 is paper.
plus = -np.ones((5, 5))
plus[2, :] = 1
plus[:, 2] = 1

# How much of a picture survives at each of 1,000 noise levels (DDPM's schedule).
kept = np.sqrt(np.cumprod(1 - np.linspace(1e-4, 0.02, 1000)))


def draw(x):  # dark ink "#", light ink "+", paper "."
    return ["".join("#" if v > 0.5 else "+" if v > 0 else "." for v in row) for row in x]


rng = np.random.default_rng(7)
x = rng.standard_normal((5, 5))  # the starting noise: random numbers, no picture
levels = [999, 749, 499, 249, 0]  # four big steps, from pure noise to almost none

frames = [draw(x)]
for now, nxt in zip(levels, levels[1:]):
    guess = plus  # stand-in: a trained network makes a new guess at every step
    noise = (x - kept[now] * guess) / np.sqrt(1 - kept[now] ** 2)  # the noise x must contain
    x = kept[nxt] * guess + np.sqrt(1 - kept[nxt] ** 2) * noise  # less noise, more guess
    frames.append(draw(x))

for row in zip(*frames):
    print("   ".join(row))
++...   .+...   .++..   ..#..   ..#..
.+#..   .+#..   ..#..   ..#..   ..#..
+++..   #++.+   ##+.+   ###+#   #####
#....   #....   +....   ..+..   ..#..
...++   ...++   .....   .....   ..#..

Here the guess is always the plus sign, which stands in for a perfect network. The real network has to work its guess out from the noisy picture at every step, which is why its early guesses in the lab are blurry and its later ones sharp. Each step is the same calculation as the lab’s (the DDIM method): keep the guess, keep the noise the picture must contain, and mix them with less noise each time.

Try "embedding", "softmax", "overfitting", or "backpropagation".