简介:围绕电力系统网络入侵检测的多源数据融合方案,面向具备Python基础、从事电力系统安全或工业控制系统安全研究的科研人员与研究生,旨在解决异构传感器数据难以协同利用、异常行为识别精度不足的问题。资料以docx文档承载完整实现思路,压缩包仅含1个docx文件,体积约77KB,轻量便于携带与查阅。文档依次覆盖数据预处理、特征提取、融合算法设计与入侵检测模型构建等环节,并给出Python代码实现,可对照理解多源数据在检测流程中的组织方式与分类模型设计细节。已有35人学习,适合作为算法复现与融合策略优化的实践参考,读者可尝试在自有数据集或仿真环境中测试调优,加深对多源数据协同检测机制的理解。
1. 需求切入:单一数据源的入侵检测为什么在电力系统里站不住
做过电力监控系统安全评估的人都碰到过同一个尴尬:只看网络流量的入侵检测模型,在实验室数据上准确率漂亮得离谱,一挂到现场镜像口上,误报多到运维直接把告警关掉。问题往往不在分类器,而在于视角本身就是残缺的——流量看不见主机上被改过的配置文件,主机日志看不见协议层被伪造的遥控报文,协议报文又看不见一次跨网段的横向扫描。多传感器融合在电力系统网络入侵检测里要解决的,就是把网络流量、主机审计日志、电力专用协议报文这三路异构数据对齐到同一时间轴,先做多源数据融合,再交给判定模型。这套 Python 实现的方案,适合做电力系统安全方向毕设、课设,也适合给工控安全方案补一块可落地的检测模块。
2. 总体架构设计:三源数据、四层结构与融合时机的取舍
2.1 三路数据源各自能看见什么
网络侧的输入通常是流记录或抓包摘要,字段包括五元组、包长分布、TCP 标志位组合、连接持续时间、单位时间新建连接数。它擅长捕捉端口扫描、暴力破解、异常外联这类有明显连接行为特征的事件,对加密流量之后的操作就基本失明。
主机侧的输入是审计日志,包含进程创建、文件写入、用户登录、权限变更、服务注册。它擅长捕捉提权、持久化驻留、配置篡改,但日志本身可能被攻击者清理,而且单条事件的价值很低,必须聚合成序列。
电力协议侧的输入是报文解析结果,Modbus、IEC 60870-5-104 这类协议的功能码、地址域、时标、遥控遥调操作序列都能结构化出来。它擅长捕捉伪造遥控、异常点表读写、非法总召唤这类业务层攻击,是电力场景区别于通用 IT 场景的核心信息源。
三者的采样节奏差异极大:流量是秒级连续,协议报文是毫秒级突发,主机事件是稀疏的离散点。这个差异直接决定了后面所有对齐工作的难度,也是很多人做融合时第一个翻车的地方。
2.2 四层结构与各层职责边界
整个方案我一般拆成采集层、对齐层、融合层、判定层,每层只干一件事,层与层之间用明确的数据契约衔接。
采集层负责从镜像口、主机探针、协议解析器拿到原始记录,只做最基础的字段裁剪,不做任何聚合。见过太多人图省事在采集端就把数据按分钟汇总了,等到后面想换窗口粒度,整条链路得推翻重来。
对齐层是这套方案的工作重心,输入是三个异构表,输出是同一个时间桶索引下的特征矩阵和时间张量,附带每个桶的标签。这一层的产出必须能独立保存成文件,方便复现和排查。
融合层负责把对齐好的多源特征送进模型,或者在分源建模之后做决策合并。融合层不关心数据怎么来的,只认列名约定。
判定层把概率转成告警,负责阈值搜索、告警分级、抑制重复告警。阈值不该写死在模型里,它属于运维策略,应该跟着现场误报承受度走。
提示:把对齐层的输出落盘成 parquet,是后期排查「为什么这条攻击没报出来」最省时间的习惯,比在内存里一遍遍重跑强得多。
2.3 融合时机怎么选:特征级、决策级与混合式
融合发生在哪一步,直接决定模型能不能学到跨源关联,也决定系统在某个源缺失时的鲁棒性。
| 融合方式 | 优势 | 代价 | 适用条件 |
|---|---|---|---|
| 特征级融合 | 模型能自动学跨源交叉项,通常上限更高 | 维度膨胀,单源长期缺失时整行无效,可解释性差 | 三源在同一时间桶内基本都有数据,样本量充足 |
| 决策级融合 | 分源可解释,某源缺失时降级运行,新增传感器不用重训全模型 | 丢掉跨源时序关联,权重需要额外调 | 主机日志稀疏、协议报文突发不均的场景 |
| 混合式融合 | 兼顾两者,用概率当特征再拼少量关键原始特征 | 工程量最大,调参项最多 | 有明确交付要求、需要同时给出告警原因解释 |
我的默认选择是先把特征级跑通当基线,因为实现最短、最快看到数字。如果发现主机源在超过三成的时间桶里是空的,就切到混合式:分源出概率,再把三个概率加上协议功能码的 one-hot 拼成最终输入。纯决策级更适合作业里用来做对比实验,证明融合策略本身的价值。
3. 数据预处理与特征工程:把异构日志对齐成统一时间张量
3.1 时间基准统一与多尺度窗口划分
对齐的第一步是时间。现场设备时钟漂移是常态,主机和网络设备之间差几秒完全不稀奇,如果直接按原始时间戳做最近邻 join,融合结果会出现大量空洞。我的做法是统一转 UTC,然后按固定桶聚合,桶边界对齐到整秒的整数倍,绝不用滑动 join。
窗口长度不是随便定的。太短(1 秒以下)会导致主机日志几乎全空,融合退化成单源;太长(5 分钟以上)会把一次扫描和它后面的提权操作压进同一个桶,标签变得含混。经验值是主窗口取 5 到 10 秒,同时并行跑一路 60 秒的长窗口,专门捕捉低频的横向移动和慢速扫描,两路特征在融合层拼接。
import pandas as pd import numpy as np def to_bucket(df, time_col, freq="5s"): """把单个源的原始记录按固定时间桶聚合 freq: 桶宽度,短窗口 5s 用于抓突发,长窗口 60s 用于抓低频行为 label/closed: 桶左闭右开,保证相邻桶不重叠计数 """ df = df.copy() # 统一 UTC,避免不同设备本地时区造成错位 df[time_col] = pd.to_datetime(df[time_col], utc=True) df = df.set_index(time_col).sort_index() # 关键:用 resample 而不是 merge_asof,桶边界严格对齐 return df.resample(freq, label="left", closed="left") def build_features(bucketed, agg_map, prefix): """bucketed: resample 对象;agg_map: 列名到聚合函数的映射 空桶填 0 而不是丢弃,否则稀疏源的正样本会被大量删除 """ feat = bucketed.agg(agg_map).fillna(0) feat.columns = [f"{prefix}_{a}_{b}" for a, b in feat.columns] return feat这段代码有两个设计点值得说明。一是用resample而不是merge_asof或join(asof),后者在时钟漂移时容易把一次攻击的报文和几秒前的主机事件硬配到一起,看着有数据,其实是假的关联。二是空桶用 0 填充而不是 dropna,主机日志在正常情况下本来就稀疏,如果直接把空桶删掉,被删掉的大多是没有主机事件的时段,模型会学到「有主机日志就危险」这种伪规律。
3.2 字段抽取与数值化策略
三类源的字段性质完全不同,数值化方式也要分开处理。
流量侧的数值字段(包长、连接时长、字节数)跨度极大,直接标准化会被少数超大值拉偏,先做log1p再标准化更稳。类别字段里,协议类型这种低基数字段直接 one-hot;目的 IP 这种高基数字段不能 one-hot,否则维度直接爆炸,我一般替换成「是否内网地址」「出现频次是否高于该桶的 95 分位」两个布尔特征。
主机侧的字段主要是事件类型和进程名。事件类型 one-hot,进程名取路径的最后一段之后做频次编码,出现次数低于阈值的一律归到other,否则会冒出几千个只出现一两次的类别。
协议侧的核心是功能码和操作对象地址。功能码 one-hot 就够用;地址域要按业务分区做映射,把「读的是哪个间隔的哪个点」编码成区域编号,比裸的地址数字有信息量得多。
另外强烈建议给每个源加一列缺失指示。当某个桶里主机日志为空时,host_missing=1,让模型自己决定是把这个空当成正常还是当成「日志被清理了」——后者在真实攻击里是非常强的信号。
3.3 标签对齐与类别不平衡处理
标签对齐是最容易被糊弄过去的一步。一次攻击往往持续几十秒到几分钟,跨了好几个时间桶。常见的错误做法是直接把攻击开始时间所在的桶标成正样本,结果训练集里正样本少得可怜,模型根本学不到东西。
合理的做法是重叠判定:攻击区间和一个桶的重叠时长超过桶宽的一半,这个桶就标正。这样一次持续 3 分钟的攻击能在 5 秒粒度上贡献 30 多个正样本,样本量够训练了。
不平衡比例在电力入侵检测里通常很夸张,1:50 到 1:200 都算正常。处理顺序是:先按时间或按攻击会话做数据集划分,再只在训练集上做 class_weight 或欠采样,测试集保持原始分布不动。这个顺序颠倒了,后面的指标全是假的。
def label_buckets(attack_intervals, bucket_index, min_overlap=0.5, freq="5s"): """按重叠比例给时间桶打标签 attack_intervals: [(start, end, label), ...] 攻击区间列表 bucket_index: 对齐后的时间桶索引 min_overlap: 重叠时长占桶宽的比例阈值,超过即标正 """ width = pd.Timedelta(freq) y = np.zeros(len(bucket_index), dtype=int) for start, end, _ in attack_intervals: for i, t in enumerate(bucket_index): b_start, b_end = t, t + width ov = (min(end, b_end) - max(start, b_start)).total_seconds() if ov > 0 and ov / width.total_seconds() >= min_overlap: y[i] = 1 return y参数min_overlap取 0.5 是一个折中。取太小会把攻击前后的正常桶也标正,取太大则样本量不足;如果攻击特别短(比如单包探测),可以降到 0.2,但要在论文或报告里写清楚这个设定,答辩时是会被问的。
4. 融合模型实现:从特征拼接基线到加权软投票
4.1 特征级融合:多源特征拼接与树模型基线
特征级融合是最短路径。把三个源的特征块按列拼接,列名带源前缀,然后送进一个带标准化的树模型。选随机森林而不是深度模型,原因很实际:样本量通常只有几万到几十万条,特征维度几十到一两百,树模型在这个规模上更稳,训练几分钟出结果,还自带特征重要度可以给答辩时讲解释性。
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.ensemble import RandomForestClassifier def build_fusion_pipeline(): """特征级融合基线:标准化 + 随机森林 class_weight: 用 balanced_subsample 让每棵树的 Bootstrap 内部自动平衡 max_depth: 限制在 18 左右,防止把时间桶的记忆当成规律 """ return Pipeline([ ("scaler", StandardScaler()), ("clf", RandomForestClassifier( n_estimators=300, max_depth=18, min_samples_leaf=3, class_weight="balanced_subsample", n_jobs=-1, random_state=42)), ]) # 拼接时保留源前缀,后面对特征重要度按前缀分组统计 # X_fused = pd.concat([ # X_net.add_prefix("net_"), # X_host.add_prefix("host_"), # X_proto.add_prefix("proto_"), # ], axis=1)max_depth=18和min_samples_leaf=3这两个值不是拍脑袋。时间桶数据里的正样本高度自相关,相邻桶几乎是同一次攻击的复制品,树太深会直接记住某一段的具体数值组合,验证集看起来很好,换一批数据就崩。加前缀除了方便分组统计重要度,还有一个好处:训练完之后打印 top 20 特征,能一眼看出模型到底依赖哪个源,这是答辩时「你的融合到底有没有用」的最有力证据。
4.2 决策级融合:分源建模与加权软投票
决策级融合给每个源单独训练一个模型,输出正类概率,再按权重合并。这条路的好处是单源缺失时可以只丢掉那一路的概率,其余照常出结果;坏处是权重需要额外确定,而且各源模型各自过拟合时,误差会在融合时叠加。
import numpy as np def fit_source_models(X_dict, y, model_factory): """每个源独立训练,互不共享参数 X_dict: {"net": X_net, "host": X_host, "proto": X_proto} """ fitted = {} for name, X in X_dict.items(): m = model_factory() m.fit(X, y) fitted[name] = m return fitted def weighted_soft_vote(fitted, X_dict, weights): """软投票:合并各源的正类概率 weights: 与 fitted 的 key 顺序一致的权重列表,内部会做归一化 用软投票而非硬投票,是因为概率能保留置信度,且方便后续调阈值 """ names = list(fitted.keys()) w = np.asarray(weights, dtype=float) w = w / w.sum() probs = np.column_stack([ fitted[n].predict_proba(X_dict[n])[:, 1] for n in names ]) return probs @ w权重不能拍脑袋,也不能在测试集上调。正确做法是从训练集里再切一段验证集(按时间切,不是随机切),在验证集上网格搜索权重组合,步长 0.1,三源加起来搜索空间只有几十组,几十次评估就够。搜完之后测试集只跑一次,整个过程只留一个最终数字。
如果某一源的模型在验证集上的单源 F1 明显低于随机水平(比如 0.3 以下),权重直接给 0,别指望融合能救回来——融合只能放大有效信号,不能把噪声变成信号。
4.3 混合融合与阈值后处理
混合式是我在正式交付里用得最多的方案:输入是三路概率加上协议功能码的 one-hot,训练一个浅层模型做最终判定。它保留了决策级「单源可降级」的好处,又让最终模型能学到「流量可疑但主机也异常」这种跨源组合。
概率出来之后还有一个阈值问题。默认 0.5 在 1:100 的不平衡数据上几乎必然把召回率压到很低。正确做法是在验证集上扫一遍阈值,按宏平均 F1 或按现场能接受的误报率来定。
from sklearn.metrics import f1_score def sweep_threshold(y_true, prob, grid=np.arange(0.05, 0.96, 0.01)): """在验证集上搜索最优判定阈值 返回 (best_threshold, best_macro_f1) 两个指标同等重要时用 macro F1,更看重漏报时改用 recall 作目标 """ best_t, best_f1 = 0.5, -1.0 for t in grid: pred = (prob >= t).astype(int) f1 = f1_score(y_true, pred, average="macro") if f1 > best_f1: best_t, best_f1 = t, f1 return best_t, best_f1这个阈值函数看着简单,但必须在验证集上调用,把结果作为一个常量配置存下来,跟模型参数一起版本化。见过太多人训练时搜了阈值、部署时又写回 0.5,线上效果直接掉一半,排查半天才发现是配置没跟过去。
5. 常见问题排查:融合之后指标不升反降的五个翻车点
5.1 时间戳漂移导致融合样本大面积空洞
现象是融合后的特征矩阵里,主机相关列有六七成是 0,模型几乎学不到主机信息,融合效果和只用流量差不多。
原因是各设备时钟没有统一,主机日志记录的是本地时间,流量探针用的是另一套时区,直接按字符串时间对齐就会整体偏移几秒到几分钟。
解决办法是在对齐层强制统一 UTC,并在采集端配置统一时间源;对齐完成后打印每个源的桶覆盖率,如果某个源覆盖率低于 60%,先怀疑时钟,别急着改模型。
5.2 随机划分造成同一条攻击流跨集泄漏
现象是验证集 F1 高达 0.97,换一批数据掉到 0.6,指标完全是玄学。
原因是按随机方式划分数据集时,同一次攻击被切成了相邻的时间桶,一部分进了训练集,一部分进了测试集,模型其实是在测试集上认出了训练时见过的片段。
解决办法是按攻击会话或按时间区间划分:把整段时间切成不重叠的几段,整段进训练或整段进测试。代价是训练集和测试集的类别分布会有点偏,但那个数字才是真的。
5.3 类别极不平衡下的准确率幻觉
现象是模型准确率 0.98,但混淆矩阵里正样本一个都没检出来。
原因是正样本占比不到 2%,全判负就能拿到 0.98。这个坑在做毕设时特别常见,因为准确率是最好看的数字。
解决办法是报告时以宏平均 F1、正类召回率、误报率为主,准确率只作参考。类别权重、欠采样这些手段只在训练集上做,测试集保持原始比例。
5.4 归一化与特征选择跨集污染
现象是线下指标很好,线上重新采集的数据上表现很差,而且怎么调模型都没用。
原因是先在全量数据上做 StandardScaler 或先做特征选择,再把数据切分训练测试。测试集的均值方差和信息量已经渗进了训练过程,这属于典型的预处理泄漏。
解决办法是把 scaler 放进 Pipeline 里,保证 fit 只在训练折上发生;特征选择同理,用 Pipeline 包起来或者用 SelectFromModel 嵌在交叉验证内部。
5.5 融合权重在测试集上调参
现象是决策级融合的权重调完之后效果非常好,但换成另一批数据就完全不行。
原因是权重搜索直接用了测试集,等于把测试集当验证集用了一遍又一遍,最终指标已经不是泛化性能了。
解决办法是训练集内部再切一份按时间排的验证集,权重只在这份上搜;测试集从头到尾只用一次。这一点在答辩时尤其要注意,评委很爱问「你的权重是怎么定的」。
6. 验收与答辩:用一组消融实验证明融合确实有效
融合类工作最容易被打回的地方,不是模型不够新,而是没法证明「融合」这个动作本身带来了增益。指标高可能只是某个单源本身强,跟你把三路拼起来没关系。所以验收的核心动作是做消融,让每一行数字都能对应到一个明确的变量。
我一般会准备下面这张表,五个配置跑同一套划分、同一套预处理:
| 实验配置 | 输入 | 预期结论 |
|---|---|---|
| M1 单源流量 | 仅 net_ 特征 | 基线,证明单源上限 |
| M2 单源协议 | 仅 proto_ 特征 | 电力场景特有信息源的贡献 |
| M3 特征级融合 | net + host + proto 拼接 | 相对 M1 的提升即融合增益 |
| M4 决策级融合 | 三源分模型软投票 | 与 M3 对比,说明融合时机的影响 |
| M5 融合 + 阈值调优 | M3 或 M4 加阈值搜索 | 说明后处理带来的边际收益 |
每行至少给宏平均 F1、正类召回、误报率三个数,别只给一个。误报率这一列在电力场景里比 F1 更打动评委,因为现场运维最在意的就是「一天弹多少次」,你把这个数算出来并说明换算方式,说服力立刻不一样。
答辩环节有三个问题几乎必问,提前准备好答案。第一是「为什么不直接用深度学习」,答案是样本量和不平衡程度决定的,可以补一句「如果样本量涨到百万级,我会先试一维卷积做时序编码」;第二是「某一源断开怎么办」,这正是决策级融合和混合式的价值,讲降级路径,别讲「不会断」;第三是「误报怎么抑制」,讲阈值搜索加告警去重,如果能把同一攻击会话在时间上相邻的告警合并成一条,这一点会加分。
还有一个容易被忽略的细节:把对齐层输出的样本数量和正样本数量写进报告里。很多人的融合模型实际上只用了不到三成的时间桶,剩下全是空行,评委一旦追问覆盖率就会翻车。我一般会在结果表下面直接注明「有效融合桶占比 82%,主机源覆盖率 64%」,把短板摆在明面上,比被问出来强。
血泪经验是:做完融合一定要回头看一眼单源基线。有次我折腾了两天调融合权重,最后发现协议单源的 F1 就已经接近融合结果,融合那点提升在方差范围内。从那以后我每次启动融合实验前,都强制先把三个单源基线跑完并记在一张纸上,再动融合的代码。希望这套结构能帮到你,少走几步弯路。
本文还有配套的精品资源,点击获取