ML-For-Beginners 强化学习实战:用 Q-Learning 教会智能体在网格世界中寻宝避狼
本篇文章取材于经典机器学习入门课程 ML-For-Beginners 的「强化学习与 Q-Learning」章节(对应文档:translations/ar/8-Reinforcement/1-QLearning/README.md,阿拉伯语版;其英文母版为 8-Reinforcement/1-QLearning/README.md)。文章以俄罗斯作曲家普罗科菲耶夫的交响童话《彼得与狼》为背景,借助一套 8×8 网格寻路模拟环境,从零讲解智能体(agent)、状态(state)、动作(action)、奖励函数(reward function)、Q 表与贝尔曼方程,并给出可运行的随机游走基线、Q-Learning 训练循环与策略验证代码。读完你不仅能复现整套实验,还能理解 ε 探索、学习率衰减、折扣因子等核心概念,并动手把世界改造成包含「能量、疲劳、杀狼」规则的进阶问题。
一、强化学习三要素:以「超级马里奥」为例理解概念
强化学习(Reinforcement Learning,RL)是一种通过在环境中反复做实验来学习最优行为的机器学习范式。它始终围绕三个核心概念展开:
- 智能体(Agent):做出决策的主体,例如游戏里的马里奥,或本课里的彼得;
- 状态(State):智能体所处场景的完整描述,例如「彼得正站在某个棋盘坐标、身边是悬崖」;
- 动作(Action):智能体在某个状态下可选的操作集合,例如「向右一步」或「跳跃」。
当智能体在某个状态执行某个动作后,环境会回馈一个奖励(Reward):马里奥向右迈入悬崖得到很低的数值分,而按下跳跃键拿到金币并存活下来,得到正的数值分。强化学习的目标,就是利用模拟器不断试错,学习出一条使累积奖励最大化的行动策略——在马里奥那里是「活下去并拿高分」,在本课里则是「找到苹果、避开狼」。
用课程原文的口吻概括:RL 是让我们在某个环境中通过运行大量实验,学习智能体最优行为的学习技术;智能体在该环境中应有一个由奖励函数定义的明确目标。
二、环境:把世界抽象成 8×8 棋盘
为降低复杂度,课程把彼得的世界建模为一张 width × height 的正方形棋盘。每个格子只能是以下五类之一:
- 地面(ground):彼得与其他角色可以行走;
- 水(water):显然无法行走,掉进去会终止回合;
- 树或草丛(tree/grass):可休息的绿色地带;
- 苹果(apple):彼得想找到并吃掉的目标;
- 狼(wolf):危险角色,必须避开。
这些格子在仓库环境实现 8-Reinforcement/1-QLearning/rlboard.py 中由一个内部枚举类 Board.Cell 表示,其数值定义如下:
class Cell:
empty = 0 # 空地
water = 1 # 水
wolf = 2 # 狼
tree = 3 # 树/草丛
apple = 4 # 苹果
棋盘由 Board 类管理。它维护一个 width × height 的 matrix 用于存放格子类型,并保存 human 属性表示彼得当前位置。randomize() 方法按参数随机生成地貌,各参数的默认值与作用在源码中一目了然:
def randomize(self, water_size=5, num_water=3, num_wolves=1,
num_trees=5, num_apples=3, seed=None):
num_water=3:随机播种 3 片水域,每片以water_size=5个相邻格子的方式扩散;num_trees=5、num_wolves=1、num_apples=3:分别随机放置树、狼与苹果,且只在空地上放置;seed:固定随机种子,保证复现(课程示例使用seed=13)。
本课把环境代码单独放进模块 rlboard.py,原因正如课程所说:「这些代码对理解核心概念并不重要」,所以直接导入使用即可。创建并绘制示例棋盘的第一步(课程中的代码块 1):
from rlboard import *
width, height = 8, 8
m = Board(width, height)
m.randomize(seed=13)
m.plot()
运行后会输出与上文环境图一致的棋盘。相关完整实验代码见 8-Reinforcement/1-QLearning/notebook.ipynb(阿拉伯语翻译版位于 translations/ar/8-Reinforcement/1-QLearning/notebook.ipynb)。若在云端打开笔记本,需要把 rlboard.py 一并放到笔记本同目录,代码才能正常 import。
三、动作与策略(Policy)
彼得的任务可概括为「找到苹果、避开狼与其他障碍」。在任意位置,他能在四个动作中选择:上、下、左、右。课程用字典把动作符号映射为坐标增量对,例如向右 R 对应 (1,0)(代码块 2):
actions = { "U" : (0,-1), "D" : (0,1), "L" : (-1,0), "R" : (1,0) }
action_idx = { a : i for i,a in enumerate(actions.keys()) }
这里 action_idx 用于把动作名映射成整数下标,供后面索引 Q 表使用。两个关键术语:
- 策略(Policy):智能体的「行动方针」,是一个在任意给定状态返回应执行动作的函数。本例中状态由整张棋盘与彼得当前位置共同刻画;
- 目标(Goal):强化学习的最终目标是学到一条好策略,从而高效地解决问题。作为基线,课程先考察最简单的策略——随机游走(random walk)。
四、基线:随机游走及其统计
随机游走策略每一步都在允许的动作里随机挑一个,直到撞上苹果或遇险(代码块 3):
def random_policy(m):
return random.choice(list(actions))
def walk(m, policy, start_position=None):
n = 0 # 步数
# 设置初始位置
if start_position:
m.human = start_position
else:
m.random_start()
while True:
if m.at() == Board.Cell.apple:
return n # 成功!
if m.at() in [Board.Cell.wolf, Board.Cell.water]:
return -1 # 被狼吃掉或溺水
while True:
a = actions[policy(m)]
new_pos = m.move_pos(m.human, a)
if m.is_valid(new_pos) and m.at(new_pos) != Board.Cell.water:
m.move(a) # 真正移动
break
n += 1
walk(m, random_policy)
walk 返回路径长度;失败时返回 -1。反复执行若干次(比如 100 次)并统计成功率与平均路径长度(代码块 4):
def print_statistics(policy):
s, w, n = 0, 0, 0
for _ in range(100):
z = walk(m, policy)
if z < 0:
w += 1
else:
s += z
n += 1
print(f"Average path length = {s/n}, eaten by wolf: {w} times")
print_statistics(random_policy)
课程指出:随机游走的平均路径长度约为 30~40 步,而棋盘上到最近苹果的平均直线距离只有 5~6 步——大量步数被浪费在无意义的折返与危险试探上。这是随机策略「无记忆、无方向」的直接代价。如果你把棋盘里的彼得替换成经典贪吃迷宫问题,同样的结论依然成立。
对照源码看,walk 内部其实调用了 Board 提供的 random_start()(随机落在空地)、at()(读取当前格类型)、is_valid()(越界检查)与 move(),这些基础方法都实现在 8-Reinforcement/1-QLearning/rlboard.py 的 Board 类中;而课程 walk 与类内自带的 Board.walk 逻辑等价,教学上独立写出便于读者看清每一步。
五、奖励函数:让「好坏」可量化
想让策略变聪明,必须先定义「哪种移动更好」。课程用奖励函数给每个状态打一个分数:数字越大越好(代码块 5):
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
奖励设计的精髓在于:多数情况下我们只在游戏结束才拿到大额奖励(正 10 或负 10),中间每一步都只是 -0.1。这意味着算法必须「记住」那些最终通向正奖励的中间步骤并抬高它们的重要性,同时抑制导致坏结果的移动——这正是下一节 Q-Learning 要解决的延迟奖励(delayed reward)信用分配问题。
六、Q 表:记录「每个状态下每个动作有多好」
Q-Learning 用一张 Q 表(Q-Table) 来隐式定义策略:它记录某状态下每个动作的「质量/吸引力」。因为棋盘是 width × height,Q 表天然可以表示成一个 width × height × len(actions) 的三维 numpy 数组(代码块 6):
Q = np.ones((width, height, len(actions)), dtype=np.float) * 1.0 / len(actions)
这里把全部状态-动作对初始化为相等的 0.25(4 个动作均分 1),对应「所有方向都一样好」的随机策略。把 Q 表传给 plot 即可在棋盘上可视化初始状态——每个格子中心只有一个圆点,因为没有方向占优。
将 Q 表传给 m.plot(Q) 后,训练前的状态大致如下图(训练后的策略分布见后文学习结果小节):
随后就要跑模拟、探索环境,让 Q 表数值重新分布,从而找到远比随机游走更短的苹果路径。
七、核心原理:贝尔曼方程
一旦开始移动,每个动作都会带来即时奖励。理论上可以「每步都挑即时奖励最高的动作」,但在绝大多数状态里,任何一步都无法立刻抵达苹果,因此不能只看眼前收益——真正重要的是模拟结束时的最终结果。为此需要引入动态规划思想递归地思考问题:
设当前处于状态 s,执行动作 a 后转移到下一状态 s′。我们将获得:
- 由奖励函数定义的即时奖励 r(s,a);
- 一定的未来奖励。
若假设 Q 表已正确反映每个动作的「吸引力」,那么在 s′ 处我们自然会选择使 Q(s′,a′) 最大的动作 a′。于是,站在 s 能获得的最佳未来收益为 max_{a′} Q(s′,a′)(对 s′ 下所有可能动作取最大)。由此得到 贝尔曼公式(Bellman Equation):
Q(s,a) = r(s,a) + γ · max_{a′} Q(s′,a′)
其中 γ(gamma)是折扣因子,决定你更看重当前奖励还是未来奖励——γ 越接近 1,智能体越有「远见」,愿意牺牲眼前收益去换取长远高回报。对应的公式插图见仓库 8-Reinforcement/1-QLearning/images/bellman-equation.png。
八、学习算法:伪代码与 Python 实现
有了贝尔曼方程,就能写出完整学习算法。课程先给出伪代码:
- 用相等的数值初始化 Q 表;
- 设置学习率 α ← 1;
- 重复多次模拟:
- 从随机位置出发;
- 循环执行:
- 在状态 s 选择动作 a;
- 执行动作,转移到新状态 s′;
- 若触发回合结束条件或累计奖励过小,则退出本次模拟;
- 计算新状态下的奖励 r;
- 按贝尔曼方程更新 Q 函数:Q(s,a) ← (1−α)Q(s,a) + α( r + γ max_{a′}Q(s′,a′) );
- 令 s ← s′;
- 更新累计奖励并减小 α。
1)概率化辅助函数 probs()。 训练前先需要一个把 Q 表任意数值向量转成动作概率向量的函数:
def probs(v, eps=1e-4):
v = v - v.min() + eps
v = v / v.sum()
return v
之所以加一个很小的 eps,是为了避免初始阶段向量各分量完全相等时出现「除以 0」的问题。
2)训练主循环。 课程随后把学习算法跑 5000 次实验(也叫 epochs,代码块 8):
for epoch in range(5000):
# 随机选择起点
m.random_start()
# 开始移动
n = 0
cum_reward = 0
while True:
x, y = m.human
v = probs(Q[x, y])
a = random.choices(list(actions), weights=v)[0]
dpos = actions[a]
# 允许走出棋盘,以此终止本回合
m.move(dpos, check_correctness=False)
r = reward(m)
cum_reward += r
if r == end_reward or cum_reward < -1000:
lpath.append(n)
break
alpha = np.exp(-n / 10e5) # 学习率随时间指数衰减
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
逐行解读这段核心代码:
- 动作选择使用
random.choices(..., weights=v),即按 Q 值比例随机抽取动作——这正是下一节「探索 vs. 利用」折衷的落地方式; m.move(dpos, check_correctness=False)允许彼得走出棋盘,越界格会触发end_reward从而自然终止回合;lpath记录每一回合的步数,用于事后绘制学习曲线;alpha = np.exp(-n / 10e5):学习率衰减。n 越大 α 越小,后期 Q 表只做微小修正,避免新样本把已学好的系数「冲毁」;gamma = 0.5为折扣因子;- 更新式
Q ← (1−α)Q + α(r + γ·max Q(下一状态))即贝尔曼方程的增量形式:当前 Q 值向「即时奖励 + 折扣后的最优未来收益」这个目标做一步靠拢。
对照仓库另一份参考实现 8-Reinforcement/1-QLearning/solution/notebook.ipynb,训练循环把 epoch 数调到了 10000、衰减设为 np.exp(-n / 3000),并利用 clear_output 实时打印进度——可见 epoch 数量、衰减速率都是需要按环境调节的超参数,课程正文与解答笔记本给出的是不同但都有效的取值。
训练完成后,把 Q 表画到棋盘上:每个格子出现一个指向「质量最高的动作方向」的箭头。训练后 Q 表可视化如下:
对比训练前的圆点图可以直观看到:箭头逐渐形成一条从起点绕过水域、避开狼、指向苹果的「策略流」。
九、探索 vs. 利用:动作究竟怎么选
前面伪代码第 2.1 步没有规定动作如何选,这里补上两种极端策略:
- 纯探索:完全随机选动作,会频繁死亡、跑到平时不会去的区域,学习低效;
- 纯利用(exploit):每步都选当前 Q 值最高的动作,会固守已知路径而错过可能更优的解。
因此最佳实践是在两者间取平衡:按与 Q 表数值成比例的概率选择动作。训练初期 Q 表全相等,退化为随机选择;随着对环境的了解加深,智能体越来越倾向走最优路线,同时仍偶尔尝试未探索的岔路。
十、验证学到的策略
Q 表列出每个状态下每个动作的吸引力,用它导航非常直接。**最朴素的「严格策略」**取 Q 值最大的动作(代码块 9):
def qpolicy_strict(m):
x, y = m.human
v = probs(Q[x, y])
a = list(actions)[np.argmax(v)]
return a
walk(m, qpolicy_strict)
注意:把上面代码多跑几次,可能会遇到「卡死」,需要在笔记本里按 STOP 中断。原因是可能存在两个状态在最优 Q 值上「互相指向对方」,智能体便在这两个状态之间无限往返。
更好的导航策略是训练时用的那种「探索+利用」混合方式——按 Q 值比例随机选动作(代码块 10):
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 次模拟,得到的平均路径长度应显著小于随机游走,落在 3~6 步区间,接近棋盘的理论最短距离——这就是 Q-Learning 学习成效的可量化证据。
🚀 两个课堂挑战
为了更深入理解策略与潜在缺陷,课程布置了两个动手任务:
- 任务 1:修改
walk函数,给路径长度设置上限(如 100 步),观察严格策略会时不时返回这个上限值——即上面提到的「死循环」现象被显式截断; - 任务 2:修改
walk,使其不再回到已经访问过的位置。这能防止循环,但智能体可能被困在一个既无法前进也无法逃脱的局部区域——可以借此体会「无状态记忆策略」的局限。
十一、观察学习过程:平均路径长度曲线
训练结果变好了,但更有趣的是观察训练过程中平均路径长度如何演化。课程用 plt.plot(lpath) 绘制出每条曲线:
从曲线可以总结出三个阶段:
- 路径长度先上升:对环境一无所知时,很容易陷入水、狼等坏状态快速送命(路径很短就失败)。随着知识积累,智能体能在环境中「活得更久」,但此时还不太清楚苹果在哪,所以成功路径反而变长;
- 学到一定程度后路径下降:智能体掌握了足够知识,能稳定地接近目标,路径长度开始缩短;但由于仍保留探索,常会偏离最优路线去试新选择,导致路径比最优略长;
- 突然出现尖峰:曲线上某些点路径长度骤增,反映过程的随机性——新写入的 Q 值可能临时「弄坏」已有系数。理想做法是在训练后期调低学习率,只对 Q 表做微小修正,尽量抑制这类波动。
这引出了课程的重要提醒:学习效果高度依赖学习率、学习率衰减策略和折扣因子。这类在训练前设定、控制学习行为本身的参数叫超参数(hyperparameters),用来与训练中优化的参数(如 Q 表系数)相区分;寻找最优超参数的过程叫超参数优化(hyperparameter optimization),是值得单独展开的主题。
十二、课后扩展:让世界更真实
课程作业《一个更真实的世界》(对应翻译版任务文档 translations/ar/8-Reinforcement/1-QLearning/assignment.md)把模拟从「找苹果」升级为带生理状态约束的生存博弈,需要你在 8-Reinforcement/1-QLearning/rlboard.py 与奖励函数基础上自行扩展:
- 移动会消耗能量(energy)并累积疲劳(fatigue);
- 吃苹果能恢复能量;
- 站到树/草丛(绿色地块)休息能消除疲劳;
- 彼得需要主动找到并击杀狼;
- 击杀狼需要能量与疲劳达到一定门槛,否则战斗失败。
文档给出的实现提示非常实用:
- 新世界的状态比原来复杂——除位置外还包括疲劳与能量水平,可把状态表示成
(Board, energy, fatigue)元组、自定义状态类(甚至从Board派生),或直接修改Board类; - 必须保留随机游走代码作为对照,最后比较两种策略的胜/负场次;
- 由于「打赢狼」是稀有事件,预计需要更长的训练时间,通常要相应调大 epoch 数等超参数。
若想参考已完成的解法,可对照查看 8-Reinforcement/1-QLearning/solution/assignment-solution.ipynb;阿拉伯语译文目录下的对照笔记本位于 translations/ar/8-Reinforcement/1-QLearning/solution/。后续课程《Gym》则会带你走出自建棋盘,用 OpenAI Gym 的标准接口重做这个「用强化学习训练彼得」的问题,其讲义在 8-Reinforcement/2-Gym/README.md,可以沿 8-Reinforcement/README.md 继续按图索骥。
小结
沿着本课主线,你从「强化学习是什么」走到了「Q-Learning 能跑出什么结果」:先定义由棋盘环境、四方向动作与稀疏奖励构成的世界,用随机游走建立约 30~40 步的基线,再用 Q 表配合贝尔曼方程把路径压到 3~6 步,最后通过学习曲线理解探索/利用平衡、学习率衰减与折扣因子这些超参数的作用。本课所有代码都沉淀在 8-Reinforcement/1-QLearning/notebook.ipynb 中,环境实现可逐行研读 8-Reinforcement/1-QLearning/rlboard.py——带着「如果棋盘更大、目标更多、奖励更稀疏,这套流程还成立吗」的问题动手改一版,是掌握强化学习最快的方式。
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



