A few weeks ago, I saw this post where the author trained a SIREN MLP to implicitly memorize Bad Apple as a coordinate function: (t, y, x) to pixel.
That got me curious about a slightly different formulation: instead of handing the network a timestamp t, could a small recurrent dynamical system (RNN-ish) learn the continuous temporal flow in latent space and generate the entire ~6,500-frame full resolution video autonomously from a single initial condition (h_0, c_0)?
The code, weights, and analysis tools with rollout scripts, plots, and standalone models are shared here: GitHub: SEBADA321/BadAppleRNN.
Architecture & Inference Footprint
At inference time, the system receives no timestamp inputs and evaluates in a closed loop:
(h_t, c_t) -> Recurrent Transition (CTF) -> (h_{t+1}, c_{t+1})
|
h_t -> Frame Decoder -> 384x512 Grayscale Frame
- Latent Dimension: 64-D for h_t (decoded) and 64-D for c_t (internal memory manifold to separate visually similar frames at different timestamps).
- Recurrent Transition (ctf): 4-gate LSTM-style recurrence with orthogonal initialization (16,640 parameters, 65 KB).
- Frame Decoder (fd): 4-stage bilinear upsampling with depthwise-separable convolutions (400,361 parameters, 1.56 MB).
- Initial State: A single pair of 64-dim vectors (h_0, c_0) (128 floats, 0.5 KB).
- Total Inference Model: 417,129 parameters (~1.60 MB in FP32).
- Runtime Performance: >200 FPS on an RTX 4080, with ~17.2 MB peak active VRAM during decoding.
Training an autonomous system across ~6,573 steps from t=0 directly was probably computationally unstable due to vanishing/exploding gradients and compounding error. The training setup I was circling around used several targeted techniques:
- Learned Latent Teacher Tables: During training, I optimize a pair of tables h_table[t] and c_table[t] alongside the model. This allows parallel segment training starting at arbitrary timestamps over a finite horizon K. These tables are scaffolding and are discarded entirely at inference.
- Rollout Horizon Curriculum: I started training with K = 2 and progressively doubled the rollout length (K = 2 -> 4 -> 8 -> 16 -> 32 -> 64 -> 128 -> 256 -> 512). Each horizon doubling produced a characteristic jump in loss before the transition function adapted to the longer trajectory.
- State Perturbation Noise (sigma = 0.005): To prevent the model from learning a brittle 1D line that diverges under small numerical errors, Gaussian noise was added to the state before passing it into the transition function (z_hat_{t+1} = F(z_t + epsilon)), while evaluating the loss against the clean target. This encourages the recurrent map to contract small deviations back toward the trajectory.
- Second-Difference Acceleration Regularization: Penalizing velocity (||h_{t+1} - h_t||) risks collapsing the trajectory. Instead, I penalized discrete acceleration (jitter) via second differences: ||h_{t+2} - 2h_{t+1} + h_t||_2^2. This enforces smooth trajectories without penalizing motion.
- Optimizer & Momentum Management: I used AdamW (1x10^-5) for the decoder/tables and Muon (0.005, momentum 0.95) for the recurrent weights. To prevent accumulated momentum from acting as stale inertia when K doubled, momentum buffers were scaled by 0.2 every 10 epochs starting from the epoch 500.
- Chunked Decoding: To handle long horizons at K = 512 without overflowing VRAM during training, the decoder was evaluated in temporal chunks of 32 frames.
Some interesting things
- The model could unroll the full 6.5k sequence even if it was, technically, trained on up to 512 frames. So that was a success.
- Training loss vs. autonomous rollout: The checkpoint with the lowest numerical training loss was not necessarily the best at autonomous generation. Because K changes across the curriculum, raw training losses are not directly comparable across stages, and short-horizon teacher-forced agreement does not guarantee long-horizon stability.
- Dynamical stability over parameter scale: The challenge was not increasing parameter count (the recurrent transition is only 16k params), but conditioning the dynamics through noise injection and acceleration penalties so error doesn't compound over thousands of recurrent steps.
- I need to improve the decoder a lot, I was mostly focused in getting the recurrent part right, and training was slow. Now that I gave gotten a successful result I will focus more into optimizing the CNN decoder.
- There are no skip connections nor normalization, which was interesting too. Obviously no attention either since I wanted to keep it simple.I also didn't want to use Truncated BPTT.
Not completely scientific, since I was doing some changes mid run or many at once, which makes it kinda not clear what contributed more. I used 'AI' to help with writting the post and README. Part of the code was also generated that way, but the architecture is what I came up with on my own and from a previous project too. There are probably many parts to improve too, so glad to get some feedback!