简介:这份资源面向学习强化学习与控制系统仿真的MATLAB用户,聚焦连续动作空间下的水箱水位控制问题,用DDPG算法替代传统控制器,帮助读者理解Actor-Critic架构在工程场景中的落地方式。压缩包共2个文件,包含1个Simulink模型文件与1个m脚本文件,整体约45KB,前者以图形化方式搭建水箱物理系统并模拟水位与流量变化,后者承载网络结构定义、训练循环与参数更新等核心逻辑。已有403人学习下载,说明该案例在强化学习入门与课程实践中具有一定参考价值。读者可从中获得一套可直接运行的DDPG控制水箱完整示例,包括Actor与Critic网络搭建、经验回放与探索噪声设置、奖励函数设计以及算法与Simulink模型的交互流程,便于在此基础上调整网络层数、学习率等参数,观察策略收敛过程,并迁移到其他连续控制任务中。
1. DDPG 控制水箱:为什么一个“连续动作”问题不能用 DQN 硬套
水箱液位控制看起来简单得不像一个强化学习问题:一个进水阀、一个出水口、一个液位传感器,目标是把水位稳在设定值。但真正动手做过的人会发现,这里藏着一个让离散动作算法直接翻车的坑——阀门开度是连续的。你可以开 37.2%,也可以开 37.3%,DQN 那套“从有限动作里挑一个”的框架在这里根本没法用,强行离散化又会带来动作抖动和精度损失。DDPG(Deep Deterministic Policy Gradient)正是为这类连续控制场景设计的:它用一个确定性策略网络直接输出动作值,再用 Critic 网络评估这个动作好不好。搞懂它,你就搞懂了“连续动作空间下策略梯度怎么落地”这条主线,而水箱恰好是最小可复现的载体——状态维度低、物理过程可解释、训练曲线能直接对应到液位波动上,比那些动辄几十维观测的仿真环境更适合把机制看透。
2. 水箱被控对象:状态、动作与奖励是怎么定义出来的
2.1 为什么状态选液位和液位变化率
水箱的物理模型通常写成离散时间形式:下一时刻液位等于当前液位加上进水流量减去出水流量,再乘以时间步长。进水流量由阀门开度决定,出水流量按托里拆利定律与液位平方根成正比。这个非线性平方根项是水箱问题的核心特征——液位越高,出水越快,系统自带一个“负反馈”,但也意味着同样的阀门开度在不同液位下产生的净流量完全不同。
常见的状态设计是二维:当前液位 h 和液位误差的变化趋势。只给液位,策略网络很难判断“现在是正在上升还是正在下降”,容易在设定值附近来回振荡;加上变化率,相当于给了一个隐式的微分信息,让策略能提前刹车。我一般会这么理解:这不是给网络“更多信息”,而是把控制器设计里常用的 PD 思想,用状态的形式喂给了策略。
动作就是一维的阀门开度,归一化到 0 到 1 之间。这里有个容易忽略的细节:动作必须做归一化,因为策略网络的输出层通常用 tanh 激活,天然落在 -1 到 1,如果不做缩放映射,网络输出和物理阀门开度之间就隔了一层手工换算,训练时梯度尺度会乱。
奖励函数是最能体现设计意图的地方。最朴素的做法是负的液位误差绝对值,但这样训练出来的策略往往“温吞”,收敛慢。更常见的写法是误差平方的负值加上一个动作变化惩罚:
# 奖励函数示意:误差惩罚 + 动作平滑惩罚 def compute_reward(h, h_target, action, prev_action): error = h - h_target reward = -(error ** 2) # 液位偏差越大,惩罚越重 reward -= 0.1 * (action - prev_action) ** 2 # 抑制阀门频繁抖动 return reward误差平方项让策略优先消除大偏差,动作变化惩罚则防止阀门高频开合——真实执行机构经不起这种折腾。系数 0.1 不是随便定的,它决定了“稳”和“快”的权衡:调大,响应变慢但更平滑;调小,收敛快但阀门动作毛糙。这个系数往往要试几轮才能定下来。
2.2 环境步进里的隐藏陷阱:积分步长与终止条件
水箱仿真通常用欧拉法步进,步长取 0.1 秒或 1 秒。步长太大会让平方根非线性项失真,甚至出现液位为负这种物理上不可能的状态;步长太小则一个回合要跑几千步,训练效率骤降。常见的实现思路是把单回合长度固定为几百步,同时在液位越界(低于 0 或高于水箱上限)时提前终止并给一个大惩罚。
终止条件的设计直接影响探索行为。如果只在越界时终止,智能体早期很容易把水箱灌满然后回合结束,学不到“接近设定值后如何微调”。更稳的做法是给一个软约束:液位进入设定值附近的一个窄带并保持若干步,就给一个小的正向奖励,鼓励它“待住”。这不是必须的,但对收敛速度影响很大。
3. DDPG 的四网络结构:Actor、Critic 与它们的影子副本
3.1 确定性策略梯度到底在优化什么
DDPG 的核心公式是策略梯度:对 Actor 参数求导时,梯度等于 Critic 对动作的梯度乘以 Actor 对参数的梯度。直白地说,Critic 告诉 Actor“往哪个方向调动作能提高 Q 值”,Actor 就顺着这个方向更新自己。这和 DQN 用 argmax 选动作完全不同——这里没有“选”,只有“调”。
正因为是确定性策略,探索必须靠外部噪声注入。常见做法是在 Actor 输出上叠加 Ornstein-Uhlenbeck 噪声或高斯噪声。OU 噪声有时间相关性,适合惯性系统;高斯噪声实现简单,在水箱这种低维问题上效果差别不大。噪声幅度通常随训练衰减,早期大胆探索,后期精细调整。
3.2 目标网络与软更新:为什么需要“影子副本”
如果直接用同一个 Critic 算目标 Q 值又更新它,会出现自举发散——目标本身在动,训练就像追自己的影子。DQN 用周期性硬拷贝目标网络解决这个问题,DDPG 则用软更新:每次只把目标网络参数往当前网络挪一点点。
# 软更新:tau 通常取 0.001 到 0.01 def soft_update(target_net, online_net, tau): for target_param, param in zip(target_net.parameters(), online_net.parameters()): target_param.data.copy_(tau * param.data + (1 - tau) * target_param.data)tau 越小,目标越稳但学得越慢;tau 越大,学得快但容易震荡。水箱这类低维问题 tau 取 0.005 左右比较常见。四个网络(Actor、Critic 及各自的目标网络)里,只有两个在线网络参与梯度更新,目标网络纯粹是“延迟的参照系”。
3.3 经验回放与批采样:打破时间相关性
DDPG 是离线策略算法,经验回放池存的是(状态、动作、奖励、下一状态、终止标志)五元组。批采样时随机抽一批,打破连续样本之间的强相关。回放池容量要覆盖足够多的工况——如果太小,早期灌满水箱的坏经验会被反复采样,策略迟迟学不会微调;如果太大,旧策略产生的数据占比过高,新经验被稀释。水箱问题上,几万条的容量通常够用。
这里有个实现细节值得说:终止标志必须正确区分“真终止”和“时间截断”。如果回合因为步数上限被截断,下一状态仍然有价值,不能当作终止处理,否则 Critic 会错误地认为“到了步数上限就没未来收益了”,导致策略在回合末期行为异常。
4. 训练循环的源码级拆解:从采样到参数更新
4.1 单步交互与回放池写入
训练主循环的骨架是:用当前策略加噪声选动作,环境步进得到奖励和下一状态,写入回放池,然后从池里采样更新。动作选择这一步要注意,网络输出经过 tanh 后范围是 -1 到 1,必须线性映射到阀门的物理范围,否则环境收到的动作和网络以为的动作不一致,训练必然崩。
# 动作选择:策略输出 + 探索噪声,再映射到物理范围 action_raw = actor(state) # tanh 输出,范围 [-1, 1] noise = np.random.normal(0, noise_std, size=action_raw.shape) action = np.clip(action_raw + noise, -1, 1) # 加噪后仍限制在合法区间 valve = (action + 1) / 2 * (valve_max - valve_min) + valve_min # 映射到阀门开度裁剪这一步不能省。噪声可能把动作推出合法范围,不裁剪的话环境会收到越界动作,轻则物理模型报错,重则训练出完全错误的策略。
4.2 Critic 更新:TD 误差与目标 Q 值
Critic 的损失是均方 TD 误差。目标 Q 值用目标 Actor 生成下一动作,再用目标 Critic 评估,加上奖励后与当前 Critic 的估计做差。这里的关键是“目标 Actor 输出不加噪声”——目标值要稳定,探索噪声只属于在线策略。
# Critic 更新核心逻辑 next_action = target_actor(next_state) target_q = reward + gamma * (1 - done) * target_critic(next_state, next_action) current_q = critic(state, action) critic_loss = F.mse_loss(current_q, target_q.detach())done的乘法项就是前面说的终止处理,detach()保证梯度不回传到目标网络。gamma 是折扣因子,水箱控制通常取 0.99,因为需要策略考虑较长时段的液位演化。
4.3 Actor 更新与梯度路径
Actor 的损失是负的 Q 值均值——Critic 给分越高,损失越小。梯度从 Critic 的动作输入反传回 Actor 参数,这条路径要求 Critic 对动作可导,这也是为什么 DDPG 只能用连续动作。
# Actor 更新:最大化 Critic 给出的 Q 值 actor_loss = -critic(state, actor(state)).mean() actor_loss.backward()注意这里 Critic 的参数在这一步不更新,只借它的梯度。实现上通常先把 Critic 设为 eval 模式或冻结参数,避免误更新。
4.4 训练监控:看什么曲线判断学没学会
水箱问题最直观的监控量是每回合平均液位误差和回合总奖励。健康的训练曲线是:总奖励前期快速上升,中期有波动,后期趋于平稳;液位误差逐步收敛到设定值附近的小幅振荡。如果奖励一直不涨,先查动作映射和奖励尺度;如果奖励涨了但液位还在大幅振荡,多半是动作惩罚系数太小或噪声衰减太慢。这些排查经验比任何理论推导都直接。
5. 避坑与排查:水箱 DDPG 训练里最常见的五个翻车点
5.1 奖励尺度失衡导致 Critic 输出爆炸
现象:训练几十回合后 Q 值变成天文数字,策略输出饱和在 ±1。原因:奖励绝对值太大或折扣因子接近 1,TD 目标累积后数值失控。解决:把奖励归一化到大致 -1 到 1 区间,或对 Q 值做梯度裁剪,同时检查 gamma 是否设得过高。
5.2 动作未归一化导致策略学不动
现象:Actor 损失几乎不变,阀门开度始终停在某个固定值附近。原因:网络输出范围与物理动作范围不匹配,梯度尺度被放大或压缩。解决:统一用 tanh 输出加线性映射,映射参数写死在环境封装里,不要在两处各写一套。
5.3 回放池未预热就开始更新
现象:前几回合损失剧烈震荡,策略行为随机。原因:回放池样本太少,批采样重复率高,梯度方向噪声极大。解决:先纯随机采样填满一个最小容量(比如几千条)再开始更新,预热阶段不训练。
5.4 终止标志处理错误导致末期行为异常
现象:回合快结束时阀门突然全开或全关。原因:时间截断被当成真终止,Critic 学到“末期没有未来”。解决:区分 terminated 和 truncated,只有真正越界才置 done,步数上限截断不置 done。
5.5 噪声不衰减导致稳态振荡
现象:液位能到设定值附近,但始终小幅抖动,阀门频繁动作。原因:探索噪声幅度固定,策略无法进入精细调整阶段。解决:噪声标准差随训练步数线性或指数衰减,后期保留极小噪声维持探索即可。
6. 设计取舍:确定性策略、样本效率与工程落地的边界
DDPG 在水箱上的表现,本质上反映了一类取舍:确定性策略换来了连续动作的直接输出,代价是探索完全依赖外部噪声,且对超参数敏感。和 PPO 这类随机策略算法比,DDPG 样本效率通常更高——它能把每条经验用得更充分,因为离线策略加回放池允许反复采样;但稳定性差一些,Critic 过估计和 Q 值发散是常见病,后来的一系列改进(双 Critic、延迟更新、目标策略平滑)都是在补这个洞。
放到工程视角,水箱这类低维连续控制问题,DDPG 的价值不在于“比 PID 强”,而在于它提供了一条从数据到策略的通用路径:不需要精确建模,奖励函数就是控制目标的直接翻译。但这也意味着奖励设计成了新的“调参现场”,误差权重、动作惩罚、终止条件,每一项都在悄悄定义最终策略的性格。我自己的习惯是,每次改奖励函数都先跑一个短回合,把液位曲线和阀门动作曲线叠在一起看,确认“惩罚项真的在惩罚我想惩罚的行为”,再开长训练。这套流程帮我省掉了不少盲目调参的时间。希望帮到你。
本文还有配套的精品资源,点击获取