联邦学习入侵检测实战:从NSL-KDD预处理到FedAvg聚合
2026/9/14 23:59:48 网站建设 项目流程

简介:基于联邦学习和NSL-KDD数据集的网络入侵检测Python项目,面向计算机相关专业正在做课程设计、期末大作业的学生,以及需要项目实战练习的学习者。项目包含完整源码与运行说明,带GUI界面,覆盖客户端本地训练、服务端模型聚合、通信连接、模型权重保存与结果对比等模块,针对NSL-KDD多分类任务,多客户端在保护数据隐私的前提下协同训练入侵检测模型,清晰展示联邦学习在入侵检测场景中的落地流程。压缩包共62个文件,以Python脚本及字节码为核心,辅以CSV数据、日志、训练权重、结果对比图和README说明,整体约26.18MB,目录结构清晰,便于按功能模块检索与调试。资源已有201人学习下载,是经导师指导并通过的高分项目,评审98分,可作为课设或大作业的参考范本,帮助读者快速复现实验、理解核心思路并进行二次扩展。

1. 联邦入侵检测落地:一个 98 分的课程项目能拆出多少东西

如果你准备做网络入侵检测(NIDS)的课设或期末大作业,大概率会遇到两个坎:一是 NSL-KDD 数据集怎么预处理才能喂给模型,二是如何在展示时说明白“联邦学习”和普通深度学习的区别。最近拆了一个标注 98 分的项目包,里面有 client/server 两套代码、GUI 界面、预训练权重和训练日志,整体结构不复杂但很完整,适合当成一个可复现的 FL-IDS 基线。本文会顺着数据处理、模型设计、联邦聚合到 GUI 集成的顺序,把这套方案的实现逻辑和重跑参数讲清楚,最后补上排错技巧和通信压缩的优化方向。适合正在做课设的学生,也适合想快速验证联邦学习在 NIDS 上效果的工程师。

2. NSL-KDD 数据清洗与特征工程:从原始记录到模型输入张量

2.1 为什么选 NSL-KDD 而不是 KDDCup99

在入侵检测研究里,KDDCup99 是最早被广泛使用的基准,但它存在两个致命问题:训练集和测试集中有大量重复记录,导致模型在重复样本上刷出虚高的准确率;同时冗余样本会让学习算法偏向出现频率高的攻击类型。NSL-KDD 由 Tavallaee 等人提出,剔除了重复项,并为每个难度级别保留合适的样本数,使得评估结果更接近真实泛化性能。

本项目的训练数据是KDDTrain+.txt,测试数据是KDDTest+.txt。每条样本有 41 个特征和 1 个标签。这 41 个特征从语义上可以分成四组:基础 TCP 连接特征(duration、protocol_type、service、flag 等)、内容特征(登录失败次数、root shell 尝试次数等)、流量特征(过去 2 秒内相同主机的连接数等)、以及基于主机的流量特征。处理的核心是把符号型特征转成数值,再把标签从 23 类攻击映射成二分类或五分类。

2.2 符号特征的一热编码与标签归一化

原代码里initDate.py承担了数据读取和预处理。它的处理顺序很有代表性:

