免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

自适应动态规划Python实现:从贝尔曼方程到调参实战

自适应动态规划Python实现:从贝尔曼方程到调参实战 简介这是一份使用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最常见的实现是HDPHeuristic Dynamic Programming启发式动态规划结构它由三个模块组成模型网络、评判网络、执行网络。听名字唬人其实分别对应“预测系统下一步状态”、“评估当前状态的价值”、“给出控制指令”。模型网络输入当前状态和控制量输出下一时刻状态。相当于对真实系统的一个可导的替身。评判网络输入当前状态输出代价函数值也叫值函数目标是逼近贝尔曼方程右侧的真实期望回报。执行网络输入当前状态输出控制量目标是让评判网络给出的值函数最小。三个网络交替训练就是ADP的核心循环。我第一次看到这个结构时觉得它简直像一个三角恋爱——三个网络互相牵制、互相依赖但正是这种互相博弈让系统最终收敛到最优解。1.2 为什么选择神经网络作为逼近器有人会问值函数为什么不能用查表法在小规模离散状态空间里完全可以但只要状态连续查表就失效了。神经网络的优势在于它是一个万能逼近器理论上只要宽度和深度足够就能拟合任意连续函数。Python生态里定义网络只需要几行代码比如下面这个评判网络import torch.nn as nn class CriticNetwork(nn.Module): def __init__(self, state_dim, hidden_dim64): 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_{t1}然后最小化预测误差。这里有一个细节我特别想提醒采样数据必须覆盖工作状态空间的各个区域如果只在平衡点附近采样模型网络在其他区域的预测会严重失真直接影响后续训练。下面是一个模型网络的简单实现用了一个两层的MLPclass ModelNetwork(nn.Module): def __init__(self, state_dim, action_dim, hidden_dim128): 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_{t1})其中r是即时代价gamma是折扣因子J是值函数。实际操作中我们会用当前评判网络在s_{t1}上的输出作为目标值的一部分然后反过来更新评判网络。这个方式和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, gamma0.95, lr1e-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(), lrlr) self.actor_optimizer torch.optim.Adam(self.actor_net.parameters(), lrlr) self.model_optimizer torch.optim.Adam(self.model_net.parameters(), lrlr) # 损失函数 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, episodes500, steps50): 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(fEpisode {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的评判网络拟合精度不够可以试试DHPDual Heuristic Programming它不再拟合值函数本身而是直接拟合值函数对状态的梯度。DHP的更新目标推导更复杂一个直觉上的好处是梯度信息对控制的指导更直接因此控制精度往往更高。代价是代码复杂度上升而且训练的不稳定性也会同步增加。另一个值得尝试的方向是把ADP和模型预测控制MPC结合用ADP离线学一个近似的值函数和策略作为MPC的初始解和终端代价在线运行时用MPC做短时域精确优化既兼顾了ADP的全局性又利用了MPC的约束处理能力。这个混合框架在我自己的实验里效果出奇地好系统响应又快又稳推荐有兴趣的朋友试一试。如果你也在用Python调ADP卡在某个奇怪的bug上出不来欢迎在评论区把你的状况写出来。我看了能帮上忙的一定会回。本文还有配套的精品资源点击获取
返回列表