Python+一维CNN实现网络入侵检测:NSL-KDD数据预处理与模型训练实战
简介基于Python与CNN卷积神经网络构建的网络入侵检测完整项目包面向计算机、数学、电子信息等专业学生可作为课程设计、期末大作业或毕业设计的参考资料。压缩包共33个文件约21.58MB结构清晰4个Python脚本分别负责数据预处理、CNN模型构建、模型训练和预测12个CSV文件为NSL-KDD数据集及处理结果7个XML工程配置文件另含模型权重文件、项目说明文档和多张准确率/精确率评估图可直观查看模型收敛与分类效果。项目对NSL-KDD数据集做了归一化与格式调整完整覆盖从原始数据到训练评估、再预测的流程能帮助学习者理解CNN在入侵检测中的网络设计、特征处理与调参思路。已有631人学习浏览适合需要搭建CNN入侵检测实验环境、借鉴完整源码与项目文档开展课程设计或毕业论文研究的学生也可基于项目说明直接复现或改进实验。1. 为什么用 Python 一维 CNN 做网络入侵检测网络入侵检测是课程设计和毕业设计里出现频率很高的题目但很多开源代码只给一个能跑的脚本数据预处理和模型评估指标经常对不上换一份数据就失效。这套基于 Python CNN 的入侵检测项目把完整链路串起来了NSL-KDD 数据集的符号特征编码、Min-Max 归一化、一维卷积网络结构、训练循环、best_model.pth 权重文件以及 Predict.py 预测脚本配套 README 和准确率、精确率图表是一个可以直接复现的完整模板。CNN 在这里不是用来做图像分类而是把一条网络连接的 41 个特征当作一维信号用卷积核自动提取局部特征组合省去了传统方法里大量的人工特征工程。适合的人群有两类一类是刚开始接触网络流量分析、需要完成课程设计或期末大作业的在校生另一类是想快速验证一维卷积在表格型网络数据上效果的研发人员。NSL-KDD 是公开基准数据集样本量适中这套项目里的网络只有十万级参数用 CPU 训练几十分钟就能跑完不需要 GPU。选型逻辑也很直接41 维特征序列很短Conv1d 参数量小、训练快又比全连接网络更能保留相邻特征的局部关联。接下来从数据形态入手把符号编码、归一化、模型结构、训练评估和权重加载这几个环节逐个拆开讲。2. NSL-KDD 数据前处理标签映射、数值特征与 Min-Max 归一化2.1 NSL-KDD 数据集结构与 41 维特征的三种类型NSL-KDD 是在 KDD CUP 99 基础上剔除冗余记录后的入侵检测基准数据集每行对应一条网络连接前 41 列是特征最后一列是标签。41 个特征可以分成三类TCP 连接基本特征、连接内容特征、基于时间的流量统计特征。基本特征包括持续时间 duration、协议类型 protocol_type、目标端口 service、连接状态 flag 以及 src_bytes、dst_bytes内容特征与登录认证行为相关例如 logged_in、root_shell流量统计特征描述过去 2 秒内的窗口行为例如 count、srv_count、serror_rate。特征类别典型特征取值特点预处理方式TCP 连接基本特征duration, protocol_type, service, flag, src_bytes, dst_bytes协议和服务是符号型字节数动态范围大符号编码 归一化连接内容特征logged_in, root_shell, is_guest_login多数是二值 0/1基本不需要额外处理流量统计特征count, srv_count, serror_rate部分连续小数部分大整数归一化后参与模型计算三类特征的预处理优先级不同。root_shell、logged_in 这类 0/1 值本身已是数值型直接参与张量计算即可protocol_type、service、flag 是字符串不做编码就无法进入模型而 src_bytes 这类跨度极大的数值如果不缩放会直接主导梯度更新方向。项目里的 PreHandle.py 要处理的核心工作就是把这些风格完全不同的特征统一成适合 CNN 输入的数值矩阵。2.2 攻击标签合并策略二分类还是五分类NSL-KDD 的原始标签由具体攻击名组成比如 neptune、smurf、ipsweep、satan、guess_passwd。常见做法是合并成四类攻击 DoS、Probe、R2L、U2R加上 normal 构成五分类也可以直接合并成 normal 和 attack 二分类。二分类更容易训练五分类能区分攻击类型但 R2L 和 U2R 样本量太少模型容易忽略这两个类别。课程设计要求不高时建议先跑通二分类再扩展到五分类README 里 accuracy.jpg 和 precision.jpg 对应的应该就是训练过程中记录下来的验证集指标。PreHandle.py 中的二分类标签映射可以写成下面这样ATTACK_MAP_2CLASS { normal: 0, neptune: 1, smurf: 1, pod: 1, teardrop: 1, land: 1, back: 1, ipsweep: 1, portsweep: 1, satan: 1, nmap: 1, guess_passwd: 1, ftp_write: 1, imap: 1, phf: 1, multihop: 1, warezclient: 1, warezmaster: 1, spy: 1, buffer_overflow: 1, loadmodule: 1, perl: 1, rootkit: 1 }这段代码把具体攻击名称统一映射成 1normal 映射成 0后续训练、评估、预测都必须使用同一份映射表否则模型输出类别序号和真实标签对不上。这里还有一个容易忽略的点如果数据集中出现映射表未收录的攻击名df[label] df[class].map(...)会产生 NaN直接导致训练张量报错。稳妥的做法是在 map 之前先对 label 列做去重检查把所有出现过的攻击名补进映射表。五分类只是把 value 改成 0 到 4 的不同编号思路完全一样。提示R2L 和 U2R 的样本量远小于其他类别直接训练时 CNN 很容易忽略它们。如果发现预测结果里这两类几乎不出现优先检查类别权重或对少数类做重采样。2.3 Min-Max 归一化流程与数据泄漏防护Min-Max 归一化的公式是 x (x - min) / (max - min)把特征统一映射到 [0,1]。项目里不选 Z-Score 的原因在于NSL-KDD 特征大量偏态且含 0 值Z-Score 以均值和标准差为中心对长尾分布并不鲁棒一维卷积网络对输入尺度又很敏感数值范围不一致时卷积核很难收敛到公共的权重尺度。项目里的 minMax.png 保存的正是特征缩放前后的分布对比左边是原始特征直方图右边是归一化后的结果。训练阶段加载数据的标准写法如下from sklearn.preprocessing import MinMaxScaler import joblib scaler MinMaxScaler() X_train scaler.fit_transform(X_train) # 只在训练集上拟合 min 和 max X_test scaler.transform(X_test) # 测试集复用同一组参数 joblib.dump(scaler, scaler.pkl)关键在fit_transform和transform的差别训练集 fit 完成后最小值和最大值已经被记录下来测试集只做缩放、不重新计算。如果对测试集单独执行 fit测试集分布范围发生变化输入特征会整体偏移最终评估指标失真。另一个容易踩的坑是划分顺序必须先切分训练集和测试集再对训练集做 fit否则缩放器已经看到测试集信息相当于测试集信息泄漏到训练过程里。预处理最后一步是把 DataFrame 转成 PyTorch 张量常见做法是torch.FloatTensor(df.values.astype(float))标签则用torch.LongTensor保存因为CrossEntropyLoss要求标签是整型索引。预测阶段的单条样本也要走同一套编码和缩放流程这一点在后面第 5 章还会再强调。3. Conv1d 网络搭建与 Train.py 训练收敛控制3.1 为什么用 Conv1d 而不是把特征 reshape 成二维图图像分类默认用 Conv2d但如果把 NSL-KDD 的 41 维特征强行 reshape 成 5×8 或 7×6 的小矩阵再卷积效果往往不如 Conv1d。原因在于 41 维特征的物理含义没有二维空间上的领接关系强行排列后横向和纵向的“相邻位置”不代表语义相关卷积核会把不相关的特征组合到一起。一维卷积只沿着特征维度滑动每次覆盖连续 3 或 5 个特征恰好可以把 src_bytes、dst_bytes 这类语义相近的连续特征组合起来也保留了特征的顺序信息。从计算角度看Conv1d 参数量比同宽度的 Conv2d 更小。以in_channels1, out_channels64, kernel_size3为例一维卷积参数量只有 1×64×3 192训练和推理都很快CPU 上几十轮 epoch 完全可接受。项目里的 CNNMould.py 正是按照这个思路设计的输入张量形状是[batch_size, 1, 41]其中 1 表示通道数41 是特征长度经过两个卷积块后再由全连接层输出类别概率。3.2 CNNMould.py 的卷积层参数与维度推演模型核心代码整理如下class CNN(nn.Module): def __init__(self, feature_dim41, num_classes2): super().__init__() self.conv_block1 nn.Sequential( nn.Conv1d(1, 64, kernel_size3, padding1), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(kernel_size2) ) self.conv_block2 nn.Sequential( nn.Conv1d(64, 128, kernel_size3, padding1), nn.BatchNorm1d(128), nn.ReLU(), nn.MaxPool1d(kernel_size2) ) self.fc nn.Sequential( nn.Flatten(), nn.Linear(128 * (feature_dim // 4), 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x self.conv_block1(x) # [B, 1, 41] - [B, 64, 20] x self.conv_block2(x) # [B, 64, 20] - [B, 128, 10] x self.fc(x) # [B, 10] - [B, num_classes] return xin_channels1表示把一条连接的 41 维特征当作单通道信号out_channels64是第一层卷积可提取的特征模式数量越大表达能力越强但参数量和过拟合风险同步上升NSL-KDD 这类中等体量数据集 64 已足够。kernel_size3决定卷积窗口覆盖的连续特征数41 维序列不长用 5 或 7 会过早压缩长度3 是平衡感受野和参数量的常用选择。padding1保证卷积后长度不变MaxPool1d(2)把 41 压到 20第二个卷积块再压到 10。全连接层输入维度来自128 * (feature_dim // 4)41 整除 4 后是 10所以实际展开为 128×10 1280 个节点。修改 feature_dim 或卷积参数时这个乘积必须同步调整否则加载 best_model.pth 时会出现维度不匹配。Dropout(0.5) 只放在全连接层中间训练时随机丢弃一半神经元作用是缓解全连接层过拟合卷积层本身参数少不需要额外加 Dropout。3.3 Train.py 训练循环与 best_model 保存逻辑训练脚本的核心流程是加载每批数据、前向传播、计算损失、反向传播、更新参数、定期在验证集上评估并保存验证准确率最高的权重。device torch.device(cuda if torch.cuda.is_available() else cpu) model CNN(feature_dim41, num_classes2).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) best_acc 0.0 EPOCHS 30 for epoch in range(EPOCHS): model.train() total_loss 0.0 for x_batch, y_batch in train_loader: x_batch x_batch.unsqueeze(1).to(device) # [B, 41] - [B, 1, 41] y_batch y_batch.to(device) optimizer.zero_grad() outputs model(x_batch) loss criterion(outputs, y_batch) loss.backward() optimizer.step() total_loss loss.item() * x_batch.size(0) val_acc evaluate(model, val_loader, device) print(fEpoch {epoch1:02d} | Loss {total_loss / len(train_loader.dataset):.4f} | Val Acc {val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth)unsqueeze(1)把[B, 41]变成[B, 1, 41]为 Conv1d 补上通道维。Adam 初始学习率 0.001 是小规模分类任务里比较稳定的起点不需要额外配置动量收敛也比 SGD 更快。损失函数用CrossEntropyLoss它内部已经做了 softmax所以模型最后一层输出不需要再手动加 softmax 激活。保存模型时只保存state_dict()文件不保存完整对象这样生成的 best_model.pth 更小预测阶段重建模型结构后直接加载权重即可。4. 准确率、精确率与召回率从评估公式到 Predict.py 落地4.1 准确率、精确率、召回率的计算公式与权衡分类评估不能只看准确率。设 TP 为攻击样本被正确识别为攻击的数量FN 为攻击样本被漏报的数量FP 为正常样本被误报为攻击的数量TN 为正常样本正确通过的数量则Accuracy (TP TN) / (TP TN FP FN)Precision TP / (TP FP)Recall TP / (TP FN)F1 2 × Precision × Recall / (Precision Recall)在入侵检测场景里FP 高意味着大量正常请求被拦截系统可用性下降FN 高则意味着攻击已经发生却没有产生告警。只看 Accuracy 容易被类别不平衡掩盖如果测试集中攻击样本占比较高模型即使把少数类别分错准确率仍然可能很好看。因此要把精确率和召回率放在一起看再结合 F1 做综合判断。README 中的 accuracy.jpg 是训练和验证阶段准确率变化曲线precision.jpg 是验证集精确率的曲线两者趋势是否同步能反映模型有没有过度偏向某一个类别。4.2 从 accuracy.jpg 和 Mould Acc.png 判断模型状态项目图片里有accuracy.jpg、precision.jpg、Mould Acc.png三类文件。accuracy.jpg展示准确率随 epoch 的变化通常两条曲线同步上升、逐渐趋平如果训练集准确率接近 1 而验证集准确率停滞甚至下降说明过拟合如果精确率和准确率曲线差异较大说明模型在正常样本和攻击样本上的误判比例不均衡。Mould Acc.png对应模型训练完成后最终准确率的记录图可用于和日志输出做一致性比对。训练日志也有规律可循二分类交叉熵初始状态下 loss 约等于 ln(2) 0.693。如果第一轮 loss 远高于这个值通常意味着标签映射或输入数据有问题如果 loss 能降到 0.3 以下验证准确率在 0.90 附近说明模型已基本收敛。这里不建议刻意追求过高的准确率NSL-KDD 本身存在特征相似导致的误分类重点是把评估逻辑做完整而不是把数字刷到接近 1。4.3 评估脚本在测试集上复现指标Predict.py 中经常需要计算完整评估指标下面是不依赖额外库的评估函数def evaluate(model, data_loader, device): model.eval() correct, total 0, 0 tp fp fn 0 with torch.no_grad(): for x_batch, y_batch in data_loader: x x_batch.unsqueeze(1).to(device) y y_batch.to(device) logits model(x) preds logits.argmax(dim1) total y.size(0) correct (preds y).sum().item() tp ((preds 1) (y 1)).sum().item() fp ((preds 1) (y 0)).sum().item() fn ((preds 0) (y 1)).sum().item() accuracy correct / total precision tp / (tp fp) if tp fp 0 else 0.0 recall tp / (tp fn) if tp fn 0 else 0.0 f1 2 * precision * recall / (precision recall) if precision recall 0 else 0.0 return {acc: accuracy, precision: precision, recall: recall, f1: f1}评估时不能省略torch.no_grad()否则 PyTorch 会为每个中间张量构建计算图内存占用翻倍且没有必要。model.eval()的作用是切换 BatchNorm 层的状态训练时 BN 使用当前批的均值和方差评估时使用训练阶段统计的全局值如果不切换验证集每批统计量都在变结果就不稳定。argmax(dim1)返回每个样本在类别维度上概率最大的索引preds y是逐元素比较直接得到正确预测数量。5. best_model.pth 的单样本预测与复现检查5.1 只保存权重时加载的前置条件best_model.pth 是用torch.save(model.state_dict(), best_model.pth)保存的所以加载时不能直接torch.load当模型用必须先实例化一个结构完全相同的 CNN 对象再调load_state_dict。model CNN(feature_dim41, num_classes2) state torch.load(best_model.pth, map_locationcpu) model.load_state_dict(state) model.eval()如果加载时报Missing key(s)或Unexpected key(s)说明当前模型类定义和保存权重时的层名不一致比如训练时用了 DataParallel模型权重会带module.前缀需要在 load 前去掉前缀或者加载时直接打印 state dict 的 key 排查。map_locationcpu把权重映射到 CPU避免在一台没有 CUDA 的机器上出现设备错误。实际预测时还要先加载预处理阶段保存的 scaler.pkl用同一组归一化参数处理输入数据。5.2 对单条流量记录做预测的完整函数把上面的步骤合并为一个预测函数def predict_one(model, scaler, raw_record, attack_map_reverse): df preprocess_raw_record(raw_record) # 与训练时相同的符号编码逻辑 x scaler.transform(df.values.reshape(1, -1)) x torch.FloatTensor(x).unsqueeze(0).unsqueeze(0) model.eval() with torch.no_grad(): logits model(x) idx logits.argmax(dim1).item() return attack_map_reverse[idx]preprocess_raw_record内部必须复用 PreHandle.py 里的 label 编码规则protocol_type、service、flag 三个字段的编码顺序要和训练时完全一致scaler.transform只做缩放、不重新 fitunsqueeze(0)加 batch 维unsqueeze(0)再加通道维最终输入形状是[1, 1, 41]。模型输出 logits 后取 argmax得到类别索引再通过attack_map_reverse把索引映射回可读的名称比如“normal”或“attack”。5.3 三个容易踩的坑第一归一化参数必须是训练集统计出来的。预测阶段如果对单条样本单独执行 fit_transform该样本自身会变成这组数据里的最小值和最大值输入分布整体偏移模型输出完全不可信。正确做法是加载保存的 scaler.pkl只调用 transform。第二随机种子不固定会导致结果无法复现。项目作为参考资料时建议在 Train.py 开头固定torch.manual_seed(0)和np.random.seed(0)DataLoader 的 shuffle 也加上generator这样多次训练得到的最优模型和指标趋于一致便于和 README 中的记录互相对照。第三加载 best_model.pth 后必须调用model.eval()。如果漏掉这一行BatchNorm 层会使用初始状态预测结果和训练时完全不同。这个问题只在带 BN 层的模型中出现而项目中每个卷积块都加了 BatchNorm1d所以eval()这一步不能省。把模型结构、标签映射和归一化参数三者对齐best_model.pth 才真正具备可复现性。本文还有配套的精品资源点击获取