☰
Matlab+Matpower实战:生成IEEE14节点FDIA攻击数据集
2026/10/2 1:03:19 网站建设 项目流程

做电力数据驱动研究,最难搞的往往不是模型,而是数据集。特别是电网FDIA(虚假数据注入攻击)这种带标签的攻击样本,公开资源少、获取门槛高,很多刚入门同学就卡在第一步:怎么生成一批“干净”、可复现、能直接喂给机器学习模型的攻击数据集?我自己在实际项目里正好用Matlab+Matpower把这事完整跑通过,这里把整套流程整理出来,从数学原理到可以直接运行的代码,手把手走一遍。

这套方案的核心载体是IEEE14节点系统,这是电力系统领域最经典的小型测试算例之一,规模不大但结构完整,特别适合用来做状态估计、攻击注入、检测算法验证这类实验。本文的目标很明确:在Matlab环境下,通过Matpower加载IEEE14节点系统,搭建直流潮流状态估计模型,在此基础上构造FDIA攻击向量,批量生成带正常/攻击标签的仿真数据集,最终得到可直接用于机器学习训练的.csv或.mat文件。适合以下读者参考:正在做电网安全研究、电力系统机器学习方向的学生,或者刚接触Matpower、想快速上手状态估计和攻击数据构造的工程人员。

1. 先搞清楚:FDIA攻击数据集到底要解决什么问题

1.1 从一次“完美犯罪”说起

FDIA全称是False Data Injection Attack,虚假数据注入攻击。它针对的是电力系统状态估计模块。正常情况下,调度中心靠状态估计来从一堆遥测数据里还原系统真实运行状态,如果某个测量值明显异常,会触发坏数据检测机制,把这个测量值踢掉或者报警。

FDIA的攻击思路非常“聪明”:攻击者如果知道当前电网拓扑和线路参数,就能构造一个特殊的攻击向量,把它叠加到正常测量值上。这个向量经过精心设计后,会让状态估计结果向着攻击者想要的方向偏移,但测量残差却和正常情况几乎一样。结果就是坏数据检测完全失效,调度员看到的仍然是一幅“岁月静好”的系统画面,实际上运行状态已经被悄悄地篡改了。

这套思路放到机器学习里就很自然:我们需要的正是“攻击前”和“攻击后”两种状态的带标签样本。一份规范的FDIA攻击数据集,至少应该包含:每一组测量向量、对应的攻击标签、攻击强度或攻击向量本身、以及状态估计的残差等信息。只有拿到足够多样本,才能训练出能识别这种隐蔽攻击的检测模型。

1.2 IEEE14节点系统为什么适合做入门载体

IEEE14节点系统来自美国中西部电网的简化模型,包含14条母线、20条支路、5台发电机组和11个负荷节点,系统规模和复杂度介于“玩具模型”和“真实大电网”之间。用一句话概括:麻雀虽小,五脏俱全。

用它来生成FDIA数据集有几个现实的好处。第一,状态数量少,计算速度快。直流状态估计下只有13个非参考母线相角状态,非常适合反复批量仿真。第二,测量配置可以自由扩展。无论是全节点注入功率测量,还是只选部分支路潮流,都很容易在代码层面控制。第三,拓扑信息由Matpower内置函数直接提供,不需要手工录入任何一张表,减少了出错概率。很多学术论文里的FDIA检测实验也是先在这个系统上验证,再推广到IEEE30、IEEE118等更大规模的算例。

我自己的经验是:14节点系统虽然简单,但足够暴露几乎所有实现层面的细节问题——H矩阵怎么构造、残差阈值怎么定、攻击强度怎么控制。把这一步跑通,后面换更大的系统只是改一行加载函数的事。

1.3 这套数据集能用在哪些场景

做出来的数据集一般有三种用途。第一种是最常见的,训练检测模型。把正常样本和攻击样本混合,标签就是0/1二分类,可以接机器学习分类器,也可以接深度学习网络。第二种是验证传统检测算法。用卡方检测、最大残差检测这些经典方法跑一遍,看它们在FDIA攻击面前是不是真的“瞎了”,这是很多论文里必放的一张图。第三种是做攻击强度与风险的定量分析。通过控制攻击向量c的幅值,观察不同攻击强度下状态偏移量和检测率的曲线关系。

