第 12 章 · 学习是怎么发生的

强化学习:试错里学策略

第二部分一路走到这里,我们讲的几乎都是监督学习:每道题旁边都贴着标准答案, 模型照着把损失降下来就行(第 4 章损失、第 6 章反传)。 但“学习”不止这一种。下棋、打游戏、机器人走路、甚至日后对齐大模型(RLHF)——这些任务 没有逐条的标准答案,只有“这一步做得好不好”的反馈(奖励)。 这一章用大白话搭起强化学习(Reinforcement Learning, RL)的骨架, 让你日后读到 RLHF、PPO(第 20 章)时,不再觉得它们是凭空冒出来的黑魔法。 配套仓库里还有一个可运行的井字棋 Q-learning demo(src/bin/rl_tictactoe),本章多处会对照它的源码讲。

读完这一章,你会明白

  • 监督学习与强化学习差在哪,为什么有些任务没法靠“标注答案”解决;
  • 跟着一只走迷宫的小老鼠,建立起 状态 / 动作 / 奖励 / 策略 / 价值 的直觉;
  • 回合、轨迹、时间步分别指什么,以及稀疏奖励为什么让 RL 难训;
  • 回报为什么要给未来“打折”(折扣 γ),以及贝尔曼方程如何把“长远价值”拆成“眼前一步 + 下一步”;
  • 探索与利用为什么永远是一对矛盾,ε-greedy 怎么折中;
  • Q-learning 怎么用一张表把价值从终点倒灌回起点(含多步手算与 Q 表演化);
  • SARSA 与 Q-learning 差在哪(on-policy / off-policy);
  • 状态太多 → DQN(经验回放、目标网络);动作太多 → 策略梯度 / Actor-Critic / PPO;
  • 一条典型的 RL 训练循环长什么样;
  • 对照仓库 rl_tictactoe(第 25 章 逐行精读),把环境、Q 表、ε-greedy、TD 更新和训练主循环对上号;
  • 第 20 章RLHF 如何对应到本章五件套。

1. 监督学习之外:只有“好不好”,没有“标准答案”

回忆第 0 章:监督学习像有老师批改作业——输入一封邮件,标签告诉你“垃圾/正常”; MNIST 给你一张图,标签告诉你“这是数字 7”。每一步都有明确的正确答案, 损失函数就是在量“离正确答案有多远”。

可很多事情压根没有逐步的标准答案。教小孩骑车,你没法在每一瞬间告诉他“车把该左转 3.7 度”; 你能做的,是让他自己蹬、自己试,摔了(负反馈)就少来点、稳住往前(正反馈)就多来点。 强化学习就是把这种“试错 + 奖惩”写成算法。

它的设定是:一个智能体(agent)环境里不断做选择, 环境不告诉你“唯一正确答案”,只在你做完之后给一个奖励(reward)——可能是 +1(吃到金币)、−1(撞墙)、 或者拖到最后才揭晓的终局分(棋赢了 +1、输了 −1)。你的目标不是拟合某个标签,而是学一套出招习惯, 让长期累积的奖励尽量大。

监督学习(本部分主线)强化学习(本章)
训练信号每条样本都有标签 / 目标值环境给的奖励(可能延迟、可能稀疏)
学什么输入 → 输出的映射在什么状态下该采取什么动作(策略)
数据从哪来事先收集好的固定数据集智能体和环境互动在线产生
反馈时机每一步立刻知道对错可能走了很多步,最后才给一个分
典型例子MNIST、语言模型预测下一个 token(第 19 章)AlphaGo、机器人控制、RLHF 对齐(第 20 章)
为什么它也算“学习是怎么发生的”

监督学习和强化学习,是机器学习的两大学习范式(还有个“无监督”管发现结构,本书不展开)。 它们的底层还是这一部分那套:用一个网络算输出、定义一个“越大越好/越小越好”的目标、 再用反向传播去调参数。区别只在于:目标从“贴好的标签”换成了“环境给的奖励”。

2. 一个贯穿全章的例子:老鼠走迷宫

抽象的词先放一放。我们用一个最小的例子把所有概念串起来:一只小老鼠在一条四格走廊里找奶酪。

格子0起点
·
格子1
·
格子2
·
格子3奶酪 +10

老鼠从格子 0 出发,每步可以往。走到格子 3(奶酪)得 +10 分,回合结束;其余每步得 0 分。

