简介:基于Python与CNN的网络入侵检测算法源码及项目说明,面向计算机专业学生完成毕业设计、课程设计,以及网络安全爱好者开展深度学习方法实战。项目利用卷积神经网络自动提取网络流量特征,在NSL-KDD标准数据集上完成训练与评估,旨在解决传统入侵检测方法对复杂攻击识别率不高的问题。压缩包共33个文件,约21.58MB,包含Python训练、预测与预处理脚本,CSV数据集,XML工程配置,PNG/JPG可视化结果图,训练好的模型权重以及一份系统性的说明文档,完整覆盖数据清洗与归一化、模型结构设计、训练调参、测试评估和应用部署环节。借助清晰的模块划分和代码注释,读者可直接运行复现实验,也可替换数据集或调整网络结构进行二次开发。目前已有134人学习下载,适合需要高质量可运行项目作为毕设参考或安全研究方向入门的学习者。
1. 网络入侵检测为什么要用 Python + CNN 而不是规则引擎
把网络入侵检测当成一个图像分类问题来做,听起来绕,但这是深度学习方案里验证成本最低、最容易复现的一条技术路线。传统入侵检测系统靠规则库和签名匹配,面对加密流量、变种攻击和未知漏洞利用时,规则根本写不过来,误报率也压不住。CNN 的卷积核天然擅长捕捉输入矩阵里的局部相关性——换个角度说,网络会话的特征值在时间窗口里也存在类似图像纹理的结构,这正是卷积神经网络的用武之地。
这个标题下的项目,本质就是先把流量会话编码成数值矩阵,再交给一个不算深的 CNN 做多分类判别。适合正在做安全数据分析、或者准备从 sklearn 切到深度学习的工程人员。读完你至少能独立跑通「数据集预处理 — 模型训练 — 评估调参 — 导出部署」的完整闭环,之后换到自己的流量特征表上,改的只是数据装载和矩阵映射这一段。
2. 数据预处理:把 NSL-KDD 流量特征变成 CNN 的输入矩阵
CNN 的输入必须是固定尺寸的数值矩阵,而原始网络流量是长度不定的会话记录,这两者之间的转换是整条链路里最容易出问题的一步。实际项目里,这一步花的时间通常比调网络结构还要多,因为特征顺序、缩放参数、列对齐一旦不一致,后面模型再准都是白搭。
2.1 NSL-KDD 的字段结构与选型理由
学术和工程里做网络入侵检测的基准数据集,最常见的是 NSL-KDD。它是对 KDD Cup 99 的改进:去掉了大量冗余记录,使训练集和测试集的分布更合理,U2R、R2L 这类少数类样本不至于被淹没在重复数据里。NSL-KDD 每条样本包含 41 个特征,归属四组:
| 特征组 | 数量 | 典型字段 |
|---|---|---|
| 基础连接特征 | 9 | duration、protocol_type、service、flag、src_bytes 等 |
| 内容特征 | 13 | 登录失败次数、root 权限操作、文件创建次数等 |
| 流量特征(时间窗口) | 9 | 过去 2 秒内同主机的连接统计 |
| 流量特征(主机窗口) | 10 | 过去 100 条连接中同服务的统计 |
标签是五分类:Normal、DoS、Probe、R2L、U2R。训练集中 Normal 约 6.7 万条,DoS 约 4.6 万条,Probe 约 1.2 万条,R2L 只有 995 条,U2R 只有 52 条,这个极端不平衡会在第四章显著影响评估方式。
拿到这类项目时,第一件事是确认默认加载哪个数据集。入门阶段用 NSL-KDD 就够了,它的优点是免版权、可直接下载、特征列固定;如果业务要贴近真实流量,再迁移到 CICIDS2017,那需要自己用 cicflowmeter 从 pcap 里提取特征,成本高出不少。我一般先用 NSL-KDD 调通链路,再换自己的特征表。
2.2 特征归一化、One-Hot 编码与标签映射
NSL-KDD 的构成是 37 个数值列加 4 个类别列,建模前要做两件事:类别列转 One-Hot、数值列标准化。这里有个新手最容易踩的坑:标准化必须只用训练集的均值和标准差,测试集和线上数据要复用同一组参数,否则特征分布信息提前泄露,评估结果虚高。
import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler # 读取 NSL-KDD 官方文件,最后一列是类别标签 # 如果下载的副本末尾多了一列难度等级值,先把它去掉 df_train = pd.read_csv('KDDTrain+.txt', header=None) df_test = pd.read_csv('KDDTest+.txt', header=None) X_train, y_train = df_train.iloc[:, :-1], df_train.iloc[:, -1] X_test, y_test = df_test.iloc[:, :-1], df_test.iloc[:, -1] # 对三个类别列做 One-Hot:协议类型约 3 个取值,service 约 70 个取值 for col in [1, 2, 3]: X_train = pd.concat([X_train.drop(columns=[col]), pd.get_dummies(X_train[col], prefix=f'c{col}')], axis=1) X_test = pd.concat([X_test.drop(columns=[col]), pd.get_dummies(X_test[col], prefix=f'c{col}')], axis=1) # 训练/测试列对齐:测试集里可能缺少某些取值,reindex 补零 X_test = X_test.reindex(columns=X_train.columns, fill_value=0) # 数值列标准化,scaler 只在训练集上 fit numeric_cols = X_train.select_dtypes(include=[np.number]).columns scaler = StandardScaler().fit(X_train[numeric_cols]) X_train[numeric_cols] = scaler.transform(X_train[numeric_cols]) X_test[numeric_cols] = scaler.transform(X_test[numeric_cols])这段代码有两个必须注意的参数点。One-Hot 之后训练集和测试集的列数可能不一致,用reindex按训练集列顺序对齐并补零是最稳妥的兜底;StandardScaler只 fit 训练集,避免把测试集分布预先引入。跑完一条样本会从 41 维展开到 120 维左右,具体取决于 service 列的取值个数。
2.3 一维特征向量到二维矩阵的三种组织方式
CNN 卷积操作要求输入是二维网格结构,所以要把一维向量重排成矩阵。常见做法有三种:补零到 121 个值后 reshape 成 (11, 11);取前 120 个值拼成 (12, 10);或者按语义分组重排,比如把时间窗口统计、主机统计、内容特征分别放在矩阵的不同区块,让卷积核按语义块扫描。
我一般倾向 reshape 成 (11, 11)。理由是第一组方案和第三组方案的实际精度差异通常只有一两个点,但 (11, 11) 的短边更好算,池化后的尺寸干净。语义分组的做法在少数项目里能多带 2 到 3 个点的精度,代价是特征顺序的代码要单独维护。
提示:reshape 的物理意义在安全场景里没有严格规定,但保持稳定复现很关键。特征顺序一旦定下来就要写死,训练和推理必须走同一个映射函数,否则模型换环境后精度悄悄掉几个点还查不出来。
3. 用 PyTorch 写出可训练的 CNN 入侵检测模型
数据准备到矩阵这一步之后,模型本身反而简单。一个五分类的流量检测任务用不着 ResNet 那类深网络,两层卷积加一层全连接已经能在 NSL-KDD 上拿到接近上限的精度。这一章把网络定义、训练循环、收敛判断一次说清。
3.1 网络结构设计:两层卷积加全连接
输入是 (1, 11, 11),即单通道的 11x11 矩阵。第一层卷积用 32 个 3x3 卷积核,padding=1 保持宽高不变,池化到 5x5;第二层用 64 个 3x3 卷积核,池化到 2x2;最后压平接 128 维全连接和 5 类输出。两处 Dropout 和 ReLU 放在全连接之间。
import torch import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=5): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), # (32, 11, 11) nn.ReLU(inplace=True), nn.MaxPool2d(2), # (32, 5, 5) nn.Conv2d(32, 64, kernel_size=3, padding=1), # (64, 5, 5) nn.ReLU(inplace=True), nn.MaxPool2d(2), # (64, 2, 2) ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 2 * 2, 128), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))选 3x3 卷积核是因为它感受野最小、参数少,两层堆叠就能覆盖 5x5 的区域,对 11x11 的输入足够。padding=1 是为了让池化前的尺寸保持 11x11,否则两层卷积后矩阵缩到 7x7 甚至更小,全连接输入维度会跟着变。Dropout 放在全连接层而不是卷积层之间,因为卷积层参数共享、本身抗过拟合,真正容易过拟合的是最后那层 128 维。
3.2 最小可运行训练代码与关键参数
模型定义好之后,把上一章的 Tensor 接上 DataLoader 就能开训。下面的代码默认跑 CPU,单个 epoch 在两三百毫秒量级,30 轮十几分钟跑完。
from torch.utils.data import TensorDataset, DataLoader from torch.optim import AdamW # 预处理完的特征转 Tensor,reshape 成 (样本数, 1, 11, 11) X = torch.tensor(X_train.to_numpy(), dtype=torch.float32).reshape(-1, 1, 11, 11) y = torch.tensor(y_train.astype(int).to_numpy(), dtype=torch.long) dataset = TensorDataset(X, y) loader = DataLoader(dataset, batch_size=256, shuffle=True) model = SimpleCNN(num_classes=5) criterion = nn.CrossEntropyLoss() optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) for epoch in range(30): model.train() total_loss = 0.0 for xb, yb in loader: optimizer.zero_grad() loss = criterion(model(xb), yb) loss.backward() optimizer.step() total_loss += loss.item() * xb.size(0) print(f"epoch {epoch+1}/30, avg_loss={total_loss / len(dataset):.4f}")两个参数值得留意。weight_decay=1e-4是 L2 正则,在五万级样本上能明显压住全连接层的过拟合;lr=1e-3配合 AdamW 是这个规模任务的经验起点,调高到 1e-2 容易在前几个 epoch 震荡,调低到 1e-4 收敛太慢。要上 GPU 就在前面加一行device = 'cuda' if torch.cuda.is_available() else 'cpu',然后把模型和每个 batch 都.to(device)。
3.3 训练收敛判断与常见问题
训练不能只看 loss 数值,要对照一个预期曲线。正常情况前 5 个 epoch 损失会从 1.6 快速降到 0.3 以下,20 个 epoch 后稳定在 0.06 到 0.1 之间。如果 10 个 epoch 还在 0.5 以上,优先检查标准化是否生效,而不是调学习率。整个网络的参数量在五万级,具体分布如下:
| 层 | 输出尺寸 | 参数量 |
|---|---|---|
| Conv2d(1,32,k=3,p=1) | (32, 11, 11) | 320 |
| MaxPool2d(2) | (32, 5, 5) | 0 |
| Conv2d(32,64,k=3,p=1) | (64, 5, 5) | 18496 |
| MaxPool2d(2) | (64, 2, 2) | 0 |
| Linear(256,128) | (128,) | 32896 |
| Linear(128,5) | (5,) | 645 |
这个体量的模型在单卡 CPU 上也能承受频繁实验,是作为基线的理想选择。常见的训练期报错集中在 reshape 对不上:预处理时特征维度不是 120 或 121,或者 DataFrame 里混入了非数值列,都会在reshape(-1, 1, 11, 11)这一行炸出来。排查时先print(X.shape),确认列数等于 121 或 120(后者需要补一个常数列)。
4. 网络入侵检测模型的评估指标与决策阈值
训练完第一件事不是看 loss,而是看多分类报告。入侵检测的评估跟普通图像分类有个本质区别:误报和漏报的成本不对称,一个漏掉的入侵可能造成实打实的损失,而一次误报只是告警噪音。所以交替评估模型精度的权重,要高于单一准确率。
4.1 混淆矩阵与分类报告怎么读
from sklearn.metrics import classification_report, confusion_matrix model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for xb, yb in test_loader: logits = model(xb) all_preds.extend(logits.argmax(dim=1).tolist()) all_labels.extend(yb.tolist()) print(classification_report(all_labels, all_preds, target_names=['Normal', 'DoS', 'Probe', 'R2L', 'U2R']))输出的每一列都值得细看。典型结果里 Normal、DoS、Probe 的 F1 都在 0.98 以上,但 R2L 和 U2R 的召回会明显偏低,常见只有 0.3 到 0.6,大量少数类样本被误判成 Normal。原因直接指向训练集分布:R2L 只有 995 条,U2R 只有 52 条,卷积核根本没有机会学到这两类的判别模式。这时候不要急着说模型不行,先确认评估用的测试集分布,再决定是调采样还是调阈值。
4.2 决策阈值调整:从 argmax 到拒绝式预测
检测系统往往不是五选一,而是「尽量把所有攻击都找出来」。一种常见做法是引入拒绝选项:当模型对某个类别的最大概率低于阈值时,不做硬分类,而是标记为需要二次确认。
probs = torch.softmax(logits, dim=1) max_prob, pred = probs.max(dim=1) # 最大置信度低于阈值的样本标记为 -1,交给规则引擎或人工复核 pred[max_prob < 0.6] = -1阈值 0.6 是经验起点,实际要根据业务里误报的成本来调:调高阈值会漏掉低置信度攻击,调低会引入大量误报。我一般会在验证集上枚举 0.4、0.5、0.6、0.7 四档,分别统计检出率和误报率,再挑业务能接受的点。对 R2L 和 U2R,还可以单独降阈值到 0.3 左右,只在这两类上提高敏感度。
4.3 和随机森林、LightGBM 基线的横向对比
CNN 并不是唯一选项,也不是在所有指标上都赢。同一个预处理管道上跑一组 sklearn 基线,才能确认深度学习带来的增量值不值得部署成本。以下是一组 NSL-KDD 五分类的实测量级:
| 模型 | Accuracy | Weighted-F1 | 30 epoch 训练耗时 |
|---|---|---|---|
| 随机森林(500 棵树) | 0.986 | 0.985 | 约 1 分钟 |
| LightGBM(300 轮) | 0.989 | 0.989 | 约 40 秒 |
| 本方案 CNN | 0.991 | 0.990 | 约 5 分钟(CPU) |
这组数字受随机种子和编码顺序影响,但量级是稳定的。CNN 相对 LightGBM 的收益主要落在 R2L、U2R 的召回上,代价是训练时间高一个量级。如果业务里这两类攻击威胁不大,直接用 LightGBM 部署反而更省事;反过来,如果少数类攻击恰是重点,CNN 值得留。
5. CNN 模型落地到真实网络的三个关键处理
NSL-KDD 上精度够了,不代表线上能直接用。真实流量有三个问题训练集里没有:少数类被梯度淹没、流量是流式到达的、特征分布会随时间漂移。这一章讲三个对应处理,都是实战里必做的。
5.1 用 WeightedSampler 缓解少数类被淹没
R2L 和 U2R 加起来不足 1%,普通 DataLoader 里一个 epoch 的梯度几乎被 Normal 和 DoS 主导。WeightedSampler 按类别频率倒数加权抽样,让少数类在一个 epoch 里被重复抽到,变相提高它们对梯度的影响。
from torch.utils.data import WeightedRandomSampler class_counts = torch.bincount(y) weights = 1.0 / class_counts.float() sample_weights = weights[y] sampler = WeightedRandomSampler(sample_weights, num_samples=len(sample_weights), replacement=True) loader = DataLoader(dataset, batch_size=256, sampler=sampler)关键参数是num_samples和replacement。保持num_samples等于原样本数,每个 epoch 的计算量不变;replacement=True允许同一批少数类样本被重复抽出。副作用是总体准确率通常会掉一两个点,但少数类召回明显上升,这正是检测系统想要的。更进一步的替代方案是 Focal Loss,通过调制因子压低易分样本的损失权重,效果类似但不用改 DataLoader。
5.2 滑动窗口在线推理与告警抑制
真实网络流量是持续到达的,检测单元通常是一个时间窗口。工程上会先按五元组把流量聚合成会话,再在固定时间窗内聚合出特征向量,交给模型打分。下面是一段简化的轮询伪代码:
import time WINDOW_SEC = 30 def detection_loop(): while True: features = aggregate_sessions(last_30s()) # 返回 (1, 121) 标准化特征 prob, label = infer_once(features) # 内部复用矩阵映射函数 if label != 'Normal' and prob > 0.85: alarm_queue.put((label, prob)) time.sleep(WINDOW_SEC)注意告警条件里加了一个经验做法:置信度大于 0.85 才触发。生产环境还要再加一层抖动抑制,要求连续 N 个窗口都报警才真正上页面告警,否则单窗口的偶发误报会淹没运维注意力。这层逻辑放在热路径之外,消耗可以忽略。
5.3 模型漂移监控与定期重训
线上特征分布会随业务变化慢慢漂移。常见做法是每周统计特征的均值、方差和模型置信度分布,跟训练时的基线做 KL 散度比较,超过阈值就触发增量重训。重训用最近一个月的数据加适量旧数据混合,避免灾难性遗忘。这个机制不需要额外框架,一支 cron 脚本加一份特征快照就能跑起来,关键是提前把训练脚本做成可重复执行的流水线,而不是手动点 Jupyter。
6. 用 PyTorch 量化导出把 CNN 压缩到适合部署的尺寸
模型只有五万参数,单看不重,但真实环境往往是十几个检测模型并排跑,每个少几 MB、推断快几毫秒都会影响整体的资源预算。PyTorch 的动态量化在十层以内的小网络上收益很稳定,而且代码量极少。
import torch from torch.quantization import quantize_dynamic # 加载训练好的权重,保证是在 CPU 上 model.load_state_dict(torch.load('best_model.pt', map_location='cpu')) model.eval() # 动态量化:只量化 Linear 层,权重从 fp32 降到 int8 qmodel = quantize_dynamic(model, {torch.nn.Linear}, dtype=torch.qint8) # 导出 ONNX,动态 batch 维度方便换推理引擎 dummy = torch.randn(1, 1, 11, 11) torch.onnx.export(qmodel, dummy, 'nids_cnn.onnx', input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch'}}, opset_version=13)量化后模型文件大约从 780KB 降到 200KB,CPU 上单样本推断耗时减少一半左右。动态量化只针对 Linear 层,Convolution 层保持 fp32,对这个结构已经够用;如果想要更激进,可以走torch.quantization的静态量化流程,但需要准备校准数据集,收益相对于复杂度并不大。
量化精度的验证必须实测。加载量化模型跑一遍验证集,跟原模型逐条对比,准确率下降控制在 0.5% 以内才允许上线。另一个实用技巧是导出前先用几组真实流量样本跑一遍推理,这一步不是为了校准,而是为了暴露特征预处理的不一致——线上调用里的 reshape 顺序、列名对齐逻辑如果没有跟训练脚本共用同一份代码,量化后的精度损失会被误认为是量化本身造成的。所有踩过的坑里,预处理不一致造成的隐性精度损失远多于模型结构的问题。
本文还有配套的精品资源,点击获取