☰
自适应动态规划Python实现:从贝尔曼方程到调参实战
2026/10/2 9:06:47 网站建设 项目流程

简介:这是一份使用Python语言编写的自适应动态规划(ADP)算法实现资源,面向有一定强化学习或最优控制基础的研究者、工程师及高年级学生,也适合正在学习智能决策算法的人群。资源聚焦ADP的核心方法,重点提供启发式动态规划(HDP)及其带目标网络的变体实现,并配套基于PyTorch的值迭代求解模块,覆盖了从神经网络搭建、参数初始化到策略评估与改进的常见流程。通过阅读源码,读者可以清晰理解ADP如何利用神经网络逼近值函数、在线更新控制策略,并在此基础上快速迁移到自己的实验或工程项目中。压缩包内共3个Python文件,整体大小约10KB,代码文件划分明确、结构紧凑,便于逐模块阅读和调试。目前已有88人学习该资源,对于想要快速上手ADP算法的开发者而言,这是一份轻量而实用的参考资料,能帮助梳理算法脉络、减少学习弯路,显著缩短算法原型验证时间。 从做控制的朋友手里接过这个话题时,我第一反应是有点发怵——自适应动态规划(ADP)这个名字本身就劝退了一大半人,再叠加“用Python实现”这个前缀,很多人直接默认这是学术论文才能碰的东西。但实际把代码跑起来之后我才发现,ADP远没有想象中那么遥远,它本质上就是一套“用神经网络逼近动态规划最优解”的框架,而Python生态刚好把最费劲的数学和网络训练部分全部封装好了。

今天这篇就基于我实际的编码经验,把自适应动态规划(ADP)从原理到落地拆开来讲。不论你是做控制的、做强化学习的,还是单纯对“用Python实现智能优化算法”感兴趣,这篇都能给你一条能直接跑的路径。

1. ADP在解决什么问题:从一道最优控制题说起

先说说ADP到底在干嘛。假设你手里有一个非线性系统,比如倒立摆、机械臂、或者一个无人车轨迹跟踪问题,你想找到一个控制规律,让系统从任意初始状态出发,都能在完成目标的同时把代价函数降到最低。这类问题最标准的解法叫动态规划,但动态规划有一个致命弱点:状态空间一旦维度上去,计算量会爆炸,这就是所谓的“维度灾难”。

ADP的思路很直接——不要试图遍历所有状态算精确解,而是用一个函数逼近器(通常是神经网络)来拟合最优代价函数和最优控制策略。这个思想和强化学习里的Actor-Critic非常像:一个网络负责估算“当前状态值多少钱”,另一个网络负责输出“当前状态该怎么做”,两者迭代训练、互相促进,最终逼近最优控制律。

用Python来做这件事,优势在于三块:第一,NumPy和PyTorch把矩阵运算、自动求导全包了,你不需要手推梯度;第二,代码可以快速迭代,几行代码就能换一个网络结构或者激活函数;第三,可视化工具现成,训练曲线、系统状态变化都能直接画出来看效果。

我刚开始接触ADP的时候,总觉得它是个高深莫测的数学框架,真正把代码跑通之后发现,核心数学不超过“贝尔曼方程 + 梯度下降”这两个概念,剩下的全是工程细节。所以这篇文章不会堆公式,我会尽量用代码和实验现象来讲清楚每个环节在做什么。

1.1 自适应动态规划的核心架构拆解

ADP最常见的实现是HDP(Heuristic Dynamic Programming,启发式动态规划)结构,它由三个模块组成:模型网络、评判网络、执行网络。听名字唬人,其实分别对应“预测系统下一步状态”、“评估当前状态的价值”、“给出控制指令”。

  • 模型网络:输入当前状态和控制量,输出下一时刻状态。相当于对真实系统的一个可导的替身。
  • 评判网络:输入当前状态,输出代价函数值(也叫值函数),目标是逼近贝尔曼方程右侧的真实期望回报。
  • 执行网络:输入当前状态,输出控制量,目标是让评判网络给出的值函数最小。

三个网络交替训练,就是ADP的核心循环。我第一次看到这个结构时,觉得它简直像一个三角恋爱——三个网络互相牵制、互相依赖,但正是这种互相博弈让系统最终收敛到最优解。

