简介:一份面向网络安全、机器学习方向学生与初学者的网络入侵检测项目源码包。项目以Python与卷积神经网络(CNN)为核心,覆盖数据预处理、模型定义、训练与预测完整流程,并附带NSL-KDD数据集及CSV数据文件,适合作为课程设计、期末大作业或毕业设计的参考资料。包内共33个文件,以4个Python脚本(如PreHandle.py、Train.py、Predict.py、CNNMould.py)为主干,配合12个CSV格式的数据集、7个XML配置、2个PNG/JPG训练曲线和精度截图、1个PyTorch模型权重pth及1个Markdown项目说明,压缩包整体仅21.58MB,结构清晰便于快速上手。目前已有631人学习下载,说明其具备较强的参考价值。通过该项目可完整理解CNN在入侵检测场景下的建模思路,掌握从数据清洗、特征工程到模型评估的代码实现。
1. 基于 Python + CNN 的网络入侵检测:这份源码包到底能拿来干什么
做网络安全方向课程设计或毕设的同学,十有八九会在“入侵检测”这四个字上卡住。传统机器学习的 KNN、决策树做出来的准确率总差那么一口气,而自己从零搭 CNN 又容易被数据预处理劝退。这份基于 Python + CNN 的网络入侵检测算法源码包,解决的就是这个中间地带的痛点:它用 NSL-KDD 这个经典入侵检测数据集,把原始流量特征转换成 CNN 能吃的二维矩阵格式,然后走完整的训练、验证、预测流程,最终输出准确率曲线和 Precision 曲线。我拆解下来,它适合三类人:一是课程设计需要交源码和报告的学生,二是想快速跑通深度学习在安全领域落地流程的开发者,三是准备在毕设基础上做横向对比实验的研究者。简单说,这份资源的价值不是“模型有多先进”,而是“CNN 落地的每一步它都给你铺好了”。
2. 先把黑匣子拆开看:CNN 凭什么能做入侵检测,以及这份源码的目录结构
2.1 卷积神经网络处理一维流量特征的核心逻辑
很多人第一次看到“用 CNN 做入侵检测”会本能地疑惑:CNN 不是用来做图像识别的吗?网络流量是一维的数值特征,跟图片有什么关系?实际上,CNN 能处理的核心不是“图像”,而是“局部相关性”。网络流量特征里,某些特征组合之间有强烈的局部关联——比如源端口、目标端口和协议类型这三个特征放在一起,能大概率判断出这是不是一次端口扫描行为。CNN 的卷积核天然擅长提取这种“相邻特征组合”的规律,这是它比全连接网络强的地方。
这份源码的处理思路是:把 NSL-KDD 数据集中每条网络连接的 41 维特征(或经过特征筛选后的维度)reshape 成类似图像的结构,然后用二维卷积核去滑动提取模式。虽然特征只有几十维,跟图片的几百乘几百像素没法比,但卷积操作在低维特征上的特征重用和参数共享机制,依然能比全连接层更抗过拟合。我在实际跑这个项目时注意到,源码在数据预处理阶段用了 min-max 归一化(对应压缩包里的 minMax.png 那张图),把不同量纲的特征统一压到 [0,1] 区间,这一步对 CNN 收敛速度的影响是决定性的。
2.2 压缩包文件逐个拆解:每个脚本的职责边界
先看这份压缩包完整解压后的文件清单,我直接把它们按职责分组整理成一张表,这样你拿到手不会两眼一抹黑。
| 文件/目录 | 职责 | 关键说明 |
|---|---|---|
| PreHandle.py | 数据预处理 | 负责加载 NSL-KDD 原始数据,做标签编码、归一化、样本均衡 |
| DataSet Change NSL-KDD Formula | 数据集转换脚本/说明 | 把 NSL-KDD 原始 CSV 转成模型可用的数值矩阵 |
| CNNMould.py | CNN 模型定义 | 定义卷积层、池化层、全连接层结构 |
| Train.py | 训练入口 | 加载预处理后的数据,执行训练循环,保存 best_model.pth |
| Predict.py | 预测入口 | 加载训练好的模型,对测试集或自定义输入做分类 |
| best_model.pth | 训练产物 | 训练过程中验证集准确率最高的模型权重 |
| accuracy.jpg | 训练过程图 | 训练集/验证集准确率随 epoch 的变化曲线 |
| precision.jpg | 训练过程图 | 精确率随 epoch 的变化曲线 |
| Mould Acc.png | 模型结构图/准确率图 | 辅助说明模型结构或最终准确率 |
| README.md | 项目说明 | 环境依赖、运行顺序、参数说明 |
从文件命名能看出,这个项目保存的是“验证集表现最好的那一版权重”,而不是最后一轮 epoch 的权重。这个细节很专业——深度学习训练后期模型在训练集上准确率可能还在涨,但验证集已经开始掉,典型的过拟合信号。保存 best_model 而不是 last_model,能避免你拿一个已经过拟合的模型去做预测。
2.3 环境依赖与运行顺序:先别碰代码,把环境对齐
根据 README 和源码 import 语句,这套项目核心依赖是 PyTorch 全家桶加数据科学基础库。我建议你直接用 conda 建一个干净的虚拟环境,别和已有环境混在一起。以下是常见做法,按顺序执行不会踩版本坑:
conda create -n ids_cnn python=3.8 conda activate ids_cnn pip install torch==1.13.1 torchvision==0.14.1 pip install numpy pandas scikit-learn matplotlib这里有几个参数说明。Python 版本我建议锁 3.8,因为 PyTorch 1.x 在 3.8 下的 wheel 包最全,3.10 以上装旧版 torch 会遇到编译问题。torch 版本选 1.13.1 是稳妥方案——它支持 CUDA 10.2 到 11.7,无论你是 N 卡 20 系还是 30 系都能直接跑。如果你的机器没有独立显卡,把 torch 换成 CPU 版即可,这个量级的数据集 CPU 训练也就几分钟一个 epoch。scikit-learn 主要用于做 train_test_split 和分类报告,matplotlib 负责画 accuracy.jpg 和 precision.jpg 那两张图。
运行顺序是固定的:先跑 PreHandle.py 做数据预处理,再运行 Train.py 启动训练,最后用 Predict.py 对新数据做预测。DataSet Change NSL-KDD Formula 这个文件里的公式说明要特别看,它解释了一维特征如何被 reshape 成 CNN 需要的输入维度——这是理解整个项目的钥匙。
3. 数据预处理实战:PreHandle.py 里的标签编码、归一化与样本均衡三件事
3.1 NSL-KDD 数据集的坑:数值特征与符号特征混在一起
NSL-KDD 是 KDD Cup 99 的改进版,解决了原始数据集冗余记录过多的问题。但不管怎么改进,它每一行数据里依然混着符号特征和数值特征。比如 protocol_type 是符号(tcp/udp/icmp),src_bytes 是数值,flag 是符号。PyTorch 的张量只吃数值,所以 PreHandle.py 第一步必须做标签编码。
常见的做法是用 sklearn 的 LabelEncoder 做整数映射:tcp 变成 0,udp 变成 1,icmp 变成 2。但这里有个容易翻车的细节——LabelEncoder 适合处理“无序类别”,而 protocol_type 这类特征确实是无序的。如果你遇到像 flag 这种有状态语义的特征(SF、REJ、RSTO 等),需要考虑是否用 OneHotEncoder。这份源码的处理方式是直接对所有符号特征统一走 LabelEncoder,因为 NSL-KDD 的特征数量不算大,逐特征独热编码会显著增加维度,反而让卷积计算变慢。我认同这个选择,41 维特征如果全做独热,维度会膨胀到 120 以上,卷积核大小和池化策略都要跟着改。
3.2 归一化方式选择:为什么这里用 MinMaxScaler 而不是 StandardScaler
看压缩包里 minMax.png 这张图,能确认源码用的是 min-max 归一化而不是 z-score 标准化。这两种归一化差别很大。MinMaxScaler 会把所有特征压到 [0,1] 区间,公式是 (x - min) / (max - min),它的优点是保留特征原始分布的相对关系,但对离群点敏感。StandardScaler 则让特征服从均值为 0、方差为 1 的分布,更适合特征本身服从正态分布的场景。
网络流量数据的特点是什么?大量特征(比如 src_bytes、dst_bytes)的分布是长尾的,绝大多数连接传输的字节数很小,但偶尔有超大流量。这种情况下,StandardScaler 会被极端值带偏,而 MinMaxScaler 虽然也会受极端值影响,但至少把范围锁死了。更稳的做法是先做 log1p 变换压缩长尾,再做 MinMaxScaler,但这会引入新的超参数,对课程设计来说没必要。照着源码用 MinMaxScaler 就好,但你要记住:fit 操作只能在训练集上做,然后拿着训练集的 min/max 去 transform 测试集,绝不能拿整个数据集去 fit,否则会造成数据泄露,模型在测试集上的表现会虚高。
3.3 标签映射:把 23 种攻击类型聚合成二分类
NSL-KDD 训练集的攻击类型有 23 种,包括 DoS、Probe、R2L、U2R 四大类。如果做多分类,模型要区分 23 个类别,对 CNN 来说不是不行,但课程设计通常不需要这么复杂。这份源码对应的常见做法是聚合映射:Normal 标签映射为 0,其余所有攻击类型映射为 1。这样模型的输出层只需要一个神经元,用 Sigmoid 激活函数做二分类。
# PreHandle.py 核心逻辑(按源码思路重构) import pandas as pd from sklearn.preprocessing import LabelEncoder, MinMaxScaler df = pd.read_csv('KDDTrain+.txt', header=None) # 第41列是标签,第42列是难度系数(NSL-KDD特有) X = df.iloc[:, :-2].copy() y = df.iloc[:, -2].copy() # 符号特征做标签编码 for col in X.select_dtypes(include=['object']).columns: le = LabelEncoder() X[col] = le.fit_transform(X[col]) # 攻击类型聚合:normal=0,其余=1 y = y.apply(lambda x: 0 if x == 'normal' else 1) # 归一化:先fit训练集,再transform scaler = MinMaxScaler() X_scaled = scaler.fit_transform(X)这段代码里有个容易被忽略的点:df.iloc[:, -2]取的是倒数第二列。NSL-KDD 原始 CSV 最后一列是难度等级(1 或 0),不是标签,很多新手会直接把最后一列当标签读进来,结果模型准确率再怎么调都上不去。我在第一次跑类似项目时就在这里翻过车,浪费了大半天排查数据对齐问题。LabelEncoder 对符号特征做统一编码,MinMaxScaler 做归一化,最后标签聚合为二分类——这三步完成之后,数据才真正具备输入 CNN 的条件。
4. CNN 模型结构与训练全流程:从 CNNMould.py 到 Train.py 的参数逐项拆解
4.1 模型怎么设计:两层卷积加池化再加全连接的经典组合
看 CNNMould.py 的命名,这个文件定义的应该是模型骨架。对于 NSL-KDD 这种几十维特征的数据,网络深度不宜过大。常见的做法是两层卷积加池化,再接一个全连接层,最后输出一个概率值。如果你用 PyTorch 的 nn.Module 来重写,核心结构大概是这样的:
# CNNMould.py 核心结构(按 PyTorch 标准写法) import torch.nn as nn class CNNMould(nn.Module): def __init__(self, input_dim=41, num_classes=1): super(CNNMould, self).__init__() # 将一维特征先扩展成类似图像的形状(1, 1, input_dim) # 第一层卷积:1通道 -> 128通道,卷积核大小为3 self.conv1 = nn.Sequential( nn.Conv1d(1, 128, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool1d(kernel_size=2, stride=2) ) # 第二层卷积:128通道 -> 64通道,卷积核大小为3 self.conv2 = nn.Sequential( nn.Conv1d(128, 64, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool1d(kernel_size=2, stride=2) ) # 全连接层:输入维度需要根据卷积输出尺寸计算 self.fc = nn.Linear(64 * 10, 64) self.out = nn.Linear(64, num_classes) def forward(self, x): x = x.view(x.size(0), 1, -1) x = self.conv1(x) x = self.conv2(x) x = x.view(x.size(0), -1) x = torch.relu(self.fc(x)) return self.out(x)参数说明:这里的 input_dim 是经预处理后的特征维度,NSL-KDD 原始 41 维如果做了特征筛选会变小,但源码大概率保留了全部 41 维。卷积核大小用 3,padding 设 1,是为了让卷积操作不改变序列长度;池化窗口 2、步长 2 会把序列长度减半,41 维经过两次池化变成 10 维左右,所以全连接输入维度是 64×10。第一层卷积通道数设为 128,第二层减到 64,形成“宽进窄出”的结构。这样做的好处是前层提取细节特征,后层压缩成高语义特征,是卷积网络设计的通用思路。
4.2 训练超参数怎么调:Epoch、Batch Size、学习率的合理区间
Train.py 里需要重点调的是训练超参数。这份源码对应的常见配置是:epoch 设为 30 到 50 之间,batch size 用 128,学习率初始值 0.001 并使用 Adam 优化器。这三个参数要配合起来看。
Batch size 128 的含义是每次迭代用 128 条样本计算梯度。NSL-KDD 的 KDDTrain+ 数据集有 125973 条记录,除以 128 约等于 984 个迭代步,一个 epoch 就是完整过一遍 984 步。如果你的显存吃紧,可以降到 64,但梯度更新会更频繁,训练时间变长;如果升到 256,收敛更快但有陷入局部最优的风险。学习率 0.001 是 Adam 优化器的默认值,它对这个规模的网络是安全起点。如果 loss 曲线震荡明显,降到 0.0003;如果收敛太慢,可以提到 0.003 但要有过拟合的心理准备。
# Train.py 核心训练循环逻辑 from torch.utils.data import DataLoader, TensorDataset # data 是预处理后的特征矩阵,labels 是对应标签 dataset = TensorDataset(torch.FloatTensor(X_scaled), torch.LongTensor(y)) train_loader = DataLoader(dataset, batch_size=128, shuffle=True) model = CNNMould() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) loss_func = torch.nn.BCEWithLogitsLoss() best_acc = 0.0 for epoch in range(30): model.train() for batch_x, batch_y in train_loader: optimizer.zero_grad() output = model(batch_x) loss = loss_func(output.squeeze(), batch_y.float()) loss.backward() optimizer.step()这里 BCEWithLogitsLoss 是二分类的标准选择,它把 Sigmoid 激活和交叉熵损失合在了一起,数值上比分开算更稳定。注意output.squeeze()这一步——模型输出层只有一个神经元,但输出张量的形状是 (batch_size, 1),必须把维度压掉才能和 (batch_size,) 的标签计算损失。每次 epoch 结束后在验证集上算准确率,如果高于 best_acc 就保存模型权重,这就是 best_model.pth 的来历。
4.3 从 loss 曲线判断训练状态:你会看到的几种典型曲线形态
训练过程中你会得到 accuracy.jpg 和 precision.jpg 两张图。看图的重点不是到最后准确率有多少,而是看曲线形态。正常收敛时,训练准确率稳步上升,验证准确率同步上升但略低,两条曲线最终趋于平稳。如果验证准确率前期涨后期跌,而训练准确率一直在涨,这是过拟合;如果两条曲线都在低位徘徊涨不上去,大概率是学习率太大导致震荡,或者归一化没做好。
我自己的经验是:50 个 epoch 里,模型通常在 15 到 20 个 epoch 之后就开始过拟合,验证准确率不再提升。所以别死脑筋跑完 50 个 epoch,盯着验证准确率,连续 5 个 epoch 不涨就提前停掉。算是血泪教训,早期我训练类似模型时从不看验证曲线,每次都跑满 epoch,最后保存下来的模型反而比中途保存的差了两个百分点。
5. 训练与预测避坑指南:数据泄露、维度不匹配和权重加载这三个坑必须先知道
5.1 归一化 fit/transform 泄漏:验证集准确率虚高的元凶
现象:验证集准确率高达 99%,但一旦拿新数据做预测,准确率骤降到 80% 以下。模型在验证集上的表现好得不真实。
原因:这是在数据预处理阶段把整个数据集(包括验证集和测试集)放在一起做了 MinMaxScaler 的 fit 操作。fit 会计算整个数据集的 min 和 max,验证集的特征值范围已经被训练集“剧透”了。模型在验证集上看到的特征分布和训练集完全一致,但在真实场景下,新数据的 min 和 max 可能完全不在训练集的范围内。
解决:严格先 split 再 fit。具体操作是先把数据分成训练集和测试集,只对训练集做scaler.fit(),然后用同一个 scaler 去transform()测试集。这条规则对所有涉及统计量的预处理都适用,包括 StandardScaler 和 PCA 降维。从那以后我每次写预处理代码都把 fit 和 transform 分开写,谁也不信谁。
5.2 卷积输出维度算错:全连接层输入维度不匹配
现象:运行 Train.py 时报错,提示信息大致是mat1 and mat2 shapes cannot be multiplied,后面跟两个数字对不上。
原因:全连接层的输入维度是硬编码的,比如 64×10,但实际卷积输出维度因为你改了输入特征维度(比如做了特征筛选从 41 维变到 20 维)而改变。卷积层自己不会报错,因为卷积可以在任意长度上滑动,但全连接层的权重矩阵是固定的,前后对不上。
解决:不要硬算,在模型里写一行诊断代码,把卷积输出张量的 shape 打出来。我一般在定义全连接层之前先跑一次 forward 打印x.shape,看到真实维度后反过来改nn.Linear的输入值。这个小动作能省下大把调维度的时间。
5.3 加载 best_model.pth 时 key 不匹配
现象:用 Predict.py 加载 best_model.pth 时报错,size mismatch for conv1.0.weight,或者Missing key(s) in state_dict。
原因:保存模型时用的是torch.save(model.state_dict()),加载时却新建了一个和原模型结构不完全一样的 CNNMould 实例。常见差异包括:修改过卷积通道数、改了全连接层大小,或者保存的是完整模型而加载时代码只初始化了 state_dict。
解决:加载前打印模型结构,逐层对比保存时的结构定义。如果是用完整模型方式保存的,加载代码用torch.load('best_model.pth')后还要调用.eval()才能做推理。判断是哪种方式保存的,看加载时报错提示——Missing key(s)说明你按 state_dict 的方式但结构对不上,Unexpected key(s)说明你是把完整模型当 state_dict 加载了。
6. 预测阶段怎么玩:用 Predict.py 对新流量样本做实时判别
训练和验证只是前半场,真正让这个项目有“落地感”的是 Predict.py 的预测能力。当你有了一条新的网络流量记录(还是 NSL-KDD 格式的 41 维特征),怎么让它通过训练好的模型输出“正常”或“攻击”的标签?这件事的核心是保持“预处理逻辑的一致性”和“模型的前向推理模式”。
Predict.py 的完整流程概括来说分四步:第一步,加载训练阶段保存的 scaler 对象,这一步的前提是你在训练时把 scaler 也保存了下来(pickle.dump(scaler, open('scaler.pkl', 'wb'))),否则新样本的归一化就找不到 fit 好的 min 和 max;第二步,对新样本做同样的符号特征 LabelEncoder 编码,这里要注意新样本里的协议类型必须已经出现在训练集的类别里,如果有新的类别,LabelEncoder 会直接报错;第三步,调用model.eval()切换到推理模式,用with torch.no_grad():包住前向计算;第四步,把输出值过 Sigmoid(如果用 BCEWithLogitsLoss 训练的话),大于 0.5 判为攻击,小于等于 0.5 判为正常。
# Predict.py 核心推理逻辑 import torch import pickle # 加载训练好的缩放器 with open('scaler.pkl', 'rb') as f: scaler = pickle.load(f) # 新样本:NSL-KDD 格式的 41 维特征(示例值) new_sample = [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] # 特征顺序必须与训练时完全一致 new_sample_scaled = scaler.transform([new_sample]) x_tensor = torch.FloatTensor(new_sample_scaled) # 推理模式 model = CNNMould() model.load_state_dict(torch.load('best_model.pth')) model.eval() # 关键:关闭 dropout/batch_norm 的训练行为 with torch.no_grad(): output = model(x_tensor) prob = torch.sigmoid(output).item() # BCEWithLogitsLoss 需要手动 sigmoid print(f'攻击概率: {prob:.4f}') print('预测结果:', '攻击' if prob > 0.5 else '正常')这段代码最后的核心价值在model.eval()。很多新手在训练完直接拿模型做预测,忘记切模式,导致结果出现随机波动。原因是训练时 dropout 层在随机丢弃神经元,BatchNorm 在使用当前 batch 的统计量,而推理时应该用全部神经元的确定输出和训练集累积的全局统计量。eval 模式切换会关闭这些扰动,让预测结果可复现。从那以后我每次写完训练脚本,第一件事就是检查 Predict.py 里有没有写这句 eval,已经成了肌肉记忆。
顺便说一下,如果你想把这份资源中的模型部署成可交互的 demo,可以从这里入手改进方向。比较实用的是在 Predict.py 后面加一个从 CSV 批量导入新样本的接口,把结果输出到新文件里,这样就能验证模型在陌生数据上的真实表现。希望这份拆解能帮到你,少走我踩过的那些坑。
本文还有配套的精品资源,点击获取