☰
基于Matpower的IEEE14节点FDIA数据集构建与攻击生成实战
2026/10/2 7:47:15 网站建设 项目流程

做电网状态估计的入侵检测研究时,最让人头疼的不是模型的搭建,而是数据从哪来。真实的 SCADA 量测数据基本拿不到,公开数据集也往往是某篇论文特定工况下的产物,解释不清楚细节。我自己做 FDIA(虚假数据注入攻击)检测时,最终选用了最“皮实”的组合:Matlab 加 Matpower,用 IEEE14 节点作为试验床,亲手生成带标签的攻击数据集。整套生成逻辑不到 150 行代码,却足以支撑一个 LSTM 或 CNN 检测模型的训练和验证。

这篇文章会把核心流程完全拆开讲:为什么要选 IEEE14,FDIA 能绕过坏数据检测的数学原理是什么,H 矩阵怎么从 Matpower 里扒出来,攻击向量怎么构造,最后附上可直接复制的完整 Matlab 代码。无论你是刚接触电力信息物理安全的研究生,还是在做电网异常检测方向的工程师,照着这份流程都能跑出一套可复现、可扩展的数据集。

1. 为什么要做 IEEE14 节点的 FDIA 数据集

1.1 先搞清楚 FDIA 和普通坏数据到底差在哪

坏数据(Bad Data)指的是传感器故障或者通信异常引入的明显错误量测值,比如某个功率表突然跳到几千兆瓦。传统的坏数据检测(BDD)依靠状态估计残差就能发现这类异常,因为随机扰动大概率会拉大残差的范数。

FDIA 不一样。虚假数据注入攻击是攻击者先掌握了电网拓扑参数和状态估计模型,专门构造一组“聪明”的偏差注入到量测向量里。这组偏差如果落在状态估计量测矩阵的列空间中,就会让状态估计结果整体偏移,但残差保持原样。说直白点:状态估计出来的电压相角已经被人为带偏了,但你用残差检查法看,一切正常。

这就是 FDIA 数据比其他异常数据难做的原因——不能随便加高斯噪声,必须按照攻击模型来构造。需要一个可控的电网模型、一套明确的状态估计模型,以及一个能证明“我生成的确实是隐蔽攻击”的验证流程。

1.2 为什么选 IEEE14 而不是 IEEE30、IEEE118

IEEE14 节点系统是电力系统研究里的经典基准模型,包括 14 条母线、20 条交流支路、5 台同步发电机和 11 个负荷点。选它有几个很现实的好处。

首先是规模适中。状态向量很小,在直流潮流模型下状态量只有 13 个(去掉参考节点后),量测矩阵 H 的尺寸大概是 20 多行 × 13 列。这么小的矩阵,Matlab 里用普通最小二乘做状态估计就是亚毫秒级。生成 2000 个攻击样本也不需要等,跑几秒就完事。

其次是可视化方便。14 节点系统手绘接线图都能画清楚,调试的时候出了问题可以逐条支路排查。做机器学习特征分析时,量测特征维度也低,训练起来特别快。

第三是和论文接轨。主流的 FDIA 研究论文大多用 IEEE14、IEEE30、IEEE118 作为标准测试系统。你在 IEEE14 上验证过思路,后面再迁移到 IEEE30 或者 IEEE118,只需要把 loadcase 的参数改掉,代码逻辑完全不用变。

1.3 这个数据集可以拿去干三件事

第一,给检测模型喂数据。这是最直接的用途,数据集包含正常量测样本和 FDIA 攻击样本,带标签,直接用于 LSTM、GRU、Transformer、CNN 等模型的二分类训练。

第二,验证已有的检测算法。比如你想证明某篇论文里的检测方法在隐蔽攻击下失效,这个数据集可以作为 benchmark 跑对照实验。

第三,教学演示。状态估计、坏数据检测、攻击向量构造,这些抽象的公式放在实际系统里跑一遍,比对着教材讲十遍都有效。我自己给学生演示 FDIA 原理时就是用的这套流程。

2. 环境准备与 case14 模型的三步初始化

2.1 Matlab 和 Matpower 的环境检查

Matpower 是一个开源电力系统潮流计算工具箱,目前最新版本是 7.x,支持 DC 潮流、AC 潮流、最优潮流、状态估计等常用功能,和 Matlab 2016 之后的版本兼容性都很好。

安装流程很简单:

  1. 从 Matpower 官网压缩包。
  2. 解压到任意目录,比如D:\matpower7.1。
  3. 在 Matlab 里执行addpath(genpath('D:\matpower7.1'))。
  4. 如果要长期使用,把这行命令写进 Matlab 的startup.m,避免每次手动加载。