老鼠一开始什么都不懂,只会乱走。但每次它偶然摸到奶酪、拿到那 +10,就会稍微记住“刚才那条路好像不错”。 反复试很多回合,它就慢慢学会了:不管在哪个格子,一直往右走准没错。 这套“在每个格子该往哪走”的习惯,就是我们要学的东西。

3. 智能体与环境:一个不停转的循环

强化学习的一切,都发生在智能体环境之间这个循环里:

智能体 agent老鼠:看格子、挑方向
动作 a往左 / 往右
环境走廊:决定下一格
新状态 s′ + 奖励 r到了哪、得几分

智能体做动作 → 环境返回新状态奖励 → 智能体再做下一个动作……循环往复,直到回合结束。

数学家把这类“状态—动作—奖励”的循环形式化成马尔可夫决策过程(MDP)。别被名字唬住,拆开就是五个直觉概念, 全能在迷宫里找到对应:

概念一句话在迷宫里是
状态 s当前局面老鼠现在在第几格(0/1/2/3)
动作 a你能做的选择往左、往右
奖励 r环境即时打的分到奶酪 +10,其余 0
转移做了动作,下一步去哪在格子1往右 → 到格子2
折扣 γ未来的奖励打几折(0~1)下一节细讲

“马尔可夫”只是说:下一步去哪,只取决于“现在这一格”,和你之前怎么绕过来的没关系。迷宫正好满足。

回合、轨迹与时间步

上面那个循环会重复很多次,直到环境说“结束了”。从起点到终点的这一整趟,叫一个回合(episode); 其中每一步留下的“状态—动作—奖励”序列,叫轨迹(trajectory)。强化学习通常不是只训一步, 而是让智能体跑完许多回合,从大量轨迹里统计“什么习惯长期更赚”。

回合开始 t=0 s₀ a₀,r₀ t=1 s₁ a₁,r₁ t=2 s₂ a₂,r₂=+10 结束 奶酪 轨迹 τ = (s₀,a₀,r₀) → (s₁,a₁,r₁) → (s₂,a₂,r₂) 算法用整条轨迹(或其中片段)更新 Q 或策略

老鼠走廊里,一次“从格子 0 走到奶酪”就是一个回合;路上每一步的 (s, a, r) 连起来就是轨迹

稀疏奖励:为什么 RL 常常很难训

我们的迷宫里,只有到奶酪才给 +10,中间全是 0——这叫稀疏奖励(sparse reward)。 智能体在摸到奶酪之前,几乎收不到“你走对了”的信号,只能靠随机乱撞偶然碰运气;价值要从终点倒灌很多步才能传到起点(第 7 节)。 若改成每往右一步就给 +1,就是密集奖励(dense reward),学起来会快得多,但也可能学到“绕圈刷分”的歪路。 真实任务(下棋赢一局才 +1、对话写完才由裁判打分)往往更接近稀疏奖励,这也是 RL 比监督学习更“费样本、费调参”的原因之一。

和代码怎么对应?

仓库 TicTacToeEnv 就是第 3 节这个循环:StepAgent 让 X 落子,StepOpponent* 让 O 回应, 返回的 StepResult 里带着 rewarddone 和下一状态的 agent_state_key。 第 12 节会把五件套和源码逐项对齐。

4. 回报:为什么要给未来“打折”

强化学习优化的从来不是“眼前这一步的奖励”,而是从现在起、一路加下去的总奖励,叫回报(return)。 但直接把未来所有奖励原样相加有两个毛病:游戏可能无限长(和会发散),而且“眼前的 10 分”通常比“十步之后的 10 分”更值钱。 解决办法是给未来的奖励逐步打折:

回报 Gt = rt + γ·rt+1 + γ²·rt+2 + γ³·rt+3 + … γ(0~1)是折扣因子:越远的奖励,乘的 γ 次方越多、缩得越小

γ 就像“利率的反面”。取 γ = 0.9:下一步的奖励只算 0.9 倍,两步后算 0.81 倍…… 于是老鼠会倾向尽快拿到奶酪,而不是绕远路——因为绕得越久,那 +10 被折得越狠。 γ 越接近 1 越“有耐心、看长远”;越接近 0 越“只顾眼前”。

手算一笔回报

仍用老鼠走廊、γ=0.9。假设某次轨迹是:在格子 0 往右(r=0) → 格子 1 往右(r=0) → 格子 2 往右(r=+10,回合结束)。 从格子 0 出发这一刻算起的回报是:

G0 = 0 + 0.9·0 + 0.9²·10 = 8.1 眼前 0 分 + 打折后的未来;若 γ=1 则是 10,γ=0 则只算眼前 0

