第 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 出发,每步可以往左或右。走到格子 3(奶酪)得 +10 分,回合结束;其余每步得 0 分。
老鼠一开始什么都不懂,只会乱走。但每次它偶然摸到奶酪、拿到那 +10,就会稍微记住“刚才那条路好像不错”。 反复试很多回合,它就慢慢学会了:不管在哪个格子,一直往右走准没错。 这套“在每个格子该往哪走”的习惯,就是我们要学的东西。
3. 智能体与环境:一个不停转的循环
强化学习的一切,都发生在智能体和环境之间这个循环里:
智能体做动作 → 环境返回新状态和奖励 → 智能体再做下一个动作……循环往复,直到回合结束。
数学家把这类“状态—动作—奖励”的循环形式化成马尔可夫决策过程(MDP)。别被名字唬住,拆开就是五个直觉概念, 全能在迷宫里找到对应:
| 概念 | 一句话 | 在迷宫里是 |
|---|---|---|
| 状态 s | 当前局面 | 老鼠现在在第几格(0/1/2/3) |
| 动作 a | 你能做的选择 | 往左、往右 |
| 奖励 r | 环境即时打的分 | 到奶酪 +10,其余 0 |
| 转移 | 做了动作,下一步去哪 | 在格子1往右 → 到格子2 |
| 折扣 γ | 未来的奖励打几折(0~1) | 下一节细讲 |
“马尔可夫”只是说:下一步去哪,只取决于“现在这一格”,和你之前怎么绕过来的没关系。迷宫正好满足。
回合、轨迹与时间步
上面那个循环会重复很多次,直到环境说“结束了”。从起点到终点的这一整趟,叫一个回合(episode); 其中每一步留下的“状态—动作—奖励”序列,叫轨迹(trajectory)。强化学习通常不是只训一步, 而是让智能体跑完许多回合,从大量轨迹里统计“什么习惯长期更赚”。
老鼠走廊里,一次“从格子 0 走到奶酪”就是一个回合;路上每一步的 (s, a, r) 连起来就是轨迹。
稀疏奖励:为什么 RL 常常很难训
我们的迷宫里,只有到奶酪才给 +10,中间全是 0——这叫稀疏奖励(sparse reward)。 智能体在摸到奶酪之前,几乎收不到“你走对了”的信号,只能靠随机乱撞偶然碰运气;价值要从终点倒灌很多步才能传到起点(第 7 节)。 若改成每往右一步就给 +1,就是密集奖励(dense reward),学起来会快得多,但也可能学到“绕圈刷分”的歪路。 真实任务(下棋赢一局才 +1、对话写完才由裁判打分)往往更接近稀疏奖励,这也是 RL 比监督学习更“费样本、费调参”的原因之一。
仓库 TicTacToeEnv 就是第 3 节这个循环:StepAgent 让 X 落子,StepOpponent* 让 O 回应,
返回的 StepResult 里带着 reward、done 和下一状态的 agent_state_key。
第 12 节会把五件套和源码逐项对齐。
4. 回报:为什么要给未来“打折”
强化学习优化的从来不是“眼前这一步的奖励”,而是从现在起、一路加下去的总奖励,叫回报(return)。 但直接把未来所有奖励原样相加有两个毛病:游戏可能无限长(和会发散),而且“眼前的 10 分”通常比“十步之后的 10 分”更值钱。 解决办法是给未来的奖励逐步打折:
γ 就像“利率的反面”。取 γ = 0.9:下一步的奖励只算 0.9 倍,两步后算 0.81 倍…… 于是老鼠会倾向尽快拿到奶酪,而不是绕远路——因为绕得越久,那 +10 被折得越狠。 γ 越接近 1 越“有耐心、看长远”;越接近 0 越“只顾眼前”。
手算一笔回报
仍用老鼠走廊、γ=0.9。假设某次轨迹是:在格子 0 往右(r=0) → 格子 1 往右(r=0) → 格子 2 往右(r=+10,回合结束)。 从格子 0 出发这一刻算起的回报是:
强化学习要学的,正是“从每个状态出发,按当前策略走下去,预期能拿多少 G”——这就是下一节的价值。
5. 策略与价值:到底在学什么
RL 里有两个核心量,别混淆:
- 策略 π:在状态 s 下该怎么出招,记作 π(a|s)。这是我们最终想要的“习惯手册”——比如“在任何格子都往右”。
- 价值 V / Q:一个状态(或“状态+动作”)到底值多少,也就是从这里出发、按当前策略走下去,预期能拿到多少回报。
V(s) 问“待在 s 有多好”;Q(s,a) 更细,问“在 s 先做动作 a 有多好”。
直觉上,离奶酪越近的格子越“值钱”。设 γ=0.9,一个学好的老鼠对“往右”这个动作的价值大概是这样(离终点越近,价值越高):
| 状态(格子) | Q(格子, 往右) | 为什么 |
|---|---|---|
| 格子2 | 10 | 再走一步就到奶酪,直接拿 +10 |
| 格子1 | 9 | 要两步,+10 被折一次:0.9 × 10 = 9 |
| 格子0 | 8.1 | 要三步,再折一次:0.9 × 9 = 8.1 |
价值像“奶酪的香味”:在终点最浓,越往回越淡(每退一格乘一次 γ)。有了这张表,策略就现成了——每步挑价值最高的动作即可。
只要你知道了每个“状态+动作”的价值 Q,策略就白送:在每个状态,挑 Q 最大的那个动作走就行。 所以很多算法(如下面的 Q-learning)干脆只学 Q,策略是顺带得到的。
贝尔曼方程:把“长远”拆成“眼前一步 + 下一步”
价值不是拍脑袋填的,它满足一条递推关系,叫贝尔曼方程(Bellman equation)。 对动作价值 Q 而言,在最优策略下可以写成:
右边正是 Q-learning 更新公式里方括号中的目标值。算法并不知道真值 Q*,而是用每次试错得到的
r + γ·max Q(s′,·) 去逼近它——这叫时序差分(TD)学习:
不必等整局下完(像蒙特卡洛那样把 G 全加起来),每走一步就能用“下一步的估计”修正“这一步的估计”。
贝尔曼思想:任何一格的价值,都能拆成“眼前奖励 + 打折后的未来”。Q-learning 每次用新样本把左边往右边靠拢。
6. 探索与利用:走老路,还是试新路?
这里有个绕不开的两难。老鼠已经发现“往右能吃到奶酪”,那它是该一直走这条已知的路(利用 exploitation), 还是偶尔试试没走过的方向(探索 exploration),万一有更近的近道?
- 只利用:稳,但可能永远发现不了更好的路(也许左边有个传送门直达奶酪);
- 只探索:一直乱试,永远不安分,拿不到稳定的高分。
最常用的平衡办法叫 ε-greedy:大部分时候(概率 1−ε)走当前最优,偶尔(概率 ε)随机试一个。 就像去餐厅:多数时候点你爱吃的招牌菜(利用),偶尔手一抖点个没吃过的(探索)——万一发现新宠呢。 训练前期把 ε 调大(多探索),后期慢慢调小(多利用),是常见套路。
| 训练阶段 | 典型 ε | 行为 |
|---|---|---|
| 刚开始,Q 表全是 0 | 0.8 ~ 1.0 | 几乎乱走,先把走廊摸熟 |
| 中期,终点附近 Q 已有值 | 0.1 ~ 0.3 | 多数走已知好路,偶尔试岔路 |
| 后期,策略基本定型 | 0 ~ 0.05 | 几乎总走最优,只做微调 |
ε 不必手动写死,也可以按步数线性或指数衰减;核心是先广后窄。
ε-greedy 决策:先决定“试新路还是走老路”,再选具体动作。
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
- 以概率 ε 从合法动作里随机挑一个——对应本节“偶尔试新路”。
- 否则在合法动作里挑 Q 最大的那个——对应“走当前已知最优”。井字棋里合法动作就是棋盘上还没子的 0–8 位置。
7. Q-learning:用一张表,把价值从终点倒灌回起点
上面那张漂亮的价值表,老鼠怎么自己算出来?这就是 Q-learning 干的事。
它维护一张 Q(s,a) 表(一开始全填 0),每走一步就用这条更新公式修一下对应的格子。
Q-learning 是离策略(off-policy)的:更新时方括号里用的是“下一步理论上最好的动作”
(maxa′ Q(s′,a′)),不必等于你实际会走的动作——所以表学的是最优价值,和当前乱探索的策略可以脱钩。
公式可以读成三句话:① r + γ·max Q(s′,·) 是走完这一步后对价值的新估计(目标);
② 减去旧的 Q(s,a) 得到误差 δ; ③ 用学习率 α 把旧值往目标挪一点。
这和第 6 章里“预测 − 标签 = 误差,再反传”是同一套逻辑,只是这里的“标签”来自环境和自己对未来的估计,不是人工标注。
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
MaxQ就是在下一状态的合法动作里取 max——正是 Q-learning 的 off-policy 目标r + γ·max Q(s′,·)。target − old_q是 TD 误差;乘以 α 写回 Q 表。Q 表用unordered_map<state_key, vector<double>>懒创建,没见过的局面默认全 0。
初始 Q 表长什么样
四格走廊、每格两个动作(左/右),表只有 4×2=8 个格子。训练开始前通常全 0(表示“还不知道好不好”):
| 往左 | 往右 | |
|---|---|---|
| 格子0 | 0 | 0 |
| 格子1 | 0 | 0 |
| 格子2 | 0 | 0 |
| 格子3(终点) | — | — |
终点不再行动,一般不更新。边界格“往左”可能撞墙(本例简化为不动且 r=0)。
手算:从终点往回渗
设 α=0.5、γ=0.9,表全是 0。下面按时间顺序看几次关键更新(老鼠每次都碰巧选对了“往右”):
第 1 次——在格子2 往右,一步到奶酪, r=+10。格子3 是终点,之后没得走, max Q(格子3,·)=0:
第 2 次——又在格子2 往右(旧 Q 已是 5):
第 3 次——在格子1 往右,到格子2, r=0。此时 max Q(格子2,·) 已是 10:
第 4 次——在格子0 往右,到格子1, r=0, max Q(格子1,·)=9:
一旦终点旁的动作学到真值,它就成为前一格更新公式里的“下一步价值”,形成倒灌。
精彩之处在于:没有人给过任何一格“标准答案”,全是试错 + 贝尔曼式更新自己算出来的。 若老鼠偶尔往左绕远,只要最终还能到奶酪,终点附近的 Q 仍会先涨起来,再慢慢纠正中间格子的估计。
收敛后的 Q 表与策略
| 往左 | 往右 | 最优动作 | |
|---|---|---|---|
| 格子0 | ≈0 | 8.1 | 右 |
| 格子1 | ≈0 | 9 | 右 |
| 格子2 | ≈0 | 10 | 右 |
每行挑 Q 最大的动作,就得到策略“一路向右”。学 Q,策略免费送。
SARSA:若下一步按“实际会走的动作”来更新
与 Q-learning 几乎并列的还有 SARSA。差别只在方括号里:不用 max Q(s′,·),
而用下一步真正采取的动作 a′ 的 Q 值:
| Q-learning | SARSA | |
|---|---|---|
| 下一步用谁 | maxa′ Q(s′,a′) 最优动作 | 实际会执行的 a′ |
| on / off-policy | off-policy(可边乱探索边学最优) | on-policy(学的是当前策略) |
| 直觉 | “假设以后每一步都走最好” | “假设以后仍按现在的习惯(含偶尔乱试)走” |
| 悬崖行走 | 更敢贴悬崖抄近路(因假设以后最优) | 更保守绕远(因担心探索时踩空) |
走廊例子太简单,两者差别不大;遇到探索有风险的环境,SARSA 往往更稳,Q-learning 往往更激进。知道这对概念,读论文时就不会被 on/off-policy 绊住。
8. 状态太多怎么办:用神经网络近似(DQN)
四格走廊只有 4 个状态,一张表绰绰有余。可要是状态是一整屏雅达利游戏画面呢?可能的画面近乎无穷,表根本存不下。 办法很自然:别存表了,训一个神经网络 Qθ(s,a)——输入状态(像素),输出每个动作的 Q 值。这就是 DQN(Deep Q-Network)。
表 lookup 换成函数逼近:同一个网络在所有状态上共享参数,相似画面会得到相似 Q。
训练目标和 Q-learning 一样,只是把表上的 TD 误差换成网络的均方误差:
注意:这里用的还是第 3 章的网络、第 6 章的反向传播,没有发明新优化器。
区别只在于“标签”从人工标注换成了自己试错算出来的 TD 目标 r + γ·max Q(s′,·)。
经验回放(experience replay)
智能体连续玩游戏的相邻帧高度相关(这一帧和下一帧几乎一样),若每步都立刻拿来训网络,梯度会严重偏、训练不稳。 DQN 把每次转移 (s,a,r,s′) 存进一个回放缓冲区,训练时随机抽一批旧经验来算损失—— 相当于把轨迹打碎、混洗,让样本更接近独立同分布,和第 11 章里打乱 minibatch 是同一精神。
目标网络(target network)
若 TD 目标里的 Q(s′,·) 和正在更新的 Qθ 是同一个网络,目标会跟着参数一起动, 像“追自己的影子”,容易发散。DQN 复制一份目标网络 θ−,每隔固定步数才从主网络同步一次; 算目标时用 θ−,算预测时用 θ——目标在一段时间内保持相对稳定,训练就稳得多。
监督学习:输入 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,方差大、学得慢。
Actor-Critic:演员出招,评论家打分
Actor-Critic 把两个网络拆开:Actor(演员) 就是策略 πθ,负责选动作; Critic(评论家) 估计价值 V(s) 或 Q(s,a),负责告诉演员“这一步比平均好还是差”。 用优势 A = G − V(s)(或 TD 残差) 代替裸回报 G,方差小很多,也不必等整局结束—— Critic 用 TD 更新,Critic 的反馈指导 Actor 更新。DQN 只有“评论家味”;Actor-Critic 是策略 + 价值双头并进。
Actor 改策略,Critic 提供基线/优势,两者一起训。
PPO:别一次更新迈太大
纯策略梯度容易“步子迈太大”——一次更新就把好不容易学到的策略带偏。PPO(近端策略优化) 在 Actor-Critic 上加两道保险:
- 裁剪(clipping):新策略相对旧策略的概率比若偏离 1 太远,就把梯度截断,单次更新幅度受限;
- KL 惩罚:显式惩罚新策略与旧策略的 KL 散度,别让分布一夜之间面目全非。
正因为实现简单、训练相对稳定,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′
可选: 衰减 ε、同步目标网络、记录回报曲线
- 每个回合从初始状态开始;老鼠走廊里就是回到格子 0。
- 前期多探索,后期多利用;PPO 则按概率分布采样 token。
- 环境只给即时奖励和下一状态,不给“标准动作”。
- 表格式 Q-learning 可跳过;DQN 几乎必用回放。
- Q-learning 改表/网络;PPO 改 Actor,并常同时训 Critic。
与第 6 章监督训练对比:那里 epoch 遍历固定数据集;这里数据是智能体自己玩出来的, 分布随策略变化(非平稳),所以更依赖回放、目标网络、PPO 裁剪这类稳定技巧。
上面是通用壳子。仓库里 TabularQLearning::RunEpisode 把“智能体走一步 → 对手走一步 → 更新 Q”直接写进了一个函数:
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
}
Reset开新回合;SelectAction(..., true)开启 ε-greedy 探索。- 若智能体这一步直接终局(如井字棋连成三子),立刻用终局奖励更新 Q,
terminal=true。 - 否则环境再让对手走一步——训练时对手可以是随机或 minimax 最优。
- 中间步奖励为 0,但要把“对手走完后、轮到我再走”的局面当作 s′ 去 bootstrap 未来价值。
11. 回到大模型:RLHF 就是一次强化学习
绕了一圈,回到本书主线。你在第 20 章会读到,大模型训练的最后一步 RLHF(基于人类反馈的强化学习), 用的正是本章这套语言——只要把五个概念对号入座,它立刻就不神秘了:
状态 s = 用户的问题 + 已经生成的前缀;动作 a = 下一个 token;
奖励 r = 整段回答写完后,由一个“奖励模型”(学人类喜好训出来的裁判)打的分;
策略 π = 当前这个大模型;算法 = PPO,一边提高“高分回答”的概率,一边用 KL 拴住它别偏离原模型太远。
所以 RLHF 不是什么新范式,就是把大模型当成一个在“对话环境”里试错的智能体,让它朝“人更喜欢”的方向调。
拴得太紧或奖励模型有偏,就会付出第 20 章说的对齐税——更安全听话,但某些能力、创造性可能打折。
RLHF 流水线:模型自己采样动作序列,裁判给延迟奖励,PPO 调策略。细节见第 20 章。
最近火的推理模型(如 o1、DeepSeek-R1)也沿用这套:对数学、代码这类有标准答案的题, 用“答对给正奖励、答错给负奖励”的强化学习,让模型自己摸索出好的推理套路(第 20 章)。可见强化学习这套“靠奖励试错”的思想,正越来越深地嵌进大模型。
12. 对照真实代码:井字棋环境怎么拼起来
第 2 节的老鼠走廊是纸上例子;配套项目用井字棋把同一套 MDP 五件套落到代码里。 下面是与源码的简要对照;完整逐行导读见实战部分的 第 25 章(与第 23 章 MNIST、 第 24 章 mini-LM 同一写法)。
| 本章概念 | 老鼠走廊 | 井字棋 demo | 代码位置 |
|---|---|---|---|
| 状态 s | 在第几格 | 当前棋盘局面 | TicTacToeEnv::AgentStateKey() |
| 动作 a | 左 / 右 | 落子位置 0–8 | LegalActions() |
| 奖励 r | 到奶酪 +10 | 赢 +1 / 输 −1 / 和 0 | ApplyMove 里赋值 |
| 策略 / Q | Q 表 | unordered_map Q 表 | TabularQLearning |
| 训练循环 | 多回合试错 | RunEpisode × N | demo/rl_tictactoe/main.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;
}
- 9 格棋盘按三进制压成一个整数
state_key,当作 Q 表的行号。只在轮到 X 落子时编码,和 Q-learning“从智能体视角”一致。
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
}
- 环境只负责“判输赢 + 给分”,不负责教该怎么下——这正是 RL 与监督学习的分界。
- 若没结束,交换行棋方,并返回下一轮智能体行动时的
state_key,供RunEpisode做 TD 更新。
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
- 超参与第 7 节手算一致:
alpha=0.5、gamma=0.99、ε 从 1.0 指数衰减到 0.05。 - 外层只是重复很多回合;每回合内的 reset / step / update 全在
RunEpisode里。 Evaluate把explore=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+TabularQLearning见src/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,先从卷积开始。