deep-neural-networks

Claude Opus 5 · Claude Code · effort: max

film

youtu.be/uSvXpGTTsf0

script

Session Script — DESCENT

A chronological record of the session that produced DESCENT, a four-minute film about deep neural networks.


0. Pre-session commands

The user ran two slash commands before the first prompt:

/model   → Kept model as Opus 5
/effort  → Set effort level to max (this session only)

1. The prompt

User: Hi Claude. You're in an empty git repository. Please use mise, python, uv, and whatever dependencies you like to create an artistic film about deep neural networks. Give viewers a visceral feeling of the depth, scale, and complexity of deep neural networks, along with the beautiful emergent properties they exhibit.


2. Environment survey

Four parallel Bash calls established the ground truth:

Check Result
Working dir ~/repos/llm-video-repos/deep-nerual-networks-opus-5-max-claude-code, empty but for .git
mise 2026.7.18 present
uv not installed
python3 3.14.6 (homebrew)
ffmpeg not installed
Network PyPI reachable (HTTP 200)
Hardware Apple M5, 10 cores, 16 GB RAM
Fonts HelveticaNeue.ttc, Avenir Next.ttc, SFNSMono.ttf available

A follow-up font probe enumerated the TTC faces and found Helvetica Neue UltraLight (index 5), Thin (12), Light (7) — the typographic register the film would use — plus SFNSMono.ttf for numerics.

Decisions taken from this survey:


3. Toolchain scaffold

Wrote mise.toml pinning python = "3.12" and uv = "latest", with an auto-created .venv and five tasks (setup, data, render, preview, film).

mise trust && mise install     → all tools installed
mise exec -- python --version  → Python 3.12.13
mise exec -- uv --version      → uv 0.12.0

Created a six-item task list to track the build:

  1. Scaffold project with mise + uv
  2. Build the render engine
  3. Train real networks and capture internals
  4. Compose the film's scenes
  5. Synthesize the generative score
  6. Render and mux the final film

pyproject.toml declared four dependencies — numpy, pillow, scipy, imageio-ffmpeg — deliberately no deep-learning framework. uv sync installed numpy 2.5.1, pillow 12.3.0, scipy 1.18.0, imageio-ffmpeg 0.6.0.


4. Foundational modules

Written in sequence:

A stylistic wrinkle was caught and fixed immediately after writing post.py: the split-tone block was convoluted (tint / tint.mean() with a fractional exponent). Replaced with clean multipliers centred on 1.0.


5. The networks — and a research detour

nn.py implemented an MLP with manual backprop and Adam, plus spiral() and grid_2d() dataset generators.

5a. Testing whether a width-2 net can untangle a spiral

The plan for Movement V was Chris Olah's classic demonstration: a deep net whose hidden layers are exactly 2-D, so the transformation of space is directly visible with no projection. This had to actually work, not be faked.

turns=1.6 depth=8  width=2 → best acc [0.67, 0.69, 0.82]
turns=1.6 depth=12 width=2 → best acc [0.91, 0.69, 0.67]
turns=1.9 depth=16 width=2 → best acc [0.67, 0.67, 0.68]

Failed. Plain width-2 tanh nets get stuck in bad optima.

5b. Identity initialisation

Initialising each 2×2 weight matrix near I × 1.35 should place the net in a much better basin:

turns=1.6 depth=10 → [0.75, 0.83, 0.81]
turns=1.9 depth=10 → [0.84, 0.70, 0.80]
turns=2.2 depth=24 → [0.61, 0.60, 0.50]

Better, still not good enough.

5c. Residual flow — the answer

Switched to h ← h + α·(tanh(hA + a)C + c) with 2-D state. Every block is a small perturbation of the identity, so the whole net is a diffeomorphism of the plane: it can stretch and fold space but never tear it.

turns=1.9 blocks=24 → acc [0.987, 0.9995]
turns=1.9 blocks=48 → acc [1.0, 1.0]
turns=2.4 blocks=48 → acc [0.934, 0.925]
turns=2.9 blocks=48 → acc [0.967, 0.898]

Worked. This is both easier to train and a stronger argument — the untangling is provably continuous deformation. ResidualFlow was added to nn.py with hand-derived backprop.

5d. MNIST classifier and filter legibility

data.py downloaded MNIST from the CVDF mirror. First training run: 784→128→64→10 ReLU MLP, 4200 steps, 97.91% held out.

Dumped a contact sheet of first-layer filters and inspected it. The emergence arc (noise → structure) was visible, but the mature filters were high-frequency and hard to read on screen. Swept four regularisation configurations:

