MATLAB实现自适应动态规划:HDP/DHP/ADHDP/ADDHP算法解析与应用
2026/9/14 5:30:44 网站建设 项目流程

简介:自适应动态规划(ADP)系列MATLAB实现,覆盖HDP、DHP、ADHDP、ADDHP四种变体,适合研究控制优化、需要在不确定性与时变环境中进行决策的工程师和算法学习者。压缩包共5个文件,包含4个.m源码文件与1个PDF理论文档,整体约162KB,体量精简。源码分别对应混合动态规划、离散动态规划及其自适应扩展版本,便于对照差异;PDF为Landelius的学术论文,可辅助理解算法背后的数学基础与典型应用场景。已有482人学习下载。通过运行和调试代码,能够直观掌握ADP的迭代更新与策略改进机制,并快速迁移到控制系统设计或仿真项目中;若在C#工程中使用,也可借助MATLAB工具部署为计算模块,拓宽应用方式。资源结构清晰,适合从理论到编码快速上手。

1. 从贝尔曼方程到ADP:HDP/DHP/ADHDP/ADDHP到底解决了什么

拿到一个同时包含 HDP、DHP、ADHDP、ADDHP 四个算法的 MATLAB 压缩包,如果只想跑通 demo,半小时就够了;但要真拿去做控制系统优化,得先把一件事想明白:ADP 不是在算一张价值表,而是用神经网络去逼近动态规划里的价值函数和策略。这套代码的价值在于,它把贝尔曼方程拆成了评价网络、动作网络、模型网络的迭代更新,让我能从离散状态空间的表格遍历,平滑过渡到连续状态空间的实时决策。对电力调度、机械臂轨迹跟踪、通信资源分配这类场景,HDP 和 DHP 正好覆盖了价值迭代和基于梯度的策略优化两条典型路线,ADHDP 与 ADDHP 又为动作依赖型问题提供了 Q 函数式解法。适合对动态规划有基础、想快速复现一种 ADP 变体并对比其收敛速度的工程师;即使你是做 C# 上位机的,也可以把编译后的 MATLAB 组件集成进去做在线决策内核。

2. 三种网络结构与MATLAB实现:模型网络、评价网络、动作网络怎么搭

打开任意一个.m文件,你会发现 MATLAB 实现 ADP 的方式非常有规律:绝大多数代码被拆成主循环、评价网络、动作网络三个相对独立的部分,模型网络有时独立成函数,有时直接用系统的状态转移矩阵代替。理解这三者各自负责什么,比看懂任何一行具体代码都重要,因为四个变体之间的差异本质上只是评价网络输出和目标构造方式不同,控制流结构几乎是同一套。

2.1 为什么ADP需要三个网络而不是一张价值表

经典动态规划里,价值表的下标就是离散状态,更新公式也是查表式地遍历。一旦状态变成连续量,比如四旋翼的偏航角、机械臂关节的力矩,表的规模会立刻爆炸。ADP 的做法是用一个带 sigmoid 或 tanh 激活的单隐层网络去近似价值函数,这样状态输入是连续的,输出是标量(HDP)或与状态同维度的向量(DHP)。动作网络负责根据当前状态给出控制量,评价网络负责给这条控制路径打分,模型网络则描述状态如何演变,可以是已知转移矩阵,也可以是从数据里辨识出来的参数化模型。三个网络各司其职,更新链条才能闭合。Landelius.pdf里对这套结构的数学收敛性做了完整推导,读第二章能避免很多权重初始化阶段的玄学问题。

2.2 模型网络:用已知系统矩阵还是在线辨识

这个压缩包里多数 demo 用的是已知模型,也就是在仿真的每一步用同一个状态转移函数推动系统前进,这不代表模型网络没用。工程上模型不精确或者根本没有模型的情况更常见。一种容易落地的做法是把系统动态封装成函数句柄,这样无论推导出的是线性矩阵AB还是非线性函数,主循环代码都不需要改。

% 把系统模型封装成函数句柄,方便在已知/辨识模型之间切换 sys = @(x, u) nonlinear_dynamics(x, u); % 非线性动态,返回x(t+1) x_next = sys(x, u); % 主循环中推进一步 % 如果只有线性模型,可以写成 % sys = @(x, u) A * x + B * u;

