ML-For-Beginners 强化学习实战:为 Q-Learning 的"Peter 与狼"世界加入能量与疲劳机制
本文基于 ML-For-Beginners 课程第 8 周强化学习模块的作业 A More Realistic World 展开,讲解如何在原有 Peter 与狼(Peter and the Wolf)棋盘环境中引入能量(energy)与疲劳(fatigue)两个新状态维度,重新设计奖励函数并用 Q-Learning 训练出能"击败狼"的策略,同时保留随机游走(random walk)作为基线,从赢/输局数维度对比两种策略。读完本文,你将掌握:扩展 RL 状态空间的方法(元组 / 独立状态类 / 修改环境类)、面向"稀有事件"目标的奖励函数设计,以及训练超参数(尤其是 epoch 数量与学习率衰减)的调整思路。
一、任务背景:为什么原世界"不够真实"
在 8-Reinforcement/1-QLearning/README.md 讲述的基础版本中,Peter 只需在棋盘上找到苹果即可获胜,遇到狼或水则失败,移动过程本身不消耗任何体能。作业文档指出了一个核心问题:
In our situation, Peter was able to move around almost without getting tired or hungry. In a more realistic world, we has to sit down and rest from time to time, and also to feed himself.
为此,作业要求在原来的世界规则上叠加以下 5 条新规则:
- 移动消耗能量、累积疲劳:Peter 每从一处移动到另一处,会损失 energy 并增加 fatigue;
- 吃苹果回能量:Peter 通过吃苹果获得更多能量;
- 树或草地可消除疲劳:走进有树或草地(绿色格子)的位置即可休息,消除疲劳;
- 终极目标是击败狼:Peter 需要找到狼并杀死它;
- 战斗胜负取决于体能:只有能量和疲劳达到一定水平才能战胜狼,否则战斗失败。
作业给出的指令(Instructions)也很明确:
- 以原 notebook.ipynb 作为解答的起点;
- 按照新游戏规则修改奖励函数,运行强化学习算法学习最优策略;
- 保留随机游走策略的代码,最终将你的算法与随机游走在"赢了多少局、输了多少局"上对比。
作业还给出了两条重要提示(Note):
提示 1(状态空间变复杂):新世界中状态更复杂,除了人的位置,还包括疲劳与能量水平。你可以选择把状态表示为元组
(Board, energy, fatigue),或者为状态定义一个类(也可以从Board派生),甚至直接修改 rlboard.py 中的Board类。提示 2(超参数):你可能需要调整超参数,尤其是 epoch 数量。因为"战胜狼"在博弈中是一个稀有事件,所以要预期比原来更长的训练时间。
下面结合仓库中的环境源码与参考解答 solution/assignment-solution.ipynb 逐节实现这套新规则。
二、环境基础:棋盘格子与移动原语
新世界的实现建立在 rlboard.py 提供的棋盘模块之上。该文件定义了 Board 类,其内部 Cell 枚举精确对应了 5 条新规则涉及的所有地形:
# rlboard.py
class Board:
class Cell:
empty = 0
water = 1
wolf = 2
tree = 3
apple = 4
empty(空地):可自由行走;water(水):不可通行,踩入即死;wolf(狼):在新规则下是"决战对象";tree(树):绿色格子,即新规则中"可以休息消除疲劳"的位置;apple(苹果):新规则中"恢复能量"的食物。
随机生成一张种子固定的棋盘只需几行:
from rlboard import *
width, height = 8, 8
m = Board(width, height)
m.randomize(seed=13)
m.plot()
Board 类中与本作业直接相关的关键方法(见 rlboard.py):
| 方法 | 作用 | 在新世界中的用途 |
|---|---|---|
at(pos=None) |
返回指定位置(默认人当前位置)的格子类型 | 判断是否踩到树、苹果、狼、水 |
is_valid(pos) |
判断坐标是否在棋盘内 | 边界检查 |
move_pos(pos, dpos) |
计算移动后的新坐标 | 计算下一步位置 |
move(dpos, check_correctness=True) |
执行人移动 | 基础移动原语 |
random_start() |
随机选一个空地作为人的起点 | 每局训练的初始状态 |
从源码结构看,randomize() 的默认参数(water_size=5, num_water=3, num_wolves=1, num_trees=5, num_apples=3)意味着一张 8×8 棋盘上有 3 片水域、1 只狼、5 棵可休息的树和 3 个苹果——树和苹果的数量保证"休息"与"进食"两条规则有足够多的触发点,这也是新规则可玩性的前提。
三、状态空间重构:引入 energy 与 fatigue
这是本作业与基础课最本质的区别:状态不再只是棋盘上人的坐标。参考解答采用作业提示 1 中推荐的"定义一个状态类"方案,把棋盘、能量、疲劳封装成一个 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
if self.at() == Board.Cell.apple:
self.energy = 10
def move(self, a):
self.board.move(a)
self.energy -= 1
self.fatigue += 1
self.update()
def is_winning(self):
return self.energy > self.fatigue
对照 5 条新规则,逐条看它是如何落地的:
- 规则 1(移动耗能、增疲劳):
move()中self.energy -= 1且self.fatigue += 1,每走一步严格对应"损失能量、增加疲劳"; - 规则 2(吃苹果获得能量):
update()中踩到apple格子时self.energy = 10,即吃到苹果把能量拉回满值 10; - 规则 3(树/草地消除疲劳):
update()中踩到tree(绿色格子)时self.fatigue = 0,一次性清空疲劳; - 规则 4(目标是击败狼):体现在下一节的胜负判定与
walk循环里——走进狼所在格子触发战斗结算,而不是像基础课那样"见到狼就判负"; - 规则 5(体能决定战斗胜负):
is_winning()用self.energy > self.fatigue作为胜利判据,即能量高于疲劳才赢得战斗。
这个类还留出了两种可扩展的设计空间(对应作业提示 1 的其余选项):如果你更倾向元组表示,可直接用 (board, energy, fatigue) 三元组并配套若干函数;如果想让环境"自包含",也可以直接在 Board 类中增加 energy/fatigue 字段。参考解答选择独立 state 类,好处是不污染原有的 Board 类——Board 继续只负责棋盘几何与渲染,新规则全部封装在 state 里,原 notebook 中 m.plot()、m.random_start() 等代码可原样复用。
四、随机游走基线:先量出"不学习"有多弱
作业要求保留随机游走代码并与学习算法对比,因此第一步仍是跑基线。随机策略与行走循环如下(参考解答 assignment-solution.ipynb):
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 # success!
else:
return -n # failure!
if s.at() == Board.Cell.water:
return 0 # died
a = actions[policy(m)]
s.move(a)
n += 1
walk(m, random_policy)
注意返回值约定与基础课不同:n 表示赢(在足够体能下遇到狼并取胜),-n 表示输(遇到狼但体能不足战败),0 表示淹死。再跑 100 局统计:
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)
这组"被狼杀死 / 赢 / 淹死"的计数就是作业要求的对比指标。随机策略下 Peter 几乎不会带着足够体能走到狼身边,胜率接近于零——这个基线数字正是后面证明 Q-Learning 有效性的参照系。
五、奖励函数:把新规则编码进标量信号
基础课中的奖励函数是三段式的(move_reward = -0.1、goal_reward = 10、end_reward = -10,苹果是目标、狼是死路)。在新规则下,胜负结构彻底改变,参考解答给出的奖励函数为:
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 的极端值:胜负是"稀有事件",只有把终局奖励的量级拉到远超普通步奖励,Bellman 更新才能把"赢"的信号逆向传播回之前的每一步;
- 普通步奖励用
energy - fatigue而非固定负值:这让奖励函数与规则 1/5 天然耦合——状态越"健康"(能量高、疲劳低),当步奖励越高,Agent 会在探索中自发地偏向"多休息、多进食、少走冤枉路"的路径,而不需要显式地为树和苹果写额外奖励项; - 水保持 -100 死惩罚:与基础课一致,淹没即终局。
六、Q-Learning 训练:稀有事件下的算法与超参数
Q-Learning 的核心仍是 Bellman 方程(沿用基础课的推导,见 8-Reinforcement/1-QLearning/README.md):
其中 γ 是折扣因子,权衡即时奖励与未来奖励。初始化 Q 表与概率化采样函数与基础课一致:
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
Q 表形状仍是 width × height × len(actions):由于参考解答把能量与疲劳放在 state 里而不进 Q 表索引,Q 表维度没有爆炸;probs() 把 Q 值转成动作概率分布,实现基础课讲的"探索—利用平衡"(Q 值趋同时退化为随机选择)。
训练主循环(10000 个 epoch):
lpath = []
for epoch in range(10000):
# Pick initial point
s = state(m)
# Start travelling
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: # 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
结合作业提示 2,这里有几个关键超参数的取值逻辑:
| 超参数 | 取值 | 说明 |
|---|---|---|
| epochs | 10000 |
基础课用 5000 就能学会"找苹果",但"带着正确体能击败狼"是稀有事件,正样本出现率低,所以 epoch 必须放大。这正是作业 Note 中"expect much longer training time"的直接体现 |
| 学习率 α | np.exp(-n / 3000) |
随单局步数 n 指数衰减:开局学习激进,中后期只小幅微调,避免把已学到的 Q 值"洗掉"(基础课衰减得更快,系数为 10e5,此处放慢以配合更长的单局) |
| 折扣因子 γ | 0.5 |
与基础课一致,偏重近期奖励 |
| 动作选择 | random.choices(..., weights=v) |
按 Q 值概率采样,兼顾探索与利用 |
两个工程细节也值得注意:
- 越界保护:
while True内循环会重掷动作直到选出棋盘内的合法移动(s.board.is_valid(...)),避免 Q 更新时索引越界——这与基础课"允许走出棋盘作为终止条件"的处理不同,是因为新规则下能量/疲劳需要随每一步move()同步更新,走出棋盘会破坏状态一致性; - 终局判定:
abs(r) == 100用奖励绝对值识别终局(赢狼、输掉战斗、淹死),干净利落。
七、结果对比:Q 策略 vs 随机游走
训练完成后,用与训练时一致的"概率采样"策略评估:
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)
print_statistics 会对两种策略各跑 100 局,输出"Killed by wolf / won / drown"三组计数。参考解答的结论是:淹死的情况显著减少,但 Peter 仍不能每次都击败狼,并建议继续调整超参数提升胜率——这本身也是作业留给学生的一道开放题。此外可以绘制 plt.plot(lpath) 观察每局步长的变化,其形态与基础课中"先升后降、偶有突刺"的学习曲线类似(见基础课中的学习过程分析)。
如果 Q 策略与随机游走的对比不显著,可优先排查的方向(按作业 Rubric 的"Exemplary"标准):
- epoch 是否足够——把 10000 继续上调,观察
won计数是否上升; - 能量/疲劳的初始值与恢复量(
energy=10、吃苹果回到 10、energy -= 1、fatigue += 1的步长)是否让"获胜"事件过稀有; - α 衰减速度——衰减过快可能停在次优 Q 表,过慢则后期持续破坏已有知识。
八、评分标准(Rubric)对照
作业文档给出了明确的三级评分表,写解答时可直接对照自检:
| 级别 | 要求 |
|---|---|
| Exemplary | 提交一个 notebook,包含新规则的定义、Q-Learning 算法实现及文字说明;Q-Learning 相比随机游走显著提升结果 |
| Adequate | 提交 notebook,实现了 Q-Learning 且结果优于随机游走,但提升不显著;或文档质量差、代码结构不佳 |
| Needs Improvement | 尝试重新定义了世界规则,但 Q-Learning 算法不工作,或奖励函数未完整定义 |
要达到 Exemplary,核心不是"代码能跑",而是对比实验的设计:同一张棋盘(相同 seed)、同一套统计口径(100 局、赢/输/淹死计数)下,随机策略与 Q 策略的胜率差要大到可复现、可展示。
九、小结与延伸阅读
本作业演示了把"体能资源"引入马尔可夫决策过程的完整路径:状态扩展(energy/fatigue)→ 奖励重设计(±100 终局奖励 + 状态量化的步奖励)→ 超参数适配(更多 epoch、更慢的 α 衰减)→ 保留基线的对比评估。它同时呼应了课程对强化学习三要素(Agent、State、Action)的强调:规则 1–3 改变的是 State 的构成与转移,规则 4–5 改变的是 Reward 的终局结构。
相关仓库资源:
- 基础课的完整推导与代码(随机游走、奖励函数、Bellman 方程、探索/利用):8-Reinforcement/1-QLearning/README.md
- 环境源码(
Board类、格子枚举、随机化与渲染):8-Reinforcement/1-QLearning/rlboard.py - 起点 notebook:8-Reinforcement/1-QLearning/notebook.ipynb
- 参考解答(本文第三~七节代码出处):8-Reinforcement/1-QLearning/solution/assignment-solution.ipynb
- 下一课将 Q-Learning 迁移到连续状态空间(CartPole 平衡问题):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 StartedRust0624
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