无论哪种用途,底层都需要一份数据分布合理、生成逻辑透明的数据集。这也是我这篇文章的重点:不仅要给出代码,还要把每个数据是怎么算出来的讲明白。

2. 环境准备:Matlab与Matpower的配置

2.1 安装Matlab与Matpower

Matlab方面,个人建议使用R2019b及以上版本,太老的版本有些矩阵运算和统计函数接口不全。安装过程不多说,重点提醒一句:路径中尽量不要出现中文和空格,否则后面跑批量仿真时容易出各种诡异问题。

Matpower是一个基于Matlab的开源电力系统潮流计算工具箱,官方网站直接下载压缩包即可。我目前用的是Matpower 7.1,功能稳定,和新版Matlab兼容性也很好。下载完成后解压,在Matlab里添加路径,注意要把主目录连同子目录一起添加:

addpath(genpath('D:\toolbox\matpower7.1')); savepath;

genpath会自动把Matpower内部所有子文件夹都加进搜索路径,这一步很关键。很多人后面运行runpf报“未定义函数”错误,多半就是子目录没加全。

2.2 加载IEEE14节点系统库

Matpower自带的测试系统都在data目录下,IEEE14节点对应的是case14。加载方式非常简单:

mpc = loadcase('case14');

返回的mpc是一个结构体,包含bus、branch、gen三个核心矩阵。其中bus矩阵的第3列是母线有功负荷,第4列是无功负荷,第9列是电压相角;branch矩阵的第1列、第2列是支路首末端母线编号,第4列是支路电抗x。这些字段后面构造测量矩阵时全都要用到。

建议刚上手时先运行下面这段,确认基础潮流能正常收敛:

mpc_flow = runpf(mpc, mpoption('OUT_ALL', 0)); fprintf('潮流计算收敛,结果保存于 mpc_flow\n');

正常情况下runpf会输出一堆迭代信息,最后显示收敛标记。如果这一步都过不去,后面所有内容都无从谈起。

2.3 用runpf确认基础潮流收敛

潮流计算是整个数据集生成流程的真实数据来源。我们后续的“真实状态”全部来自潮流计算结果,而不是自己随便假设一组相角。这样生成的数据才符合电力系统物理规律。

这里补充一点:FDIA数据集生成的常规做法是在一个基准工作点附近做随机扰动,模拟多时段的运行工况。每次扰动后都要重新跑一遍潮流,得到新的相角作为该时段的真实状态。runpf虽然速度不慢,但如果后面要生成几千个样本,建议把输出关掉,也就是上面代码里的OUT_ALL',0,否则终端会被刷屏,速度也受影响。

3. 状态估计与FDIA攻击的数学原理

3.1 DC状态估计的测量模型

完整的交流状态估计是非线性问题,计算复杂,对初学者不太友好。实际生成FDIA数据集时,业界非常流行先在线性化的直流潮流模型上做,原理清晰、实现简单,且能完整体现FDIA攻击的核心逻辑。交流模型下的攻击构造思路是一样的,只是把线性矩阵换成每个工作点重新计算的雅可比矩阵。

直流潮流模型下,支路k的有功潮流可以近似表示为:

Pij = (θi - θj) / xij

其中θi、θj是母线i、j的电压相角,xij是支路电抗。如果把全部非参考母线的相角组成状态向量x,那么测量值和状态之间就是线性关系:

z = H * x + e

这里的H是测量矩阵,也叫灵敏度矩阵;e是测量噪声。测量值z可以全部选支路有功潮流,也可以加一部分母线有功注入,形成冗余测量。冗余度越高,状态估计的鲁棒性越强,FDIA攻击的难度也相应增加。

3.2 加权最小二乘状态估计

状态估计最经典的方法是加权最小二乘(WLS)。它的目标是最小化加权残差平方和:

J(x) = (z - Hx)' * W * (z - Hx)

其中权重矩阵W一般取测量误差方差的倒数对角阵。因为是线性模型,最优解有闭式表达式:

x_hat = (H' * W * H) \ (H' * W * z)

得到状态估计值后,残差为:

r = z - H * x_hat

