基于模型的强化学习,很多项目做到最后会发现,真正卡住的地方不是算法论文里那些花哨的收敛证明,而是连续动作控制里一个特别现实的问题:模型预测了一堆动作,到底该信哪一步。这个系列到第四篇,正好可以把这条链路彻底打通——从一段原始的状态动作数据出发,训练一个能推演连续状态变化的动力学模型,再在模型空间里规划出连续动作序列,最后经由模型预测控制回环送回真实环境。简单说,前三篇解决的是“能不能学”,这篇解决的是“学了之后怎么用,以及怎么让用的过程从玄学变成工程”。
整篇文章围绕一套 Python 工具箱展开,面向已经被无模型强化学习在连续动作任务里折磨过的人——比如你调过 PPO 或者 SAC,但真实系统上样本成本太高;又或者你已经跑过一些基于模型强化的实验,但每一步都靠手工调参,模型、数据和规划器之间没有统一接口。我会把从数据收集、归一化、动力学模型训练、集成预测、模型预测控制到最终问题排查的经验全部摊开讲,里面每一个坑都是我实际踩过的。
1. 为什么连续动作场景必须用“模型 + 规划”工具箱
1.1 连续动作让无模型方法开始“烧钱”
大量真实控制任务的输出不是一组离散选项,而是一组连续实数:机械臂每个关节的力矩、小车油门开度、无人机旋翼转速、温控阀门的开度。想用 DQN 处理这些东西,常规做法是把动作空间离散化,但离散化带来的爆炸式增长立刻让训练变味。假设一个六轴机械臂,每个关节给 10 个离散档位,动作空间的规模是 10 的 6 次方。就算并行开几十个环境,经验回放里也塞满了大量低效样本。
我在实际项目里最常看到的场景是:团队想快速验证一个连续控制想法,直接拿 PPO 硬跑。PPO 在连续动作空间确实能工作,可它的样本效率摆在那——每次策略更新都要收集一大轮 rollout,每个 rollout 又是几十步对真实系统或高精度仿真器的交互。更麻烦的是,动作维度一旦增加,想保持同样的策略精度,训练轮数几乎是线性到指数地往上涨。这时候性能瓶颈已经不在算法上,而在你愿意投入的显卡时间和设备磨损。
无模型方法还有一层隐蔽的浪费:连续动作空间里大量动作是近似的、无效的,算法必须一个个试错才能把它们排除掉。而基于模型的方法可以先把明显无望的动作在模型里过滤掉,把真实环境的交互集中在有前景的区域。这个结构性差异,就是连续动作任务里基于模型方法最核心的立身之本。
1.2 基于模型方法在连续控制里的真实优势
我不打算把基于模型的方法吹成全场景最优,但它在“样本预算有限、动作连续、需要快速迭代”这三个条件下确实表现非常出色。真实环境只负责两件事:初始阶段收集一批覆盖动作空间的轨迹,以及规划阶段对每一小段最优动作做一次真实校验。中间那些动辄几十上百步的轨迹推演、候选动作打分、动作序列优化,全在模型内部完成。相当于你给控制任务配了一台虚拟试验机,想怎么跑就怎么跑,成本几乎为零。
连续动作空间对模型类方法还有一个额外好处:模型输出是实数向量,天然携带梯度信息,规划器可以用随机优化甚至梯度上升直接在连续空间里找更好动作,而不是靠离散枚举。这个好处让规划搜索的平滑性和稳定性都明显好于离散动作情况,特别是任务奖励函数本身比较平滑的时候,基于模型的规划器用很少的迭代就能找到高质量动作序列。
但坦白讲,动力学模型在高维连续状态下非常容易漂移。一步预测误差看着是 0.01,滚到 20 步之后可能偏出一个身位。这就是工具箱存在的意义——你需要的不是某次实验里精妙的调参,而是一套能在误差不断累积时还能稳定兜底的工程范式。算法解决“有没有可能”,工程解决“能不能稳定复现”。
1.3 工具箱的整体架构:数据、模型、规划三件套
这套 Python 工具箱在结构上被我拆成三块,彼此通过稳定接口而非共享内部变量通信。
- 数据流水线:负责从环境采样原始轨迹,清洗异常帧,归一化,写入轨迹缓冲区,并按轨迹维度切分训练集和验证集。
- 模型流水线:维护一组动力学预测器,支持单步预测或粒子预测,能返回多步回滚的均值与方差,同时负责训练、早停和自检。
- 规划流水线:在给定当前状态的前提下,调用模型做轨迹推演,使用 CEM 等随机优化方法在连续动作空间搜索动作序列,并输出将要实际执行的第一个动作。
每条流水线都用一个 Python dataclass 定义输入输出,比如TrajectoryData、ModelOutput、PlanOutput。三条线之间不互相导入对方的具体实现,只靠公共字段传递。所以换环境、换模型结构、换优化器都像换模块,不用重新接线。我从一开始就坚持这个设计,后来它在多个项目里反复救了我——做工程最难的不是某个算法,而是你改了一处,却不知道它会牵连哪些看不见的地方。
2. 数据管道的处理:连续控制的地基
2.1 先统一数据形态,再谈建模
不管数据来自 Gym、自定义模拟器还是真实设备,工具箱里一律先把原始日志转成统一的五元组:(state, action, next_state, reward, done)。这套格式看着简单,但在连续动作任务里藏着三个高频坑。
第一个坑是时滞。真实系统的传感器信号通常要经过滤波、缓存,最后进入你程序的状态往往比当前时刻的真实状态落后几十毫秒,控制指令又因为总线调度存在延迟。如果数据管道不记录时间戳,动力学模型学到的就是一个错位的因果关系:当前状态是旧的,动作是新的,两者拼在一起根本不能代表真实物理过程的一步转移。工具箱在数据记录里强制带上 timestamp 字段,哪怕纯仿真环境我也会保留,成本极低,收益却极大。
第二个坑是量纲混乱。角度、角速度、力、电流可能出现在同一个状态向量里,取值范围从 0.01 到上千。神经网络对输入特征尺度非常敏感,不归一化的话,小数值维度几乎直接失明。第三个坑是动作边界。每个动作维度的合法范围不一定相同,比如油门 0~1,方向盘 -1~1。如果只做归一化但忘记恢复缩放关系,模型在规划阶段就会频繁产生越界动作,环境强 clip 之后,模型看到的状态分布和训练时完全不同,整个闭环直接失效。
2.2 归一化:被很多人跳过的要命细节
连续动力学模型的输入通常是[state, action]的拼接向量。比如一个四足机器人状态 28 维,动作 12 维,拼起来就是 40 维。这 40 个维度里既有姿态角,又有角速度、接触力等量级悬殊的变量。如果直接塞给网络,ReLU 这类激活函数很容易被大尺度维度主导,模型对小幅度的反馈分量几乎“听不见”。
工具箱里的做法是先算统计量、再持久化。启动时从缓冲区抽取一批样本,计算每维的均值和标准差,存成 JSON 文件。训练和推理两侧都加载同一个 JSON,确保模型输入始终处于同一个坐标空间。最坑的错误就是训练时做了归一化,推理时由于配置加载不全而用了原始状态,模型拿到分布完全不同的输入,预测结果自然全面崩盘。把归一化参数固化保存以后,这类低级错误基本就能从代码层面杜绝。
还要提一个细节:如果某一维的标准差恰好是零,说明它在整个数据集里是常数。这种特征如果不是刻意构造的 mask,建议直接删掉,否则模型要白白分出一部分容量去拟合一个没有信息量的维度。
2.3 数据编排:按轨迹切,别按样本切
动力学模型训练里最隐蔽的过拟合,来自数据划分方式。普通机器学习习惯按样本随机切分,但轨迹数据里,第 t 帧和第 t+1 帧之间往往只差一个动作,状态极其接近。如果你把第 t 帧放进训练集、第 t+1 帧放进验证集,模型几乎等于看过验证样本的“邻居”,验证 loss 被严重低估。等真正拿模型去做多步推演,误差经过累计,立刻原形毕露。
所以工具箱里的数据切分是轨迹级别的:一条完整轨迹要么整体进训练集,要么整体进验证集,绝不跨切。如果任务存在多个工况,比如仿真里采了“空载运行”和“满载运行”两组轨迹,还要按工况分组切分,否则验证集里可能混入大量同工况样本,泛化性测试形同虚设。按这种切分方式做出来的回滚评估曲线,才真正能反映模型在陌生轨迹上的表现。
2.4 增量数据与回放缓冲区设计
MBRL 的数据不是一次性收集完的,而是“初采-训练-规划-执行-回填”循环迭代出来的。工具箱里的缓冲区需要支持三个操作:追加轨迹、随机采样、按轨迹索引抽取。我用带最大长度的 deque 或环形数组实现,容量按经验设成能装下最近 5 万到 10 万步轨迹。
回放时要注意新旧数据失衡。如果最近一批数据反复主导训练,模型会对新分布过拟合,反而忘掉早期学到的通用动力学规律。我的做法是给新数据一个较小的采样权重,或者对旧轨迹做概率性过采样,具体比例看验证集回滚误差来调——如果最近轨迹带来的提升开始变小,就调高老数据权重,保证模型始终能看到一定比例的历史分布。
另外,缓冲区底层存原始未归一化数据,归一化参数单独在一个类里维护。这样想对比不同归一化策略时,不需要重新采集数据,只要换一个预处理模块就能跑通。
3. 动力学模型:几个能打的连续预测方案
3.1 单步 MLP:先把基准立住
最省事也最不容易出错的起始模型就是两层 MLP。输入[state, action],输出delta_state而不是next_state。原因之前提过:连续控制里相邻两个时刻的状态高度相似,直接预测next_state会让网络学会“复制输入”这种偷懒行为,而预测增量能强迫模型真正去理解这一动作让状态改变了多少。
我给一个参考结构:输入维度state_dim + action_dim,第一层 256 个神经元加 ReLU,第二层 256 个神经元加 ReLU,输出层state_dim,不加激活。损失函数用 Huber Loss 而不是 MSE,因为真实轨迹里难免有传感器毛刺和极端离群点,MSE 会把异常样本的梯度放大得非常厉害。再按状态维度做加权,比如角度误差权重 1.0,速度误差权重 0.3,让模型不要把小噪声的速度误差当成主要学习目标。
这个模型的验证标准不是一步误差,而是多步回滚误差。如果工具箱同时训练多个模型变体,我用回滚误差最小的那个作为基准模型。它通常在倒立摆级别的连续动作任务上表现不错,但一到更复杂的控制场景里就显得吃力。这正好引出概率集成的方法。
3.2 概率集成神经网络(PETS 思路)
处理连续动作和高维状态,我真正推荐的是 PETS 风格的集成模型。它不是单个网络,而是一组结构相同、内部随机的网络,每个网络输出高斯分布均值和方差。规划阶段,集成模型对同一段候选动作给出多条预测轨迹,工具箱把这些轨迹的均值和方差汇总后交给规划器决策。
为什么要集成而不是追求单模型单点预测的极致精度?因为规划器的搜索本质上是在模型误差最小的区域里找动作。单模型在自己不熟悉的区域里给出的均值可能是完全错误的,规划器却不自知,一头扎进去就产生了“幻觉轨迹”。集成模型能让意见分歧暴露出来——多个子网络对某个候选动作的预测结果差异很大,说明这块区域不确定性高,规划器要么避开,要么通过增大采样方差来兜底。这种把不确定性显式化的能力,在连续动作任务里价值极高。
集成的数量我实测 5 个是性价比拐点,再往上收益不大,每一次前向传播的成本却线性增加。粒子数在规划时可设 20 到 50。训练时必须让每个子网络用不同随机种子、不同 batch 顺序、甚至不同 dropout mask,否则子网络快速趋同,集成等于白做。
3.3 训练与验证:按轨迹拆,不按样本刷
模型训练阶段与数据处理阶段一样,必须按轨迹切分验证集。验证指标也不能只看一步误差,因为 MBRL 模型最终要支撑的是多步推演。工具箱里专门写了一个rollout_loss函数:从验证集某个时间点出发,输入真实初始状态和一段真实动作序列,让模型自回归生成未来 H 步状态序列,再与真实序列逐帧对比。只有这个指标在降,说明模型的推演能力真的在进步。
优化器我常用 Adam,初始学习率 1e-3。每训练 50 轮看一次验证集回滚误差,连续 10 轮不降就把学习率降到 3e-4。早停耐心值不用设太大,防止模型死命拟合训练集中小概率出现的动态模式。对集成模型来说,每个子网络单独记录最优权重,规划时也分别加载,而不是共享一份集成平均权重。
3.4 多步回滚误差控制的实操技巧
回滚误差是 MBRL 的核心敌人,我分享几个从工程上很实用的技巧。
第一,预测目标用delta_state而不是next_state,这是最基础也最有效的一招。
第二,训练时偶尔让模型“走两步”。常规训练是一步监督:输入真实状态和真实动作,输出预测增量。工具箱里我让部分 batch 用模型自己上一步的输出作为下一步输入,再去对齐真实轨迹,形成一种课程式训练。这样模型被迫在自己的预测误差上保持稳定,回滚能力会明显提升。但这个比例不能太高,我控制在 10% 到 20% 的 batch,否则模型容易在自产状态里跑偏。
第三,模型更新频率要控制。MBRL 数据流是增量的,如果每采到一点新数据就全量重训,早期噪声大的阶段会把模型反复折腾坏,后期反而学不好。我设置了触发条件:新数据量超过 5000 步,或者验证集回滚误差比当前模型提升超过 5%,才触发重训。实际跑下来,这个节奏对连续动作任务非常合适。
4. 从模型到连续动作:MPC 与随机优化
4.1 规划器接口设计:不要让规划器依赖模型细节
工具箱里规划器对外的接口收敛成一个函数:plan(current_state) -> continuous_action。内部可以用 CEM,也可以用随机 shooting,但外部只看得到这个统一入口。好处是主循环、模拟器甚至 ROS 节点都不需要了解规划器内部怎么搜动作,想换规划策略就改一行配置,完全不影响其他模块。
规划器内部逻辑其实很直白:
- 在当前状态下,初始化一批候选动作序列。
- 把每个候选序列喂给动力学模型,预测未来 H 步的状态轨迹。
- 根据成本函数为每个轨迹打分。
- 用得分去更新下一步候选动作的分布。
- 从最优候选序列中取出第一个动作返回给环境执行。
这就是标准 MPC。我选择 CEM 作为主优化器,因为它在连续动作空间里既简单又可靠,特别是成本函数非线性比较强的时候,纯采样方式比一阶梯度法更不容易陷入局部陷阱。
4.2 CEM 交叉熵方法:参数与计算量
CEM 的思路很朴素:假设动作序列里的每个元素服从高斯分布,迭代执行“从分布采样一批序列、用模型打分、选出 top-k、用这些序列重新估计均值和方差”,几轮之后,均值序列就指向一个不错的连续动作规划。
参数选择上,规划步数 H 是最关键的。H 太小,模型只看得到短期收益,动作偏短视;H 太大,模型回滚误差经过多步累积,规划的“完美轨迹”在真实环境里根本走不出来。我在状态维度 10 到 30 的任务里,通常从 H=12 开始调,遇到稀疏奖励任务再加到 20,但一定要同步加大集成粒子和 CEM 迭代次数来弥补误差。
候选样本数和迭代次数,我一般用num_samples=200, top_k=20, iters=3起步。效果不好时优先加候选样本数,因为这是直接扩大搜索范围;迭代次数不是越多越好,到后面 CEM 方差收缩,多迭代只是在一个局部区域反复确认。规划的计算开销主要花在模型前向上:200 条轨迹乘 H=15 步、再乘 5 个集成网络,大约 15000 次前向传播,GPU 上是毫秒级,CPU 上可能要到几百毫秒。实时控制要求高的话,每 2 到 3 个控制周期做一次重规划也够用。
4.3 连续动作空间的映射与限幅
规划器输出的动作一定不能超出环境边界。如果环境动作本身就在 [-1, 1],采样初始均值自然是 0,初始方差可选 0.3 至 0.5。如果某个动作维度只能工作在 [0, 1] 区间,就需要决定用 clip 还是用 sigmoid 映射。
我的经验是:能映射就不要直接裁剪。clip 会在边界处制造大量“撞墙”行为,规划器发现边界动作得分也不错,就容易把所有动作推到极限,真实执行时执行器一旦饱和,系统就失控。用 sigmoid 把高斯采样输出压到 (0,1) 后,边界附近虽然也会饱和,但梯度平滑,规划器不会那么执拗地停在边界上。如果实在只能 clip,那至少要在成本函数里对贴近边界的动作加惩罚项。
连续动作还有一个老问题就是平滑性。机械臂、无人机这类系统对动作跳变极其敏感,规划器相邻两拍动作值差异太大,真实执行时不是在控制它,是在虐待它。工具箱的做法是对候选动作序列做一次高斯滤波,或者在成本函数里加动作变化惩罚:lambda * sum((a_t - a_{t-1})^2)。lambda 从 0.01 开始调,效果立竿见影。
4.4 MPC 闭不了环等于白做
模型预测控制的价值在闭环,不在开环。工具箱里规划器每次只输出第一个动作,执行完之后环境返回真实状态,再以新状态为起点重新规划下一段轨迹。这个“执行一步-真实观测-重新规划”的循环,能不断把模型回滚误差从路径上拉回来。如果发出去的是完整轨迹、执行器直接跑完,那就退化成开环控制,模型误差稍微有一点,后半段必然失控。
有个错误我见过很多次:规划器给出完整轨迹后,执行器全程不参考真实状态。模型精度高、环境噪声小的时候还能勉强应付,但只要有一点未建模摩擦力或者延迟,后半段轨迹基本报废。所以我特意把规划器输出的exec_horizon默认设为 1,只有测试过模型相当稳、且实时性实在不允许时,才允许把这个值调大一点。
5. 工具箱实战中的坑与排查技巧
5.1 高频问题速查表
调试连续动作 MBRL 那段时间,我攒了一张非常实用的排查表,几乎每个问题都能在表里找到第一反应。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 模型训练 loss 很低,但回滚轨迹很快发散 | 验证集切分太随机;回滚误差没被监控 | 改按轨迹切分;打开回滚评估曲线 |
| 规划器输出几乎是一条直线,动作不变化 | 动作变化惩罚太大;CEM 方差过早收敛到 0 | 调小动作惩罚;给方差设下限 |
| 真实环境效果明显差于模型预测 | 推理时没加载归一化参数;动作范围处理不一致 | 检查归一化 JSON;校验动作边界 |
| 模型预测的 next_state 几乎等于 state | 直接预测了下一个状态,网络复制输入 | 改成预测 delta_state |
| 真机设备剧烈抖动 | 动作序列不平滑;控制周期与执行器频率不匹配 | 加动作平滑;调整重规划频率 |
| 奖励分数忽高忽低、规划器不稳定 | 奖励函数噪声大;模型输出存在 NaN | 清洗异常数据;对奖励做平滑归一 |
这张表不是万能药,但真的能把“玄学调参”拉回到“找原因”的路子上。
5.2 三个让我改代码的调试现场
第一个现场是 CEM 方差崩溃。连续几次实验里,规划器一开始还能做出多样化动作,几轮迭代后候选样本全挤在一个窄区间,分数也不再提升。排了很久发现是方差更新逻辑只降不升,探索能力断崖式消失。修复方案很简单:方差不低于 0.05,连续 5 次迭代分数无提升就把方差恢复到初始值,让探索重新启动。
第二个现场是集成模型内部打架。5 个子网络各自验证集回滚误差都不错,但规划时取均值后的效果反而比单模型还差。排查下来发现是随机种子生成太接近,网络初始化和 batch 顺序高度相似,集成退化成多个相似模型的平均,个体间的真实差异根本没体现。换成互不相同的随机种子、不同 batch 洗牌方式、不同 dropout mask 之后,集成才开始提供有效的不确定性信息。
第三个现场是真实系统的时滞问题。仿真环境里调好的策略切到真机就动作滞后,追踪轨迹永远慢半拍。最后确认是传感器滤波和指令延迟导致状态与动作的时间对齐错位。用时间戳把传感器状态、控制指令、执行反馈对齐,再用对齐后的数据重新训练模型,问题才消除。纯仿真环境很难遇到这类问题,但既然工具箱面向“从数据到连续动作”,真机环节必须提醒。
5.3 关于代码工程质量的一点坚持
连续动作 MBRL 的坑一多,就特别需要工程纪律。工具箱里我做了一个很固执的决定:所有关键配置全部收进一个config.yaml,包括环境名称、状态维度、动作范围、归一化参数、模型结构、CEM 参数、采样器路径和随机种子。任何训练和推理脚本都从配置读取,而不是在代码里写死。这样换任务时只需要改配置文件,就能避免“搬代码时漏改一个参数”这种灭顶之灾。
另外,每次重训模型之前,我会强制跑一次自检脚本:从历史轨迹中挑几条,让模型对随机起点做一步预测,打印逐维度误差。只要自检输出的数量级不对,就能迅速定位是归一化没加、维度错位还是动作范围没对齐。这个一分钟的自检,替我节省过的调参时间加起来绝对超过一天。工具箱能不能真正帮你,不在于核心算法多新颖,而在于它在你不注意的角落替你拦住了多少隐形错误。
走到这里,“从数据到连续动作”这条链就完整了:数据管道把原始轨迹盘成模型的食物,动力学模型在虚拟环境里推演未来,CEM 规划器从推演中搜出连续动作,MPC 回环把推演结果不停拉回现实。我个人最大的体会是,基于模型强化学习工程的难点从来不是单独哪个环节的算法设计,而是把数据、模型、规划三条线拧成一个能自我纠偏的整体。好在工具箱把那些反复出现的坑都提前踩了一遍。每当我换上新环境新任务,优先做的永远是三件事:核对动作范围、按轨迹切分数据、盯住回滚评估曲线。只要这三件事稳了,后面的建模和规划基本不会让我加班到深夜。