☰
基于模型的强化学习实战:Python工具箱实现连续动作控制
2026/10/12 20:02:49 网站建设 项目流程

这几年一直在折腾连续运动控制,最头疼的是把一堆采集下来的数据变成能在真实或仿真环境里稳定跑起来的连续动作。基于模型的强化学习(MBRL)之所以让我彻底转向,是因为它把“学环境动态”和“优化策略”拆成两个相对独立的环节,在样本受限的工况下,数据利用效率比纯无模型的思路高一截。连续动作不像离散动作那样按按键选择,油门、关节力矩、电机转速全是连续向量,策略稍微不稳,系统马上开始抖动或者发散。

这篇文章是我基于模型的强化学习 Python 工具箱实战的第一篇,目标是完整讲清楚一条链路:怎么整理数据、怎么训练环境动态模型、怎么把模型拿来规划并输出连续动作。整个实践过程我已经跑过几轮,里面涉及到的模块都保持框架无关,纯 Python 组织,能平移到你自己的工具箱里。适合刚接触 MBRL、想在连续控制场景里落地的朋友参考。下面直接进入正题。

1. 为什么在连续动作场景里选择基于模型的强化学习

1.1 连续动作空间到底难在哪

连续动作空间的标准定义并不复杂:每一步动作a是一个向量,每个分量的取值范围通常是某个区间,比如关节力矩限制在[-1.0, 1.0]之间,油门开度在[0, 1]之间。离散动作可以枚举,连续动作却有无穷多个取值,策略网络输出维度不高,但对精度和稳定性很敏感。

我一开始用常见的无模型算法做连续控制,比如采用 actor-critic 结构,理论上能覆盖连续动作,但实际落地时有两个痛点:第一,样本需求非常大,连续空间探索效率低,很多状态根本没去过,策略只能靠大量试错覆盖;第二,策略梯度对奖励尺度、网络初始化和超参数敏感,稍微调得不好,动作就会高频振荡,看起来像“电风扇式”抖动。

这时候基于模型的路线优势就体现出来了。它先把“环境怎么响应”这件事单独学出来,再用学出来的模型做规划或者虚拟轨迹生成。相当于你先建一座“风洞”,再让策略在风洞里试飞,而不是每次都在真机上试错。对样本量有限、动作维度又连续的任务来说,这条路明显更合理。

1.2 基于模型路线的核心思路

基于模型的强化学习,核心是训练一个关于环境动态的预测函数。给定当前状态s_t和动作a_t,模型输出下一状态s_{t+1},同时最好也预测即时奖励r_t和终止标志done_t。有了这个预测函数,我就能在当前状态下“想象”未来一段轨迹。

这个思路的关键并不在模型本身,而在于怎么用它。一种方式是把模型当作模拟器,用模型生成海量轨迹来训练策略;另一种方式是我在这次工具箱里更常用的方式:用模型做模型预测控制(MPC),每个控制周期规划一组连续动作序列,只执行第一个动作,然后滚动重规划。这种方式不需要单独维护价值函数,也不需要让策略网络直接输出连续动作,规划过程天然保证了动作合理性。

我选择 MPC 的另一个原因是连续动作任务通常有明确的物理约束,比如速度限制、力矩限制。MPC 可以在规划时显式限制动作范围,甚至把动作平滑度写进代价函数。相比让神经网络“隐式”学会约束,这种显式处理更可靠,调试也直观。

1.3 工具箱的三个模块和整体流程

这个 Python 工具箱我拆成三个大模块:数据模块、模型模块、规划模块。数据模块负责与环境的交互、经验缓冲区的读写和归一化预处理;模型模块负责训练环境动态模型、评估模型精度、处理集成的不确定性;规划模块负责从当前状态出发,采样连续动作序列,用模型前推轨迹,计算代价值,并返回最优动作。

