首页
/ ML-For-Beginners 强化学习实战:用 Q-Learning 教会智能体在网格世界中寻宝避狼

ML-For-Beginners 强化学习实战:用 Q-Learning 教会智能体在网格世界中寻宝避狼

2026-09-06 18:35:01作者:田桥桑Industrious

本篇文章取材于经典机器学习入门课程 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 × heightmatrix 用于存放格子类型,并保存 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=5num_wolves=1num_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.pyBoard 类中;而课程 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′。我们将获得:

  1. 由奖励函数定义的即时奖励 r(s,a)
  2. 一定的未来奖励。

若假设 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;
  • 重复多次模拟:
    1. 从随机位置出发;
    2. 循环执行:
      1. 在状态 s 选择动作 a
      2. 执行动作,转移到新状态 s′
      3. 若触发回合结束条件或累计奖励过小,则退出本次模拟;
      4. 计算新状态下的奖励 r
      5. 按贝尔曼方程更新 Q 函数:Q(s,a) ← (1−α)Q(s,a) + α( r + γ max_{a′}Q(s′,a′) )
      6. s ← s′
      7. 更新累计奖励并减小 α。

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 表可视化如下:

训练后 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 与奖励函数基础上自行扩展:

  1. 移动会消耗能量(energy)并累积疲劳(fatigue)
  2. 吃苹果能恢复能量;
  3. 站到树/草丛(绿色地块)休息能消除疲劳;
  4. 彼得需要主动找到并击杀狼
  5. 击杀狼需要能量与疲劳达到一定门槛,否则战斗失败。

文档给出的实现提示非常实用:

  • 新世界的状态比原来复杂——除位置外还包括疲劳与能量水平,可把状态表示成 (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——带着「如果棋盘更大、目标更多、奖励更稀疏,这套流程还成立吗」的问题动手改一版,是掌握强化学习最快的方式。

登录后查看全文
热门项目推荐
相关项目推荐