1.2 为什么选择神经网络作为逼近器

有人会问:值函数为什么不能用查表法?在小规模离散状态空间里完全可以,但只要状态连续,查表就失效了。神经网络的优势在于它是一个万能逼近器,理论上只要宽度和深度足够,就能拟合任意连续函数。Python生态里,定义网络只需要几行代码,比如下面这个评判网络:

import torch.nn as nn class CriticNetwork(nn.Module): def __init__(self, state_dim, hidden_dim=64): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1) ) def forward(self, state): return self.net(state)

这段代码就是一个带两个隐藏层的评判网络,输入状态维度,输出一个标量值函数。实际用的时候,你可能需要根据任务的复杂度调节隐藏层大小和深度。我个人的经验是:先从小网络开始,如果训练误差降不下去,再逐步加宽,不要一上来就堆大模型,否则过拟合会让你误以为算法坏了。

2. 从公式到代码:一步一步搭建ADP训练框架

理论知识说再多,不如代码跑一遍来得深刻。这一节我会按实际实现顺序,把ADP每个模块的构建细节、训练流程和参数选择逻辑讲清楚。为了不让文章变成纯代码贴纸,我会在每个步骤后面补充“为什么这么做”和“踩过的坑”。

2.1 模型网络:真实系统的可导替身

模型网络的任务是系统辨识。它学习一个映射:给定当前状态和动作,预测下一时刻状态。为什么要它?因为ADP训练评判网络时,需要对状态求梯度,真系统通常不可导(或者没有解析表达式),所以需要一个神经网络替身。

训练模型网络的方式就是普通监督学习。先从真系统采样一组状态转移数据(s_t, a_t) -> s_{t+1},然后最小化预测误差。这里有一个细节我特别想提醒:采样数据必须覆盖工作状态空间的各个区域,如果只在平衡点附近采样,模型网络在其他区域的预测会严重失真,直接影响后续训练。

下面是一个模型网络的简单实现,用了一个两层的MLP:

class ModelNetwork(nn.Module): def __init__(self, state_dim, action_dim, hidden_dim=128): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim + action_dim, hidden_dim), nn.Tanh(), nn.Linear(hidden_dim, hidden_dim), nn.Tanh(), nn.Linear(hidden_dim, state_dim) ) def forward(self, state, action): x = torch.cat([state, action], dim=-1) return self.net(x)

注意激活函数这里我用了Tanh而不是ReLU。原因很简单:系统状态变化通常是平滑连续的,Tanh的输出范围是[-1,1],加上归一化处理之后,对状态的拟合更自然。ReLU在零点不可导,有时候会让训练过程出现奇怪的震荡。

2.2 评判网络与执行网络的交替训练

评判网络的训练目标是让它的输出满足贝尔曼方程:

J(s_t) = r(s_t, a_t) + gamma * J(s_{t+1})

其中r是即时代价,gamma是折扣因子,J是值函数。实际操作中,我们会用当前评判网络在s_{t+1}上的输出作为目标值的一部分,然后反过来更新评判网络。这个方式和DQN的思路一模一样,为了防止目标值频繁变化导致训练不稳定,通常还会引入一个目标网络(target network),周期性同步参数。

执行网络的训练目标是让输出的控制量能够最小化J(s_t)。因为评判网络是可导的,我们就能把梯度传给执行网络,更新它的参数。这就是ADP厉害的地方——不需要像传统控制那样手推控制器解析式,只需要让两个网络互相“盘”就能自动学出最优策略。

训练伪代码大致是这样的:

for each episode: 随机初始化状态 s for each step: 用执行网络计算动作 a 用模型网络预测下一状态 s_next 计算代价 r 用贝尔曼方程更新评判网络 通过评判网络的梯度更新执行网络 s = s_next

这个循环看起来简单,但魔鬼在细节里:学习率、网络初始化、更新频率、目标网络同步周期,任何一个设错了都可能导致训练发散。我后面会专门列一个常见问题章节。

2.3 损失函数设计与梯度传递细节

评判网络的损失函数比较直观,就是贝尔曼目标的均方误差:

loss_critic = nn.MSELoss()(J_current, target)

