Python实现自适应动态规划(ADP):HDP三网络协作实战指南

发布时间:2026/9/2 2:51:27
Python实现自适应动态规划(ADP):HDP三网络协作实战指南 简介面向强化学习与最优控制方向的开发者和研究者这套Python代码实现了自适应动态规划ADP中的启发式动态规划方法HDP可在非线性系统最优控制、自适应评价等场景下学习与验证算法。压缩包体积约10KB内含3个Python脚本分别对应基础HDP流程、引入目标网络的HDP改进版本以及基于PyTorch的评价网络与值函数更新模块整体代码精简、便于快速阅读。目前已有88人学习下载通过对三个脚本的对比可以直观看到算法在基础版本与引入目标网络版本之间的差异适合初学者结合论文公式理解算法迭代逻辑也适合有经验的读者在此基础上替换网络结构、奖励函数或控制对象继续做扩展实验与算法优化。虽然资源包不大但把ADP的核心训练步骤和关键变体都覆盖到了是一个实用的小型学习与二次开发起点。 去年年底我在做最优控制相关调研时第一次完整地用Python把自适应动态规划ADP算法跑通。说实话一开始我翻到那些论文时是有点头大的满篇的哈密顿函数、策略迭代、值函数近似数学符号一套接一套感觉门槛特别高。但真正动手实现之后我才发现ADP的核心骨架其实很朴素——三块简化版神经网络搭在一起就能跑出一个能用的控制器完全没有想象中那么神秘。这篇文章我想把整个项目的设计思路、代码模块、训练踩坑过程都梳理一遍给正在学强化学习或者最优控制的朋友一个可快速上手的参照。这篇文章适合有一定Python基础、会用PyTorch搭过简单多层感知机、但对ADP本身不太熟悉的读者。我会从一个最简单的非线性控制系统出发把HDP启发式动态规划这种最经典的ADP变体从头到尾实现一遍并说明每一个关键细节背后的原因。1. 核心原理与方案设计思路1.1 为什么动态规划在连续系统上失效经典的动态规划Dynamic Programming, DP在离散状态空间上是非常优美的通过贝尔曼方程从终端的代价函数倒推能得到全局最优的值函数和策略。但一旦换成连续状态、连续动作的系统DP立刻遇到一个尴尬的问题状态空间是无穷的没法对所有点逐一求值。就算你把状态空间离散成网格状态变多维之后网格数也会爆炸成天文数字这就是常听到的“维度灾难”。ADP的思路是用函数逼近器最常见的就是神经网络去近似动态规划里的值函数和策略函数把“遍历全空间”换成“在采样点上拟合”。你不需要真的知道所有状态的值函数只要神经网络的拟合能力足够强它就能从有限的采样中泛化出一个平滑的函数表达。这本质上和普通监督学习里用有限样本拟合未知函数是一回事。1.2 三个网络各自扮演什么角色ADP尤其是HDP结构的实现里通常有三个神经网络模型网络、Critic网络和Actor网络。三个角色可以这么理解Actor是玩家负责做决策Model是物理引擎负责告诉玩家“你这么做之后世界会变成什么样”Critic是裁判负责评价“当前这个状态到底有多糟糕”。模型网络Model Network输入当前状态和控制量输出下一时刻的状态。它是对系统动力学的拟合。评论网络Critic Network输入状态输出值函数估计值即从当前状态出发按照当前策略继续走下去所需要付出的长期累积代价。执行网络Actor Network输入状态输出控制量是最终我们要用的控制器。三个网络不是并列训练而是有一个清晰的依赖链。Critic的评价要参考模型网络给出的下一状态和Actor给出的下一步控制Actor的更新要依靠“模型网络Ciritc网络”构成的梯度路径来计算它对长期代价的影响模型网络的训练则相对独立它是一个监督学习任务输入是状态和控制标签是真实系统的下一状态。这个依赖关系是理解整个ADP训练流程的钥匙。1.3 为什么从HDP变体入手ADP家族里有很多变体最常见的是HDPHeuristic Dynamic Programming、DHPDual Heuristic Programming和GDHP。HDP只用Critic网络逼近值函数本身DHP则是逼近值函数对状态的偏导数GDHP两者都逼近。我最终选择HDP原因很简单DHP虽然在某些问题里收敛更快、精度更高但它需要额外计算值函数对状态的梯度作为监督信号无论是数学推导还是代码实现都更绕。而HDP的损失函数就是一个均方误差理解起来非常直白让Critic网络的输出去拟合“当前代价 折扣后的未来代价”。先把HDP的实现链路吃透后续再切DHP其实是水到渠成的事。2. 代码整体架构与核心模块实现2.1 项目结构与依赖准备项目文件组织得尽可能简单我分了四个脚本再加上一个环境定义。实际跑起来也不需要复杂工程化一个训练脚本就能搞定。adp_demo/ ├── envs.py # 仿真环境定义系统动态方程 ├── networks.py # 三个神经网络的结构定义 ├── train.py # 模型网络预训练 ADP主循环 └── controller.py # 训练完成后用Actor做闭环控制依赖只有三样Python 3.9、PyTorch 2.0、NumPy。我在Windows和Linux上都跑过这段代码没有遇到环境兼容问题。测试环境我选了一个非常经典的一阶非线性系统x_{t1} 0.5 * sin(x_t) 1.2 * u_t这个系统简单、可控、有非线性。代价函数设为当前状态和控制量的二次型r_t x_t^2 u_t^2目标很明确让控制器学会“用小代价尽快把状态稳定回0”。2.2 模型网络设计与训练逻辑模型网络的结构是所有网络里最直观的输入当前状态 x 和控制量 u输出下一状态的预测值。它有两层隐藏层激活函数用Tanh。import torch import torch.nn as nn class ModelNet(nn.Module): def __init__(self): super().__init__() self.fc nn.Sequential( nn.Linear(2, 32), nn.Tanh(), nn.Linear(32, 32), nn.Tanh(), nn.Linear(32, 1) ) def forward(self, x, u): return self.fc(torch.cat([x, u], dim-1))训练模型网络时我在状态区间 [-2, 2] 和控制区间 [-0.5, 0.5] 内均匀随机采样生成几千组 (x, u, x_next) 数据然后用MSE损失训练到收敛。这里有一个关键原则模型网络是整个ADP的估值基础。Critic网络的TD目标需要下一时刻的状态Actor网络更新需要计算代价对控制量的梯度两者都需要经过模型网络来传导。如果模型网络预测误差太大后面的训练基本就是空中楼阁。关于数据来源仿真环境里我们当然可以用真实方程生成数据。但如果你要控制的是实物系统没有解析模型模型网络就必须靠采集实际系统的输入输出数据来训练这也是ADP相较传统DP更实用的原因之一。2.3 Critic网络与贝尔曼方程的落地Critic网络输入当前状态 x输出值函数估计值 J(x)。它的结构同样是一个多层感知机class CriticNet(nn.Module): def __init__(self): super().__init__() self.fc nn.Sequential( nn.Linear(1, 32), nn.Tanh(), nn.Linear(32, 32), nn.Tanh(), nn.Linear(32, 1) ) def forward(self, x): return self.fc(x)Critic的训练目标是从贝尔曼方程导出的。在标准动态规划里值函数满足J(x_t) r_t gamma * J(x_{t1})所以Critic网络要做的事就是拟合等式右边。具体来说在当前状态 x_t 下先让Actor输出 u_t然后从环境拿到 x_{t1}再计算目标值target r_t gamma * critic_target(x_next).detach() loss_critic nn.functional.mse_loss(critic(x_t), target)注意这里用了detach()或者一个独立的critic_target网络来生成目标。这个细节极其重要。如果不切断梯度Critic网络的更新目标里有一部分是依赖它自己当前参数的这会导致自举效应被放大训练很容易震荡甚至发散。我一开始偷懒直接使用同一个网络计算TD目标结果Critic损失在初期快速下降到1e-3之后就开始剧烈抖动怎么调学习率都不行后来改成目标网络软更新每步更新参数向Critic网络靠近一小段之后就稳定了。2.4 Actor网络与策略梯度传导Actor网络输入当前状态 x输出控制量 u。由于控制量有限幅要求所以最后一层带了Tanh激活函数来做约束class ActorNet(nn.Module): def __init__(self): super().__init__() self.fc nn.Sequential( nn.Linear(1, 32), nn.Tanh(), nn.Linear(32, 32), nn.Tanh(), nn.Linear(32, 1), nn.Tanh() # 输出范围 [-1, 1] ) def forward(self, x): return self.fc(x) * 0.5 # 映射到 [-0.5, 0.5]Actor网络没有直接的监督标签它的学习信号完全来自Critic网络和模型网络构成的复合路径。思路很直白既然我们要让长期代价最小那Actor的参数更新方向就应该让“当前代价 未来代价”减小。具体实现方式是把Actor接到模型网络上做链式传播。输入 xActor给出 uu 作为模型网络的一部分输入模型网络输出 x_nextx_next 输入Critic得到 J(x_next)。再加上当前代价 r_t就能得到从当前控制策略出发的长期代价。然后计算这个长期代价关于Actor参数的梯度就可以反向更新Actor。u actor(x) x_next_pred model(x, u) cost_now x.pow(2) u.pow(2) cost_future gamma * critic(x_next_pred) total_cost cost_now cost_future actor_optimizer.zero_grad() total_cost.backward() # 梯度会流经critic - model - actor actor_optimizer.step()这一小段代码是整个项目最核心的部分。注意梯度回传路径是整体贯通的但actor_optimizer只更新actor参数其它网络参数在这个步骤里保持不变。如果不小心把Critic的参数也放在了同一个优化器里Critic就会在Actor更新时被顺带修改整个体系就乱了。所以我刻意分成三个独立的optimizer实例。2.5 训练主循环的完整顺序训练流程分两个阶段。第一个阶段是模型网络预训练第二个阶段是ADP主循环三个网络交替更新。主循环的伪代码逻辑如下1. 随机初始化三个网络和它们的目标网络 2. 预训练模型网络 3. 重复M个epoch: a. 随机采样初始状态x b. 用真实环境方程推进几步采集转移样本 c. 更新Critic让critic(x)逼近 r gamma * critic_target(x_next) d. 每N步软更新critic_target e. 更新Actor通过(actor - model - critic)路径最小化长期代价 f. 更新模型网络用在线新采集的转移样本继续微调一个epoch里三个网络轮流更新而不是分别固定训练很久。这样做的好处是三者共同进化Critic逐渐变准Actor逐渐变好模型网络也逐渐修正对环境的拟合误差。实际操作中模型网络只在预训练阶段用较大学习率训练主循环里只做很小的在线微调防止它被后续更新的样本带偏。3. 训练实操与参数调节心得3.1 先训练模型网络不然后面全是空中楼阁我强烈建议不要跳过模型网络的预训练。在很多ADP教程里模型网络被认为是“已知的”实际落地时往往需要自己拟合。对于这个一阶系统我在 [-2, 2] 的状态区间和 [-0.5, 0.5] 的控制区间内均匀采样了4000组数据用Adam优化器以0.001的学习率训练了500步模型网络的MSE损失已经能降到1e-4级别对系统动态的拟合足够精准。预训练完成后我做了一个验证随机生成一些测试点对比模型预测值和真实系统方程输出最大误差不到0.01。这样后面Actor的梯度路径才有意义。如果你的模型网络误差在0.1级别梯度方向很可能被噪声淹没训练基本就是原地打转。3.2 推荐参数表我整理了一份实际跑下来比较稳的参数配置新手可以照抄起步参数推荐值说明折扣因子 gamma0.95越大越看重长远代价但训练收敛越慢Actor 学习率0.001比Critic学习率更低避免策略反复横跳Critic 学习率0.005比Actor高一些让评价更早跟上当前策略模型网络学习率0.001预训练和在线微调都用这个值隐藏层神经元数32当前问题足够复杂问题可加到64或128控制量上限0.5和环境匹配由Actor输出层的缩放系数决定Critic 目标网络软更新系数 tau0.01每步向Critic网络参数靠拢1%主循环epoch数2000训练早期损失下降明显后期振荡减小为什么Actor学习率要比Critic低因为如果Actor大步更新策略会剧烈变化而Critic的估计还是基于旧策略的二者之间会形成恶性循环。把Actor步子放小一点Critic有足够时间追上策略变化训练稳定很多。3.3 训练效果怎么判断训练过程中我主要盯三个指标。第一是Critic损失。理想情况下它会先快速下降然后在一个小范围内波动大体趋势是收敛的。如果出现持续增大或剧烈震荡优先检查TD目标是否错误地通过梯度流回了目标网络。第二是Actor输出的规律性。训练完成后我直接用测试脚本观察当x0时Actor输出应该非常接近0当x为正时u为负x为负时u为正。这是这个系统的稳定控制器应该有的行为符号反了就是策略学反了。第三是闭环控制效果。等训练完成后我用训练好的Actor做闭环控制从初始状态 x1.5 出发状态能在几步之内被拉回接近0而且控制量有界、没有高频抖动。如果出现状态发散或者控制量在边界反复横跳就说明策略网络不稳定。我自己跑这个一阶系统时大概训练到第600个epoch左右从x1.5出发的闭环轨迹已经能在三步内收敛到0.1以内。整个训练过程在笔记本CPU上也就一分多钟没有任何性能压力。4. 常见问题与调试技巧实录4.1 Critic和Actor“互相打架”这是我在调试过程中遇到最典型的问题。现象是Critic损失一开始下降但到了训练中段开始震荡Actor输出的策略曲线在相近状态附近出现明显不连续。经过排查根因是Critic目标值里混入了当前网络自身的梯度。也就是说Critic在更新时目标值不是固定的而是不断跟着网络参数漂移的。自举本身不是问题但目标值漂移速度过快就会放大误差。解决办法就是前面提到的用目标网络或者detach()切断梯度。另外一个辅助手段是经验回放。我把最近2000条状态转移样本存进一个列表每次从里面随机采样32条批量训练Critic。这个方法可以有效打破相邻样本之间的相关性让Critic不会因为一串按时间顺序到达的样本而出现局部过拟合。4.2 模型网络误差导致策略发散有一次我加宽了状态采样范围从 [-2, 2] 扩大到了 [-5, 5]但没有增加模型网络的训练数据量。结果模型网络在状态绝对值大的区域拟合误差明显上升训练出来的Actor在那些区域给出了完全错误的控制方向。这说明模型网络的训练数据覆盖范围必须和控制器实际工作的状态范围匹配。如果你的应用场景要求控制器在大范围状态空间工作模型网络就必须覆盖足够大的采样范围并且在这个范围内的拟合误差要足够小。发现问题后我调整了采样分布保证整个工作区间内都有足够密度的训练数据再重新训练模型网络策略就恢复了正常。4.3 调试问题速查表现象可能原因排查方法Critic损失不下降Critic更新时的目标值没有detach检查目标值计算是否切断梯度改为使用目标网络策略发散状态越来越远模型网络预测误差大检查模型网络在运行轨迹范围内的预测误差增强样本覆盖Actor输出在相近状态下差异大Actor学习率过高把Actor学习率降为Critic的1/5甚至1/10控制量在限幅边界抖动代价函数里控制代价权重太低增大 r_t 里 u^2 的权重训练后期小幅度震荡Critic和Actor交替更新频率失衡软更新目标网络参数增加经验回放批大小不同随机种子结果差异大没有固定随机种子固定torch.manual_seed并统一初始化方式4.4 一个容易被忽视的调参习惯我强烈建议在开始训练前就固定随机种子。ADP这种有三个网络交替更新的算法对初始化和采样路径比较敏感同一个参数配置在不同种子下可能收敛结果差异不小。固定种子之后你每次调参观察到的变化才是真实由参数引起的而不是随机波动。另外我在调试时习惯把训练过程里的关键数值落盘记录下来比如每个epoch的Critic损失、模型网络误差、当前策略在某几个固定状态下的输出值。这些数值曲线能非常直观地反映训练健康度比直接看最终的收敛结果更有诊断价值。5. 一些后续扩展思路把这条一阶系统的ADP链路跑通之后扩展方向其实非常多。你可以把系统换成桌面倒立摆通过步长积分得到离散动力学然后用同样的代码结构去训练一个平衡控制器。也可以在HDP基础上升级成DHP额外拟合值函数对状态的偏导收敛速度通常会有明显提升。还可以在训练中加入探索噪声。用初始随机的Actor跑数据时加入均值为0、方差逐渐衰减的高斯噪声可以让模型网络和Critic的采样覆盖更全面减少早期策略过于单一导致的偏差。我在一阶系统上没有加噪声也能收敛但明显感觉到加噪声之后训练过程对初始随机种子的敏感度更低。在更复杂的环境中这一步基本上是标配。如果后续要做真实系统的在线控制可以考虑在多轮迭代中每隔一段时间重新采集一批真实数据来微调模型网络让模型跟踪系统参数漂移。这个方法在工程落地时比纯离线训练要可靠得多。最后再分享一个小体会ADP虽然听起来是新东西但它骨子里的思路和上世纪就有的动态规划是一脉相承的。用Python实现一遍之后你会发现真正值的项目不是堆砌神经网络结构而是把贝尔曼方程里那条“当前代价加未来代价”的链路用代码原汁原味地实现出来。只要这条链路清晰了中间换什么网络结构、调什么参数都是在树干上长叶子和分枝。建议你手头先跑通这个最简单的例子哪怕只是看看损失曲线怎么变化也会对ADP的工作原理有完全不同的理解。本文还有配套的精品资源点击获取