📘 第8章 强化学习和分布式人工智能
📄 一句话概括

前面所有方法都有一份「标准答案」:要么是标签,要么是「这个划分好不好」的分数。 强化学习把这份答案也拿掉,只留一个迟到的分数——走完一串动作之后,才告诉你总共得了多少。 它的核心问题不是「怎么拟合」,是「现在的选择,怎么跟很久以后的结果挂上钩」。答案是值函数: 把将来的回报打折折回现在,然后每一步都挑「折回来最大」的那个。

📝 本节和前面几节的关系

这一节是第 8 章的重心,也是全书唯一一节「连训练数据都要自己造」的内容。 监督学习的数据是别人给的;这里的数据是智能体自己走出来的——走法不对,数据就没有。 它跟第 5 章是同一件事的两种说法: 5.2 的 A* 是在已知地图上找最短路,这里的地图未知,只能边走边试。连「探索与利用」的张力, 都跟 5.3 遗传算法里「变异率不能太小」是同一类问题。

一、没有答案,只有一个迟到的分数

先把这一节的五个词对齐。任何强化学习问题都由这五样东西组成:

要素 Demo 里对应什么 作用
状态 网格上的一个格子 此刻在哪
动作 上下左右四个方向 此刻能做什么
奖励 到终点 +10、掉陷阱 −5、每步 −0.1 做对做错的唯一信号
策略 每个格子该往哪走 要学的东西
折扣因子 γ 隔一步的奖励还算几成 多在乎将来
「每步 −0.1」这个设计很关键。它让「绕远路」有代价,于是智能体不只要到终点,还要尽快到。
没有这一条,它会站在原地转圈。
Demo 的起点先用一个「每个格子随便定一个固定动作」的策略试了试:
200 次平均回报 −6.00,到终点 0%。这就是「什么都不学」的基线。
强化学习跟监督学习最本质的差别在这儿:监督学习每一步都有反馈(这个预测对不对),
强化学习只有一个迟到的总分,而且你还不知道是哪一步的功劳。
这叫信用分配问题——一万步里拿到一分,这一分该记在哪一步头上?

二、值函数:把「将来」折回现在

答案是把「将来」折成「现在的一个数」。 定义:一个状态的值 = 从这里出发,将来能拿到的所有奖励的折扣和。 折扣就是 γ 的幂次:隔一步的奖励乘 γ,隔两步乘 γ²……于是「越远的奖励越不值钱」, 而且这个无穷级数一定收敛(γ < 1 时)。 有了值,决策就变得极其简单:看哪个动作走完之后的值最大,就走哪个。这就是贝尔曼那一步: 值(s) = max over 动作 [ 奖励 + γ × 值(下一格) ]。 如果规则已知(知道每个动作会到哪儿),反复套这个式子就能算出全部的值。这叫值迭代。 Demo 的 5 乘 5 网格,γ = 0.95,扫 9 遍就收敛了:

格子 值 怎么读
起点 (0, 0) 6.38 从这里出发,将来能拿 6.38
终点旁边 (4, 3) 10.00 再走一步就是 +10
陷阱 (3, 2) 不参与 踩进去就结束,没有「从这里出发」
整条最优路 8 步、总回报 9.30 10 − 0.1 × 7
注意「值 6.38」和「总回报 9.30」不是一回事。9.30 是不打折的实际得分;
6.38 是把每一步按 0.95 的幂次折回起点之后的数。+10 隔了 8 步,被乘成 6.63。
值迭代能收敛,是因为它是一个「单调收缩」的过程。每次都用旧值算新值,误差被乘上 γ,
γ < 1 时误差自己就缩没了。这也是为什么 γ 不能等于 1。

三、Q-learning:不知道规则,就走一步改一格

