ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于CNN的NSL-KDD网络入侵检测实战指南

基于CNN的NSL-KDD网络入侵检测实战指南 简介本资源为基于Python与CNN卷积神经网络的网络入侵检测算法完整项目面向计算机相关专业正在开展毕业设计、课程设计或期末大作业的学生也适合希望进行项目实战练习的学习者。项目经导师指导并评审通过评分98分具备较强的教学参考与复现价值。压缩包共33个文件约21.59MB核心包含4个Python源码文件涵盖数据预处理、模型构建、训练与预测流程、12个csv格式的数据集文件、模型权重文件pth以及项目说明文档、训练与评估图表等目录结构完整可帮助读者快速理解从NSL-KDD数据加载、特征处理到模型训练、精度评估的完整流程。已有191人学习或下载适合作为网络入侵检测方向入门与进阶的实践参考。1. 网络入侵检测为什么要用CNNNSL-KDD和卷积的适配逻辑做基于pythonCNN的网络入侵检测第一步不是调参而是先想清楚数据集和模型为什么是这么配的。NSL-KDD是KDD Cup 99的改进版去掉了冗余重复记录保留了41维特征加一个标签训练集和测试集划分固定是毕设和课程设计里最稳的基准数据集。而CNN在图像上成名把一维特征序列搬到CNN上做入侵检测反而是个被反复验证的路子——把每条连接记录当作长度为41的信号用一维卷积提取局部特征再通过全连接层分类。这份源码正好覆盖了从CSV原始数据到训练、预测的完整链路适合正在做毕设或者想快速跑通CNN入侵检测流程的人。2. 数据预处理把NSL-KDD的41维特征变成CNN能吃的张量NSL-KDD的原始CSV里41维特征不是都能直接丢进CNN的。protocol_type、service、flag这三列是符号型Attack Type列有几十种攻击名要先做两件事符号特征数值化、攻击标签粗粒度化。预处理脚本在PreHandle.py里输入是KDDTrain.csv这类原始文件输出是模型能直接读的数值特征文件。DataSet Change目录里一般放的是转换后的中间文件对应社区常见的改动版NSL-KDD如果你下载的是原始KDD数据集直接跑PreHandle.py生成即可。2.1 符号特征数值化LabelEncoder和手动映射的差别NSL-KDD的41维特征里有三列符号特征protocol_typetcp/udp/icmp3种取值、servicehttp/ftp/smtp等约70种取值、flagSF/REJ/S0等11种取值。CNN不能直接吃字符串标准做法是用LabelEncoder把它们编码成整数。import pandas as pd from sklearn.preprocessing import LabelEncoder df pd.read_csv(KDDTrain.csv, headerNone) symbol_cols [1, 2, 3] # protocol_type, service, flag for col in symbol_cols: le LabelEncoder() df[col] le.fit_transform(df[col])逻辑说明sklearn的LabelEncoder把字符串转成0到n-1的整数tcp/udp/icmp会被映射成0/1/2service按字母序编码。这里有个后面会踩的坑预测阶段出现训练时没见过的service字符串直接transform会报错所以要在预处理阶段就固定全局映射或者把encoder对象保存下来。参数说明headerNone是因为NSL-KDD原始csv没有列名行列索引从0开始[1,2,3]正好对应41维特征的前三列。如果用的数据集带表头要改成header0并按字段名取列。符号列取值数量会影响后续编码结果这个项目直接用整数编码没有做embedding对网络结构是个简化效果也足够。2.2 攻击标签映射从几十种攻击名到五分类NSL-KDD标签列由正常normal和几十种攻击名组成常见做法是映射成5大类Normal、DoS、Probe、R2L、U2R。DoS包含neptune、smurf、pod等拒绝服务攻击Probe包含portscan、satan等探测行为R2L是远程到本地的未授权访问U2R是本地提权。训练集里这五类的样本量差异极大理解这个分布后面评估时才不会只看整体accuracy。类别含义训练集样本量约Normal正常连接67343DoS拒绝服务攻击45927Probe端口扫描与漏洞探测11656R2L远程未授权访问995U2R本地提权52attack_map { normal: Normal, back: DoS, land: DoS, neptune: DoS, pod: DoS, smurf: DoS, teardrop: DoS, ipsweep: Probe, nmap: Probe, portscan: Probe, satan: Probe, ftp_write: R2L, guess_passwd: R2L, imap: R2L, multihop: R2L, phf: R2L, spy: R2L, warezclient: R2L, warezmaster: R2L, buffer_overflow: U2R, loadmodule: U2R, perl: U2R, rootkit: U2R } df[41] df[41].str.strip().str.lower().map(attack_map).fillna(Unknown)逻辑说明先统一转小写去空格再通过字典映射到五大类。KDDTrain里的攻击类型基本都能映射但KDDTest里包含训练时没出现过的攻击变种映射不到会被fillna成Unknown。跑评估的时候Unknown样本建议单独统计不要在训练阶段统一丢弃否则测试集分布被改得不真实指标也失去说服力。参数说明标签列是41列索引。map是pandas的字典映射速度比apply加if-else快12万行数据差异尤其明显。如果你做的是二分类入侵检测把Normal以外的四类全部映射成Attack就行五分类和二分类的多分类头、评估指标展示逻辑都要跟着改。2.3 特征分组的意识理解41维特征再谈CNN建模41维特征大致分四组基础连接特征1到9列duration、protocol_type、service、src_bytes、dst_bytes等、内容特征10到22列logged_in、num_compromised等、基于时间的流量特征23到31列count、srv_count等、基于主机的流量特征32到41列dst_host_count等。CNN把整条记录当一维序列来扫卷积核会把相邻列组合出局部模式比如src_bytes和dst_bytes的差值、count和srv_count的比例这些是传统规则检测里专家会关注的高频组合。做完这步再回头看归一化就会有感觉内容特征大部分是0/1标志位基础特征跨度从1到几十万字节不归一化的话一个duration10000的样本会把卷积层的梯度方向带偏。minMax.png这个图展示的就是归一化前后的特征分布变化答辩时放这一张图就能解释清楚为什么CNN需要预处理也顺便证明预处理步骤是成体系的不是随便拼的。2.4 归一化和数据集划分的顺序不能乱41维特征里duration、src_bytes这类数值跨度极大不归一化的话CNN的梯度更新会被大数值列带偏。常见做法是逐特征列做min-max把值压到[0,1]。import numpy as np from sklearn.preprocessing import MinMaxScaler from sklearn.model_selection import train_test_split feature_cols list(range(0, 41)) scaler MinMaxScaler() X scaler.fit_transform(df[feature_cols].values.astype(np.float32)) y df[41].values X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, stratifyy, random_state42 )逻辑说明MinMaxScaler逐列计算min和max把每个特征压到[0,1]。stratify让切分后每一类的比例和全量一致避免随机切分把占比本来就小的U2R全部切到验证集里。random_state固定切分结果实验对比时才不会因为数据集不同引入额外变量。但这段代码里有个顺序问题必须警惕如果先对整个X做fit_transform再切分验证集的min和max信息已经渗进训练过程这就是特征泄漏。正确顺序是先切分再对X_train做fit_transform对X_val和测试集只做transformX_train, X_val, y_train, y_val train_test_split( df[feature_cols].values.astype(np.float32), df[41].values, test_size0.2, stratifydf[41].values, random_state42 ) scaler MinMaxScaler() X_train_scaled scaler.fit_transform(X_train) X_val_scaled scaler.transform(X_val)参数说明注意第二个写法里scaler只对训练集fit验证集用同一套min和max做transform测试集同样只用transform不许再fit。如果项目提供测试集专用的归一化文件就直接用没有的话用训练集保存下来的scaler参数。这份源码包里的PreHandle.py对这个顺序是处理过的但如果你改成自己的数据集这条顺序仍然是最容易翻车的地方。提示归一化必须遵循「先切分、再fit训练集、transform其他集」的顺序这条检查清单能挡住大半精度虚高问题。3. CNNMould.py拆解一维卷积结构、通道数与Dropout怎么定CNNMould.py是模型定义文件也是这个项目里最值得一行行读的部分。很多毕设代码把模型和训练写在一个文件里这个项目拆开了CNNMould.py只负责网络结构Train.py负责训练循环Predict.py负责加载模型做预测。这种拆分的好处是结构清晰调试时单独验证前向传播的输出维度很方便。下载包里还有Mould Acc.png一般是模型结构和精度变化的对照图答辩前整理思路用得上。3.1 输入张量怎么从41维向量变成卷积能处理的形状PyTorch的Conv1d期望输入是(batch, channels, length)。原始一条记录是(41,)的向量需要先unsqueeze成(batch, 1, 41)再进卷积层。import torch import torch.nn as nn class CNNMould(nn.Module): def __init__(self, input_dim41, num_classes5): super(CNNMould, self).__init__() self.conv1 nn.Sequential( nn.Conv1d(1, 64, kernel_size3, padding1), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(kernel_size2, stride2) ) self.conv2 nn.Sequential( nn.Conv1d(64, 128, kernel_size3, padding1), nn.BatchNorm1d(128), nn.ReLU(), nn.MaxPool1d(kernel_size2, stride2) ) self.dropout nn.Dropout(0.5) self.fc nn.Linear(128 * 10, num_classes) def forward(self, x): x x.unsqueeze(1) # (batch, 1, 41) x self.conv1(x) # (batch, 64, 20) x self.conv2(x) # (batch, 128, 10) x x.view(x.size(0), -1) # (batch, 1280) x self.dropout(x) x self.fc(x) # (batch, num_classes) return x逻辑说明第一条Conv1d把通道数从1扩到64kernel_size3配合padding1保持长度不变MaxPool1d(2)把41压到20第二条卷积把64通道扩到128池化后长度变10。view把三维特征图展平成128*101280维向量再进全连接层输出5分类logits。参数说明kernel_size3在一维信号上是稳的选择卷积窗口覆盖3个相邻特征比如src_bytes、dst_bytes、协议类型这三个列会被组合成一个局部模式。MaxPool1d的核大小2和步长2把长度减半连续两层后41变成了10相当于把原本离散的特征列压缩成高层抽象。如果你的数据集不是41维而是别的维度比如以后换成CICIDS2017的80多维特征要改两个点一是unsqueeze(1)前的长度不再是41二是fc层的输入维度要跟着第二次池化后的length重新算。常见做法是拿一批假数据跑一遍forward在每一层后print(shape)而不是手推公式省时间也更不容易错。3.2 BatchNorm和Dropout在这里各自的职责NSL-KDD的41维特征即使做了min-max归一化深层网络中间层的激活值分布还是会漂移。BatchNorm按batch维度把激活值拉回均值0方差1附近训练过程会稳很多。Dropout(0.5)在前向时随机屏蔽一半神经元给网络加了正则约束。我实际对比过去掉BatchNorm只用Dropout训练loss降得很快但验证精度上不去典型的过拟合。两个都保留的时候训练精度和验证精度的gap能控制住。Dropout的比率也可以调0.5是默认值如果训练数据只有一两万条可以提到0.6数据量充足时0.3到0.4反而保留更多信息。BatchNorm还有一个容易忽略的行为训练模式下它按batch统计均值和方差eval模式下用训练阶段累计的全局统计量。所以Predict.py里加载模型后必须调model.eval()否则推理结果会和你训练时看到的验证精度对不上这种差距在单条样本预测时尤其明显。3.3 选型对照为什么CNN在这里比LSTM和Transformer更合适网络入侵检测的输入特征是物理含义明确的字段集合不像文本有强时序依赖。CNN在一维特征上的优势是局部感受野自动组合相邻特征而LSTM把41维顺序当作时序来处理但NSL-KDD的特征顺序本身没有严格时间先后含义比如第5列是src_bytes、第6列是dst_bytes换一种特征排列顺序LSTM的结果就会明显变化这种不稳定性放在毕设里是硬伤。Transformer在这个数据量下也吃亏。NSL-KDD训练集约12万条对注意力机制来说样本偏少容易过拟合要发挥多头注意力的优势往往需要更大规模数据和预训练。所以在固定特征表的入侵检测场景里CNN是性价比最高的选择代码量小、训练快、答辩解释起来直观。CNN在推理阶段的耗时也远低于LSTM单条样本在CPU上毫秒级出结果这对做成实时检测工具很关键。4. Train.py训练全流程损失曲线、精度曲线与最优模型保存Train.py是项目的发动机读预处理后的数据实例化CNNMould跑若干轮epoch把验证集上精度最高的权重存成best_model.pth。文件里值得注意的不只是训练循环本身还有模型保存策略和评估指标的可视化输出。accuracy.jpg和precision.jpg是训练结束后绘制的曲线图Mould Acc.png是模型结构图这些图答辩时直接放进PPT里就是有效证据。4.1 训练主循环CrossEntropyLoss、Adam与epoch设置分类任务的标准组合是交叉熵损失加Adam优化器。训练循环按batch取数、清梯度、前向、算损失、反向传播、更新参数。import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset from CNNMould import CNNMould def train_epoch(model, loader, criterion, optimizer, device): model.train() running_loss 0.0 for xb, yb in loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() out model(xb) loss criterion(out, yb) loss.backward() optimizer.step() running_loss loss.item() * xb.size(0) return running_loss / len(loader.dataset) def evaluate(model, loader, device): model.eval() correct, total 0, 0 with torch.no_grad(): for xb, yb in loader: xb, yb xb.to(device), yb.to(device) out model(xb) pred torch.argmax(out, dim1) correct (pred yb).sum().item() total yb.size(0) return correct / total criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr0.001)逻辑说明训练循环里最容易被忽略的是optimizer.zero_grad()必须在backward之前调用否则梯度会在batch间累积loss曲线周期性抖动。交叉熵内部自带softmax模型输出层不需要额外接Softmax。evaluate函数里model.eval()和torch.no_grad()缺一不可前者切换BatchNorm到推理模式后者关闭梯度计算省显存。参数说明lr0.001是Adam常用学习率如果loss震荡不收敛优先降到0.0005。batch_size选64或128NSL-KDD训练集约12万条时64的batch一个epoch约2000次迭代收敛速度和显存占用比较平衡。epoch建议30到50轮盯着验证精度不提升就早停不要盲目跑满。4.2 训练过程可视化与accuracy、precision图表的判读方法项目里的accuracy.jpg、precision.jpg是用matplotlib绘制的曲线图。训练时每完成一个epoch记录一次train_acc和val_acc结束后画两条曲线。判读曲线时重点不是看曲线多平滑而是看两条曲线的gap和趋势。如果val_acc曲线贴着train_acc走说明模型没有过拟合如果train_acc冲到99%而val_acc停在80%以下先怀疑数据泄漏再怀疑网络过深。precision曲线比accuracy更能体现分类质量因为Normal样本占比过半模型全预测Normal都能拿50%以上accuracy但per-class precision会立刻暴露这个病。import matplotlib.pyplot as plt plt.figure(figsize(8, 5)) plt.plot(range(1, len(train_acc_list) 1), train_acc_list, labeltrain acc) plt.plot(range(1, len(val_acc_list) 1), val_acc_list, labelval acc) plt.xlabel(epoch) plt.ylabel(accuracy) plt.legend() plt.savefig(accuracy.jpg, dpi300)逻辑说明train_acc_list和val_acc_list在训练循环里每个epoch追加一次当前精度训练结束后一次性绘图。横坐标用range(1, len1)让曲线从第1个epoch开始而不是从0开始。savefig的dpi300是给答辩PPT用的清晰度直接插进论文也不糊。参数说明figsize控制画布尺寸8x5英寸是常规比例。两条曲线画在同一张图上是为了对比train和val的gap如果想单独看每个类别的precision可以每类画一条线也可以直接打印classification_report后者信息更全。训练结束后额外打印一份classification_report很值得from sklearn.metrics import classification_report y_true, y_pred [], [] model.eval() with torch.no_grad(): for xb, yb in val_loader: out model(xb.to(device)) y_pred.extend(torch.argmax(out, dim1).cpu().numpy()) y_true.extend(yb.cpu().numpy()) print(classification_report(y_true, y_pred, digits4))逻辑说明classification_report输出每个类的precision、recall、f1-score和样本数。如果DoS的f1是0.99而U2R的f1是0.3问题不在总体精度而是少数类样本不足或特征区分度不够。这个表比loss曲线更早暴露模型短板我一般每轮训练完都顺手打一次。4.3 best_model.pth的保存与加载约定文件里的best_model.pth保存的是验证精度最高的权重。训练过程中每个epoch结束都跑一遍验证集当前精度超过历史最佳就覆盖保存。best_val_acc 0.0 epochs 50 for epoch in range(epochs): train_loss train_epoch(model, train_loader, criterion, optimizer, device) val_acc evaluate(model, val_loader, device) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_model.pth) print(fepoch {epoch} save best model, val_acc{val_acc:.4f})逻辑说明只保存state_dict而不保存整个model对象是因为state_dict体积小、不受PyTorch版本变化影响。加载时先实例化CNNMould再load_state_dict这个模式在Predict.py里同样使用。保存条件用严格大于而不是大于等于避免验证精度轻微波动时反复覆盖文件。训练过程建议每保存一次就在控制台打印epoch和val_acc跑完能清楚看到模型在第几轮收敛。后续复现实验时best_model.pth加上固定的random_state结果可以精确复现。5. 避坑排查预测全同类、特征泄漏与设备不匹配的五个真实现场这一章写我在复现和调试CNN入侵检测项目时踩过的坑每一条都是实际出现过的不是理论推导。5.1 预测脚本跑出来全是Normal类现象训练时精度很高但Predict.py对任意新样本都输出Normal换了输入也一样。原因两个常见来源。一是标签映射在训练和预测时不统一预测脚本里把攻击类别全归到unknownunknown又被当成Normal处理二是归一化参数不匹配预测时用了另一个scaler或者对全量数据重新fit了一次输入分布偏移后CNN输出偏向样本量最大的类别。解决检查Predict.py加载的是不是训练阶段保存的同一份scaler。规范做法是把scaler和LabelEncoder对象用joblib持久化import joblib joblib.dump(scaler, scaler.pkl) joblib.dump(label_encoder, label_encoder.pkl) # 预测时重新加载 scaler joblib.load(scaler.pkl) label_encoder joblib.load(label_encoder.pkl)逻辑说明joblib保存的scaler对象里含每个特征列的min和max预测时用同一份参数transform输入分布才能和训练一致。这也顺便说明为什么预处理脚本里要单独拆出保存逻辑而不是在训练脚本里顺手归一化。5.2 训练集精度99%测试集精度掉到70%怀疑特征泄漏现象Train.py训练集accuracy图上到99%换到KDDTest.csv上预测精度掉到70%出头。原因一方面NSL-KDD测试集包含训练集没见过的攻击变种这是数据集本身特点但更常见的是代码层面的特征泄漏——先对全量X做了fit_transform再切分验证集和测试集的信息提前进入训练。解决按第2章的顺序做两层检查。第一train_test_split在归一化之前执行第二scaler只对训练集调用fit验证集和测试集只调用transform。用pipeline把这两步锁死更稳from sklearn.pipeline import Pipeline from sklearn.preprocessing import MinMaxScaler preprocessor Pipeline([(scaler, MinMaxScaler())]) X_train_scaled preprocessor.fit_transform(X_train) X_val_scaled preprocessor.transform(X_val) X_test_scaled preprocessor.transform(X_test)参数说明Pipeline把预处理流程封装成单对象fit_transform和transform的区别在代码层面被强制区分从根上堵住泄漏路径。注意pipeline的transform不会重新计算min和max只复用训练阶段存下的参数。注意特征泄漏不像模型过拟合那么容易被发现它表现为测试集指标虚高但换新数据立刻崩。查归一化是最容易犯错的位置。5.3 GPU上训练好的模型在CPU上加载报错现象训练机有显卡Predict.py换到没有显卡的机器上运行torch.load时直接报错报错信息通常包含Attempting to deserialize object on a CUDA device。原因保存的state_dict里tensor是cuda类型CPU环境下无法直接反序列化。解决加载时指定map_locationcpu或在训练保存前把权重转到CPU再存。model CNNMould(41, 5) model.load_state_dict(torch.load(best_model.pth, map_locationtorch.device(cpu))) model.eval()参数说明map_locationcpu把所有CUDA张量映射到CPU。反向情况是CPU训练保存的模型在GPU上加载PyTorch默认能加载但要记得把模型和输入tensor都.to(cuda)。毕设答辩演示时经常在教室电脑的CPU环境跑所以这个坑很常见。5.4 LabelEncoder在预测时遇到训练集没见过的字符串现象预测脚本输入一条新记录service字段出现训练集70种取值之外的值LabelEncoder抛ValueError程序中断。原因sklearn的LabelEncoder对未知类别没有兜底机制transform时发现陌生值直接报错。解决手动把类别字符串转成字典映射未知值统一归到新下标service_map {v: i for i, v in enumerate(le.classes_)} def safe_encode(val): return service_map.get(val, len(service_map))逻辑说明先枚举训练集见过的所有取值预测时遇到新值不再中断而是落到额外的未知类别。这样至少保住整条预测流程不崩后续还可以单独看这批未知样本的分类结果判断是否真的属于新攻击模式。5.5 U2R和R2L的recall常年接近0现象classification_report里U2R的f1在0到0.1之间R2L也好不到哪去DoS和Normal都在0.99。原因NSL-KDD类别分布极不均衡U2R训练集只有几十条R2L不到一千条Normal和DoS都是几万条。CNN在交叉熵损失下优先拟合大类别小类别梯度被稀释。解决给损失函数加类别权重或对少数类过采样。最简单的方式是用样本占比的倒数作权重传进CrossEntropyLossfrom sklearn.utils.class_weight import compute_class_weight class_weights compute_class_weight(balanced, classesnp.unique(y_train), yy_train) criterion nn.CrossEntropyLoss( weighttorch.FloatTensor(class_weights).to(device))参数说明balanced模式让大类的loss权重小、小类的loss权重大相当于放大了U2R和R2L的梯度贡献。加权重后整体accuracy会略微下降这是正常权衡答辩时把加权前后的两套指标都展示出来反而更有说服力。6. 把Predict.py改成检测工具单样本封装、批量验证与分类报告前面的章节把数据、模型、训练和坑都讲完了这一章落到Predict.py的改造上。下载包里的Predict.py大概率是读一条CSV记录再输出一个类别但实际用起来我们要的是一个能反复调用的函数输入一条41维特征输出类别和置信度。模型从训练代码里彻底解耦出来才算真正变成可用的检测工具。6.1 单样本预测函数的封装def predict_one(model, sample_41, scaler, label_map, device): arr np.array(sample_41, dtypenp.float32).reshape(1, -1) arr scaler.transform(arr) tensor torch.FloatTensor(arr).to(device) model.eval() with torch.no_grad(): logits model(tensor) prob torch.softmax(logits, dim1) pred_idx torch.argmax(prob, dim1).item() conf prob[0, pred_idx].item() return label_map[pred_idx], conf逻辑说明sample_41的每个位置必须和训练时的特征列顺序严格一致。scaler.transform用的是训练时保存的min和max如果新样本某一列值超出训练范围归一化后可能落在[0,1]区间外模型仍能推理但置信度会下降这个特性可以反过来用于异常样本初筛。返回的conf是softmax概率可以设阈值比如低于0.8就标记为低置信度待人工复核比直接相信单次输出可靠。6.2 批量验证改完预测逻辑必跑的一致性检查每改一次Predict.py的预处理链路就取测试集前100条跑一遍看输出类别分布。如果输出里95%都是Normal说明预处理链路大概率断了如果分布和训练集类别占比接近再跑全量测试集算指标。我现在的习惯是每改一次Predict.py强制跑一遍全量测试集用classification_report对比改动前后的per-class f1指标不低于原结果才继续往下做。这套验证流程跟了我好几个项目了。从那以后我每次做数据切分和归一化都会强制走一遍「切分在前、fit训练集、transform其他集」的检查清单模型权重保存也固定加map_locationcpu。希望帮到你。本文还有配套的精品资源点击获取
返回列表