做强化学习的人,可能都遇到过这种让人发疯的情况:环境里的动作要么是离散的(左转、右转、抓取、放下),要么是连续的(方向盘转角、关节力矩、功率大小),但真实世界从来不按教科书出牌。机械臂要“抓取”一个杯子,得同时决定选“抓取”这个离散动作,以及“用多大的力、以什么角度接近”这些连续参数。自动驾驶要在“跟车”“变道”“刹车”之间做离散决策,又要把转向角、加速度这些连续量控制好。这类问题就是今天想聊的核心:混合动作空间。
这类问题让DQN、DDPG、PPO这些主流算法集体失灵——不是哪个方法不好,而是它们的动作建模方式天然就不兼容混合空间。近几年有两篇文章把这条路打通了:一篇是NeurIPS 2019上的P-DQN(Parametrized Deep Q-Networks),另一篇是对P-DQN做重要改进的MPDQN(Multi-Pass Q-Networks)。这篇博客我会从问题定义讲起,把这两个算法的核心机制、数学原理、实现细节和踩坑经验一次说透。无论你是刚入门强化学习的新手,还是正在发愁动作空间建模的研究者、工程师,这篇文章都值得你花二十分钟认真读一遍。
1. 混合动作空间:为什么标准强化学习算法在这里集体失灵
在聊算法之前,得先把“混合动作空间”这个东西钉死。因为很多人在环境建模阶段就搞错了,后面全在硬调bug。
1.1 从“点餐”看动作空间:离散、连续、参数化
动作空间这个概念,说人话就是“智能体在一个状态下,可以做哪些事”。我们可以用点餐来打比方:
离散动作空间,相当于餐厅菜单上固定搭配好的套餐。你只能选套餐A、套餐B、套餐C,不能改里面的任何东西。对应的算法就是DQN那一系,输出一个概率分布或Q值表,选一个动作。这类问题的特点是动作是枚举的,有限且不连续。
连续动作空间,相当于自助餐,你可以自由决定夹多少肉、舀多少汤,动作取值是一个连续区间。对应的算法是DDPG、SAC、TD3这些,输出一个连续向量。这类问题的特点是动作可以是任意实数,没法一个个枚举。
参数化动作空间,也就是本文的主角,相当于餐厅给你定制套餐:你先选一个套餐(离散动作),再自定义里面的配料和用量(连续参数)。形式上可以写成:
$A = {(d, x_d) \mid d \in [K], x_d \in X_d}$
意思是总共有K个离散动作,选择第d个动作的同时,给它附上一个连续的参数向量x_d。注意,不同离散动作可能对应不同维度的参数。比如“抓取”需要“力度、角度”两个参数,而“滑动”只需要“距离”一个参数。所以参数维度可以是动作相关的。
这种结构在现实任务里极其常见。游戏AI里,英雄要选择放哪个技能(离散),然后又决定释放方向、力度(连续)。推荐系统里,策略要先决定推荐哪个商品品类(离散),再决定给多少折扣(连续)。资源调度里,要决定调度哪台机器(离散),再决定分配多少带宽或算力(连续)。可以说,凡是“先做决定、再调细节”的决策层级,本质上都是参数化动作空间。
1.2 混合动作空间的真实场景:机械臂、资源调度、游戏AI、自动驾驶
再看看真实场景,这类任务真的不是少数。
机械臂控制可能是最典型的。机械臂做一个抓取任务,动作空间是:移动到目标附近(连续:关节角度),选择抓取或放置(离散),决定抓取力度(连续)。我见过很多团队一开始用DDPG,把离散动作也编码成连续值去逼近,效果非常差。因为离散动作本质上是不可微的,你让一个连续策略去逼近一个“跳变”的决策边界,它很容易在一个动作边界来回震荡。
资源调度是最近两三年工业界特别关注的方向。比如云数据中心的服务器管理,控制器要决定当前应该扩容还是缩容(离散决策),然后决定具体要调整多少个容器实例、调整多少CPU配额(连续参数)。传统的启发式方法完全没法处理这种组合爆炸式的策略空间,而纯强化学习算法又很难同时处理好决策和参数两部分的协同。
游戏AI里更不用说了。MOBA游戏里,智能体要选择攻击哪个目标(离散),放哪个技能(离散),然后决定技能释放方向、距离(连续)。这类任务如果只用离散动作建模,需要把连续方向离散化成几十个挡位,动作维度瞬间爆炸,训练难度成千上万倍上升。
自动驾驶虽然说到底是连续控制为主,但在决策层面同样面临混合建模的问题:变道、超车、靠边停车是离散决策,而方向转角、加速度、刹车力度是连续参数。很多论文里把决策层和控制层分开处理,但在端到端学习的框架里,混合动作空间的问题始终绕不过去。
1.3 你踩过的坑:DQN、DDPG、PPO都试过了,为什么效果不好
说了这么多场景,到底为什么标准算法搞不定?我挨个说:
DQN面对连续参数,最常见的做法是把连续参数离散化。比如力度分成10档,角度分成36档,组合起来就是360个动作。这种做法有两个致命问题:一是维度爆炸,参数每增加一个维度就指数级膨胀;二是精度丢失,离散化粒度不够,策略永远做不出精细控制。你可以想象一个机械臂只有36个角度、10个力度可选,抓杯子的成功率能有多低。
DDPG面对离散动作,只能把离散动作也当成连续值输出,再取整或者取argmax。这相当于把一个本来就是分类的问题硬生生当成回归问题处理。最直接的后果是策略在动作边界上不稳定,训练出的策略经常出现“明明要抓取,却输出一个模棱两可的中间值”的诡异行为。
PPO这类方法相对灵活一些,可以设计一个混合策略网络,一部分输出离散动作的概率分布,一部分输出连续参数的高斯分布。但这样做的最大问题是:离散动作和连续参数的梯度更新是割裂的,两者的协调完全靠reward信号隐式引导,训练效率很低。而且连续参数摊到每个离散动作上时,如果某些动作很少被采样到,对应参数的训练就会被饿死。
我自己最开始的尝试是走PPO路线,结果发现一个残酷的事实:在混合动作空间里,随机策略的探索效率低得惊人。因为策略不仅要探索“用哪个动作”,还要探索“这个动作对应的参数是什么”,探索空间是两者的笛卡尔积,大多数随机探索都在无效区域里浪费掉了。
所以混合动作空间真正需要的,是一个能同时处理离散选择和连续参数,并且能够让两者共享决策信息、协同更新的算法框架。这便是P-DQN和MPDQN登场的原因。
2. P-DQN算法机制解析:把连续参数“挂”在离散动作上
P-DQN的核心思想并不复杂,但对当时(2019年)的强化学习社区来说,确实是一个很有创意的角度。它的思路可以总结为一句话:离散动作的Q值,被建模成连续参数的函数。
2.1 P-DQN的核心思想:动作由“离散选项+连续参数”构成
我们先把目标梳理清楚。假设有K个离散动作可选,每个离散动作d都对应一个连续参数向量x_d。智能体的完整动作就是选一个离散动作d,再加上相应的参数x_d。
如果沿用DDPG的思路,我们可以构造一个策略网络,记作x = μ(s, d),输入状态s和离散动作d,输出连续参数。这个过程可以理解为:给定当前局面,针对每一个离散动作,智能体都在心里想好了“如果选这个动作,那么参数应该怎么设”。然后我们再用一个Q网络Q(s, d, x_d),在选定了d和x_d之后评估这个完整动作的价值。
决策的时候,智能体只需要对每个离散动作d计算:
$Q(s, d, \mu(s, d))$
然后选值最大的那个d作为最终决策,并使用对应的μ(s, d)作为参数。这里其实是做了一个关键转化:原来需要同时优化离散和连续,现在变成了先由Actor网络对每个离散动作给出参数,再用Q网络对这些“动作-参数对”打分,选出最优秀的那个离散动作。
这个过程很像选房子:中介(Actor)先给每个小区(离散动作)提供一套具体房型方案(连续参数),然后评委(Q网络)给每套方案打分,最后选分数最高的小区。这里的重点在于,每个小区都要考虑一遍,而不是直接把所有小区混在一起选。
2.2 网络结构与更新流程:评估网络、参数生成网络
P-DQN的网络结构可以分为两大块:
第一块是Q网络,也就是评估网络。它不再像DQN那样接收离散动作的one-hot编码,而是接收状态s和连续参数x_d,输出该状态下采取动作d(并配上参数x_d)的Q值。注意,这里有K个离散动作,意味着Q网络最好有K个输出头,每个头对应一个离散动作的Q值,输入则是相应动作的参数向量。
第二块是Actor网络,也就是参数生成网络。它的任务是根据状态s生成所有离散动作对应的参数μ(s, d)。注意,这里有一个非常容易忽略的细节:对于每一个离散动作d,Actor都要单独生成一套参数,哪怕最终决策只会选一个动作。这确实增加了计算量,但是保证了Q网络在评估每个离散动作时都有对应的参数可用。
网络结构实现上,我建议把Q网络设计成这种结构:共享一个状态编码层,然后每个离散动作单独分流出一个多头感知机,输入该动作的参数向量,输出对应的Q值。Actor网络则相对简单:输入状态s,输出一个拼接向量,d维动作参数全部拼在一起输出,使用时再按动作维度切分。
训练流程可以简单分成两步交替进行:
第一步,更新Q网络。采样一个转移样本(s, a_d, x_d, r, s'),计算目标值。这里的难点在于s'状态下的Q值如何估计,因为我们需要知道s'下选择哪个离散动作、参数是什么,才能得到最大的Q值。P-DQN的方案是:用当前的Actor网络μ(s', d)为每个离散动作生成参数,然后计算max_d Q'(s', d, μ(s', d)),其中Q'是目标网络。然后套用标准的TD目标公式。
第二步,更新Actor网络。因为最终决策是选Q值最大的离散动作,那么Actor的优化目标就是让“对应那个最优离散动作的参数”产生的Q值尽可能高。但问题在于,当前最优离散动作会随着训练不断变化,所以P-DQN的实现里通常对所有离散动作都做梯度上升,目标是提升Q(s, d, μ(s, d))的平均值或总和。这样,无论最后选哪个离散动作,其对应的参数都已经经过了优化。
2.3 P-DQN的数学原理:损失函数与梯度更新推导
P-DQN的数学推导其实并不复杂,但要搞明白里面的近似假设。我们定义策略π的形式是“先选离散动作,再输出连续参数”,于是整体目标可以写作:
$J = \mathbb{E}{s}\left[\max{d} Q(s, d, \mu(s, d))\right]$
这里我们隐含地使用了贪心策略,即每次选择Q值最大的d。
Q网络的损失函数就是一个标准的时间差分误差。给定一个transition(s, d, x_d, r, s'),目标值可以写成:
$y = r + \gamma \max_{d'} Q_{\theta^-}\left(s', d', \mu_{\phi^-}(s', d')\right)$
其中θ^-和φ^-分别是Q网络和Actor网络的目标网络参数。然后Q网络的损失就是均方误差:
$L_Q = \mathbb{E}\left[\left(y - Q_\theta(s, d, x_d)\right)^2\right]$
这个式子里有一个微妙的问题:理论上目标值应该对所有可能的d'和x_d'求max,但P-DQN用Actor网络μ给每个d'提供一个x_d',然后只在离散维度d'上求max。这意味着连续参数部分不是全局最优的,只是“Actor当前给出的最优点”。这是P-DQN的一个关键近似,也是后续几个改进版本的切入点。
接下来是Actor的更新。我们要让Q(s, d, μ(s, d))最大化。用确定性策略梯度的思路,对Actor参数φ求梯度:
$\nabla_\phi J \approx \mathbb{E}\left[\sum_{d} \nabla_\phi Q_\theta\left(s, d, \mu_\phi(s, d)\right)\right]$
展开后是:
$\nabla_\phi Q_\theta = \nabla_{x_d} Q_\theta(s, d, x)\big|{x=\mu\phi(s, d)} \cdot \nabla_\phi \mu_\phi(s, d)$
这就是标准的链式法则。需要注意的是,P-DQN在更新Actor时,是对所有K个离散动作的Q值求和(或取均值)做梯度上升,而不仅仅是当前选中的那个动作。这样做的好处是让所有离散动作的参数生成能力都得到训练,坏处是可能会让Actor把精力分散到那些“本来就不该选”的动作上。这个设计在有些场景下是稳定训练的功臣,在另一些场景下则成了拖后腿的元素,后面讲MPDQN时会再展开。
2.4 关于P-DQN的收敛性思考与局限
P-DQN的收敛性并不能直接由DQN的收敛性保证,因为这里的目标值计算依赖一个同时更新的Actor网络。也就是说,Q网络的训练目标本身会随着Actor的更新而移动,这让训练过程更容易出现震荡。
我自己的实验经验是,P-DQN在动作空间维度较低(比如离散动作不超过10个,参数维度不超过3个)的任务里表现还行,但一旦动作组合变复杂,训练就容易出现两个典型问题:一是Q值高估,导致Actor被误导到一套“看起来Q值很高、实际收益很低”的参数上;二是参数更新时梯度方向忽东忽西,因为Q网络对不同离散动作的梯度方向可能完全相反,Actor在更新参数时相当于在对抗一个“分裂”的优化目标。
此外,P-DQN在高维状态空间下还有样本效率的问题。因为Actor要为每个离散动作都生成一套参数,如果离散动作数量很大,这个输出维度会相当可观,训练起来会非常吃力。这也是MPDQN出现的重要动机。
3. MPDQN关键改进:参数共享与混合梯度传播
MPDQN的全称是Multi-Pass Q-Networks,我更喜欢把它理解为“对P-DQN的‘缝合手术’”。它的出发点非常具体:P-DQN里那个Actor要同时优化所有离散动作对应的参数,这导致了梯度冲突和训练不稳定。MPDQN正是从这个痛点下手。
3.1 P-DQN的一个明显毛病:Full-α联合更新带来的不稳定
在P-DQN中,Actor网络的目标函数是所有离散动作的Q值之和,这个设计被称为Full-α联合更新。你可以想象一下:演员同时排练了十个剧本,每个剧本的导演都对表演提出了完全不同的要求,一会儿让他演英雄,一会儿让他演反派,一会儿让他演喜剧人物。最后演员被逼成了一个各方面都平庸、哪方面都不突出的“折中形态”。
在具体任务里,这意味着什么?假设机械臂任务里有“抓取”“滑动”“按压”三个离散动作,它们的参数空间完全不同。抓取需要学习力度和角度,滑动需要学习距离,按压需要学力的大小。如果用P-DQN的Full-α更新,Actor网络需要在同一次梯度更新里同时提升这三个动作对应的参数质量。但问题是,这三个动作对应的Q值梯度大概率是不同甚至冲突的,于是Actor参数更新的方向会被拉成一个“四不像”,最终结果是每个动作的参数都学不精。
MPDQN说白了就是把“演员被不同导演拉扯”的问题解决掉。它的核心思路是:不再让Actor通过一个输出头去生成所有动作的所有参数,而是让Q网络自己掌握“参数如何影响价值”的信息,再通过一种混合梯度的方式,把连续参数的学习从离散动作的Q值评估中解耦出来。
3.2 MPDQN的核心机制:把Q值当作参数的函数来优化
MPDQN的关键改进在于对Q值的理解。在P-DQN里,我们先把参数x_d给Actor网络生成,然后塞进Q网络算Q值。这里x_d只是Q网络的一个“外部输入”,Q网络对x_d的依赖完全来自输入层。
MPDQN换了一个视角:它把Q网络重新设计成Q(s, d, x_d)的形式,但同时在网络结构上实现了“Q值对参数x_d的显式依赖”。具体来说,MPDQN在Q网络内部会先将状态s编码成一个特征向量,然后对不同离散动作的参数分别计算价值,最后再聚合起来。这里的“多路传递”(Multi-Pass)指的正是:在Q网络内部,同一个状态特征会分别“通过”不同离散动作各自的参数分支,每个分支输出该离散动作的Q值。
这样做的好处是:参数x_d不再是外部单纯塞进来的数值,而是真正进入了Q值计算的“内部通道”。这就像以前点外卖的时候,你只把订单丢给店里,完全不管厨房怎么处理;现在你能看到厨房的内部流程,并且可以精确指导“这份订单里加多少辣、多少盐”更合适。Q网络学会了参数对价值的边际影响,于是梯度可以更准确地回传到参数生成网络。
实现上的一个重点是:MPDQN让Actor网络的更新不仅仅依赖Q值对参数的梯度∂Q/∂x_d,而是综合考虑了Q网络内部的中间特征。这就等于在反向传播时,让参数的梯度和状态-动作价值评估之间建立了更直接的高速通道,减少了过去那种“梯度传到一半就消散或者扭曲”的情况。
3.3 混合梯度传播的具体操作与实现细节
MPDQN的具体实现可以选择两种方式,我这里介绍最直观的一种:在Q网络中为每个离散动作分支都计算Q(s, d, x_d),但Actor的输出μ_φ(s)只输出“当前状态s下所有动作的参数向量”,然后对每个离散动作分支都计算Q值。
训练时关键的区别在于Actor的梯度计算。MPDQN中Actor损失函数虽然是类似的形式:
$L_{actor} = -\sum_{d} Q_\theta\left(s, d, \mu_\phi(s, d)\right)$
但它对总Q值的评估是直接对Q网络内部做了一次关于x_d的“全微分”,同时考量了Q网络内部的状态特征和动作参数的耦合方式。这带来的结果是,理论推导中的梯度计算会更稳定,因为参数更新不再只是通过Q网络的“输入层”来传播,而是通过Q网络的“中间层特征”来传播,梯度路径明显短了不少。
另外还有一个工程实现上的小细节:MPDQN在更新Q网络时,目标值计算用了和P-DQN类似的方式,即用目标Actor网络给s'下的每个离散动作生成目标参数,再计算max_d Q_target(s', d, μ_target(s', d))。如果Q网络没有参数化的内部结构,这种目标值的计算很容易出现高估偏差。而MPDQN由于Q值对参数的建模更精确,高估问题会得到一定的缓解。
3.4 MPDQN与其他变体的横向对比:PDQN、Hybrid-PPO、Parameterised Action DDPG
MPDQN不是唯一解决混合动作空间的方案,但它在很多实验中的确表现得更加稳定和高效。我把几个常见方案放在一起做个对比:
| 方法 | 动作建模方式 | 更新机制 | 主要优势 | 主要短板 |
|---|---|---|---|---|
| P-DQN | 离散选择+连续参数生成 | Q网络TD更新 + Actor确定性策略梯度 | 结构清晰,容易实现 | Full-α更新导致梯度冲突,训练不稳定 |
| MPDQN | 离散选择+参数化Q值 | Q网络内部混合梯度传播 | 参数学习更稳定,样本效率更高 | 网络结构稍复杂,需要更多工程细节调优 |
| Hybird-PPO | 离散策略+连续高斯策略混合 | 一个策略网络输出混合分布,用PPO更新 | 理论上更通用,适合随机策略 | 探索效率低,训练慢,实现复杂度高 |
| Parameterised Action DDPG | 类似P-DQN,但用确定性策略梯度 | 类似DDPG,对连续参数和离散决策联合更新 | 更接近DDPG的直觉 | 容易忽略离散动作之间的独立差异,导致更新不精确 |
表格里每一项都值得展开说。Hybrid-PPO这类方法最大的问题我在前面提过:探索空间太大了。它把离散选择和连续参数当成两个独立分布的联合采样,随机初始化的策略几乎就是在随机游走,reward信号非常稀疏,训练效率感人。Parameterised Action DDPG的问题在于,它把离散动作也当成可微的连续变量来处理,会模糊离散动作的本质特征,容易出现我在第一节里说到的“中间值困境”。
MPDQN为什么在对比里胜出?我觉得核心就是它同时尊重了两个事实:第一,离散动作的选择本质上是一个不可微的argmax问题,所以选择那一步用Q值评估;第二,连续参数对价值的影响是可微的,所以参数优化走梯度路径。两种机制各司其职,用网络结构把各自的优势发挥出来,这是它相比其他方法最聪明的地方。
4. 动手实现:从搭建网络到跑通一个混合动作环境
这一节我分享一套可以直接上手的实现思路。我不打算贴完整代码,因为代码仓库里已经有很多成熟实现,我更想讲清楚实现时容易踩的坑,以及什么样的设计决策会让你的训练效率天差地别。
4.1 网络结构设计建议:离散头、连续头和共享编码层
先说Q网络。我的建议是采用“共享编码层+离散动作独立分支”的结构。具体来说:状态s先经过一个两层的MLP编码成一个特征向量h。然后对每个离散动作d,有一个独立的小型MLP,接收的特征是[h, x_d]的拼接,输出该动作的Q值。这样的好处是:不同离散动作的参数维度可以不同,每个分支可以独立处理自己维度的输入;同时共享编码层让状态特征在不同动作之间复用,节省参数数量。
这里有一个不容易注意到的坑:如果不同离散动作的参数维度差异很大,比如动作1的参数是3维,动作2的参数是10维,那么拼接[h, x_d]之后,各分支的输入维度不一样。这在PyTorch里实现很简单,每个分支定义不同的Linear层就行。但要注意做好维度管理和参数初始化的对齐,否则训练初期某些分支的Q值会特别大,把共享编码层的梯度带偏。
Actor网络的输出是一个向量,它将所有离散动作的参数拼接在一起输出。假设动作d的参数维度为dim_d,那么Actor输出维度就是Σ dim_d。输出的每个分段就是对应动作的参数,训练时直接切片丢给Q网络对应分支即可。激活函数方面,连续动作参数如果有边界,一定要在输出层加上tanh再缩放到边界范围内。这一点极其重要,因为强化学习里的动作参数通常在环境交互时要求有物理意义(比如角度只能在-180°到180°之间),如果Actor输出越界,环境会直接给一个奇怪的reward甚至崩溃。
4.2 训练循环的伪代码与关键细节
我写一段核心训练循环的伪代码,这个结构基本可以复用:
# 初始化Q网络 Q_theta、Actor网络 mu_phi,以及对应的target网络 # 初始化经验回放池 replay_buffer for episode in range(max_episodes): state = env.reset() episode_reward = 0 while not done: # 1. 用Actor给所有离散动作生成参数 params_all = mu_phi(state) # shape: [sum(dim_d)] params_by_action = split(params_all) # 按动作切分 # 2. 计算每个离散动作的Q值,选最优离散动作 q_values = [] for d in range(K): q_values.append(Q_theta(state, d, params_by_action[d])) discrete_action = argmax(q_values) # 3. 执行动作,得到转移样本 action = (discrete_action, params_by_action[discrete_action]) next_state, reward, done, info = env.step(action) # 4. 存储transition replay_buffer.add((state, discrete_action, params_by_action[discrete_action], reward, next_state, done)) # 5. 从回放池采样一个batch,更新网络 batch = replay_buffer.sample(batch_size) update_networks(batch) state = next_state episode_reward += reward def update_networks(batch): state, action_d, action_x, reward, next_state, done = batch # 用target Actor给next_state生成参数 next_params_all = mu_phi_target(next_state) next_params_by_action = split(next_params_all) # 计算target Q值 target_q_values = [] for d in range(K): target_q_values.append(Q_theta_target(next_state, d, next_params_by_action[d])) max_target_q = max(target_q_values) # 对离散维度取max y = reward + gamma * (1 - done) * max_target_q # 更新Q网络,注意只更新当前选择的离散动作对应的分支 q_pred = Q_theta(state, action_d, action_x) loss_q = MSELoss(q_pred, y) optimizer_q.zero_grad() loss_q.backward() optimizer_q.step() # 更新Actor网络 # 先让Actor生成当前state下的所有参数 params_all = mu_phi(state) params_by_action = split(params_all) # Actor的损失是所有离散动作Q值的负均值 q_vals = [] for d in range(K): q_vals.append(Q_theta(state, d, params_by_action[d])) loss_actor = -mean(q_vals) optimizer_actor.zero_grad() loss_actor.backward() # 注意:只更新Actor参数,不更新Q网络参数 # 关键:需要把Q网络的梯度冻结 loss_actor.backward(retain_graph=True) # 或者使用detach等技巧 optimizer_actor.step()这段伪代码里有几个容易踩坑的细节,我特别提醒:
第一,Q网络更新的时候,只更新当前采样到的离散动作对应的分支。其他分支的Q值预测不做更新。原因是你只对实际执行的那个动作有真实的reward反馈,没有执行的动作没有ground truth,强行更新反而会把Q值搞乱。
第二,Actor更新时,需要对Q网络的所有分支都算一次梯度。这里有个技术问题:如果Q网络的不同分支共享了编码层,那么一次backward就会同时更新Q网络和Actor网络,这会导致训练不稳定。标准做法是在Actor的backward之前,把Q网络的参数暂时固定,例如在PyTorch里用with torch.no_grad():包住Q网络前向传播,或者调用Q_theta.requires_grad_(False)。
第三,目标网络用软更新比较好,也就是target_params = tau * current_params + (1 - tau) * target_params,tau取0.005左右。硬更新(定期复制)在P-DQN这类算法里很容易因为Q值突变导致训练崩溃,我吃过一次大亏。
4.3 超参数选择与调参经验
P-DQN/MPDQN的超参数选择和普通DQN、DDPG有一些不太一样的地方,我列一张表:
| 超参数 | 建议取值 | 注意事项 |
|---|---|---|
| 学习率(Q网络) | 1e-3 ~ 3e-4 | 太大会导致Q值高估,太小则收敛太慢 |
| 学习率(Actor网络) | 1e-4 ~ 1e-3 | Actor学习率建议比Q网络低,因为Actor的梯度来自Q网络的间接传播 |
| 回放池大小 | 100k ~ 1M | 混合动作空间的探索样本质量参差,大回放池有助于稳定 |
| batch size | 128 ~ 256 | 采样量太小的话,某些离散动作可能一整个batch都没被采到 |
| 目标网络更新系数tau | 0.005 | 软更新比硬更新稳定得多 |
| 探索噪声 | 参数空间加高斯噪声 | 不要只在Q值选择上加ε-greedy,否则连续参数部分探索不足 |
| reward normalization | 建议做 | 不同离散动作的reward尺度差异很大,不做归一化会让梯度偏置 |
关于探索噪声,我要多讲一句。在混合动作空间里,探索需要同时考虑离散选择和连续参数两部分。我试过最有效的方案是在Actor输出的参数上叠加一个均值为零的高斯噪声,噪声方差随训练进度逐渐衰减。同时,在离散动作选择上,保持一个较小的ε-greedy探索概率(比如0.1到0.3),但核心探索动力应该放在连续参数上。原因是离散选择一旦确定,对结果的影响是决定性的,频繁乱试反而会让Q网络学到错误的价值信号。
4.4 简单实验验证与结果观察
如果你第一次跑通P-DQN或MPDQN,我建议先用一个简单环境验证:比如带参数版本的多臂老虎机,或者OpenAI Gym里稍作修改的CartPole——把动作改成“向左推/向右推”两个离散动作,参数是推力大小。这种环境收敛快、可解释性强,适合验证算法实现的正确性。
观察训练曲线时,我建议重点看两个指标:episode reward曲线和Q值曲线。如果Q值在训练早期迅速飙升,但episode reward没有同步上升,说明出现了Q值高估。这时候你要先检查目标网络更新是否太频繁,再检查Actor更新时是否把Q网络也给更新了。如果Q值曲线很正常,但episode reward长期没有起色,问题大概率出在探索参数上——试试加大参数噪声方差,或者增加ε-greedy概率。
我自己的经验里,遇到最多的问题是Actor更新时不小心把Q网络参数也更新了,导致整个训练像“左脚踩右脚”一样螺旋升天。这个bug非常隐蔽,因为loss值看起来在正常下降,但策略效果越来越差。排查方法很简单:训练过程中打印Q网络第一层权重的范数,如果一直在变化,说明你的梯度隔离没有做好。
5. 应用场景与行业落地:从仿真到现实
算法不能只活在论文里。这一节我聊聊混合动作空间在几个真实场景中的应用趋势,以及MDPQN这类算法在落地时需要留意的现实工程问题。
5.1 机械臂控制:抓取规划的“动作-参数”解耦
机械臂抓取可能是混合动作空间最直接的应用场景。一个机械臂的完整控制策略包括:目标定位(连续位置)、轨迹规划(连续路径)、末端执行器动作(离散:抓取、释放、切换工具)、执行力度控制(连续)。
我以前和一个做机器人分拣的朋友聊过,他们团队的早期方案是把“抓取”和“释放”建模成两个离散动作,用DQN来学,力度则固定不变。效果还行,但一旦遇到易碎品或者不同重量的物体,固定力度完全不够用。后来他们换成混合动作空间建模,力度作为连续参数,“抓取”“释放”作为离散动作,训练效率和抓取成功率都有了大幅度提升。
这里面有一个非常关键的工程问题:仿真环境与真实环境的差距。在仿真环境里,你可以在几十个episode内让策略探索各种极端参数,然后收到反馈。但在真实机械臂上,一次错误的抓取力度可能直接损坏工件甚至机械臂。所以落地的时候,一定要先在高保真仿真环境里把策略训到足够鲁棒,再迁移到真实世界,并且迁移时对Actor输出做物理边界约束。
5.2 推荐系统与资源调度:离散决策+连续分配
推荐系统里有一个很经典的问题:给用户推荐什么内容(离散选择),以及以什么频率、什么渠道发送(连续参数)。这个场景最近两年引起了不少团队的关注,尤其是推送时机和展示方式的优化,本质上是一个混合动作空间问题。
资源调度就更典型了。云平台上要决定给某个任务分配哪台服务器(离散选择),以及分配多少CPU、多少内存、多少带宽(连续参数)。这个场景的特点是状态空间巨大、动作组合复杂,而且环境是动态变化的。有意思的是,这类问题最近也出现了与运筹优化结合的尝试,用混合整数线性规划(MILP)做离线求解,再用强化学习做在线决策,混合动作空间正好是两者的中间层。
在这些场景落地时,最需要注意的是reward函数的设计。资源调度的reward通常包含多个目标:任务完成时间、资源利用率、能耗等。如果reward设计成简单的线性加权,策略很容易学到“钻空子”的行为。我建议在reward里加入约束惩罚项,比如超过资源上限时给予大的负惩罚,这比事后调整策略要高效得多。
5.3 游戏AI与自动驾驶:决策层与执行层分层协同
在游戏AI里,混合动作空间的建模方式尤其自然。像《星际争霸》《Dota 2》这类游戏,智能体既要选择使用哪个技能(离散),又要决定释放方向和距离(连续),同时还要决定移动目标(连续位置)。
2023年以来,业界在游戏AI方面出现了一个趋势:不再从头训练一个完整的大模型,而是用分层强化学习,高层策略输出离散意图,低层策略输出连续动作序列。混合动作空间在这种分层架构里正好充当了“上层决策和下层参数”的桥梁。P-DQN在这一层有天然的优势,因为它的离散动作选择本身就是基于Q值评估的,天然适合做“意图筛选”。
自动驾驶方向虽然暂时还不是混合动作空间的主战场,因为其控制层面更倾向于纯连续控制,但在决策层面已经有了混合建模的尝试:换道(离散)加上换道轨迹(连续参数),通过混合策略来处理交互决策。我个人的判断是,未来端到端自动驾驶如果要在决策层引入可解释性,混合动作空间会成为一个重要的建模选项。
6. 常见问题与避坑实录
最后这一节,我整理一下自己实际操作中遇到的典型问题,按症状排查,基本能覆盖大部分新手用户的困境。
6.1 训练不稳定的典型表现与对策
训练不稳定的表现很多,最常见的是reward曲线剧烈震荡、Q值发散、策略在训练后期完全退化。我遇到过的一个典型案例是:训练前期reward稳步上升,到了中期突然崩盘,一切回到原点。排查后发现,问题出在Actor的学习率太高,导致参数更新迈的步子太大,Q网络来不及“纠正”Actor的方向,两者互相拉扯,最后把策略拉进了坑里。
对策如下:
- 把Actor学习率降低到Q网络的五分之一甚至十分之一,给Q网络多一点“权威”;
- 目标网络软更新的tau调小一点,比如0.001,让目标值的更新更平滑;
- 对网络参数做梯度裁剪,clip范围控制在1.0以内,防止梯度爆炸。
6.2 探索策略设计:ε-greedy的变形
在混合动作空间里,简单的ε-greedy并不够用。因为即使你以一定概率随机选了离散动作,随机选择的参数也可能是完全不合理的。如果你在离散动作上做随机探索,比如有K个动作就有1/K的概率选到某个动作,但参数却是随机噪声——这个噪声样本对Q网络的训练几乎是有害的。
我的经验是采用分层探索策略:离散动作选择用ε-greedy(ε从0.3线性衰减到0.05),连续参数则在Actor输出的基础上叠加高斯噪声。高斯噪声的初始标准差设为参数范围的10%左右,然后逐渐衰减到2%左右。这个方法比单纯的ε-greedy或者单纯的参数噪声都更稳定。
6.3 辅助损失与奖励塑形的实战经验
在混合动作空间任务里,reward信号往往非常稀疏。比如机械臂抓取,只有成功抓取才有+1的reward,其他时刻都是0。稀疏reward会让Actor不知道“哪个阶段的参数是好的”,训练速度极慢。
我在实践里用过两种有效的做法:
第一种是奖励塑形。在稀疏reward之外,给每一步加上一个辅助奖励,比如“物体靠近目标的距离变化”。奖励塑形的时候要注意,不要喧宾夺主。如果辅助奖励的值域远大于主奖励,策略可能会去优化“靠近目标”而不是“抓取成功”。我通常会设置辅助奖励为“前进多少距离”的增量,并且把系数压到很小,比如0.1,让主奖励始终主导学习方向。
第二种是HER(Hindsight Experience Replay)式的目标重标注。对于机械臂这类任务,可以把“成功抓取”的状态修改为“虽然没有抓到目标物体,但成功抓到了另一个物体”,以此构造正样本。这个方法在处理混合动作空间时尤其有用,因为它让Q网络能在稀疏reward下也学到“动作-参数”与结果之间的关系。
6.4 常见问题速查表:报错、发散、维度不匹配
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 训练刚开始Q值就爆炸 | 学习率过高、Q值未做归一化 | 降低学习率,检查reward尺度 |
| reward长期没有提升 | 探索不足、reward太稀疏 | 增大参数噪声、加入reward shaping |
| 不同离散动作的Q值差距悬殊 | 采样不均衡,某些动作太少被执行 | 增加ε-greedy概率,或者对每个动作单独设置采样权重 |
| Actor输出越界导致环境崩溃 | 输出层激活函数使用不当 | 使用tanh激活函数,然后缩放到动作边界 |
| 动作参数维度不匹配 | 参数切分逻辑错误 | 打印每个动作分支的输入输出维度,逐个核对 |
| 训练中期Q值突然飙升 | 目标网络更新过快 | 调低tau,或者改用软更新 |
| 同一个任务跑多次结果差异极大 | 随机种子、网络初始化差异 | 固定随机种子,或者做多次重复实验取均值 |
这张表是我最想让大家收藏的,因为我发现80%的实现问题都能在这里找到对应答案。尤其是维度不匹配的问题,混合动作空间实现里非常容易出现,因为每个离散动作的参数维度不同,一旦在拼接、切分、索引的过程中差了一个维度,代码不会报错,但训练效果会莫名其妙地变差。遇到这种情况,我的排查方法是:在训练的最开始,手动构造一个假batch,走一遍完整的前向和反向传播,把每一步的张量形状打印出来,仔细核对。
另一个很容易被忽略的问题是:离散动作参数的索引映射。在P-DQN的实现里,Actor输出的拼接向量按动作顺序排列,Q网络的不同分支按同样的顺序接收。这个顺序一旦在代码里写错,比如切分时把动作0的参数切给了动作1,策略就会学到一套完全错乱的映射。由于网络本身具备拟合任意映射的能力,这种错误不会导致明显的报错,但会让训练收敛到奇奇怪怪的局部最优。我的建议是:在最开始就把动作索引映射封装成一个函数,加上单元测试,确保任何地方调用都不会错位。
再说一个我在多进程训练环境里踩过的坑。混合动作空间的任务通常需要多个环境并行采集数据,以提升采样效率。但如果不同进程的随机种子设置不当,可能导致某个离散动作在某个进程里永远不会被采样到,造成样本分布严重偏移。建议为每个进程设置独立的随机种子,并在采集数据时记录每个离散动作被执行的比例,如果某个动作的比例长期为0,需要主动干预采样策略。
写在最后,但都是硬货
P-DQN和MPDQN给我的最大感受是:它们解决的不是“能不能用强化学习”的问题,而是“如何让强化学习的动作建模更贴合真实世界”的问题。真实世界的决策从来不是纯离散或纯连续的,而是两者的精细嵌套。如果你的任务正好是这种结构,这两个算法值得你认真研究。
最后分享一个我踩过很多次坑之后总结出的调参小技巧:如果你在两个算法之间犹豫不决,可以先跑P-DQN做快速原型验证,因为它的实现更简单、调试更容易;等确认整个pipeline没有问题之后,再切换到MPDQN做精调。MPDQN在大多数场景下效果更好,但它的网络结构和训练流程多了一些细节,直接上手调试,容易分不清是算法的问题还是代码的bug。先简单后复杂,一步一步迁移,你会省下大量的调试时间。