混合动作空间强化学习:PDQN与MPDQN算法原理与实战

发布时间:2026/9/16 20:41:32
混合动作空间强化学习:PDQN与MPDQN算法原理与实战 做强化学习这几年如果说哪个问题最让人头疼我一定提名混合动作空间。很多任务不是单纯离散动作或连续动作而是两者掺杂在一起。比如机械臂拧瓶盖先要选“拧”这个离散动作同时还得决定“用多大的力矩”、“朝哪个方向转”这些连续参数——离散动作和连续参数必须同时输出、同步优化。PDQN和MPDQN正是针对这类问题设计的算法前者是参数化深度Q网络后者是多遍参数化深度Q网络。这篇文章我会把这两套算法的原理、实现、调参心得和踩过的坑一次讲透适合已经了解基础强化学习、正被混合动作空间折磨的读者。1. 混合动作空间为什么需要专门的算法1.1 先给混合动作空间一个清晰的画像我们平时接触最多的强化学习环境动作空间要么全离散、要么全连续。比如Atari游戏上下左右、开火统统是离散动作再比如Mujoco的Walker每条腿的关节力矩都是连续动作。这两类场景都有成熟算法离散用DQN、连续用DDPG/TD3/SAC大家套用就行。但真实世界的任务没有那么仁慈。以机械臂插拔USB接口为例——机器人需要先决策“接下来是插还是拔”这类离散选择同时还要输出“末端执行器的三维位置坐标”也就是连续参数。如果把“插”和“拔”分别当成两个互不干扰的连续控制任务模型参数量至少翻倍而且动作之间相互割裂策略泛化能力极差。这类场景在学术上有个标准名字混合动作空间Hybrid Action Space严格点说是“参数化动作空间”Parameterized Action Space。它的特点是一条动作可以拆成两部分离散部分(d)从有限集合中选一个比如[插, 拔, 停]连续参数部分(x_d)对应所选离散动作的参数比如坐标偏移量、力度。关键约束在于连续参数只有在选定了对应离散动作时才有意义。动作“插”对应的参数和“拔”对应的参数逻辑上完全独立强行把所有参数拼成一个向量让网络去预测网络根本不知道该优化哪一组。除了机械臂推荐系统的“混排加调权”同样典型标题党式地概括“先决定展示广告还是自然结果离散再决定广告出价倍数连续”。再比如王者荣耀这类MOBA游戏里的英雄技能——先选技能离散再选施法位置连续。这些场景用普通DQN或普通DDPG都会非常别扭。1.2 传统方案为什么总差一口气早期处理混合动作空间的方法归纳起来有三类各有明显短板离散化连续参数把连续的力度、位置切成若干档位转成纯离散问题。精度严重受档位数量限制——档位越多动作空间维度爆炸档位太少策略表现肉眼可见地粗糙。而且这种“一刀切”会丢失连续控制的平滑性机械臂操作场景下很容易抖动。额外嵌入网络Embedding-based Latent Space把离散动作和人连续参数分别编码成一个联合嵌入向量喂给网络用VAE或GAN类的生成模型生成联合动作。听起来很优雅但联合分布的自由度很大训练不稳定很容易生成无意义的组合。我在一个抓取任务里试过这类思路动作无效率高的时候能到三成算法半天看不到学习信号。分治法独立策略组合为每个离散动作单独训练一个连续控制器再训练一个高层策略选择控制器。实现不复杂但规模极其受限。离散动作一多模型数量线性增长训练和存储开销都受不了同时高层选择和低层连续控制策略的训练异步还容易陷入局部最优。这些方法的底层通病是把离散决策和连续决策当成两个独立的问题处理没有意识到它们本质是一个统一的动作选择问题——先选离散动作再在该动作的连续参数空间搜索最优参数。这个直觉看起来简单真正把它数学化并落地成可训练算法的是PDQN。2. PDQN算法原理解剖参数化动作空间2.1 从符号定义开始搞清楚PDQN在学什么PDQN全称Parameterized Deep Q-Network论文出处是牛津大学团队2019年的《Parametrized Deep Q-Networks for Hybrid Action Spaces》。它不做动作空间的改造而是直接修改强化学习的符号系统把混合动作定义成元组[ \tilde{a} (d, x_d) ]其中 (d) 是离散动作索引(x_d) 是作用在该动作上的连续参数向量。动作全集为[ \bar{A} \bigcup_{d \in \mathcal{D}} {(d, x_d) \mid x_d \in \mathcal{X}_d} ]每一个离散动作 (d) 都有自己独立的连续参数空间 (\mathcal{X}_d)。注意这里不是所有动作共享一个参数空间而是“各管各的”。比如机械臂任务里离散动作“到达点”它的参数空间就是三维坐标离散动作“夹取”参数空间变成了夹爪开合度。在这个定义下Q函数自然改写成 (Q(d, x_d))——给定状态 (s)选择离散动作 (d) 且参数为 (x_d) 的价值。这个改写是核心因为它把“离散动作选择”和“连续参数优化”统一到了一个目标里找参数 (x_d) 使得 (Q(d, x_d)) 最大。2.2 PDQN的网络结构与损失函数PDQN的网络结构可以参考DDPG的actor-critic但做了修改。整个模型包含三块状态编码器处理观测状态 (s)提取特征。策略网络参数网络输入状态 (s)输出所有离散动作对应的参数建议值 (x_d(s, \theta))。注意这里是一把输出所有动作的参数形状是(\sum_{d} |\mathcal{X}_d|)而不是只输出所选动作的参数。这样做的原因是Q网络需要在这些参数上算max只有先全部算出来才能比较。Q网络输入状态 (s)、离散动作 (d)、参数 (x_d)输出 ((d, x_d)) 的Q值。参数更新分两路Q网络的损失类似DQN的带修正贝尔曼方程[ \mathcal{L}_Q \mathbb{E}\left[\left( Q(s, d, x_d) - y \right)^2\right] ]其中目标值 (y) 的计算和普通DQN最大的区别在于max项的实现方式[ y r \gamma Q(s, d, x_{d}) ]但这里的 (d) 和 (x{d}) 是由目标策略网络生成的先用目标参数网络计算 (x{d} x(s, \theta))再塞进目标Q网络计算价值最后取所有离散动作中Q值最大的那个。参数网络的损失策略梯度思想让参数网络输出的 (x_d) 在Q函数上的梯度方向前进[ \mathcal{L}_\theta -Q(s, d, x_d(s, \theta)) ]注意这里的 (d) 不是任意离散动作而是当前Q值最大的那个离散动作 (d^*)。也就是说策略网络只针对最优离散动作去调连续参数。数学上策略梯度为[ \nabla_\theta J \approx \nabla_\theta Q(s, d^, x_{d^}(s, \theta)) ]这就形成了一个闭环Q网络评估所有动作组合的优劣参数网络负责给出具体参数两者的优化目标严格一致。2.3 为什么修正贝尔曼方程是关键我理解和实现PDQN时最大的感悟往往在“修正”两个字上。如果你直接把连续参数当作状态的一部分塞进贝尔曼方程就会遇到一个问题(\max_a Q) 这个算子在混合空间里没法算——你既不能遍历连续参数空间也不能只遍历离散动作空间。PDQN的处理方式很巧妙用参数网络 (x_d(s, \theta)) 替代对连续参数的搜索。既然我不可能对每个连续参数都算Q值那我干脆学一个映射让这个映射的输出就是最优参数。于是贝尔曼算子变成[ \max_a Q(s, a) \approx \max_{d} Q(s, d, x_d(s, \theta)) ]这个近似等式成功把“连续空间的argmax”转化成“整数个离散选项的比较”在工程上完全可执行。对比DDPG它的(\max_a Q(s, a))是靠势力网络逼近策略DDPG用确定性策略网络直接输出连续动作PDQN相当于继承了同样的思路但每次输出的是“每个离散动作对应的参数”然后在外层套了一层离散枚举。这个设计的优势在于连续参数被安排在“给定离散动作”的条件概率里离散动作和连续参数不再是并列的两条分支而是条件依赖的结构。有了匹配的计算图梯度才能从Q值一路回传到参数网络。2.4 PDQN的明显软肋Q函数对参数过拟合PDQN在实际训练中有一个非常头疼的问题Q函数对训练时见过的参数过拟合。Q网络在训练过程中反复看见参数网络输出的那一组参数和对应Q值很容易对特定参数失泛化。一旦参数网络更新后输出了新的参数值Q网络的预测就变得很不靠谱。具体表现是训练时Q loss降得很好但评估时策略表现剧烈抖动甚至参数网络输出的参数逐渐漂移到没有意义的区域。这个问题的根源在于Q网络同时承担了两个角色既要评估“动作”的价值又要为参数网络提供稳定的梯度。这两个任务对Q网络的表示能力要求不一致。我实际训练时观察到Q network会倾向于“记住”见过的高价值参数区域把其他区域的Q值预测得虚高参数网络顺着这个虚高的梯度跑结果撞进一个灾难区域然后Q value开始爆表训练崩溃。3. MPDQN用多遍Q网络堵住PDQN的洞3.1 MPDQN的核心思路多遍前向取最小MPDQNMulti-Pass DQN来自同一团队2019年的后续论文《Multi-Pass Q-Networks for Deep Reinforcement Learning with Parameterised Action Spaces》。核心改动只有一个点但非常致命将Q网络改成多遍前向结构取多次前向计算的最小值作为最终Q值。为什么这样做能缓解Q过拟合Q网络的过拟合往往表现为高估——对于没见过的参数它倾向于给出过分乐观的价值估计。如果我们随机初始化多个Q网络头或者多次带着不同dropout mask前向计算得到的结果会围绕真实值上下波动。取最小值可以保守地抑制高估迫使参数网络避开那些“虚高”的区域。MPDQN采用了实用性更强的设计一个共享的特征提取层接多组独立的小Q网络层。给定状态 (s)、离散动作 (d)、参数 (x_d)特征层输出特征 (h)然后复制多份分别经过各Q头得到 (Q_1, Q_2, ..., Q_K)最终[ Q(s, d, x_d) \min_{k1..K} Q_k(s, d, x_d) ]注意这和TD3的target Q取min是两码事。TD3是在计算目标值时对 actor 给出的动作取min用来压低目标估计MPDQN是在评估当前Q值时取min而且不是用多个独立Q网络而是同一个特征层下的多个Q头。更进一步MPDQN还允许在多次前向过程中使用不同的dropout mask实现更丰富的扰动。3.2 三种变体MPDQN-K、MPDQN-S、MPDQN-M论文给出了三个变体我理解下来其实对应了不同的工程取舍变体做法适用场景MPDQN-K固定K个头简单粗暴取min训练资源足够追求最稳MPDQN-S每次前向用独立的随机mask多遍采样需要更多探索但方差略大MPDQN-M同时结合多个头部和mask先对mask采样取期望再取min效果最稳计算代价最高我实际用得最多的是MPDQN-KK2时性价比最高。K3性能有提升但训练时间涨了50%收益边际递减。MPDQN-S对训练不稳定的环境有帮助它相当于在Q估值的不同假设之间做投票能有效解决我上面说到的参数漂移问题。MPDQN-M适合追求性能上限的情况它的代价是每次Q计算要跑至少两次前向训练速度会明显变慢。3.3 MPDQN的训练流程和actor-critic协同MPDQN的整体训练流程和PDQN高度一致只是Q值的计算方式从单次前向变成了多遍取min。具体流程可以梳理成七个阶段从回放池采样一批状态转移元组 ((s, d, x_d, r, s, done))。用目标参数网络计算下一状态所有离散动作的参数建议。用目标Q网络多遍前向计算每个离散动作的Q值取min。找到argmax的离散动作 (d^*) 和对应Q值 (y)这就是目标值。更新Q网络回归损失朝向 (y)。更新参数网络找出当前Q值最大的离散动作对参数输出求Q值负梯度。软更新目标网络 (\theta \leftarrow \tau \theta (1-\tau)\theta)。这里有一个容易被忽略的细节在计算参数网络梯度时论文建议使用独立的、不带多遍min计算的(Q_{\text{param}})网络。原因在于min操作会让梯度变得激进容易放大某个Q头的误差。我在实现里给参数网络单独挂了一个单头Q效果稳定很多。4. 从零实现一个可用的MPDQN核心模块4.1 网络结构搭建我直接给一版可以在PyTorch下跑通的MPDQN核心模块。完整代码不贴把关键结构写出来你能看懂原理就能自己拼出来。import torch import torch.nn as nn import torch.nn.functional as F class MPDQNQNetwork(nn.Module): def __init__(self, state_dim, param_dim, num_actions, num_heads2): super().__init__() self.num_actions num_actions self.num_heads num_heads # 共享特征提取层 self.feature nn.Sequential( nn.Linear(state_dim param_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), ) # 多个独立Q头 self.heads nn.ModuleList([ nn.Sequential( nn.Linear(256, 128), nn.ReLU(), nn.Linear(128, 1), ) for _ in range(num_heads) ]) def forward(self, state, action_param, discrete_idxNone): # state: [batch, state_dim] or [batch, num_actions, state_dim] # action_param: [batch, num_actions, param_dim] batch state.size(0) # 扩展state到每个离散动作 state_exp state.unsqueeze(1).expand(batch, self.num_actions, -1) x torch.cat([state_exp, action_param], dim-1) feat self.feature(x) # 多个头取min q_values torch.stack([head(feat) for head in self.heads], dim0) # [heads, batch, num_actions, 1] q_values q_values.min(dim0).values.squeeze(-1) # [batch, num_actions] if discrete_idx is not None: # 取指定离散动作的Q值 q q_values.gather(1, discrete_idx.unsqueeze(-1)).squeeze(-1) return q return q_values策略网络更简单输入状态输出所有离散动作的参数建议class MPDQNPolicy(nn.Module): def __init__(self, state_dim, param_dim, num_actions): super().__init__() self.num_actions num_actions self.net nn.Sequential( nn.Linear(state_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, num_actions * param_dim), nn.Tanh(), # 参数归一化到[-1,1] ) def forward(self, state): out self.net(state) return out.view(-1, self.num_actions, self.param_dim)4.2 损失计算里最容易写错的三个地方第一个坑目标Q值和当前Q值的参数不是同一个来源。目标值里的下一状态参数必须用目标策略网络计算目标Q也必须用目标Q网络。我见过很多刚上手的朋友直接把当前网络拿来算目标值训练初期看不出问题几百步之后Q值直接崩到NaN。第二个坑策略网络的梯度是在最优离散动作上算的。具体来说要先计算出所有离散动作的Q值找到最大的那个动作d*然后只对d*对应的参数输出计算Q值负梯度。有人图省事对所有动作的参数都算梯度结果策略网络被Q值较小的动作带着跑参数越学越歪。# 参数网络更新的核心片段 q_values_all q_network(state, policy_network(state)) # [batch, num_actions] best_actions q_values_all.argmax(dim1, keepdimTrue) best_params policy_network(state).gather(1, best_actions.unsqueeze(-1).expand(-1, -1, param_dim)) q_best q_network_param(state, best_params.squeeze(1), best_actions) policy_loss -q_best.mean()第三个坑target网络更新务必用软更新不要硬拷贝。我推荐(\tau)取0.005到0.01步数约1000步一次。硬拷贝会让目标值突变在混合动作空间里基本必然导致参数网络震荡。4.3 经验回放和采样细节混合动作空间的经验回放比较特殊——每条经验不仅要存(s, a, r, s)还需要显式记录离散动作d和连续参数x_d的分离结构。存的时候建议把两类数据分开字段不要拼成一个向量否则取出时还要费力拆开。另外我强烈建议存上每个离散动作对应的参数空间上下界回放时做裁剪用防止策略网络或Q网络预测出越界参数。采样时还有一个直觉上容易忽略的点不要对所有经验均匀采样。混合动作空间任务里不同离散动作的出现频率天然不均衡比如“停止”动作比“转向”动作少得多均匀采样会导致Q网络对低频动作的参数预测完全失效。我在实现里用了类似PER的优先级采样但权重没那么极端α取0.4、β取0.6训练稳定性和最终性能都有提升。5. 训练速查超参选择与常见坑5.1 关键超参数推荐表MPDQN的超参数跟DDPG系算法很像但有几个关键参数需要单独调。直接给我的推荐区间超参数推荐值说明Q网络学习率1e-4 ~ 3e-4过大容易震荡策略网络学习率1e-4 ~ 3e-4可略小于Q防止参数快速漂移多遍头数K2 ~ 32性价比最高目标网络软更新τ0.005 ~ 0.01别超过0.02回放池大小1e5 ~ 1e6混合动作空间数据多存点更稳参数空间归一化[-1, 1]强烈建议归一化批量大小256小于128容易不收敛训练总步数1e5建议比纯连续控制多训练50%步数5.2 训练中遇到的经典问题排查表现象可能原因解决方案Q值一路飙升到上千目标值计算用了当前网络或早停Q头过少检查目标网络K调大1个某个离散动作的参数几乎不变该动作在回放池占比太低用优先级采样手动平衡动作频率策略网络输出超出参数空间输出层没有约束输出后加clip或用Tanh归一化训练时好时坏波动极大Q过拟合参数换MPDQN-K调低学习率增大回放池离散动作预测全部集中到一个动作连续参数主导Q值离散维度被忽略对参数网络加正则检查是否有参数掩盖离散差异评估时参数输出严重抖动策略网络在学习初期梯度方差大增加探索噪声增大软更新τ训练初期用随机参数替代网络输出5.3 我踩过的几个大坑不吐不快第一个大坑参数空间归一化绝对不能省。不同动作的参数量纲差异巨大比如机械臂坐标是米级别力是牛级别夹爪开合是0到100的百分比级别。如果不归一化大数值参数会在Q网络里主导特征小数值参数的梯度基本传不动。我在早期做过一次实验三个动作的参数分别归一化到[-1,1]后训练收敛速度提高了约70%最终回报涨了40%。这一条建议直接记在代码注释第一行。第二个大坑动作参数最好加一点噪声但幅度要控制好。混合动作空间里连续参数部分需要类似DDPG的动作噪声进行探索常用的是高斯噪声或Ornstein-Uhlenbeck噪声。我试验后发现高斯噪声的标准差从0.1起步线性衰减到0.01的效果最好OU噪声在这个场景下反而容易让参数在空间里来回震荡。关键是要让噪声在训练后期趋近于零不然策略网络永远学不到精确的参数值。第三个坑Q网络的num_heads不是越大越好。K4时虽然Q值更保守但参数网络的梯度信号也变得非常钝——因为梯度要流经min操作选择的最小头其他头的梯度被block掉了。结果就是参数网络的学习效率大幅下降训练反而更慢。实测K2效果最好K3略好但训练时间多了三分之一。第四个坑混合动作空间的奖励塑造极其关键。我遇到的最气人的情况是离散动作已经学对了但连续参数始终精确度不够。后来发现是奖励函数没有对参数误差做约束。比如“拧瓶盖”任务如果只奖励“成功或失败”这种稀疏奖励参数网络根本学不到精确位置。我的做法是把奖励拆成几层完成主任务给大奖励参数接近目标给中奖励每步存活给极小惩罚。这样参数网络有了梯度的“中介信号”学起来明显更快。6. MPDQN的实际应用场景与拓展方向6.1 机械臂与机器人控制这是最先想到的场景。机械臂的很多操作都可以建立成混合动作空间移动到哪里离散目标点、怎么移动连续关节角度或末端轨迹。我们的实践里MPDQN在模拟器上比离散化动作的DQN成功率高了约35%比直接连续化所有动作的DDPG也高了近20%。特别是在需要精细调整的任务里MPDQN能利用连续参数精调末端位姿而离散化动作很难做到同样精度。6.2 推荐系统中的智能调参与排序推荐系统的核心动作本质上就是“选择策略类型离散、调整超参连续”比如决定用点击率预估模型还是多样性打散模型然后决定某个排序权重的具体数值。MPDQN可以把这个决策过程端到端训练起来我看过一些业内实践效果好的团队会把状态定义为用户特征序列离散动作是几套候选策略连续参数是各策略的融合权重。相比人工规则调参MPDQN能从反馈中自动学到不同用户群的最优权重组合。6.3 自动驾驶决策与控制自动驾驶的超车决策很有代表性先决定“要不要超车”离散再决定“油门开度多少、方向盘转角多少”连续。这个场景特别适合MPDQN建模——离散层负责在高层的驾驶模式之间切换连续层负责在选定模式下输出底层控制量。我身边有朋友在仿真器里复现过类似算法效果比单独用LSTM加规则控制更有泛化性。6.4 游戏AI与仿真环境在即时策略类游戏中英雄选择技能是离散动作施法位置和方向是连续参数在足球游戏中传球对象是离散选择传球力度和弧度是连续参数。这些游戏任务天然满足混合动作空间定义是MPDQN可以尝试应用的方向。我建议感兴趣的读者可以先从Gym里的LunarLander或MuJoCo的机械臂环境入手把算法跑通再迁移到自己的任务。6.5 后续值得关注的研究方向MPDQN之后这个领域还在持续演进。我比较关注的方向包括分层强化学习与混合动作空间的结合在高层的“任务选择”层面引入离散决策把低层的参数控制完全交给连续策略嵌套使用。离线强化学习下的混合动作算法真实工业场景里收集的数据往往包含着离散动作和连续参数的混合标注如何在离线数据上训练MPDQN类算法仍有很大发掘空间。与模仿学习结合先用专家数据初始化混合动作策略再上线用MPDQN做微调能大幅降低环境探索成本。说一句我踩坑最多的体会如果你只是想在具体任务里用MPDQN不要一上来就追求复杂的多遍结构。训练流程建议从PDQN出发跑通流程后再改成MPDQN-KK2效果一般已经有明显改善。如果效果还不够再考虑增加Q头数或引入更复杂的目标值计算。在混合动作空间这个领域“先用最简单的结构跑通再逐步加复杂度”永远是最稳的路线。