首页
/ ML-For-Beginners 强化学习实战:为 Q-Learning 的"Peter 与狼"世界加入能量与疲劳机制

ML-For-Beginners 强化学习实战:为 Q-Learning 的"Peter 与狼"世界加入能量与疲劳机制

2026-09-06 19:41:02作者:史锋燃Gardner

本文基于 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 条新规则

  1. 移动消耗能量、累积疲劳:Peter 每从一处移动到另一处,会损失 energy 并增加 fatigue
  2. 吃苹果回能量:Peter 通过吃苹果获得更多能量;
  3. 树或草地可消除疲劳:走进有树或草地(绿色格子)的位置即可休息,消除疲劳;
  4. 终极目标是击败狼:Peter 需要找到狼并杀死它;
  5. 战斗胜负取决于体能:只有能量和疲劳达到一定水平才能战胜狼,否则战斗失败。

作业给出的指令(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()

Peter 与狼的 8x8 棋盘环境:蓝色为水、绿色为树/草地、红苹果为食物、黑狼为决战对象

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 -= 1self.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.1goal_reward = 10end_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

设计上值得推敲的三点:

  1. 终局奖惩用 ±100 的极端值:胜负是"稀有事件",只有把终局奖励的量级拉到远超普通步奖励,Bellman 更新才能把"赢"的信号逆向传播回之前的每一步;
  2. 普通步奖励用 energy - fatigue 而非固定负值:这让奖励函数与规则 1/5 天然耦合——状态越"健康"(能量高、疲劳低),当步奖励越高,Agent 会在探索中自发地偏向"多休息、多进食、少走冤枉路"的路径,而不需要显式地为树和苹果写额外奖励项;
  3. 水保持 -100 死惩罚:与基础课一致,淹没即终局。

六、Q-Learning 训练:稀有事件下的算法与超参数

Q-Learning 的核心仍是 Bellman 方程(沿用基础课的推导,见 8-Reinforcement/1-QLearning/README.md):

Q-Learning 使用的 Bellman 方程:Q(s,a) = r(s,a) + γ·max_{a'}Q(s',a')

其中 γ 是折扣因子,权衡即时奖励与未来奖励。初始化 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"标准):

  1. epoch 是否足够——把 10000 继续上调,观察 won 计数是否上升;
  2. 能量/疲劳的初始值与恢复量energy=10、吃苹果回到 10、energy -= 1fatigue += 1 的步长)是否让"获胜"事件过稀有;
  3. α 衰减速度——衰减过快可能停在次优 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 的终局结构。

相关仓库资源:

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