File size: 18,262 Bytes
a91b194
 
 
 
 
 
 
 
 
 
 
 
acbd4c5
a91b194
 
 
 
 
 
 
acbd4c5
 
92423f0
acbd4c5
e1ecae1
acbd4c5
e1ecae1
acbd4c5
92423f0
a91b194
 
 
acbd4c5
a91b194
92423f0
 
a91b194
92423f0
a91b194
e1ecae1
92423f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e1ecae1
92423f0
 
 
acbd4c5
 
 
 
a91b194
92423f0
a91b194
acbd4c5
 
92423f0
acbd4c5
92423f0
a91b194
 
 
acbd4c5
a91b194
acbd4c5
a91b194
acbd4c5
 
 
 
 
 
 
 
92423f0
acbd4c5
 
 
 
 
 
a91b194
acbd4c5
a91b194
92423f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acbd4c5
a91b194
acbd4c5
a91b194
acbd4c5
a91b194
92423f0
a91b194
acbd4c5
a91b194
92423f0
a91b194
acbd4c5
a91b194
acbd4c5
a91b194
acbd4c5
a91b194
92423f0
a91b194
acbd4c5
a91b194
acbd4c5
a91b194
acbd4c5
a91b194
acbd4c5
a91b194
acbd4c5
a91b194
acbd4c5
a91b194
92423f0
a91b194
92423f0
 
 
 
 
 
 
 
 
a91b194
92423f0
a91b194
 
acbd4c5
 
 
a91b194
 
e1ecae1
a91b194
92423f0
a91b194
92423f0
 
 
a91b194
92423f0
a91b194
92423f0
acbd4c5
92423f0
 
acbd4c5
92423f0
acbd4c5
92423f0
acbd4c5
92423f0
 
 
 
 
acbd4c5
92423f0
acbd4c5
92423f0
acbd4c5
92423f0
e1ecae1
acbd4c5
e1ecae1
a91b194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acbd4c5
 
a91b194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e1ecae1
a91b194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385cc9f
a91b194
 
 
 
 
 
 
 
 
 
 
 
acbd4c5
a91b194
acbd4c5
a91b194
 
acbd4c5
 
 
a91b194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
acbd4c5
 
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
---
title: DQN Maze Pathfinding Demo
emoji: 🧩
colorFrom: blue
colorTo: purple
sdk: docker
pinned: false
license: mit
---

# RL Maze Navigator

### 4 种 DQN 变体在随机 10×10 迷宫上的系统性消融实验 · SPL 评估 · Holdout 成功率 84%

