简介一份面向电力系统控制与深度强化学习研究者的技术PDF针对现代电网高不确定性下传统离线控制方案适应性不足的问题系统梳理了基于DRL的自适应紧急控制方法。内容涵盖开源平台RLGC的应用、发电机动态制动与低压减载两类场景设计以及两区域四机系统和IEEE 39总线系统的实验验证重点展示了DRL在高维状态空间中的特征提取与端到端学习优势。资源为单份PDF文档大小约767KB适合具备编程基础、希望将DQN等算法落地到电力系统控制的科研人员和工程师。文档不仅给出理论解析还提供了环境建模、DQN模型构建、训练流程及鲁棒性测试等详细代码示例可帮助读者快速复现核心实验并理解实现细节。目前已有107人学习对于入门DRL电力系统控制具有较高的参考价值。1. 为什么要用深度强化学习重做电力系统紧急控制传统的电力系统紧急控制说白了就是“查表”。调度中心离线算出一堆典型工况下的切机、切负荷方案做成决策表等到故障真的发生了就去表里找最接近的那个工况照着执行。这套思路在电网结构相对固定、运行方式变化不大的时候是够用的。但最近这几年的实际情况是新能源占比越来越高、交直流混联越来越复杂电网的运行方式每天都在变离线表里的场景根本覆盖不全。我自己在仿真里就遇到过这种情况同一套切负荷策略在某个拓扑下能把暂态电压稳稳压住换一个拓扑同样的动作反而会让频率二次跌落。深度强化学习解决的就是这个“适应性”问题。它的核心逻辑不是记住某个场景下“该切多少”而是通过跟环境大量交互学出一个从“系统当前状态”到“控制动作”的映射策略。也就是说不管系统此刻运行在什么方式下只要把关键状态量喂给策略网络它就能输出一个适合当下情况的动作。这个思路在AlphaGo下棋、机器人走路这些领域已经被验证过了搬到电力系统紧急控制上本质上是一样的——把电网当成一个环境把切负荷当成动作把“维持稳定且经济”当成得分标准。这篇博文我会把整个项目拆开来讲环境怎么搭、动作空间怎么定、奖励函数怎么设计、网络怎么训、踩了哪些坑。附带可以跑的代码尽量做到你照着抄也能复现出一版能用的紧急控制智能体。2. 环境建模状态空间、动作空间与奖励函数的设计逻辑强化学习里面有一句话叫“无环境不学习”。环境的建模质量直接决定策略能用不能用。很多人在这一步就翻车了因为他们把“电力系统仿真”和“强化学习环境”混为一谈。仿真器算的是电磁暂态或机电暂态但强化学习环境要回答的是另外三个问题智能体看什么、智能体做什么、智能体怎么评分。2.1 状态空间智能体需要看哪些量状态空间的设计原则是“能反映系统稳定程度的信息都要给”。我最终选定了这样一组状态量各台发电机的转速偏差Δω它反映频率稳定程度各关键母线的电压幅值V它反映电压稳定程度关键联络线的有功功率P它反映潮流重载情况母线电压相角差Δθ它反映功角稳定趋势。有人可能会问为什么不把全部节点的电压和相角都放进去一是维度太高训练收敛会很慢二是很多信息是冗余的智能体反而会被噪声干扰。我的做法是用一套筛选逻辑先基于拓扑分析挑出对稳定性影响最大的关键母线再把这些母线的量测构成状态向量。这里有一个实操细节状态量必须做归一化。发电机转速偏差大概在±0.05 pu范围母线电压在0.8~1.2 pu范围联络线功率可能在±1000 MW范围如果不归一化直接喂给神经网络梯度更新会被大数值的量主导小数值的量根本学不到东西。我用的方法是对每个量减去典型值再除以基准值比如电压用1.0 pu做基准Δω用0.01 pu做基准。2.2 动作空间切负荷控制的连续化处理紧急控制里最常用的手段之一是切负荷。切负荷在物理上是离散的一条馈线要么切要么不切。但标准的深度强化学习算法里DQN这类离散动作算法在处理“多节点多档位”的动作组合时动作空间会爆炸。比如10个可切节点每个节点分5档动作总数就是5的10次方根本没法训。我的做法是把动作设计成连续值每个可切节点输出一个[0, 1]之间的实数代表切负荷比例。这样动作空间从指数级压缩成了10维连续空间用DDPG或TD3这类连续控制算法就能处理。等策略训练好了实际执行时再把这个连续比例映射到最接近的离散档位。这个连续化处理有一个隐藏好处策略网络学到的其实是一个“光滑”的映射——系统越接近失稳边沿输出的切负荷比例越大系统越稳定输出越接近0。这种光滑性让策略具备了一定的泛化能力即使遇到训练时没见过的工况输出的动作也大概率是合理的。2.3 奖励函数让智能体自己权衡“稳”和“省”奖励函数是整个环境建模的灵魂。设计得太简单智能体会钻空子设计得太复杂训练半天不收敛。我参考了多篇文献后定了一个加权组合形式的奖励R R_stability R_economyR_stability这一项用“电压偏差惩罚”和“频率偏差惩罚”来刻画系统是否稳定。具体做法是每一步仿真结束后用当前电压和频率相对额定值的偏差来计算惩罚系统越偏离额定值惩罚越大。如果不稳定直接判失败给一个很大的负分。R_economy则是切负荷代价。每切掉1 MW负荷都记录一个经济惩罚系数取0.1也就是切得越多代价越大。这样设计之后智能体的行为倾向就变成了在能维持稳定的前提下尽量少切负荷。我实测下来这个奖励组合能让策略收敛到一个比较合理的平衡点——轻微故障时几乎不动作严重故障时才下狠手。3. 核心代码实现从仿真环境到训练主循环下面这部分是真正的干货我会把项目里最关键的三段代码贴出来逐行拆解。完整工程包含环境封装、DDPG智能体、训练脚本和可视化模块这里挑选最核心的部分说明。先说明一下我这里用的是Python Gym风格的环境接口底层仿真用的是电力系统暂态仿真程序但接口做得比较通用。3.1 紧急控制环境类框架import numpy as np import gym from gym import spaces class PowerSystemEmergencyEnv(gym.Env): 电力系统紧急控制环境 状态: 发电机转速偏差、关键母线电压、联络线功率、相角差 动作: 各可切节点的切负荷比例(连续, 0~1) def __init__(self, psse_case, controllable_buses, obs_scale): super().__init__() self.controllable_buses controllable_buses self.obs_scale obs_scale # 动作空间: 每个可控节点一个连续维度 self.action_space spaces.Box( low0.0, high1.0, shape(len(controllable_buses),), dtypenp.float32 ) # 状态空间: 由系统关键量测维数决定 self.obs_dim self._get_obs_dim() self.observation_space spaces.Box( low-np.inf, highnp.inf, shape(self.obs_dim,), dtypenp.float32 ) self.current_step 0 self.max_steps 200 self._load_case(psse_case) def _get_obs_dim(self): # 发电机台数*1(转速偏差) 关键母线数*1(电压) 联络线数*1(功率) 关键母线数*1(相角差) return len(self.gen_list) len(self.key_buses) len(self.tie_lines) len(self.key_buses) def reset(self, fault_typethree_phase, fault_busNone): 重置到故障前状态, 然后注入故障 self._init_steady_state() self.current_step 0 self._apply_fault(fault_type, fault_bus) obs self._get_observation() return obs def step(self, action): 执行切负荷动作, 推进仿真, 返回(obs, reward, done, info) self.current_step 1 # 将连续动作映射到切负荷档位 shed_ratios self._map_action_to_shed(action) # 在仿真中执行切负荷 self._execute_load_shedding(shed_ratios) # 推进暂态仿真一个步长 self._advance_simulation() obs self._get_observation() reward, done self._compute_reward_and_done() info { step: self.current_step, voltage_profile: self._get_voltage_profile(), shed_amount: np.sum(shed_ratios) } return obs, reward, done, info这段代码是环境的主干。step函数里_map_action_to_shed做的事情是把连续的动作值映射为具体的切负荷量这里我用了一个简单但有效的策略当动作值大于0.05时才实施切除小于0.05视为不动作。这样可以防止智能体总是给一个很小的正数来“糊弄”环境提高动作的稀疏性训练也会更稳定。_execute_load_shedding是在仿真器里把指定母线的负荷按比例减掉。这一步在实际工程里要跟仿真器的API对接好否则很容易出现切了负荷但潮流不更新的情况。我踩过这个坑后面会专门说。3.2 归一化后的观测处理强化学习里输入观测的处理直接决定训练效率。我给环境加了一个归一化步骤同时把观测拼成一个一维向量供网络消费def _get_observation(self): # 从仿真器读取原始量测 raw_obs { delta_omega: self._read_gen_speed_deviation(), # shape: (n_gen,) voltage: self._read_bus_voltage(self.key_buses), # shape: (n_bus,) tie_power: self._read_tie_line_power(), # shape: (n_line,) theta_diff: self._read_bus_angle(self.key_buses) # shape: (n_bus,) } # 逐项归一化 obs_list [] obs_list.append(raw_obs[delta_omega] / self.obs_scale[delta_omega]) obs_list.append((raw_obs[voltage] - 1.0) / self.obs_scale[voltage]) obs_list.append(raw_obs[tie_power] / self.obs_scale[tie_power]) obs_list.append(raw_obs[theta_diff] / self.obs_scale[theta_diff]) return np.concatenate(obs_list).astype(np.float32)电压基准取1.0是因为标幺值系统下额定电压就是1.0电压偏差本身就是有界量。相角差在稳定运行时通常不超过30度按30度归一化后基本能落到[-1, 1]区间。这里的所有scale参数都需要在训练前从一组典型仿真中统计出来不能拍脑袋定。3.3 奖励计算的工程化处理奖励计算有一个特别容易踩的坑仿真器返回的电压和频率是一堆曲线不是单点值你取哪个时刻的值计算惩罚是有讲究的。我采用的是“滑窗内最差值”策略def _compute_reward_and_done(self): # 取当前窗口内最差的电压偏差和频率偏差作为稳定惩罚依据 v_min np.min(self.voltage_history[-10:]) # 最近10步的最低电压 v_max np.max(self.voltage_history[-10:]) # 最近10步的最高电压 f_dev_max np.max(np.abs(self.freq_history[-10:] - 50.0)) voltage_penalty 0.0 if v_min 0.80: voltage_penalty (0.80 - v_min) * 10.0 if v_max 1.20: voltage_penalty (v_max - 1.20) * 10.0 if f_dev_max 0.5: voltage_penalty (f_dev_max - 0.5) * 5.0 voltage_penalty np.clip(voltage_penalty, 0.0, 20.0) # 经济惩罚: 切负荷总量 shed_economy_penalty 0.1 * self.total_shed_mw / 100.0 # 失稳判据 done False if self._is_unstable(): reward -50.0 done True return reward, done if self.current_step self.max_steps: done True reward 10.0 - voltage_penalty - shed_economy_penalty return reward, done reward -voltage_penalty - shed_economy_penalty return reward, done取最差值而不是当前值的原因在于暂态过程中电压和频率是振荡的某些时刻可能已经恢复但系统其实还在临界状态。如果只取当前值智能体可能学会“赌”——只要当前时刻是安全的就算赢实际上系统已经快崩了。取滑窗最差值能逼着智能体关注整个暂态过程的安全性训练出来的策略往往更保守、更可靠。_is_unstable这个函数里我综合了三个判据电压持续低于0.7 pu超过0.5秒、频率偏差超过1 Hz、功角差超过180度。任何一个成立都判为失稳。4. 策略网络与DDPG智能体实现从原理到逐行debug4.1 为什么选DDPG而不是其他算法选择算法的时候我做了一个对比DQN只能处理离散动作前面说过动作空间爆炸的问题直接排除PPO是on-policy算法采样效率相对低每个episode的数据用完就扔而电力系统仿真本身就慢PPO的训练时间会非常可观DDPG是off-policy算法用经验回放池把历史数据反复利用样本利用率高连续动作控制也是它的强项。最终选了DDPG。它的核心结构是Actor-Critic双网络Actor做策略拟合输入状态输出动作Critic做价值评估输入状态和动作输出Q值。两个网络各带一个目标网络通过软更新来稳定训练过程。这套结构有点像一个新员工Actor和一位老专家Critic新员工提出方案老专家打分新员工根据打分调整思路老专家的标准也在缓慢变化但变得不快以免新员工头晕。4.2 Actor-Critic网络结构定义import torch import torch.nn as nn import torch.nn.functional as F class Actor(nn.Module): 策略网络: 状态 - 动作 def __init__(self, state_dim, action_dim, hidden_dim256): super().__init__() self.fc1 nn.Linear(state_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.fc3 nn.Linear(hidden_dim, action_dim) self._init_weights() def _init_weights(self): # 初始化策略网络参数, 使初始输出接近0(不动作) nn.init.xavier_uniform_(self.fc1.weight) nn.init.xavier_uniform_(self.fc2.weight) nn.init.uniform_(self.fc3.weight, -0.003, 0.003) nn.init.uniform_(self.fc3.bias, -0.003, 0.003) def forward(self, state): x F.relu(self.fc1(state)) x F.relu(self.fc2(x)) return torch.sigmoid(self.fc3(x)) # 输出0~1切负荷比例 class Critic(nn.Module): 价值网络: (状态, 动作) - Q值 def __init__(self, state_dim, action_dim, hidden_dim256): super().__init__() self.fc1 nn.Linear(state_dim action_dim, hidden_dim) self.fc2 nn.Linear(hidden_dim, hidden_dim) self.fc3 nn.Linear(hidden_dim, 1) def forward(self, state, action): x torch.cat([state, action], dim1) x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) return self.fc3(x)Actor输出层用sigmoid激活保证动作落在[0,1]区间正好对应切负荷比例。这里有一个细节最后一层权重初始化范围取±0.003而不是Xavier初始化这是为了让智能体一开始几乎不动作。如果初始就随机输出大比例切机切负荷前期采集到的经验会全是失稳场景价值网络根本学不到有效信息。这个小技巧能明显提升前期训练的稳定性。4.3 DDPG更新逻辑与经验回放class DDPG: def __init__(self, state_dim, action_dim, lr3e-4, gamma0.99, tau0.005): self.actor Actor(state_dim, action_dim) self.critic Critic(state_dim, action_dim) self.target_actor Actor(state_dim, action_dim) self.target_critic Critic(state_dim, action_dim) # 目标网络初始化为与在线网络相同 self.target_actor.load_state_dict(self.actor.state_dict()) self.target_critic.load_state_dict(self.critic.state_dict()) self.actor_optim torch.optim.Adam(self.actor.parameters(), lrlr) self.critic_optim torch.optim.Adam(self.critic.parameters(), lrlr) self.gamma gamma self.tau tau self.replay_buffer [] self.buffer_capacity 100000 self.batch_size 256 def select_action(self, state, noise0.0): 选择动作, 添加探索噪声 state torch.FloatTensor(state).unsqueeze(0) action self.actor(state).detach().cpu().numpy().squeeze() if noise 0: action np.clip(action np.random.normal(0, noise, sizeaction.shape), 0, 1) return action def update(self): 从经验池采样batch更新网络 if len(self.replay_buffer) self.batch_size: return 0.0, 0.0 batch random.sample(self.replay_buffer, self.batch_size) states torch.FloatTensor(np.array([b[0] for b in batch])) actions torch.FloatTensor(np.array([b[1] for b in batch])) rewards torch.FloatTensor(np.array([b[2] for b in batch])).unsqueeze(1) next_states torch.FloatTensor(np.array([b[3] for b in batch])) dones torch.FloatTensor(np.array([b[4] for b in batch])).unsqueeze(1) # 更新Critic target_actions self.target_actor(next_states) target_q self.target_critic(next_states, target_actions) y rewards (1 - dones) * self.gamma * target_q current_q self.critic(states, actions) critic_loss F.mse_loss(current_q, y.detach()) self.critic_optim.zero_grad() critic_loss.backward() self.critic_optim.step() # 更新Actor actor_loss -self.critic(states, self.actor(states)).mean() self.actor_optim.zero_grad() actor_loss.backward() self.actor_optim.step() # 软更新目标网络 self._soft_update(self.actor, self.target_actor) self._soft_update(self.critic, self.target_critic) return critic_loss.item(), actor_loss.item() def _soft_update(self, net, target_net): for target_param, param in zip(target_net.parameters(), net.parameters()): target_param.data.copy_( self.tau * param.data (1.0 - self.tau) * target_param.data )经验回放池我设了10万条容量每次采样256条更新。5000个episode训练下来回放池基本会被填满并不断滚动更新。这样做的好处是打破样本之间的时间相关性——如果按时间顺序连续学智能体会被最近的经历带着跑偏回放机制相当于把历史的好经验和坏经验混在一起给智能体“复习”学习过程稳得多。4.4 训练主循环最容易忽略的“噪声衰减”def train(env, agent, episodes2000): total_rewards [] noise_std 0.3 noise_decay 0.999 for episode in range(episodes): obs env.reset() episode_reward 0 done False step_count 0 while not done: action agent.select_action(obs, noisenoise_std) next_obs, reward, done, info env.step(action) # 存入经验池 agent.replay_buffer.append( (obs, action, reward, next_obs, done) ) obs next_obs episode_reward reward step_count 1 # 更新网络 agent.update() # 限制单episode最大仿真步数, 防止死循环 if step_count 300: break noise_std * noise_decay total_rewards.append(episode_reward) if episode % 50 0: print(fEpisode {episode}, Reward: {episode_reward:.1f}, fNoise: {noise_std:.3f}, Steps: {step_count}) return total_rewards噪声衰减是训练前中期非常关键的一环。初始噪声标准差取0.3意味着智能体前期一半靠策略、一半靠瞎试。随着训练推进噪声逐渐衰减智能体的输出越来越依赖学到的策略而不是随机探索。如果噪声不衰减或者衰减太慢智能体后期学到的策略会被噪声掩盖表现为reward曲线一直在某个水平震荡上不去。如果衰减太快前期探索不够充分策略会陷入局部最优。我训练的过程中发现噪声衰减到0.01以下后策略基本稳定后续再怎么训练reward曲线也只有小幅波动。5. 训练效果与调参经验那些文档里不会写的坑5.1 训练收敛曲线解读我把训练过程分成三个阶段来看前500个episodereward基本在-30到-50之间徘徊这个阶段智能体在“乱试”大部分情况下切负荷要么切太多把系统搞崩了要么切太少没稳住。核心网络还在学状态和动作之间的关系。500到1500个episodereward开始缓慢爬升稳定在-10到0之间。这个阶段智能体已经学会了基本规律故障越严重动作越大、系统越稳定动作越小。但还不够精细经常出现过度切除。1500个episode之后reward曲线趋于平稳智能体在大多数故障场景下都能以较小的切负荷代价维持系统稳定。这时候让我比较惊喜的是它有了一定的外推能力——我把故障持续时间从0.1秒调长到0.15秒它依然能在两个仿真步长内做出合理的切负荷决策而传统查表方案在同样情况下需要重新离线计算。5.2 训练中的典型问题排查表这里列一下我训练过程中遇到的高频问题以及对应的排查方向现象可能原因排查建议训练初期reward始终在-50左右动作空间太大智能体在随机探索阶段频繁触发失稳减小初始噪声或给动作加一个“先验偏置”reward曲线中期出现剧烈震荡经验池中极端场景严重故障占比过高调整故障场景采样分布减少严重故障出现的概率策略只学会大比例切负荷经济惩罚权重太小稳定惩罚相对过大适当调大经济惩罚系数让智能体更多考虑切负荷代价泛化测试时遇到训练集外的拓扑直接失效训练场景太少状态空间覆盖不足增加多种拓扑场景或使用域随机化方法让状态分布更宽关于“先验偏置”我再展开说一下。DDPG在训练初期是纯随机探索对电力系统这种“动作做错一步就可能失稳”的环境来说探索成本太高。一个改进办法是给Actor的初始输出加一个可学习的偏置项让它在训练开始时就更倾向于输出一个保守的小动作。我在部分实验里用了一个更简单的技巧把动作输出乘以0.1再送入环境相当于压缩了探索幅度效果也不错。5.3 奖励函数权重调节的实操经验奖励函数的权重调节没有标准答案跟你的应用场景强相关。我自己的经验是分两轮调第一轮先把稳定惩罚的权重调大确保智能体能学出“保证稳定”的基本策略这个阶段经济惩罚系数可以设得很小比如0.02。第二轮等策略基本稳住系统之后再逐步增大经济惩罚系数观察切负荷量是否下降、稳定性是否还能保持。我最后定在0.1是因为在这个值附近策略的切负荷量有明显下降而稳定裕度依然充足。有一个细节点经济惩罚的绝对值要跟电压惩罚在同一个数量级否则梯度会被一方主导。比如电压越限0.2 pu要惩罚2分切100 MW负荷如果罚20分那经济惩罚就太大了智能体可能为了省钱直接放弃稳定。我代码里的惩罚量纲都控制在0~20之间就是为了平衡这两个目标。6. 从仿真到工程落地的额外提醒训练好的策略离真正部署还有一段距离。我这里提几个工程落地中容易忽视的点供参考。第一仿真器与策略的网络通信延迟问题。我的环境里每个step要调用一次暂态仿真程序单次仿真耗时从几毫秒到几百毫秒不等。训练时还好但实际部署时如果策略决策周期跟仿真步长不匹配会出现动作“迟到”的现象。解决办法是部署时把策略做成异步调用提前一个控制周期计算动作或者用轻量级的代理模型替代完整仿真。第二切负荷指令下发到实际执行终端的过程在仿真里被完全忽略了。真实系统里切负荷指令有通信延迟、有执行机构响应时间这些都需要在环境建模时建模进去否则训练出的策略在真实系统上会“水土不服”。我后来在环境里加了一个执行延迟模块强制动作延迟一个控制周期才生效训练出的策略明显更保守但实际落地效果更可靠。第三把训练好的策略导出成部署格式。PyTorch模型转成ONNX之后可以部署到边缘计算节点上。电力系统调度对可靠性要求极高部署前一定要做充分的IEC 61850协议兼容性测试这里就不再展开了。本文还有配套的精品资源点击获取
