资讯详情 多传感器融合电力入侵检测:时间对齐与特征拼接实战
📅 2026/10/13 19:21:38
简介本资源面向具备Python基础、从事电力系统安全或工业控制系统安全研究的科研人员与研究生提供一套针对网络入侵的多源数据融合完整代码实现。内容围绕多传感器融合技术展开涵盖数据预处理、特征提取、融合算法设计与入侵检测模型构建等关键环节通过融合异构传感器数据提升对异常行为的识别精度与响应效率可用于电力系统网络安全防护中的早期预警与精准识别也为关键基础设施安全监控提供实践参考。资源包共1个docx文件约77KB以文档形式组织代码说明与实现细节便于逐模块分析融合逻辑与分类模型设计。目前已有31人学习读者可结合代码复现算法并在实际数据集或仿真环境中测试调优深入理解多源数据协同检测机制。1. 多传感器融合在电力系统入侵检测中的真实位置变电站里那台工控机同时挂着 SCADA 遥测、继电保护报文、网络流量镜像和机房温湿度探头四路数据各说各话。传统 IDS 只看流量遇到针对 IEC 60870-5-104 或 Modbus TCP 的慢速扫描就集体失明——攻击者把恶意指令拆进正常遥调帧里单看流量根本分不出来。多传感器融合要解决的就是这件事把网络层、物理层、业务层的异构信号对齐到同一时间轴用互补性把单源漏报补上。这套方案适合做电力监控系统安全、工控协议异常检测的工程师也适合手里已有流量数据集、想加物理侧特征提升召回的人。下面按「数据怎么对齐 → 特征怎么拼 → 模型怎么训 → 坑在哪」走一遍代码可直接跑。2. 四路数据源怎么对齐到同一时间轴2.1 电力场景下多源数据的物理含义与采样率差异先厘清每路数据到底在测什么。网络流量镜像pcap给出的是报文级时间戳微秒精度但只在有通信时才有数据SCADA 遥测是秒级或分钟级轮询反映的是断路器位置、有功无功继电保护报文GOOSE/SV是毫秒级甚至微秒级突发环境传感器温度、振动通常是秒级。四路采样率差三个数量级直接 concat 就是灾难——高频段被低频段的时间空洞稀释模型学到的全是「没数据」的模式。常见做法是选一个基准时间粒度我一般用 1 秒。理由SCADA 轮询周期通常 1~5 秒网络流量的统计特征在 1 秒窗口内已经稳定保护报文虽然快但可以聚合成窗口内的计数和极值。基准定了所有源都往这个格子上落。对齐的核心不是插值而是「窗口聚合 缺失标记」。电力系统里缺失本身就是信号——比如某秒突然没有 GOOSE 报文可能是保护装置被绕过。所以不要用前向填充把缺失抹掉要显式加一列goose_missing。2.2 用 pandas 做时间窗聚合的最小实现import pandas as pd import numpy as np # 假设四路原始数据已读成 DataFrame各自有时间戳列 ts # net_df: 网络流统计, scada_df: 遥测, goose_df: 保护报文, env_df: 环境 FREQ 1S # 基准 1 秒 def window_agg(df, freq, agg_map, prefix): 按时间窗聚合缺失窗口保留并标记 df df.set_index(ts).sort_index() # 用 resample 落格closed/left 保证窗口边界一致 g df.resample(freq, closedleft, labelleft) out g.agg(agg_map) out.columns [f{prefix}_{c} for c in out.columns] # 标记该窗口是否有原始数据 out[f{prefix}_missing] g.size() 0 return out # 网络流包数、字节数、唯一目的端口数、SYN 比例 net_agg window_agg(net_df, FREQ, { pkt_len: [count, sum, mean], dst_port: pd.Series.nunique, syn_flag: mean }, net) # SCADA遥测量的均值、极差 scada_agg window_agg(scada_df, FREQ, { active_power: [mean, max, min], breaker_pos: last }, scada) # GOOSE报文计数、最大间隔 goose_agg window_agg(goose_df, FREQ, { msg_len: [count, max], gap_ms: max }, goose) # 环境温度、振动均值 env_agg window_agg(env_df, FREQ, { temp: mean, vib: std }, env) # 按时间索引外连接缺失窗口自动变 NaN但 missing 列已标记 fused net_agg.join([scada_agg, goose_agg, env_agg], howouter) fused fused.fillna(0) # 数值填 0missing 列保留布尔语义逻辑说明resample的closedleft和labelleft必须成对用否则相邻窗口边界会重叠或漏掉。g.size() 0是判断空窗口的可靠方式比isnull()稳因为有些聚合函数对空组返回 0 而不是 NaN。fillna(0)只对数值列做missing列是布尔填 0 后语义变成「无缺失」所以实际代码里我会把 missing 列单独拎出来最后拼避免被误填。参数上FREQ不是拍脑袋定的。如果 SCADA 轮询是 2 秒用 1 秒窗会导致一半窗口 SCADA 缺失模型会过度依赖scada_missing这个特征。我一般让FREQ等于最慢那路源的周期或者用 2 秒。窗口太大会把攻击的瞬态抹平太小则高频源稀疏。1~2 秒是电力工控场景的甜点区。2.3 时间对齐后必须检查的三件事第一检查各源的时间戳时区是否一致。现场经常出现 SCADA 用本地时间、流量采集用 UTC 的情况差 8 小时对齐后全是错位。统一转 UTC 再落格。第二检查窗口内是否有跨源因果倒置。比如网络流量显示 10:00:01 发出跳闸指令但 GOOSE 报文在 10:00:00 就动了——这种在真实系统里可能是时钟不同步也可能是攻击重放。对齐后要人工抽几个窗口看原始报文确认不是采集端时钟漂移。第三检查类别标签怎么落到窗口上。攻击标注通常是一个时间段比如 10:00:00~10:00:30 是注入攻击。落格时要把这 30 秒内所有窗口标为正样本但边界窗口可能只有部分攻击行为我一般把边界窗口丢弃宁可少要样本也不引入噪声标签。3. 特征拼接与模型选型为什么不是简单 concat3.1 多源特征的三类融合层级融合分三个层次数据级raw fusion、特征级feature fusion、决策级decision fusion。数据级要求各源同构且严格同步电力场景里网络包和温度根本不同构做不了。决策级是每个源单独训一个模型再投票实现简单但丢掉了跨源关联——而跨源关联恰恰是检测「网络指令导致物理动作异常」的关键。特征级是主流每个源先提自己的特征拼成一个向量再送分类器。本文走特征级。但特征级有个陷阱不同源的特征量纲和稀疏度差异大。网络流的包数字段可能上万温度是 20~30直接送进 SVM 或 KNN 会被大数值主导。所以拼接前必须做 per-source 标准化而不是全局标准化。全局标准化会把「网络流量大」和「温度高」混在一个尺度里破坏源内结构。3.2 用 sklearn Pipeline 做分源标准化与拼接from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.compose import ColumnTransformer from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import TimeSeriesSplit # 按前缀分组列名 net_cols [c for c in fused.columns if c.startswith(net_)] scada_cols [c for c in fused.columns if c.startswith(scada_)] goose_cols [c for c in fused.columns if c.startswith(goose_)] env_cols [c for c in fused.columns if c.startswith(env_)] # 每个源独立标准化missing 列不参与标准化0/1 语义 pre ColumnTransformer([ (net, StandardScaler(), net_cols), (scada, StandardScaler(), scada_cols), (goose, StandardScaler(), goose_cols), (env, StandardScaler(), env_cols), ], remainderpassthrough) # missing 列原样保留 clf Pipeline([ (pre, pre), (rf, RandomForestClassifier( n_estimators300, max_depth12, class_weightbalanced, # 攻击样本远少于正常 random_state42, n_jobs-1 )) ]) # 时序数据不能用随机 KFold必须用 TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) X fused.drop(columns[label]) y fused[label] for train_idx, test_idx in tscv.split(X): clf.fit(X.iloc[train_idx], y.iloc[train_idx]) score clf.score(X.iloc[test_idx], y.iloc[test_idx]) print(ffold recall: {score:.3f})逻辑说明ColumnTransformer让每个源走自己的StandardScalerremainderpassthrough把 missing 列直接透传避免被标准化成无意义的小数。class_weightbalanced在电力入侵检测里几乎是必须的因为攻击窗口占比通常低于 5%不加权模型会全预测正常。TimeSeriesSplit不能用KFold否则未来窗口泄漏到训练集离线指标虚高上线就翻车。参数上max_depth12是防过拟合的保守值。如果特征维度超过 200可以放到 15~18但要配合min_samples_leaf5。n_estimators300在千级样本下足够再多边际收益很低。如果换成 XGBoostscale_pos_weight设成负正样本比效果通常比class_weight更稳。3.3 为什么我优先用树模型而不是深度模型电力入侵检测的样本量通常不大一个变电站几个月的标注数据可能就几万条窗口其中攻击几百条。深度模型在这种量级下容易过拟合而且调参成本高。树模型对特征尺度不敏感、能输出特征重要性、训练快适合快速迭代。特征重要性还能反过来验证融合是否有效——如果goose_missing或net_syn_ratio排在前列说明跨源特征确实被用上了如果全是scada_mean这类单源特征说明融合没起作用得回去检查对齐。另外工控现场对推理延迟敏感树模型推理是微秒级深度模型在无 GPU 的工控机上可能几十毫秒遇到高频 GOOSE 场景会丢窗口。这不是理论问题是现场血泪经验。4. 避坑与排查多源融合最容易翻车的五个点4.1 时间戳时区不一致导致对齐全错现象离线指标 AUC 0.95上线后几乎全漏报。原因训练数据里 SCADA 时间戳是本地时间流量是 UTC对齐后网络特征和物理特征错位 8 小时模型学到的是「凌晨的网络流量对应白天的温度」这种伪关联。解决所有源在读入时第一件事就是pd.to_datetime(..., utcTrue)统一转 UTC 再落格。落格后再转回本地时间做可视化检查。4.2 用前向填充处理缺失窗口现象模型在正常数据上表现很好但攻击窗口召回极低。原因fillna(methodffill)把缺失窗口填成了上一个正常窗口的值攻击导致的「数据突然消失」被抹平了。解决数值列填 0 或中位数同时保留missing布尔列。缺失本身是特征不要消灭它。4.3 全局标准化破坏源内结构现象特征重要性里网络包数字段权重异常高物理特征几乎没用。原因全局StandardScaler把所有列混在一起算均值和方差网络包数的量纲主导了缩放。解决用ColumnTransformer分源标准化每个源独立算统计量。如果某源特征维度很高可以先做源内 PCA 再拼。4.4 随机划分数据集导致时序泄漏现象交叉验证指标 0.98测试集 0.6。原因用train_test_split(shuffleTrue)把未来窗口混进了训练集模型记住了未来模式。解决TimeSeriesSplit或按时间切分训练集时间必须早于测试集。如果攻击类型随时间变化还要做按攻击类型的留出验证。4.5 忽略类别不平衡直接看准确率现象准确率 97%但攻击一个没抓到。原因攻击样本占比 3%全预测正常就有 97% 准确率。解决看召回率、F1 和 PR-AUC不看准确率。class_weightbalanced或scale_pos_weight必加。如果召回还是低降低决策阈值——树模型的predict_proba输出概率阈值从 0.5 降到 0.3 通常能换回不少召回代价是误报上升现场要权衡。5. 把融合模型落到工控机上的三个实操技巧5.1 用滑动窗口做在线推理而不是等整秒离线训练用 1 秒窗在线推理如果也等整秒再算遇到 GOOSE 突发会延迟 1 秒才报警对跳闸类攻击太慢。我一般用滑动窗口每收到 200ms 的新数据就重算一次最近 1 秒的聚合特征模型推理一次。这样延迟降到 200ms代价是计算量增加 5 倍。工控机上用轻量树模型max_depth8能扛住。from collections import deque class OnlineFuser: def __init__(self, window_sec1.0, step_sec0.2): self.buf deque() # 存 (ts, source, features) self.window window_sec self.step step_sec self.last_infer 0.0 def push(self, ts, source, feats): self.buf.append((ts, source, feats)) # 丢弃窗口外的旧数据 while self.buf and ts - self.buf[0][0] self.window: self.buf.popleft() if ts - self.last_infer self.step: self.last_infer ts return self._aggregate_and_predict() return None def _aggregate_and_predict(self): # 按 source 分组聚合逻辑同离线此处省略具体统计 # 返回 (is_attack, prob) pass逻辑说明deque做滑动缓冲popleft保证内存不涨。last_infer控制推理频率避免每个数据点都推理。聚合逻辑要和离线训练完全一致否则训练推理不一致train-serving skew会让指标崩掉。我一般把离线聚合函数抽成独立模块离线和在线共用同一份代码。5.2 特征版本管理模型和特征必须同版本上线现场常见事故模型更新了但特征计算脚本没更新新模型收到旧特征输出全是垃圾。解决给特征计算代码打版本号模型文件里存特征版本加载时校验。不匹配就拒绝启动宁可停服也不要输出错误报警。组件版本字段校验时机特征计算模块feature_version模型加载时模型文件model_version feature_version服务启动时聚合窗口配置window_sec, step_sec每次推理前5.3 用对抗样本做上线前验证离线指标好看不代表能扛攻击。我一般会构造几类对抗样本把攻击窗口的网络特征微调改包数、改端口分布看模型召回掉多少把物理特征加高斯噪声看是否还稳。如果微调后召回从 0.9 掉到 0.3说明模型过拟合到特定特征值需要加正则或扩充训练数据。这一步没有捷径但能提前暴露大部分上线翻车。最后说个习惯每次调完融合方案我都会把特征重要性打出来逐条问「这个特征在物理上说得通吗」。如果某个网络特征权重极高但解释不了对应的物理行为大概率是对齐或标注出了问题。这个习惯帮我省过好几次后悔药。希望帮到你。本文还有配套的精品资源点击获取