值迭代的前提是「知道每个动作会到哪儿」。 现实里常常不知道,只知道走完拿到多少。于是把「状态的值」换成「状态加动作的值」——这就是 Q。 更新式只有一行:Q(s, a) ← Q(s, a) + α × [ r + γ × max Q(下一格) − Q(s, a) ]。 括号里那一项叫 TD 误差:「本来以为能拿多少」和「实际拿到加下一格最好」的差。 它有个很妙的地方:目标里用的 max Q 是它自己的估计。这叫自举—— 用旧的猜测去改新的猜测,一开始全是 0,慢慢地 0 就长出了真值。 Demo 跑了 500 个回合(每回合最多 100 步):

训练回合数 最近 50 回合到终点率 平均回报
1 0.00 −6.00
10 0.00 −6.00
50 0.60 9.30
100 1.00 9.30
500 1.00 9.30
50 回合就到 0.60,100 回合稳在 1.00。学完之后跟值迭代比:21 个格子里 17 个选了同一个动作。
剩下 4 个格子动作不同,可两条路一样长、一样挣钱(都是 8 步、9.30)。
网格里常常有好几条同样好的路,选哪条都不算错。
这也是「怎么评价一个策略」的要点:看值,不是看「每一步有没有跟标准答案一样」。
标准答案本身在强化学习里往往不存在。

四、探索率 ε:不试就不知道

ε 是「随机乱选」的概率。它管的是强化学习里最根本的那个矛盾: 不试就不知道那条路好不好,可一直试就一直在浪费步数。 教科书上的说法是:纯贪心(ε = 0)会卡死在一个角落里。这张 5 乘 5 的图上实测一遍, 看它到底卡不卡:

ε 平均每回合步数 学到的策略到终点 走几步
0.00 9.1 是 8
0.02 9.2 是 8
0.10 9.8 是 8
0.20 10.9 是 8
0.50 16.1 是 8
1.00 46.5 是 8
ε = 0 那一行没有卡死。原因很朴素:第一步朝上撞墙,那个动作的 Q 立刻变负,
argmax 下一次自然就换了动作。确定性网格里,它总能一路试到终点。
这不是算法聪明,是「地图小、规则确定」把这个毛病盖住了。真正拉开差距的是「学会它花了多少步」:
ε = 0 平均 9.1 步一回合,ε = 1.0 要 46.5 步,同样的本事花了五倍多的步数。
地图一大、状态一多,开局那一整片 0 会让它一直沿同一个方向试,
碰不到奖励就永远没有正反馈——那时候「必须探索」才真的成立。
常规做法是让 ε 从大变小(先探索、后利用)。
这跟 5.3 遗传算法里「变异率不能太小」是同一件事:都是「怎么在保持多样性和收敛之间找平衡」。

五、折扣因子 γ:你到底多在乎将来

γ 决定「隔一步的奖励还算几成」。 它是这一节里最容易被当成技术细节、实际上最影响结果的那个旋钮。扫一遍:

γ 起点的值 照着它走能到终点吗 用了几步
0.00 −0.1000 否 60(撞上限)
0.50 −0.1203 是 8
0.90 4.2613 是 8
0.95 6.3800 是 8
0.99 8.6413 是 8
γ = 0 那一行最能说明问题:起点的值只有 −0.1000,跟「撞墙」的值一模一样。
因为远处的 +10 被乘了 0,根本没传回来——每个方向的数都一样,策略等于瞎指。
γ 越大,远处的奖励越值钱,决策越看长远。但γ 太接近 1 有两个麻烦:值会变得很大、收敛变慢,
而且它会开始把「远处的噪声」也当成信号。
实践里 γ 常常取 0.9 到 0.99。这个范围不是拍脑袋:它大约等于「十几步之内的奖励还算钱」,
超过这个视野的信号就被压得听不见了。
最后把这一节收一下。一条主线:
没有答案 → 用值函数把将来折回现在 → 不知道规则就拿步子去试 → 试多少由 ε 和 γ 定。
第 9 章的深度网络,常常就是用来当这里的「值函数」的——状态太多、表格存不下的时候,
就让网络去猜那个数。