import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler, LabelEncoder COLUMNS = [ 'duration', 'protocol_type', 'service', 'flag', 'src_bytes', 'dst_bytes', 'land', 'wrong_fragment', 'urgent', 'hot', # ... 省略中间特征,共 41 列 'dst_host_srv_rerror_rate', 'label' ] df = pd.read_csv('KDDTrain+.txt', header=None, names=COLUMNS) df['label'] = df['label'].apply(lambda x: 0 if x == 'normal' else 1) # 对三个符号特征做 one-hot df = pd.get_dummies(df, columns=['protocol_type', 'service', 'flag']) # 分离特征和标签 X = df.drop('label', axis=1).values.astype(np.float32) y = df['label'].values # 标准化:只 fit 训练集,再 transform 测试集 scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test)

逻辑说明:get_dummies会把protocol_type变成protocol_type_tcpprotocol_type_udp等列,原本 41 维特征经过这一步会扩展到 120 维左右。标签这里采用二分类映射,但如果你要做五分类,可以直接把label映射成normal, probe, dos, u2r, r2l对应的整数。标准化时只对训练集fit再对测试集transform,这点很关键——如果把测试集也参与fit,相当于把测试集信息泄露给训练过程,评估结果会偏乐观。

2.3 参数表:输入维度和类分布

下表是该项目中实际使用的预处理参数和数据集规模,重跑时可以直接参考。

参数项数值说明
原始特征数41包含 3 个符号特征
one-hot 后特征数122取决于 service 的取值数量
训练样本数125973去掉重复样本后的标准值
测试样本数22544与训练集分布不同,更具挑战性
二分类标签normal / attack本项目采用
标准化方式StandardScalerz-score 归一化

因为 NSL-KDD 中 U2R 和 R2L 类别样本极少,二分类可以避免极端类别不平衡带来的训练震荡。如果你想做细粒度检测,建议对这两个类别做上采样或使用加权损失函数,否则模型很容易把所有样本都预测成 normal。

3. 联邦学习下的 DNN 模型:本地训练与参数聚合的设计逻辑

3.1 模型结构:为什么用全连接网络而不是 CNN/LSTM

对于 NSL-KDD 这类结构化表格数据,卷积网络没有空间结构可挖,LSTM 虽然能建模序列,但单条网络连接记录并不是天然的时序序列。因此项目采用多层全连接网络(DNN),输入层接收预处理后的 122 维特征,中间层用 ReLU 激活,输出层是 sigmoid 二分类。模型定义在model.py中,核心代码如下:

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout def create_model(input_dim=122): model = Sequential([ Dense(64, activation='relu', input_shape=(input_dim,)), Dropout(0.2), Dense(32, activation='relu'), Dropout(0.2), Dense(1, activation='sigmoid') ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) return model

参数说明:第一层 64 个神经元,Dropout 设为 0.2 用于缓解过拟合;第二层压缩到 32 维,减少参数量。因为 NSL-KDD 特征之间线性相关性较强,这个规模的网络已经有足够容量。实际训练时每轮本地 epoch 数不必太多,联邦学习的核心不在单模型精度的极致,而在多客户端协同后的全局收敛。

3.2 联邦聚合:FedAvg 的实现与边界条件

联邦学习在入侵检测中的价值在于多个机构可以共享模型参数,而无需直接交换原始流量数据。服务端采用最常见的 FedAvg 算法:每个客户端在本地数据上训练若干轮,然后把模型权重(model.get_weights())上传给服务器;服务器按样本数量加权平均所有客户端的权重,再下发回客户端作为下一轮初始模型。

用公式表达就是:

w_global = Σ (n_k / n) * w_local_k

其中n_k是第 k 个客户端的样本数,n是总样本数。这个加权系数能防止数据量小的客户端主导全局模型。在本项目的 client/server 架构中,main_client_1.pymain_client_2.py分别代表两个参与方。本地训练的逻辑如下:

def local_train(model, x_local, y_local, epochs=3): history = model.fit(x_local, y_local, batch_size=32, epochs=epochs, verbose=0) return model.get_weights()

服务端聚合代码在main_server.py里,大致是:

client_weights = [] for client_socket in client_sockets: # 接收序列化后的权重 weights = recv_weights(client_socket) client_weights.append(weights) # 简单平均(没有加权时) avg_weights = [] for layer_idx in range(len(client_weights[0])): layer_sum = sum(w[layer_idx] for w in client_weights) avg_weights.append(layer_sum / len(client_weights)) model.set_weights(avg_weights)

注意这里的常见误区:如果两个客户端数据量差异很大,简单平均会导致全局模型偏向小数据集的一方。正确做法是让客户端同时上报n_k,服务端加权平均。本项目因为把 NSL-KDD 训练集平均切分成两份,所以简单平均也能得到不错的结果,但你不应该在生产环境中这么做。

3.3 非独立同分布数据对收敛的影响

NSL-KDD 本身是人为构造的均衡数据集,直接切分后每个客户端拿到的是独立同分布(IID)数据。实际联邦学习场景中,比如不同学校的网络日志,攻击类型分布差异巨大,这就是非 IID 数据。此时 FedAvg 收敛速度会明显下降,甚至出现灾难性遗忘——客户端 A 学过 DoS,客户端 B 学过 Probe,聚合后的模型可能把两者都忘掉。

针对非 IID,可以做的改进是:服务端在聚合前对每层的权重做归一化检查,或者引入弹性平均(FedProx)。本项目为了演示方便没有做这些增强,但代码结构里权重传输和聚合的接口是保留的,你可以在utils.py中增加权重裁剪逻辑。通信压缩也是一个方向,比如对权重做 Top-k 稀疏化,只上传梯度变化最大的 10% 参数,能显著降低带宽开销,代价是本地模型需要维护一个误差补偿向量。这套做法在热词搜索里通常叫作偏置压缩,实现起来比想象中简单,后面第 5 章会给出具体代码思路。

4. 训练流程与通信机制:从 client 启动到 GUI 展示的完整闭环

4.1 项目目录结构与启动顺序

拿到源码包后,你会看到两台“虚拟节点”的代码:main_client_1.py负责在本地跑模型并发送权重,main_client_2.py几乎一样,只是端口和数据集切片不同;main_server.py监听端口、聚合权重并下发。connFun.py封装了 socket 通信的序列化和反序列化,解决 numpy 数组在 TCP 上的传输问题。

运行顺序固定是:

# 1. 启动服务端 python main_server.py # 2. 打开两个终端,分别启动客户端 python main_client_1.py python main_client_2.py # 3. 所有客户端就绪后,服务端开始发初始权重 python GUI.py

提示:GUI.py需要 matplotlib 和 tkinter 环境,在无桌面服务器上可以把 GUI 关掉,只保留训练日志输出。

4.2 通信封装:numpy 数组如何在 socket 上传输

connFun.py是这套代码里容易被忽略但很重要的模块。PyTorch 或 Keras 的权重是 NumPy 数组,socket 只能发送 bytes,因此需要先把数组转成 bytes,接收方再重建。常见做法是把数组tobytes()并带上 shape 和 dtype 元信息。项目里的实现大致是:

import socket import pickle def send_object(sock, obj): data = pickle.dumps(obj, protocol=4) length = len(data) sock.sendall(length.to_bytes(8, 'big')) sock.sendall(data) def recv_object(sock): length_bytes = b'' while len(length_bytes) < 8: length_bytes += sock.recv(8 - len(length_bytes)) length = int.from_bytes(length_bytes, 'big') chunks = [] remaining = length while remaining > 0: chunk = sock.recv(min(65536, remaining)) chunks.append(chunk) remaining -= len(chunk) return pickle.loads(b''.join(chunks))

说明:前 8 个字节用固定大端序表示后续 payload 长度,然后按这个长度循环接收。pickle对模型权重这种纯数值数据是安全的,但如果你做的是在线服务,建议改用 MessagePack 或自定义协议以防反序列化攻击。注意发送numpy数组前先调np.asarray(obj, dtype=np.float32)统一类型,避免大端小端问题。

4.3 训练超参与时间开销对照

以下是本项目实测中比较稳定的超参设置,来自argu.py

超参数数值说明
联邦通信轮数 rounds10每轮客户端做一次本地多 epoch 训练
本地 epoch5每个客户端每轮训练 5 次全量数据
batch_size32影响收敛平稳度,过大容易震荡
学习率0.001Adam 默认值已能满足
客户端数量2超过 2 个也可,只要改 main_client 副本
验证集比例0.2服务端每轮在测试集上评估

在两张无 GPU 的云主机上,每轮训练大约 20 秒,10 轮总共 3 分多钟就能跑完。如果你本地只有一台机器,也可以开 3 个终端模拟多客户端,注意修改每个客户端的hostport,以及数据切分的起始偏移量,否则所有客户端都在训练同一份数据,联邦学习就退化成单机训练。

4.4 GUI 状态机与日志输出

GUI 的核心作用不是增强功能,而是向评审展示“正在训练”的证据。它轮询读取data.log中新增的行,解析出每轮的 loss 和 accuracy,然后绘制实时曲线。data.log的格式是服务端每轮追加一行:

round=3, client1_loss=0.42, client2_loss=0.38, global_acc=0.93

如果你没有显示环境,同样可以从data.log里拿到结果。这里的技巧是把日志写到内存而不是磁盘文件,减少高频轮询的 I/O 压力;但课程项目为了可读性,直接写文件没问题,评审老师一般会打开文件看训练过程。

5. 重跑实验的排错技巧:从权重文件到结果对比图

5.1 从预训练权重判断收敛状态

压缩包里有Net.weightlocal_model.weightlocal_testModel.weight三个权重文件。区分它们很简单:local_model.weight是某个客户端本地最后一轮的模型参数,Net.weight是服务端聚合得到的全局模型参数,local_testModel.weight则是客户端用本地数据测试全局模型后保存的参数。三者数值不同是正常的,你要关注的是它们对应的测试准确率。

判断收敛的快速方法是加载全局权重到create_model(),然后用model.evaluate(X_test, y_test)打印 loss 和 accuracy。如果 loss 曲线在第 5 轮之后还在明显下降,说明训练轮数不够,可以把argu.py里的rounds调大;如果出现 loss 降低但准确率静默不变,多半是类别不平衡导致模型只预测 normal。

5.2 通信链路故障定位

最常见的报错是服务端ConnectionResetError,原因通常是两个客户端的端口冲突,或者服务端先关闭了连接。排查顺序如下:

# 查看两个客户端是否连上服务端 netstat -an | grep 9010 # 服务端打印接收到的权重长度 python main_server.py --debug

如果客户端频繁掉线,优先检查connFun.pyrecv循环是否处理了socket.timeout异常。建议在send_object时用socket.setdefaulttimeout(60),避免客户端训练太久导致服务端误判超时断开。

5.3 通信压缩的进阶改造

最后给一个能写进“工作展望”的技巧:偏置压缩。常规联邦学习上传的是完整权重向量,假设权重有 50 万个浮点,每个 4 字节就是 2 MB。使用稀疏化方法后,只上传绝对值最大的 10% 权重,其余不下发。你需要为每个客户端保存一个“误差累加器”,把没上传的权重变化累积下来,等下一轮和新的变化一起算。

import numpy as np def compress_weights(weights, top_k_ratio=0.1): # 将多层权重展平并拼成一个向量 flat = np.concatenate([w.flatten() for w in weights]) k = max(1, int(flat.size * top_k_ratio)) indices = np.argpartition(np.abs(flat), -k)[-k:] # 只保留 top-k 的值和索引 compressed = { 'indices': indices.astype(np.uint32), 'values': flat[indices].astype(np.float32) } return compressed

在服务端接收后,把缺失位置的权重解释为上一轮的旧值,再做平均。这样每轮通信量能缩减到原来的 10% 左右,在广域网上优势明显。作为对比,你可以把resultCompare1.pngresultCompare2.png分别视为“未压缩”和“压缩后”的准确率曲线,观察两者差异是否在可接受范围内。

5.4 最终验证方法

把整套代码跑通后,请保留三样东西:data.log、最终的模型权重文件、以及 GUI 截屏。这是课程评审最关心的证据链。用下述命令快速导出模型在测试集上的混淆矩阵:

import joblib from sklearn.metrics import confusion_matrix y_pred = (model.predict(X_test) > 0.5).astype(int) print(confusion_matrix(y_test, y_pred))

注意本项目默认是二分类,混淆矩阵只看 normal 和 attack 的判别情况。如果你需要五分类的细粒度结果,就要回到 2.2 节重新映射标签,并且把输出层的sigmoid换成softmax。到这一步,你已经不只是“会跑通代码”,而是能针对不同数据集和联邦场景调整模型与通信策略了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询