强化学习要学的,正是“从每个状态出发,按当前策略走下去,预期能拿多少 G”——这就是下一节的价值

5. 策略与价值:到底在学什么

RL 里有两个核心量,别混淆:

直觉上,离奶酪越近的格子越“值钱”。设 γ=0.9,一个学好的老鼠对“往右”这个动作的价值大概是这样(离终点越近,价值越高):

状态(格子)Q(格子, 往右)为什么
格子210再走一步就到奶酪,直接拿 +10
格子19要两步,+10 被折一次:0.9 × 10 = 9
格子08.1要三步,再折一次:0.9 × 9 = 8.1

价值像“奶酪的香味”:在终点最浓,越往回越淡(每退一格乘一次 γ)。有了这张表,策略就现成了——每步挑价值最高的动作即可。

价值和策略,一体两面

只要你知道了每个“状态+动作”的价值 Q,策略就白送:在每个状态,挑 Q 最大的那个动作走就行。 所以很多算法(如下面的 Q-learning)干脆只学 Q,策略是顺带得到的。

贝尔曼方程:把“长远”拆成“眼前一步 + 下一步”

价值不是拍脑袋填的,它满足一条递推关系,叫贝尔曼方程(Bellman equation)。 对动作价值 Q 而言,在最优策略下可以写成:

Q*(s,a) = E[ r + γ · maxa Q*(s′,a′) ] “这一步的奖励 + 打折后下一步的最好选择”= 这一步的真实长期价值

右边正是 Q-learning 更新公式里方括号中的目标值。算法并不知道真值 Q*,而是用每次试错得到的 r + γ·max Q(s′,·)逼近它——这叫时序差分(TD)学习: 不必等整局下完(像蒙特卡洛那样把 G 全加起来),每走一步就能用“下一步的估计”修正“这一步的估计”。

Q(s,a)这一步值多少?
=
r即时奖励
+
γ·max Q(s′,·)下一步最好还能拿多少

贝尔曼思想:任何一格的价值,都能拆成“眼前奖励 + 打折后的未来”。Q-learning 每次用新样本把左边往右边靠拢。

6. 探索与利用:走老路,还是试新路?

这里有个绕不开的两难。老鼠已经发现“往右能吃到奶酪”,那它是该一直走这条已知的路(利用 exploitation), 还是偶尔试试没走过的方向(探索 exploration),万一有更近的近道?

最常用的平衡办法叫 ε-greedy:大部分时候(概率 1−ε)走当前最优,偶尔(概率 ε)随机试一个。 就像去餐厅:多数时候点你爱吃的招牌菜(利用),偶尔手一抖点个没吃过的(探索)——万一发现新宠呢。 训练前期把 ε 调大(多探索),后期慢慢调小(多利用),是常见套路。

训练阶段典型 ε行为
刚开始,Q 表全是 00.8 ~ 1.0几乎乱走,先把走廊摸熟
中期,终点附近 Q 已有值0.1 ~ 0.3多数走已知好路,偶尔试岔路
后期,策略基本定型0 ~ 0.05几乎总走最优,只做微调

ε 不必手动写死,也可以按步数线性或指数衰减;核心是先广后窄

掷骰子随机数 u ~ [0,1]
u < ε ?探索分支
是 →
随机动作左或右各 50%
否 →
argmax Q(s,·)利用:挑 Q 最大

ε-greedy 决策:先决定“试新路还是走老路”,再选具体动作。

src/deeplearning/rl/tabular_q_learning.cpp · SelectAction(精简)
if (explore && config_.epsilon > 0.0) {
  double sample = random.CreateRandom() / 1000000.0;
  if (sample < config_.epsilon)                                    1
    return TicTacToeEnv::ChooseRandomAction(legal_actions, ...);   // 探索
}
return ArgmaxQ(state_key, legal_actions);                          2
  1. 以概率 ε 从合法动作里随机挑一个——对应本节“偶尔试新路”。
  2. 否则在合法动作里挑 Q 最大的那个——对应“走当前已知最优”。井字棋里合法动作就是棋盘上还没子的 0–8 位置。

7. Q-learning:用一张表,把价值从终点倒灌回起点

上面那张漂亮的价值表,老鼠怎么自己算出来?这就是 Q-learning 干的事。 它维护一张 Q(s,a) 表(一开始全填 0),每走一步就用这条更新公式修一下对应的格子。 Q-learning 是离策略(off-policy)的:更新时方括号里用的是“下一步理论上最好的动作” (maxa′ Q(s′,a′)),不必等于你实际会走的动作——所以表学的是最优价值,和当前乱探索的策略可以脱钩。