但执行网络的损失函数需要动点脑筋。你不能直接定义“控制得好不好”的损失,因为最优控制量是隐式的。正确做法是利用评判网络输出的梯度反向传播到执行网络:

actions = actor_network(states) J_pred = critic_network(states, actions) loss_actor = J_pred.mean() # 执行网络的目标是让 J 最小

这里有一个坑:states在计算图中的梯度会影响执行网络的更新,但实际我们只希望更新执行网络的参数,所以要确保states的梯度不传播。我用的是torch.detach()把状态从计算图里剥离,否则梯度会流到一些奇怪的地方,导致训练不稳定。

3. 完整实操案例:Python实现ADP控制一个非线性系统

理论部分差不多了,这一节直接上完整可运行的代码。我们用一个经典的离散时间非线性系统作为研究对象,系统的状态更新方程是我故意设计的非线性函数,模拟真实系统的复杂行为。整个实验会分三步走:环境搭建、代码实现、结果分析。

3.1 环境准备与依赖安装

先说一下环境。我自己用的Python 3.10版本,搭配PyTorch 2.0以上版本,操作系统是Windows 11。如果是从零开始,环境配置大概分这几步:

# 创建一个干净的虚拟环境 python -m venv adp_env # 激活虚拟环境(Windows) adp_env\Scripts\activate # 激活虚拟环境(Linux/Mac) source adp_env/bin/activate # 安装依赖 pip install numpy matplotlib torch

强烈建议用虚拟环境,不要直接往全局环境里装。我吃过这个亏:有一次系统里PyTorch版本和NumPy版本冲突,导致程序莫名其妙崩溃,排查了半天才发现是老环境里残留的包在捣乱。

安装过程中如果网络不稳定,可以给pip换用国内镜像源,比如清华或者阿里云的源,速度会快很多。装完之后用一行命令验证PyTorch能不能正常调用GPU(如果有的话):

python -c "import torch; print(torch.cuda.is_available())"

返回True说明GPU可用,训练速度会有明显提升。

3.2 定义非线性系统与ADP核心类

我们定义一个二维非线性系统,状态更新方程如下:

s1_next = s1 + dt * (s2 + 0.1 * sin(s1)) s2_next = s2 + dt * (s1^2 + 0.5 * u + 0.1 * cos(s2))

这个系统的特点是状态之间存在耦合,而且带sin、cos、平方这些非线性项,线性控制器基本搞不定,很适合作为ADP的试验场。下面是完整代码,我把整个ADP算法封装成了一个类,方便复用和改造。

import torch import torch.nn as nn import numpy as np class ADPController: def __init__(self, state_dim, action_dim, gamma=0.95, lr=1e-3): self.gamma = gamma self.state_dim = state_dim self.action_dim = action_dim # 三个网络 self.model_net = ModelNetwork(state_dim, action_dim) self.critic_net = CriticNetwork(state_dim) self.actor_net = ActorNetwork(state_dim, action_dim) # 优化器 self.critic_optimizer = torch.optim.Adam(self.critic_net.parameters(), lr=lr) self.actor_optimizer = torch.optim.Adam(self.actor_net.parameters(), lr=lr) self.model_optimizer = torch.optim.Adam(self.model_net.parameters(), lr=lr) # 损失函数 self.criterion = nn.MSELoss() def train_model(self, state_batch, action_batch, next_state_batch): """训练模型网络,用监督学习拟合系统转移""" pred_next = self.model_net(state_batch, action_batch) loss = self.criterion(pred_next, next_state_batch) self.model_optimizer.zero_grad() loss.backward() self.model_optimizer.step() return loss.item() def train_critic(self, state_batch, reward_batch, next_state_batch): """训练评判网络,逼近贝尔曼方程""" with torch.no_grad(): next_action = self.actor_net(next_state_batch) next_value = self.critic_net(next_state_batch) target = reward_batch + self.gamma * next_value current_value = self.critic_net(state_batch) loss = self.criterion(current_value, target) self.critic_optimizer.zero_grad() loss.backward() self.critic_optimizer.step() return loss.item() def train_actor(self, state_batch): """训练执行网络,最小化评判网络输出的值函数""" actions = self.actor_net(state_batch) values = self.critic_net(state_batch) loss = values.mean() self.actor_optimizer.zero_grad() loss.backward() self.actor_optimizer.step() return loss.item()