整体流程可以用六个步骤概括:

  1. 与环境交互,收集初始数据,数据来源于随机探索策略或者混合策略;
  2. 对状态、动作、奖励做归一化统计,写入经验缓冲区;
  3. 用缓冲区的小批量数据训练动态模型,通常训练多个模型组成集成;
  4. 在真实环境中采样当前真实状态;
  5. 用训练好的模型做滚动时域规划,搜索最优连续动作序列;
  6. 执行第一个动作,等下一时刻获得新状态后,重新规划。

这样每个环节都能单独拿出来测试。数据模块检测数据分布是否覆盖完整,模型模块测试预测误差,规划模块测试连续动作是否平滑。我在实战中遇到的绝大多数问题,最后都能定位到这三层之间的接口上。

2. 从数据到可学习样本:数据收集与预处理的细节

2.1 数据从哪里来:探索策略与样本覆盖

基于模型的算法对数据的要求,和普通监督学习不太一样。监督学习希望样本独立同分布,而强化学习的数据来自环境交互,存在强时序相关性。初始阶段,我一般用随机策略和一个小收益导向策略混合跑数据。随机策略能充分探索动作空间的边界,比如关节力矩的上限和下限;收益导向策略则能让系统靠近一些“有用”的动力学区域,比如维持平衡、追踪轨迹的附近区域。

如果只用随机策略,数据里大量是无意义的高速度高力矩状态,模型很难学会低速段的行为;如果只用已训练的策略,模型又容易在未见过的状态上“睁眼瞎”。我现在的做法是:先跑一批随机数据打底,然后用最朴素的零阶规划器生成少量相对平滑的轨迹,混进缓冲区。这样数据里既有高探索多样性的样本,也有接近真实控制序列的局部结构,训练出的模型泛化会好很多。

另外,数据量不是越大越好。动态模型需要的数据规模通常在几十万条转移样本量级,但如果你把采样时间步设得太短,连续两步的样本几乎完全相同,模型会过度拟合低频部分。我一般会根据任务周期设置一个最小采样间隔,确保相邻样本在状态空间上有一定间隔。

2.2 样本组织:经验缓冲区的设计与时序对齐

经验缓冲区是数据和模型之间的桥梁,设计不好会直接污染训练。每一个转移样本至少包含五个字段:当前状态、当前动作、下一状态、即时奖励、终止标志。在连续控制环境里,我建议额外保存动作的实际执行值,因为有些环境会存在动作延迟或饱和,保存原始指令和执行结果能帮你排查模型误差来源。

我实现了一个很朴素的循环缓冲区类,容量设置成可配置的,默认是最近五十万条转移样本。循环缓冲区的好处是内存可控,且足够新的数据会覆盖老旧分布,保证模型始终贴近最近环境状态。但也要注意,如果环境本身是时变的,比如温度变化导致机械特性偏移,旧样本占比过大反而会让模型预测不准。这时可以给样本加上时间戳,在采样时给近期样本更高权重。

代码层面不依赖任何特定框架,核心逻辑很直白:

class ExperienceBuffer: def __init__(self, capacity): self.capacity = capacity self.states = [] self.actions = [] self.next_states = [] self.rewards = [] self.dones = [] self.index = 0 def push(self, s, a, s_next, r, done): if len(self.states) < self.capacity: self.states.append(s) self.actions.append(a) self.next_states.append(s_next) self.rewards.append(r) self.dones.append(done) else: self.states[self.index] = s self.actions[self.index] = a self.next_states[self.index] = s_next self.rewards[self.index] = r self.dones[self.index] = done self.index = (self.index + 1) % self.capacity

注意:终止标志done必须准确。很多连续控制任务里,系统走出稳定区间后会被环境强制终止,如果不记录这个标志,模型会把“被外部重置”误认为正常的动力学转移,导致预测下一状态时产生巨大偏差。

2.3 预处理关键点:状态、动作、奖励与异常转移

动态模型的输入输出尺度直接影响训练稳定性。连续控制的状态量纲各异,比如角度可能在0.01级别,角速度却在10级别,如果直接喂进神经网络,模型会倾向于忽略小量纲特征。我在实践中会维护一组运行均值和方法差,对所有状态、动作、下一状态做标准化,模型学习的目标也同步标准化。