Q(s,a) ← Q(s,a) + α · [ r + γ · maxa Q(s′,a′) − Q(s,a) ] α 是学习率(步子多大);方括号里是 TD 目标 − 旧估计,叫 TD 误差 δ

公式可以读成三句话:① r + γ·max Q(s′,·) 是走完这一步后对价值的新估计(目标); ② 减去旧的 Q(s,a) 得到误差 δ; ③ 用学习率 α 把旧值往目标挪一点。 这和第 6 章里“预测 − 标签 = 误差,再反传”是同一套逻辑,只是这里的“标签”来自环境和自己对未来的估计,不是人工标注。

src/deeplearning/rl/tabular_q_learning.cpp · Update(精简)
double old_q = QRow(state_key)[action];
double target = reward;
if (!terminal)
  target += config_.gamma * MaxQ(next_state_key, legal_actions_next);  1
QRow(state_key)[action] = old_q + config_.alpha * (target - old_q);    2
  1. MaxQ 就是在下一状态的合法动作里取 max——正是 Q-learning 的 off-policy 目标 r + γ·max Q(s′,·)
  2. target − old_q 是 TD 误差;乘以 α 写回 Q 表。Q 表用 unordered_map<state_key, vector<double>> 懒创建,没见过的局面默认全 0。

初始 Q 表长什么样

四格走廊、每格两个动作(左/右),表只有 4×2=8 个格子。训练开始前通常全 0(表示“还不知道好不好”):

往左往右
格子000
格子100
格子200
格子3(终点)

终点不再行动,一般不更新。边界格“往左”可能撞墙(本例简化为不动且 r=0)。

手算:从终点往回渗

设 α=0.5、γ=0.9,表全是 0。下面按时间顺序看几次关键更新(老鼠每次都碰巧选对了“往右”):

第 1 次——在格子2 往右,一步到奶酪, r=+10。格子3 是终点,之后没得走, max Q(格子3,·)=0:

Q(格子2, 右) ← 0 + 0.5 · [ 10 + 0.9·0 − 0 ] = 5

第 2 次——又在格子2 往右(旧 Q 已是 5):

Q(格子2, 右) ← 5 + 0.5 · [ 10 − 5 ] = 7.5 → 再来几次 → 10

第 3 次——在格子1 往右,到格子2, r=0。此时 max Q(格子2,·) 已是 10:

Q(格子1, 右) ← 0 + 0.5 · [ 0 + 0.9·10 − 0 ] = 4.5 → … → 9

第 4 次——在格子0 往右,到格子1, r=0, max Q(格子1,·)=9:

Q(格子0, 右) ← 0 + 0.5 · [ 0 + 0.9·9 − 0 ] = 4.05 → … → 8.1
Q₀右≈8.1 Q₁右≈9 Q₂右=10 +10 价值沿“往右”链从奶酪 ← 一格一格回传

一旦终点旁的动作学到真值,它就成为前一格更新公式里的“下一步价值”,形成倒灌

精彩之处在于:没有人给过任何一格“标准答案”,全是试错 + 贝尔曼式更新自己算出来的。 若老鼠偶尔往左绕远,只要最终还能到奶酪,终点附近的 Q 仍会先涨起来,再慢慢纠正中间格子的估计。

收敛后的 Q 表与策略

往左往右最优动作
格子0≈08.1
格子1≈09
格子2≈010

每行挑 Q 最大的动作,就得到策略“一路向右”。学 Q,策略免费送。

SARSA:若下一步按“实际会走的动作”来更新

与 Q-learning 几乎并列的还有 SARSA。差别只在方括号里:不用 max Q(s′,·), 而用下一步真正采取的动作 a′ 的 Q 值:

Q(s,a) ← Q(s,a) + α · [ r + γ · Q(s′,a′) − Q(s,a) ] SARSA 是 on-policy:估计的是“当前这套出招习惯(含探索)”下的价值
Q-learningSARSA
下一步用谁maxa′ Q(s′,a′) 最优动作实际会执行的 a′
on / off-policyoff-policy(可边乱探索边学最优)on-policy(学的是当前策略)
直觉“假设以后每一步都走最好”“假设以后仍按现在的习惯(含偶尔乱试)走”
悬崖行走更敢贴悬崖抄近路(因假设以后最优)更保守绕远(因担心探索时踩空)

