首页
/ ML-For-Beginners 强化学习作业实战:为「彼得与狼」Q-Learning 世界加入能量与疲惫机制

ML-For-Beginners 强化学习作业实战:为「彼得与狼」Q-Learning 世界加入能量与疲惫机制

2026-09-04 18:16:38作者:虞亚竹Luna

本篇指南围绕 ML-For-Beginners 课程第 8 周「Peter and the Wolf」强化学习作业(A More Realistic World)展开:在原始棋盘世界中引入能量(energy)与疲惫(fatigue)机制,重新定义状态空间与奖励函数,用 Q-Learning 训练出能击杀狼的获胜策略,并保留随机游走基线做量化对比。读完并动手完成后,你将掌握「状态扩展—奖励重定义—超参数调整—策略评估」这一完整的环境改造流程,这是把 Q-Learning 应用到任何带资源约束的 MDP 问题的通用套路。

Peter 的世界棋盘环境

任务背景:原始世界与基线算法

本作业建立在第 8 周第一课的「彼得与狼」路径规划世界之上(详见 课程讲义课程笔记本)。原始设定中,Peter 在一块 width x height 的棋盘上行走,格子类型由 rlboard.pyBoard.Cell 枚举定义:empty(空地)、water(水域,不可进入)、wolf(狼)、tree(树,绿色休息区)、apple(苹果)。原始目标仅仅是避开狼与水域、找到苹果,奖励函数只有三档:

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

Q-Learning 侧的 Q-Table 形状为 width x height x len(actions),初始化为均匀值 0.25(等价于随机游走策略):

Q = np.ones((width, height, len(actions)), dtype=np.float) * 1.0 / len(actions)

训练循环按 Bellman 方程更新:Q(s,a) ← (1-α)·Q(s,a) + α·(r + γ·max_a' Q(s',a')),动作按 Q 值概率化采样以平衡探索与利用(probs() 函数)。基线策略是随机游走,其平均路径长度约为 30–40 步,远高于最近苹果约 5–6 步的理论距离。本作业要求在这个基础上把世界「变得更真实」,并验证强化学习在新规则下依然有效。

新世界规则:完整继承的 5 条设定

根据 assignment.zh-cn.md 的定义,假想中 Peter 原本几乎可以一直走动而不感到疲倦或饥饿;在更真实的世界里,需要时不时坐下休息、也要吃东西。为此引入以下 5 条规则:

  1. 从一个地方走到另一个地方,Peter 失去能量(energy)并获得疲惫(fatigue)
  2. Peter 可以通过吃苹果来获得更多能量;
  3. Peter 可以通过在树下或草地上休息来消除疲惫(即走进有树和草的棋盘位置——绿色格子);
  4. Peter 需要找到并杀死狼
  5. 为了杀死狼,Peter 需要保持一定级别的能量与疲惫(能量要占优),否则他会输掉这场战斗。

注意规则 4 带来的本质变化:终点从「摘到苹果」变成了「击杀狼」,苹果降级为恢复能量的补给品,树/草地(Board.Cell.tree)从普通可走格子升级为恢复疲劳的功能格子。这意味着状态、转移、终止条件和奖励函数全部需要重做。

作业要求与三条关键提示

作业的「说明」部分(assignment.zh-cn.md)给出了硬性要求,逐条列出以便对照执行:

  1. 起点:使用原始 notebook.ipynb 笔记本作为解决方案的起点;
  2. 核心工作:根据游戏规则修改奖励函数,运行强化学习算法学习赢得游戏的最佳策略,并在赢/输场次上将自己的算法与随机游走算法对比
  3. 提示一(状态表示):新世界中状态更复杂,除位置外还包括疲惫和能量水平。你可以选择把状态表示为元组 (Board, energy, fatigue),或为状态定义一个类(也可以从 Board 派生),甚至直接修改 rlboard.pyBoard 的源码;
  4. 提示二(保留基线):方案中必须保留负责随机走动策略的代码,并在最后将两者结果对比;
  5. 提示三(超参数):可能需要调整超参数才能跑通,尤其是 epoch 数——因为游戏成功(与狼搏斗)是一个罕见事件,需要更长的训练时间。

状态建模:官方参考方案的 state 类

仓库在 solution/assignment-solution.ipynb 中提供了参考实现。它没有去改 Board 源码,而是定义了一个独立的 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        # 规则 3:树上/草地上休息消除疲惫
        if self.at() == Board.Cell.apple:
            self.energy = 10        # 规则 2:吃苹果恢复能量

    def move(self, a):
        self.board.move(a)
        self.energy -= 1             # 规则 1:每走一步失去 1 点能量
        self.fatigue += 1            # 规则 1:每走一步增加 1 点疲惫
        self.update()

    def is_winning(self):
        return self.energy > self.fatigue   # 规则 5:能量占优才算赢下战斗

几个实现细节值得注意:

  • 规则 1–3 全部收敛在 move()update():移动先改变 energy/fatigue,再调用 update() 让格子效果生效,顺序保证了「走进树格即清疲惫、走进苹果格即回满能量」的语义清晰;
  • random_start() 复用自 Board:从 rlboard.py 源码看,random_start() 会在 Cell.empty 格子上随机落子(不会落在狼、苹果或水上),这保证了初始局面合法;棋盘布局本身由 randomize(seed=13) 生成,num_wolves=1num_apples=3num_trees=5 等参数可调,实验时建议固定 seed 以便两次对比(随机 vs Q-Learning)使用同一张地图;
  • 规则 4、5 体现在终止判定is_winning() 简单地用 energy > fatigue 判胜。你可以按作业要求把它改成更严格的阈值(例如能量必须高于某个固定值),这正是作业鼓励自行设计的空间。

另外,state 只持有 board 的引用而不复制它,训练循环中反复 state(m) 重建时棋盘布局保持不变、仅重置能量/疲惫与起点,这是保持实验一致性的关键。

基线改造:面向胜负统计的随机游走

原始 notebook 中的 walk() 以「到苹果」为成功,作业要求以「击杀狼」为成功、以溺水/战斗失败为失败,因此基线本身也要重写。参考方案的做法:

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     # 成功击杀!
            else:
                return -n    # 战斗失败!
        if s.at() == Board.Cell.water:
            return 0        # 溺水死亡
        a = actions[policy(m)]
        s.move(a)
        n += 1

walk(m, random_policy)

返回值语义被编码进符号:正数=击杀步数,负数=战斗失败,零=溺水。统计函数相应扩展为三类计数,并满足作业「保留随机游走代码并在最后对比」的要求:

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)