奖励处理更值得注意。原始奖励的尺度有时候非常大,比如某些任务为了鼓励远距离前进,奖励会达到100以上;有时候又小到0.001级别。如果直接把奖励作为回归目标,损失很容易被极端值主导。我通常会做一次奖励裁剪或者奖励缩放,让奖励目标落在[-1, 1]附近。这个预处理不是简单的归一化,还要考虑任务目标:如果奖励存在稀疏长尾,可以拆解成“距离项+存活项”的加权组合,让模型学到更稳定的奖励面。

异常转移样本也必须处理。比如环境因为物理穿透或数值发散输出了一个 NaN 状态,或者某一步奖励远超正常范围,这种样本要直接剔除,不能进缓冲区。我踩过一次坑:仿真环境在某个特殊姿态下产生了超大加速度,导致状态出现极端值,模型训练时损失直接爆炸。后来加了异常检测,对每个状态分量的绝对值上限做校验,超过阈值就丢弃并打印警告。

3. 环境动态模型:网络结构、不确定性处理与训练策略

3.1 预测什么:从“转移”到“差分”

环境动态模型最常见的输出是直接预测下一状态s_{t+1}。但直接预测绝对状态有一个问题:状态变化通常比较小,网络很容易把输出收敛到“复制输入”的状态,这在动作对状态影响微弱时尤其明显。更好的做法是预测状态增量,也就是delta = s_{t+1} - s_t,模型实际输出的目标变成这个差值。

状态增量预测的意义在于,它让网络只需要学习“当前状态下动作引起的改变量”。对于大多数连续控制任务,系统的惯性使得delta的幅度远小于状态本身,预测难度更低。我的模型输出维度通常等于状态维度,加上一个标量奖励预测,再加上一个标量终止概率。终止概率用二分类输出,在训练时和回归损失一起计算。

代码骨架如下:

# 伪代码:动态模型前向传播 def forward(self, state, action): normalized_state = self.state_norm(state) normalized_action = self.action_norm(action) features = self.encoder(torch.cat([normalized_state, normalized_action], dim=-1)) delta = self.delta_head(features) reward = self.reward_head(features) done_logit = self.done_head(features) next_state = state + delta return next_state, reward, done_logit

注意这里我用了torch.cat这样的写法,实际工具类里可以替换成任意你习惯的张量库。整体结构是框架无关的,只要你保留“状态增量”这个设计,就能规避很多训练陷阱。

3.2 网络结构怎么搭:输入输出、激活、残差与归一化

动态模型的主体网络不需要很花哨。我试过简单的多层感知机,也试过带卷积编码器的结构。对于状态维度在几十以内的连续控制任务,四到五层全连接网络已经足够。每一层设置 256 到 512 个隐单元,中间使用 Swish 或者 ReLU 激活函数。不要在一开始就堆大网络,数据量可能撑不起参数量。

一个非常有效的技巧是残差连接。我在编码器输出之后加一个残差块,让网络既可以选择直接传递输入特征,也可以在此基础上叠加非线性变换。这在动态模型训练里能明显减少“模式坍缩”的情况,也就是模型倾向于输出均值状态而忽略动作的影响。残差连接让“动作是否真的影响下一状态”变成一个可以学习的偏移量。

输入层需要把状态和动作拼接起来。如果状态和动作的维度相差很大,比如状态是 20 维、动作只有 2 维,建议分别做特征提取再融合,避免动作信息被淹没。我的工具箱里有一个可配置的稠密编码层,对状态和动作分别映射到相同维度,再拼接后送入主网络。这在实际调试中让模型对动作的响应灵敏了许多。

3.3 用集成网络量化不确定性

动态模型天生有误差,但在连续动作规划里,我们最怕的不是误差本身,而是“模型不知道自己哪里不准”。解决办法就是训练多个模型组成集成,用多个模型的预测差异来估计不确定性。经典做法是训练 N 个网络,每个网络用不同随机种子或者不同数据子集初始化。预测时让每个模型输出下一状态,然后计算均值和方差。