走廊例子太简单,两者差别不大;遇到探索有风险的环境,SARSA 往往更稳,Q-learning 往往更激进。知道这对概念,读论文时就不会被 on/off-policy 绊住。

8. 状态太多怎么办:用神经网络近似(DQN)

四格走廊只有 4 个状态,一张表绰绰有余。可要是状态是一整屏雅达利游戏画面呢?可能的画面近乎无穷,表根本存不下。 办法很自然:别存表了,训一个神经网络 Qθ(s,a)——输入状态(像素),输出每个动作的 Q 值。这就是 DQN(Deep Q-Network)

游戏画面 s高维状态
卷积 / 全连接Q 网络 θ
[Q(s,←), Q(s,→), …]每个动作一个分数
argmax 或 ε-greedy选动作

表 lookup 换成函数逼近:同一个网络在所有状态上共享参数,相似画面会得到相似 Q。

训练目标和 Q-learning 一样,只是把表上的 TD 误差换成网络的均方误差:

损失 L(θ) = [ Qθ(s,a) − ( r + γ · maxa Qθ(s′,a′) ) ]² θ 是目标网络(下面解释);对 θ 做反向传播最小化 L

注意:这里用的还是第 3 章的网络、第 6 章的反向传播,没有发明新优化器。 区别只在于“标签”从人工标注换成了自己试错算出来的 TD 目标 r + γ·max Q(s′,·)

经验回放(experience replay)

智能体连续玩游戏的相邻帧高度相关(这一帧和下一帧几乎一样),若每步都立刻拿来训网络,梯度会严重偏、训练不稳。 DQN 把每次转移 (s,a,r,s′) 存进一个回放缓冲区,训练时随机抽一批旧经验来算损失—— 相当于把轨迹打碎、混洗,让样本更接近独立同分布,和第 11 章里打乱 minibatch 是同一精神。

目标网络(target network)

若 TD 目标里的 Q(s′,·) 和正在更新的 Qθ同一个网络,目标会跟着参数一起动, 像“追自己的影子”,容易发散。DQN 复制一份目标网络 θ,每隔固定步数才从主网络同步一次; 算目标时用 θ,算预测时用 θ——目标在一段时间内保持相对稳定,训练就稳得多。

和 supervised 对照

监督学习:输入 x,标签 y 固定,最小化 (ŷ−y)²。
DQN:输入 s,动作 a,标签是动态的 r+γ·max Q(s′,·),且随训练慢慢变准——这叫自举(bootstrapping)

DeepMind 2015 年用这套办法让 AI 仅看像素就学会玩几十种雅达利游戏,是深度强化学习的里程碑。

9. 动作太多怎么办:直接学策略(策略梯度 / Actor-Critic / PPO)

Q-learning 每步都要 max Q(s′,·)——把所有动作比一遍挑最大。动作只有“左/右”时没问题; 可要是动作空间大得吓人呢?比如语言模型每一步要从几万个 token 里挑一个(第 19 章), 为每个 token 维护 Q 并取 max,代价和存储都难以接受。

这时换个思路:不去估每个动作的价值,直接学“出招习惯” πθ(a|s)—— 一个网络输入状态,输出动作概率分布(语言模型里就是下一个 token 的 softmax)。

策略梯度:回报高,就加大该动作概率

最朴素的 REINFORCE 算法:跑完一整局,若总回报 G 高,就对轨迹里每个 (s,a) 增大 πθ(a|s);回报低就减小。梯度里会出现 log π,直觉是“在概率空间里往高回报方向挪”。 缺点:要等一局结束才有 G,方差大、学得慢。

θ J ≈ Σtθ log πθ(at|st) · Gt 不必背公式:记住“log 概率 × 回报”驱动参数更新即可

Actor-Critic:演员出招,评论家打分

Actor-Critic 把两个网络拆开:Actor(演员) 就是策略 πθ,负责选动作; Critic(评论家) 估计价值 V(s) 或 Q(s,a),负责告诉演员“这一步比平均好还是差”。 用优势 A = G − V(s)(或 TD 残差) 代替裸回报 G,方差小很多,也不必等整局结束—— Critic 用 TD 更新,Critic 的反馈指导 Actor 更新。DQN 只有“评论家味”;Actor-Critic 是策略 + 价值双头并进。

状态 s
Actor πθ选动作 a
环境r, s′
Critic Vφ这步好不好?

Actor 改策略,Critic 提供基线/优势,两者一起训。

