简介:资源围绕基于MILP-TD3的用户侧储能系统优化运行展开,面向具备编程基础、关注储能调度与深度强化学习结合的科研人员和工程师,重点解决深度强化学习在储能控制中难以严格满足运行约束的问题,适用于实时调度策略设计、MILP与TD3算法融合、运行成本优化及算法性能对比等场景。包内为单个docx文档,文件总数为1,大小58KB,内容涵盖环境建模、MILP求解器、TD3算法、增强型MILP求解器、完整训练流程、性能对比分析以及实时调度测试,Python代码可直接复制运行,便于学习与二次开发。当前已有94人学习浏览。文档不仅给出理论模型与核心创新点分析,还详细展示了电池退化成本建模、基于Pyomo的约束处理机制、奖励函数设计等关键环节,并配有性能评估指标解读,可帮助读者快速上手MILP-TD3组合方法,在储能控制中验证降本增效与安全运行效果。
1. MILP-TD3从哪来:用户侧储能实时调度为什么不能只靠混合整数线性规划
某制造园区的配电台区装了一套 1.2MW/2.4MWh 用户侧储能,光伏午间出力大,晚间负荷尖峰明显,电价按 15 分钟出清。调度系统要回答的问题很直接:未来 96 个时段里,每一段该充多少、放多少、启停怎么切。这个模型天然是混合整数线性规划,功率是连续变量,启停是 0/1 变量。可真到了线上跑,每次滚动窗口重新求解,MILP 求解器在园区那台 ARM 控制器上常常几秒钟出不了可行解,预测数据一更新又得重算。MILP-TD3 的思路是把这两层拆开:离线下让 MILP 生成最优解样本,在线用深度强化学习把“状态到动作”的映射固化成神经网络,让实时调度策略不再依赖求解器反复迭代。适合正在做储能 EMS、需求响应或微电网控制器的工程师参考。
2. 用户侧储能MILP建模:SOC递推、目标函数与滚动时域约束
无论 TD3 的奖励函数怎么写,最终要逼近的都是混合整数线性规划的解分布。我的建议是先把储能问题完整写成一个 MILP,再把目标函数里的每一项映射成强化学习的奖励项。很多实现直接从强化学习环境代码入手,训练完以后连 SOC 约束和末端约束都没对齐,最后只能靠运气验收。
2.1 决策变量与SOC递推的数学骨架
取 Δt=15min 作为调度步长,一天划分为 t=0,…,95 共 96 个时段。决策变量分两组:充放电功率 p_ch,t、p_dis,t 为连续变量,范围 [0, P_max];电池是否处于充电或放电状态用二进制变量 u_ch,t、u_dis,t 表示。电池不能同时充放电,所以必须加入 u_ch,t + u_dis,t ≤ 1,这是模型从线性规划升格为混合整数线性规划的关键。
SOC 作为状态变量,递推关系写成:
SOC_{t+1} = SOC_t + (η_ch · p_ch,t − p_dis,t / η_dis) · Δt / E_rated
式中 E_rated 是额定容量,η_ch 和 η_dis 分别取 0.9~0.95。业务上为了避免电池长期顶在上界运行,SOC 上下限设为 0.1 和 0.9,而不是物理 0% 和 100%。这样既延长电池寿命,也留出了充放电调节空间。给 TD3 做训练时,这套上下限会直接变成奖励函数里的惩罚边界。
| 变量类型 | 符号 | 取值范围 |
|---|---|---|
| 连续变量 | p_ch, p_dis | 0 ~ P_max |
| 二进制变量 | u_ch, u_dis | {0, 1},且 u_ch + u_dis ≤ 1 |
| 状态变量 | SOC | 0.1 ~ 0.9 |
| 参数 | E_rated, P_max | 根据实际设备铭牌设定 |
2.2 目标函数与关键约束的MILP表达
用户侧储能优化目标是最小化一天内的外购电总成本,同时把电池退化折算进成本项,写成:
min Σ_t (price_buy,t · P_grid_buy,t − price_sell,t · P_grid_sell,t + λ · D_t) · Δt
其中 P_grid_buy 是从电网购入的功率,P_grid_sell 是余电上网功率,price_buy 和 price_sell 是两个价格序列,λ 是电池退化成本的折算系数,D_t 是第 t 时段的充放电量。这里如果省略退化项,策略会倾向于每天满充满放,账面收益很好看,但电池循环寿命会明显缩短。
必要的约束条件至少包括:
- 功率平衡:P_load,t = P_pv,t + P_dis,t − P_ch,t + P_grid_buy,t − P_grid_sell,t
- 爬坡约束:|P_ch,t+1 − P_ch,t| ≤ ΔP_max,防止功率突变冲击变流器
- 启停互斥:u_ch,t + u_dis,t ≤ 1
- 末端 SOC 回位:SOC_95 与 SOC_0 偏差小于预设阈值,避免每天末尾把电量耗尽
| 约束类型 | 表达式 | 作用 |
|---|---|---|
| 功率平衡 | 负荷 = 光伏 + 电池 + 电网 | 保证能量守恒 |
| 启停互斥 | u_ch + u_dis ≤ 1 | 防止同时充放电 |
| SOC 边界 | 0.1 ≤ SOC_t ≤ 0.9 | 保护电池寿命 |
| 末端回位 | SOC_95 ≈ SOC_0 | 保证日内调度可重复 |
2.3 滚动时域逻辑与纯MILP实时求解的瓶颈
实际运行不是求解一次就结束,而是每隔 15 分钟滚动更新一次。预测数据更新、负荷突变、光伏波动,都会让上一轮最优解失效。96 时段 MILP 里大约包含 192 个二进制变量,在园区级控制器上求解耗时可以从几秒到几分钟,已经逼近甚至超过调度周期。
回头看运行记录,启停决策其实高度重复:午间光伏大发时充电,晚间峰段放电。MILP 不是没有规律,而是每一次都把规律重新算一遍。MILP-TD3 的做法是用大量离线 MILP 解作为样本,训练一个深度强化学习策略,把“什么时候充、什么时候放”固化到神经网络参数里。MILP 的角色从在线求解器退位为离线样本生成器,这正是它和深度强化学习结合后最实用的分工。
3. 深度强化学习与MILP分工:TD3状态空间、动作接口与奖励塑形
MILP 提供了离线最优解分布,TD3 负责把分布拟合成可实时推理的策略。训练效果是否符合预期,主要看状态、动作、奖励三件事的接口设计是否和 MILP 对齐,任何一个环节建模粗糙,后面调参都会非常被动。
3.1 为什么选TD3而不是DQN、PPO或DDPG
对比深度强化学习算法列表就能看出选型理由。DQN 处理连续功率时需要先离散化动作,而储能的功率调节可能出现上百个档位,离散化直接引入经济性损失;PPO 支持连续动作,但对延迟奖励和超参数更敏感,储能收益往往在一天结束后才体现,PPO 的价值估计在这种场景下容易波动;DDPG 是 TD3 的前身,使用单 Critic,Q 值过估计问题一直没有被有效抑制,训练样本少的时候最容易陷入局部最优。
TD3 用双 Critic 网络取最小值抑制过估计,通过延迟更新降低 Actor 和 Critic 耦合,再引入目标策略平滑噪声提升鲁棒性,整体上更适合用户侧储能这种连续控制任务。如果储能电站之间形成多站点协同网络,图强化学习、联邦深度强化学习确实是有价值的扩展方向,但单站点的实时调度任务,TD3 是最稳妥的起点。
| 算法 | 动作空间 | Q值过估计抑制 | 在储能调度中的问题 |
|---|---|---|---|
| DQN | 离散 | 弱 | 功率离散化精度差 |
| DDPG | 连续 | 弱 | 超参数敏感,训练振荡 |
| PPO | 连续 | 无 | 延迟奖励场景收敛不稳定 |
| TD3 | 连续 | 双 Critic + 延迟更新 | 适合连续功率调节 |
3.2 状态空间设计:包含必要预测窗口的归一化向量
状态不仅包含当前时刻的 SOC,还应该包含未来 24 小时的电价预测、负荷预测和光伏预测。TD3 是离线训练,训练数据和上线数据分布不一致时,策略会直接失效。常见做法是把预测窗口做成固定长度的归一化向量,拼到 SOC 后面。
import numpy as np import math def make_state(soc, price_fc, load_fc, t): # 电价按窗口内最大值归一化到 [0,1] price = (price_fc / price_fc.max()).astype(np.float32) # 负荷先归一化再平移,落到 [-0.5, 0.5] load = (load_fc / load_fc.max() - 0.5).astype(np.float32) # 时间用 sin/cos 编码,避免 0 点和 24 点距离突变 time_code = np.array([ math.sin(2 * math.pi * t / 96), math.cos(2 * math.pi * t / 96) ], dtype=np.float32) return np.concatenate([[soc * 2 - 1], price[:24], load[:24], time_code])归一化操作里最容易忽略的是 SOC 的缩放。SOC 本身在 0.1~0.9 之间,直接丢给网络会导致数值范围和其他特征不一致。这里把 SOC 映射到 [-0.8, 0.8] 左右,等价于soc * 2 - 1,方便网络快速收敛。
3.3 动作接口:如何将[-1,1]变成物理功率
TD3 网络输出的是连续动作,储能调度又必须有启停状态。常见做法是让网络只输出一个带正负号的功率指令 a ∈ [-1, 1],正值为充电,负值为放电,绝对值对应功率幅值。环境在 step 入口根据 a 的正负完成物理量映射。
if a >= 0: p_ch = a * p_max p_dis = 0.0 u_ch, u_dis = 1, 0 else: p_ch = 0.0 p_dis = -a * p_max u_ch, u_dis = 0, 1这种设计的好处是不增加网络输出维度,也不需要二值化采样。网络只学习连续功率,物理启停由符号决定,天然满足 u_ch + u_dis ≤ 1 的互斥约束。执行前再经过一道安全性过滤,检查 SOC 边界和变流器功率限制,越界时直接截断。
3.4 奖励函数分解:经济项、退化项与约束惩罚的权重设置
奖励函数按负成本设计,网络最大化奖励等价于最小化运行成本:
r_t = −(购电费用_t + 退化成本_t) − penalty_t
惩罚项包含 SOC 越界、末端 SOC 偏移和频繁启停三类:
penalty = c_soc * (soc < soc_min or soc > soc_max) + \ c_end * (t == 95 and abs(soc_final - soc_init) > 0.02) + \ c_sw * (abs(u_t - u_{t-1}) > 0)初始权重建议设为 c_soc=5.0、c_end=3.0、c_sw=0.1,经济项保持 1.0。c_sw 过大会导致策略几乎不动作,整个储能变成摆设;过小则启停频繁,增加接触器磨损。这个权重关系和 MILP 里约束的拉格朗日乘子非常相似,可以先在小规模数据集上跑几轮,观察启停次数再调整。
4. 最小可运行的MILP-TD3实现:用户侧储能仿真环境与训练循环
下面给出一个能直接运行的简化版本,重点展示环境、网络、训练循环三块骨架。真实项目还要接入更细的功率平衡和电费计费逻辑,但核心结构不变。
4.1 用户侧储能仿真环境骨架
环境接收动作,更新 SOC,计算奖励,输出状态。这里用net_load表示从电网实际购电的功率,SOC 递推与第 2 章公式保持一致。
class BESSEnv: def __init__(self, price_hist, load_hist, pv_hist, dt=0.25, cap=2.0, p_max=1.0): self.price = price_hist self.load = load_hist self.pv = pv_hist self.dt = dt # 调度周期,小时 self.cap = cap # 额定容量,MWh self.p_max = p_max # 最大功率,MW self.soc = 0.5 self.t = 0 def reset(self): self.soc = 0.5 self.t = 0 return self._state() def step(self, a): a = float(np.clip(a, -1, 1)) if a >= 0: p_ch, p_dis = a * self.p_max, 0.0 else: p_ch, p_dis = 0.0, -a * self.p_max soc_new = self.soc + (0.9 * p_ch - p_dis / 0.9) * self.dt / self.cap soc_new = np.clip(soc_new, 0.1, 0.9) net = self.load[self.t] - self.pv[self.t] + p_ch - p_dis cost = max(net, 0) * self.price[self.t] * self.dt degrad = 0.02 * (p_ch + p_dis) * self.dt r = -cost - degrad if soc_new >= 0.899 and p_ch > 0: r -= 1.0 # SOC 已到上界仍充电,给负惩罚 self.soc = soc_new self.t += 1 done = self.t >= len(self.price) return self._state(), r, done def _state(self): return np.array([ self.soc * 2 - 1, self.load[self.t] / self.p_max - 2.0 ], dtype=np.float32)该环境定义里退化成本用0.02的简化系数替代复杂的循环寿命模型,实际项目应替换为基于 DOD 统计的分段线性函数。SOC 上界惩罚r -= 1.0是为了阻止策略在满电状态继续充电,否则网络学到的是无效动作也能拿高奖励。
4.2 TD3的Actor与Critic网络定义
Actor 网络输出在 [-1, 1] 范围内的动作,Critic 网络输出 Q 值。两个 Critic 使用相同输入结构、不同参数。
import torch import torch.nn as nn class Actor(nn.Module): def __init__(self, state_dim, action_dim=1): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, action_dim), nn.Tanh() ) def forward(self, s): return self.net(s) class Critic(nn.Module): def __init__(self, state_dim, action_dim=1): super().__init__() self.q1 = nn.Sequential( nn.Linear(state_dim + action_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 1) ) self.q2 = nn.Sequential( nn.Linear(state_dim + action_dim, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 1) ) def forward(self, s, a): x = torch.cat([s, a], dim=-1) return self.q1(x), self.q2(x)Actor 最后一层固定用 Tanh,配合 Kaiming 初始化或默认初始化都能把输出限制在 [-1, 1],与环境的动作接口吻合。Critic 把状态和动作拼接后输入,两个分支独立计算,目的是取较小值作为目标 Q。
4.3 训练主循环与目标网络软更新
训练循环里关心三件事:探索噪声、延迟更新、目标网络参数软更新。
def soft_update(target, source, tau): for tp, sp in zip(target.parameters(), source.parameters()): tp.data.copy_(tau * sp.data + (1 - tau) * tp.data) for step in range(total_steps): # 行为策略加探索噪声 a = actor(state).detach().cpu().numpy() a = np.clip(a + np.random.normal(0, 0.15), -1, 1) next_state, reward, done = env.step(a) replay.add((state, a, reward, next_state, done)) state = env.reset() if done else next_state if len(replay) < batch_size: continue s, a, r, s2, d = replay.sample(batch_size) with torch.no_grad(): a2 = target_actor(s2) a2 = (a2 + torch.randn_like(a2) * 0.2).clamp(-1, 1) q1_t, q2_t = target_critic(s2, a2) q_target = r + 0.99 * (1 - d) * torch.min(q1_t, q2_t) q1, q2 = critic(s, a) critic_loss = nn.functional.mse_loss(q1, q_target) + \ nn.functional.mse_loss(q2, q_target) critic_opt.zero_grad() critic_loss.backward() critic_opt.step() if step % 2 == 0: actor_loss = -critic.q1(s, actor(s)).mean() actor_opt.zero_grad() actor_loss.backward() actor_opt.step() soft_update(target_actor, actor, 0.005) soft_update(target_critic, critic, 0.005)Critic loss 是双 Q 的误差之和,目标值用min(q1_t, q2_t)。Actor 每两步更新一次,延迟更新的目的是让 Critic 先收敛出一个相对稳定的 Q 值方向,再调整策略。软更新系数 tau=0.005,把目标网络向训练网络缓慢靠拢,避免目标跳动太剧烈。
4.4 训练超参数参考表
| 参数 | 参考值 | 影响说明 |
|---|---|---|
| 探索噪声 | 0.15 | 过大动作抖动,过小探索不足 |
| 目标噪声 | 0.2 | 平滑 Q 值,配合 clip 0.5 |
| 策略延迟 | 2 | Actor 每 2 步更新一次 |
| 折扣因子 γ | 0.99 | 储能收益在日末端才能体现 |
| 软更新 tau | 0.005 | 目标网络跟踪速度 |
| 批大小 | 256 | 样本多样性 |
| 经验池容量 | 100000 | 需覆盖四季典型场景 |
开训时先用“谷充峰放”的规则策略预填充一部分经验池,会明显加快前期收敛。这点在用户侧储能场景里很实用,规则解虽然不最优,但能避免冷启动阶段策略在完全随机动作上浪费大量 episode。
5. MILP-TD3关键超参与训练排错:奖励权重、噪声系数与边缘推理
训练跑通只是第一步,真正花时间的是调参和排错。TD3 的超参数、奖励权重和部署方式,都会影响最终调度效果。
5.1 噪声参数与学习率怎么配
| 参数 | 建议范围 | 现象 |
|---|---|---|
| 探索噪声 | 0.1 ~ 0.2 | 超过 0.3 时动作全程抖动 |
| 目标噪声 | 0.15 ~ 0.25 | 过大导致 Critic 无法收敛 |
| 噪声裁剪 | 0.4 ~ 0.6 | 限制探索动作越界 |
| Actor 学习率 | 1e-4 ~ 3e-4 | 大于 1e-3 容易发散 |
| Critic 学习率 | 1e-4 ~ 3e-4 | 与 Actor 同量级即可 |
探索噪声只影响训练期间的数据收集,评估时必须去掉。很多项目出现训练 reward 很高但离线回放效果差,排查第一步就是确认评估代码里没有加噪声。
5.2 奖励权重如何映射MILP约束
奖励函数里的 c_soc、c_end、c_sw 对应 MILP 里 SOC 边界约束、末端回位约束和启停约束。先跑 3 组不同权重的小规模实验,观察训练后的 SOC 曲线。
如果 SOC 频繁顶在 0.9,说明 SOC 上界惩罚系数不够,策略觉得顶着限幅充电也无所谓;如果 SOC 长期停在 0.1 附近不敢充电,说明上界惩罚过重,策略学会了保守。末端偏差大就提高 c_end,反复启停就提高 c_sw。调整顺序建议从 c_soc 开始,再动 c_sw,最后调 c_end。
5.3 训练日志:发散信号与排查顺序
| 现象 | 优先排查方向 |
|---|---|
| Critic loss 持续上升 | 学习率过大或奖励量级过大 |
| Actor 输出长期贴在 ±1 | 训练样本不足或状态特征缺失 |
| 训练 reward 高但离线评估差 | 评估时未关闭探索噪声 |
| 相同输入对应差异大的动作 | 缺少时间编码或电价特征 |
每一轮训练结束后,固定抽取同一天的初始化状态,用确定性推理生成一条 SOC 曲线并打印。如果曲线没有日周期规律,不要先去调网络,大概率是状态拼接或奖励计算写错了,这种 bug 很难靠调参解决。
5.4 边缘部署:模型导出与推理延迟
训练好的 Actor 导出为 ONNX,在边缘设备上用 ONNX Runtime 或 TensorRT 推理。储能控制器侧的状态特征维度通常不超过 80 个,三层 MLP 的推理时间在 ARM 平台可以做到 5ms 以内。
torch.onnx.export( actor, dummy_input, "td3_actor.onnx", opset_version=13, input_names=["state"], output_names=["action"] )导出后检查输出名和输入维度,避免上线后状态向量拼接顺序不一致。多站点协同训练时,如果不想把各站点负荷曲线集中到数据中心,可以参考联邦深度强化学习的思路,各站点用本地数据训练若干轮,再把网络权重上传聚合。MILP-TD3 在这条路线上的限制是各站点电价时段尺度不同,聚合前需要先做特征分布对齐。
6. MILP-TD3上线验证:回放对照、实时安全约束与收益指标核对
离线训练指标不能直接证明线上可用,上线前至少要过三关:回放对照、安全兜底、收益指标复核。
6.1 回放对照:同一段历史数据跑三套策略
选择四季各一周的历史数据,同一时序分别用 MILP 理想解、TD3 策略和简单规则策略回放,对比平均日收益、启停次数和 SOC 日末偏差。如果 TD3 与 MILP 的收益差距超过 10%,优先检查状态窗口是否覆盖了完整的电价峰谷段,而不是急着改网络结构。
| 策略 | 平均日收益 | 启停次数 | SOC 日末偏差 |
|---|---|---|---|
| MILP 理想解 | 基准 | 低 | 最小 |
| MILP-TD3 | 接近基准 | 中 | 可接受 |
| 固定规则 | 低于基准 | 高 | 波动大 |
6.2 实时安全约束兜底
即使训练中已经加入约束惩罚,线上推理结果也必须经过硬约束过滤。在动作进入变流器前检查功率边界和 SOC 边界。
def safe_apply(soc, action, p_max, soc_min=0.1, soc_max=0.9): p = action * p_max if p > 0 and soc >= soc_max: p = 0.0 if p < 0 and soc <= soc_min: p = 0.0 p = np.clip(p, -p_max, p_max) return p这一段逻辑独立于神经网络部署,目的不是限制策略发挥,而是在模型决策异常时保住设备和电网安全边界。线上运行建议记录所有被截断的动作,定期检查截断比例,如果持续偏高说明训练场景与实时场景偏差过大,需要更新训练数据。
6.3 验收指标与上线周期复核
核心指标就三个:日均套利收益、日均等效循环次数、SOC 越界事件数。第一个看经济性,第二个看电池寿命损耗,第三个看安全性。每周把新一周预测数据送入模型跑一遍,和上一周模型输出做对比,差异超过 10% 时不要自动切换模型,先人工看 SOC 曲线确认边界是否合理。
最后一个建议:模型更新选在周末低负荷时段切换,不要在周一早高峰前上线新模型。新模型可能在某些极端场景下行为突变,留出低风险窗口观察一天再决定。
本文还有配套的精品资源,点击获取