我一般训练 5 个网络。数量太少估计不稳定,数量太多训练和预测成本线性增长。高维连续动作任务中,预测均值大概在 5 个模型时已经足够平滑,再多提升有限。方差可以用作规划时的惩罚项:如果多个模型预测的下一状态差异大,说明当前状态一动作对是模型不熟悉的区域,代价函数可以加一个不确定性惩罚项,促使规划器避开这些危险区域。

另一个细节是集成模型的初始化差异必须足够大。如果所有模型从相同初始权重开始训练,即使数据分组不同,也会因为梯度下降路径相似而收敛到相近结果,不确定性估计失效。我在每个模型前加不同的正交初始化扰动,效果比只换随机种子更明显。

3.4 训练细节:损失函数、早停、批次与设备

动态模型的训练本质上是多任务回归:下一状态增量、奖励、终止概率三个输出一起优化。状态增量损失和奖励损失都使用均方误差,终止概率使用二分类交叉熵。这三个损失需要加权求和。我在实战中的默认权重是1.0 : 0.5 : 0.1,理由是终止预测只是辅助信号,权重太大容易让网络忽略状态演变。

训练批次大小通常取 256 或 512。动态模型不像图像模型那样需要很大批次,但批次太小会增加方差。优化器我用自适应矩估计类优化器,初始学习率1e-3,每若干轮衰减 0.5。早停非常关键,我会保留一份验证集,当验证集状态增量损失连续十轮不下降时停止训练。这份验证集必须从数据收集阶段就独立出来,不能和训练样本混合。

还要注意训练数据里相邻时间步样本高度相关。如果直接随机打乱,模型会看到太多重复信息。我通常在缓冲区里先按 episode 切块,再在块内随机采样,这样既保留时序结构,又避免两个连续样本同时进批次。

4. 从模型到连续动作:MPC 与交叉熵方法实战

4.1 为什么规划优于直接输出动作

有了环境动态模型之后,下一个问题是:怎么把它变成连续动作指令?一种思路是拿模型生成虚拟轨迹,再训练一个策略网络直接输出动作。这种思路很常见,但实现复杂,而且策略网络本身会引入新的误差。我更喜欢模型预测控制(MPC):每一步都规划未来固定长度的一段动作序列,用动态模型前推轨迹,计算这段轨迹的累计代价,然后选择代价最小的动作序列。

MPC 的优势在于,动作是显式搜索出来的,不是网络的一次前向传播“猜”出来的。规划过程可以穷举很多候选动作,然后在里面挑最好的,天然具有更强的鲁棒性。我最初以为 MPC 计算量很大,实际上对于控制周期 10 到 20 赫兹的任务,现代计算设备完全能承担。如果控制频率更高,可以把规划器挂到单独进程,或者用 GPU 并行采样。

代价函数也比直接优化策略网络容易调。比如我想让机械臂平滑移动到目标点,代价设置为目标距离平方加上动作变化量平方,就可以同时抑制超调和高频抖动。这些规则在规划器里是显式而透明的,换成策略网络就很难这样直观调参。

4.2 CEM:连续动作采样的可靠选择

连续动作规划的核心是在高维连续空间里找一个低代价动作序列。最朴素的方法是随机采样一堆动作序列,前推模型,选代价最小的那个。但纯随机采样在高维空间效率很低。交叉熵方法(CEM)通过迭代地拟合精英样本的分布,逐步把采样的重心移到低代价区域,是实际工程里很可靠的优化方法。

CEM 的完整流程是这样的:

  1. 设定规划时域H,比如未来 10 步;设定动作维度d;
  2. 初始化高斯分布的均值mean为零向量,标准差std为动作上限的一半;
  3. 从高斯分布里采样K组动作序列,每组形状是H x d;
  4. 对每一组候选序列,用动态模型从当前状态开始滚动仿真,累计代价;
  5. 选出代价最低的K_elite组序列;
  6. 用这些精英序列重新估计均值和标准差,然后回到第 3 步;
  7. 迭代 5 到 10 次,取最终得到的最优序列的第一个动作作为当前控制指令。

伪代码可以写得很短:

