做强化学习这几年,如果说哪个问题最让人头疼,我一定提名混合动作空间。很多任务不是单纯离散动作或连续动作,而是两者掺杂在一起。比如机械臂拧瓶盖:先要选“拧”这个离散动作,同时还得决定“用多大的力矩”、“朝哪个方向转”这些连续参数——离散动作和连续参数必须同时输出、同步优化。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的核心思路:多遍前向取最小
MPDQN(Multi-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_{k=1..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-K,K=2时性价比最高。K=3性能有提升但训练时间涨了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_heads=2): 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_idx=None): # 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], dim=0) # [heads, batch, num_actions, 1] q_values = q_values.min(dim=0).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(dim=1, keepdim=True) 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,防止参数快速漂移 |
| 多遍头数K | 2 ~ 3 | 2性价比最高 |
| 目标网络软更新τ | 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不是越大越好。K=4时虽然Q值更保守,但参数网络的梯度信号也变得非常钝——因为梯度要流经min操作选择的最小头,其他头的梯度被block掉了。结果就是参数网络的学习效率大幅下降,训练反而更慢。实测K=2效果最好,K=3略好但训练时间多了三分之一。
第四个坑:混合动作空间的奖励塑造极其关键。我遇到的最气人的情况是,离散动作已经学对了,但连续参数始终精确度不够。后来发现是奖励函数没有对参数误差做约束。比如“拧瓶盖”任务,如果只奖励“成功或失败”这种稀疏奖励,参数网络根本学不到精确位置。我的做法是把奖励拆成几层:完成主任务给大奖励,参数接近目标给中奖励,每步存活给极小惩罚。这样参数网络有了梯度的“中介信号”,学起来明显更快。
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-K(K=2),效果一般已经有明显改善。如果效果还不够,再考虑增加Q头数或引入更复杂的目标值计算。在混合动作空间这个领域,“先用最简单的结构跑通,再逐步加复杂度”永远是最稳的路线。