ML-For-Beginners 强化学习实战:Q-Learning 训练“Peter”在 8×8 网格世界里采苹果、避狼
本文基于微软 ML-For-Beginners 课程的《Introduction to Reinforcement Learning and Q-Learning》讲义(本仓库为其中韩语版 README.ko.md,与英文版 README.md 内容一致),完整讲解如何利用 Q-Learning 让智能体 Peter 在一个由水、树、苹果和狼组成的棋盘环境中学会“采苹果、避狼”的最优策略。读完后,你将掌握强化学习的核心概念(agent / state / action / reward)、Q-Table 与 Bellman 方程的数学原理、完整的 Python 训练循环实现,以及 exploration/exploitation 平衡与超参数调优的实战经验。
强化学习的基本概念:agent、state、action 与 reward
强化学习(Reinforcement Learning, RL)混合了三个核心概念:agent(智能体)、若干 states(状态),以及每个状态下的一组 actions(动作)。agent 在某个状态下执行动作后,会获得相应的奖励(reward)。
课程用 Super Mario 的例子来解释这套机制:你扮演马里奥,站在游戏关卡中悬崖边缘,头顶有一枚金币——“你在某个关卡、某个具体位置”这一事实就是 state;向右走一步(action)会掉下悬崖,得到很低的分数;而按下跳跃键则能拿到分数并存活下来,这是一个正向结果,应该给予正向奖励。借助强化学习与一个模拟器(游戏本身),智能体可以学会如何玩游戏来最大化奖励——即存活并尽可能多地得分。
课程给出的 RL 定义是:RL 是一种通过反复运行大量实验,学习某个 environment(环境)中 agent 最优行为的技术。环境中的 agent 必须拥有由 reward function(奖励函数) 定义的一定 goal(目标)。
前提条件与环境搭建
本课程的实验部分使用 Python 与 Jupyter Notebook 完成,代码可以在本地电脑或任意云环境中运行。讲义配套的 notebook.ipynb 是动手实践的入口。
注意:如果在云环境中打开代码,还需要把
rlboard.py文件取来,并放在与 notebook 相同的目录下——notebook 中的代码直接依赖这个模块。
本课的探索主题是俄罗斯作曲家 Sergei Prokofiev 的童话音画剧 《Peter and the Wolf》(彼得与狼)。我们将用强化学习让 Peter 探索他的环境、收集美味的苹果、并躲避狼。
为了保持简单,Peter 的世界被建模为一个 width × height 大小的方形棋盘。棋盘上每个格子只能是以下五种之一:
- ground(地面):Peter 和其他生物可以在上面行走;
- water(水):显然不能走;
- tree(树)或 grass(草地):可以休息的地方;
- apple(苹果):Peter 想要自己吃到的目标物;
- wolf(狼):危险,必须躲避。
这个环境的代码封装在独立的 Python 模块 rlboard.py 中。理解其内部实现不是必须的,但对照源码可以确认讲义中每个 API 的真实行为。从源码结构看:
Board类定义在 rlboard.py,其内部Cell常量给出五种格子的数值编码:empty=0, water=1, wolf=2, tree=3, apple=4(rlboard.py);randomize()方法(rlboard.py)负责随机生成地图,默认参数为water_size=5, num_water=3, num_wolves=1, num_trees=5, num_apples=3,即默认 3 片水域、1 只狼、5 棵树、3 个苹果,并支持seed固定随机种子以便复现;is_valid(pos)(rlboard.py)判断坐标是否在棋盘内;move(dpos, check_correctness=True)(rlboard.py)执行移动,当check_correctness=False时允许越界——这一行为正是后面训练循环中“走出棋盘即结束回合”的实现依据;random_start()(rlboard.py)只在empty格子上随机放置 Peter 作为起点。
导入模块并创建一个示例棋盘(代码块 1):
from rlboard import *
width, height = 8,8
m = Board(width,height)
m.randomize(seed=13)
m.plot()
这段代码会输出一张与上面类似的棋盘图。
动作空间与“随机游走”基线策略
在这个示例中,Peter 的目标是躲避狼和障碍、找到苹果,因此在找到苹果之前会一直走动。无论从哪个位置出发,他都只能从四个动作中选择一个:up(上)、down(下)、left(左)、right(右)。
用字典定义动作,并把字符映射为坐标变化量。例如向右移动(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()) }
这个场景下的策略与目标可以概括为:
- agent(Peter)的策略称为 policy(策略),定义为一个函数:给定 state,返回一个 action。在这里,问题的 state 就是由棋盘(含玩家当前位置)表示的东西。
- 强化学习的目标是最终学会一个能高效解决问题的“好策略”。作为起点,先考虑最简单的策略——random walk(随机游走)。
用随机游走解第一题
随机游走策略是:在触碰到苹果之前,从允许的动作中随机挑一个作为下一个动作(代码块 3):
def random_policy(m):
return random.choice(list(actions))
def walk(m,policy,start_position=None):
n = 0 # number of steps
# set initial position
if start_position:
m.human = start_position
else:
m.random_start()
while True:
if m.at() == Board.Cell.apple:
return n # success!
if m.at() in [Board.Cell.wolf, Board.Cell.water]:
return -1 # eaten by wolf or drowned
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) # do the actual move
break
n+=1
walk(m,random_policy)
调用 walk 后,每次运行都会返回一个长度不一的路径:到达苹果时返回步数 n;被狼吃掉或掉进水里则返回 -1。这与 rlboard.py 内置的 Board.walk()(rlboard.py)逻辑一致——两处都采用“若目标格无效或是水,则原地重选动作”的循环,保证 Peter 不会主动走进水里,但仍可能“卡”在原地空转,这正是随机策略效率低下的直接体现。
接着把行走实验重复多次(例如 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)
两个关键观察:
- 已知“到最近的苹果大约需要 5–6 步”,但随机游走的平均路径长度却在 30–40 步左右——相当低效;
- 上图展示了随机游走过程中 Peter 的行为:在棋盘上漫无目的地打转。
奖励函数:给“好坏”打分
要让策略变得更聪明,首先需要理解一个动作为什么比另一个“更好”,也就是要显式定义目标。目标可以用一个 reward function 表示:它在每个 state 上返回一个分值,分数越高的部分说明这个结果越好(代码块 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
参数含义:每走一步扣 0.1 分(负奖励抑制漫无目的的移动);吃到苹果得 +10;走进水、狼的格子或走出棋盘边界则记 -10,标志本轮结束。
奖励函数有一个有趣的性质:在很多情况下,只有在游戏结束时才给予关键奖励(goal/end reward)。这意味着算法必须学会“记住并加权”那些最终导向正向奖励的步骤,同时避免所有导向坏结果的动作——这正是**延迟奖励(delayed reward)**问题的来源,也是下一节 Bellman 方程要解决的核心。
Q-Learning 的核心:Q-Table 与 Bellman 方程
本课讨论的算法叫 Q-Learning。在该算法中,策略由一个称为 Q-Table 的函数(数据结构)来定义:它记录给定 state 下每个 action 的“好坏程度”(goodness)。由于 Q-Table 常常方便地表示为一个多维数组,故得名“表”。
棋盘大小为 width × height,所以 Q-Table 可以用形状为 width × height × len(actions) 的 numpy 数组表示(代码块 6):
Q = np.ones((width,height,len(actions)),dtype=np.float)*1.0/len(actions)
所有 Q-Table 值都初始化为相等的 0.25(即 1/4)。这相当于“每个状态下所有动作同样值得尝试”,对应于 random walk 策略。把 Q-Table 传给 plot 函数(m.plot(Q))可以把它可视化到棋盘上:
每个格子中心会画出方向“箭头”;由于四个方向初始完全相同,rlboard.py 的 image() 方法(rlboard.py)把四个方向的期望位移相加后得到零向量,于是只画出一个点。要让 Peter 更快找到苹果,现在需要开始运行仿真、探索环境,从而学到更好的 Q-Table 分布。
延迟奖励与 Bellman 方程
一旦开始移动,每个动作都会有相应的奖励。理论上可以“立即选择奖励最高的下一个动作”,但绝大多数状态下,单步动作并不能直接达成吃到苹果的目标,因此无法立刻判断哪个方向更好。
请记住:真正重要的不是即时发生的结果,而是仿真结束时才出现的最终结果。
要解释延迟奖励,需要借助**动态规划(dynamic programming)**的递归思想。假设当前处于 state s,想要移动到 state s'。根据奖励函数,会立即获得即时奖励 r(s,a),此外还会得到一些未来奖励。若 Q-Table 正确记录了每个动作的“吸引力”,那么在 s' 状态下会选择使 Q(s',a') 最大的那个动作。于是从 state s 出发可获得的最优未来奖励即由 maxa'Q(s',a') 定义(最大在 s' 下所有可能的 a' 动作上取)。
由此得到给定动作 a 下计算 state s 的 Q-Table 值的 Bellman 公式:
Q(s,a) ← (1 − α)·Q(s,a) + α·[ r(s,a) + γ · maxa' Q(s',a') ]
其中 **γ(gamma)**是 discount factor(折扣因子),衡量当前奖励与未来奖励之间的相对偏好(γ 越接近 1,越重视长期奖励)。
学习算法伪代码
有了上面的方程,就可以写出学习算法的伪代码:
- 用同一个数字初始化所有 state 和 action 的 Q-Table Q;
- 学习率 α ← 1;
- 重复多次仿真:
- 从随机位置开始;
- 循环:
- 在 state s 下选择动作 a;
- 执行动作,移动到新的 state s';
- 若出现游戏终止条件,或累计奖励过小——结束仿真;
- 在新状态下计算奖励 r;
- 按 Bellman 方程更新 Q 函数:Q(s,a) ← (1−α)Q(s,a) + α(r + γ·maxa'Q(s',a'));
- s ← s';
- 更新累计奖励并衰减 α。
Exploration vs. Exploitation:探索与利用的平衡
上面的算法中“第 2.1 步如何选动作”并没有明确。两种极端:
- 随机选择动作:会随机地 explore(探索) 环境,探入平常不去的区域,但也经常会死掉;
- 利用已知的 Q-Table 值(exploit):在 state s 下直接选 Q 值最高的动作。这能避免走回头路,但也可能堵死通往其他 state 的通道,导致找不到最优解。
最优方案是在 exploration 与 exploitation 之间取得平衡。实现方式是:在 state s 下,按与 Q-Table 值成比例的概率选择动作。训练初期所有 Q 值相同,这退化为随机选择;随着环境知识的积累,agent 更大概率沿最优路径走,同时仍保留一定概率去探索未走过的路径。
Python 完整实现
实现学习算法之前,先需要一个把 Q-Table 中任意数值转换为动作概率向量的函数:
def probs(v,eps=1e-4):
v = v-v.min()+eps
v = v/v.sum()
return v
当向量的所有分量都相等时,为避免在原向量上除以 0,先给原向量加一个很小的 eps。(对照源码:rlboard.py 中也自带一个 probs 工具函数,rlboard.py,其做法是先减去最小值再归一化,未做 eps 防护;notebook 版本加了 eps 以保证数值稳定。)
然后用 5000 个 epochs 运行学习算法(代码块 8):
for epoch in range(5000):
# Pick initial point
m.random_start()
# Start travelling
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) # we allow player to move outside the board, which terminates episode
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
逐行拆解这段训练循环,可以印证伪代码的每一步:
- 动作选择:
v = probs(Q[x,y])把当前格子的 4 个 Q 值归一化成概率,random.choices(..., weights=v)按概率抽取动作——这就是上文“按 Q 值比例探索/利用”的落地实现; - 越界处理:
m.move(dpos, check_correctness=False)允许 Peter 走出棋盘。对照 rlboard.py 的move实现,check_correctness=False时is_valid检查被跳过,位置可以直接越界;随后reward里m.is_valid(pos)为假,返回end_reward,从而结束本回合; - 终止条件:单步得到
end_reward(撞上狼/水/越界),或累计奖励cum_reward < -1000(每步 −0.1 的“步数惩罚”累积到 −1000 意味着已浪费约 1 万步)时终止,并把本回合步数n记入lpath列表; - 学习率衰减:
alpha = np.exp(-n / 10e5)按回合内步数指数衰减——训练后期用很小的学习率微调 Q 值,对应伪代码中“衰减 α”; - Bellman 更新:
Q[x,y,ai] = (1-alpha)*Q[x,y,ai] + alpha*(r + gamma*Q[下一格].max()),其中gamma = 0.5,Q[下一格].max()即方程中的max_{a'} Q(s',a')。
算法跑完后,Q-Table 中的每个值都定义了对应步骤中各动作的吸引力。可以按“想移动的方向”在每个格子里画出向量来可视化 Q-Table——简单起见,讲义用小圆点代替箭头头部(m.plot(Q) 的内部机制即上文提到的 image() 方法,对四个方向按概率加权求和得到期望位移):
策略验证:严格贪心策略与“死循环”陷阱
Q-Table 保存了每个 state 下每个动作的“吸引力”,因此用它定义一个能力更强的导航策略相当容易。最简单的情况是直接选 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)
多次运行这段代码你会发现,它偶尔会“hang(挂起)”,需要点击 notebook 的 STOP 按钮中断。原因是从最优 Q-Value 的角度看,可能存在两个 state 互相“指向”对方的情况,此时 agent 会在这两个 state 之间无限移动——纯贪心策略缺乏跳出局部循环的机制,这恰好反衬出保留探索概率的必要性。
随机化 Q 策略:结合利用与探索的最终导航策略
更好的导航策略是把 exploitation 和 exploration 结合起来——这正是训练时使用的策略:按 Q-Table 值成比例的特定概率选择每个动作。这个策略虽然可能让 agent 回到已经探索过的地方,但如代码所示,结果是到达目标位置的平均路径变得非常短(回忆 print_statistics 会跑 100 次仿真,代码块 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)
运行后,平均路径长度应落在 3–6 步的区间——相比随机游走的 30–40 步是一个数量级的提升。
学习过程调查:平均路径长度曲线揭示了什么
如前所述,学习过程的核心是在“对环境结构已有知识”与“继续探索”之间保持平衡。观察训练过程中平均路径长度如何变化也很有启发(讲义展示的运行轨迹图见 lpathlen1.png):
对这条曲线可以概括出三点:
- 平均路径长度先增长。一开始平均长度上升,很可能是因为对环境了解不多时,会频繁踩到水或狼等坏状态;随着知识积累,Peter 能存活得更久、探索得更深,但仍不清楚苹果长在哪里。
- 学得越多,路径越短。学得足够后,agent 更容易达成目标,路径长度开始下降;但由于仍在探索,它会偏离最优路径、尝试新选项,使路径略长于严格最优。
- 突然的长度飙升。曲线中偶发的突然变长体现了过程的随机性:某些点上 Q-Table 系数可能因被新值覆盖而“变坏”。学习率随时间惯性衰减可以缓解这一问题(例如训练末尾只用很小的值微调 Q-Table)。
总体上,学习过程的成败与质量高度依赖于学习率、学习率衰减方式、折扣因子等参数。训练时需要优化的这类量(区别于 Q-Table 系数等“parameters”)常被称为 hyperparameters(超参数);寻找最优超参数值的过程称为 hyperparameter optimization(超参数优化),足以作为一个独立主题展开。
挑战练习与进阶作业
挑战 1:修改 walk 函数,把路径最大步数限制为一个特定值(如 100),并观察代码偶尔会返回这个上限值的情形。
挑战 2:修改 walk 函数,让 agent 不再回到已经去过的地方。这能防止 walk 无限循环,但 agent 仍然可能被“困”在无退路的区域。
进阶作业见 A More Realistic World:让 Peter 的世界更贴近现实——移动消耗能量并累积疲劳,吃苹果补充能量,走进树或草地(绿色格子)可以消除疲劳,并且 Peter 需要在满足一定能量与疲劳条件的前提下找到并击败狼。作业要求以 notebook.ipynb 为起点,按新规则重写奖励函数、重新训练,并与随机游走策略对比胜负场次;由于“战胜狼”是稀有事件,可能还要调大 epochs 等超参数。
小结
这篇讲义用一个 8×8 网格世界完整走通了 Q-Learning 的教学闭环:从 rlboard.py 提供的可复现环境出发,先建立随机游走基线(平均 30–40 步),再通过奖励函数定义目标,借助 Q-Table 与 Bellman 方程把“延迟奖励”问题转化为可迭代的更新公式,用 5000 个 epochs 的仿真训练出策略(平均 3–6 步),并从路径长度曲线上分析了探索/利用平衡与学习率衰减的作用。整套代码均可在 notebook.ipynb 与 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 StartedRust0625
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




