简介:这份资源面向电力系统、配电网优化与强化学习方向的学习者与研究人员,聚焦利用深度强化学习实现主动配电网电压控制,解决分布式能源高渗透下电压波动、传统规则控制适应性不足的问题。压缩包共4个文件,以m脚本与xls数据表为主,包含IEEE33节点标准配电系统数据、潮流计算与SOCP相关脚本,以及训练所需的数据文件,整体约14KB,结构紧凑,便于直接运行与二次开发。资源围绕DRL模型构建、环境模拟、奖励函数设计、训练实施与性能评估展开,读者可据此复现智能体与IEEE33节点环境的交互流程,理解变压器分接头调节、电容器组投切等控制动作的建模方式,并对比传统方法在收敛速度与控制精度上的差异。目前已有778人学习下载,适合具备一定电力系统与强化学习基础、希望快速上手主动配电网电压控制实验的读者参考。
1. 主动配电网电压控制为什么开始转向深度强化学习
去年夏天跟一个做配网自动化的朋友聊天,他说他们那条馈线上光伏渗透率刚过 40%,中午光伏大发时段末端电压能冲到 1.08 p.u.,传统调压手段基本失效——电容器组一天动作几十次,寿命掉得飞快,有载调压变压器分接头来回切,结果电压还是压不住。这不是个例。分布式光伏、储能、电动汽车充电桩大规模接入之后,配电网从「无源」变成「有源」,电压波动的随机性和双向潮流让基于规则的调压策略越来越力不从心。
这就是「基于深度强化学习的主动配电网电压控制策略」要解决的问题。核心思路是:把配电网的电压调节建模成马尔可夫决策过程,用深度强化学习(DRL)训练一个智能体,让它学会在光伏出力波动、负荷变化的情况下,自主决定无功补偿装置出力、储能充放电功率、OLTC 分接头位置等控制变量,把各节点电压维持在安全范围内。适合谁看?做配网自动化、新能源并网、微电网能量管理的工程师,以及想从传统优化方法(如二阶锥规划、模型预测控制)转向学习型控制的研究生和研发人员。
2. 把电压控制写成 MDP:状态、动作、奖励怎么定
2.1 为什么是 MDP 而不是最优潮流
传统做法是每个控制周期解一次最优潮流(OPF),把电压约束、功率平衡、设备容量都写进去,用二阶锥松弛或者半定松弛求解。问题在于:光伏出力秒级波动,OPF 求解速度跟不上;而且配电网三相不平衡、参数不精确,模型误差大。深度强化学习不依赖精确模型,它通过与环境交互学策略,推理时只需一次前向传播,毫秒级出动作。
但前提是你得把问题正确地写成 MDP。写错了,训练不收敛,或者收敛到一个「摆烂」策略——比如智能体发现什么都不做奖励也不差,就干脆不动作。
2.2 状态空间设计:别把量测当状态
状态设计有个血泪经验:不是量测越多越好。我一般会选以下几类:
- 各节点电压幅值(标幺值),尤其是末端节点和光伏接入点
- 光伏有功出力、负荷有功无功
- 储能 SOC
- 当前 OLTC 分接头位置、电容器组投切状态
- 时间信息(时刻编码,因为光伏出力有强日周期)
状态维度控制在 50 维以内,太多会导致训练慢且容易过拟合。归一化必须做,电压用标幺值本身就在 0.9~1.1 附近,光伏出力除以额定容量,SOC 除以容量。
import numpy as np def build_state(voltages, pv_power, load_power, soc, tap_pos, cap_state, hour): """ 构建强化学习状态向量 voltages: 各节点电压标幺值, shape (n_bus,) pv_power: 光伏有功出力, shape (n_pv,), 单位 MW load_power: 负荷有功, shape (n_load,), 单位 MW soc: 储能荷电状态, shape (n_ess,), 0~1 tap_pos: OLTC 分接头档位, 标量 cap_state: 电容器投切状态, shape (n_cap,), 0/1 hour: 当前时刻, 0~23 """ # 电压归一化到 [-1, 1] 附近 v_norm = (voltages - 1.0) / 0.1 # 光伏出力按额定容量归一化 pv_norm = pv_power / 5.0 # 假设单台逆变器额定 5MW load_norm = load_power / 10.0 # 时间编码为正弦余弦,避免 23 和 0 之间跳变 hour_sin = np.sin(2 * np.pi * hour / 24) hour_cos = np.cos(2 * np.pi * hour / 24) state = np.concatenate([ v_norm, pv_norm, load_norm, soc, [tap_pos / 10.0, hour_sin, hour_cos], cap_state.astype(float) ]) return state.astype(np.float32)这段代码的关键点:电压用(v - 1.0) / 0.1归一化,因为配电网电压通常在 0.9~1.1 p.u. 之间,这样归一化后范围在 -1 到 1 附近,对神经网络友好。时间用正弦余弦编码,避免 23 点到 0 点之间出现数值跳变,这个细节很多初学者会忽略,导致智能体在午夜附近策略抖动。
2.3 动作空间:连续还是离散
配电网电压控制变量分两类:连续变量(储能充放电功率、SVC 无功出力)和离散变量(电容器投切、OLTC 分接头)。常见做法是:
- 如果全是连续变量,用 DDPG、TD3、SAC 这类连续控制算法
- 如果全是离散变量,用 DQN 及其变体
- 混合动作空间,用混合动作网络或者把离散动作也松弛成连续再量化
我一般推荐 SAC,因为它对超参数不敏感,样本效率高,而且输出的是随机策略,天然有探索能力。动作空间设计成连续向量,离散变量通过取整和映射实现。
import torch import torch.nn as nn class Actor(nn.Module): """SAC 策略网络,输出动作均值和标准差""" def __init__(self, state_dim, action_dim, hidden_dim=256): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), ) self.mu_head = nn.Linear(hidden_dim, action_dim) self.log_std_head = nn.Linear(hidden_dim, action_dim) def forward(self, state): x = self.net(state) mu = self.mu_head(x) log_std = self.log_std_head(x).clamp(-20, 2) # 限制标准差范围 return mu, log_std def sample(self, state): mu, log_std = self.forward(state) std = log_std.exp() normal = torch.distributions.Normal(mu, std) x_t = normal.rsample() # 重参数化采样 action = torch.tanh(x_t) # 压缩到 [-1, 1] log_prob = normal.log_prob(x_t) - torch.log(1 - action.pow(2) + 1e-6) log_prob = log_prob.sum(dim=-1, keepdim=True) return action, log_problog_std的 clamp 范围 -20 到 2 是经验值,太小会导致策略过早收敛到确定性动作,太大则训练不稳定。tanh压缩后动作在 [-1, 1],再线性映射到实际控制范围,比如储能功率 -1MW 到 1MW,电容器档位 0 到 5 档。
2.4 奖励函数:电压偏差和设备动作成本怎么平衡
奖励函数是 DRL 电压控制里最玄学的部分。设计不好,智能体会学出「疯狂动作」或者「完全不动」两种极端。我一般用分段奖励:
def compute_reward(voltages, action, prev_action, tap_change, cap_change): """ 奖励 = 电压偏差惩罚 + 设备动作成本 + 越限硬惩罚 voltages: 各节点电压标幺值 action: 当前动作 prev_action: 上一时刻动作 tap_change: OLTC 是否动作 cap_change: 电容器是否动作 """ v_ref = 1.0 v_dev = np.abs(voltages - v_ref) # 电压偏差惩罚,偏差越大惩罚越重 reward_voltage = -np.sum(v_dev ** 2) * 100 # 越限硬惩罚 violation = np.sum((voltages > 1.05) | (voltages < 0.95)) reward_violation = -violation * 500 # 设备动作成本,抑制频繁动作 action_cost = -np.sum(np.abs(action - prev_action)) * 10 # 离散设备动作额外惩罚 switch_cost = -(tap_change + cap_change) * 50 return reward_voltage + reward_violation + action_cost + switch_cost电压偏差用平方惩罚,因为电压偏离越远危害越大。越限硬惩罚系数要远大于偏差惩罚,让智能体把「不越限」作为第一优先级。设备动作成本是抑制频繁调节的关键,没有这一项,智能体会让电容器组每秒投切一次,仿真里看着电压很稳,实际设备早坏了。
注意:奖励系数的量级要匹配。如果电压偏差惩罚是 -100 量级,动作成本是 -0.01 量级,那动作成本形同虚设。建议先跑几百步,打印各项奖励的数值范围,再调系数。
3. 用 Python 搭一个配电网电压控制仿真环境
3.1 仿真环境选型:为什么不用真实配网数据
真实配网数据难拿,而且涉及安全合规问题。常见做法是用 IEEE 33 节点、IEEE 69 节点或者某地区典型配网拓扑,在 OpenDSS 或 pandapower 里搭仿真环境。pandapower 是纯 Python 的,和 PyTorch 集成方便,我一般用它。
环境需要实现reset()和step(action)两个方法,符合 Gym 接口。光伏出力和负荷用历史曲线或者概率模型生成,每个 episode 模拟一天 24 小时,步长 15 分钟,共 96 步。
3.2 环境代码骨架
import pandapower as pp import numpy as np class VoltageControlEnv: """配电网电压控制仿真环境""" def __init__(self, net, pv_profile, load_profile): self.net = net self.pv_profile = pv_profile # shape (96, n_pv) self.load_profile = load_profile # shape (96, n_load) self.n_step = 96 self.current_step = 0 self.voltages = None self.prev_action = np.zeros(self.action_dim) @property def action_dim(self): # 储能功率 + SVC 无功 + 电容器档位 + OLTC 档位 return self.n_ess + self.n_svc + self.n_cap + 1 def reset(self): self.current_step = 0 self.prev_action = np.zeros(self.action_dim) self._apply_power_flow() return self._get_state() def step(self, action): # 动作映射到实际控制量 self._apply_action(action) self._apply_power_flow() reward = self._compute_reward(action) self.current_step += 1 done = self.current_step >= self.n_step next_state = self._get_state() if not done else None return next_state, reward, done, {} def _apply_power_flow(self): """运行潮流计算,获取各节点电压""" try: pp.runpp(self.net, algorithm='nr', numba=False) self.voltages = self.net.res_bus.vm_pu.values except pp.LoadflowNotConverged: # 潮流不收敛时给一个惩罚性电压 self.voltages = np.ones(len(self.net.bus)) * 0.5runpp是 pandapower 的潮流计算函数,algorithm='nr'指定牛顿-拉夫逊法。潮流不收敛在配电网里很常见,尤其是光伏出力大、电压越限严重的时候。这里给一个惩罚性电压 0.5 p.u.,让智能体学到「这种动作会导致潮流崩溃」,从而避开。
3.3 训练循环与关键超参数
import torch from collections import deque import random def train_sac(env, actor, critic, target_critic, replay_buffer, n_episodes=1000): """SAC 训练主循环""" gamma = 0.99 # 折扣因子 tau = 0.005 # 目标网络软更新系数 alpha = 0.2 # 熵温度系数 batch_size = 256 warmup_steps = 1000 # 预热步数,先随机探索 total_steps = 0 for episode in range(n_episodes): state = env.reset() episode_reward = 0 done = False while not done: if total_steps < warmup_steps: action = np.random.uniform(-1, 1, env.action_dim) else: with torch.no_grad(): state_t = torch.FloatTensor(state).unsqueeze(0) action, _ = actor.sample(state_t) action = action.squeeze(0).numpy() next_state, reward, done, _ = env.step(action) replay_buffer.push(state, action, reward, next_state, done) state = next_state episode_reward += reward total_steps += 1 if len(replay_buffer) >= batch_size and total_steps >= warmup_steps: # 从经验回放池采样并更新网络 batch = replay_buffer.sample(batch_size) update_networks(batch, actor, critic, target_critic, alpha, gamma, tau) if episode % 50 == 0: print(f"Episode {episode}, Reward: {episode_reward:.2f}")gamma=0.99意味着智能体关注未来约 100 步的累积奖励,对应 96 步的 episode 刚好覆盖一天。tau=0.005是目标网络软更新,比硬更新(每隔 N 步复制参数)更平滑。alpha=0.2是 SAC 的熵温度系数,控制探索强度,配电网控制里一般 0.1~0.3 比较合适。warmup_steps=1000是先随机探索填充经验池,避免一开始就用随机初始化的策略网络产生垃圾数据。
4. 训练不收敛、电压振荡、设备频繁动作:避坑与排查
4.1 奖励曲线震荡不上升
现象:训练了几百个 episode,奖励一直在 -5000 到 -3000 之间震荡,没有上升趋势。
原因:最常见的是状态归一化没做好,或者奖励系数量级不匹配。电压偏差惩罚如果是 -100 量级,而动作成本是 -0.01 量级,智能体根本感知不到动作成本,就会乱动。另一个原因是经验回放池太小,样本相关性太强。
解决:先打印每一步的各项奖励分量,确认量级。经验回放池至少 10 万条。如果状态里有量纲差异大的变量(比如电压 1.0 和功率 10.0),必须分别归一化。
4.2 电压在 0.95 和 1.05 之间来回振荡
现象:智能体学会了「打乒乓球」——电压高了就切电容器,切完电压低了又断开,一个 episode 里电容器动作几十次。
原因:奖励函数里设备动作成本太低,或者动作空间映射有问题。比如电容器档位从 0 到 5,智能体输出连续值 2.5,取整后变成 2 或 3,相邻步之间跳变。
解决:加大设备动作成本系数,尤其是离散设备。另外可以在动作输出后加一个低通滤波,或者用「动作保持」机制——连续 N 步动作不变才允许切换。
4.3 潮流不收敛导致训练崩溃
现象:训练到某个 episode,潮流计算频繁不收敛,奖励突然掉到 -10000 以下,之后再也恢复不了。
原因:智能体探索到了极端动作,比如储能满功率放电同时电容器全切,导致电压崩溃。潮流不收敛时如果给一个固定的惩罚电压,智能体可能学不到「避免崩溃」而是学到「崩溃也没事」。
解决:潮流不收敛时给一个非常大的负奖励(比如 -1000),并立即结束 episode。同时限制动作范围,储能功率不超过额定值,电容器档位有上下限。
4.4 训练好的策略换到测试场景就失效
现象:训练时奖励很好,换一组光伏出力曲线或者负荷曲线,电压越限严重。
原因:过拟合到训练场景。如果训练时只用了一条光伏曲线,智能体可能记住了「中午 12 点切电容器」,而不是学会了「电压高时切电容器」。
解决:训练时随机化光伏出力和负荷曲线,每个 episode 从历史数据里随机采样一天,或者在基准曲线上加高斯噪声。这叫域随机化,是提升泛化性的关键。
4.5 推理时动作抖动
现象:训练好的策略在实际部署时,相邻控制周期动作差异很大,设备频繁调节。
原因:SAC 是随机策略,推理时如果还采样,动作会有随机性。另外状态里的量测噪声也会导致策略输出抖动。
解决:推理时用确定性动作(取均值而不是采样)。状态输入前做滑动平均滤波。如果还抖,可以在动作输出后加一个变化率限制,比如相邻步动作变化不超过 0.1。
5. 从仿真到落地:策略迁移与在线微调的三个技巧
仿真里训练好的策略,直接搬到实际配电网,大概率翻车。原因很简单:仿真模型和实际网络有偏差,量测有噪声,通信有延迟。我一般用三个技巧做迁移。
第一,用实际量测做状态对齐。仿真里的电压是潮流计算出来的,实际是量测装置采的,两者有系统误差。部署前先收集一周实际量测数据,和仿真结果对比,如果某节点电压长期偏差 0.02 p.u.,就在状态输入里加一个偏置校正。这个校正值可以离线算,也可以在线自适应。
第二,在线微调而不是从头训练。把仿真预训练的策略网络作为初始化,在实际环境里用少量数据做微调。微调时学习率调小(比如 1e-5),只更新策略网络,不更新价值网络。这样既能适应实际网络,又不会把预训练学到的知识忘掉。
第三,加安全层兜底。不管策略输出什么动作,都经过一个安全校验层:如果动作会导致电压越限或者设备越限,就裁剪到安全范围。安全层用简单的规则实现,比如「电容器档位变化不超过 1 档/步」「储能功率不超过额定值」。这样即使策略输出异常,也不会造成实际危害。
def safety_layer(action, prev_action, voltages, action_limits): """ 安全层:裁剪动作到安全范围 action: 策略网络输出, [-1, 1] prev_action: 上一时刻动作 voltages: 当前电压 action_limits: 各动作维度的上下限 """ # 映射到实际控制范围 action_real = action * (action_limits[:, 1] - action_limits[:, 0]) / 2 \ + (action_limits[:, 1] + action_limits[:, 0]) / 2 # 限制相邻步变化率 max_delta = (action_limits[:, 1] - action_limits[:, 0]) * 0.1 delta = np.clip(action_real - prev_action, -max_delta, max_delta) action_real = prev_action + delta # 电压越限时强制动作 if np.any(voltages > 1.05): # 电压偏高,增加无功吸收或降低分接头 action_real[0] = min(action_real[0], 0) # 储能吸收无功 if np.any(voltages < 0.95): action_real[0] = max(action_real[0], 0) # 储能发出无功 return action_real这个安全层的关键是「变化率限制」和「越限强制动作」。变化率限制防止设备频繁调节,越限强制动作保证极端情况下电压能被拉回来。实际部署时,安全层的规则可以写得更细,比如考虑设备动作次数寿命、通信延迟补偿等。
最后说一个我自己的教训:早期做这个方向时,我花了两周调奖励函数,怎么调都不收敛。后来发现是状态里的电压没有归一化,范围在 0.9 到 1.1 之间,而光伏出力归一化后在 0 到 1 之间,神经网络对这两个量纲的敏感度差异巨大,导致策略网络学偏了。归一化做完,当天就收敛了。所以如果你也在做深度强化学习电压控制,先把状态归一化和奖励量级检查一遍,能省很多时间。希望帮到你。
本文还有配套的精品资源,点击获取