强化学习在连续控制任务里有个老毛病:样本效率低得让人抓狂。尤其是那些高维观测输入(比如图像、多模态传感器读数)的场景,模型动辄要几百万步交互才能学到像样的策略,训练成本高到很多团队根本玩不起。我最早接触随机潜在变量模型这条路线,就是因为在一个机械臂抓取项目里被样本效率卡了脖子——环境交互一次要几秒钟,一天跑下来也就攒个几万条数据,用常规的无模型算法根本喂不饱网络。后来把目光转向“在潜空间里做强化学习”这个思路,才算是找到了突破口。Stochastic Latent Actor-Critic(简称 SLAC)就是这条路线里一个相当有代表性的工作,它把随机潜在变量模型和 actor-critic 框架揉在一起,专门解决高维观测下样本效率低的问题。这篇博文我会从设计思路、核心原理、实操复现、踩坑排查几个维度把它拆开讲透,适合有一定强化学习基础、想在自己的项目里落地潜空间策略学习的同学参考,也适合刚入门想搞清楚“为什么要在潜空间做 RL”这个问题的朋友。
1. 为什么要在潜空间里做强化学习
1.1 高维观测带来的样本效率困境
先说清楚问题背景。在标准的无模型强化学习设定里,智能体直接从原始观测(比如一张 84x84 的图片,展开就是七千多维)映射到动作。这个映射关系由神经网络来拟合,听起来很直接,但实际训练时问题一大堆。最典型的就是维度灾难:观测维度越高,策略网络和价值网络需要拟合的函数就越复杂,需要的样本量呈指数级上升。你在 Atari 上跑 DQN 能work,是因为那些任务的状态空间相对规整、奖励信号密集;一旦换到真实机器人控制或者复杂物理仿真,观测维度上去了、奖励又稀疏,直接端到端学习基本就是灾难。
另一个隐性问题是观测里混杂了大量与决策无关的信息。一张机械臂视角的图像里,背景纹理、光照变化、相机噪声占了绝大部分像素,真正决定动作的只有机械臂末端和目标的相对位置那一小块区域。端到端网络被迫把这些无关信息也编码进去,等于让网络在噪声里找信号,样本效率自然上不去。我实测过一个对比:同样的任务,用原始图像输入和用人工提取的低维状态输入,达到相同性能所需的交互步数差了将近一个数量级。这就是为什么大家要想办法先把观测“压缩”成紧凑的表示,再在这个表示上做决策。
潜空间方法的核心动机就在这儿:用一个学习到的低维潜在状态来概括原始观测,把决策问题从高维观测空间搬到低维潜空间。这样一来,策略网络面对的输入维度可能从几千维降到几十维,函数拟合难度大幅下降,样本效率自然提升。而且潜空间表示是通过无监督或自监督的方式从数据里学出来的,不需要人工设计特征,泛化性也更好。
1.2 随机潜在变量模型相比确定性编码的优势
把观测压缩成潜表示,最朴素的做法是用一个确定性编码器(比如普通的自编码器),把观测映射成一个固定向量。但 SLAC 选择的是随机潜在变量模型,也就是潜状态本身是一个概率分布而不是一个点。这个选择不是拍脑袋的,背后有明确的理由。
确定性编码有个致命弱点:它无法表达观测的不确定性。真实环境里,传感器有噪声、部分观测会遮挡、同一时刻的观测可能对应多种合理的潜在状态。确定性编码器会强行把这些不确定性压成一个点估计,导致学到的表示对噪声敏感、对未见过的观测泛化差。更麻烦的是,在部分可观测的设定下(POMDP),当前观测不足以确定真实状态,必须结合历史信息推断,这时候一个点估计根本没法表达“我对当前状态有多确定”。
随机潜在变量模型把潜状态建模成一个分布 $q(z_t \mid \text{history})$,用采样的方式得到具体的潜状态。这样做的好处是:模型能显式地表达不确定性,采样过程相当于一种正则化,让策略对潜状态的扰动更鲁棒;同时,随机潜变量配合序列模型(比如 RNN)可以自然地处理部分可观测性,把历史观测的信息累积到潜状态的分布里。SLAC 正是利用了这一点,用一个序列潜变量模型来同时完成表示学习和状态推断。
提示:随机潜变量模型不是万能药。如果你的任务是完全可观测的、观测维度本来就不高,那上随机潜变量模型属于杀鸡用牛刀,反而增加训练难度。它真正发挥价值的地方是高维观测加部分可观测的组合场景。
1.3 SLAC 的整体定位与它解决的问题
把 SLAC 放到整个方法谱系里看,它属于基于模型的表示学习 + 无模型策略优化的混合路线。注意这里有个容易混淆的点:SLAC 虽然用了潜变量模型,但它并不是严格意义上的“基于模型的强化学习”(MBRL),因为它不直接用学到的模型去做规划或者生成虚拟轨迹来训练策略。它的做法是:先用潜变量模型学一个好的表示,然后在这个表示空间里跑标准的 actor-critic 算法。所以它继承了无模型方法的稳定性和通用性,又拿到了表示学习带来的样本效率红利。
具体来说,SLAC 要解决三个层面的问题。第一是表示层面:如何从高维观测序列里学到一个紧凑、信息充分、能表达不确定性的潜状态。第二是推断层面:在部分可观测条件下,如何根据历史观测在线推断当前潜状态的后验分布。第三是控制层面:如何在潜空间里高效地做策略评估和策略改进,同时保证训练稳定。这三个层面环环相扣,任何一个环节出问题都会拖垮整体性能,后面的章节我会逐一拆解。
2. 核心架构拆解:潜变量模型与 actor-critic 如何咬合
2.1 序列潜变量模型的结构设计
SLAC 的潜变量模型是一个序列化的随机模型,核心结构可以理解成三层:编码器、转移模型、解码器。编码器负责把当前观测 $o_t$ 编码成潜变量的近似后验参数;转移模型描述潜状态随时间的演化 $p(z_t \mid z_{t-1}, a_{t-1})$;解码器负责从潜状态重建观测 $p(o_t \mid z_t)$。这三者构成一个变分自编码器(VAE)的序列版本,训练目标是最大化观测的对数似然下界(ELBO)。
这里的关键设计在于潜状态的时序依赖。普通的 VAE 对每一帧独立编码,丢掉了时间信息;SLAC 的潜状态 $z_t$ 同时依赖当前观测和历史潜状态,通过一个循环结构(实践中常用 GRU 或 LSTM 来承载历史信息)把过去的信息压缩进隐状态。这样即使当前观测有遮挡或者噪声,模型也能借助历史推断出合理的潜状态。我在复现时对比过带时序和不带时序两个版本,在部分可观测的抓取任务上,带时序的版本收敛速度快了大约 40%,最终成功率也高出十几个百分点,差距非常明显。
从实现角度看,编码器输出的不是潜状态本身,而是高斯分布的均值和对数方差,然后通过重参数化技巧采样得到 $z_t$。这个重参数化是让梯度能穿过采样操作回传的关键,具体做法是 $z = \mu + \sigma \odot \epsilon$,其中 $\epsilon$ 从标准正态分布采样。这个技巧看似简单,但它是整个模型能端到端训练的基础,少了它梯度就断了。
2.2 潜空间中的 actor-critic 设计
表示学好了,接下来是在潜空间里做控制。SLAC 的 actor 和 critic 都以潜状态 $z_t$ 为输入,而不是原始观测。Actor 网络输出动作分布的参数(连续动作通常是高斯分布的均值和方差),critic 网络输出状态价值或动作价值。这里有个设计细节值得说:critic 的输入是潜状态加动作,而不是像某些方法那样直接用观测。这样做的好处是输入维度低、函数简单,但也带来一个隐患——如果潜状态丢失了决策相关的信息,critic 的估计就会有偏。所以潜变量模型的质量直接决定了控制性能的上限,这也是为什么 SLAC 在表示学习上下了很大功夫。
另一个关键点是潜空间的奖励信号处理。奖励是从环境里拿到的原始标量,它不依赖潜状态,所以可以直接用在潜空间的时序差分学习里。SLAC 用的是类似 SAC(Soft Actor-Critic)的最大熵框架,在 TD 目标里加入了熵项,鼓励策略保持一定的随机性。这个选择对训练稳定性帮助很大,我在早期版本里试过不带熵项的普通 actor-critic,策略很快就塌缩到确定性动作,探索能力丧失,在稀疏奖励任务上直接卡死。
2.3 两个模块的联合训练与梯度流动
SLAC 最精妙也最容易出问题的地方,是潜变量模型和 actor-critic 的联合训练方式。理论上两者可以分开训:先训好 VAE,冻结编码器,再训策略。但这样有个问题——VAE 的表示是为重建观测优化的,不一定对控制任务友好。SLAC 采用的是联合优化,让策略学习的梯度能够回传到编码器,促使表示朝着“有利于决策”的方向调整。
具体实现上,actor 的损失函数里包含了对潜状态的依赖,梯度会经过采样操作回传到编码器参数。这里重参数化技巧再次发挥作用,保证了梯度路径的畅通。不过联合训练也带来了稳定性挑战:表示学习和策略学习的目标不一致,训练早期表示还很粗糙,策略梯度噪声大,容易把编码器带偏。SLAC 的应对策略是给不同损失项配不同的权重,并且用目标网络(target network)来稳定 critic 的更新。这些超参数的调节我在第 4 章会详细讲,这里先记住一个原则:表示学习的损失权重要足够大,保证潜空间不会在策略优化的过程中退化。
| 模块 | 输入 | 输出 | 训练目标 | 关键技巧 |
|---|---|---|---|---|
| 编码器 | 观测 $o_t$ + 历史隐状态 | 潜变量后验参数 | ELBO 重建项 + KL 项 | 重参数化采样 |
| 转移模型 | $z_{t-1}$ + $a_{t-1}$ | $z_t$ 的先验分布 | KL 散度对齐 | 时序循环结构 |
| 解码器 | $z_t$ | 重建观测 | 重建误差 | 与编码器共享部分结构 |
| Actor | $z_t$ | 动作分布参数 | 最大化 Q 值 + 熵 | 潜空间输入 |
| Critic | $z_t$ + $a_t$ | Q 值 | TD 误差 | 双 Q 网络 + 目标网络 |
3. 从零复现 SLAC 的关键步骤
3.1 环境与依赖准备
复现 SLAC 之前,先把环境搭好。我用的技术栈是 Python 3.8 以上、PyTorch 1.10 以上,仿真环境选的是 MuJoCo 系列的连续控制任务(比如 HalfCheetah、Walker2d 这类),因为 SLAC 原文就是在这些任务上验证的。如果你手头没有 MuJoCo 授权,也可以用 PyBullet 的对应任务替代,接口基本兼容,只是物理参数略有差异,不影响方法本身的验证。
依赖清单大致如下:torch、numpy、gym、mujoco-py(或pybullet)、tensorboard用于训练监控。我建议额外装一个tqdm来看训练进度,长任务没有进度条会很难受。环境配置这块有个坑要提醒:MuJoCo 的版本和 gym 的版本耦合比较紧,装之前先确认版本兼容性,不然会卡在 import 阶段。我踩过一次,gym 0.21 配 mujoco-py 2.1 死活跑不起来,换成 gym 0.18 加 mujoco-py 2.0 才通。
pip install torch numpy gym tqdm tensorboard pip install mujoco-py==2.0.2.13装完之后跑一个简单的环境交互测试,确认env.reset()和env.step()能正常返回,观测维度和动作维度符合预期。这一步别省,很多后续的维度不匹配错误都是环境没配好导致的。
3.2 潜变量模型的实现要点
潜变量模型是整个复现的核心,我把它拆成编码器、转移模型、解码器三块分别实现。编码器用一个 MLP 加 GRU 的组合:MLP 先把观测压成一个特征向量,GRU 维护历史隐状态,最后输出潜变量的均值和对数方差。这里有个维度设计的经验:潜变量维度不要设太大,原文在 MuJoCo 任务上用的是 8 到 16 维,我实测 16 维在大部分任务上够用,设到 32 维以上反而容易过拟合、训练变慢。
转移模型是一个以 $z_{t-1}$ 和 $a_{t-1}$ 为输入的小 MLP,输出下一时刻潜变量的先验分布参数。解码器则是从 $z_t$ 重建观测的 MLP,输出维度等于观测维度。训练损失由三部分组成:重建误差(高斯观测用 MSE,二值观测用交叉熵)、KL 散度(后验和先验之间)、以及一个可选的时序一致性项。KL 项的权重需要仔细调,太小了潜空间不规范,太大了重建质量下降,我一般从 1.0 开始试,根据重建损失的量级微调。
class LatentModel(nn.Module): def __init__(self, obs_dim, action_dim, latent_dim=16, hidden_dim=256): super().__init__() self.encoder = nn.Sequential( nn.Linear(obs_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU() ) self.rnn = nn.GRUCell(hidden_dim, hidden_dim) self.fc_mu = nn.Linear(hidden_dim, latent_dim) self.fc_logvar = nn.Linear(hidden_dim, latent_dim) self.transition = nn.Sequential( nn.Linear(latent_dim + action_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, latent_dim * 2) ) self.decoder = nn.Sequential( nn.Linear(latent_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, obs_dim) )上面这段是骨架代码,实际实现时要注意 GRU 的隐状态要在序列维度上正确传递,别把 batch 维和时间维搞混了。我第一版就是这里写错了,导致时序信息完全没利用上,训练出来的模型和单帧 VAE 没区别,白跑了两天。
3.3 actor-critic 网络的搭建与超参数选择
Actor 和 critic 都是简单的 MLP,输入是潜状态(critic 额外拼接动作)。Actor 输出高斯动作分布的均值和标准差,标准差要经过一个 softplus 或者 exp 变换保证为正,并且做上下界裁剪防止数值爆炸。Critic 用双 Q 网络结构,取两个 Q 值的最小值作为目标,这是 SAC 系列的标准做法,能有效缓解 Q 值高估问题。
超参数方面,我整理了一份实测比较稳的配置:学习率 actor 和 critic 用 3e-4,潜变量模型用 1e-3(表示学习通常可以快一点);batch size 用 256;目标网络软更新系数 tau 取 0.005;熵温度系数用自动调节(SAC 的自适应 alpha),初始值 0.1。经验回放池大小设 1e6,但实际在样本效率敏感的任务上,1e5 到 2e5 就够启动训练了。这些数值不是金科玉律,不同任务需要微调,但作为起点能省不少试错时间。
| 超参数 | 推荐值 | 调节范围 | 说明 |
|---|---|---|---|
| actor/critic 学习率 | 3e-4 | 1e-4 ~ 1e-3 | 太大不稳定,太小收敛慢 |
| 潜变量模型学习率 | 1e-3 | 5e-4 ~ 3e-3 | 表示学习可略快 |
| batch size | 256 | 128 ~ 512 | 影响梯度噪声 |
| 软更新系数 tau | 0.005 | 0.001 ~ 0.01 | 控制目标网络更新速度 |
| 潜变量维度 | 16 | 8 ~ 32 | 过高过拟合,过低欠拟合 |
| 回放池大小 | 1e6 | 1e5 ~ 1e6 | 视内存而定 |
3.4 训练循环的组织与关键细节
训练循环的组织方式直接影响复现成败。我的做法是每个环境步做一次模型更新,但潜变量模型的更新频率可以比 actor-critic 高一些,因为表示学习需要更多数据。具体来说,每收集一个 batch 的数据,潜变量模型更新两次,actor-critic 更新一次。这个比例是我试出来的,表示学习欠拟合时策略学不动,所以给它更多更新预算是合理的。
序列采样这块有个细节:从回放池里采样时,要采连续的序列片段而不是独立帧,否则 RNN 的历史信息就断了。我一般采长度为 8 到 16 的子序列,太短了时序信息不足,太长了显存吃不消。采样的时候要注意处理序列边界,episode 结束的地方要重置 RNN 隐状态,不然会把上一个 episode 的信息错误地串到下一个。
注意:训练初期不要急着看策略性能。潜变量模型需要几千步才能学出一个像样的表示,这之前 actor-critic 基本是在噪声上学习。我一般等潜变量模型的重建损失降到稳定区间后,才开始认真评估策略,否则容易被早期的糟糕曲线误导。
4. 实操中的典型问题与排查手册
4.1 潜空间坍塌:表示退化的识别与修复
潜空间坍塌是复现 SLAC 时最常见的坑,表现是潜变量模型的重建损失看着还行,但潜变量本身几乎不携带信息——不同观测编码出来的潜状态几乎一样,策略网络拿到的输入等于常数,自然学不出东西。这个问题在 KL 项权重过大时特别容易出现,因为模型为了最小化 KL 散度,干脆让后验等于先验,潜变量退化成纯噪声。
识别方法很简单:训练过程中定期统计潜变量在不同样本间的方差,如果方差趋近于零,基本就是坍塌了。修复手段有几个:降低 KL 项权重、用 KL 退火(训练初期 KL 权重设小,逐渐增大)、或者改用 free bits 技巧(给每个潜变量维度设一个最小的 KL 下界,低于这个下界就不惩罚)。我实测下来 KL 退火最稳,从 0.1 的权重开始,线性增加到 1.0,坍塌问题基本消失。
4.2 训练不稳定的三大诱因
除了坍塌,训练不稳定还有几个常见诱因。第一个是梯度爆炸,RNN 结构在序列较长时容易出现,解决办法是加梯度裁剪,阈值设 1.0 到 5.0 之间,我一般用 5.0。第二个是Q 值发散,critic 的估计值越来越大,最后变成 NaN,这通常是目标网络更新太快或者学习率太大导致的,把 tau 调小、学习率降一档通常能缓解。第三个是策略熵塌缩,策略过早变得确定,探索不足,这时候检查熵温度系数是不是太小,或者自动调节的 alpha 是不是卡在下界了。
| 问题现象 | 可能原因 | 排查方法 | 解决手段 |
|---|---|---|---|
| 潜变量方差趋零 | KL 权重过大 | 统计潜变量跨样本方差 | KL 退火 / free bits |
| 损失变 NaN | 梯度爆炸 | 监控梯度范数 | 梯度裁剪 |
| Q 值持续增大 | 目标网络更新过快 | 打印 Q 值均值 | 调小 tau / 降学习率 |
| 策略过早确定 | 熵系数过小 | 监控策略熵 | 调大 alpha 初值 |
| 重建模糊 | 解码器容量不足 | 看重建可视化 | 加宽解码器 / 降 KL 权重 |
4.3 样本效率不达预期的诊断路径
有时候训练能跑通、也不崩,但样本效率就是不如预期,跑了几十万步性能还上不去。这时候要按顺序排查几个环节。先看潜变量模型的重建质量,如果重建出来的观测和原始观测差太多,说明表示本身就没学好,后面全是白搭。再看潜状态和真实状态的对应关系,可以在简单任务上做个可视化,看看潜空间是否保留了决策相关的维度。最后看 actor-critic 的更新是否正常,Q 值有没有在合理范围内波动、策略熵有没有维持在健康水平。
我遇到过一次样本效率异常低的情况,排查了半天发现是回放池采样时序列片段的重叠处理有问题,导致同一段数据被反复采样,有效样本多样性不足。改成按 episode 为单位采样、保证片段不重叠之后,样本效率立刻恢复正常。这种问题不会有任何报错,只能靠对训练曲线的敏感度去发现,所以训练监控一定要做细。
4.4 部分可观测场景下的额外注意事项
如果你的任务本身是部分可观测的(比如观测有遮挡、只有局部视野),SLAC 的时序结构会发挥更大作用,但也有额外要注意的地方。RNN 隐状态的初始化要合理,episode 开始时重置为全零;序列长度要足够覆盖任务的关键历史窗口,太短了推断不出真实状态。另外,部分可观测场景下潜变量的不确定性会更大,KL 项的调节要更保守,否则模型会倾向于忽略观测、只靠先验瞎猜。我在一个带遮挡的导航任务上试过,把序列长度从 8 加到 16,成功率提升了将近 20%,这个投入产出比很划算。
5. 我踩过的坑与几条实操心得
5.1 关于表示学习和策略学习的平衡
联合训练最大的难点是平衡。我早期犯过一个错误:为了让策略尽快出效果,把 actor-critic 的学习率调得很高,结果策略梯度把编码器带偏了,潜空间朝着“有利于当前策略”而不是“有利于表示”的方向退化,最后策略和表示一起崩。后来我调整策略,让表示学习先行——训练前一万步只更新潜变量模型,等表示稳定了再引入策略梯度,整体稳定性好了很多。这个“预热”阶段看起来浪费了样本,实际上省下了后面反复调参的时间。
5.2 监控指标比损失曲线更重要
光看总损失是不够的,SLAC 的训练过程里有很多隐性问题不会直接反映在总损失上。我建议至少监控这几个量:潜变量的跨样本方差(防坍塌)、重建误差的绝对值(防表示退化)、Q 值的均值和方差(防发散)、策略熵(防探索不足)、以及梯度范数(防爆炸)。这些指标用 tensorboard 画出来,训练异常时一眼就能定位到是哪个环节出了问题。我现在的习惯是训练脚本里把这些监控全部埋好,跑起来之后先看半小时曲线再决定要不要继续。
5.3 小规模验证再上大任务
SLAC 的超参数对任务比较敏感,直接上复杂任务调参成本太高。我的做法是先在简化版任务上把流程跑通、把超参数调到一个大致合理的区间,再迁移到目标任务上微调。简化版任务可以是降维的观测、缩短的 episode、简化的奖励,只要能验证表示学习和策略学习的基本流程就行。这个习惯帮我省下了大量算力,也避免了在复杂任务上被各种耦合问题搞晕。
最后分享一个我觉得挺有用的小技巧:潜变量模型训练好之后,可以单独拿出来做可视化分析,把潜状态用降维方法(比如 PCA 或 t-SNE)画出来,看看不同状态在潜空间里的分布。如果同类状态聚在一起、不同状态分得开,说明表示质量不错;如果混成一团,那策略再调也白搭。这个分析花不了多少时间,但对判断问题出在表示层还是控制层特别有帮助。