ML-For-Beginners Q-Learning 实战:为「Peter and the Wolf」构建含能量与疲劳机制的更真实世界
本篇技术指南基于 ML-For-Beginners 课程 8-Reinforcement 章节第 1 课的课后挑战作业 assignment.es.md(西语版,对应英文版 assignment.md)展开。该作业要求在 Q-Learning 原环境基础上引入能量(energy)与疲劳(fatigue)机制,重写奖励函数并用强化学习训练出能击败狼的最优策略,再与随机游走基线做胜率对比。读完后,你将掌握:如何扩展 RL 环境的状态定义、如何围绕「稀有成功事件」设计奖励与超参数,以及如何用仓库自带的源码级参考解答对照验证自己的实现。
1. 挑战背景:从「找到苹果」到「打败狼」
课程第 1 课 notebook.ipynb 中,Peter 在一个 width x height 的棋盘上移动,目标是找到苹果、避开狼与水域。原始版本里 Peter 几乎「不知疲倦、不会饥饿」——移动本身不消耗任何资源,奖励函数只有三档(来自 README.md):
move_reward = -0.1
goal_reward = 10
end_reward = -10
def reward(m, pos=None):
pos = pos or m.human
if not m.is_valid(pos):
return end_reward
x = m.at(pos)
if x == Board.Cell.water or x == Board.Cell.wolf:
return end_reward
if x == Board.Cell.apple:
return goal_reward
return move_reward
而本作业要求把世界改造成「更真实」的版本,Peter 必须管理体能才能完成真正的目标——找到并杀死狼。作业给出的五条世界规则(完整继承自原文档)是:
- 移动即消耗:从一处移动到另一处,Peter 会损失 能量(energy) 并获得一些 疲劳(fatigue);
- 吃苹果回能量:Peter 可以通过吃苹果获得更多能量;
- 在树下或草地上休息:走进棋盘上的树(tree)或草地(grass,即绿色方块)格子,即可消除疲劳;
- 核心目标改变:Peter 需要找到并杀死狼;
- 战斗有门槛:要杀死狼,Peter 必须保持一定的能量与疲劳水平,否则战斗失败(被狼击败)。
注意规则 3 与原环境语义的变化:在原课程中 tree/草地只是可通行的休息地标记,在新规则中它承担了「重置疲劳」的机制功能;apple 从「游戏终点」变成了「补给品」;wolf 从「必须避开的死区」变成了「可以攻击的目标」。
2. 原环境源码解读:改造前必须先读懂 rlboard.py
作业明确指出:以原始 notebook.ipynb 为起点。而 Notebook 的环境逻辑全部封装在 rlboard.py 的 Board 类中。改造前,先看清这个类的关键成员(以下均引自 rlboard.py):
-
格子类型常量(rlboard.py#L44-L49):
class Cell: empty = 0 water = 1 wolf = 2 tree = 3 apple = 4这五个取值正是新规则的挂载点:
tree(3)用于恢复疲劳,apple(4)用于恢复能量,wolf(2)从「失败态」变为「结算战斗的终局态」。 -
随机生成棋盘(rlboard.py#L67-L97):
randomize(water_size=5, num_water=3, num_wolves=1, num_trees=5, num_apples=3, seed=None)会先随机游走生成水域,再在空地上放置树、狼、苹果。seed参数用于复现实验,作业中的参考解答使用seed=13。 -
移动与合法性:
is_valid(pos)(rlboard.py#L105-L106)判断坐标是否越界;move(dpos, check_correctness=True)(rlboard.py#L111-L114)把self.human更新为新位置,并支持check_correctness=False允许走出棋盘以终止回合。 -
内置
walk循环(rlboard.py#L174-L195):接受一个policy函数,随机起点出发,碰到apple返回步数(成功)、碰到wolf/water返回-1(失败)。新规则下成功/失败条件全部改变,因此需要重写这个循环(见第 5 节),而不能直接复用Board.walk。 -
可视化:
image(Q=None)/plot(Q=None)(rlboard.py#L129-L169)能把 Q-Table 以「指向最优方向的箭头」形式画到棋盘上,训练前后各画一次即可直观对比策略变化。
适用前提:
Board.__init__会通过imload读取images/wolf.png、images/apple.png、images/human.png(rlboard.py#L59-L63),并依赖numpy、matplotlib、opencv-python。若从云环境打开 Notebook,务必把rlboard.py放到与 Notebook 同目录(README.md 中也有此提示)。
3. 状态建模:作业给出的三种实现路线
原文档在第一条 Note 中给出了三种合法的状态表示方案(这是本作业设计上的核心自由度):
| 方案 | 做法 | 特点 |
|---|---|---|
| 元组 | 把状态表示为 (Board, energy, fatigue) 三元组 |
改动最小,但函数签名和索引逻辑会比较啰嗦 |
| 独立状态类 | 定义一个 state 类携带棋盘 + 能量 + 疲劳(作业提示也可以让它继承自 Board) |
仓库参考解答采用的方案,语义最清晰 |
改造 Board |
直接修改 rlboard.py 中的 Board 类,把 energy/fatigue 塞进棋盘对象 |
改动集中,但污染了原环境模块 |
仓库官方参考解答 solution/assignment-solution.ipynb 选择了第二种路线,定义如下(原样摘自该 Notebook):
class state:
def __init__(self, board, energy=10, fatigue=0, init=True):
self.board = board
self.energy = energy
self.fatigue = fatigue
self.dead = False
if init:
self.board.random_start() # 随机起点
self.update()
def at(self):
return self.board.at()
def update(self):
if self.at() == Board.Cell.water:
self.dead = True
return
if self.at() == Board.Cell.tree:
self.fatigue = 0 # 规则 3:树下休息消除疲劳
if self.at() == Board.Cell.apple:
self.energy = 10 # 规则 2:吃苹果恢复能量
def move(self, a):
self.board.move(a)
self.energy -= 1 # 规则 1:移动消耗能量
self.fatigue += 1 # 规则 1:移动增加疲劳
self.update()
def is_winning(self):
return self.energy > self.fatigue # 规则 5:能量须高于疲劳才能取胜
这段代码把五条世界规则逐条映射到了具体数值:每走一步 energy -= 1, fatigue += 1;踩到树 fatigue 归零;踩到苹果 energy 重置为初始值 10;落水则置 dead;而「能否杀死狼」的判定从 is_winning() 可见,参考实现采用 energy > fatigue 作为战斗胜利条件——这就是对作业规则 5「certain levels of energy and fatigue」的一种具体化。你可以自行更换该阈值(例如要求 energy > 8 且 fatigue < 2),这会直接影响训练难度。
从源码结构看,参考解答的 Q-Table 仍然只以棋盘坐标 (x, y) 为键(第 6 节会看到),即把能量与疲劳放进了奖励信号而非状态索引中。如果你严格按 Note 中「状态包含能量与疲劳」的思路,也可以把 Q-Table 扩展为 (width, height, energy_levels, fatigue_levels, len(actions)) 的多维数组——状态空间会更准确,但维度增长会带来训练时间代价,这正是作业暗示「训练时间会更长」的另一个原因。
4. 重写奖励函数:把「打到狼」变成真正的目标
作业的核心指令是「Modify the reward function according to the rules of the game」。原奖励函数以「踩到苹果 = +10」为终点,新世界里苹果只是补给、狼才是结算点。参考解答的奖励函数(摘自 solution/assignment-solution.ipynb):
def reward(s):
r = s.energy - s.fatigue
if s.at() == Board.Cell.wolf:
return 100 if s.is_winning() else -100 # 规则 4/5:战斗结算 ±100
if s.at() == Board.Cell.water:
return -100
return r
它包含两层设计,值得逐点拆解:
- 终局大额奖励:与狼相遇时,按
is_winning()返回+100(杀狼成功)或-100(战斗失败),落水同样是-100。这沿用了原课程「实质奖励只在游戏末端给出」的 RL 设定(README.md 的 Reward function 一节),要求算法自己去「记住」哪些中间步骤最终导向了胜局。 - 过程性塑形(reward shaping):非终局状态下返回
energy - fatigue,即「体能净值」。这使 Q-Table 在训练早期就能区分「健康前进」与「透支前进」,相当于给稀疏的终局奖励补了一条连续梯度。
设计权衡:塑形奖励如果与终局奖励量级失衡,会诱导 Agent「永远躲在树下」这类捷径行为。实现时可以用「杀狼次数 / 死亡次数」的统计(第 7 节)来检验奖励设计是否偏航。
5. 重写 walk 循环:新规则的胜负结算
原 walk 以「碰到苹果=成功,碰到狼/水=失败」结算,新世界里结算条件完全不同。参考解答的改写(摘自 solution/assignment-solution.ipynb,保留随机游走策略 random_policy 以便对比):
def random_policy(state):
return random.choice(list(actions))
def walk(board, policy):
n = 0 # number of steps
s = state(board)
while True:
if s.at() == Board.Cell.wolf:
if s.is_winning():
return n # 成功:杀死狼,返回正步数
else:
return -n # 失败:打不过狼,返回负步数
if s.at() == Board.Cell.water:
return 0 # 溺水:单独计数
a = actions[policy(m)] # 注意:参考解答此处引用了 notebook 全局棋盘 m
s.move(a)
n += 1
walk(m, random_policy)
返回值约定被重定义为三档:正数 = 杀狼成功(步数),负数 = 与狼交手但战败,0 = 溺水死亡。这个约定直接服务于作业要求的「compare the results of random walk with your algorithm in terms of number of games won and lost」(按胜负场数对比两种策略)。
6. 运行 Q-Learning:学习循环如何适配新世界
学习算法主体(Bellman 更新、exploit/explore 平衡)与原课程一致,差异集中在三处。参考解答的完整训练循环(摘自 solution/assignment-solution.ipynb):
Q = np.ones((width, height, len(actions)), dtype=np.float) * 1.0 / len(actions)
def probs(v, eps=1e-4):
v = v - v.min() + eps
return v / v.sum()
lpath = []
for epoch in range(10000): # ① 原课程为 5000,这里翻倍
s = state(m) # 每回合从随机起点、初始能量重新开始
n = 0
while True:
x, y = s.board.human
v = probs(Q[x, y])
while True: # 拒绝越界动作,原地重采样
a = random.choices(list(actions), weights=v)[0]
dpos = actions[a]
if s.board.is_valid(s.board.move_pos(s.board.human, dpos)):
break
s.move(dpos)
r = reward(s)
if abs(r) == 100: # ② 终局条件改为「打到狼或落水」
lpath.append(n)
break
alpha = np.exp(-n / 3000) # ③ 学习率按步数指数衰减
gamma = 0.5
ai = action_idx[a]
Q[x, y, ai] = (1 - alpha) * Q[x, y, ai] \
+ alpha * (r + gamma * Q[x + dpos[0], y + dpos[1]].max())
n += 1
与原课程循环(README.md 中的 5000 轮实现)逐条对照,适配点为:
- 状态来源:
x, y取自s.board.human,奖励取自已改造的reward(s),回合生命周期由state对象驱动; - 终止条件:原来是
r == end_reward or cum_reward < -1000,现在简化为abs(r) == 100——因为新奖励函数里只有「战斗结算/落水」才会给出 ±100; - 动作合法性:原实现允许走出棋盘来终止回合(
m.move(dpos, check_correctness=False)),参考解答改为在棋盘内重采样动作直到合法。两者都是可行策略,但语义不同:前者把「走出地图」算作一次失败体验,后者保证每个被更新的 Q 值都对应真实可达转移。 - 超参数:epochs 从 5000 提到 10000,
alpha = exp(-n/3000)按步数衰减,gamma = 0.5保持不变。
训练完成后执行 m.plot(Q) 即可把学到的策略画到棋盘上——参考解答的结论文字写道:溺水案例明显减少,但 Peter 仍不能总是杀死狼,邀请读者继续调节超参数(这正对应作业 Note 中「you may need to adjust the hyperparameters」的提示)。
7. 与随机游走基线对比:按胜负场数统计
作业明确要求「保留随机游走代码并在最后对比」。参考解答的统计函数把第 5 节的三档返回值翻译成胜负计数(摘自 solution/assignment-solution.ipynb):
def print_statistics(policy):
s, w, n = 0, 0, 0
for _ in range(100):
z = walk(m, policy)
if z < 0:
w += 1 # 战败(与狼交手失败)
elif z == 0:
n += 1 # 溺水
else:
s += 1 # 杀狼成功
print(f"Killed by wolf = {w}, won: {s} times, drown: {n} times")
print_statistics(random_policy) # 基线
print_statistics(qpolicy) # Q-Learning 策略
其中 qpolicy 与原课程完全相同——按 Q-Table 概率采样动作(而非 argmax 的 qpolicy_strict,后者在两个状态互相指向最优时会死循环,原课程 Notebook 中有专门说明)。评估时注意两点:
- 每次
walk都会从随机起点、energy=10, fatigue=0的初始状态出发,因此 100 局统计天然覆盖不同地形难度; - 对比口径必须统一为「杀狼成功局数 vs 战败局数 vs 溺水库数」,这正是作业原文「in terms of the number of games won and lost」所指。
8. 超参数调优指南:稀有事件下的训练策略
作业第二条 Note 是关键经验提示:「在新世界里,游戏成功(与狼交战)是一个稀有事件,因此你可能需要调整超参数,尤其是 epoch 数,训练时间会显著变长。」 结合仓库证据,可以给出可操作的调优清单:
| 超参数 | 原课程取值 | 参考解答取值 | 调优方向与依据 |
|---|---|---|---|
| epochs | 5000 | 10000 | 杀狼是稀有事件,需要更多回合才能采样到足够多的 ±100 结算来修正 Q 值 |
| 学习率 α | exp(-n/10e5) |
exp(-n/3000) |
衰减更快。原课程指出路径长度曲线后期的「突增」源于低学习率下 Q-Table 被新值冲坏,加速衰减可缓解(README.md「Investigating the learning process」一节) |
| 折扣因子 γ | 0.5 | 0.5 | 控制「当前体能净值」与「未来杀狼收益」的权衡,调大 γ 会让 Agent 更看重终局奖励 |
| 战斗门槛 | — | energy > fatigue |
收紧(如 energy > 8)会使成功事件更稀有,训练更难;放宽则 Q-Table 收敛更快 |
| 能量/疲劳步长 | — | 每步 ±1 | 调大步长会加速能量耗尽,等价于「缩短回合」,也会影响稀有事件频率 |
此外可以记录每回合步数序列 lpath 并绘图(参考解答末尾 plt.plot(lpath))来观察学习曲线:若杀狼局数长期为零,优先加大 epochs 或放宽战斗门槛;若 Q 值震荡,优先调低 α 衰减速度。原课程还强调了 hyperparameter optimization 的通用概念(超参数 vs 参数的区别),可作为深入学习的入口。
9. 验收标准:三档评分细则
作业附带的 Rubric(完整继承自原文档)定义了交付物应达到的质量档位:
| 档位 | 达标要求 |
|---|---|
| Exemplar(优秀) | 提交了定义新世界规则的 Notebook,包含 Q-Learning 算法与必要的文字解释;Q-Learning 相比随机游走有显著提升 |
| Adequate(合格) | 提交了 Notebook,Q-Learning 已实现并优于随机游走但提升不显著;或 Notebook 文档化不足、代码组织混乱 |
| Needs improvement(待改进) | 对重新定义世界规则做了一些尝试,但 Q-Learning 算法跑不通,或奖励函数定义不完整 |
对照这份 Rubric,一份「优秀」级解答的自检清单是:① 五条世界规则是否全部落地(对照第 3 节 state 类的逐行映射);② 奖励函数是否同时定义了终局结算与过程奖励;③ 是否保留了 random_policy 基线并输出同口径的胜负统计;④ 是否有文字解释训练结果与超参数选择。
10. 仓库中的配套资源与延伸
- 8-Reinforcement/1-QLearning/README.md:完整课程讲义,含随机游走、奖励函数、Bellman 方程、exploit/explore 的推导与全部代码块,是本作业的直接前置知识;
- 8-Reinforcement/1-QLearning/notebook.ipynb:作业指定「以此为起点」的原始 Notebook;
- 8-Reinforcement/1-QLearning/solution/notebook.ipynb:第 1 课讲义的完整解答;
- 8-Reinforcement/1-QLearning/solution/assignment-solution.ipynb 与 8-Reinforcement/1-QLearning/solution/rlboard.py:本作业的官方参考解答(注意 solution 目录下的
rlboard.py与主版本仅图片相对路径../images/不同,逻辑完全一致); - 8-Reinforcement/1-QLearning/translations/:该课的多语言翻译,本文依据的西语版作业即 assignment.es.md;
- 完成本课后再挑战 8-Reinforcement/2-Gym/README.md 中的 Gym 环境课程,可以把本课积累的状态/奖励设计经验迁移到标准 RL 库上。
总结:这份作业的价值在于完整走了一遍「环境改造 → 状态扩展 → 奖励重设计 → 学习循环适配 → 基线对比 → 超参数调优」的 RL 实战闭环。五条世界规则不是孤立设定,而共同制造了一个成功事件稀有、过程信号必须自行塑形的难训练场景——这正是真实业务中稀疏奖励问题的缩影。以 solution/assignment-solution.ipynb 为对照,你可以逐段验证自己的 state 类、reward 函数与训练循环是否符合 Rubric 的「优秀」档位要求。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00