
简介这份资源面向计算机相关专业学生与项目实战学习者提供一套基于联邦学习与NSL-KDD数据集的网络入侵检测Python实现可用于课程设计、期末大作业或安全方向练手。项目在保证数据隐私的前提下通过多个参与方本地训练并共享模型参数完成对网络流量异常行为的识别配套运行说明与带GUI界面的数据集便于直观查看和处理数据。压缩包共63个文件约26.19MB以py源码、pyc缓存、txt说明、log日志、weight模型权重及png对比图为主另含csv数据、md文档与license覆盖数据预处理、模型构建、训练与测试等模块。目前已有87人学习下载。读者可据此掌握联邦学习框架搭建、NSL-KDD特征处理、模型训练与结果对比的完整流程并借助现成脚本与权重文件快速复现实验、排查运行问题。1. 联邦学习遇上 NSL-KDD这套入侵检测代码到底能不能跑起来拿到一个压缩包标题写着「联邦学习 NSL-KDD 网络入侵检测」第一反应往往是又是拿公开数据集套个新名词的课程作业。但真拆开看联邦学习用在入侵检测这个场景其实相当合理——流量数据天然分散在各个节点上谁也不想把自己的原始流量交出去联邦学习正好卡在这个需求点上。这份资源给的就是一套能跑的 Python 代码把 NSL-KDD 数据集按客户端切分用联邦平均的方式训练一个分类模型最后输出检测指标。它适合两类人一类是刚接触联邦学习、想找个完整项目跑通流程的另一类是做网络安全方向、想看看联邦学习在 IDS 上到底怎么落地的。下面按「数据怎么切、模型怎么搭、联邦怎么聚合、坑在哪」的顺序拆一遍。2. 数据准备与客户端切分NSL-KDD 的预处理不是读进来就完事2.1 为什么 NSL-KDD 不能直接喂给模型NSL-KDD 是 KDD Cup 99 的修正版去掉了原始数据里大量重复记录训练集 125973 条、测试集 22544 条每条 41 个特征加一个标签。特征里混了三种类型连续数值如 duration、src_bytes、离散符号如 protocol_type、service、flag、以及二值特征。标签有 23 种攻击类型通常归成五大类Normal、DoS、Probe、R2L、U2R。直接读进来会翻车的地方有三个。第一protocol_type 这类字符串特征模型不认识必须做编码。第二src_bytes 这种特征的量纲跨度极大从 0 到上亿都有不归一化的话梯度会被大值主导。第三NSL-KDD 的测试集里包含训练集没出现过的攻击子类这是它故意设计的用来测模型的泛化能力所以不能简单按随机划分来评估。常见做法是符号特征做 one-hot数值特征做 min-max 归一化标签做 label encoding 映射到 0-4 或 0-1二分类时。这套代码里用的是 sklearn 的 LabelEncoder 加 MinMaxScaler顺序不能反——先编码再归一化否则编码后的列会被当成连续值一起缩放。2.2 联邦场景下的 Non-IID 切分脚本联邦学习最怕的就是数据 Non-IID非独立同分布。如果每个客户端的数据分布差不多那联邦平均退化成单机训练的分布式版本体现不出联邦的价值。入侵检测场景里不同节点的流量特征本来就差异很大所以切分时要故意制造分布差异。import numpy as np import pandas as pd from sklearn.preprocessing import LabelEncoder, MinMaxScaler # 读取 NSL-KDD列名按官方定义 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,label,difficulty] train pd.read_csv(KDDTrain.txt, namescol_names) test pd.read_csv(KDDTest.txt, namescol_names) # 标签映射23 种攻击归为 5 大类 attack_map { normal: Normal, back:DoS,land:DoS,neptune:DoS,pod:DoS,smurf:DoS, teardrop:DoS,apache2:DoS,udpstorm:DoS,processtable:DoS,worm:DoS, ipsweep:Probe,nmap:Probe,portsweep:Probe,satan:Probe, mscan:Probe,saint:Probe, ftp_write:R2L,guess_passwd:R2L,imap:R2L,multihop:R2L, phf:R2L,spy:R2L,warezclient:R2L,warezmaster:R2L, sendmail:R2L,named:R2L,snmpgetattack:R2L,snmpguess:R2L, xlock:R2L,xsnoop:R2L,httptunnel:R2L, buffer_overflow:U2R,loadmodule:U2R,perl:U2R,rootkit:U2R, ps:U2R,sqlattack:U2R,xterm:U2R } train[label] train[label].map(attack_map) test[label] test[label].map(attack_map) # 符号特征 one-hot数值特征归一化 cat_cols [protocol_type,service,flag] full pd.concat([train, test], axis0) full pd.get_dummies(full, columnscat_cols) num_cols [c for c in full.columns if c not in [label,difficulty]] scaler MinMaxScaler() full[num_cols] scaler.fit_transform(full[num_cols]) le LabelEncoder() full[label] le.fit_transform(full[label]) # 按标签分层切给 N 个客户端制造 Non-IID def split_noniid(df, n_clients5, alpha0.5): Dirichlet 分布切分alpha 越小分布越偏 labels df[label].unique() client_data [[] for _ in range(n_clients)] for lbl in labels: idx df[df[label] lbl].index.tolist() np.random.shuffle(idx) proportions np.random.dirichlet([alpha] * n_clients) splits (np.cumsum(proportions) * len(idx)).astype(int)[:-1] for i, chunk in enumerate(np.split(idx, splits)): client_data[i].extend(chunk) return [df.loc[c] for c in client_data] clients split_noniid(full, n_clients5, alpha0.5)这段脚本的关键在split_noniid函数。Dirichlet 分布的alpha参数控制分布偏斜程度alpha 越小每个客户端拿到的类别越集中Non-IID 越严重alpha 趋近无穷大时退化成均匀分布。我一般先用 alpha0.5 跑一遍看基线再降到 0.1 观察模型掉多少点这样能直观感受到 Non-IID 对联邦聚合的影响。n_clients5是常见起点客户端太少体现不出联邦优势太多则每轮通信开销大、小客户端样本不足。注意pd.get_dummies在 train 和 test 上分别做会导致列不对齐所以这里先 concat 再编码最后按索引切回去。如果内存吃紧可以只对 train 做 fittest 用 reindex 对齐列。2.3 数据加载器与批处理参数切分完每个客户端拿到的是一个 DataFrame训练时要转成 PyTorch 的 DataLoader。批大小一般设 64 或 128太小梯度噪声大太大在客户端样本少时一个 epoch 只有几步。学习率用 0.001 配 Adam这是联邦学习里比较稳的组合。如果客户端数据量差异大可以在聚合时按样本数加权而不是简单平均。3. 模型搭建与联邦聚合从本地训练到全局更新3.1 本地模型选型为什么用 MLP 而不是深网络入侵检测的特征是 41 维结构化数据不是图像也不是序列用 CNN 或 Transformer 属于杀鸡用牛刀。这套代码用的是三层 MLP输入层接 41 维one-hot 后维度会涨到 120 左右中间两个隐藏层各 128 和 64 个神经元输出层 5 类。激活函数 ReLU最后接 softmax。Dropout 设 0.3 防过拟合因为 NSL-KDD 训练集虽然大但某些攻击类样本很少U2R 只有几十条不加 dropout 容易在这几类上过拟合。import torch import torch.nn as nn class IDSModel(nn.Module): def __init__(self, input_dim, num_classes5): super().__init__() self.net nn.Sequential( nn.Linear(input_dim, 128), nn.ReLU(), nn.Dropout(0.3), nn.Linear(128, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, num_classes) ) def forward(self, x): return self.net(x) # 本地训练一个 epoch def local_train(model, loader, epochs1, lr0.001): optimizer torch.optim.Adam(model.parameters(), lrlr) criterion nn.CrossEntropyLoss() model.train() for _ in range(epochs): for x, y in loader: optimizer.zero_grad() loss criterion(model(x), y) loss.backward() optimizer.step() return model.state_dict()input_dim要跟预处理后的特征维度对上one-hot 之后大概是 120 上下具体跑一遍full.shape就知道。epochs1是联邦学习的常见设置——每个客户端每轮只训一个 epoch然后上传参数这样通信轮数多但每轮计算轻。如果设成 5 或 10本地过拟合风险上升聚合后的全局模型反而不一定好。3.2 FedAvg 聚合加权平均的细节联邦平均FedAvg的核心就一句话按各客户端样本数加权平均模型参数。听起来简单但实现时有几个细节容易写错。第一聚合的是state_dict里的每一层不是整个模型对象。第二权重是n_k / n_total不是1 / K。第三聚合后要把新参数 load 回全局模型再分发给客户端。def fedavg(global_model, client_states, client_sizes): 按样本数加权平均 total sum(client_sizes) new_state {} for key in global_model.state_dict().keys(): new_state[key] sum( client_states[i][key] * (client_sizes[i] / total) for i in range(len(client_states)) ) global_model.load_state_dict(new_state) return global_modelclient_sizes是每个客户端的样本数列表。如果所有客户端样本量差不多加权平均和简单平均结果接近但如果某个客户端只有几百条而另一个有几万条不加权的话小客户端会拖偏全局模型。这也是 Non-IID 场景下 FedAvg 容易掉点的原因之一——加权只能缓解不能根治。3.3 完整联邦训练循环与评估把上面的拼起来一个完整的联邦训练循环大概长这样初始化全局模型每轮选一部分客户端参与或全选各客户端复制全局参数做本地训练上传参数服务端聚合然后在一份独立的测试集上评估。from torch.utils.data import DataLoader, TensorDataset def run_federated(clients, input_dim, rounds20, clients_per_round5): global_model IDSModel(input_dim) for r in range(rounds): states, sizes [], [] selected np.random.choice(len(clients), clients_per_round, replaceFalse) for i in selected: df clients[i] X torch.tensor(df.drop(columns[label]).values, dtypetorch.float32) y torch.tensor(df[label].values, dtypetorch.long) loader DataLoader(TensorDataset(X, y), batch_size64, shuffleTrue) local_model IDSModel(input_dim) local_model.load_state_dict(global_model.state_dict()) state local_train(local_model, loader, epochs1) states.append(state) sizes.append(len(df)) global_model fedavg(global_model, states, sizes) # 每轮评估 acc evaluate(global_model, test_loader) print(fRound {r1}: test acc {acc:.4f}) return global_modelrounds20是起步值实际跑的时候看准确率曲线什么时候平。clients_per_round如果等于总客户端数就是全参与小于则是部分参与后者更接近真实场景有些节点可能掉线。评估用的 test_loader 要从原始测试集构建不能混进训练数据。提示联邦学习里有个经典现象叫灾难性遗忘——如果客户端数据分布差异极大模型在某一轮偏向某个客户端后下一轮可能把之前学的忘了。缓解办法包括增加参与客户端比例、降低本地学习率、或者在聚合时做正则。这套代码没内置这些需要自己加。4. 避坑与排查跑不通的时候先看这几条4.1 准确率卡在 75% 上不去现象训练轮数加到 50测试准确率一直在 75% 左右晃loss 也不降。原因最常见的是标签没做类别映射23 种攻击直接当 23 类训但测试集里有些攻击类训练集根本没有模型只能瞎猜。另一个可能是归一化用了 fit_transform 在测试集上重新 fit导致训练和测试的缩放标准不一致。解决确认标签已经归成 5 大类归一化只在训练集上 fit测试集用 transform。如果还不行检查 one-hot 后 train 和 test 的列是否对齐用train.columns.difference(test.columns)看一眼。4.2 某些客户端 loss 变成 NaN现象训练几轮后某个客户端的 loss 突然变 NaN之后全局模型也崩了。原因NSL-KDD 里有些数值特征在归一化后仍然有极端值或者某个客户端分到的样本里全是某一类batch 里标签单一导致交叉熵计算异常。另外学习率设太大比如 0.01在 Non-IID 下也容易梯度爆炸。解决在归一化后加一步np.clip把值截到 [0,1]学习率降到 0.001 或 0.0005如果某个客户端样本太少少于 100 条考虑合并到其他客户端或直接跳过。4.3 聚合后模型比单机训练还差现象同样数据放一起单机训练能到 85%联邦跑完只有 78%。原因这就是 Non-IID 的代价。如果 alpha 设得太小比如 0.1每个客户端只见到一两类样本本地模型严重偏斜加权平均后全局模型被拉偏。另外如果客户端数量少比如 3 个FedAvg 的方差会很大。解决把 alpha 调到 0.5 或 1.0 先看基线增加客户端数量到 10 以上或者改用 FedProx在本地 loss 里加一项近端正则限制本地模型不要偏离全局太远。4.4 测试集准确率远低于训练集现象训练集上 95%测试集只有 70%。原因NSL-KDD 的测试集故意包含了训练集没有的攻击类型这是数据集设计如此不是代码 bug。如果按随机划分自己的测试集准确率会高很多但那样评估的是同分布泛化不是真实场景。解决接受这个差距把它当作泛化能力的真实反映。如果想缩小差距可以在训练时做数据增强对少数类过采样或者用开集识别的方法处理未知攻击。4.5 多进程 DataLoader 在 Windows 上报错现象num_workers设成大于 0 时Windows 上直接抛RuntimeError或卡死。原因Windows 的进程启动方式是 spawn 而不是 forkDataLoader 的多进程需要把数据集对象序列化如果数据集里有不可序列化的东西就会炸。解决把num_workers设成 0或者把训练代码包在if __name__ __main__:里。联邦学习本身客户端之间就是串行训练的DataLoader 用单进程完全够。5. 进阶技巧让联邦入侵检测真正可用的三个改动跑通基线之后如果想让这套代码更接近能用的状态有三个改动值得试。第一个是用 FedProx 替代 FedAvg。FedProx 在本地训练的 loss 里加了一项(mu/2) * ||w - w_global||^2mu 一般设 0.01 到 0.1。它的作用是限制本地模型不要跑太偏在 Non-IID 严重时比 FedAvg 稳。改起来很简单在local_train的 loss 后面加一项就行def local_train_fedprox(model, global_state, loader, mu0.01, epochs1, lr0.001): optimizer torch.optim.Adam(model.parameters(), lrlr) criterion nn.CrossEntropyLoss() model.train() for _ in range(epochs): for x, y in loader: optimizer.zero_grad() loss criterion(model(x), y) prox 0.0 for name, param in model.named_parameters(): prox ((param - global_state[name]) ** 2).sum() loss (mu / 2) * prox loss.backward() optimizer.step() return model.state_dict()mu越大本地模型越不敢偏离全局但太大就退化成不训练了。我一般从 0.01 开始试看测试准确率有没有提升。第二个是按攻击类别分层评估。整体准确率会掩盖少数类的表现——U2R 只有几十条样本全预测成 Normal 也能有 99% 的准确率。用sklearn.metrics.classification_report看每一类的 precision 和 recall重点关注 R2L 和 U2R。如果这两类 recall 低于 0.5说明模型对稀有攻击基本没学到需要过采样或调类别权重。第三个是通信轮数与本地 epoch 的权衡。联邦学习里有个经验本地多训几个 epoch 能减少通信轮数但 Non-IID 下本地训太多会过拟合。我一般先固定本地 epoch1 跑 50 轮看曲线再试 epoch3 跑 20 轮对比总计算量和最终准确率。多数情况下 epoch1 配更多轮更稳。从那以后我每次跑联邦项目都会先把 Non-IID 程度alpha和客户端数量这两个参数固定住再调模型和聚合策略——不然变量太多出了问题根本不知道是哪个环节的锅。希望帮到你。本文还有配套的精品资源点击获取