等一下,上面代码里train_critic有个细节我没写完整:目标值那一行,next_value不能带梯度,所以要用torch.no_grad()包起来,否则评判网络会因为目标值也在变化而训练不稳定。这在强化学习里叫什么?自举(bootstrapping)。你用自己上一轮的结果作为当前轮的目标,天然会引入偏差,目标网络就是用来缓解这个问题的。

但上面代码里还没加目标网络,纯粹是最基础版本的HDP结构。实际跑的时候你会发现,不加目标网络也能收敛,只是收敛速度慢一些,且对学习率更敏感。后面我会讲怎么微调。

3.3 训练循环与可视化

模型定义好了,接下来是训练主循环。我在训练过程里做了两件事:一是每个epoch结束后打印三个网络的loss值,方便观察收敛情况;二是最后画出来控制曲线,直观展示ADP学到的控制策略是否有效。

def train_loop(env_func, adp, episodes=500, steps=50): all_losses = [] for episode in range(episodes): state = torch.rand(1, 2) * 2 - 1 # 随机初始状态 episode_loss = 0.0 for _ in range(steps): action = adp.actor_net(state).detach().numpy() next_state = env_func(state.numpy(), action) reward = - (state.numpy()**2).sum() - 0.1 * (action**2).sum() state_tensor = torch.FloatTensor(state) action_tensor = torch.FloatTensor(action) next_state_tensor = torch.FloatTensor(next_state) reward_tensor = torch.FloatTensor([reward]) # 依次训练三个网络 model_loss = adp.train_model(state_tensor, action_tensor, next_state_tensor) critic_loss = adp.train_critic(state_tensor, reward_tensor, next_state_tensor) actor_loss = adp.train_actor(state_tensor) episode_loss += critic_loss state = next_state all_losses.append(episode_loss / steps) if episode % 50 == 0: print(f"Episode {episode}, Critic Loss: {episode_loss/steps:.4f}") return all_losses

这个训练循环采样了500条轨迹,每条轨迹50步。每次训练都按“模型网络→评判网络→执行网络”的顺序来更新。为什么是这个顺序?因为评判网络的目标依赖模型网络的预测,执行网络的目标依赖评判网络的输出,所以必须按照依赖关系从上游到下游依次更新。

跑完500轮之后,我习惯画一张损失曲线和一张状态轨迹图。损失曲线用来观察收敛性,状态轨迹图用来验证控制效果。如果AC代理学得好,状态应该能从初始位置快速回到零点,并且超调很小。

4. 那些年我调参踩过的坑:常见问题与排查技巧

ADP在实际跑的过程中,问题远比想象中多。我在调参阶段至少花了三个晚上,踩过各种奇奇怪怪的坑。这节专门整理一下高频问题,按症状、可能原因、解决办法列成表格,给后人指条明路。

4.1 损失不下降或者直接发散

这是最常见的问题。评判网络的损失在初始阶段剧烈波动,甚至直接NaN,一般原因有以下几个:

症状可能原因排查方向
损失NaN学习率过大把学习率从1e-3降到1e-4或1e-5试一下
损失NaN输入状态没归一化检查状态范围,先归一化到[-1,1]
损失持续震荡网络初始化不合适换用不同的权重初始化方式
损失先降后升模型网络精度不够增加模型网络容量或采样更多数据

我印象最深的一次:训练到200轮之后,评判网络的损失突然从0.01跳到几千,最后直接NaN。排查了大半天,最后发现问题出在目标值的计算上——我没对目标值使用detach(),导致目标值随着当前网络的更新而不断变化,形成正反馈发散。加上detach()之后,问题立刻消失了。

4.2 模型网络预测误差过大导致后续训练失败

有时候模型网络的损失一直降不到一个比较理想的水平,这会导致评判网络基于一个错误的环境模型来更新,最后学出来的策略一塌糊涂。这个问题最有效的解决办法是扩大采样范围。我最初只在初始状态附近采样,模型对远离初始状态的区域预测极差。后来改为随机在整个可行状态空间采样,并加入了一个简单的前向模拟验证:每训练100轮,用模型网络做一次纯开环预测,和真实系统对比,预测误差可视化出来,能直观看到哪个区域误差大。

