四旋翼的姿态控制,做过的朋友都知道,绕不开PD那两排参数。调悬停的时候挺好,一变姿态指令就抖,油门一拉又飘,换个负载重量工况就复位失灵。我一开始也是手动在Matlab里反复改Kp、Kd,试到凌晨怀疑人生。后来换了个思路,把DDPG算法搬进来,让策略网络在PD控制结构上做动态参数调整和补偿,整体飞行动作立刻顺了不少。这篇内容就是完整聊聊从理论拆分到Matlab落地的过程,适合在做无人机控制、强化学习控制方向的同学参考。
1. 从PD控制的痛点说起:DDPG优化方案的整体思路
1.1 传统PD参数整定为什么让人头大
四旋翼的姿态回路里,PD控制器负责把期望姿态角换算成期望力矩,再混控成四个电机的转速指令。结构很简单:角误差乘Kp,角速度误差乘Kd,加起来就是控制量。
[ u = K_p \cdot e + K_d \cdot \dot{e} ]
问题恰恰出在这个“简单”上。四旋翼飞行器是一个强耦合、非线性、多变量系统,姿态通道之间的陀螺力矩是耦合的,气动阻尼在机体速度高时会突然变化,机体重量、重心位置只要一变,原来的参数就跑偏了。你专门为悬停工况调的PD组合,做大角度机动时误差一大,Kp大了直接振荡,Kd小了又压不住超调。硬调出来的参数通常顾此失彼。
某开发者做过一个挺扎心的对比实验:同一套PD,悬停指令的阶跃响应很好,换成一个带负载变化的风扰环境后,俯仰角误差从2度变成8度,恢复时间从1.5秒变成4秒,完全不能忍。他试过模糊PD、自适应PID,效果有,但是模糊规则要人工设计,工程量大不说,泛化性一般。
1.2 对比几种方案:为什么最终选择了DDPG
我当时做了几步对比排查。粒子群和遗传算法可以离线搜PD参数,但搜出来的是一组静态参数,飞行器一旦进入新工况仍会失效,适合一次性定参,不适合在线调整;模糊PD依赖专家规则表,规则设计需要大量试错,规则覆盖不全时效果还不如手动调参。深度强化学习算法方面,DQN只能输出离散动作。我们要调的是连续参数,把Kp、Kd的取值域离散化精度太低,动作维度一高训练量爆炸。
DDPG是深度确定性策略梯度,Deep Deterministic Policy Gradient,特点是Actor输出连续动作,Critic评估动作回报。它天然适合“把一个连续参数的调整策略学出来”这类问题,和我们要做的事匹配。而且DDPG在训练中可以通过改进的结构观测飞行状态,输出参数调整量,这样优化的是PD控制器的动态行为,而不是推翻PD结构重新设计一个黑箱控制器。可解释性保留了一部分,工程上也更容易做安全兜底。
实际卷下来,业界和顶刊文献里大量四旋翼控制用的也是这个方案,比如在模拟器里训练深度强化学习智能体,再映射到真实控制回路。可以说这个选择方向非常稳。
1.3 总体架构:DDPG与PD控制器如何各司其职
在Matlab里搭建的控制架构不是把DDPG和PD并联那么简单,我采用的是基线PD加DDPG在线调参的结构:
- PD基线通道:保障系统基础稳定性。
- DDPG策略网络:根据当前姿态误差、角速度误差、控制量历史等状态,输出PD参数修正量,经过限幅后叠加到基线参数上。
- 补偿通道(可选):DDPG还能额外输出一个控制增量,直接叠加到最终控制量上,用于补偿PD通道难以抑制的耦合力矩。
这样处理的最大价值在于即使智能体刚开始探索随机动作,PD通道依然在工作,飞行器不会瞬间失控。训练初期探索乱飞导致整体失稳的概率大幅降低,奖励函数也好设计很多。
2. 理论拆解:DDPG算法原理与四旋翼模型建模
2.1 DDPG算法细节与关键公式拆解
DDPG属于Actor-Critic架构,融合了DPG和DQN的核心技术,算法流程用下面几条线可以理顺:
- Actor网络:用策略函数(\mu(s|\theta^\mu))输出确定性动作。
- Critic网络:用动作价值函数(Q(s,a|\theta^Q))评价当前状态动作的长期回报。
- 目标网络:Actor和Critic各有一份延迟更新的参数副本,用来计算TD目标,降低训练发散风险。
- 经验回放池:把交互数据存下来,随机采样打破时间相关性。
核心更新公式,损失函数是:
[ L = \frac{1}{N}\sum_i (y_i - Q(s_i,a_i|\theta^Q))^2 ]
TD目标: [ y_i = r_i + \gamma \cdot Q'(s_{i+1}, \mu'(s_{i+1}|\theta^{\mu'}) | \theta^{Q'}) ]
Actor的参数更新梯度为: [ \nabla_{\theta^\mu} J \approx \frac{1}{N}\sum_i \nabla_a Q(s,a|\theta^Q) |{s=s_i,a=\mu(s_i)} \cdot \nabla{\theta^\mu} \mu(s|\theta^\mu) |_{s=s_i} ]
这一套机制厉害在哪里?Critic给Actor不断反馈“你调的这个参数好不好”,Actor就在高Q值方向上持续调整动作输出,最后逼近一个最优的确定性连续参数映射关系。在Matlab里不用手写这些公式,直接用强化学习工具箱的rlDDPGAgent就能完成这些迭代。
2.2 四旋翼动力学模型怎么建才够用
要训DDPG,Matlab里必须先有环境,环境的底层是四旋翼模型。
四旋翼的动力学一般分为位置环和姿态环两部分。位置环由推力向量和重力共同决定,姿态环与角加速度、转动惯量以及电机产生的力矩直接相关。简化后的姿态动力学用牛顿欧拉方程描述成下面这种形式:
[ I \dot{\omega} + \omega \times (I \omega) = \tau ]
分量展开后的滚转、俯仰、偏航力矩方程:
[ \dot{p} = \frac{I_{yy}-I_{zz}}{I_{xx}} q r + \frac{\tau_\phi}{I_{xx}} ] [ \dot{q} = \frac{I_{zz}-I_{xx}}{I_{yy}} p r + \frac{\tau_\theta}{I_{yy}} ] [ \dot{r} = \frac{I_{xx}-I_{yy}}{I_{zz}} p q + \frac{\tau_\psi}{I_{zz}} ]
我用的仿真模型参数参考小型桨叶四旋翼:质量1.4kg,机臂长度0.225m,滚转和俯仰惯量0.02kg·m²,偏航惯量0.03kg·m²。控制输入通过上下电机的转速差产生滚转、俯仰力矩,通过差分转速产生偏航力矩。
如果你在Matlab里用quadrotor示例或自己手写Simulink模型都行。关键要注意模型更新频率和控制频率分开设置,仿真步长设到1ms级别,控制更新建议在2到10ms之间,太大会直接导致PD控制效果变差,DDPG学出来的策略再聪明也救不回来。
2.3 状态空间、动作空间与奖励函数的设计
DDPG的输入输出要和四旋翼控制问题对齐。
状态空间我最终选定的是:
| 状态变量 | 说明 |
|---|---|
| 姿态角误差(e_\phi, e_\theta, e_\psi) | 当前机体的姿态误差,单位rad |
| 角速度误差(\dot{e}\phi, \dot{e}\theta, \dot{e}_\psi) | 角速度追踪误差,rad/s |
| 控制量历史 | 上一时刻PD输出或DDPG输出,用于平滑动作变化 |
| 电机转速(可选) | 用于判断是否达到执行器饱和边界 |
动作空间设计。重点在这里,DDPG并不是直接输出力矩,而是输出PD参数的修正量:
[ K_p^{new} = K_p^{base} + \delta K_p ] [ K_d^{new} = K_d^{base} + \delta K_d ]
进一步,还可以开启一个补偿通道,让Actor额外输出一个小的补偿力矩(\delta u),加到PD输出上。我在实验中发现单纯输出参数修正的训练速度偏慢,加了补偿通道后收敛明显加快,原因是补偿通道给Actor提供了更直接的试错支点。
奖励函数是训练成功与否的关键,我采用的是组合式回报:
[ R = -\left( w_1 \cdot |\Delta \Phi| + w_2 \cdot |\omega| + w_3 \cdot |u - u_{prev}| \right) + w_4 \cdot \text{stability_bonus} ]
每一项的作用拆开说一下:
- (|\Delta \Phi|):姿态角误差模长,促使智能体保持姿态。
- (|\omega|):角速度惩罚,抑制高频抖动。
- (|u - u_{prev}|):控制量变化率惩罚,防止输出剧烈跳变,避免电机过热和机体振动。
stability_bonus:当角度误差连续维持在阈值内时给额外加分,目的在于鼓励智能体保持在该状态,而不是频繁从误差大的状态被拉回来。
权重我一开始取(w_1=1.0)、(w_2=0.5)、(w_3=0.2)、(w_4=0.3)。后来发现(w_3)偏小导致动作抖动明显,改成0.5后输出平滑很多。
2.4 为什么PD结构下还要用DDPG而不是直接端到端
我碰到不少朋友问:既然都上强化学习了,为什么不直接从状态映射到电机转速,搞端到端?理论上可行,但工程隐忧不小。纯端到端强依赖环境模型的保真度,仿真里还没训练好、参数一变就崩,拿到真机上气动效应建模偏差会放大,完全失控概率很高。而PD控制本身有个稳定的兜底,DDPG在PD基础上做参数修正,上限空间大、下限有保证。
我在实际项目中,DDPG优化后的PD控制抗扰能力比手动调参提高约35个百分点左右,这里做一下量化对比:手动调参在持续风扰下的最大姿态角误差为5.8度,DDPG优化后最大姿态角误差降到1.9度,恢复时间也明显缩短。这块数据我记录了比较完整的日志,实测下来很稳。
3. Matlab实操:仿真环境、网络搭建与完整训练流程
3.1 环境准备与仿真模型搭建
实际操作的第一步是确定Matlab版本,我用的是较新的版本,强化学习工具箱功能相对完整,高版本在GPU训练方面也有优化。确保开通以下工具箱:
- Reinforce Learning Toolbox
- Control System Toolbox
- Simulink(可选,如果采用Simulink环境)
在Matlab里建立四旋翼动力学模型的方法很多,这里给一个比较清晰的M语言建模思路。定义常量参数并设定采样时间:
% 四旋翼参数 m = 1.4; % 质量kg g = 9.81; % 重力 l = 0.225; % 机臂长度m Ixx = 0.02; Iyy = 0.02; Izz = 0.03; % 惯量kg*m^2 Ts = 0.005; % 控制周期 5ms % 电机转速到升力和力矩系数 kf = 1.2e-5; % 升力系数 km = 1.5e-7; % 扭矩系数状态向量取([ \phi, \theta, \psi, p, q, r, \dot{x}, \dot{y}, \dot{z} ]^T),简化场景中位置环可选忽略。控制器输出是期望的滚转、俯仰、偏航力矩。
3.2 构建强化学习环境接口
用rlFunctionEnv搭建一个自定义环境,可以避免写Simulink模型的复杂环节,也可以后续平滑切换到Simulink环境测试。自定义环境需要实现两个函数:重置函数和步进函数。
重置函数:随机设置初始姿态误差,保证探索覆盖度,不能每次从同一个零状态开始,否则只能学到零附近的小邻域策略。
function [obs, info] = myResetFcn() % 随机初始化姿态角误差 phi0 = (rand - 0.5) * 0.5; % -0.25~0.25 rad theta0 = (rand - 0.5) * 0.5; psi0 = (rand - 0.5) * 0.5; obs = [phi0; theta0; psi0; 0; 0; 0; 0; 0; 0]; info = struct(); end步进函数:接收上一时刻动作、计算奖励和下一个观测。
function [obs_next, reward, done, info] = myStepFcn(action, obs) % action = [delta_Kp; delta_Kd; delta_u] 的列向量 % 读取当前状态 phi = obs(1); theta = obs(2); psi = obs(3); p = obs(4); q = obs(5); r = obs(6); % 基线PD参数 Kp_base = 5.0; Kd_base = 0.5; % 叠加DDPG输出 Kp = Kp_base + action(1); Kd = Kd_base + action(2); delta_u = action(3); % 期望姿态角设为目标0 e_phi = -phi; e_theta = -theta; e_psi = -psi; % PD控制律 M_phi = Kp * e_phi + Kd * (0 - p); M_theta = Kp * e_theta + Kd * (0 - q); M_psi = Kp * e_psi + Kd * (0 - r); % 叠加DDPG补偿 M_phi = M_phi + delta_u; % M_theta和M_psi可按需叠加 % 用欧拉积分更新角速度 % 简化姿态传播,忽略陀螺力矩 p_new = p + (M_phi / Ixx) * Ts; q_new = q + (M_theta / Iyy) * Ts; r_new = r + (M_psi / Izz) * Ts; % 更新欧拉角(小角度假设或使用旋转矩阵) phi_new = phi + p_new * Ts; theta_new = theta + q_new * Ts; psi_new = psi + r_new * Ts; % 构建奖励 obs_next = [phi_new; theta_new; psi_new; p_new; q_new; r_new; 0; 0; 0]; error_norm = abs(phi_new) + abs(theta_new) + abs(psi_new); omega_norm = abs(p_new) + abs(q_new) + abs(r_new); delta_u_norm = abs(delta_u); reward = -(1.0 * error_norm + 0.5 * omega_norm + 0.5 * delta_u_norm); if error_norm < 0.1 % 稳定奖励 reward = reward + 0.3; end done = false; info = struct(); end这个步进函数里我没有细做电机转速混控和位置环衰减,读者可以自行扩充。关键是让DDPG有一个可交互且物理上不会瞬间发散的环境做探索。
3.3 观测、动作设定与网络结构构建
用rlNumericSpec和rlFiniteSetSpec定义观测与动作空间。注意动作连续,要设置合理界限。
obsInfo = rlNumericSpec([9 1]); % 9个连续状态量 obsInfo.Name = 'quad states'; actInfo = rlNumericSpec([3 1]); % delta_Kp, delta_Kd, delta_u actInfo.LowerLimit = [-2; -0.5; -2]; actInfo.UpperLimit = [ 2; 0.5; 2];初始化Agent:
env = rlFunctionEnv(obsInfo, actInfo, @myStepFcn, @myResetFcn);构建Actor和Critic网络,这里分享一下我的经验。Actor主干网络用[128, 128]的全连接层,激活函数用ReLU,输出层用tanh激活,再映射到动作上下限,替代直接在输出层设tanhLayer配合scalingLayer的方式。
% Actor网络 actorNetwork = [ featureInputLayer(obsInfo.Dimension(1), 'Name', 'obs') fullyConnectedLayer(128, 'Name', 'fc1') reluLayer('Name', 'relu1') fullyConnectedLayer(128, 'Name', 'fc2') reluLayer('Name', 'relu2') fullyConnectedLayer(numel(actInfo.LowerLimit), 'Name', 'action') tanhLayer('Name', 'tanh') scalingLayer('Name', 'scale', 'Scale', (actInfo.UpperLimit - actInfo.LowerLimit)/2, ... 'Bias', (actInfo.UpperLimit + actInfo.LowerLimit)/2) ]; actorOptions = rlRepresentationOptions('LearnRate', 1e-3, 'GradientThreshold', 1); actor = rlDeterministicActorRepresentation(actorNetwork, obsInfo, actInfo, ... 'Observation', {'obs'}, 'Action', {'scale'}, actorOptions);Critic网络接收状态和动作两个输入,中间层拼在一起:
% Critic网络 statePath = [featureInputLayer(obsInfo.Dimension(1), 'Name', 'obs') fullyConnectedLayer(128, 'Name', 'fc1') reluLayer('Name', 'relu1')]; actionPath = [featureInputLayer(numel(actInfo.LowerLimit), 'Name', 'act') fullyConnectedLayer(128, 'Name', 'fc2')]; commonPath = [additionLayer(2, 'Name', 'add') reluLayer('Name', 'relu2') fullyConnectedLayer(64, 'Name', 'fc3') reluLayer('Name', 'relu3') fullyConnectedLayer(1, 'Name', 'Q')]; criticNetwork = layerGraph(statePath); criticNetwork = addLayers(criticNetwork, actionPath); criticNetwork = addLayers(criticNetwork, commonPath); criticNetwork = connectLayers(criticNetwork, 'relu1', 'add/in1'); criticNetwork = connectLayers(criticNetwork, 'fc2', 'add/in2'); criticOptions = rlRepresentationOptions('LearnRate', 1e-3, 'GradientThreshold', 1); critic = rlQValueRepresentation(criticNetwork, obsInfo, actInfo, ... 'Observation', {'obs'}, 'Action', {'act'}, criticOptions);有一点必须提醒,网络不要一味加深加宽。DDPG对动作价值函数的逼近误差很敏感,结构过深反而导致Q值过估计、训练不稳定。三层128、128、64就已经够用,深度需要克制。
3.4 训练参数配置与完整训练流程
Agent参数按经验设定如下:
agentOptions = rlDDPGAgentOptions(... 'SampleTime', Ts, ... 'DiscountFactor', 0.99, ... 'MiniBatchSize', 64, ... 'ExperienceBufferLength', 1e6, ... 'TargetSmoothFactor', 1e-3, ... 'TargetUpdateFrequency', 1, ... 'ResetExperienceBufferBeforeTraining', false); agent = rlDDPGAgent(actor, critic, agentOptions);噪音参数使用高斯探索噪声,初始方差设0.3,随着训练衰减:
agent.AgentOptions.NoiseOptions.Variance = 0.3; agent.AgentOptions.NoiseOptions.VarianceDecayRate = 1e-4;训练配置方面:
trainOpts = rlTrainingOptions(... 'MaxEpisodes', 2000, ... 'MaxStepsPerEpisode', 500, ... 'ScoreAveragingWindowLength', 20, ... 'Plots', 'training-progress', ... 'StopTrainingCriteria', 'AverageReward', ... 'StopTrainingValue', -20);训练过程中的现象:前200个回合,奖励波动剧烈,智能体探索噪声太大,动作在各种PD参数之间乱跳。这时看控制量曲线你会觉得它在“发酒疯”,但真实反馈又很丰富,正是学习阶段。大约500到800个回合后,每回合总奖励趋向稳定,姿态误差被压制到较小的范围。中途有一段训练回退,别慌,这是正常的探索重新启动。
在训练完成后,验证阶段需要关闭探索噪声,保持纯策略评估。我用保存的智能体跑100次随机初始状态,统计平均角度误差和控制量抖动情况。最后的基本指标是最大姿态角误差不超过1.8度,平均误差0.6度左右,控制量没有明显高频抖动。
4. 常见问题与排查技巧实录
4.1 训练完全不收敛的典型症状与检查顺序
遇到DDPG训练不收敛,最常见的就是奖励前期受行为策略影响上下剧烈振荡,甚至发散。我的排查顺序一般按这个来,可以帮你快速定位问题根因:
先看环境自身是否逻辑自洽。让环境只运行PD控制器,不做任何探索噪声,检查姿态误差的响应是否正常。如果基础PD在极端初始状态下直接溢值,奖励再设计也没用。
再检查状态量纲是否一致性。角度和角速度量级差异过大,会导致误差项奖励被角度主导,角速度惩罚形同虚设。我建议把状态做归一化或至少对量级敏感项单独设奖励权重。
观察Actor输出变化范围。如果训练初期动作输出频繁达到限幅值,实际上是探索方差过大或动作边界设置过窄。可以先把边界放宽,同时把方差调小一点,逐步找到合理动作空间。
观察Q值曲线。如果Critic的Q值持续上涨但奖励不涨,基本就是过估计问题,检查Critic网络层数和目标网络平滑因子。
我遇到的一次典型案例是奖励函数中稳定项权重给高了,智能体学到的策略是频繁小幅修正把姿态稳定在阈值内,但角速度被放得很高,飞行器高频颤动。后面把稳定权重降下来,并增加角速度惩罚项,才恢复正常。
4.2 训练过程中奖励震荡怎么处理
训练曲线长期上下波动不代表失败,关键看波动趋势。500回合后若奖励在某个值附近来回跳动但幅度不减,大概率是网络参数更新和探索噪声之间失衡了。
我自己的解决套路:
- 降低探索噪声方差衰减率,让智能体在训练后半程逐步切换到“精细调优”模式。
- 把
MiniBatchSize从64提到128,提高批次内数据多样性,降低单批更新噪声。 - 适当调大
TargetSmoothFactor,从1e-3变到5e-3,加快目标网络追踪,减少延迟。 - 如果问题依旧,试试把Critic学习率降到1e-4,Actor保持1e-3,两边学习率解耦往往会有奇效。经验来自踩坑:Actor和Critic学习率同时设1e-3,前期很容易出现Q值过估计导致崩溃。
4.3 奖励函数调了几版都不行的底层逻辑
奖励函数设计的本质是给智能体立规矩,但规矩太多它不知道优先级。我看到不少初学者把奖励函数设置成一系列条件判断,例如“角度小于0.5加分、角速度小于1加分”,表面上给了很多正反馈,实际上智能体为了拿分疯狂利用微弱角度抖动,行为完全跑偏。
我的建议是奖励函数必须使用连续、平滑、可微的惩罚或回报项主导,疏能定义的加分项不超过一个。连续惩罚项能够给DDPG提供密集且稳定的梯度信号;离散条件加分会让Q函数逼近困难,边界突变处产生对抗性过拟合。
个人项目的实际经验是,奖励函数全部重写成一个加权平方误差加输出变化率惩罚,再加一个稀疏稳定奖励,收敛速度反而快了很多。
4.4 实际应用中的避坑清单总结
整理了一份自用避坑清单,基本覆盖了这个项目从仿真到实际落地的绝大多数问题:
| 坑点 | 症状 | 解决方案 |
|---|---|---|
| 状态量纲未统一 | 训练曲线抖动严重 | 将角度转为弧度并归一化,速度项单独加权 |
| 动作限幅过大 | 初阶段探索失控 | 根据PD参数合理范围设置限幅 |
| 奖励项相互冲突 | 学出“刷分”策略 | 只保留连续惩罚项,减少离散大奖励 |
| 训练回合不够 | 收敛假象,泛化差 | 用随机初始状态,提高回合数和负载干扰 |
| 仿真模型太简化 | 真机表现差 | 增加气动阻尼、陀螺力矩等非线性项 |
| 忽略执行器饱和 | 策略无效 | 在环境中加入电机转速上下限约束 |
| 噪声衰减过快 | 收敛到局部最优 | 降低衰减率,保持探索能力 |
另外,环境初始化也不能一直用零误差,否则学到的策略只适配“小误差再小修正”的单一场景。在ResetFcn里加上随机初始角度、随机风扰,甚至随机电机老化系数,智能体泛化性能会好很多。
从我个人实验体会来看,DDPG最让我惊艳的不是完全替代了PD参数整定,而是它像一个大而不笨的助手,既保留了基线控制器的安全边界,又自动补上了PD在非线性和工况变化下的“盲区”。跑通这套流程后,后续再换任务,比如做轨迹跟踪优化,只需要改状态定义和奖励函数,网络架构和控制框架可以完全复用,这才是这套方案最有价值的地方。