Config Test acc Verdict
A: l2=1.2e-3, no noise, 8000 steps 0.9777 Clean, smooth stroke/loop detectors — chosen
B: l2=2e-4, input noise 0.35 0.9767 Still noisy
C: decoupled wd 0.55, noise 0.30 0.9706 Too many dead units
D: decoupled wd 1.6, noise 0.35 0.9213 Accuracy collapse

Config A locked in. Firm weight decay does real work here: without it the same network reaches the same accuracy through noise you cannot see, and Movement IV would have had nothing to show.

Final archive: 109,386 parameters, 97.77% held out; flow 3,942 parameters, 48 blocks, 99.95% separated.


6. Film structure

timeline.py defined seven movements with deliberate overlaps so nothing ever cuts:

# Movement Start Duration
I SPARK 0.0 27.0
II LATTICE 25.5 37.5
III FLOOD 61.5 37.0
IV EMERGENCE 96.8 39.0
V MANIFOLD 134.0 47.0
VI DESCENT 179.4 31.0
VII RECOGNITION 206.4 30.2

Total: 236.6 s (3:56.6), 7,098 frames at 30 fps.

Plus an Ambience cue running the full length — a drifting dust field that keeps the frame from ever being empty and gives every wide shot parallax.


7. Scenes, and the QA loop

Each scene was written, rendered as preview stills, inspected, and corrected. The loop ran roughly a dozen times.

Movement I — SPARK

Twelve real weights of one real hidden unit converge on a soma that saturates via tanh. First render was far too dark; boosted exposure 1.06→1.30, dendrite intensity, and ambience. Added a beat where the column of neurons opens into a disc — the near face of the first layer, matching Lattice's opening shot.

Movement II — LATTICE

Three iterations of geometry:

  1. SPACING=2.45, RADIUS=1.62 → structure a tiny blob in frame; camera receded to 52 units.
  2. Pulled camera in to 25, widened FOV, raised bead brightness → filled frame but read as a "comet"; far end showed moiré from identical golden-angle lattices.
  3. SPACING=4.30, RADIUS=1.85, added positional jitter to break the moiré, fog reaching exactly to the built horizon → distinct luminous plates receding into dark, with visible filaments. The title "DESCENT" drops at t≈41.

Movement III — FLOOD

Real activations, real signed weights, gradients flowing back in amber. Cycles accelerate geometrically (fwd *= 0.76 each iteration) until they strobe.

Bug found: cv.glow left a visible square edge — the gaussian's tail was being clipped by its bounding box. Replaced with a compact quartic kernel q²(0.55q + 0.45) that reaches exactly zero at the radius. Fixed everywhere at once.

Movement IV — EMERGENCE

128 receptive fields as 100,352 splatted points, scrubbing through 63 log-spaced training snapshots.

Two corrections:

  1. First render was sparse dashes — global normalisation meant only the strongest weights showed. Switched to per-unit normalisation with a floor (max(peak, 0.24·global_peak)) so dead units stay dark on purpose, and applied a mag**0.62 perceptual curve.
  2. The push-in got darker. Zooming by Z spreads the same points over Z² more pixels, so energy per point must rise by Z². Added min(zoom², 11.0).

Required re-running training to capture cls_snap_steps so the on-screen correction counter would be honest.

Movement V — MANIFOLD

The film's argument. A rotating line reports its measured accuracy (swept over 720 half-planes) and fails. Then 48 blocks bend the plane. At the end a straight white line separates the classes; then the flow runs backwards and the boundary returns to input space, revealing it was spiral-shaped all along.

The classified field was initially too dim to read as territory — raised base intensity 0.030→0.115.

Movement VI — DESCENT

First attempt read as a distant explosion. Three fixes:

  1. The central cv.glow at 0.40·h and intensity 0.65 was washing out everything — cut to 0.20·h / 0.10.
  2. Added helical cable filaments running the length of the shaft.
  3. Sampled those filaments uniformly in 1/z, not z — which is uniform in screen space, so the wall stays evenly dense from beside the lens to the horizon.

Result: a genuine corridor filling the frame.

Movement VII — RECOGNITION

Real logits as evidence bars, real confidence, dissolution into dust, end card. Removed a redundant large numeral overlaying the pixel digit; shrank the winner's glow from a yellow blob to a point.


8. Performance work

Benchmarked one 1080p frame per movement:

I spark  237ms   II lattice 292ms   III flood  1303ms   IV emergence 408ms
V manifold 1905ms   VI descent 262ms   VII recog 315ms
→ mean 675 ms → ~9 min on 9 workers