先运行这段基线,记录 100 局中的获胜/战败/溺水分布——这就是你最终要超越的数字。

奖励函数重定义:把「资源差」变成稠密信号

新世界的奖励函数不再只有三档终点奖励,而是把 energy - fatigue 的差值作为中间步的稠密奖励,让每一步的「资源状态好坏」都直接反馈给 Q-Table:

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

设计逻辑:

  • 终端事件 ±100:击杀狼 +100、战败 -100、溺水 -100,量级远大于中间奖励,保证「结果导向」依然压倒一切;
  • 中间步返回 energy - fatigue:这是一条天然的塑形(shaping)信号——吃苹果会抬升该值、连续赶路会压低该值,等于把作业规则 1–3 的资源经济直接翻译成了梯度方向,帮助稀疏奖励问题中「难得才发生一次的击杀」传播信用;
  • 你完全可以按作业要求尝试自己的版本(例如按疲惫程度惩罚、对休息格给小额正奖励),并用「胜率是否提升」来检验哪种塑形更合理——这正是评判标准中「奖励函数是否完全定义」的考察点。

Q-Learning 训练循环与超参数调整

Q-Table 的结构不需要变——作业提示三中关于「元组/类/改 Board 源码」的三种状态表示方案,参考方案选择了独立 state 类,Q-Table 仍按 (width, height, len(actions)) 索引(以棋盘位置为主键,能量与疲惫通过奖励间接进入学习信号):

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

训练主循环与原始 notebook 几乎同构,只把「走到即终止」改成了「进入狼格/水格才终止」:

lpath = []

for epoch in range(10000):
    # Pick initial point
    s = state(m)

    # Start travelling
    n = 0
    cum_reward = 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

m.plot(Q)

对照作业提示三,这里有三个值得动手调的超参数:

超参数 参考值 调整依据
epoch 数 10000(原始世界为 5000) 击杀狼是罕见事件,正样本稀少,需要更长训练时间让「通往狼格且资源占优」的经验被反复强化
学习率衰减 alpha = exp(-n/3000) 衰减尺度 3000 步 原始世界用 exp(-n/10e5) 几乎不衰减;新世界步内奖励波动大,更快的衰减能在训练后期稳定 Q 值、减少「覆盖破坏」
折扣因子 gamma 0.5 资源状态的价值高度依赖近期决策,较大折扣让 agent 更看重眼前能量/疲惫的变化

训练收敛后的 Q-Table 可视化(m.plot(Q))应该呈现指向狼格的偏好流向:

训练后的 Q-Table 可视化

策略评估:与随机游走对比胜负

评估阶段复用同一套 walk + print_statistics 框架,只替换策略函数。与原始世界相同的两种策略选项在这里同样适用:

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)
  • qpolicy(概率采样):保留少量探索,参考方案实测它能显著减少溺水次数,但 Peter 仍不能每次都赢——作业明确鼓励你继续调超参数去改善这一结果;
  • 贪心版本 qpolicy_strict(直接取 argmax)可作对照,但注意原始课程提到的「两个状态互指导致死循环」问题在新世界里依然存在,建议像课程 Challenge Task 1 那样给 walk 加上最大步数限制,避免统计被卡死拖垮。

对比口径要严格遵循作业要求:在同一张地图(固定 seed=13)、同样 100 局下,比较随机策略与 Q 策略的 won / killed by wolf / drown 三项分布。判定「显著改善」时,最好把随机基线多跑几组(不同 seed)给出波动范围,避免单局运气造成的误判。

评判标准(Rubric)

作业给出了三档评分标准,对照它自检你的方案:

标准 优秀 中规中矩 仍需努力
笔记本上有新世界规则的定义、Q-Learning 算法和一些文字解释;与随机游走相比,Q-Learning 能够显著改善结果 介绍了 Notebook,实现了 Q-Learning 并与随机走动算法相比提高了结果,但不显著;或者 notebook 文档不完善、代码结构不合理 有一些重新定义世界规则的尝试,但 Q-Learning 算法不起作用,或奖励函数没有完全定义

从这份 rubric 可以提炼出「优秀档」的交付清单:

  1. 笔记本开头有 5 条新规则的完整文字定义(本文第二节即模板);
  2. 状态表示、walk/终止判定、奖励函数、Q-Learning 主循环四个代码块齐全,且保留随机游走代码;
  3. 末尾有同一张地图下的胜负对比输出与结论性文字解释;
  4. 对超参数(epoch、学习率衰减、gamma)有明确的调整记录——因为「击杀」是罕见事件,这段记录本身就是评分证据。

延伸与相关资源

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341