传统坏数据检测的思路就是看残差是否超限。正常情况下残差服从特定分布,残差平方和J不应超过卡方分布的一个阈值。这就是后面代码中chi2inv的由来。

3.3 FDIA攻击向量的构造逻辑

FDIA的核心思想是构造一个攻击向量a,使得攻击后量测z_a = z + a,状态估计结果偏移到x_hat + c,但残差不变。这里的c是攻击者希望注入的状态偏移量。

关键是:如果攻击者知道测量矩阵H,那么令:

a = H * c

代入残差表达式就能看到:

z_a - H * (x_hat + c) = z + Hc - H*x_hat - Hc = z - H*x_hat

残差和攻击前一模一样,坏数据检测完全失效。这意味着攻击者不改变任何测量残差,就成功篡改了调度中心看到的系统状态。

注意到攻击向量并不需要人为指定到每个测量值上。只要选定一个随机的c,然后乘以H,就能自动得到一组满足隐蔽性条件的攻击向量。这也是代码里最精妙的地方:攻击向量的计算是一步矩阵乘法,而不是逐个测量值去设计。

4. 核心代码:一步一步生成IEEE14 FDIA数据集

4.1 通用参数与仿真初始化

下面进入正题。打开Matlab,新建一个脚本,保存为gen_fdia_data.m。首先做一些全局初始化:

clear; clc; close all; rng(2024); % 固定随机种子,保证结果可复现 mpc = loadcase('case14'); nb = size(mpc.bus, 1); % 母线数量 14 nl = size(mpc.branch, 1); % 支路数量 20 ref = 1; % 参考母线编号 state_num = nb - 1; % 去掉参考母线的状态数量,13

固定随机种子是个很容易被忽略但非常关键的细节。同样的代码,不固定种子每次生成的数据都不一样,论文实验结果无法复现。建议生成正式数据集前先固定种子,并记录种子的值。

4.2 构造DC潮流测量矩阵H

构造H矩阵是整个流程最核心也最容易出错的一步。我们先搭建支路潮流的测量矩阵,再叠加母线注入测量。

branch = mpc.branch; fb = branch(:, 1); % 首端母线 tb = branch(:, 2); % 末端母线 x_ij = branch(:, 4); % 支路电抗 b_ij = 1 ./ x_ij; % 支路电纳 Hf = zeros(nl, state_num); % 支路潮流的测量矩阵 for k = 1:nl i = fb(k); j = tb(k); if i == ref Hf(k, j-1) = -b_ij(k); elseif j == ref Hf(k, i-1) = b_ij(k); else Hf(k, i-1) = b_ij(k); Hf(k, j-1) = -b_ij(k); end end

这里为什么要把母线编号减1?因为参考母线的相角被固定为0,不作为状态量。当支路一端是参考母线时,自由度就少了一个。常见错误是把参考母线也当成状态,导致H矩阵列数多1,后面矩阵乘法全部对不上。

接着构造母线注入功率的测量矩阵。母线i的注入功率,等于与该母线相连的所有支路潮流之和:

Hbus = zeros(nb, state_num); for k = 1:nl Hbus(fb(k), :) = Hbus(fb(k), :) + Hf(k, :); Hbus(tb(k), :) = Hbus(tb(k), :) - Hf(k, :); end

这里有个物理细节:从母线流出的功率取负号,流入取正号。支路k对首端母线来说是流出,所以Hbus(fb(k),:)加上Hf(k,:);对末端母线来说是流入,所以减去。

最终把测量矩阵组合起来。我选的测量配置是所有支路潮流加上所有非参考母线的注入功率:

H = [Hf; Hbus(2:end, :)]; meas_num = size(H, 1); % 20 + 13 = 33 个测量量

这种配置下,测量数33,状态数13,冗余度20,比满秩多出20个方程,满足可观测条件。如果想做不同测量配置的对比实验,只需在这里调整H的组装方式即可。

4.3 模拟正常测量值

测量数据的“真实状态”来自潮流计算。我们对基准case14跑一次潮流,得到各母线相角,然后取非参考母线部分作为真实状态:

mpc_flow = runpf(mpc, mpoption('OUT_ALL', 0)); theta_true = mpc_flow.bus(:, 9) * pi / 180; % 相角转弧度 x_true = theta_true(2:end); % 去掉参考母线 z_true = H * x_true; % 理想无噪声测量 sigma = max(0.001, abs(z_true) * 0.01); % 测量误差标准差 noise = randn(meas_num, 1) .* sigma; % 高斯测量噪声 z_meas = z_true + noise; % 实际测量值

关于噪声标准差,这里我采用了相对误差加底噪的做法。绝对误差统一设为0.001,再叠加测量幅值1%的相对误差。这样做的好处是:支路潮流小的测量值不会被噪声淹没,支路潮流大的测量值又能体现出合理的波动幅度。实际工程中,PMU和SCADA的量测误差特性不同,可以根据需要调整。

然后做一次WLS状态估计,得到正常情况下的估计值和残差:

W = diag(1 ./ sigma.^2); G = H' * W * H; x_hat = G \ (H' * W * z_meas); r = z_meas - H * x_hat; J_norm = r' * W * r;

G是信息矩阵,在测量配置不变时它是固定的。后面所有样本的WLS求解都可以复用这个矩阵,不用每次都重新计算,能省不少时间。

4.4 注入FDIA攻击向量

现在开始构造攻击。核心逻辑就三行:生成随机状态偏移c,让攻击向量a = H * c,然后把攻击向量加到测量值上。

attack_amp = 0.02; % 攻击强度控制参数 c = (rand(state_num, 1) - 0.5) * attack_amp; a = H * c; z_attack = z_meas + a;

这里的attack_amp表示攻击者希望注入的状态偏移幅度,单位是弧度。0.02弧度大约1.15度,对IEEE14节点系统来说是一个既明显又不过分的偏移量。实际使用中建议做一个参数扫描,看看不同攻击强度下数据的区分度。

但直接随机生成的c有可能让攻击向量a的某些分量特别大,明显超出正常测量范围,反而容易被识别。所以一般会加一个约束,只保留攻击幅度可控的样本:

max_a = max(abs(a)); if max_a > max(abs(z_true)) * 0.2 % 攻击向量过大,重新生成或做比例缩放 c = c * (max(abs(z_true)) * 0.2 / max_a); a = H * c; z_attack = z_meas + a; end

这种方式保证攻击向量幅值不超过正常测量最大值的20%,让攻击更隐蔽。如果完全不约束攻击幅度,生成的攻击样本很容易被简单阈值规则识别,数据集就失去了研究价值。

4.5 WLS估计与残差检测

攻击后的测量值送入同样的WLS估计器,看看会发生什么:

x_hat_att = G \ (H' * W * z_attack); r_att = z_attack - H * x_hat_att; J_att = r_att' * W * r_att;

理论上,由于攻击向量满足a = H * c,估计结果会从x_hat偏移到x_hat + c,而残差保持不变。但实际因为有测量噪声,J_att和J_norm会有细微差别,不过总体上不会触发检测阈值。

设置卡方检测阈值:

alpha = 0.05; df = meas_num - state_num; % 自由度,33 - 13 = 20 threshold = chi2inv(1 - alpha, df); detected_norm = J_norm > threshold; detected_att = J_att > threshold;

可以打印一下结果验证效果:

fprintf('正常样本 J = %.4f, 检测结果 = %d\n', J_norm, detected_norm); fprintf('攻击样本 J = %.4f, 检测结果 = %d\n', J_att, detected_att);

正常情况下,正常样本的J小于阈值,检测结果为0;攻击样本的J同样小于阈值,检测结果也是0。这就直观展示了FDIA成功骗过坏数据检测的过程。如果攻击样本的J也超阈值了,说明攻击强度太大或测量冗余度太高,需要适当减小attack_amp。

5. 批量生成实验样本与数据集落地

5.1 批量循环与随机抽样策略

单组样本做好之后,批量生成就只是套一个循环的问题。但这里有几个细节需要处理好。

第一,每个样本的工况不能完全一样。我们需要在基准case14上对负荷做随机扰动,模拟不同时间断面的运行方式:

mpc_r = mpc; mpc_r.bus(:, 3) = mpc.bus(:, 3) .* (1 + 0.05 * randn(nb, 1)); % 有功负荷扰动 mpc_r.bus(:, 4) = mpc.bus(:, 4) .* (1 + 0.05 * randn(nb, 1)); % 无功负荷扰动

第二,潮流计算可能因为扰动过大而不收敛。要在循环里加一个判断,不收敛就跳过当前样本。尤其是当某个节点负荷被扰动到负值时,潮流结果可能不合理。建议对负荷扰动做截断处理,比如保证负荷不小于原值的50%。

第三,攻击标签要随机化。一般按50%概率决定当前样本是否被攻击。如果想做不平衡数据集,可以调整这个概率。

完整批量生成核心代码:

T = 1000; % 样本数量 data = zeros(T, meas_num + 3); % 测量值 + 标签 + J + 检测标志 attack_amp = 0.02; alpha = 0.05; threshold = chi2inv(1 - alpha, meas_num - state_num); for t = 1:T mpc_r = mpc; load_factor = 1 + 0.05 * randn(nb, 1); load_factor = max(load_factor, 0.5); % 避免负荷扰动过大 mpc_r.bus(:, 3) = mpc.bus(:, 3) .* load_factor; mpc_r.bus(:, 4) = mpc.bus(:, 4) .* load_factor; flow_r = runpf(mpc_r, mpoption('OUT_ALL', 0)); if ~flow_r.success t = t - 1; continue; end theta_r = flow_r.bus(:, 9) * pi / 180; x_true_r = theta_r(2:end); z_true_r = H * x_true_r; sigma_r = max(0.001, abs(z_true_r) * 0.01); z_r = z_true_r + randn(meas_num, 1) .* sigma_r; label = rand < 0.5; if label c = (rand(state_num, 1) - 0.5) * attack_amp; max_a = max(abs(H * c)); if max_a > max(abs(z_true_r)) * 0.2 scale = max(abs(z_true_r)) * 0.2 / max_a; c = c * scale; end z_final = z_r + H * c; else z_final = z_r; end W_r = diag(1 ./ sigma_r.^2); x_hat_r = G \ (H' * W_r * z_final); r_r = z_final - H * x_hat_r; J_r = r_r' * W_r * r_r; det_flag = J_r > threshold; data(t, :) = [z_final', label, J_r, det_flag]; end

这里有一个实现细节:每次循环里mpc_r都是基于原始mpc重新拷贝并扰动,而不是在上一轮基础上继续扰动,否则负荷会随机游走,越偏越离谱。这一点务必注意。

5.2 保存为.mat与CSV

批量生成完成后,需要把数据落到磁盘上。我一般同时保存两份:一份是Matlab原生的.mat文件,保持最高精度;一份是CSV,方便Python或者其他工具读取。

% 保存为.mat文件 save('ieee14_fdia_dataset.mat', 'data', 'attack_amp', 'alpha', 'threshold', 'H', 'sigma'); % 保存为CSV文件,第一行为列名 fid = fopen('ieee14_fdia_dataset.csv', 'w'); % 测量列名 header = {}; for k = 1:meas_num header{end+1} = sprintf('meas_%d', k); end header{end+1} = 'label'; header{end+1} = 'J_residual'; header{end+1} = 'detected'; fprintf(fid, '%s\n', strjoin(header, ',')); fclose(fid); writematrix(data, 'ieee14_fdia_dataset.csv', 'WriteMode', 'append');

CSV文件中每一行就是一个样本,前33列是33个测量值,第34列是标签(0表示正常、1表示攻击),第35列是残差平方和J,第36列是残差检测结果。这样的格式设计,方便直接读入Python的pandas或者sklearn做后续处理。

5.3 数据集使用建议

拿到数据集之后,建议先做一个简单的分布检查。正常样本和攻击样本的残差J分布应该高度重合,这说明攻击确实骗过了检测器;同时状态估计结果确实发生了偏移。可以画一张J的直方图对比:

fig = figure('Position', [100 100 800 400]); histogram(data(data(:, 34) == 0, 35), 30, 'FaceAlpha', 0.5); hold on; histogram(data(data(:, 34) == 1, 35), 30, 'FaceAlpha', 0.5); legend('正常样本', '攻击样本'); xlabel('残差平方和 J'); ylabel('样本数量'); title('FDIA攻击数据集残差分布对比'); saveas(fig, 'fdia_residual_dist.png');

如果两条分布曲线几乎完全重叠,说明攻击隐蔽性合格;如果攻击样本的J明显右偏,说明攻击强度设置过大,需要调小attack_amp。这一步是数据集质量验证的重要环节,强烈建议在正式做模型训练前先做。

实际训练时,数据应当划分为训练集、验证集和测试集。注意打乱顺序后划分,避免同一段工况下的样本全部落在同一集合里,造成数据泄露。若用深度学习模型,建议将测量值做归一化,但归一化参数只能在训练集上计算,测试集要沿用同样的参数。

6. 常见问题与实操避坑

6.1 安装与版本问题

Matpower版本不同,部分函数接口有差异。最典型的是mpoption的参数写法,老版本可能用mpoption('pf.enforce_q_lims', 0)这种写法,新版本统一改为mpoption('PF_ENFORCE_Q_LIMS', 0)或者直接用OUT_ALL。建议用Matpower 7.0以上版本,接口更稳定。

还有一类经典报错是“未定义函数或变量 'runpf'”,这百分之百是路径没有添加完整。记住用genpath把工具箱所有子目录都加进去,addpath只加主目录是不够的。

6.2 攻击失效的排查

批量生成时,可能会发现部分攻击样本的残差J明显偏大,甚至超出检测阈值。排查思路按优先级排列:

第一,检查H矩阵维度。矩阵维度不对,后面全是白算。第二,检查是否存在测量配置导致攻击向量异常。如果你只选支路潮流测量,而攻击者又不知道具体测量配置,攻击可能只能影响部分测量,残差就会被拉大。第三,检查攻击幅度约束。如果attack_amp太大,生成的攻击向量可能已经超出线性化近似范围,WLS迭代或检测逻辑就会触发。第四,检查噪声水平。噪声如果设得太大,会淹没攻击信号,导致数据集中攻击样本和正常样本几乎不可区分,模型也学不到有效特征。

我的经验是:第一次跑通时,把样本数设小一点比如100,打印前5个样本的J值和检测结果,人工确认逻辑正确后再放大规模。

6.3 数据不平衡与标签策略

如果只关心二分类精度,50%攻击比例是合理的。但真实电网中攻击是稀疏事件,攻击比例可能只有1%甚至更低。这时候直接训练分类器,模型会偏向把所有样本都判为正常,准确率看着很高,实际毫无价值。建议做法是生成数据时按你需要的正负样本比来定概率,生成完后再用采样策略控制训练集平衡度。

另外,标签除0/1之外,建议把攻击向量a也一并存储。攻击向量本身包含攻击者意图信息,后续如果要研究攻击定位、攻击重构、攻击意图识别,都需要原始攻击向量作为监督信号。我的习惯是生成三个文件:data_measurements.csv存测量值和标签,data_attack_vectors.csv存攻击向量,data_states.csv存真实状态和估计状态。分开存,灵活性更高。

6.4 IEEE14节点系统下H矩阵构造的三个高频错误

第一,参考母线的处理。直流潮流中参考母线相角是0,不参与状态估计。如果H矩阵的列数等于母线数而不是母线数减1,后面求逆必然报错。

第二,母线注入功率的正负号。支路潮流从母线流出时,对母线来说是注入功率减小的方向。我在代码里用Hbus(fb(k),:) = Hbus(fb(k),:) + Hf(k,:)处理流出方向,用减号处理流入方向。这个符号一旦弄反,母线注入测量就和支路潮流测量自相矛盾,状态估计会出很大的残差。

第三,相角单位。Matpower的bus矩阵第9列相角单位是度,而直流潮流公式用的是弧度。如果忘了乘pi/180,状态值全部错了一个数量级,生成的数据没有任何物理意义。

我在实际跑这套流程时被坑最多次的就是相角单位转换。第一次生成的攻击数据集,攻击向量大得离谱,排查了半天,最后发现是theta_true忘了转弧度。后来我养成一个习惯:不管代码多短,一定先打印几行关键变量做人工检查。建议你也这么做——输入x_true前几个值,看看是不是在-0.3到0.3弧度这个合理范围内,再继续往下跑。

把这一步做好,后面的数据生成就是流水线作业,干净利落。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询