PPO:别一次更新迈太大

纯策略梯度容易“步子迈太大”——一次更新就把好不容易学到的策略带偏。PPO(近端策略优化) 在 Actor-Critic 上加两道保险:

正因为实现简单、训练相对稳定,PPO 成了第 20 章 RLHF 与许多大模型后训练里的默认选项之一。 语言场景里,动作是离散 token,策略网络就是语言模型本身,输出层 softmax 即 π(·|s)。

10. 一条典型的 RL 训练循环

把前面散落的步骤收成一张“流水线”,大多数 Q-learning / DQN / PPO 代码都是这个壳子:

强化学习训练循环 · 示意伪代码
初始化 策略 π 或 Q 网络、环境 env
重复很多个回合:
  s = env.reset()                          // 回到起点           1
  重复直到回合结束:
    用 ε-greedy 或 π 采样动作 a            // 探索或按策略出招   2
    s′, r, done = env.step(a)              // 环境反馈           3
    存 (s,a,r,s′) 到回放缓冲区(若用 DQN)   4
    用 TD 目标或策略梯度更新参数           // 核心学习步         5
    s = s′
  可选: 衰减 ε、同步目标网络、记录回报曲线
  1. 每个回合从初始状态开始;老鼠走廊里就是回到格子 0。
  2. 前期多探索,后期多利用;PPO 则按概率分布采样 token。
  3. 环境只给即时奖励和下一状态,不给“标准动作”。
  4. 表格式 Q-learning 可跳过;DQN 几乎必用回放。
  5. Q-learning 改表/网络;PPO 改 Actor,并常同时训 Critic。

第 6 章监督训练对比:那里 epoch 遍历固定数据集;这里数据是智能体自己玩出来的, 分布随策略变化(非平稳),所以更依赖回放、目标网络、PPO 裁剪这类稳定技巧。

上面是通用壳子。仓库里 TabularQLearning::RunEpisode 把“智能体走一步 → 对手走一步 → 更新 Q”直接写进了一个函数:

src/deeplearning/rl/tabular_q_learning.cpp · RunEpisode(精简)
env.Reset();
while (!env.IsTerminal()) {
  int state_key = env.AgentStateKey();
  int action = SelectAction(state_key, env.LegalActions(), true);     1
  env.StepAgent(action, agent_step);
  if (agent_step.done) {
    Update(state_key, action, agent_step.reward, -1, {}, true);      2
    break;
  }
  env.StepOpponentRandom(opponent_action, opponent_step);            3
  if (opponent_step.done) {
    Update(state_key, action, opponent_step.reward, -1, {}, true);
    break;
  }
  Update(state_key, action, 0.0, opponent_step.agent_state_key,
         env.LegalActions(), false);                                  4
}
  1. Reset 开新回合;SelectAction(..., true) 开启 ε-greedy 探索。
  2. 若智能体这一步直接终局(如井字棋连成三子),立刻用终局奖励更新 Q,terminal=true
  3. 否则环境再让对手走一步——训练时对手可以是随机或 minimax 最优。
  4. 中间步奖励为 0,但要把“对手走完后、轮到我再走”的局面当作 s′ 去 bootstrap 未来价值。

11. 回到大模型:RLHF 就是一次强化学习

绕了一圈,回到本书主线。你在第 20 章会读到,大模型训练的最后一步 RLHF(基于人类反馈的强化学习), 用的正是本章这套语言——只要把五个概念对号入座,它立刻就不神秘了:

RLHF 用 RL 语言怎么说?

状态 s = 用户的问题 + 已经生成的前缀;动作 a = 下一个 token; 奖励 r = 整段回答写完后,由一个“奖励模型”(学人类喜好训出来的裁判)打的分; 策略 π = 当前这个大模型;算法 = PPO,一边提高“高分回答”的概率,一边用 KL 拴住它别偏离原模型太远。
所以 RLHF 不是什么新范式,就是把大模型当成一个在“对话环境”里试错的智能体,让它朝“人更喜欢”的方向调。 拴得太紧或奖励模型有偏,就会付出第 20 章说的对齐税——更安全听话,但某些能力、创造性可能打折。

预训练 LM初始策略
生成回答轨迹 τ
奖励模型打分 r
PPO 更新 π+ KL 约束

RLHF 流水线:模型自己采样动作序列,裁判给延迟奖励,PPO 调策略。细节见第 20 章

