从 CartPole 到两足小人
这本书是从一个 QWOP 的视频开始的。这一章我们把那个小人真的造出来——在你的浏览器里,从随机抽搐训到走路,一百毫秒。然后给你一条自己动手的路线。
先训一个给你看
随机那版是抽搐。训练那版走了 9.47 米,500 帧里站住了 497 帧。
然后用下面那排按钮换奖励函数——换一个,它就变成另一种生物。
这台小人是怎么造的
物理
不是动画,是真的在积分:
- 躯干:一个有质量(8 kg)和转动惯量的刚体,受重力,有位置、速度、角度、角速度四个自由度。
- 两条腿:每条两节(大腿、小腿各 0.45 m),由髋和膝两个关节角驱动。腿是运动学驱动的(角度直接给定,不算腿的动力学),这是为了让参数维度控制在 13 维。
- 地面接触:脚踩进地面时,按弹簧-阻尼算法向力
N = k·penetration − c·v_y,切向按库仑摩擦(用 tanh 光滑化,避免数值抖动)。 - 积分:显式欧拉,dt = 0.02 秒,一个回合 500 步 = 10 秒。
摔倒判据:躯干低于 0.5 m,或者倾角超过约 57°。
控制器
不是神经网络,是一个周期步态发生器:
# 每个关节的角度都是一条正弦波 angle(t) = offset + amplitude * sin(2π·f·t + phase) # 参数:1 个频率 + 4 个关节 × 3 个参数(幅度、偏置、相位)= 13 维
为什么用这个而不是神经网络?因为 13 维的参数空间,CEM 在一百毫秒内就能搜完,可以在网页里当场跑。神经网络需要几十万步的环境交互,浏览器扛不住。
代价是它是开环的——它不看状态,只按时间播放一个固定的动作序列。所以它学出来的是「一段刚好能站住的舞蹈」,而不是真正的平衡控制。真被推一把它就完了。
训练:CEM
重复 30 代:
① 按当前的均值和方差,随机撒 60 组参数
② 每组都放进物理里跑 500 帧,用 reward 打一个分
③ 留下分最高的 12 组
④ 用这 12 组的均值和方差,替换掉当前的均值和方差
交叉熵方法(CEM)。不算梯度,纯粹靠「留下好的、扔掉差的」。
这本书不想含糊这一点。CEM 严格说属于黑箱优化 / 进化策略,不是这本书前十八章讲的那类强化学习。它没有价值函数,没有 TD 误差,不做信用分配——它只知道「这一整组参数最后拿了多少分」。
那为什么这里用它?两个理由,都很实在:
① 它能在浏览器里当场跑完。三十代 × 60 个个体 × 500 帧 = 九十万次物理步,一百毫秒。PPO 做不到这个。
② 这本书的主线在奖励那一层,而那一层两者完全一样。第 19、20 章那五种截然不同的结局,换成 PPO 训一遍,结论会一模一样——因为它们都在最大化同一个函数。
顺带一个有意思的事实:进化策略在参数少的时候,真的能打得过 PPO。OpenAI 2017 年那篇论文把 ES 拿去打 MuJoCo 和 Atari,性能相当,而且因为完全不需要反向传播,并行扩展性极好——上千台机器几乎线性加速。
所以第 1 章那个「进化算法 vs 强化学习」的分野,没有那么绝对。真正的分野是:一局产出一个数字,还是一局产出五百个数字。在参数少、模拟便宜的时候,前者的劣势没那么致命。
为什么随机撒参数不行
demo 里那个对照值得琢磨:随机撒 40 组参数,最好的一组得分 0.88;CEM 训三十代,得分 18.75。
差了二十倍。而两者用的是同一个物理、同一个奖励函数。
13 维空间里,「能走路」的那个区域小得可怜。纯随机撞上它的概率约等于 0。CEM 做的事是把搜索的均值一步步挪过去、把方差一步步收紧——每一代都在「好的那一撮」附近重新撒网。
而「哪一撮算好」,完全由那一行 reward 决定。这就是这本书的主线在这台机器上的具体形态。
换成真机:一条不浪费时间的路线
这是这一章更实用的一半。按顺序走,每一步都有明确的检查点。
第 0 步:装环境(五分钟)
pip install "gymnasium[box2d]" "stable-baselines3[extra]" tensorboard
box2d 那一项是 LunarLander 和 BipedalWalker 需要的物理引擎。如果装不上(Windows 上偶尔会),先跳过它,CartPole 不需要。
第 1 步:CartPole(十分钟)
import gymnasium as gym
from stable_baselines3 import PPO
env = gym.make("CartPole-v1")
model = PPO("MlpPolicy", env, verbose=1)
model.learn(total_timesteps=50_000)
# 看一眼它学成了什么 —— 这一步别省
env = gym.make("CartPole-v1", render_mode="human")
obs, _ = env.reset()
for _ in range(1000):
action, _ = model.predict(obs, deterministic=True)
obs, r, term, trunc, _ = env.step(action)
if term or trunc: obs, _ = env.reset()
检查点:平均回报能到 500(这个环境的上限)。几分钟的事。
然后做一件更有价值的事:照第 12 章那二十行,自己手写一个表格版 Q-learning,跑 FrozenLake,再拿第 7 章那个 value_iteration() 核对。这一步很多人跳过,但它是唯一一次你能确切知道「学对了没有」的机会。
第 2 步:LunarLander(半小时)
model = PPO("MlpPolicy", "LunarLander-v3", verbose=1)
model.learn(total_timesteps=500_000)
检查点:平均回报 > 200(官方认为「解决」的门槛)。
这是第一个「看起来像那么回事」的成果——你会看到它学会点火减速、调整姿态、稳稳落在两面旗子中间。
推荐在这一步做一个实验:把 gamma 从 0.99 改成 0.9,重训一遍。你会亲眼看到第 6 章那个「短视的 agent」——它会为了省燃料而摔得更快。
第 3 步:BipedalWalker(几小时)
这就是本章那台小人的正规版本,也是 QWOP 的精神续作。
from stable_baselines3 import SAC
model = SAC("MlpPolicy", "BipedalWalker-v3", verbose=1)
model.learn(total_timesteps=1_000_000)
检查点:平均回报 > 300。笔记本 CPU 上大概几小时。
这里换成 SAC 而不是 PPO 是有理由的:连续控制上 SAC 的样本效率明显更高(第 13 章那张表)。可以两个都跑一遍对比,这个对比本身很有教育意义。
看完这本书,一个很自然的冲动是「我去把 QWOP 训出来」。不建议,理由很具体:
QWOP 是个网页游戏。要用 RL 训它,你得写一层环境包装:截屏当观测、模拟按键当动作、从画面 OCR 出距离当奖励。
问题在于这里面 90% 的工作量是浏览器自动化和图像处理,和强化学习本身没关系。而且更致命的是——截屏采样慢到训练根本跑不动。RL 动辄要几百万步交互,你的环境每秒只能跑三十步。
BipedalWalker-v3 就是为这个存在的:同样的双足控制问题,24 维观测(关节角度、速度、雷达测距),4 个连续力矩,pip install 就能用,每秒能跑上千步。
想要更狠的,MuJoCo 里有 Walker2d-v5 和 Humanoid-v5(Gymnasium 1.0 之后 MuJoCo 环境统一升到了 v5)。Humanoid 是 17 个自由度,那是真正的硬骨头。
上面每一步我都写了「看一眼它学成了什么」,这不是凑字数。
第 20 章那个教训在这里是可操作的:每训完一版,花三十秒渲染出来看一眼。
你会发现很多曲线上看不出来的东西:
- LunarLander 有时候会学会悬停在空中不落地——因为「不摔」的奖励足够高,落地反而有风险
- BipedalWalker 常见的一个局部最优是单腿蹦——能走,但姿态很怪
- 训练崩了的 agent 会把所有动作顶到极限值(第 16 章那个征兆)
这三种情况在回报曲线上可能都是「在涨」。只有看录像能分辨。
这本书到这儿就把地基铺完了。往下有几条明确的路:
- Hugging Face Deep RL Course:免费、开源、每单元三四小时,第一课就让你训一个月球着陆器并把模型传到 Hub 上。对工程师最友好的下一站。
- Sutton & Barto《Reinforcement Learning: An Introduction》第二版:领域圣经,作者官网免费放 PDF。这本书里的悬崖行走、Baird 反例、TD vs MC,全都来自它。读完这本小书之后再去读它,会顺畅很多。
- CleanRL:每个算法一个单文件、无抽象层。PPO 三百行读完你就真懂了,比读 SB3 那种工程化的实现好懂得多。
- Berkeley CS285 / Stanford CS 224R:硬核研究生课,讲义和录像都公开。想往研究方向走就是它们。
还有一条被低估的路:回去把第 3 章那个多臂老虎机用在你自己的工作上。A/B 测试、推荐冷启动、超参搜索——那是这本书里最容易明天就落地的东西。
这本书从一个视频开始:一个小人抽搐、翻滚、脸着地,然后忽然站起来了。
现在你知道那一分钟里发生了什么:
- 那些抽搐是探索(第 4 章),不是「还没学会」
- 那几万次摔倒就是学习过程本身——价值函数里那道「这个倾角很危险」的断崖,是从它们里面长出来的(第 7 章)
- 「站起来」那一刻,是策略网络的标准差收缩到某个程度,动作终于变得连贯(第 17 章)
- 而它到底会长成什么样——走路、前扑、还是站着不动——取决于作者写的那一行 reward(第 19 章)
那句「AI 自己学会了走路」现在可以说得更准确一点:
AI 做的是搜索,人做的是打分。而那个小人最后长成什么样,答案早就写在那一行代码里了——只是写的人自己也不一定看得出来。
这一章的一句话
造一台会走路的小人,需要的不是复杂的算法,是一行写对了的 reward。别拿 QWOP 本体入门——BipedalWalker 就是为你准备的。
最后一章:一张表。当你自己的训练跑不出来的时候,按「先查哪一层」的顺序去查——而第一层永远是奖励层,因为只有它不会喊。