六、动手 Demo:没有人告诉你答案,只有分数

五段:网格环境、值迭代、Q-learning、探索率、折扣因子。 第 2 段和第 3 段是同一个问题的两条路:规则已知怎么算,规则未知怎么试。 注意 step 那个函数只有十几行,它就是全部的「世界」。强化学习的所有难度, 都来自「这个函数是黑箱」。

def step(s, a):                      # 环境:只有「走一步、给个分」
    dr, dc = ACT[a]
    t = (s[0] + dr, s[1] + dc)
    if t[0] < 0 or t[0] >= H or t[1] < 0 or t[1] >= W or t in WALL:
        t = s                        # 撞墙就留在原地
    if t in PIT:
        return t, -5.0, True
    if t == GOAL:
        return t, 10.0, True
    return t, -0.1, False

def value_iter(gamma):               # 规则已知:反复用「下一步的值」更新这一步
    V = dict((s, 0.0) for s in FREE)
    for it in range(500):
        new = {}
        for s in FREE:
            best = None
            for a in range(4):
                t, r, done = step(s, a)
                q = r if done else r + gamma * V[t]      # 贝尔曼那一步
                if best is None or q > best:
                    best = q
            new[s] = best
        V = new
        if max(abs(new[s] - V[s]) for s in FREE) < 1e-9:
            break
    return V

# Q-learning:规则未知,走一步改一格
a = eps_greedy(s, 0.2)
t, r, done = step(s, a)
tgt = r if done else r + GAMMA * Q[t].max()   # 用「下一格最好的值」当目标
Q[s][a] += ALPHA * (tgt - Q[s][a])            # 朝目标挪一小步
s = t

实跑输出:

--------------------------------------------------------------------
没有人告诉你答案,只有分数
--------------------------------------------------------------------
==============================================================

[1] 先看环境:一张 5 乘 5 的网格

  符号:S 起点、G 终点(+10)、X 陷阱(−5)、# 墙、每走一步 −0.1。

    S . . . . 
    . # . # . 
    . . . # . 
    # . X . . 
    . . . . G 

  能站的状态 21 个(去掉 4 面墙),每个状态 4 个动作,一共 84 个格子要填。
  **跟前面几节最大的不同:这里没有「正确答案」。**没人告诉你哪一步对,
  **只有走完之后拿到的一个分数。这叫延迟回报——现在的选择,结果要过几步才知道。**

  先拿「每个状态随便选个固定动作」的策略跑 200 次:
    平均回报 -6.00,到终点的 0%
  **这个数就是「什么都不学」的水平。下面的所有努力,都是把它抬上去。**

[2] 规则已知时:值迭代,把「将来」折回现在

  γ = 0.95,扫描 9 遍之后每个格子的值不再变化(差值小于 1e-9):

    6.38    6.82    7.29    7.77    8.29    
    6.82    ####    6.82    ####    8.83    
    7.29    7.77    7.29    ####    9.40    
    ####    8.29    陷阱    9.40    10.00   
    8.29    8.83    9.40    10.00   目标    

  从起点照着最优动作走:8 步到终点,总回报 9.30。
  **那个 +10 被打了折扣:隔 8 步,就乘 γ 的 8 次方 = 0.6634,值 6.6342。**
  每一步还要付 0.1,所以整条路的值 = 奖励的折扣和 − 步数 × 0.1。
  **这就是值迭代做的事:把「将来能拿多少」一层层折回现在,直到每个格子的数不再变。**