def cem_plan(model, init_state, horizon, action_dim, iterations, num_samples, num_elites): mean = torch.zeros(action_dim * horizon) std = torch.ones(action_dim * horizon) * 0.5 for _ in range(iterations): candidates = sample_gaussian(mean, std, num_samples) costs = evaluate_trajectories(model, init_state, candidates) elites = select_lowest(candidates, costs, num_elites) mean = elites.mean(dim=0) std = elites.std(dim=0) + 1e-6 best_action = mean[:action_dim] return best_action

每轮迭代里标准差要加一个极小量,防止精英样本分布退化到零方差,导致采样停在一个局部点出不去。这个极小量相当于给探索性留了一个“安全阀”。

4.3 重规划与动作平滑的工程细节

MPC 不是规划一次就完事。真实环境会受模型误差、外部扰动的影响,每一步执行完动作后,需要拿真实感知到的当前状态重新规划。这个“滚动”机制能让控制更稳定。我建议控制周期稳定在一个频率上,比如每 100 毫秒规划一次。如果规划耗时太长,宁可减小规划时域H,也不要让控制频率抖动。

有一个容易被忽略的细节:动作序列的第一段和第二段之间的衔接。CEM 每次重新规划都会得到一个全新的动作序列,但两次规划动作可能在边界处跳变,导致执行机构冲击。我的处理方式是保存上一时刻实际执行的动作a_prev,在代价函数中追加一项动作变化惩罚,形式为lambda_smooth * ||a_t - a_prev||^2。这个惩罚会促使规划器输出平稳连续的连续动作序列。

连续动作的上下限也必须约束。CEM 采到的动作可能超出执行器范围,我会在仿真前做一次 clamp,同时在拟合精英分布时,把超出边界的样本丢弃或者映射回边界附近。如果直接硬 clip,会造成分布积累在边界上,影响后续采样的多样性。更稳妥的办法是把采样空间映射到一个无约束区间,比如用 tanh 变换,让 CEM 在无约束空间里搜索,再在仿真时映射回物理动作范围。

另外,代价函数里的每个惩罚项都需要做好权重调参。目标跟踪项权重太大会导致动作剧烈,平滑项权重太大会导致动作响应迟钝。我一般从零开始逐个加项,先只放目标误差,等系统能接近目标,再加平滑惩罚,然后微调系数。

5. 真实调试记录:常见问题与排查建议

5.1 模型越推越飘,规划效果崩溃

我最常遇到的问题是动态模型在一步预测上误差很小,但滚动多步后状态就飞到离谱范围。原因很典型:单步误差在迭代过程中不断累积,模型训练时没有充分考虑多步一致性。解决思路有两个方向。

第一个方向是多步训练。我试过在训练过程中随机从真实轨迹中取一段状态,让模型连续预测多步,把每步预测误差都累加进损失。这会让模型更重视长期动态一致性,但实现复杂,且有可能放大终端附近的误差。第二个方向更像工程修复:在 MPC 中把规划时域缩短,同时给代价函数增加状态边界惩罚,一旦预测状态越界就施加重惩罚。这个方案简单有效,适合优先尝试。

如果误差累积还是严重,我建议检查训练数据的动作分布。模型在规划时会频繁遇到它没见过的“好动作”,而这些动作可能离训练数据分布很远。解决方法是让数据收集阶段多跑一些 CEM 规划产生的动作,让模型见一见规划器的动作分布,这也是我前面提到混合数据的重要原因。

5.2 连续动作高频抖动

连续动作高频抖动是规划加执行场景里最常见的问题。现象是系统在目标点附近来回震荡,动作序列呈锯齿状。我排查时发现两个主要原因:一是代价函数缺少平滑项,二是 CEM 每次从零开始初始化分布,导致前后两步规划结果不连续。

平滑项我刚才已经提到,加lambda_smooth * ||a_t - a_prev||^2后会明显改善。除此之外,还可以对最终执行动作做一次移动平均,用历史两步动作的加权平均替代当前指令。但移动平均不能太激进,否则会引入相位延迟,让系统反应变得迟钝。我的经验是平滑系数在0.1到0.3之间,具体根据控制频率微调。