上面代码里,nonlinear_dynamics需要你自己定义,返回值必须和x同维度。用函数句柄的好处是,主循环不需要关心模型是解析推导还是神经网络辨识出来的,只要输出接口对齐,模型部分可以被任意替换。我在做倒立摆控制时,先替换成从 Simulink 导出的线性化矩阵验证算法,再换成非线性模型,调试时问题边界非常清晰。参数说明:x代表状态向量,u是动作向量,AB是线性化系数矩阵;切换模型类型时保持x_next维度不变,否则评价网络输出维度也要同步修改。

2.3 评价网络的反向传播结构与权值更新

2.3.1 权值的两种传递方式:全局变量和函数句柄

这个压缩包里大概率能看到global w_critic这类写法,或者通过结构体critic.W传入传出。全局变量在 MATLAB 老代码里非常常见,但我建议复用时改成结构体传参,因为全局变量在多实验对照时很容易串,尤其在addpath多个目录时会出现同名变量互相覆盖的诡异问题。

% 推荐用结构体保存评价网络组件 critic.W1 = rand(hidden_size, n_state) * 0.1 - 0.05; critic.W2 = rand(1, hidden_size) * 0.1 - 0.05; critic.b1 = zeros(hidden_size, 1); critic.b2 = 0; % 前向计算 h1 = tanh(critic.W1 * x + critic.b1); J = critic.W2 * h1 + critic.b2;

critic.W1是从状态到隐藏层的权值,critic.W2是隐藏层到评价输出的权值,b1b2是偏置项。初始化范围取正负 0.05 这类小量,是为了避免 tanh 把梯度压死在饱和区;如果直接使用rand不做缩放,前几步更新会非常不稳定,表现为评价输出一直停留在零附近。hidden_size的选取跟状态维度相关,一般取状态维度的两到三倍即可,过大会让单步更新噪声变大。

2.3.2 在线更新与批量更新的取舍

ADP 大多数应用是边采样边更新,也就是每得到一个(x, u, r, x_next)四元组,就立即计算一次误差并更新权值。这种方式响应快,适合在线控制任务,但误差曲线波动明显。如果是在数据集上离线训练,则应该攒够一个 batch 再统一更新,此时误差曲线平滑,代价是要多维护一个环形缓存。压缩包里的 demo 基本都是前者,直接修改成批量模式时需要增加一个buffer结构,否则每次都覆盖旧样本,经验回放就失去意义。

% 在线更新:每步立即反向传播 error = target - J; critic.W2 = critic.W2 + lr_c * error * h1'; critic.W1 = critic.W1 + lr_c * error * (critic.W2' .* (1 - h1.^2)) * x';

上面是典型的单步梯度下降写法。lr_c是评价网络学习率,一般取 0.01 到 0.05 之间;1 - h1.^2是 tanh 函数的导数,它对隐藏层激活值逐元素计算;critic.W2'把评价标量误差往隐藏层反向扩散。这里的维度关系是:状态维度一旦变化,critic.W1的列数必须跟着变,否则矩阵乘法直接报维度不匹配。批量更新时,只需把多步的error向量化,把x换成矩阵即可,但要注意隐藏层激活矩阵的形状和单样本写法相反。

2.4 三个网络输入输出速查表

网络角色输入输出更新依据
模型网络当前状态 x、动作 u下一时刻状态 x_next系统辨识误差 x_next - f(x,u)
评价网络当前状态 x(或含动作 u)价值标量 J 或协状态 lambdaHDP:r+gamma*J(x_next)-J(x),DHP:lambda 误差
动作网络当前状态 x控制量 u使评价输出最小化的梯度方向

对 ADHDP 和 ADDHP,评价网络的输入会额外拼接动作量 u,对应表格中的“含动作 u”一列。这样设计的原因是:只有把动作纳入评价输入,Q 函数才能回答“当前状态用哪个动作最优”,而状态价值 J 只能评估状态好坏,无法直接对比动作。

3. HDP.m与DHP.m的迭代差异:状态价值路线与协状态路线