检查是否安装成功,直接跑一次最基础的潮流:

mpc = loadcase('case14'); res = runpf(mpc);

如果res.success等于 1,说明环境没问题。这一步几乎不会出岔子,唯一可能遇到的是低版本 Matlab 的路径问题,确认addpath用的是完整路径就行。

2.2 case14 的系统参数快速摸底

我用loadcase('case14')读进来的是一个结构体,关键字段包括:

字段含义case14 里的情况
mpc.bus母线数据14 行,包含母线编号、类型、负荷、电压等
mpc.branch支路数据20 行,包含首末端母线、电阻、电抗、电纳
mpc.gen发电机数据5 行,包含有功无功出力、电压幅值等
mpc.baseMVA基准容量100 MVA

在生成攻击数据集之前,必须对支路参数做一次确认。因为后面构建 H 矩阵时要用到每条支路的电抗值x,如果支路数据的列顺序弄错了,整个攻击向量都会出错。Matpower 的mpc.branch里,第 3 列是电阻 R,第 4 列是电抗 X,第 5 列是电纳 B。直流潮流模型里通常忽略电阻和电纳,只保留电抗,因为直流模型假设线路电阻为零、电压幅值恒定、相角差很小。

2.3 用直流潮流算一个稳定的基础工作点

FDIA 攻击向量构造依赖线性模型,所以这里使用直流潮流(DC Power Flow)而不是交流潮流。rundcpf是 Matpower 里专门跑直流潮流的函数,它直接基于导纳矩阵和注入功率求解电压相角,速度快,模型是线性的,非常适合状态估计和攻击向量推导。

res_dc = rundcpf(mpc); theta = res_dc.bus(:, 9) * pi / 180; % 相角,单位从度转弧度 x_true = theta(2:end, 1); % 去掉参考母线,作为 DC 状态真值

这里特别注意:res_dc.bus的第 9 列是相角,单位是度,必须转成弧度才能和直流模型的弧度制公式一致。参考母线(通常是母线 1)相角固定为 0,不参与状态估计,所以状态向量取第 2 到第 14 个母线。

如果你用的是 Matpower 7.x,rundcpf直接可用。如果是 6.x 或更老的版本,可能会提示找不到这个函数,那就用runpf先跑交流潮流,再取出相角,但后续直流线性模型的误差分析会复杂一些。我的建议是直接装 7.x,一步到位。

3. FDIA 能绕过坏数据检测的数学内核

3.1 状态估计和残差检测的基本公式

电力系统状态估计的目标是根据量测向量 z 和量测矩阵 H,估算系统状态向量 x。在直流模型下:

z = H * x + e

其中 e 是量测噪声。加权最小二乘估计的解析解是:

x_hat = (H' * W * H)^(-1) * H' * W * z

W 是量测权重矩阵,通常取各量测方差的倒数。有了估计值以后,计算残差:

r = z - H * x_hat

坏数据检测最常见的方法是残差范数检验:计算 J(x_hat) = r' * W * r,如果 J 超过某个阈值,就认为数据里存在坏数据。从统计角度看,J 服从卡方分布,阈值可以通过置信度查表得到。

3.2 隐蔽攻击的核心等式:a = Hc

假设攻击者想把量测从 z 篡改成 z + a,其中 a 是攻击向量。如果 a 满足:

a = H * c

其中 c 是任意非零向量,那么攻击后的量测为:

z_att = z + a = H * x_true + e + H * c = H * (x_true + c) + e

如果用加权最小二乘重新估计,估计结果会从 x_hat 变成 x_hat + c。再看残差:

r_att = z_att - H * (x_hat + c) = (z + Hc) - (H * x_hat + Hc) = z - H * x_hat = r

看到了吗?残差完全不变。这就是为什么 FDIA 被称为“隐蔽攻击”——J 检验的数值在噪声允许范围内不会出现任何异常,但系统状态已经被攻击者篡改。攻击者可以通过设计 c 来误导状态估计结果,比如让某条线路的有功功率看起来比真实值低,从而影响调度决策。

3.3 如果只能篡改部分测量,需要动用零空间

实际工程里攻击者不可能同时篡改所有 SCADA 量测。假设攻击者只能篡改测量集合 S,剩余测量集合记为 U。为了让攻击继续保持隐蔽,必须让攻击向量 a 在 U 上等于零,同时整体的残差不改变。

这等价于找一个非零向量 c,使得:

H_U * c = 0

其中 H_U 是未篡改测量对应的 H 矩阵行块。也就是说,c 必须位于 H_U 的零空间里。Matlab 里直接用null(H_U)就能求出零空间基向量。

零空间是否非空,和攻击测量集合的大小有直接关系。在我们选取的量测配置里,总测量数是 24,状态数是 13。只要未篡改测量数少于状态数 13,这个欠定方程组就一定有非零解。这也是为什么本文代码里攻击测量集合取 12 个,未篡改测量数为 12,刚好保证零空间至少有一维。

如果你把攻击测量集合取太少,比如只攻击 3 个测量,未篡改测量数高达 21,这种情况通常不存在满足条件的非零 c,攻击就不是严格隐蔽的。所以在生成数据集之前必须检查null(H_U)是否为空。

4. 完整代码实现:从 H 矩阵到攻击数据集

4.1 测量集合设计与 H 矩阵装配

做状态估计前必须先确定量测配置。我这里选的量测包括:

  • 全部 20 条支路的有功功率潮流,对应 H_flow 矩阵。
  • 母线 2、6、9、14 的有功注入功率,对应 H_inj 矩阵。

这样总共有 24 个量测,量测矩阵 H 的大小是 24×13。支路潮流和注入功率都工作在直流模型下,公式为:

P_ij = b_ij * (theta_i - theta_j) P_i = sum_j b_ij * (theta_i - theta_j)

其中 b_ij = 1/x_ij,是支路电纳。H_flow 的行很容易构造:对支路 i,如果在首端母线加 b_ij,在末端母线减 b_ij。H_inj 则等价于直流导纳矩阵 Bbus 去掉参考节点列。

特别说明一下为什么注入量测只取 4 条母线而不是全部 14 条。一方面考虑实际电网的量测冗余度不会那么高,另一方面是为了让数据集更有挑战性。你如果想改成全部母线注入量测,只需要把inj_idx改成(1:14)',代码不用动。

4.2 攻击空间计算与批量生成

在批量生成攻击样本时,每次从零空间里随机抽取一个组合系数,得到随机的 c,再通过 a = Hc 计算攻击向量。为了让攻击强度可控,我对 c 做归一化后再乘上一个随机幅度。

这里有个细节要注意:攻击向量 a 加到全部量测上,但由于 a 在未篡改测量上的分量约等于 0,实际修改的量测只有 S 集合中的那 12 个。代码里我把z_att(attack_idx) = z_att(attack_idx) + a(attack_idx)写清楚了,这也是为了让读者直观看到“攻击只改了一部分仪表”。

4.3 批量生成完整代码

下面是完整代码,包含正常样本和 FDIA 攻击样本的批量生成、文件保存。直接复制到 Matlab 里就能跑。

%% FDIA_dataset_generator.m % 基于 Matpower 的 IEEE14 节点 FDIA 攻击数据集生成 % 作者:电力系统安全分析组 % 说明:直流潮流模型,隐蔽攻击,输出 mat 和 csv 两份数据 clear; clc; rng(2024); % 固定随机种子,保证结果可复现 %% 1. 加载系统并计算基础直流潮流 mpc = loadcase('case14'); res_dc = rundcpf(mpc); theta = res_dc.bus(:, 9) * pi / 180; x_true = theta(2:end, 1); nbus = size(mpc.bus, 1); nbranch = size(mpc.branch, 1); %% 2. 构建直流导纳矩阵 Bfull B_line = 1 ./ mpc.branch(:, 4); % 每条支路电纳 = 1 / 电抗 Bfull = zeros(nbus, nbus); for i = 1:nbranch f = mpc.branch(i, 1); t = mpc.branch(i, 2); Bfull(f, f) = Bfull(f, f) + B_line(i); Bfull(t, t) = Bfull(t, t) + B_line(i); Bfull(f, t) = Bfull(f, t) - B_line(i); Bfull(t, f) = Bfull(t, f) - B_line(i); end %% 3. 构建量测矩阵 H % 支路有功潮流的 H 矩阵 H_flow = zeros(nbranch, nbus - 1); for i = 1:nbranch f = mpc.branch(i, 1); t = mpc.branch(i, 2); if f > 1 H_flow(i, f - 1) = H_flow(i, f - 1) + B_line(i); end if t > 1 H_flow(i, t - 1) = H_flow(i, t - 1) - B_line(i); end end % 节点注入有功的 H 矩阵(去掉参考节点) H_inj = Bfull(:, 2:end); % 选择量测:全部 20 条支路 + 4 个注入母线 flow_idx = (1:nbranch)'; inj_idx = [2; 6; 9; 14]; H = [H_flow(flow_idx, :); H_inj(inj_idx, :)]; m = size(H, 1); % 量测数量 = 24 n = nbus - 1; % 状态数量 = 13 measurement_names = [ arrayfun(@(i) sprintf('PL_%02d', i), flow_idx, 'UniformOutput', false), ... arrayfun(@(i) sprintf('PI_%02d', i), inj_idx, 'UniformOutput', false) ]; disp(['量测数量 m = ', num2str(m), ', 状态数量 n = ', num2str(n)]); %% 4. 设计攻击者能篡改的测量集合 S attack_idx = (1:12)'; % 攻击前 12 条支路功率量测 unattack_idx = setdiff((1:m)', attack_idx); % 验证是否存在不可检测攻击空间 H_U = H(unattack_idx, :); Z_space = null(H_U); if isempty(Z_space) error('当前量测组合下攻击空间为空,请调整 attack_idx 或量测配置'); end fprintf('不可检测攻击零空间维数:%d\n', size(Z_space, 2)); %% 5. 批量生成数据集 N = 2000; % 总样本数 halfN = floor(N / 2); sigma = 1e-3; % 量测噪声标准差,标幺值 X = zeros(N, m); Labels = zeros(N, 1); for k = 1:N % 为了让数据更丰富,每次随机调整负荷水平 0.8 ~ 1.2 倍 load_scale = 0.8 + 0.4 * rand; mpc_k = mpc; mpc_k.bus(:, 3) = mpc.bus(:, 3) * load_scale; mpc_k.bus(:, 4) = mpc.bus(:, 4) * load_scale; mpc_k.gen(:, 2) = mpc.gen(:, 2) * load_scale; res_k = rundcpf(mpc_k); theta_k = res_k.bus(:, 9) * pi / 180; xk = theta_k(2:end, 1); z_true = H * xk; noise = sigma * randn(m, 1); z = z_true + noise; if k <= halfN % 正常样本 X(k, :) = z'; Labels(k) = 0; else % FDIA 隐蔽攻击样本 c = Z_space * randn(size(Z_space, 2), 1); c = c / norm(c) * (0.05 + 0.05 * rand); % 控制攻击幅度 a = H * c; z_att = z; z_att(attack_idx) = z_att(attack_idx) + a(attack_idx); X(k, :) = z_att'; Labels(k) = 1; end end %% 6. 保存数据 save('fdia_dataset.mat', 'X', 'Labels', 'measurement_names', ... 'H', 'attack_idx', 'unattack_idx', 'sigma', 'Z_space'); writematrix(X, 'fdia_X.csv'); writematrix(Labels, 'fdia_labels.csv'); fprintf('数据集生成完成:正常样本 %d 个,攻击样本 %d 个\n', halfN, N - halfN);

代码跑完以后,你会在当前目录里得到三个文件:fdia_dataset.mat保存了完整的矩阵和变量,方便接着做 Matlab 分析;fdia_X.csv和fdia_labels.csv是给 Python 或深度学习框架准备的,直接用 pandas 就能读。

5. 验证攻击效果:残差对比和样本可视化

5.1 用普通最小二乘验证隐蔽性

生成完数据集后,不要急着拿去训练模型,先做一次“隐蔽性验证”。从数据里取一个正常样本和一个攻击样本,分别做状态估计,对比残差范数和状态偏差。

%% verify_FDIA.m % 加载一个正常样本和一个攻击样本 load('fdia_dataset.mat'); idx_normal = find(Labels == 0, 1); idx_att = find(Labels == 1, 1); X_H = [X(idx_normal, :)', X(idx_att, :)']; for k = 1:2 z = X_H(:, k); x_hat = (H' * H) \ (H' * z); r = z - H * x_hat; J = r' * r; fprintf('样本 %d: 残差范数 J = %.6f\n', k, J); end % 对比真实状态和攻击后估计状态 theta_all = zeros(14, 1); % 第一个是参考母线,角度为 0 % 注意:这里为了演示,我们直接用第一个正常样本和攻击样本对比 x_hat_normal = (H' * H) \ (H' * X(idx_normal, :)'); x_hat_att = (H' * H) \ (H' * X(idx_att, :)'); theta_all(2:end, 1) = x_hat_normal; disp('正常样本估计相角(弧度):'); disp(theta_all'); theta_all(2:end, 1) = x_hat_att; disp('攻击样本估计相角(弧度):'); disp(theta_all');

实际运行结果里你会看到,正常样本和攻击样本的残差范数 J 非常接近,差距在噪声波动范围内,这说明攻击确实骗过了基于残差的坏数据检测。但如果仔细对比状态估计结果,攻击样本的相角已经发生了整体偏移。这就是 FDIA 最危险的地方:表面风平浪静,底层数据已经被污染。

5.2 直接拿残差做检测会发生什么

如果把你刚生成的正常样本和攻击样本的残差范数画成直方图,你会发现两条分布几乎完全重合。这意味着任何基于残差范数阈值的检测器,无论如何调阈值,都没法把两类样本分开,准确率约等于随机猜测。

这也是很多人第一次跑 FDIA 实验时会惊到的地方——不是检测器不够强,而是攻击向量构造时已经确保残差不含攻击信息。要检测这种隐蔽攻击,必须换思路。这也是这份数据集有价值的原因:它挑战的是检测器能否从量测本身的高维时空关联中发现异常,而不是简单算个残差了事。

5.3 给机器学习训练前的数据质量检查

数据集生成完,建议按下面几步排查常见问题:

  1. 检查 NaN 和 Inf。sum(isnan(X(:)))应该为 0。
  2. 检查标签平衡。正常和攻击样本应该各占一半。
  3. 归一化要用训练集的统计量。比如用 z-score 标准化时,先在训练集上计算均值方差,测试集也用同一组参数,绝对不能整份数据一起归一化,否则会造成数据泄漏,评估结果虚高。
  4. 固定随机种子。代码开头rng(2024)保证复现。换种子后基线会波动,论文里如果想展示某个检测算法比另一个算法强,最好固定多种种子做多次实验取平均。

6. 实际做数据集时踩过的坑与扩展思路

6.1 直流和交流模型混用的误差

初学者最容易踩的坑,是先用runpf跑交流潮流,再拿交流潮流的角度代入直流 H 矩阵构造攻击。交流潮流的结果和直流模型存在模型偏差,比如线路有功损耗、电压幅值变化等,这些在直流模型里都被省略了。结果就是攻击向量在直流残差检验下确实是隐蔽的,但换到交流模型下残差会有一点波动,攻击就不是百分之百隐蔽。

严格的做法是全程使用同一个模型假设。本文代码用rundcpf从生成工作点到构造攻击全部在直流框架内完成,因此数学上闭环。如果你要做更贴近实际系统的实验,后续可以把直流模型换成交流状态估计,但攻击构造会复杂很多,涉及雅可比矩阵的迭代,这部分可以留作进阶扩展。

6.2 Matpower 版本兼容问题

rundcpf在 Matpower 7.0 里新增了返回值,早期版本要么没有这个函数,要么返回结构不一样。如果报错,优先检查 Matpower 版本,不要硬调代码。另外,不同版本的mpc.branch列顺序略有差异,但第 4 列一直是 X,这在手册里写了,我用的是通用列顺序,跨版本应该都能跑通。

还有一个和版本无关的细节:H = [H_flow(flow_idx, :); H_inj(inj_idx, :)]拼接顺序决定了量测数据的列顺序。如果之后要做特征分析,请记住前 20 列是支路功率,后 4 列是节点注入功率。measurement_names变量已经客观记录了这个顺序。

6.3 数据泄漏是个隐形杀手

很多人在生成数据集时会顺手把整个 X 标准化,然后才开始划分训练集和测试集。这其实已经造成数据泄漏。正确的流程是:先划分原始数据,再用训练集计算均值和方差,最后用这套参数转换训练集、验证集和测试集。

这份代码生成的.mat文件里保存的是原始量测值,没有做任何标准化,就是为了让你在进入模型训练前自己掌控预处理步骤。

6.4 后续可以怎么扩展

如果你已经拿到 IEEE14 的数据集并跑通了一个检测模型,下面几个方向都可以继续做:

  • 换更大的系统:把loadcase('case14')换成case30、case57、case118,代码其余部分不用改,只是 H 矩阵的维度和零空间计算时间会变化。
  • 加入拓扑攻击:FDIA 未必只改量测值,也可以配合支路开断的拓扑信息攻击,这种攻击更难检测。
  • 加入 PMU 量测:同步相量量测的采样频率高、精度高,加入部分 PMU 量测后检测难度会下降,这本身就是一个很好的对照实验设置。
  • 从 DC 模型升级到 AC 模型:用交流状态估计替代直流线性模型,攻击向量需要通过迭代求解非线性方程,隐蔽性分析也会更复杂。

最后再说一个实操上的建议:不要只保存攻击前后的量测矩阵,最好连同 H 矩阵、量测名称、攻击索引、随机种子都存进同一个.mat文件。这样遇到审稿人质疑或者实验结果对不上的时候,你能快速复盘整个数据生成链路的每一步。我现在写论文附数据的时候,一直坚持这个习惯,省掉了无数次返工。

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

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

立即咨询