ML-For-Beginners 强化学习作业实战:为「彼得与狼」Q-Learning 世界加入能量与疲惫机制
本篇指南围绕 ML-For-Beginners 课程第 8 周「Peter and the Wolf」强化学习作业(A More Realistic World)展开:在原始棋盘世界中引入能量(energy)与疲惫(fatigue)机制,重新定义状态空间与奖励函数,用 Q-Learning 训练出能击杀狼的获胜策略,并保留随机游走基线做量化对比。读完并动手完成后,你将掌握「状态扩展—奖励重定义—超参数调整—策略评估」这一完整的环境改造流程,这是把 Q-Learning 应用到任何带资源约束的 MDP 问题的通用套路。
任务背景:原始世界与基线算法
本作业建立在第 8 周第一课的「彼得与狼」路径规划世界之上(详见 课程讲义 与 课程笔记本)。原始设定中,Peter 在一块 width x height 的棋盘上行走,格子类型由 rlboard.py 中 Board.Cell 枚举定义:empty(空地)、water(水域,不可进入)、wolf(狼)、tree(树,绿色休息区)、apple(苹果)。原始目标仅仅是避开狼与水域、找到苹果,奖励函数只有三档:
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
Q-Learning 侧的 Q-Table 形状为 width x height x len(actions),初始化为均匀值 0.25(等价于随机游走策略):
Q = np.ones((width, height, len(actions)), dtype=np.float) * 1.0 / len(actions)
训练循环按 Bellman 方程更新:Q(s,a) ← (1-α)·Q(s,a) + α·(r + γ·max_a' Q(s',a')),动作按 Q 值概率化采样以平衡探索与利用(probs() 函数)。基线策略是随机游走,其平均路径长度约为 30–40 步,远高于最近苹果约 5–6 步的理论距离。本作业要求在这个基础上把世界「变得更真实」,并验证强化学习在新规则下依然有效。
新世界规则:完整继承的 5 条设定
根据 assignment.zh-cn.md 的定义,假想中 Peter 原本几乎可以一直走动而不感到疲倦或饥饿;在更真实的世界里,需要时不时坐下休息、也要吃东西。为此引入以下 5 条规则:
- 从一个地方走到另一个地方,Peter 失去能量(energy)并获得疲惫(fatigue);
- Peter 可以通过吃苹果来获得更多能量;
- Peter 可以通过在树下或草地上休息来消除疲惫(即走进有树和草的棋盘位置——绿色格子);
- Peter 需要找到并杀死狼;
- 为了杀死狼,Peter 需要保持一定级别的能量与疲惫(能量要占优),否则他会输掉这场战斗。
注意规则 4 带来的本质变化:终点从「摘到苹果」变成了「击杀狼」,苹果降级为恢复能量的补给品,树/草地(Board.Cell.tree)从普通可走格子升级为恢复疲劳的功能格子。这意味着状态、转移、终止条件和奖励函数全部需要重做。
作业要求与三条关键提示
作业的「说明」部分(assignment.zh-cn.md)给出了硬性要求,逐条列出以便对照执行:
- 起点:使用原始 notebook.ipynb 笔记本作为解决方案的起点;
- 核心工作:根据游戏规则修改奖励函数,运行强化学习算法学习赢得游戏的最佳策略,并在赢/输场次上将自己的算法与随机游走算法对比;
- 提示一(状态表示):新世界中状态更复杂,除位置外还包括疲惫和能量水平。你可以选择把状态表示为元组
(Board, energy, fatigue),或为状态定义一个类(也可以从Board派生),甚至直接修改 rlboard.py 中Board的源码; - 提示二(保留基线):方案中必须保留负责随机走动策略的代码,并在最后将两者结果对比;
- 提示三(超参数):可能需要调整超参数才能跑通,尤其是 epoch 数——因为游戏成功(与狼搏斗)是一个罕见事件,需要更长的训练时间。
状态建模:官方参考方案的 state 类
仓库在 solution/assignment-solution.ipynb 中提供了参考实现。它没有去改 Board 源码,而是定义了一个独立的 state 类来承载「棋盘 + 能量 + 疲惫」三元信息,这是对作业提示一的第一种推荐的落地方式:
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:每走一步失去 1 点能量
self.fatigue += 1 # 规则 1:每走一步增加 1 点疲惫
self.update()
def is_winning(self):
return self.energy > self.fatigue # 规则 5:能量占优才算赢下战斗
几个实现细节值得注意:
- 规则 1–3 全部收敛在
move()与update()中:移动先改变energy/fatigue,再调用update()让格子效果生效,顺序保证了「走进树格即清疲惫、走进苹果格即回满能量」的语义清晰; random_start()复用自Board:从 rlboard.py 源码看,random_start()会在Cell.empty格子上随机落子(不会落在狼、苹果或水上),这保证了初始局面合法;棋盘布局本身由randomize(seed=13)生成,num_wolves=1、num_apples=3、num_trees=5等参数可调,实验时建议固定 seed 以便两次对比(随机 vs Q-Learning)使用同一张地图;- 规则 4、5 体现在终止判定:
is_winning()简单地用energy > fatigue判胜。你可以按作业要求把它改成更严格的阈值(例如能量必须高于某个固定值),这正是作业鼓励自行设计的空间。
另外,state 只持有 board 的引用而不复制它,训练循环中反复 state(m) 重建时棋盘布局保持不变、仅重置能量/疲惫与起点,这是保持实验一致性的关键。
基线改造:面向胜负统计的随机游走
原始 notebook 中的 walk() 以「到苹果」为成功,作业要求以「击杀狼」为成功、以溺水/战斗失败为失败,因此基线本身也要重写。参考方案的做法:
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)]
s.move(a)
n += 1
walk(m, random_policy)
返回值语义被编码进符号:正数=击杀步数,负数=战斗失败,零=溺水。统计函数相应扩展为三类计数,并满足作业「保留随机游走代码并在最后对比」的要求:
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)
先运行这段基线,记录 100 局中的获胜/战败/溺水分布——这就是你最终要超越的数字。
奖励函数重定义:把「资源差」变成稠密信号
新世界的奖励函数不再只有三档终点奖励,而是把 energy - fatigue 的差值作为中间步的稠密奖励,让每一步的「资源状态好坏」都直接反馈给 Q-Table:
def reward(s):
r = s.energy - s.fatigue
if s.at() == Board.Cell.wolf:
return 100 if s.is_winning() else -100
if s.at() == Board.Cell.water:
return -100
return r
设计逻辑:
- 终端事件 ±100:击杀狼 +100、战败 -100、溺水 -100,量级远大于中间奖励,保证「结果导向」依然压倒一切;
- 中间步返回
energy - fatigue:这是一条天然的塑形(shaping)信号——吃苹果会抬升该值、连续赶路会压低该值,等于把作业规则 1–3 的资源经济直接翻译成了梯度方向,帮助稀疏奖励问题中「难得才发生一次的击杀」传播信用; - 你完全可以按作业要求尝试自己的版本(例如按疲惫程度惩罚、对休息格给小额正奖励),并用「胜率是否提升」来检验哪种塑形更合理——这正是评判标准中「奖励函数是否完全定义」的考察点。
Q-Learning 训练循环与超参数调整
Q-Table 的结构不需要变——作业提示三中关于「元组/类/改 Board 源码」的三种状态表示方案,参考方案选择了独立 state 类,Q-Table 仍按 (width, height, len(actions)) 索引(以棋盘位置为主键,能量与疲惫通过奖励间接进入学习信号):
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
v = v / v.sum()
return v
训练主循环与原始 notebook 几乎同构,只把「走到即终止」改成了「进入狼格/水格才终止」:
lpath = []
for epoch in range(10000):
# Pick initial point
s = state(m)
# Start travelling
n = 0
cum_reward = 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: # end of game:击杀/战败/溺水
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
m.plot(Q)
对照作业提示三,这里有三个值得动手调的超参数:
| 超参数 | 参考值 | 调整依据 |
|---|---|---|
| epoch 数 | 10000(原始世界为 5000) | 击杀狼是罕见事件,正样本稀少,需要更长训练时间让「通往狼格且资源占优」的经验被反复强化 |
学习率衰减 alpha = exp(-n/3000) |
衰减尺度 3000 步 | 原始世界用 exp(-n/10e5) 几乎不衰减;新世界步内奖励波动大,更快的衰减能在训练后期稳定 Q 值、减少「覆盖破坏」 |
折扣因子 gamma |
0.5 | 资源状态的价值高度依赖近期决策,较大折扣让 agent 更看重眼前能量/疲惫的变化 |
训练收敛后的 Q-Table 可视化(m.plot(Q))应该呈现指向狼格的偏好流向:
策略评估:与随机游走对比胜负
评估阶段复用同一套 walk + print_statistics 框架,只替换策略函数。与原始世界相同的两种策略选项在这里同样适用:
def qpolicy(m):
x, y = m.human
v = probs(Q[x, y])
a = random.choices(list(actions), weights=v)[0]
return a
print_statistics(qpolicy)
qpolicy(概率采样):保留少量探索,参考方案实测它能显著减少溺水次数,但 Peter 仍不能每次都赢——作业明确鼓励你继续调超参数去改善这一结果;- 贪心版本
qpolicy_strict(直接取argmax)可作对照,但注意原始课程提到的「两个状态互指导致死循环」问题在新世界里依然存在,建议像课程 Challenge Task 1 那样给walk加上最大步数限制,避免统计被卡死拖垮。
对比口径要严格遵循作业要求:在同一张地图(固定 seed=13)、同样 100 局下,比较随机策略与 Q 策略的 won / killed by wolf / drown 三项分布。判定「显著改善」时,最好把随机基线多跑几组(不同 seed)给出波动范围,避免单局运气造成的误判。
评判标准(Rubric)
作业给出了三档评分标准,对照它自检你的方案:
| 标准 | 优秀 | 中规中矩 | 仍需努力 |
|---|---|---|---|
| 笔记本上有新世界规则的定义、Q-Learning 算法和一些文字解释;与随机游走相比,Q-Learning 能够显著改善结果 | 介绍了 Notebook,实现了 Q-Learning 并与随机走动算法相比提高了结果,但不显著;或者 notebook 文档不完善、代码结构不合理 | 有一些重新定义世界规则的尝试,但 Q-Learning 算法不起作用,或奖励函数没有完全定义 |
从这份 rubric 可以提炼出「优秀档」的交付清单:
- 笔记本开头有 5 条新规则的完整文字定义(本文第二节即模板);
- 状态表示、
walk/终止判定、奖励函数、Q-Learning 主循环四个代码块齐全,且保留随机游走代码; - 末尾有同一张地图下的胜负对比输出与结论性文字解释;
- 对超参数(epoch、学习率衰减、gamma)有明确的调整记录——因为「击杀」是罕见事件,这段记录本身就是评分证据。
延伸与相关资源
- 原始课程讲义(世界设定、随机游走、Bellman 方程、探索/利用权衡的完整推导):8-Reinforcement/1-QLearning/README.md;
- 原始课程笔记本(本作业的起点代码):8-Reinforcement/1-QLearning/notebook.ipynb;
- 环境模块源码(
Board类、randomize参数、random_start、格子渲染):8-Reinforcement/1-QLearning/rlboard.py; - 官方参考方案笔记本(本文第三至六节代码出处):8-Reinforcement/1-QLearning/solution/assignment-solution.ipynb;
- 英文原版作业:8-Reinforcement/1-QLearning/assignment.md;
- 完成本作业后,下一课将转向 Gym 环境中的 Q-Learning 实践(8-Reinforcement/2-Gym/README.md),本作业积累的「状态扩展 + 奖励塑形」经验可以直接迁移。
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 StartedRust0622
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