先纠正一个命名误区:DHP 在全称上常被写成 Discrete Heuristic Programming,但在 Landelius 的博士论文和多数 ADP 文献里,DHP 的标准全称是 Dual Heuristic Programming,对应的中文叫对偶启发式规划,和“离散”无关。HDP 则是 Heuristic Dynamic Programming。两者共享同一个主循环框架,区别在于评价网络输出的对象从价值标量变成了价值梯度,这一步改动带来了收敛速度的质变。

3.1 HDP.m核心:状态价值估计与TD误差构造

HDP 是最容易上手的变体,评价网络输出状态价值J(x),更新目标是经典的 TD 形式。主循环先由动作网络给出控制量,推进到下一状态,再通过评价网络得到J_next,然后构造误差。代码结构与第 2 章给出的示例一致,核心差异在目标值的构造方式。

% HDP主循环:价值目标构造与评价网更新 u = policy(x); % 动作网络给出控制 x_next = sys(x, u); % 模型网络推进一行 J_now = critic_eval(x); % 当前状态的价值 J_next = critic_eval(x_next); % 下一状态的价值 reward = cost_fun(x, u); % 一步代价,通常取负奖励 target = reward + gamma * J_next; % HDP的TD目标 e_c = target - J_now; % 评价误差 critic_update(e_c); % 单步更新评价网

gamma是折扣因子,HDP 代码里通常取 0.95 到 0.99,它控制未来代价在目标中的权重。reward一般写成负的代价函数,目的是让 HDP 在最小化累计代价时恰好等价于最大化累计奖励。critic_update完成的是第 2 章写过的反向传播。注意target的计算依赖x_next,所以模型网络的前向传播必须先于评价网络的目标构造执行,如果顺序写反,误差会一直震荡不收敛。

3.2 DHP.m的协状态输出:为什么梯度值比价值更好使

DHP 和 HDP 表面差别只在评价网络的输出维度,但更新公式差了一个层级。DHP 的输出是协状态lambda = dJ/dx,也就是价值对状态的梯度。目标更新不再是标量差分,而是两个协状态之间的递推关系。这要求系统模型可微,且雅可比矩阵必须正确,这也是 DHP 实现比 HDP 更容易出 Bug 的根因。

% DHP核心:构造协状态目标并回归 lambda_now = critic_eval(x); % 当前协状态 lambda_next = critic_eval(x_next); % 下一协状态 % 目标由系统雅可比与价值梯度链式导出 target_lambda = d_cost_dx(x, u) + gamma * d_sys_dx(x, u)' * lambda_next; e_c = target_lambda - lambda_now; critic_update(e_c); % 按向量误差更新

这里d_cost_dx是代价函数对状态的梯度,d_sys_dx是系统动态对状态的雅可比矩阵。如果模型是线性化后的A矩阵,d_sys_dx直接取A即可。相比 HDP,DHP 多了一阶导数信息,收敛速度更快,但代价是模型梯度必须是解析的或数值上稳定,否则误差会在导数放大下变得很吵。gamma的取值在 DHP 里也需要相应调大,否则目标中当前代价占比过重,协状态的长期信息会被逐步稀释。

3.3 同一套主循环下两种算法的对接位置

差异点HDP.mDHP.m
评价网络输出标量 J向量 lambda
目标构造reward + gamma*J_nextd_cost_dx + gamma*A'*lambda_next
动作网络反传先算 dJ/du 再回传经协状态和雅可比直接回传
收敛速度慢但稳快但易震荡

这个对照规律意味着,把HDP.m改成 DHP 时不需要重写整个框架,只需复制一份主循环,替换评价网络前向函数与target构造函数,剩下的模型推进、回合终止、reward 计算逻辑几乎可以原样保留。实际项目里,如果系统动态能够求导,我一般先跑通 HDP 确认逻辑,再切到 DHP 提速,避免一开始就在协状态维度上排查基础错误。

3.4 怎么判断算法真的收敛了