[![CI](https://github.com/Lee93whut/rl-maze/actions/workflows/test.yml/badge.svg)](https://github.com/Lee93whut/rl-maze/actions/workflows/test.yml)
[![Python](https://img.shields.io/badge/python-3.10-blue)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

**在线 Demo**:[Hugging Face Spaces](https://huggingface.co/spaces/lil58/interview) · **代码**:[GitHub](https://github.com/Lee93whut/rl-maze) · **实验记录**:[docs/experiment_log.md](docs/experiment_log.md)

---

## 项目背景

10×10 随机迷宫本身足够简单(原始 DQN 在固定起终点可达 90%+),但**随机起终点 + 随机地图**让状态空间急剧扩大,把"走通迷宫"变成"学一种导航策略"——初版 DQN 在此设定下仅 **61% 成功率**。

本项目的目标不是再提升一两个百分点,而是**走完一遍完整的方法论**:先做冒烟验证排除工程 bug,再做 4 轮超参消融(每轮只改一组变量,用问题诊断驱动下一轮变更),超参调优至可接受的成功率后固定下来,最后做四算法横向消融。整个过程把 Holdout 成功率从 61% 推到 84%(+23pp:超参调优 +17pp,算法切换 +6pp)。

途中还发现并修复了一类常见的 RL 工程错误——**奖励依赖 episode 内历史,违反马尔可夫性**(详见"核心发现")。

---

## 最终结果

> **Holdout 评估**:100 张训练中从未见过的独立地图(seed+200000),ε=0 贪心推理,裸 argmax。
> ⚠️ n=100 单次跑点,95% CI ≈ ±5pp;相邻算法差距 3–6pp 处于 CI 边缘,严格对比需多次独立运行(Henderson et al., 2018)。

### 训练结果呈现顺序

本节按"训练过程的时间顺序"组织:先看超参演进的纵向消融(同一算法、固定数据集、不同超参),再看 R4 算法横向消融(固定最优超参、不同算法)。

#### 第一阶段:超参演进纵向对比(Double DQN,保持算法一致)

每轮变更都不是"再调一组数字试试",而是**先看曲线、定位问题、再改超参**:

| 轮次 | 核心变更 | Holdout | SPL | 上一轮 → 本轮:问题诊断与决策 |
|------|---------|:-------:|:---:|--------------------------|
| Round 1 | 初版超参(`ep=2000`, `decay=0.995`) | 61.0% | 0.605 | 起点:随机起终点初次尝试 |
| Round 2 | `ep=6000`(×3), `decay=0.9985` | 64.0% | 0.633 | R1 训练曲线 ep≈800 即触底、ep≈2000 仍未收敛 → 训练量不足 + 探索衰减过快 |
| Round 3 | `buffer=80k`(×4), `target=1500`(×3) | 74.0% | 0.735 | R2 EVAL 出现 400–500 ep 周期的剧烈振荡 → buffer 太小、target 更新过频(Q 目标漂移) |
| **Round 4** | EVAL checkpoint + BFS 连通性 + visited_map 4通道 | **78.0%** | **0.773** | R3 EVAL 峰值 84% / Holdout 74% 差 10pp → checkpoint 时序错位(保存的是训练奖励最高的而非 EVAL 最佳的);训练出现"循环"失败模式 → 修复 checkpoint 策略 + BFS 保证训练数据无死锁 + visited_map 状态编码抑制循环 |

四轮演进曲线(同一 Double DQN 算法,仅超参不同):

![R1→R4 超参演进纵向对比(Double DQN)](docs/assets/compare/cmp_eval_success_rate_r1_to_r4_double.png)

**这一阶段说明**:在算法不变的前提下,靠"消融 + 诊断"已经把成功率从 61% 推到 78%。接下来的 6pp 提升不能继续靠调超参——超参空间已饱和,曲线形状说明 R4 已接近该算法的能力上限。

#### 第二阶段:R4 算法横向消融(固定 R4 最优超参,唯一变量 = 算法)

R4 超参调优至可接受准确率后,换算法就是"把相同的训练数据 + 同样的训练步数给四种网络架构,看谁能在 Holdout 上多拿 6pp":

| 排名 | 算法 | Holdout | SPL | EVAL→Holdout Gap |
|:---:|------|:-------:|:---:|:----------------:|
| 🥇 | **Dueling DQN** | **84.0%** | **0.817** | −6pp(泛化最稳) |
| 🥈 | Double + Dueling | 81.0% | 0.793 | −9pp |
| 🥉 | Double DQN | 78.0% | 0.773 | −10pp |
| 4️⃣ | Vanilla DQN | 75.0% | 0.726 | −19pp |

四算法 EVAL 训练曲线:

![R4 四算法 EVAL 成功率对比](docs/assets/round4/r4_eval_success_rate_all_algos.png)

**这一阶段说明**:Dueling 比 Vanilla 在 Holdout 上多 9pp、EVAL→Holdout Gap 小 13pp——这是结构性的,不是统计噪声。Dueling 适配本任务的具体机制见"核心发现"。

---

---

## 实验方法论

本项目遵循 Henderson et al. (2018) 的 RL 实验规范,分三个阶段执行:

```
阶段一:冒烟验证(Round 0)
    固定起终点,四算法均达 90%+
    → 验证训练流程无系统性 bug,建立对照组

阶段二:超参消融(Round 1–4)
    每轮只改一组变量,用问题诊断驱动下一轮变更
    R1 → R2:修复训练量 + 探索衰减(单变量)
    R2 → R3:修复 buffer + target(Q 目标漂移)
    R3 → R4:修复 checkpoint 时序 + 连通性验证 + 状态编码

阶段三:算法横评(Round 4 续)
    固定 R4 最优超参,四算法串行训练
    唯一变量 = 网络架构与 Q 目标计算方式
```

**为什么先调超参再比算法**:先把超参调到最优,比的才是算法本身的能力差异;否则比的是"哪个算法对糟糕超参更鲁棒"。

### 三个评价指标在不同阶段的作用

本项目主要用三个指标,每个指标在不同阶段回答不同的问题:

| 指标 | 表征什么 | 主要用在哪一阶段 | 怎么驱动决策 |
|------|---------|----------------|------------|
| **训练奖励(Frontend_Env/Episode_Reward)** | 当前策略在训练地图上的即时表现 | 阶段二(超参消融)粗筛 | 看是否"在涨"——R1 ep≈800 触底 → 触发"R2 延长训练"的决策 |
| **EVAL 成功率(Evaluation_Exam/Test_Success_Rate)** | 当前策略在固定 EVAL 集上的泛化表现 | 阶段二精筛 + 阶段三对比 + 阶段一/二的 checkpoint 选择 | R2 出现 400–500 ep 振荡 → 触发"R3 调 buffer/target";R4 峰值 88% 但 Dueling 末段 90% → 触发"阶段三切 Dueling" |
| **Holdout 成功率 + SPL** | 训练全程未见的 100 张独立地图上的最终泛化能力 | 仅阶段三最终一次性报告 | 不参与中途决策(防止"对着 Holdout 调超参"导致的间接过拟合),只用于回答"项目最终能拿多少分" |

**三套指标配合使用的关键约束**:
- **阶段一看训练奖励就够了**——固定起终点下,奖励能涨就说明训练代码无 bug
- **阶段二必须看 EVAL 成功率**——训练奖励在随机地图下噪声太大(与泛化能力相关性仅 0.3–0.5),用它调超参会选出"训练集噪声"模型
- **阶段三对比算法时同时看 EVAL 峰值 + Holdout + Gap**——EVAL 峰值反映"训练过程见过的最佳泛化",Holdout 反映"训练过程完全没见过的泛化",两者差距(Gap)反映模型稳定程度
- **Holdout 不允许中途观察**——一旦中途用 Holdout 调参,就等于把 Holdout 间接泄露进训练过程,最终数字就不可信

完整逐轮记录见 [docs/experiment_log.md](docs/experiment_log.md)。

---

## 核心发现

### 发现一:奖励依赖 episode 内历史会破坏马尔可夫性(P9)

R4 首先尝试用 `revisit_penalty`(重复访问格子施加递进奖励惩罚)抑制推理时的循环行为,但训练至 ep=1000 时成功率仅 38%,持续低于基线 15pp,最终终止。

**根因**:Q-learning 的收敛定理(Watkins & Dayan, 1992)要求奖励函数 $r(s,a,s')$ 仅依赖当前转移,贝尔曼最优算子在此条件下是压缩映射。`revisit_penalty` 使奖励依赖 episode 内访问历史(隐变量),相同的 $(s,a,s')$ 在 episode 内的不同时刻返回不同奖励,破坏马尔可夫性,**收敛性保证失效**——这不意味着"必然不收敛",而是理论不再保证收敛,实际行为取决于具体问题结构。**本任务中真正致命的次生问题是训练/推理分布不一致**——训练时奖励含惩罚项,推理时不含,导致训练期学到的 Q 函数与推理期使用的环境动力学失配,策略崩溃不可修正。

**正确解法**:把历史信息从奖励空间移到状态空间。观测张量第四通道编码二值访问图(ch3=visited_map),Q 函数合法学习"已访问格价值低"的策略,马尔可夫性完整保持。

> 这一诊断具有通用性:**任何依赖 episode 内历史的奖励项都是这类错误的变体**,正确方向永远是状态编码。

### 发现二:Dueling 架构与本任务的结构适配性

随机起终点迷宫中存在大量"多动作等效"状态(死胡同、走廊段),Dueling 的 V(s)/A(s,a) 分解使价值流与优势流解耦——V(s) 通过所有动作路径的反向传播获得梯度信号,样本效率上等价于用同一个状态 batch 下的所有动作共同监督 V(s) 的估计,而每个 A(s,a) 仅在该动作对应路径上获得梯度,因此 V(s) 的有效更新样本量是 A(s,a) 分支的 `num_actions` 倍(4 个动作时为 4×),估计更稳定,泛化更强。

实测证据:EVAL→Holdout Gap 仅 6pp(最小),而 Vanilla DQN 的 Gap 高达 19pp。

### 发现三:checkpoint 保存策略是系统性问题

R3 用训练滚动奖励触发 checkpoint,但训练奖励受随机地图难度影响,与泛化能力相关性弱(约 0.3–0.5)。实测 EVAL 峰值 84%、Holdout 仅 74%,差距 10pp 全部来自时序错位。

改为 EVAL-based checkpoint(每次评估若成功率创新高则保存)后,Holdout 直接对应训练过程出现过的最佳泛化能力,R4 Holdout 回升至 78%(+4pp)。

---

## 项目亮点

### 1. 训练-评估-报告三集严格分离 + EVAL-based Checkpoint

```
训练 buffer(Replay)── 梯度更新用,分布漂移无所谓


   EVAL 集(固定 100 张地图,固定 seed)── 训练期每 N 局评估一次,触发 checkpoint


 Holdout 集(独立 100 张地图,seed+200000)── 训练全程不接触,仅最终一次性报告
```

Checkpoint 触发:EVAL 集成功率创新高时保存,确保 checkpoint 永远对应"训练过程中 EVAL 表现最好的一刻"——而不是训练奖励偶然高的一刻。

```python
if eval_success_rate > best_eval_success:
    best_eval_success = eval_success_rate
    torch.save({"state_dict": policy_net.state_dict(), ...}, best_model_path)
```

**直接效果**:避免 R3 的 EVAL 峰值 84% / Holdout 仅 74% 的 10pp 错位,R4 Holdout 回升至 78%(+4pp)。

### 2. 训练信号侧的"三项叠加"(ep/buffer/target 调优 + BFS 连通性)

- `ep=6000` + `decay=0.9985`:训练量翻 3 倍、ε 衰减 50× 慢,给策略充分时间收敛
- `buffer=80k`(×4)、`target_update=1500`(×3):buffer 太小会让成功样本被冲掉、target 更新太频会让 Q 目标变成"移动靶",两者都是 R2 振荡的直接原因
- BFS 连通性保证:`reset()` 内嵌 BFS,无解迷宫直接重采样——不修这个,训练信号会被"无法完成任务"污染,Q 值学出"迷宫无解"的错误估计

**效果**:仅这三项把成功率从 61% 推到 74%(+13pp)。

### 3. Episode 级 Warmup + BFS Ground Truth

- Warmup:前 N 局 ε=1.0 纯随机,不做任何梯度更新——防止早期 Q 值被少量样本"冲偏"
- BFS Ground Truth:每次评估计算当前迷宫的最短路径步数作为 SPL 的 $\ell^*$——不依赖 Q 值估计,避免"用 Q 值评估 Q 值"的循环

**作用**:提效(减少无效探索 + 评估指标无偏)。

### 4. TensorBoard 三解耦看板

| 看板 | X 轴 | 记录时机 | 避免的问题 |
|---|---|---|---|
| `Backend_Net/` | `global_update_steps` | 每次 `backward()` 后 | — |
| `Frontend_Env/` | `episode` | 每局结束后 | 避免"每局更新步数不同"导致 Loss 曲线横向压缩 |
| `Evaluation_Exam/` | `episode` | 每 N 局暂停训练、`model.eval()` | 训练/评估指标分轴对比,避免视觉误导 |

### 5. 唯一随机源 + 评估集可复现

`maze_env/env.py` 所有随机操作统一走 Gymnasium 注入的 `self.np_random`。

- 训练时 `env.reset()` 不传 seed → 训练地图每局不同,强制学泛化
- 评估时 `env.reset(seed=X)` 固定 → 同一 seed 永远生成同一张地图,Holdout 100 张地图可精确复现

**作用**:可复现性是评估结果具备可对比性的前提。

---

## 技术架构

```
config.yaml


src/train.py ──────────────────────────────────────────────────────┐
    │  读取超参数                                                    │
    ├── MazeEnv (maze_env/env.py)   Gymnasium 标准环境接口           │
    │       ├── generator.py        随机迷宫生成 + BFS 连通性校验     │
    │       ├── actions.py          Action 枚举 + DELTAS 方向向量     │
    │       └── renderer.py         ASCII 渲染器                     │
    ├── DQNNetwork / DuelingDQNNetwork (src/model.py)               │
    ├── ReplayBuffer (src/replay_buffer.py)                         │
    └── maze_env/bfs.py             BFS 最短路径 / SPL Ground Truth  │

app.py (Streamlit Web Demo)  ◄──── results/*.pth (训练权重) ◄───────┘
    ├── MazeEnv                     迷宫生成与状态渲染
    ├── DQNNetwork / DuelingDQNNetwork  加载权重执行推理
    ├── bfs.py                      可视化对比最短路径
    └── Plotly go.Heatmap           交互式迷宫可视化
```

> **注**:`app.py` 推理时叠加了计数惩罚兜底(访问次数 ≥ 2 时对高频格施加递进 Q 值惩罚),仅影响 Demo 视觉体验。所有 Holdout/EVAL 数字均来自 `run_evaluation()`,使用裸 argmax,不受此影响。根本解法是将 ch3 改为归一化计数图重新训练,因时间限制未实施。

---

## 快速开始

```bash
git clone https://github.com/Lee93whut/rl-maze.git && cd rl-maze

# 安装(maze_env 包 + 训练依赖)
pip install -e ".[dev]"
pip install -r requirements.txt

# 下载训练权重(从 HF Spaces,约 4×4MB)
python scripts/download_weights.py

# 启动 Web Demo(本地)
streamlit run app.py

# Docker 本地验证
docker build -t maze-dqn-demo .
docker run --rm -p 7860:7860 maze-dqn-demo
```

> 权重文件统一存放于 [HF Spaces lil58/interview](https://huggingface.co/spaces/lil58/interview)。
> `download_weights.py` 会跳过已存在的文件,重复执行安全。

```bash
# 从头自行训练(无需下载权重)
python src/train.py --config config.yaml

# 调试模式(5×5 迷宫,快速验证代码正确性)
python src/train.py --config config.yaml --overfit

# 查看训练曲线
tensorboard --logdir runs/

# 生成算法对比报告
python src/report.py
```

---

## 项目结构

```
rl-maze/
├── app.py                      Web Demo 主程序(Streamlit + Plotly)
├── config.yaml                 训练与环境超参数配置
├── pyproject.toml              包元数据 + pytest/coverage 配置
├── requirements.txt            运行时依赖
├── Dockerfile                  Hugging Face Spaces Docker SDK 部署配置

├── src/
│   ├── train.py                DQN 训练主循环(Warmup + 三看板 + 盲测评估)
│   ├── model.py                DQNNetwork / DuelingDQNNetwork(3-Conv + 2-FC)
│   ├── replay_buffer.py        环形经验回放池
│   └── report.py               四算法 Holdout 横向对比报告生成器

├── maze_env/
│   ├── __init__.py             包入口(MazeEnv / Action / bfs)
│   ├── env.py                  Gymnasium 标准环境(连通性保证、唯一随机源)
│   ├── bfs.py                  BFS 最短路径算法
│   ├── generator.py            随机迷宫生成器
│   ├── actions.py              Action 枚举与 DELTAS 方向向量
│   └── renderer.py             ASCII 终端渲染器

├── docs/
│   ├── experiment_log.md       逐轮训练记录(配置 + 结果 + 诊断 + 后续优化项)
│   ├── hyperparameter_study.md 超参分析报告(含论文依据)
│   ├── technical_report.md     技术报告(算法原理 + 评估指标 + 结果分析)
│   └── assets/                 训练曲线截图(Round 1/2/3 + 对比图)

├── reports/
│   └── comparison.md           R4 四算法最终对比报告(含 SPL 共线性分析)

└── tests/                      pytest 测试套件(90%+ 覆盖率)
    ├── test_00_gymnasium_check.py
    ├── test_01_spaces.py ~ test_09_validation.py
    ├── test_bfs.py
    ├── test_model.py
    ├── test_replay_buffer.py
    └── test_train.py
```

---

## 参考文献

1. Mnih et al. (2015). *Human-level control through deep reinforcement learning*. **Nature**.
2. van Hasselt, Guez & Silver (2016). *Deep Reinforcement Learning with Double Q-learning*. **AAAI**.
3. Wang et al. (2016). *Dueling Network Architectures for Deep Reinforcement Learning*. **ICML**.
4. Ng et al. (1999). *Policy invariance under reward transformations*. **ICML**.
5. Anderson et al. (2018). *On Evaluation of Embodied Navigation Agents*. arXiv:1807.06757.
6. Henderson et al. (2018). *Deep Reinforcement Learning that Matters*. **AAAI**.
7. Lin (1992). *Self-improving reactive agents based on reinforcement learning, planning and teaching*. **Machine Learning**, 8(3–4).
8. Schaul et al. (2016). *Prioritized Experience Replay*. **ICLR**.