最近火的推理模型(如 o1、DeepSeek-R1)也沿用这套:对数学、代码这类有标准答案的题, 用“答对给正奖励、答错给负奖励”的强化学习,让模型自己摸索出好的推理套路(第 20 章)。可见强化学习这套“靠奖励试错”的思想,正越来越深地嵌进大模型。

12. 对照真实代码:井字棋环境怎么拼起来

第 2 节的老鼠走廊是纸上例子;配套项目用井字棋把同一套 MDP 五件套落到代码里。 下面是与源码的简要对照;完整逐行导读见实战部分的 第 25 章(与第 23 章 MNIST、 第 24 章 mini-LM 同一写法)。

本章概念老鼠走廊井字棋 demo代码位置
状态 s在第几格当前棋盘局面TicTacToeEnv::AgentStateKey()
动作 a左 / 右落子位置 0–8LegalActions()
奖励 r到奶酪 +10赢 +1 / 输 −1 / 和 0ApplyMove 里赋值
策略 / QQ 表unordered_map Q 表TabularQLearning
训练循环多回合试错RunEpisode × Ndemo/rl_tictactoe/main.cpp
src/deeplearning/rl/tic_tac_toe_env.cpp · 状态编码(精简)
int TicTacToeEnv::EncodeBoardKey(const std::array<Cell, 9> &board) {
  int key = 0, base = 1;
  for (int i = 0; i < 9; i++) {
    key += static_cast<int>(board[i]) * base;   // 空=0, X=1, O=2   1
    base *= 3;
  }
  return key;
}
  1. 9 格棋盘按三进制压成一个整数 state_key,当作 Q 表的行号。只在轮到 X 落子时编码,和 Q-learning“从智能体视角”一致。
src/deeplearning/rl/tic_tac_toe_env.cpp · ApplyMove 奖励(精简)
board_[action] = player;
result_ = CheckResult();
out.done = IsTerminal();

if (result_ == GameResult::X_WIN)      out.reward = 1.0;              1
else if (result_ == GameResult::O_WIN)   out.reward = -1.0;
else if (result_ == GameResult::DRAW)    out.reward = 0.0;

if (!out.done) {
  agent_turn_ = !agent_turn_;
  out.agent_state_key = agent_turn_ ? AgentStateKey() : -1;           2
}
  1. 环境只负责“判输赢 + 给分”,不负责教该怎么下——这正是 RL 与监督学习的分界。
  2. 若没结束,交换行棋方,并返回下一轮智能体行动时state_key,供 RunEpisode 做 TD 更新。
src/demo/rl_tictactoe/main.cpp · 训练入口(精简)
TabularQLearning agent;
agent.Init(config);                              // α, γ, ε 等超参     1
TicTacToeEnv env;

for (int episode = 0; episode < option.episodes; episode++) {
  agent.RunEpisode(env, train_opponent, stats);  // 第 10 节内循环       2
  agent.DecayEpsilon();
}
auto eval = agent.Evaluate(env, OpponentType::RANDOM, eval_games); 3
  1. 超参与第 7 节手算一致:alpha=0.5gamma=0.99、ε 从 1.0 指数衰减到 0.05。
  2. 外层只是重复很多回合;每回合内的 reset / step / update 全在 RunEpisode 里。
  3. Evaluateexplore=false,纯 greedy 下棋,统计对随机 / 最优对手的胜率和。
自己跑一个

配套项目 demo 在 src/demo/rl_tictactoe。在 src/ 目录编译后运行: ./bin/rl_tictactoe --episodes 30000 --eval-games 1000 --show-sample。 默认 3 万局对随机对手训练后,greedy 评估约 99% 胜率;对 minimax 最优对手可稳守和棋。 加 --play 可在 stdin 里下一盘(你是 O,输入 0–8)。 更全的参数说明见仓库 docs/rl-tictactoe-demo.md;逐行精读见 第 25 章

