1. 项目背景与解决思路1.1 电力系统网络安全为什么需要多源数据融合这几年做电力监控系统的安全分析我最直观的感受是单看一种数据根本不够用。网络入侵这件事在电力系统里不是“丢几个数据包”那么简单攻击者往往从信息外网摸进调度网滞留在变电站、发电厂的控制系统里再慢慢找机会下发假指令。这个过程中防火墙会记录连接主机日志会留下进程调用SCADA系统能发现遥测量异常PMU会捕捉电压相角的微小波动。单独看任何一路都容易被正常扰动掩盖或者被攻击者精心伪装掉。举个例子某台远动终端短时间内产生大量写操作指令从网络日志看像极了入侵。结果排查下来是远方维护人员在批量刷新定值。反过来真正发生的一次恶意报文注入网络流量只比平时多个百分之几单纯靠流量阈值根本触发不了告警。但如果同时看站端电压值、有功功率变化和PMU相角数据之间突然出现了不合常理的相关性突变问题一下就暴露了。这就是多源数据融合的价值让来自不同传感器、不同协议、不同采样频率的数据互相印证把“孤证不立”变成“多方证据链”。本项目针对的就是这个场景。它把电力系统运行量测电气量、相量、网络侧流量、主机侧日志、控制指令流等数据在特征层面融合起来构建一个入侵检测模型。适合需要做安全防护的电力监控系统开发、运维人员也适合正在研究工业控制系统安全算法的同学参考。代码我全部用 Python 实现尽量贴近现场能拿到的数据结构不依赖任何专用平台拿到就能跑。1.2 多源数据融合的三个层次实际做融合之前先要清楚自己该融合到哪一层。业内一般分三层数据级融合、特征级融合和决策级融合。数据级融合最直观把多路原始数据在时间上对齐后直接拼在一起。优点是不丢失原始信息缺点也明显数据量大了之后计算开销高而且量纲差异大的特征会互相干扰。对于电力系统这种高采样频率又有大量噪声的场景直接做数据级融合很容易让模型学到噪声。决策级融合是每一路数据单独训练一个模型出告警后再投票或加权。这种方式最鲁棒单路模型挂了不影响全局但问题在于各模型独立决策时已经丢掉了跨源相关性的信息。比如网络流量异常和电气量突变在单路上都不足以触发阈值但组合起来就是典型的同步攻击特征决策级融合很难体现这种“112”。本项目采用的是中间路线特征级融合。先对每一路原始数据做统计特征提取比如滑动窗口均值、方差、变化率、极值等再把所有源的特征拼接成一个融合特征向量送入同一个分类模型。这样既保留了一定的物理意义又能让模型自动学习跨源特征的交叉关系。实际测试下来准确率和时效性比单源模型高出不少可解释性也比纯深度模型好。2. 项目框架多源数据定义、融合策略与模型选型2.1 多源数据来源定义先明确我们要融合哪些“传感器”。这里的传感器不只是电压互感器、电流互感器还包括网络探针、日志收集器等广义信号源。我把整个模拟系统设计成五路数据源第一路是SCADA遥测数据包括有功功率、无功功率、电压幅值等主要反映电力系统的稳态运行状态。网络入侵如果最终影响了控制指令电气量往往最先出现规律性偏差。比如一次假分闸指令注入会让某条馈线功率在几十毫秒内跳变。第二路是PMU同步相量数据记录电压相角、频率变化率。这套数据最大的价值是时间同步精度很高能捕捉到电力系统动态过程中的微小相位异常。很多针对性攻击会先在相角上露出马脚。第三路是网络流量数据来自变电站或电厂边界处的流量探针统计每秒包数、字节数、新建连接数、协议分布等。这是传统入侵检测系统最依赖的信号但我们不把它当成唯一信号。第四路是主机操作系统和业务系统的安全日志包含登录失败次数、进程异常退出、特权命令执行频率、审计事件计数等。攻击者在系统内部横向移动时这些日志会明显变多。第五路是控制指令流数据也就是主站下发到终端和终端上报主站的报文序列。像修改定值、切换运行方式这类指令正常情况下频率很低异常时段会密集出现。把这五路数据按时间戳对齐就形成了一个多通道时间序列。攻击场景出现时各通道会在不同程度上出现异常但没有任何一个通道孤零零地显著偏离到能直接告警。这正是需要融合建模的原因。2.2 融合策略和时序对齐多源数据最大的麻烦是采样频率不一致。SCADA经常是秒级甚至分钟级采样PMU可能是毫秒级网络流量探针按秒聚合日志则是事件驱动的。直接拼接会制造大量缺失值。我的做法是先定一个基础时间窗口比如500毫秒或1秒。然后把所有源数据通过线性插值、重采样和按时间戳聚合三种手段统一到同一个时间轴上。网络流量按时间戳对齐后取每秒总量SCADA遥测用最近邻前向填充PMU高频数据在窗口内取均值降采样日志事件按窗口做计数。对齐之后再开一个长度为20个时间步的滑动窗口做特征提取。特征提取的核心逻辑是不要直接拿原始波形送模型而是计算每个窗口内的统计特征。常用的有均值、标准差、最大值、最小值、一阶差分均值、差分标准差、超过阈值的次数等。这些统计量比原始值稳定对采样抖动不敏感也更贴近现场工程师的判据思路。然后把各源特征拼接成一个长向量做标准化预处理。因为不同源的统计量范围差异很大比如电压可能是220网络包速率可能是800直接训练会让高数值特征主导梯度。我用标准化把所有特征压到均值0、方差1附近让融合模型真正学习到模式而不是量级。2.3 模型选型为什么选随机森林模型方案上我一开始对比过几类算法深度神经网络、XGBoost、随机森林和孤立森林。实际效果最稳的是随机森林作为主干分类器再搭配孤立森林做辅助异常校验。选随机森林主要因为三点第一它不需要复杂特征工程以外的额外结构设计训练速度快现场稍微改改特征就能重训迭代效率高。第二它对特征重要性有内建输出能告诉我们哪些融合特征在决策中起作用这在实际安全运营里非常重要。第三电力系统入侵样本通常很少深度神经网络在这种小样本、高噪声场景下很容易过拟合需要大量调参而随机森林不容易出这个问题。孤立森林在这里承担一个特殊角色作为无监督校验。随机森林是监督模型需要带标签的历史数据。但电力系统里很多攻击第一次发生前是没有历史标签的。我常用孤立森林对随机森林分的“正常”样本做二次检查把偏离度高的样本再标记一次用来发现那些监督模型没见过的新型攻击。3. Python代码实现与复现3.1 环境准备与依赖代码基于 Python 3.8 以上版本核心依赖是 numpy、pandas、scikit-learn 三个库。如果只是跑通示例不需要 GPU。我通常在虚拟环境里装一遍避免污染系统环境python -m venv power_fusion_env source power_fusion_env/bin/activate pip install numpy pandas scikit-learn matplotlibmatplotlib 主要用于可视化不需要出图也可以不装。所有代码我都按模块组织方便改成自己的数据接入。3.2 模拟多源传感器数据我先写一个数据生成器不依赖真实系统也能演示完整流程。生成五路对应的模拟数据有功负荷、电压幅值、网络包速率、日志错误计数、PMU相角。正常时段有缓慢波动和噪声攻击时段叠加突变和趋势漂移更贴近实际入侵数据不是纯粹阶跃的情况。import numpy as np import pandas as pd def generate_power_system_data(n_points5000, attack_start3200): 模拟多源电力数据。 n_points: 总时间步数 attack_start: 攻击开始时间步 t np.arange(n_points) # 1. SCADA 有功负荷带日负荷曲线形态和噪声 load_mw ( 100 20 * np.sin(2 * np.pi * t / 300) 2 * np.sin(2 * np.pi * t / 60) 3 * np.random.randn(n_points) ) # 2. SCADA 电压幅值正常时有小幅波动 voltage_kv ( 220 0.05 * np.sin(2 * np.pi * t / 240) 0.02 * np.random.randn(n_points) ) # 3. 网络流量包速率 net_pkt_rate 500 30 * np.random.randn(n_points) # 4. 日志错误计数泊松分布事件稀疏 log_error_cnt np.random.poisson(0.5, n_points) # 5. PMU 电压相角正常时正弦波动 pmu_phase ( 0.3 * np.sin(2 * np.pi * t / 360) 0.01 * np.random.randn(n_points) ) label np.zeros(n_points) # 攻击注入从 attack_start 开始各源协同异常 label[attack_start:] 1 # 攻击造成负荷爬升不是瞬间跳变 attack_ramp np.clip((t - attack_start) / 100, 0, 1) load_mw[attack_start:] 15 * attack_ramp voltage_kv[attack_start:] 0.5 * attack_ramp net_pkt_rate[attack_start:] 300 60 * np.random.randn(n_points - attack_start) log_error_cnt[attack_start:] 4 pmu_phase[attack_start:] 0.35 * attack_ramp df pd.DataFrame({ time: t, load_mw: load_mw, voltage_kv: voltage_kv, net_pkt_rate: net_pkt_rate, log_error_cnt: log_error_cnt, pmu_phase: pmu_phase, label: label, }) return df这里有一个容易被忽视的点攻击样本不能全部是同一种阶跃形态。真实攻击可能先潜伏、再横向移动、最后执行破坏指令反映到数据上是不同步变化的。我在模拟器里加入attack_ramp就是为了让电气量有一个缓慢爬坡过程避免模型学到“只要电压突然变大就是攻击”这种过拟合逻辑。3.3 特征工程滑动窗口统计特征原始时序数据不能直接进模型我写一个特征提取函数对每一列算滑动窗口内的统计量并且用shift(1)避免未来信息泄漏。很多初学做时序预测的人容易在这里掉坑如果窗口里包含了当前时刻的值训练时模型会“偷看”未来测试表现虚高上线就崩。def extract_features(df, window20): 输入: 包含数据列的DataFrame不含label 输出: 融合特征DataFrame if label in df.columns: df df.drop(columns[label]) cols [load_mw, voltage_kv, net_pkt_rate, log_error_cnt, pmu_phase] feature_parts [] for col in cols: roll df[col].rolling(window, min_periods1) part pd.DataFrame({ f{col}_mean: roll.mean().shift(1), f{col}_std: roll.std().shift(1), f{col}_max: roll.max().shift(1), f{col}_min: roll.min().shift(1), f{col}_delta_mean: df[col].diff().rolling(window, min_periods1).mean().shift(1), }) feature_parts.append(part) features pd.concat(feature_parts, axis1) return features窗口大小我设为20相当于滑过20个时间步。这个参数不是拍脑袋定的。窗口太小统计特征受噪声影响大窗口太大检测响应又太慢。我在实战中一般先用5、10、20、50分别跑一遍比较验证集F1分数再决定最终值。示例里写20是因为它在模拟数据上综合表现最平衡。特征提取之后前几行会因为shift(1)出现NaN直接用dropna()丢掉即可。后续训练时注意标签也要同步对齐。3.4 融合特征训练与评估核心训练流程如下。这里有一个时序场景的关键原则不能随机切分训练集和测试集必须按时间顺序切分否则模型在用未来的信息预测过去的攻击结果完全没有参考意义。from sklearn.ensemble import RandomForestClassifier, IsolationForest from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score from sklearn.preprocessing import StandardScaler # 生成数据 raw generate_power_system_data(n_points5000, attack_start3200) features extract_features(raw, window20) features features.iloc[2:].reset_index(dropTrue) # 丢弃NaN头部 labels raw[label].iloc[2:].reset_index(dropTrue) # 时间顺序切分前70%训练后30%测试 split int(len(features) * 0.7) X_train_raw features.iloc[:split].values X_test_raw features.iloc[split:].values y_train labels.iloc[:split].values y_test labels.iloc[split:].values # 标准化 scaler StandardScaler() X_train scaler.fit_transform(X_train_raw) X_test scaler.transform(X_test_raw) # 随机森林分类器设置 class_weight 应对不平衡 clf RandomForestClassifier( n_estimators300, max_depth10, min_samples_split5, class_weightbalanced_subsample, random_state42, n_jobs-1, ) clf.fit(X_train, y_train) # 预测和评估 y_pred clf.predict(X_test) y_prob clf.predict_proba(X_test)[:, 1] print(ROC-AUC:, roc_auc_score(y_test, y_prob)) print(classification_report(y_test, y_pred, target_names[正常, 入侵])) print(混淆矩阵:\n, confusion_matrix(y_test, y_pred))class_weightbalanced_subsample是我比较喜欢的一个参数它会在每个随机森林的随机子样本上自动平衡正负类权重比固定的class_weightbalanced更稳健。在攻击样本很少、正常样本很多的情况下这一点对召回率影响很大。3.5 加入孤立森林做无监督校验监督模型负责识别已知模式无监督模型负责兜底未知模式。我通常这样组合先用随机森林预测结果把预测为正常的样本再交给孤立森林判断。如果某样本在孤立森林里的异常分数很高就说明它虽然长得像正常但分布偏离正常簇值得再人工确认。# 只拿训练集里的正常样本拟合孤立森林 normal_train X_train[y_train 0] iso IsolationForest(contamination0.05, random_state42) iso.fit(normal_train) # 对测试集所有样本做异常打分 anomaly_score iso.decision_function(X_test) # 将孤立森林分数大于阈值的样本视为疑似入侵可生成二次告警 suspicious anomaly_score -0.3 print(疑似未知攻击样本数:, int(suspicious.sum()))注意decision_function分数越大越正常分数越小越异常。阈值-0.3是示例值实际使用时我会先统计正常样本在训练集上的分数分布按99分位数取阈值这样更客观。4. 关键参数与调优心得4.1 滑动窗口、步长和响应速度怎么平衡我在实际项目中把窗口和步长看成是一对需要权衡的指标。窗口越大统计特征越稳定但攻击发生到被检测出来的时间延迟也越大。电网安全场景里有些攻击在毫秒级就能造成物理设备误动作响应窗口压不下来会很难用。通用的调法是这样的先确定你希望攻击发生后最多多久能告警比如1秒。如果数据采样是每秒一个点那么窗口就不能超过1秒否则最少延迟就大于1秒。然后在这个约束下最大化模型指标。对于更高频的PMU数据我通常不会直接提高到几十个毫秒级窗口因为计算量太大且误报率高。更实用的做法是高频数据在固定时间窗内先降采样聚合再把聚合值作为一路特征参与融合。这本身就是数据级融合的一部分。4.2 样本不平衡问题电力系统的正常样本和攻击样本数量相差极其悬殊一天下来正常标签能有几十万个攻击标签可能只有几十个。直接把这种数据丢给随机森林模型会把所有样本都判成正常因为准确率也能达到99%以上。所以我在代码里用了class_weightbalanced_subsample让分类器在每棵树的抽样阶段就自动调整类别权重。如果这种方法压不住漏报下一步我会对攻击样本做简单的SMOTE过采样但有一点要注意SMOTE必须在时间序列训练集内部做不能让合成样本泄漏到测试集。还有一种做法是在评估指标上动手脚不要只看准确率重点关注攻击类别的召回率、精确率和ROC-AUC。如果召回率上去了但精确率掉得厉害说明告警太多实际运营人员很快就不看了。这时候需要引入决策阈值调整比如把predict_proba的阈值从0.5降到0.3宁可误报多几张单子也不能漏掉一次真实入侵。4.3 多源特征的重要性排序与取舍随机森林训练完可以直接看feature_importances_这一步对理解系统很有价值。在我做过的一次模拟测试里排在前面的特征往往是跨源组合比如net_pkt_rate_delta_mean和pmu_phase_delta_mean同时重要而单一传感器的均值特征重要度较低。这说明模型确实在利用多源相关性而不是只盯着一路数据。特征并不是越多越好。有些日志计数特征在大量时间步里都是0对模型没有信息量还会拖慢训练。我一般会把方差太低的特征直接删掉。比如模拟数据里的日志错误计数如果只看正常段全是0那零方差特征就必须排除否则会影响树节点划分。下面是我常用的一组特征筛选规则可以在融合训练前跑一遍特征缺失率超过30%的直接删除。在训练集上特征方差低于1e-5的直接删除。和标签相关性超过0.95的冗余特征只保留其中一种。通过随机森林特征重要性排序逐步删除底部10%重要性特征观察验证集指标不降就继续删。4.4 阈值与误报率的现场调优每种融合特征在标准化之后都会有一个“融合分数”。随机森林输出的是概率孤立森林输出的是异常分。现场部署时我会把两块分数加权成一个综合评分然后设置双阈值高于高阈值直接告警低于低阈值放行中间值进入人工复核区。这样能最大限度降低误报造成的“告警疲劳”。一个很实用的技巧把阈值选择放在验证集的精确率-召回率曲线上做。找到召回率大于0.9前提下精确率最高的点对应的阈值最接近真实运维诉求。不要总想着准召都到0.99现实中做不到必须让业务方参与权衡。5. 常见问题与排查技巧5.1 多源数据时间不同步这类问题我遇到的次数最多。SCADA和PMU的时钟经常不统一或者网络探针用的是另一套NTP服务器几毫秒的偏差在应急场景里都会被放大。处理的办法是做一个时间对齐器让所有通道以最小粒度时间戳为基准重采样到统一频率。重采样有两种思路高频向低频对齐取窗口平均值低频向高频对齐用前向填充插值。对于日志这类事件数据必须在聚合窗口内计数否则直接用原始记录会造成事件“在下一个窗口才出现”的假象。def align_multisource(df, freq1S): df[time] pd.to_datetime(df[time], units) df.set_index(time, inplaceTrue) # 数值列重采样聚合日志计数列求和 df_resampled df.resample(freq).mean() return df_resampled5.2 数据量纲差异对模型的影响我最早犯过的错就是把电压、功率、网络包速率直接拼起来丢进模型。结果训练出的模型基本只看电压特征网络流量那一路等于白送。解决办法就是标准化。但要注意标准化参数只能用训练集拟合测试集不能再用测试集自己算均值和方差。还有一个容易被忽略的问题特征在标准化之前要先做异常值截断。电力数据偶尔会出现遥测坏值比如某个传感器突然返回一个巨大值如果不先做百分位截断标准化后的正常样本都会被压缩到一个很小的区间模型整体失效。5.3 模型误报率偏高怎么办先不要急着调模型参数而是看特征工程。误报通常意味着正常样本的特征波动没有被刻画好。我一般先输出验证集上误报的那些样本逐个看是哪些特征偏离了正常分布。有一个特别常见的来源负荷曲线的自然波动。比如突增的大工业负荷会让电流和功率上升网络侧可能也有批量下发峰谷电价指令多个正常事件叠加特征向量就会出现类似攻击的模式。解决办法是在特征集中加入运行方式上下文特征比如日前计划值、季节系数、峰谷时段标志让模型知道这类波动是正常的。如果正常业务调整经常变化最简单实用的降误报手段是设置冷却时间。同一个告警源在一定时间内只告警一次不再重复触发。这个策略不是机器学习却能显著减少值班员的疲劳感。5.4 快问快答排查速查表现象可能原因排查思路训练集AUC很高测试集大跌时间序列随机打乱了改为按时间顺序切分所有样本都被判正常样本严重不平衡添加class_weight或做过采样特征重要性集中在单一源量纲未标准化或特征冗余检查标准化和特征去重孤立森林把大量正常标为异常正常样本形态多变提高contamination或加上下文特征代码报错维度不一致对齐后不同源的行数不同统一重采样频率和窗口长度在线推理比训练慢很多特征提取没有向量化用numpy/pandas的滑动窗口向量化实现结尾一点个人体会做了这么多融合检测的实践我最深的一个感受是多源数据融合不是把数据堆在一起这么简单。每加一路传感器都要先想清楚它到底能提供什么独立证据会不会和已有数据高度相关。比如电流和功率在很多场景下相关性极强同时纳入可能带来的不是互补而是噪声。真正有效的融合是找到那些在攻击场景下会背离正常物理关系的跨源组合。后续如果要继续扩展我建议在三个方向上下功夫一是接入更多时序异常检测算法比如时序卷积网络或轻量级Transformer替换掉人工统计特征二是增加在线学习机制让模型定期用新的正常样本做增量更新三是把融合结果做成可解释告警报告输出哪些特征异常最大、对应哪个传感器、可能属于哪类攻击这会大大提升现场运维的接受度。最后分享一个很实在的调试技巧每次改完特征或模型参数先把验证集里预测错的样本逐个看一遍能从里面挖掘出很多测试指标看不出来的问题。尤其是那些被漏掉的攻击样本别急着归因于“样本太少”很多其实是特征工程里某个细节没做到位。数据融合的功夫一半在模型另一半在“懂数据的人”对细节的坚持。