

强化学习算法程序实践(4):连续控制(DDPG / TD3 / SAC)
Actor/Critic 输入输出、Replay Buffer、探索噪声、以及各自 update 的关键差异
前言#
到了连续控制(continuous action space),你会发现 DQN 这套“输出离散动作 Q 值”的方式没那么顺手了。
我第一次做连续控制任务时最大的感受是:动作不再是“选 A 还是选 B”,而是“输出一段连续向量”。这时再用 DQN 去离散化动作,往往既不优雅也不高效。更顺的路线是 Actor-Critic:Actor 直接产出连续动作,Critic 负责评价这个动作在当前状态下到底值不值。
我自己当时的“翻车现场”也很典型:把动作离散成 11 档、21 档,看起来很细了,但策略依然像在抖腿——要么动作不够细导致控制很粗糙,要么离散太细导致动作维度膨胀,学习变得又慢又不稳定。更要命的是,reward 曲线的抖动和环境随机性混在一起,你根本不知道是探索问题、还是值估计的问题。于是我就彻底转到 Actor-Critic:动作直接从网络里出来,探索要么靠噪声(DDPG/TD3),要么靠熵正则(SAC),思路一下子清爽很多。
这一篇我就沿着我自己的实践顺序,把三套最常用的 off-policy 连续控制算法串起来:DDPG 是最基础的 deterministic Actor-Critic;TD3 主要是修 DDPG 容易高估、容易抖的问题;SAC 则在目标函数里把“熵”引入进来,让探索更自然、训练更稳。
这篇我按“你写代码时要实现哪些模块”来整理。
DDPG:deterministic Actor-Critic 的最小工程结构#
DDPG 的结构非常“工科”:Actor 输出动作 ,Critic 评估 的 Q 值。这里有一个我认为必须牢牢记住的关键点:Critic 的输入是 state + action 的拼接,不是只喂 state。
网络结构#
- Actor(策略网络):输入 state,输出连续动作;输出层常用
tanh把动作限制到 (再映射到环境动作范围) - Critic(价值网络):输入
[state, action]拼接向量,输出
实现时我会额外注意一个特别容易被忽略的小细节:Actor/Critic 的输出层初始化不要太“放飞”。如果初始动作幅度很大,或者 Critic 初始 Q 值异常夸张,训练经常会在前几千步就开始发散。很多实现会对最后一层的权重做更小范围的初始化,就是为了解决这个。
Replay Buffer#
和 DQN 类似(push+sample),但 transition 里的 action 是连续向量。
探索噪声#
DDPG 是确定性策略(deterministic policy),所以探索通常靠给动作加噪声:
action = actor(state) + noise
我的习惯是:训练阶段加噪声促进探索;测试阶段完全关掉噪声,只看学到的确定性策略到底能拿多少回报。否则你会出现一个很“玄学”的现象:测试时看起来也很随机,根本不知道模型到底学没学会。
update:先 critic 再 actor,再软更新 target#
DDPG 的 update() 我通常按固定顺序写死:
- 从 replay 采样一批 transition
- 用贝尔曼方程算 critic target
- 更新 critic(最小化 TD error)
- 更新 actor(最大化 critic 对当前 actor 动作的 Q 值)
- 更新各自目标网络参数(soft update)
TD3:Twin Delayed DDPG(解决高估与不稳定)#
当我用 DDPG 在更复杂的连续控制任务上跑起来以后,很常见的痛点是:回报曲线一会儿很好看,一会儿又突然崩掉;或者 Critic 的 Q 值虚高,导致 Actor 被带偏。TD3 的出现基本就是为了解决这些问题,它把 Double Q-learning 的“取较小估计更保守”的思想带进了 DDPG。
工程上 TD3 的“显眼差别”主要体现在三件事:
- 双 Critic:两个 ,算 target 时取较小者缓解过估计
- 延迟更新 Actor:critic 更新更频繁,actor 慢一点(delayed policy update) 3.(常见实现里还有 target policy smoothing:对 target 动作加一点小噪声)
实际写代码时,你只要把“双 critic”写对、把“actor 延迟更新”写对,就能看到稳定性明显提升;其它技巧(比如 target policy smoothing)可以作为锦上添花后面再加。
SAC:Soft Actor-Critic(熵正则化让探索更“软”)#
SAC 是我在连续控制里最喜欢的一套,因为它对“探索”这件事的处理更自然:不是靠外部噪声硬塞随机性,而是在优化目标里直接鼓励策略保持一定随机性(最大化熵)。很多时候这会让训练更稳,也更不容易陷入坏的局部最优。
从工程上看,SAC 依然离不开三块:网络、replay、update。常见的网络拆分是 ValueNet / QNet / PolicyNet(也有实现会省略 ValueNet,用 twin Q + target V 替代,但主旨不变)。
你选哪种网络拆法都可以,关键是 update 的目标里要正确引入熵正则项,以及 Policy 更新时要能拿到 action 的 log_prob。
SAC 的直觉#
如果你把 PPO 理解成“动作概率分布 + on-policy 约束”,那 SAC 更像:
在 off-policy 的 Q 学习里,鼓励策略保持一定随机性(最大化熵),让探索更稳定。
工程差异点:PolicyNet 的动作采样#
我在实现 SAC 时,会把策略网络的动作相关函数明确拆成两种:
evaluate() 用于训练:返回采样动作以及它的 log_prob(因为熵项要用到它);get_action() 用于交互/测试:给定 state 输出动作(测试时通常用更确定的方式,比如取均值,或者关掉随机性)。
你可以把它们理解为我们前面文章中的 sample/predict 的连续动作版本:训练期需要概率信息,测试期只需要行为本身。
小结#
把连续控制这三套算法压缩成一句工程结论:
DDPG/TD3/SAC 都是“Actor 产动作、Critic 评估 、Replay 做 off-policy”,差异主要体现在 update() 的 target 计算方式、是否用双 critic、是否延迟更新 actor、以及是否引入熵正则。
如果你后面要把这些算法写成统一框架,我建议保留同样的接口:
sample(state):训练交互(DDPG/TD3 加噪声,SAC 采样分布)predict(state):测试(不加噪声/取均值动作)update():从 replay 采样并更新网络
后续我会看情况把这个系列继续补成“通用工程模板仓库”:统一日志、统一评估、统一保存/加载与配置管理。
连续控制里我最常踩的坑 & 排障顺序#
连续控制的训练“崩”起来比离散动作更快,尤其是 DDPG/TD3 这种 deterministic 方法,稍微一个尺度不对就会直接发散。我一般按下面顺序排查:
- 动作缩放是否正确:Actor 输出常是
tanh的 ,环境动作空间可能是别的范围。这个映射一旦错了,训练会表现得非常诡异(像是永远学不会某个动作方向)。 - 噪声别太大/别太小:噪声太大动作乱飞,buffer 里全是坏数据;噪声太小又不探索。建议先把噪声单独打印出来,看它的量级相对动作范围是否合理。
- 先盯 Q 值是否爆炸:每隔一段时间打印
Q1/Q2的均值、最大值;如果 Q 值飙升,优先怀疑学习率、reward 尺度、done 处理、target 计算。 - target 的软更新系数(tau)要保守:tau 太大,target 跟着在线网络跑,稳定性下降;tau 太小,又会学得很慢。建议先用保守设置跑通,再微调。
- TD3 的双 critic/取 min 要写对:这是它解决过估计的核心。如果你写成了 max,或者 target 用错了 critic,会直接退化/发散。
- SAC 重点看 entropy / alpha:熵项权重不合适,会出现“动作太随机”或“过早确定”。如果实现支持自动调 alpha,一般会更省心。
- 先用短 horizon 验证闭环:把 episode length 缩短、reward 简化、甚至先关掉部分随机性,确认 update 不会爆,再逐步加回真实设置。