简介:基于联邦学习与NSL-KDD数据集的网络入侵检测完整项目,面向机器学习、网络安全方向的开发者与学生,适合课程设计、毕业设计或实践参考。项目完整覆盖客户端与服务端联邦训练流程,包括数据预处理、模型定义、参数聚合、结果对比与GUI可视化,可在本地直接编译运行,难度适中,内容经过助教审定。
压缩包共63个文件,包含12个Python脚本和26个pyc缓存,另有多份txt说明、log日志、weight权重、csv数据及png对比图等,整体26.19MB。目录按server、client、utils等模块划分,便于检索学习。目前已有351人学习下载。
资源提供一套可复现的联邦学习入侵检测方案,从NSL-KDD数据初始化、本地模型训练、参数上传到服务端聚合与效果对比均有完整实现,附README和文档说明。对理解联邦学习在网络异常检测中的应用、掌握分布式训练框架搭建有实际帮助,可在此基础上扩展实验或继续优化。
1. 联邦学习 + NSL-KDD:这个入侵检测源码项目究竟要解决什么
联邦学习和 NSL-KDD 数据集加上网络入侵检测,这三个词组合在一起,常见于毕设选题和求职项目里。你拿到一个 python 实现的压缩包,里面大概率有源码、文档说明和训练好的结果图;但真正决定它是不是“高分项目”的不是代码排版,而是能不能回答一个问题——为什么必须在联邦学习框架下做入侵检测。道理很简单:集中式训练精度通常更高,可真实网络场景里,流量数据分散在多个运营商、多个部门,甚至多家公司手里,合规上根本不允许汇总到一台机器。联邦学习就是为这种“数据不动模型动”的场景设计的。本文会带你从原理到代码,把这个方向完整复现一遍。
2. 为什么是联邦学习和 NSL-KDD:拆清原理与数据边界
2.1 数据孤岛让集中式训练失效:联邦学习的立场
网络入侵检测的常规做法是把流量特征汇总进一个训练池,跑 XGBoost 或深度学习模型,效果确实不错。可一旦换成真实的多方环境,问题就来了:A 公司的出口流量、B 政务网的审计日志、C 云的攻击告警,分别属于不同责任主体,谁也不会把原始数据打包发给某个中心节点。这时候数据集本身就变成孤岛,集中式训练在合规层面就不成立。
联邦学习解决的是“怎么在数据不离开本地的情况下联合建模”。标准的横向联邦拓扑里有一个中心服务端和若干客户端。每个客户端保留自己的训练集,本地算梯度或更新模型参数,只把“模型增量”发回服务端;服务端聚合出一版新模型,再下发。整个过程中训练数据始终留在客户端侧,这就是它和分布式训练最大的区别。
要提醒的是,模型参数照样可能泄露信息,所以联邦学习不等于绝对安全。实际项目中后续常叠加差分隐私、安全多方计算。但在 NSL-KDD 这个公开数据集上做验证,首要目标是把 FedAvg 训练链路跑通,让各客户端扬长避短。
2.2 NSL-KDD 的数据结构:41 维特征、三类符号属性与标签体系
NSL-KDD 是 KDD99 的改良版,由加拿大新不伦瑞克大学整理发布。它保留了原始网络的 41 维特征,但剔除了大量冗余记录,解决训练集和测试集里重复样本过多导致的指标失真问题。文件里常用到三份数据:
- KDDTrain+:训练集
- KDDTest+:完整测试集
- KDDTest-21:测试集中去掉“难度最低”记录后的子集,专门用来检验模型对困难样本的识别能力
41 维特征大致分成三组:第一组是 TCP 连接基本属性,比如 duration、protocol_type、service、flag、src_bytes;第二组是 2 秒窗口内的流量统计,例如 count、srv_count、serror_rate;第三组是主机维度的统计,列名以 dst_host_ 开头。其中 protocol_type、service、flag 是符号型,直接喂给神经网络前必须编码。剩下基本都是连续性数值,少数列有极端长尾分布,归一化时要注意。
标签体系有两种用法。第一种是二分类:Normal 记作 0,其它攻击类型统一记作 1,简单直接;第二种是五分类:Normal、DoS、Probe、R2L、U2R。NSL-KDD 里 DoS 和 Probe 样本量充足,R2L 和 U2R 非常稀少,直接做五分类会遭遇严重类别不平衡。我一般建议先跑通二分类验证联邦框架,再扩展五分类看细粒度表现。
2.3 FedAvg 的完整工作流:分发、局部更新、按样本量加权聚合
联邦平均 FedAvg 是现在最常用的基线算法,流程可以拆成四步。第一步,服务端初始化一个全局模型,把参数广播给本轮选中的客户端;第二步,每个客户端用自己的私有数据在本地训练若干 epoch,得到一个新模型;第三步,客户端把新模型的参数或梯度返回服务端;第四步,服务端按各客户端训练样本量的占比,对参数做加权平均,更新全局模型,再进入下一轮。
关键细节在第四步。加权平均不是简单把模型参数求均值。假设客户端 A 有 10 万条样本,客户端 B 只有 1 万条,两者平权会让 A 的信息被严重稀释。正确做法是用样本量占比当权重。用公式表示就是:
全局参数 = Σ (客户端样本量 / 总样本量) × 客户端参数
如果你用的是 PyTorch,实现起来并不复杂:取出每个参数字典,按权重累加,最后 load_state_dict。后面第三章会给出可直接复制的代码。
3. 从零复现:把 NSL-KDD 切给 5 个客户端并跑通 FedAvg
3.1 环境准备与数据下载:torch、pandas、sklearn 复用
先准备环境。这个项目依赖不重,CPU 就能跑,有 GPU 当然更快。我建议用 python 3.9 以上版本,装以下库:
- torch:模型训练和参数聚合
- pandas:读入 CSV 格式的 KDD 数据
- numpy:矩阵运算
- scikit-learn:数据切分、归一化、指标计算
安装命令示意如下:
pip install torch pandas numpy scikit-learn数据可以到 NSL-KDD 官方页面下载三个 txt 文件:KDDTrain+.txt、KDDTest+.txt、KDDTest-21.txt,放到项目 data/ 目录。文件本质是 CSV,只是后缀是 txt,读的时候直接用 pd.read_csv 就行。如果网络不方便,也可以用镜像仓库里的副本,三个文件加起来不大。
下载后先确认文件是否齐全:
ls -lh data/正常情况下能看到三个文件的名称。最后把文件路径和列名定义写进一个常量文件,方便后面脚本复用。
3.2 预处理与特征编码:别让客户端之间的特征空间不一致
NSL-KDD 的特征由 41 列加一个标签列构成,但 3 个符号列必须单独处理。物理链路里 TCP、UDP、ICMP 协议类型是类别,service 有几十种取值,flag 也有 11 种状态。常见做法是 one-hot 编码。下面这段代码定义完整列名并完成编码:
import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler, OneHotEncoder col_names = ["duration","protocol_type","service","flag","src_bytes", "dst_bytes","land","wrong_fragment","urgent","hot", "num_failed_logins","logged_in","num_compromised", "root_shell","su_attempted","num_root","num_file_creations", "num_shells","num_access_files","num_outbound_cmds", "is_host_login","is_guest_login","count","srv_count", "serror_rate","srv_serror_rate","rerror_rate","srv_rerror_rate", "same_srv_rate","diff_srv_rate","srv_diff_host_rate", "dst_host_count","dst_host_srv_count", "dst_host_same_srv_rate","dst_host_diff_srv_rate", "dst_host_same_src_port_rate","dst_host_srv_diff_host_rate", "dst_host_serror_rate","dst_host_srv_serror_rate", "dst_host_rerror_rate","dst_host_srv_rerror_rate"] categorical_cols = ["protocol_type", "service", "flag"] numeric_cols = [c for c in col_names if c not in categorical_cols] def load_data(path): df = pd.read_csv(path, header=None, names=col_names + ["label"]) y = df.pop("label").values return df, y这段代码里header=None是因为源文件没有表头,names参数直接指定列名。y取出的是字符串标签,下一步需要映射成数值。
接着做编码和归一化。这里有一个联邦学习独有的坑:如果直接对整个训练集一次性 fit MinMaxScaler 和 OneHotEncoder,相当于把测试集统计信息泄露给了每个客户端,后面验证联邦效果时指标会虚高。正确做法是先在服务端用一份“公共参考数据”拟合编码器,再复制给各客户端。公共参考数据可以随机采样一部分,也可以理解为所有参与方约定的公共特征字典。
def fit_encoder(df, ref_idx): ref_df = df.iloc[ref_idx] enc = OneHotEncoder(handle_unknown="ignore", sparse_output=False) scaler = MinMaxScaler() enc.fit(ref_df[categorical_cols]) scaler.fit(ref_df[numeric_cols]) return enc, scaler def transform(df, enc, scaler): cat = enc.transform(df[categorical_cols]) num = scaler.transform(df[numeric_cols]) X = np.hstack([num, cat]) return X.astype(np.float32)注意handle_unknown="ignore"很关键。不同客户端只见过各自协议子集,测试时如果出现训练时没见过的 service 值,不会导致编码崩溃,而是补齐零向量。
3.3 把训练集分层切给 5 个客户端:FedAvg 的数据分片方式
真实的联邦场景里,每个客户端数据分布天然不同,比如有的客户端流量大多是 HTTP,有的全是 SSH。但直接用随机切分会让所有客户端分布几乎一样,太理想了。为了更接近真实,我通常采用分层切分保证每个客户端都有 Normal 样本,同时保留类别比例差异。
from sklearn.model_selection import train_test_split def federated_split(X, y, num_clients=5, seed=42): """按分层采样把数据集切成 num_clients 份,返回 [(X0,y0),...],每份类别结构与原数据一致""" client_data = [] tmp_X, tmp_y = X.copy(), y.copy() for c in range(num_clients - 1): tmp_X, rem_X, tmp_y, rem_y = train_test_split( tmp_X, tmp_y, test_size=0.5, stratify=tmp_y, random_state=seed + c ) client_data.append((tmp_X, tmp_y)) tmp_X, tmp_y = rem_X, rem_y client_data.append((tmp_X, tmp_y)) return client_data这个函数每轮保留一半数据,把另一半留给后续客户端,最后一次全部分给末尾的客户端。stratify=tmp_y保证每个分片里 Normal 和攻击的比例和原始数据一致,避免某客户端只有纯攻击样本。test_size=0.5可以按需调,比如 6 个客户端切三次,参数保持不变即可。
得到的client_data是 NumPy 数组或 PyTorch Tensor 的组合。训练时还需要封装成 DataLoader,每个客户端持有自己的加载器。
3.4 FedAvg 核心循环:服务端聚合与客户端更新的最小实现
接下来是重头戏:实现 FedAvg。模型用三层 MLP 已经够用了,NSL-KDD 不是图像任务,不需要复杂网络。输入维度根据预处理后的 X.shape[1] 动态确定。
import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset class IDSMLP(nn.Module): def __init__(self, in_dim): super().__init__() self.net = nn.Sequential( nn.Linear(in_dim, 64), nn.ReLU(), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, x): return self.net(x).squeeze(-1) def local_update(model, X_local, y_local, epochs=3, lr=1e-3, batch_size=64): """客户端本地训练,返回更新后的 state_dict""" dataset = TensorDataset(torch.from_numpy(X_local), torch.from_numpy(y_local).float()) loader = DataLoader(dataset, batch_size=batch_size, shuffle=True) optimizer = torch.optim.Adam(model.parameters(), lr=lr) loss_fn = nn.BCEWithLogitsLoss() model.train() for _ in range(epochs): for xb, yb in loader: optimizer.zero_grad() logits = model(xb) loss = loss_fn(logits, yb) loss.backward() optimizer.step() return model.state_dict()这段代码中BCEWithLogitsLoss在最后一层不加 sigmoid,损失函数内部会替你做数值稳定处理,训练更稳。TensorDataset把 numpy 数组包成数据集,DataLoader 自动按batch_size打乱。
关键聚合函数如下,使用样本数加权:
def fed_aggregate(global_model, client_sd_list, sample_nums): """按样本数加权平均客户端参数,更新全局模型""" global_dict = global_model.state_dict() weights = torch.tensor(sample_nums, dtype=torch.float32) weights = weights / weights.sum() for k in global_dict.keys(): global_dict[k] = torch.zeros_like(global_dict[k]) for sd, w in zip(client_sd_list, weights): for k in global_dict.keys(): global_dict[k] += sd[k].float() * w global_model.load_state_dict(global_dict)这里sample_nums是每个客户端的本地样本量列表。先把所有权重缩放到和为 1,再逐参数字典加权累加。注意一定要把参数转 float 再乘权重,否则整数类型会报错。
最后是完整的训练循环:
def run_fedavg(client_data, test_data, num_rounds=10, local_epochs=3): in_dim = client_data[0][0].shape[1] global_model = IDSMLP(in_dim) logs = [] for rnd in range(num_rounds): client_sds, sample_nums = [], [] for X_local, y_local in client_data: model_copy = IDSMLP(in_dim) model_copy.load_state_dict(global_model.state_dict()) sd = local_update(model_copy, X_local, y_local, epochs=local_epochs) client_sds.append(sd) sample_nums.append(len(X_local)) fed_aggregate(global_model, client_sds, sample_nums) acc = evaluate(global_model, test_data) # 自行实现 logs.append((rnd, acc["accuracy"], acc["f1"])) return global_model, logsnum_rounds=10和local_epochs=3是起步配置。你会观察到前几轮精度上升较快,后面变缓,这很正常。每轮所有客户端都参与,当客户端数量变大后,需要改成随机挑一部分参与,控制通信开销。
4. 复现避坑手册:从训练到评估最容易翻车的地方
4.1 特征归一化用全量数据拟合,联邦结果虚高得离谱
现象:代码在全局模型训练之前,拿整个 KDDTrain+ 的数值列 fit 了一个 MinMaxScaler,然后才切分给客户端。训练完的 Accuracy 高达 99%,比集中式还高,明显不科学。
原因:这是典型的信息泄露。全局 scaler 已经看到了所有客户端的数据分布,相当于每个客户端都在“开卷考试”。联邦场景下服务端不可能持有全部数据,这种预处理方式让对比实验失效。
解决:把编码器和归一化器都看作联邦协议的一部分。常见做法是服务端在初始化时公开一份“参考特征统计量”,可以来自公共先验数据,也可以由参与方通过安全聚合计算全局均值和方差。在你的实验代码里,至少要把 fit 过程放在全局模型之外,并且确保测试集不参与 fit。建议在文档说明里明确写出“预处理器版本”,这样别人复现时才不会有玄学差异。
4.2 模型参数聚合没有按样本量加权,小客户端被平权
现象:5 个客户端里有一个数据量特别大,占 70%,剩下四个加起来才 30%。跑出来的联邦模型精度明显低于集中式,而且大客户端的表现不升反降。
原因:聚合时直接对 state_dict 求平均,相当于每个客户端有相同投票权,大客户端的信息被四个小客户端稀释了。FedAvg 论文里明确指出加权平均是按数据量比例,而不是模型数量。
解决:检查聚合函数里有没有weights = sample_nums / sum(sample_nums)这一步。还有一个小坑:sample_nums必须用本地实际训练样本数,而不是 DataLoader 的迭代次数,因为最后一个 batch 可能不满。
4.3 本地训练轮数设太大,客户端各自漂移引发灾难性遗忘
现象:把 local_epochs 从 3 调到 20,想提升本地拟合效果,结果全局模型反而开始震荡,准确性忽高忽低。
原因:客户端本地训练太多轮,各自模型严重偏向本地数据分布,梯度方向彼此背离。聚合后把这些极端参数做平均,得到的全局模型哪个分布都没吃透。这和联邦学习里的“客户端漂移”本质相关,严重时会出现灾难性遗忘,客户端学到的知识互相覆盖。
解决:控制 local_epochs 在 1~5 之间。想提升精度,优先增加 num_rounds 而不是 local_epochs。每轮通信后再评估一次全局模型,如果发现连续两轮测试指标下滑,就调低本地学习率或加一个余弦衰减。
4.4 用 Accuracy 评估极度不平衡测试集,模型看起来满分实则偏科
现象:二分类模型在 KDDTest+ 上 Accuracy 有 97%,但换上 KDDTest-21 就只有 81%,再看 R2L 和 U2R 两个攻击类别的召回率几乎为零。
原因:KDDTest+ 里绝大多数样本是 Normal 和 DoS,样本量占比极高。模型只要把这两类学好,Accuracy 就很高,R2L 和 U2R 被淹没在大部分类别里,评估指标没有惩罚这种遗漏。
解决:评估指标换成一个表格,至少包含 Accuracy、Weighted-F1、Macro-F1 和被攻击类别的 Recall。其中 Macro-F1 对小类别更敏感,能直观反映联邦模型对稀有攻击的检测边界。建议同时输出 KDDTest+ 和 KDDTest-21 两张结果表,后者才是真正的压力测试。
4.5 标签映射出错,Normal 与攻击类别编码颠倒
现象:训练时 loss 不下降,或者一开始就崩到十几。检查数据发现,“normal”被映射成了 1,其它 attack 映射成了 0,逻辑完全反了。
原因:label列的取值大小写不一,有的记录是normal,有的是NORMAL。直接 compare 字符串会漏项。
解决:统一转小写再映射。用一个最简单字典:
label_map = {} for v in y_all: is_attack = 0 if v.lower() == "normal" else 1 label_map[v] = is_attack y_all = np.array([label_map[v] for v in y_all])注意先统计所有出现的标签,再一次性映射,避免测试集出现训练集没见过的类别字符串。
5. 验证联邦 IDS 究竟有没有价值:三步对比与两个收尾习惯
验证一个联邦项目不是把训练曲线画出来就完事,你得证明联邦模型逼近集中式模型,且优于单客户端独训。我的做法分三步。
第一步,固定随机种子,跑三个对照:单客户端本地模型、集中式模型、FedAvg 全局模型。对比在 KDDTest-21 上的 Accuracy 和 Macro-F1。通常集中式最高,FedAvg 会低 1 到 2 个百分点,而单个客户端独训会明显更差。如果 FedAvg 高过集中式,多半是你踩了 4.1 的信息泄露坑。
第二步,调参与率。把 clients_per_round 从 100% 降到 20%,看测试精度损失。精度损失在 3% 以内说明聚合算法稳,超过这个值就要检查客户端数据异构程度。
第三步,把标签从二分类扩展成五分类。你只需要把y的映射改成五个类别,模型输出层改成 5 个节点,损失函数换成 CrossEntropyLoss。特征、切分、聚合逻辑完全不用动。这一步能看出稀有类别 R2L/U2R 的瓶颈在哪里。
我自己第一次跑这类源码时,栽在了 4.1 的归一化信息泄露上,当时还以为是联邦占优。后来把随机种子、数据划分文件和预处理器版本全部固化到 Git,当作复现契约的一部分。也希望这份踩坑经验能帮你在联邦学习与网络入侵检测这个方向上,少被“玄学指标”骗一次,真正做出一个可信、能讲清原理的高分项目。希望这些细节对你有用。
本文还有配套的精品资源,点击获取