File size: 3,424 Bytes
2fa05eb
 
 
 
 
 
 
 
 
faea3aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2fa05eb
 
 
 
 
 
faea3aa
2fa05eb
faea3aa
2fa05eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
faea3aa
 
 
 
 
 
 
 
2fa05eb
 
 
faea3aa
2fa05eb
faea3aa
 
 
 
 
2fa05eb
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
---
library_name: pytorch
license: apache-2.0
tags:
- world-model
- diffusion
- atari
- space-invaders
- video-generation
model-index:
- name: space-invaders-world-model
  results:
  - task:
      type: other
      name: Next-frame rollout
    dataset:
      name: Space Invaders, held-out random play
      type: ale/space-invaders
    metrics:
    - type: psnr_changed_1
      value: 18.17
      name: PSNR on changed pixels at 1 frames
    - type: psnr_changed_5
      value: 18.35
      name: PSNR on changed pixels at 5 frames
    - type: psnr_changed_15
      value: 16.64
      name: PSNR on changed pixels at 15 frames
---

# space-invaders-world-model

A diffusion world model of Atari Space Invaders, trained from scratch. Given the last 4 frames and actions, a small U-Net denoises the next 64×64 frame; fed its own frames, it generates a video that your actions steer. After [DIAMOND](https://arxiv.org/abs/2405.12399). `atari.py` in this repo collects the data, trains, plays and doubles as the loader.

![Real frames (left) and the model's rollout from the same start and actions (right)](results/rollout.gif)

Left: real game. Right: the model, from the same four starting frames and the same actions, never seeing a real frame again.

## 🚀 Usage

Play it in the browser. Each button press generates one frame:

```
uv run "$(hf download jgalego/space-invaders-world-model atari.py --quiet)" play --game SpaceInvaders
```

From Python, with `atari.py` on the path:

```python
import torch
from atari import DEVICE, collect, load, rollout, to_float

model = load("jgalego/space-invaders-world-model")
frames, _, _ = collect(4, seed=0, game="SpaceInvaders")  # four real frames to start from
context = to_float(frames[:4])[None].to(DEVICE)
moves = torch.tensor([[0, 0, 0] + [1] * 16], device=DEVICE)  # FIRE for 16 frames
video = rollout(model, context, moves)  # (1, 16, 3, 64, 64) in [-1, 1]
```

Actions: 0 NOOP, 1 FIRE, 2 RIGHT, 3 LEFT, 4 RIGHTFIRE, 5 LEFTFIRE.

## 🏋️ Training

| | |
|---|---|
| Model | U-Net, channels 64, 128, 256, 4.2M parameters; noise level and the last 4 actions modulate every block |
| Diffusion | EDM preconditioning, σ_data 0.5, log-normal training noise; 3 Euler steps per frame from σ=5.0 |
| Data | 200,000 frames (382 episodes) of a random policy that holds each uniformly random action for 1 to 6 steps; frameskip 4, no sticky actions |
| Steps | 50,000, batch size 64 |
| Optimizer | AdamW, lr 0.0001, gradient clipping 1.0 |
| Hardware | NVIDIA A10G, 55.9 min |

## 📊 Results

Rollouts on 256 windows from episodes whose seeds never appear in training. The model gets four real frames and the real actions, then only its own frames. **Repeat** keeps showing the last real frame. Much of an Atari frame never changes, so **changed pixels** scores only the pixels where the real or the predicted frame differs from the last real frame.

| Frames ahead | Model, all pixels (dB) | Repeat, all pixels (dB) | Model, changed pixels (dB) | Repeat, changed pixels (dB) |
|---|---|---|---|---|
| 1 | 35.43 | 36.56 | 18.17 | 16.0 |
| 5 | 31.07 | 30.94 | 18.35 | 15.9 |
| 15 | 27.33 | 26.77 | 16.64 | 14.54 |

## ⚠️ Limitations

- A random policy does not get far, so the model knows the early game best.
- 64×64 frames: small sprites are a pixel or two and can blur or vanish over long rollouts.
- Errors compound: every generated frame becomes input for the next.