Two hot spots, two fixes:

  1. Canvas.lines length bucketing. Sample count was derived from the longest segment in a batch, so one stretched grid segment forced all 5,772 to be oversampled. Grouped segments into power-of-two sample counts. → Manifold 1905 ms → 313 ms.
  2. Flood lattice caching. The 4,940-synapse lattice never moves, so it is baked once into a layer at setup and added with a scalar each frame. → Flood's 1303 ms turned out to be the one-time bake; steady state 235 ms.

Re-benchmarked with all scenes warm: mean 293 ms → ~4 min projected.


9. The score

score.py, synthesised entirely in numpy and placed from the film's own timeline so picture and sound cannot drift:

Two bugs:

  1. Negative start offsets (the first swell begins at t = −2.2) sliced arrays from the end and produced shape-0 writes. Added a _place() helper that clips at both ends.
  2. Entire output was NaN. np.sin(linspace(0,1)·π) can land microscopically below zero at the endpoint in float32, and (−1e-8)**2.1 is NaN. Clamped with np.clip(s, 0, None) and added a nan_to_num guard at master.

Spectral analysis then showed Movement III's centroid at 5,545 Hz — the ticks were audibly hissy. Darkened them (660 Hz + 1320 Hz tone, heavily filtered noise).


10. Full preview pass

mise exec -- uv run descent-render --preview
→ 30 segments, 10 workers, 3.7 minutes, 68.8 MB

Built a 6×5 contact sheet from the encoded file to review the whole arc at a glance. The structure read correctly. Extracted five full-size transition frames.

Found a real defect: at t≈209 the film went black for ~2 seconds. Depth's fade-out ended before Recognition drew anything — Recognition's first element began at local t = 2.4, exactly when Depth finished. Fixed by extending Depth to 31 s, starting the image forming at local t = 0.4 over a 5 s ramp, and adding Recognition's missing title card (it was the only movement without one).


11. Final render — and an abort

Started the 1080p render, then continued reviewing code and found:

Movement I was reading a dead unit. The hardcoded hidden unit 3 had been zeroed entirely by weight decay (peak |w| = 0.0000, rank 122 of 128). The twelve "strongest weights" were twelve zeros. Also corrected a README claim of "seven pushing and five pulling" — the real split is 6/6.

Killed the in-flight render, replaced the hardcode with a setup-time search for the strongest sign-balanced unit (found unit 83: 6 push / 6 pull, peak |w| = 0.2086), verified visually, and restarted.

→ 30 segments, 10 workers, 29.0 minutes, 565.3 MB

Slower than the 4-minute projection: per-worker setup dominates the first batch, and the splat kernel is memory-bandwidth-bound, so 10 workers don't scale linearly.

A ruff pass mid-render removed four unused imports and sorted __slots__ — verified as non-behavioural, so the in-flight render stayed valid.


12. Verification and delivery

ffmpeg -i out/descent-final.mp4 -f null -     → decodes clean, no errors
Duration: 00:03:56.60
Video: h264 (High), yuv420p, 1920x1080, 18872 kb/s, 30 fps
Audio: aac (LC), 48000 Hz, stereo, 231 kb/s

Pulled eight full-resolution frames and confirmed the emergence contact sheet and manifold reveal held up at 1080p.

The crf-16 master is 565 MB — the film grain is near-incompressible. Added a --crf CLI option (passed through the job tuple, since spawned workers re-import the module fresh and would not see a mutated module global) and produced a 142 MB viewing copy at crf 21. Both verified to decode.

Wrote a README documenting the seven movements, exactly what is real, the splat architecture, the two bucketing optimisations, and the score.


13. Final state

out/descent-final.mp4    546 MB   crf 16 master
out/descent-1080p.mp4    144 MB   crf 21 viewing copy
out/score-final.wav       43 MB
src/descent/            3,894 lines across 22 modules

Lint clean. Nothing committed — 6 untracked paths, and the user had not asked for a commit.


14. Closing exchange

User: Write the full script of this session to script.md. Write notes about this session to notes.md. Write a metadata.toml file …

Loaded the claude-api skill to get current Opus pricing rather than answering from memory, gathered session timestamps, and wrote the three files.

session

duration
1h 54m total · 57m 30s model
tokens
12.30M in · 235k out
details

$/MTok uncached-input $5.00 cached-input-write $10.00 cached-input-read $0.50 output $25.00 effective-blended $1.82

notes.md metadata.toml

artifacts

select a file