简介:这份资源面向强化学习与自动控制方向的学习者和研究者,提供在MATLAB环境中用DDPG算法控制水箱系统的完整实现。DDPG属于Actor-Critic架构的深度强化学习方法,适合处理连续动作空间问题,资源通过Actor网络生成控制策略、Critic网络评估状态动作价值,并结合经验回放与探索噪声完成训练,最终让水位稳定在期望范围内。压缩包共2个文件,包含1个slx与1个m文件,整体约45KB:Simulink模型负责搭建水箱物理系统、模拟水位与流量变化并提供状态反馈,MATLAB脚本则实现网络结构、学习率、噪声策略与更新规则等核心逻辑。目前已有403人学习。读者可据此理解强化学习与Simulink联合仿真的完整流程,掌握参数调整、网络结构修改与奖励函数设计思路,并在此基础上迁移到其他连续控制场景,具备较强的工程参考价值。
1. 从“调参调到怀疑人生”说起:这套 DDPG 水箱控制资源到底能干什么
如果你做过水箱液位控制,大概率经历过这种场景:PID 参数整定了一下午,超调还是压不住,一加水就震荡,一放水就掉得飞快。传统控制方法在非线性、大滞后、强耦合的水箱系统里,往往需要反复试凑,换个工况就得重新来一遍。这套基于 MATLAB 的 DDPG 水箱控制资源,解决的正是这个问题——用深度确定性策略梯度算法,让智能体自己学会怎么开水阀、关水阀,把液位稳在目标值附近。
资源本身是一套完整的 MATLAB 工程,包含水箱环境建模、DDPG 智能体定义、训练脚本和仿真验证四个部分。适合两类人:一类是正在学强化学习、想找一个连续控制落地案例的学生或转行者;另一类是做过程控制、想看看无模型方法到底能不能替代 PID 的工程师。它不要求你先把《强化学习导论》啃完,但需要你对 MATLAB 的基本操作和 Simulink 有概念,否则连环境都跑不起来。
我拿到这套东西的第一反应是:终于不用自己从零搭水箱模型了。但跑通之后发现,真正值钱的地方不在模型本身,而在训练脚本里那几个容易被忽略的参数——它们直接决定智能体是学会控制还是学会摆烂。
2. DDPG 控制水箱的底层逻辑:为什么选它而不是 PID 或 Q-learning
2.1 水箱系统的连续控制特性与 DDPG 的匹配点
水箱液位控制的核心变量是阀门开度,这是一个连续量。你可以把阀门开到 37.2%,也可以开到 37.3%,动作空间是连续的。如果用 Q-learning 这类传统强化学习,动作必须离散化——比如把阀门分成“全开、半开、全关”三档。三档控制精度太粗,分一百档又会导致 Q 表爆炸,训练效率断崖式下降。
DDPG 的设计恰好绕开了这个矛盾。它用两个网络:Actor 网络直接输出连续动作值,Critic 网络评估这个动作好不好。Actor 不需要遍历所有可能的动作,而是根据当前状态直接给出一个确定性的阀门开度。这个“确定性”很关键——水箱控制不需要随机探索,它要的是稳定输出。
另一个匹配点是水箱的滞后性。你打开阀门,液位不会立刻变化,要等水流进去、传感器读数更新。DDPG 的经验回放机制(Replay Buffer)把历史状态-动作-奖励存起来,训练时随机采样,打破了时间相关性。这相当于让智能体在“记忆”里反复练习,而不是被当前这一刻的滞后带偏。
2.2 奖励函数设计:一个让智能体“听话”的关键公式
奖励函数是 DDPG 训练里最玄学的一环。这套资源里用的奖励函数形式大致是这样的:
% 奖励函数核心逻辑(基于资源中的实现整理) function reward = computeReward(currentLevel, targetLevel, action, prevAction) % 液位偏差惩罚:偏差越大,惩罚越重 levelError = abs(currentLevel - targetLevel); errorPenalty = -10 * levelError^2; % 动作变化率惩罚:抑制阀门频繁抖动 actionChange = abs(action - prevAction); smoothPenalty = -0.5 * actionChange; % 稳态奖励:偏差小于阈值时给正奖励 if levelError < 0.02 steadyReward = 5; else steadyReward = 0; end reward = errorPenalty + smoothPenalty + steadyReward; end这段代码的逻辑是:偏差平方惩罚让智能体优先把液位拉回目标值,动作变化率惩罚防止阀门来回抖,稳态奖励给一个“到位了就有糖吃”的正反馈。三个项的量级需要匹配——如果误差惩罚系数设成 -100,智能体就会过度保守,阀门几乎不动;如果设成 -1,它又可能放任液位飘走。
我一般会先把误差惩罚系数设为 -10,动作变化率惩罚设为 -0.5,稳态奖励设为 5,跑 200 个 episode 看曲线。如果液位曲线像心电图一样上下跳,说明动作惩罚太弱;如果液位半天不动,说明误差惩罚太强,智能体被吓住了。
2.3 网络结构与超参数的选型依据
资源里的 Actor 和 Critic 都是三层全连接网络。Actor 的隐藏层神经元数是 64 和 32,Critic 是 128 和 64。这个配置不是拍脑袋来的——水箱系统的状态维度低(通常就液位、流速几个量),动作维度是 1,网络太深容易过拟合,太浅又学不到非线性关系。
超参数方面,学习率用的是 Actor 1e-4、Critic 1e-3。Critic 学得快一点是常见做法,因为它负责评估,需要更快跟上 Actor 的变化。软更新系数 tau 设为 0.001,意味着目标网络每步只更新千分之一,保持训练稳定。经验回放池大小是 1e6,批量大小 128。
注意:如果你的电脑内存小于 16GB,把回放池调到 1e5 就够了。1e6 的池子会吃掉大量内存,训练后期容易卡顿。
3. 从零跑通训练脚本:环境配置、参数修改与训练监控
3.1 MATLAB 版本要求与工具箱检查
这套资源依赖 Reinforcement Learning Toolbox 和 Deep Learning Toolbox。我实测在 MATLAB R2021b 上跑通,R2020a 缺少部分 DDPG 智能体函数,会报错。检查工具箱的命令很简单:
% 检查必需工具箱是否安装 ver('reinforcement') % 应返回 Reinforcement Learning Toolbox 版本信息 ver('deeplearning') % 应返回 Deep Learning Toolbox 版本信息如果返回空,说明没装。在 MATLAB 的“附加功能”里搜索安装即可。注意 Deep Learning Toolbox 需要配套的深度学习后端,CPU 训练也能跑,但速度大概是 GPU 的十分之一。水箱模型不大,CPU 跑 500 个 episode 大约 40 分钟,GPU 只要 4 分钟。
3.2 水箱环境接口的四个关键方法
资源里的水箱环境是一个自定义的 MATLAB 类,继承自rl.env.MATLABEnvironment。它必须实现四个方法:reset、step、getObservation、isDone。我拆开看了一下,核心逻辑集中在step方法里:
function [nextObs, reward, isDone, loggedSignals] = step(this, action) % action 是阀门开度增量,范围 [-1, 1] % 限制动作范围,防止阀门开度越界 action = max(min(action, 1), -1); % 更新阀门开度(带惯性,模拟真实阀门响应) this.ValveOpening = this.ValveOpening + 0.1 * action; this.ValveOpening = max(min(this.ValveOpening, 1), 0); % 水箱液位动态方程(简化的一阶模型) inflow = this.ValveOpening * this.InflowRate; outflow = this.OutflowCoeff * sqrt(max(this.Level, 0)); this.Level = this.Level + (inflow - outflow) * this.Ts; % 计算奖励 reward = computeReward(this.Level, this.TargetLevel, ... this.ValveOpening, this.PrevValveOpening); % 更新观测 nextObs = this.getObservation(); % 判断是否结束(液位溢出或排空) isDone = this.Level > 1.0 || this.Level < 0.0; loggedSignals.Level = this.Level; loggedSignals.ValveOpening = this.ValveOpening; end这里有几个参数值得注意:Ts是采样时间,默认 0.1 秒;InflowRate是最大入水速率,默认 1.0;OutflowCoeff是出水系数,默认 0.5。如果你要模拟更大的水箱,把Ts调小到 0.05,同时把训练 episode 长度翻倍,否则智能体来不及看到液位变化。
3.3 训练脚本的参数修改与启动
训练脚本的结构很清晰:创建环境、创建 DDPG 智能体、配置训练选项、调用train函数。我一般会改三个地方:
% 训练参数配置(基于资源中的脚本整理) numEpisodes = 500; % 训练轮数,建议 300-1000 maxStepsPerEpisode = 200; % 每轮最大步数,对应 20 秒仿真 agentOpts.SampleTime = 0.1; % 智能体采样时间,与环境 Ts 一致 % 训练选项 trainOpts = rlTrainingOptions(... 'MaxEpisodes', numEpisodes, ... 'MaxStepsPerEpisode', maxStepsPerEpisode, ... 'ScoreAveragingWindowLength', 20, ... % 平滑奖励曲线 'Verbose', true, ... % 打印训练进度 'Plots', 'training-progress'); % 实时画图 % 启动训练 trainingStats = train(agent, env, trainOpts);ScoreAveragingWindowLength设为 20 意味着奖励曲线是最近 20 轮的平均值,这样能过滤掉单轮的随机波动。如果你看到平均奖励在 100 轮后还在 -50 以下,说明奖励函数或网络结构有问题,别硬跑,先回头调参数。
3.4 训练过程监控:看什么曲线、怎么判断收敛
训练窗口里会显示两条曲线:Episode Reward 和 Average Reward。健康的训练曲线应该是:前 50 轮奖励快速上升(智能体在探索),50 到 200 轮缓慢上升(在优化策略),200 轮后趋于平稳(收敛)。如果曲线一直震荡不上升,常见原因是学习率太大或奖励函数量级不匹配。
还有一个隐藏指标是 Average Q-value,在训练窗口里默认不显示,需要手动加一行代码:
% 在训练选项中开启 Q-value 记录 trainOpts = rlTrainingOptions(... 'Plots', 'training-progress', ... 'SaveAgentCriteria', 'EpisodeReward', ... 'SaveAgentValue', -10); % 奖励超过 -10 就保存智能体Q-value 代表 Critic 对当前策略的估值。如果 Q-value 一直下降,说明 Critic 认为策略越来越差,通常是 Actor 更新太快导致的。这时候把 Actor 学习率降到 5e-5 试试。
4. 避坑指南:训练不收敛、液位震荡、仿真报错的排查清单
4.1 现象:奖励曲线一直趴在 -200 不动
原因:奖励函数里的误差惩罚系数太大,智能体每一步都被重罚,干脆选择“摆烂”——阀门不动,液位飘到哪算哪。或者动作空间被限制得太死,智能体发现无论怎么动都是错,梯度消失。
解决:先把误差惩罚系数从 -10 降到 -1,跑 50 轮看奖励是否上升。如果还是不动,检查 Actor 网络最后一层的激活函数。连续控制的输出层应该用 tanh,把动作压到 [-1, 1],如果用 sigmoid 会导致动作永远为正,阀门只能开不能关。
4.2 现象:液位在目标值附近高频震荡
原因:动作变化率惩罚太弱,智能体学会了“快速开关阀门”这种偷懒策略——液位低了猛开,高了猛关,看起来平均误差小,但实际阀门在疯狂抖动。这种策略在仿真里奖励不错,放到真实设备上阀门三天就坏了。
解决:把动作变化率惩罚系数从 -0.5 调到 -2,同时把稳态奖励的阈值从 0.02 放宽到 0.05。让智能体明白“稳”比“准”更重要。如果还震,在奖励函数里加一项阀门开度绝对值的惩罚,抑制大幅动作。
4.3 现象:训练到一半 MATLAB 报内存不足
原因:经验回放池设成了 1e6,每个经验包含状态、动作、奖励、下一状态四个数组,状态维度如果是 10,一个经验就是 40 个 double,1e6 个经验就是 320MB。加上网络参数和梯度缓存,16GB 内存的机器跑到 300 轮左右就会爆。
解决:把回放池降到 1e5,批量大小从 128 降到 64。如果还不行,把状态维度精简一下——水箱控制不需要把液位、流速、阀门开度、历史误差全塞进去,保留液位和阀门开度两个量就够了。
4.4 现象:仿真时液位曲线正常,但阀门开度一直是 0
原因:Actor 网络的输出被 tanh 压到了 [-1, 1],但环境里的阀门开度范围是 [0, 1]。如果智能体输出 -0.5,阀门开度变成 -0.5,被max(min(action, 1), 0)截断成 0。智能体发现输出负值没惩罚,就学会了永远输出负值。
解决:在环境接口里把动作映射改一下,把 [-1, 1] 线性映射到 [0, 1]:
% 动作映射:[-1, 1] -> [0, 1] valveCommand = (action + 1) / 2; this.ValveOpening = this.ValveOpening + 0.1 * valveCommand;这样智能体输出 -1 对应阀门关死,输出 1 对应阀门全开,中间值线性对应。改完之后重新训练,阀门开度曲线就正常了。
4.5 现象:换了一个目标液位,智能体完全不会控制
原因:训练时目标液位是固定的,智能体把“目标液位=0.5”当成了环境的一部分,而不是一个可变的输入。它学到的策略是“不管当前液位多少,阀门开到某个固定角度”,而不是“根据偏差调整阀门”。
解决:在训练时随机化目标液位。每轮 episode 开始时,把TargetLevel在 [0.2, 0.8] 之间随机取一个值。这样智能体被迫学会“看偏差调阀门”,而不是死记硬背。改完之后训练轮数可能要增加到 800 轮,但泛化能力会好很多。
5. 进阶技巧:用 Domain Randomization 让智能体适应真实水箱的“不完美”
仿真里跑得再好,搬到真实水箱上往往翻车。原因很简单:仿真模型太理想了。真实水箱的出水系数会随液位变化,传感器有噪声,阀门有死区,管道有阻力。这些在仿真里都没建模,智能体没见过,自然应对不了。
我一般会做一层 Domain Randomization——在训练时随机扰动环境参数,让智能体学会在“不完美”中控制。具体做法是在reset方法里加几行:
function [obs, loggedSignals] = reset(this) % 随机化环境参数,模拟真实水箱的不确定性 this.OutflowCoeff = 0.5 + 0.1 * randn(); % 出水系数 ±10% 波动 this.InflowRate = 1.0 + 0.05 * randn(); % 入水速率 ±5% 波动 % 随机化初始液位 this.Level = 0.1 + 0.3 * rand(); % 随机化目标液位 this.TargetLevel = 0.2 + 0.6 * rand(); % 传感器噪声(在 getObservation 里加) % this.NoiseLevel = 0.01; obs = this.getObservation(); loggedSignals = []; end这段代码做了四件事:出水系数和入水速率加高斯扰动,初始液位随机,目标液位随机。传感器噪声我注释掉了,因为加噪声会让训练变慢,建议先不加噪声跑通,再逐步加。
Domain Randomization 的代价是训练轮数要增加。原来 500 轮收敛,现在可能要 1000 轮。但换来的是智能体对参数变化的鲁棒性——我试过把出水系数从 0.5 改到 0.65,没做随机化的智能体直接失控,做了随机化的还能稳住液位,超调不超过 5%。
还有一个技巧是课程学习(Curriculum Learning)。先让智能体在目标液位固定的简单环境里学 200 轮,再把目标液位随机化,再学 300 轮。这样比一上来就随机化收敛快得多。MATLAB 里可以用rlTrainingOptions的Callback函数实现分阶段训练,但代码量不小,新手建议先用固定目标液位跑通,再手动改参数分阶段训练。
验证智能体是否真的学会了,不能只看训练曲线。我一般会做三组测试:第一组,目标液位设成训练时没见过的值(比如 0.35),看能不能稳住;第二组,在仿真中途突然改变目标液位,看智能体能不能平滑过渡;第三组,把出水系数调高 20%,看会不会震荡。三组都过了,才敢说这个智能体“能用”。
从那以后我每次训练完 DDPG,都会强制走一遍这三组测试,不再只看奖励曲线。希望这套水箱控制资源能帮你省下搭环境的时间,把精力花在调策略上。
本文还有配套的精品资源,点击获取