还有一个小技巧:CEM 每一代的标准差下界不要设得太小。如果标准差过早收缩到接近零,动作多样性丧失,规划器就会陷入局部极小点,表现为动作卡在一个固定值附近抖。我把标准差下界设为动作幅度的 1% 到 2%,相当于给探索留一个最小随机性。

5.3 模型在训练集表现好、规划时却失效

这种情况通常意味着模型过拟合了训练数据里某些特定状态区域。我在数据模块里要求保存训练验证集,就是为了这个问题。当模型验证集损失很低,但规划时动作乱飞,我首先会画一张预测误差的热力图,看模型在哪些状态区域误差特别大。

如果误差集中在状态空间的边界区域,我会增加数据收集时随机探索的比例。如果误差集中在某个动作值周围,比如动作接近零时误差反而大,说明数据里动作零附近样本不足。连续控制任务里零动作往往对应“保持不动”的工况,很重要,必须专门补采样。

另一个隐蔽原因是标准化参数没对齐。训练时用的状态均值和方差来自训练集,但在线规划时真实状态可能分布不同。如果在线状态超出了原标准化范围,模型预测会失真。我会定期在线收集新的状态统计量,并更新标准化层,这个过程要在模型中动态更新,相当于做了一次简单的在线适应。

5.4 奖励或代价函数过于敏感

代价函数的尺度直接影响 CEM 精英样本的分布结果。如果代价项之间量级差异过大,比如目标距离项的数值远远大于平滑项,规划器会忽略平滑项,动作就会剧烈。我调试时会把每个代价项除以它的经验均值,让各项量级都落在0.1到1.0之间。

奖励预测模型如果训练不好,MPC 的轨迹评价也会失真。常见做法是让奖励预测只作为辅助信号,主要靠任务本身的先验代价函数,也就是不依赖模型学到的奖励,直接根据预测状态计算代价。这样模型只需要预测下一状态,奖励模型是否精确影响不大。我实际用下来,这种“模型只预测状态,代价外部计算”的解耦方式更稳。

当然,如果环境中没有显式可写的代价函数,只能用模型奖励,那我建议把奖励模型的输出做一点低通滤波,避免单步奖励的噪声被 CEM 当成有效信号。滤波的目的是让规划器更关注轨迹的整体收益,而不是单步瞬时波动。

5.5 常见问题速查表

我把实战中容易踩的问题整理成一个速查表,方便大家对照。

表现可能原因建议处置
多步预测发散单步误差累积缩短规划时域,加状态边界惩罚
动作高频抖动代价缺平滑项 / 前后规划不连续加动作变化惩罚,输出移动平均
训练好但规划失效数据分布覆盖不足 / 标准化漂移补探索数据,在线更新标准化统计量
代价函数调不动各项量级差异大按经验均值归一化每个代价项
模型输出 NaN数据本身有异常或网络学习率过高清洗异常样本,降低学习率
集成模型不确定性失效初始化差异不足使用更多样的权重初始化
CEM 收敛到局部解标准差收缩过快标准差加下界,增加迭代次数
在线状态偏移环境动态变化定期重置模型或做在线微调

这张表是我最近十几轮调试经验的浓缩。如果你也遇到表里没覆盖的问题,优先去看数据分布和模型预测误差,大多数问题都能顺着这条线找到根因。

最后再分享一个我个人的感受:基于模型的连续动作控制,最大的瓶颈不在算法理论,而在工程细节。数据怎么组织、状态增量怎么预测、CEM 标准差下界怎么设,这些看起来不起眼的细节,决定了整个工具箱能不能稳定工作。很多次我以为换一个更聪明的规划算法就能解决问题,最后发现只是某一层数据标准化没对齐。先把手头这条数据到动作的链路打磨顺,再琢磨更高级的算法,会省掉大量返工时间。这个系列后续我还会继续拆解模型校准、在线适应和更复杂的策略优化,有具体问题欢迎在实际调试中多交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询