doom-world-model / README.md
jgalego's picture
Upload folder using huggingface_hub
ea0bbe3 verified
|
Raw History Blame Contribute Delete
4.96 kB
---
library_name: pytorch
license: apache-2.0
tags:
- world-model
- diffusion
- doom
- vizdoom
- video-generation
model-index:
- name: doom-world-model
results:
- task:
type: other
name: Next-frame rollout
dataset:
name: Freedoom E1M1, held-out random play
type: vizdoom/freedoom1-e1m1
metrics:
- type: psnr_changed_1
value: 19.77
name: PSNR on changed pixels at 1 frames
- type: psnr_changed_5
value: 16.62
name: PSNR on changed pixels at 5 frames
- type: psnr_changed_15
value: 13.53
name: PSNR on changed pixels at 15 frames
---
# doom-world-model
A diffusion world model of Doom, trained from scratch on the first level of [Freedoom](https://freedoom.github.io/) (E1M1) in [ViZDoom](https://github.com/Farama-Foundation/ViZDoom). Given the last 4 frames and actions, a small U-Net denoises the next 128×96 frame; fed its own frames, it generates the level as you walk, turn and shoot through it. There is no game engine at play time, only the model. After [DIAMOND](https://arxiv.org/abs/2405.12399) and [GameNGen](https://arxiv.org/abs/2408.14837), at a much smaller scale. `doom.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 streams four generated frames, about half a second of game time:
```
uv run "$(hf download jgalego/doom-world-model doom.py --quiet)" play
```
From Python, with `doom.py` on the path:
```python
import torch
from doom import DEVICE, collect, load, rollout, to_float
model = load("jgalego/doom-world-model")
size = (model.config.width, model.config.height)
frames, _, _ = collect(4, seed=0, size=size) # four real frames to start from
context = to_float(frames[:4])[None].to(DEVICE)
moves = torch.tensor([[0, 0, 0] + [1] * 10 + [3] * 6], device=DEVICE) # FORWARD, then LEFT
video = rollout(model, context, moves) # (1, 16, 3, height, width) in [-1, 1]
```
Actions: 0 NOOP, 1 FORWARD, 2 BACKWARD, 3 LEFT (turn), 4 RIGHT (turn), 5 ATTACK, 6 USE (doors, switches). One step is 4 game tics, about 0.11 s.
## 🏋️ Training
| | |
|---|---|
| Model | U-Net, channels 64, 128, 256, 256, 7.5M parameters; noise level and the last 4 actions modulate every block |
| Diffusion | EDM preconditioning, σ_data 0.5, log-normal training noise (log σ mean 0.4, std 1.2); 3 Euler steps per frame from σ=5.0 |
| Data | 200,000 frames (217 episodes of up to two minutes) of a random policy on Freedoom E1M1: it holds each action for 1 to 6 steps, mostly walks forward, and turns away or tries USE when the depth buffer shows a wall ahead |
| Steps | 5,000, batch size 64 |
| Optimizer | AdamW, lr 0.0001, 1000 warmup steps, cosine decay, gradient clipping 1.0; weights are an EMA with decay 0.999 |
| Hardware | NVIDIA A10G, 16.5 min |
## 📊 Results
Rollouts on 256 windows from episodes whose seeds never appear in training; they play the same level. The model gets four real frames and the real actions, then only its own frames. **Repeat** keeps showing the last real frame. **Changed pixels** scores only the pixels where the real or the predicted frame differs from the last real frame, so the HUD and still views do not dominate.
| Frames ahead | Model, all pixels (dB) | Repeat, all pixels (dB) | Model, changed pixels (dB) | Repeat, changed pixels (dB) |
|---|---|---|---|---|
| 1 | 22.36 | 21.29 | 19.77 | 16.2 |
| 5 | 17.55 | 19.2 | 16.62 | 15.5 |
| 15 | 14.01 | 18.16 | 13.53 | 15.09 |
## ⚠️ Limitations
- **Rollouts collapse.** From four real frames, the first generated frame is already a brown haze and by the fifth only a grey wall, the gun and a breaking HUD remain (see the GIF). It still beats repeating the last frame on PSNR up to 5 frames ahead, because a grey average scores well on PSNR. The model learned to denoise frames but not to predict the next one from context.
- **Training longer makes it worse.** Held-out rollout quality peaks after about 4,000 steps and then declines while the training loss keeps falling. A 50,000-step run, an EMA of the weights, fp32 instead of bf16, warmup with cosine decay and 64×48 frames did not change that.
- One level, seen through random play: the model knows the parts of E1M1 a random walker reaches, not the whole map, and nothing of other levels.
- Four frames of memory, under half a second: walk out of a room and back and it may not be the same room.
- 128×96 frames, three denoising steps: textures blur and enemies smear, more so the longer it runs.
- Errors compound: every generated frame becomes input for the next.
Freedoom assets are under the [BSD 3-clause license](https://github.com/freedoom/freedoom/blob/master/COPYING.adoc).