小结

  • 强化学习是与监督学习并列的另一种学习范式:没有标准答案,靠奖励在试错中学策略;数据由智能体与环境在线产生。
  • 五件套(MDP):状态 s、动作 a、奖励 r、转移、折扣 γ;回合是从起点到结束的一趟,轨迹是其中的 (s,a,r) 序列。
  • 优化目标是打过折的长期回报 G;贝尔曼方程把价值写成“即时奖励 + 打折后的下一步”。
  • 策略 π 是“该怎么出招”,价值 Q 是“这么出招值多少”;知道 Q 后策略即 argmaxa Q(s,a)。
  • 稀疏奖励让学习变慢;ε-greedy 在探索与利用间折中。
  • Q-learning(off-policy) 用 TD 目标把价值从终点倒灌回起点;SARSA(on-policy) 用实际下一步动作更新。
  • 状态太多 → DQN:神经网络近似 Q,配合经验回放目标网络稳定训练。
  • 动作太多 → 策略梯度 / Actor-Critic / PPO 直接学 π;PPO 用裁剪与 KL 限制更新幅度。
  • 典型训练循环:reset → 采样动作 → step → 更新 Q;RunEpisode 封装内层,main.cpp 外层重复多回合。
  • RLHF(第 20 章)= 大模型当策略、奖励模型当裁判、PPO 当优化器的一次强化学习。
  • TicTacToeEnv + TabularQLearningsrc/demo/rl_tictactoe;第 25 章 逐行精读。

动手与思考

问题 1:强化学习和监督学习最本质的区别是什么?

训练信号不同。监督学习每条样本都有标准答案(标签),损失衡量“离答案多远”;强化学习没有逐步答案,只有环境给的奖励(可能延迟、可能稀疏),目标是最大化长期累积回报,学的是“在什么状态做什么动作”的策略。

问题 2:为什么回报要引入折扣 γ?

两个原因:①防止无限长的任务里奖励加起来发散;②让“眼前的奖励”比“很久以后的奖励”更值钱,促使智能体尽快达成目标。γ 越接近 1 越看长远,越接近 0 越只顾眼前。

问题 3:Q-learning 里“价值从终点倒灌回起点”是什么意思?

更新公式 Q(s,a) ← Q(s,a)+α[r+γ·max Q(s′,·)−Q(s,a)] 依赖“下一状态的最好价值”。终点旁边的动作最先学到真值(直接拿奖励),然后它成为更前一格的“下一状态价值”,于是价值一格格往回传播,最终从奖励所在的终点渗到起点——全程没有人工标注,靠试错自己算出。

问题 4:探索与利用为什么是矛盾?ε-greedy 怎么折中?

只利用已知最优,可能错过更好的路;只探索则永远拿不到稳定高分。ε-greedy 让智能体以概率 1−ε 选当前最优(利用)、以概率 ε 随机试(探索),通常前期 ε 大、后期 ε 小。

问题 5:动作空间巨大(如语言模型的几万 token)时,为什么偏爱策略梯度/PPO 而不是 Q-learning?

Q-learning 每步要 max Q(s′,·),得把所有动作比一遍;动作有几万个时代价太大。策略梯度直接用网络输出各动作的概率、按“回报高就加大概率”来学,不必逐个估价值。PPO 再加裁剪和 KL 惩罚保持更新稳定,因此成为 RLHF 的常用算法。

问题 6:把 RLHF 翻译成 RL 的五件套,分别对应什么?

状态 = 问题 + 已生成前缀;动作 = 下一个 token;奖励 = 整段回答由奖励模型打的分;策略 = 当前大模型;算法 = PPO(加 KL 约束别偏离原模型太远)。本质就是让大模型在“对话环境”里试错,朝“人更喜欢”的方向调。

问题 7:Q-learning 和 SARSA 更新公式差在哪?各是 on-policy 还是 off-policy?

Q-learning 用 r + γ·maxa′ Q(s′,a′),假设下一步走最优,是 off-policy;SARSA 用 r + γ·Q(s′,a′),其中 a′ 是实际会执行的动作,是 on-policy。前者更激进地学最优价值,后者更贴合当前探索策略。

问题 8:DQN 为什么要经验回放和目标网络?

相邻游戏帧高度相关,立刻训练会导致梯度偏、不稳定;回放把旧转移打乱再抽样,样本更接近独立。目标网络让 TD 目标在一段时间内固定,避免“追自己移动的靶子”导致发散。两者都是为深度 Q 学习提供稳定训练。

问题 9:仓库里井字棋 demo 的 state_key 怎么编码?奖励在哪给?

TicTacToeEnv::EncodeBoardKey 把 9 格棋盘按三进制(空/X/O → 0/1/2)压成一个整数,作为 Q 表键;只在轮到智能体 X 时编码。ApplyMove 在判赢/判输/判和后设置 reward=+1/−1/0,中间步为 0。TabularQLearning::Update 用第 7 节 TD 公式写回 Q 表。

两大学习范式都认识了。下一部分换个视角:不同的数据,适合不同结构的网络—— 我们去认识处理图像的 CNN 和处理序列的 RNN,先从卷积开始。