跑通 demo 后,把每一步的评价误差e_c画出来,正常情况下误差会在前几百步剧烈震荡后呈衰减趋势,最终维持在小范围内波动。如果误差完全不降,先检查 reward 符号是否与目标一致,再检查 gamma 是否过大,最后检查模型网络返回的x_next是否发散。DHP 还要额外看雅可比矩阵的规模,如果范数长期大于 10,梯度目标会把协状态推得非常大,几乎必然导致 NaN。注意误差曲线不能只看最后的绝对值,要同时观察是否出现周期性的尖峰,那通常意味着动作网络和评价网络的更新频率不匹配。

4. ADHDP.m与ADDHP.m的自适应机制:Q函数求解与梯度级联

ADHDP 和 ADDHP 里的 A 常被误解为“在线学习”,但它们强调的是策略在运行过程中持续调整,而不是训练一次就冻结。HDP/DHP 可以固定模型离线训练,ADHDP/ADDHP 则更多用于模型不完全已知时,边获得新数据边更新评价和动作网络。因此这两个变体对超参数和权值初始化更敏感,跑出来的波动也更大。

4.1 为什么Q函数会改变评价网络的输入结构

ADHDP 对应 Q 函数版本。状态价值 J 只能评估状态好坏,无法直接对比动作,Q 价值把动作作为评价网络输入的一部分,于是评价网络可以直接回答“当前状态采取这个动作能获得多少长期回报”。代价是评价网络的输入维度变成n_state + n_action,隐藏层规模也要相应增大,否则 Q 曲面的拟合精度不够,动作网络很容易收敛到局部次优点。

4.2 ADHDP.m:动作价值目标构造的实现

% ADHDP:Q函数作为评价网络的输出 u = policy(x); x_next = sys(x, u); Q_now = critic_eval([x; u]); % Q(s,a) Q_next = critic_eval([x_next; policy(x_next)]); % 下一状态下取最优动作的Q target_q = reward + gamma * Q_next; % 贝尔曼最优性方程 e_c = target_q - Q_now;

评价网络的输入从单独的 x 变成[x; u]的拼接向量,输出仍是标量 Q。动作网络的更新目标是让动作在评价网络里对应的 Q 值最大,这会对动作网络施加一个负梯度方向的更新。训练时,动作网络和评价网络必须交替更新:先固定动作网络采样,再固定评价网络更新 Q 函数,接下来再固定 Q 更新动作。如果两边学习率一样大,整个系统很容易振荡,这也是 ADHDP 调参中最常见的失控模式。

4.3 ADDHP.m:协状态与动作梯度的一体化链式更新

ADDHP 是四个变体里最绕的一个。它保留 DHP 的协状态输出,又引入 Q 函数的动作维度。评价网络输出的是dQ/dxdQ/du的组合,动作网络的更新直接依赖这些梯度。链条大致是:动作网络给出 u,评价网络给出对状态的梯度,模型网络给出状态转移的雅可比,三者相乘得到动作部分的改进方向。

% ADDHP梯度回传路径 [lam_x, ~] = critic_eval(x, u); % 评价网输出对状态的偏导 f_x = model_jacobian_x(x, u); % 模型对状态求导 f_u = model_jacobian_u(x, u); % 模型对动作求导 % 动作网络梯度近似由 f_u^T * lam_x 得到 grad_u = f_u' * lam_x;

model_jacobian_xmodel_jacobian_u分别是对状态和对动作求雅可比,不同系统差别很大,必须手动确认结果。ADDHP 某些实现里还会对动作网络多套一层链式法则,导致公式变长,但本质没有变。调试时如果动作不收敛,优先打印grad_u的范数,看是否出现量级漂移;量级在训练过程中持续上涨,通常是评价网络没有收敛带动了动作梯度发散。

4.4 调参表与稳定性边界

参数含义HDPDHPADHDPADDHP
lr_c评价网络学习率0.01-0.030.005-0.020.01-0.030.005-0.02
lr_a动作网络学习率0.005-0.010.005-0.010.001-0.0050.001-0.005
gamma折扣因子0.950.95-0.990.9-0.950.95
hidden隐藏层单元数6-108-1210-2012-20
epochs训练步数2000200050005000

上表是我用倒立摆和线性二次型基准测试时常用的起始值。ADHDP/ADDHP 因为动作参与评价输入,需要更大的 hidden 规模保证 Q 曲面的拟合能力,这是它们训练更慢的原因之一。如果某个变体发散,优先调小 lr_c 和 lr_a,而不是调整网络宽度,因为隐层宽度对稳定性的影响远小于学习率。