[3] 规则未知时:Q-learning,一边走一边改主意

  值迭代要「知道每个动作会到哪儿」,现实里常常不知道。
  **Q-learning 只用「走一步、看结果」:**
  **Q(s,a) ← Q(s,a) + α × [r + γ × max Q(s',·) − Q(s,a)]**

    训练回合数      最近 50 回合到终点率      平均回报
    ------------------------------------------------------------------
    1               0.00                      -6.00
    10              0.00                      -6.00
    50              0.60                      9.30
    100             1.00                      9.30
    200             1.00                      9.30
    500             1.00                      9.30

  学完之后跟值迭代的最优策略比:21 个格子里有 17 个选了同一个动作(81.0%)。
  除掉终点,还有 2 个格子的动作不一样,可两条路一样长、一样挣钱:
  照 Q 表走 8 步到终点、总回报 9.30;值迭代那条路 8 步、9.30。
  **网格里常常有好几条同样好的路,选哪条都不算错。**
  **一个算法「学会没学会」,看的是值,不是「每一步有没有跟标准答案一样」。**

[4] 探索率 ε:这张小地图,盖住了教科书上的那个坑

  ε 是「随机乱选」的概率。**教科书说:纯贪心(ε=0)会卡在一个角落里出不来。**
  这张 5 乘 5 的图上实测一遍,看它到底卡不卡:

    ε       平均每回合步数      学到的策略到终点      走几步
    ----------------------------------------------------------------------
    0.00    9.1                 是                    8
    0.02    9.2                 是                    8
    0.10    9.8                 是                    8
    0.20    10.9                是                    8
    0.50    16.1                是                    8
    1.00    46.5                是                    8

  **ε=0 那一行没有卡死。**原因很朴素:第一步朝上撞墙,那个动作的 Q 立刻变负,
  **argmax 下一次自然就换了动作。**确定性网格里,它总能一路试到终点。
  **这不是算法聪明,是「地图小、规则确定」把这个毛病盖住了。**
  真正拉开差距的是「学会它花了多少步」:ε=0 平均每回合 9.1 步,
  ε=1.0 要 46.5 步——**同样的本事,花了 5 倍的步数。**
  地图一大、状态一多,开局那一整片 0 会让它一直沿同一个方向试,
  **碰不到奖励就永远没有正反馈——那时候「必须探索」才真的成立。**
  常规做法是让 ε 从大变小(先探索、后利用),
  这跟第 5 章模拟退火里「温度慢慢降」是同一个思路。

[5] 折扣因子 γ:你到底多在乎「将来」

  γ 决定「隔一步的奖励还算几成」。扫一遍,每个 γ 都跑值迭代,再看走出来的路:

    γ       起点的值      走到终点    用了多少步    总回报
    ------------------------------------------------------------------
    0.00    -0.1000       否          60            -6.00
    0.50    -0.1203       是          8             9.30
    0.90    4.2613        是          8             9.30
    0.95    6.3800        是          8             9.30
    0.99    8.6413        是          8             9.30

  **γ=0 那一行:起点的值只有 -0.1000,跟「撞墙」的值一模一样。**
  照着它走,一直走到上限 60 步也没到终点。
  **因为 γ=0 等于「只在乎眼前这一步」,远处的 +10 被乘了 0,根本没传回来。**
  每个方向的数都一样,**策略等于瞎指。**
  **γ 越大,远处的奖励越值钱,决策越看长远。**
  **但 γ 太接近 1,值会变得很大、收敛变慢**,而且容易把远处的噪声也当信号。

  四段连起来:**有奖励没答案 → 用值函数把将来折回来 → 不知道规则就拿步子去试。**
  **试多少由 ε 和 γ 定。**
  **跟监督学习最大的差别就在这:它没有「这一步对不对」的标签,只有一个迟到很久的分数。**

第 2 段那张 V 表值得逐格看一遍。越靠近终点的格子值越大(10.00 就在终点旁边), 离得远的就小(起点 6.38)——这正好是「折扣」的视觉化:距离就是折价。 第 3 段那个「50 回合 0.60、100 回合 1.00」不是平滑曲线。强化学习的学过程经常是这样: 很久没动静,然后在某一次偶然撞到终点之后,信息突然沿着那条路往回传,成绩就跳上去。 第 4 段那个「ε = 0 没卡死」是这次实测最有价值的发现。 教科书没错,但它描述的是「大状态空间 + 稀疏奖励」的情形; 这张 5 乘 5 的图上,撞一次墙就足以让它换动作。 所以看任何「某某方法会失败」的结论,都得问一句:在什么条件下失败。 第 5 段 γ = 0 那一行是个干净的失败案例。值全是 −0.1、策略乱指, 说明「折扣」不是可选的修饰,是把远处的信息拉回眼前的那根绳子。把 γ 设成 0,等于把绳子剪断。 最后一句提醒:这段代码没用神经网络,Q 就是一张 21 乘 4 的表。真实问题的状态是连续的、 成千上万维,表存不下——那才需要第 9 章的深度网络来当近似函数。

30 秒实验

  1. 把陷阱从 1 个加到 3 个(再加 (3, 3)、(1, 2)),重跑第 2 段。 最优路会绕开它们,起点那个值会掉下来——因为绕路要付更多的 −0.1。
  2. 把每步的 −0.1 改成 0,重跑第 2 段。 所有路的值得一样,智能体不再「赶时间」, 最优策略会变得不唯一——这一改就说明「步数惩罚」是在给「快」定价。
  3. 把 ε 从固定值改成随回合线性衰减(从 1.0 降到 0.05),跟固定 0.2 比。 通常前 50 回合学得更快——因为它一开始就敢乱走,后面又不再浪费步子。

常见误区

  1. 以为强化学习有「标准答案」。它只有奖励。 策略对不对,看的是「照这么走值多少」, 不是「有没有跟某条标准路径一样」——Demo 里 Q-learning 和值迭代走的就是两条不同的路。
  2. 以为奖励设计是小事。每步 −0.1 这一条,决定了智能体会不会绕远; 把奖励写错,学出来的行为就会荒腔走板(这是强化学习落地最常见的坑)。
  3. 以为 ε 越小越「好」。ε = 0 在这张图上碰巧没事,但它的平均步数是最少的恰恰因为环境小; 在没有正反馈的大环境里,它就是死路。
  4. 以为 Q-learning 只需要「多跑几轮」。它要的是探索过的状态。 没走到过的格子,Q 永远是 0,永远学不出来。
自测三问(点开对答案) 1. 值迭代和 Q-learning 都要「把将来的奖励折回现在」,它们差在哪? —— **差在「知不知道规则」。**值迭代用 `step(s, a)` 的返回值直接算「下一格的值」, **所以它要求环境是已知的(基于模型)。**Q-learning 只从实际走过的那一步里学, **连「这个动作会到哪儿」都是走出来的。**Demo 里两条路都收敛到 8 步、9.30, **但一个用 9 遍扫描算出来,另一个用 500 个回合试出来。** 2. 为什么 γ = 0 的时候,智能体连终点都走不到? —— 因为**值迭代的传递全靠 γ 这个乘数**。 γ = 0 时,远处的 +10 被乘成 0,**信息传不回起点**——每个格子的值都变成「这一步的奖励」, 全是 −0.1,**所有动作一样好,策略等于瞎指。**Demo 里起点的值正好是 −0.1000, **跟撞墙的值一模一样,这就是「看不见将来」的样子。** 3. 为什么说「信用分配」是强化学习最难的地方? —— 因为**只有一个迟到的总分,不知道是哪一步挣来的。**Demo 里走到终点只给一个 +10, **前面那 7 步每步都是 −0.1、看上去都在扣分**, **可它们其实是必要的。**值函数和 TD 误差就是用来解决这个的: **把「这一步之后还能拿多少」一点点往回传,让每一步都分到它该得的那份。**

集成学习管「把一堆弱模型合起来」,强化学习管「没有答案时怎么试」。 下一节换一个更实际的问题:已经在一个任务上学好的东西,能不能搬到另一个任务上? 那就是迁移学习——它省的不是训练时间,是标注数据。

相关笔记