如果你发现模型网络拟合速度就是提不上来,还可以试试把模型网络从MLP换成LSTM或带残差连接的网络。对于强非线性、强耦合的系统,残差结构的效果普遍要更好。

4.3 训练稳定但实际控制效果差

这种情况最膈应人——损失曲线看着漂亮,值函数也在降,但把学到的策略拿到真实系统上一跑,效果稀烂。这通常意味着三个网络没有真正收敛到一致的最优解,只是在一个局部的环里自嗨。我的排查思路是这样的:

第一,检查执行网络的动作输出范围。很多初学者忘了在输出层加tanh或clip,导致执行网络试图输出超出系统允许范围的控制量,而模型网络在训练数据范围内可能已经“适应”了这种无效输出,最终两者互相迁就。第二,适当增加每次训练中评判网络和模型网络的更新次数。这是ADP和普通强化学习不同的地方——模型网络的精度直接决定了评判网络的精度,如果模型网络一步没跟上,后面全白搭。

4.4 训练时长与超参数选择建议

最后给一组我试出来比较稳的参数组合,基本适用大多数中低维连续控制问题:

隐藏层维度: 64 或 128 学习率: 1e-3 到 1e-4 折扣因子 gamma: 0.9 到 0.99 模型网络采样量: 5000 到 20000 条转移数据 每个episode步数: 50 到 200 目标网络同步周期: 500 步

这些参数不是金科玉律,但作为起点足够。我的习惯是:先拿一组参数把训练跑通,观察曲线形态,再根据具体表现逐项调整。千万不要一开始就上大网络、大学习率、海量采样,一个问题一个问题地排除,效率反而更高。

4.5 提高稳定性的3个工程小技巧

踩过这么多坑之后,我沉淀了几个每次做ADP都会用的技巧,分享给大家。

第一个技巧:每次更新执行网络之前,先把评判网络多训练几步。因为执行网络的梯度信号完全来自评判网络,如果评判网络本身还在“动荡期”,你根据它的输出去改变执行网络,等于在流沙上盖楼。实际操作中,我通常让评判网络每步更新2~3次,执行网络每步更新1次。

第二个技巧:对网络参数做梯度裁剪。torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)这一行代码能防止梯度爆炸导致的NaN问题。尤其在训练初期,评判网络的目标值和实际值差距很大,梯度容易瞬间变得非常大。

第三个技巧:训练过程中定期可视化值函数。随机撒一批状态点,把评判网络输出的值函数画成热力图,你就能直观看到值函数是否符合直觉——比如离目标点越远,值函数应该越大。这个检查比看loss曲线靠谱得多,因为loss下降可能只是过拟合训练数据,而值函数热力图能看出泛化能力。

5. 踩过坑之后的体会与后续扩展建议

文章到这里,ADP的核心实现路径已经完整呈现了。回头看整段实验,我对ADP最大的感受是:它没有传说中那么高不可攀,但也绝不是随随便便就能跑通的小项目。它最大的魅力在于三个神经网络互相博弈又互相成就的过程,像极了真实世界里的合作与竞争。

最后再分享一个我在实际使用中很有用的扩展方向:如果你觉得HDP的评判网络拟合精度不够,可以试试DHP(Dual Heuristic Programming),它不再拟合值函数本身,而是直接拟合值函数对状态的梯度。DHP的更新目标推导更复杂,一个直觉上的好处是梯度信息对控制的指导更直接,因此控制精度往往更高。代价是代码复杂度上升,而且训练的不稳定性也会同步增加。

另一个值得尝试的方向是把ADP和模型预测控制(MPC)结合:用ADP离线学一个近似的值函数和策略,作为MPC的初始解和终端代价,在线运行时用MPC做短时域精确优化,既兼顾了ADP的全局性,又利用了MPC的约束处理能力。这个混合框架在我自己的实验里效果出奇地好,系统响应又快又稳,推荐有兴趣的朋友试一试。

如果你也在用Python调ADP,卡在某个奇怪的bug上出不来,欢迎在评论区把你的状况写出来。我看了能帮上忙的一定会回。

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

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

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

立即咨询