4.5 最常见的三个坑:初始化、归一化和NaN

四份代码最常踩的坑有三个。第一,权重初始化范围过大,tanh 激活函数直接饱和,梯度消失导致评价网络输出恒为零,误差曲线看起来是一条水平线,实际是网络根本没有学习。第二,状态和动作没有归一化,当状态量级从 0.1 到 1000 变化时,雅可比矩阵元素量级差异会把梯度更新带偏,表现为前几步误差正常,随后突然爆炸。第三,NaN 经常发生在 DHP/ADDHP 的雅可比计算里,尤其是状态穿越零点时出现除零或奇异矩阵,这时要在模型网络出口加isnan检查,打印出是哪一步产生的,才能区分是模型发散还是评价网络发散。

提示:排查 NaN 时不要只看误差曲线,要在x_next计算之后立刻执行any(~isfinite(x_next)),把异常定位到模型网络还是评价网络。

5. MATLAB实例封装为C#可调用模块的验证捷径

如果要把这份 ADP 代码嵌入 C# 上位机,比较常见的做法是用 MATLAB Compiler SDK 生成 .NET 程序集,把 MATLAB 的数值计算能力封装成 C# 可调用的类。这个方案适合控制算法已经验证完毕、只差部署的场景,不适合把整个训练过程都搬到 C# 里,因为跨语言重复造轮子的成本远超收益。

5.1 编译成.NET组件的命令与文件要求

在 MATLAB 命令窗口执行deploytool,选择 .NET Assembly 目标,把主函数和依赖的.m文件全部添加进去,再设置输出类名。注意主函数里不要用global传递权值,编译后全局变量不会保留跨调用状态;要么把参数全部通过函数入参传递,要么把权值序列化成文件在 C# 侧管理。

% 在MATLAB命令窗口执行 deploytool -build adp_solver.prj % 或者直接用命令行编译 compiler.build.dotNETAssembly('adpHDP.m', 'OutputDir', 'build');

adp_solver.prj是部署项目文件,首次创建后建议纳入版本管理。入口函数要声明为function u = PolicyEvaluate(x)而不是脚本,因为编译后的组件需要有明确的函数签名可供 C# 反射调用。编译成功后,生成目录里会包含.dll文件和对应的setup.exe,后者用于目标机器安装 MATLAB Runtime。

5.2 C#侧的MWArray参数构造与调用

using MathWorks.MATLAB.NET.Arrays; using MathWorks.MATLAB.NET.Utility; var solver = new ADPSolver(); // 类名由编译时定义 double[] state = { 0.1, 0.2, -0.3 }; MWArray x = new MWNumericArray(state); MWArray u = solver.PolicyEvaluate(x); // 入口函数名与.m函数一致 double[,] result = (double[,])u.ToArray();

MWNumericArray是 MATLAB 与 C# 之间数值数组的桥接类型。状态变量必须是一维或二维数组,不能传 null,MATLAB 侧函数签名要写成function u = PolicyEvaluate(x)。若返回值是矩阵,ToArray()返回double[,],再自行索引即可。C# 里传入的数组默认是列优先,和 MATLAB 一致,不需要额外转置,但如果你在 C# 侧使用了MathNet.Numerics这类库构造矩阵,那就必须检查其内部存储顺序,否则数据通路是反的。

5.3 验证策略:快照比对

跨语言调用最容易出问题的是矩阵维度顺序和数值精度差异。验证做法是:先在 MATLAB 里跑一次完整策略序列,把每一步的 u 保存成二进制.bin文件,再在 C# 里调用编译后的组件跑同一组初始状态,逐点比较。误差在 1e-6 量级说明数据通路没有问题;如果差异超过 1e-3,优先检查 C# 侧输入的矩阵是否需要转置,而不是怀疑算法本身。我一般会把对比脚本和基准数据放在同一个工程目录,每次改完参数后重新生成快照,再跑 C# 单元测试,这样每次改动都能自动回归,避免在集成阶段才发现策略漂移。

本文还有配套的精品资源,点击获取

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

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

立即咨询