首页
/ ML-For-Beginners 强化学习实战:Q-Learning 训练“Peter”在 8×8 网格世界里采苹果、避狼

ML-For-Beginners 强化学习实战:Q-Learning 训练“Peter”在 8×8 网格世界里采苹果、避狼

2026-09-06 20:50:03作者:史锋燃Gardner

本文基于微软 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 平衡与超参数调优的实战经验。

Peter 的世界环境:8×8 棋盘,包含水、树、苹果、狼与 Peter 本人

强化学习的基本概念: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=4rlboard.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)

Peter 的随机游走过程动图

两个关键观察:

  1. 已知“到最近的苹果大约需要 5–6 步”,但随机游走的平均路径长度却在 30–40 步左右——相当低效;
  2. 上图展示了随机游走过程中 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))可以把它可视化到棋盘上:

Q-Table 初始状态:每个格子四个方向等概率,因此只画出一个点

每个格子中心会画出方向“箭头”;由于四个方向初始完全相同,rlboard.pyimage() 方法(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,越重视长期奖励)。

学习算法伪代码

有了上面的方程,就可以写出学习算法的伪代码:

  1. 用同一个数字初始化所有 state 和 action 的 Q-Table Q;
  2. 学习率 α ← 1;
  3. 重复多次仿真:
    1. 从随机位置开始;
    2. 循环:
      1. 在 state s 下选择动作 a
      2. 执行动作,移动到新的 state s'
      3. 若出现游戏终止条件,或累计奖励过小——结束仿真;
      4. 在新状态下计算奖励 r
      5. 按 Bellman 方程更新 Q 函数:Q(s,a) ← (1−α)Q(s,a) + α(r + γ·maxa'Q(s',a'))
      6. s ← s'
      7. 更新累计奖励并衰减 α。

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.pymove 实现,check_correctness=Falseis_valid 检查被跳过,位置可以直接越界;随后 rewardm.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.5Q[下一格].max() 即方程中的 max_{a'} Q(s',a')

算法跑完后,Q-Table 中的每个值都定义了对应步骤中各动作的吸引力。可以按“想移动的方向”在每个格子里画出向量来可视化 Q-Table——简单起见,讲义用小圆点代替箭头头部(m.plot(Q) 的内部机制即上文提到的 image() 方法,对四个方向按概率加权求和得到期望位移):

训练后学到的 Q-Table:每个格子上的点表示期望移动方向

策略验证:严格贪心策略与“死循环”陷阱

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.ipynbrlboard.py 中逐步复现,适合作为强化学习入门的第一段可运行实现。

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