ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

入侵检测项目实战:基于UNSW_NB15的机器学习与深度学习建模

入侵检测项目实战:基于UNSW_NB15的机器学习与深度学习建模

简介:一套基于Python机器学习与深度学习的入侵检测项目完整工程,定位清晰,主要面向计算机相关专业正在筹备毕业设计、课程设计或期末大作业的学生,也适合希望积累项目经验的入门学习者。项目经导师指导并通过高分评审(99分),代码完整可靠,从数据读入到结果输出均可直接运行,降低小白上手门槛。资源包共31个文件,压缩后约26.58MB,涵盖14个Python脚本、3个CSV训练/测试数据、HDF5模型权重、Markdown说明与技术方案文档;其中Python脚本覆盖CNN/LSTM模型训练与预测、特征提取、特征相关性分析、数据不平衡处理、归一化与编码等核心环节。原始网络流量数据集及相关中间处理数据也已一并打包,整体结构清晰,目前已吸引95人学习/下载。除可用源码外,还附带项目文档、参考论文和使用说明,能够帮助读者理解入侵检测从特征工程、算法建模到性能评估的完整流程,适合作为毕业设计或课程设计的系统参考。

1. 一套能跑的入侵检测项目:数据、模型和文档都备齐了

做入侵检测相关的毕设或者期末大作业,最头疼的不是算法本身,而是「从哪弄一份像样的数据」和「怎么证明你的模型真的有效」。用 MNIST 练手分类,跟用网络流量数据做入侵检测完全是两回事。这份基于 Python 机器学习与深度学习的入侵检测项目,把整条链路都铺好了:UNSW_NB15 数据集的原始包和处理后的 CSV、特征工程的完整脚本、机器学习与深度学习的双路线实现,外加项目文档和参考论文,适合直接拿来复现、改参数、写进毕设。

我拆完这份资源的第一反应是「它该有的都有了」——随机森林、CNN、LSTM 三套模型都各自有训练和预测脚本,数据预处理里连不平衡样本处理和相关性分析都单独拆了文件。对于正在做课程设计或期末大作业的学生来说,这比从零开始抓流量、标数据、调模型要省太多时间;对想练手的人而言,它也是一份能对着跑通全流程的完整样例。论文部分还能帮你快速理解特征选择和数据平衡在入侵检测里的必要性,至少写文档时不会没话讲。

2. 数据准备:UNSW_NB15 上场,特征工程先于模型

2.1 为什么选 UNSW_NB15 而不是 KDD99

很多入门教程还在用 KDD99 做入侵检测练习,但学术界这几年基本默认 UNSW_NB15 更贴近真实网络环境。KDD99 是 1998 年采集的数据,攻击类型和流量特征都偏老;UNSW_NB15 由澳大利亚网络安全中心生成,包含了正常流量和九大类攻击流量,比如 Fuzzers、Analysis、Backdoors、DoS、Exploits、Generic、Reconnaissance、Shellcode 和 Worms,特征维度也扩大到 49 个左右。

这份资源里自带UNSW_NB15.zip原始包和Process_data.zip处理后的数据,解压后能看到train.csv、test.csv以及2_feture.csv。train.csv和test.csv是已经拆好的训练与测试集,2_feture.csv应该是特征筛选后保留的简化特征版本。原始数据集字段很多,直接塞进模型之前必须过一遍特征工程,这也是整个项目里最需要操作的一步。

2.2 预处理脚本拆解:从原始 CSV 到能训练的特征矩阵

资源里ML目录下的几个脚本把预处理链路拉得很清晰。我按常见的执行顺序读了一遍,整个流程大致是:先做标签编码(Lable_encoder.py),再做数值归一化(Min-max.py),然后用Get_corr.py做相关性分析去冗余,最后用Process_Imbanlce.py处理类别不平衡,再用get_feature.py做特征筛选。下面给出一个典型的读取和清洗流程,和项目里的做法基本一致:

import pandas as pd from sklearn.preprocessing import MinMaxScaler, LabelEncoder df = pd.read_csv('train.csv') # UNSW_NB15 里有些列是类别型特征,比如 proto/service/state cat_cols = ['proto', 'service', 'state'] for col in cat_cols: le = LabelEncoder() df[col] = le.fit_transform(df[col].astype(str)) # 数值列统一做 Min-Max 归一化 num_cols = df.select_dtypes(include=['float64', 'int64']).columns scaler = MinMaxScaler() df[num_cols] = scaler.fit_transform(df[num_cols])

逻辑说明:类别型特征不能直接喂给树模型和神经网络,所以先用LabelEncoder把字符串映射成整数;数值型特征用Min-Max缩放到 0 到 1 之间,避免某些字段(比如字节数)的数值范围过大压过其他特征。参数说明:scaler.fit_transform是在整个训练集上拟合再变换,后面对测试集操作时应该只transform,千万别再fit一次,否则测试集的分布会被重新缩放,指标会虚高。这一点在后面的避坑章节还会专门展开。

2.3 相关性与不平衡:两份脚本的核心价值

Get_corr.py做的事是计算特征列之间的皮尔逊相关系数,筛掉高度相关的冗余特征。入侵检测数据里很多特征是统计派生出来的,比如源字节数和总字节数就有很强的相关性,同时保留不但不会提升精度,还会拖慢训练速度。我在类似项目里的习惯是保留相关系数低于 0.95 的特征,相关性超过这个阈值的只留一个。

Process_Imbanlce.py处理的是类别不平衡问题。UNSW_NB15 里正常流量占比远高于各类攻击流量,直接训练会让模型倾向于把所有样本预测成多数类。这里常用的做法是 SMOTE 过采样或者对少数类加权重。你需要先查一下标签列里各个类别的分布,再决定是过采样少数类还是用class_weight惩罚误分多数类。这个脚本的存在说明项目作者已经把这一步纳入流程,实际训练前值得仔细看一下它的具体实现。

3. 机器学习路线:用随机森林做分类器,把细节抠到特征层

3.1 从RFP.py看模型选型:为什么先选随机森林

ML目录下的RFP.py是随机森林分类器的训练脚本。入侵检测场景里,随机森林比 SVM 和朴素贝叶斯更稳的原因主要有三个:能直接处理编码后的特征并给出特征重要性;对类别不平衡有一定容忍度;训练速度快,适合在 49 维特征上跑多轮试验。

从项目文件还能看到3d.py和classify.py,前者大概率是特征三维可视化,用来观察正常和攻击样本在降维空间的分布,后者是通用分类器入口。我的建议是把随机森林当成这个项目的基线模型,先跑通拿到一组基础指标,再切换到深度学习模型做对比,论文里就有一条完整的对比曲线。

下面给一个可运行的随机森林训练片段,跟RFP.py的逻辑一致:

import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix train_df = pd.read_csv('train.csv') test_df = pd.read_csv('test.csv') X_train = train_df.drop(columns=['label']) y_train = train_df['label'] X_test = test_df.drop(columns=['label']) y_test = test_df['label'] rf = RandomForestClassifier( n_estimators=200, max_depth=20, min_samples_leaf=2, n_jobs=-1, random_state=42 ) rf.fit(X_train, y_train) y_pred = rf.predict(X_test) print(classification_report(y_test, y_pred)) print(confusion_matrix(y_test, y_pred))

逻辑说明:n_estimators=200表示用 200 棵决策树做集成,决策树多了稳定性好但训练会变慢;max_depth=20限制单棵树深度,防止过拟合;min_samples_leaf=2要求叶子节点至少两个样本,也是抑制过拟合的手段。参数说明:n_jobs=-1会让 sklearn 用满所有 CPU 核心,如果你的机器核数不多或者同时还在跑别的东西,建议改成n_jobs=4之类的数值。

3.2 用get_feature.py的结果反推特征工程好不好

特征工程做得怎么样,最直接的验证方式是看随机森林输出的feature_importances_。如果某些特征的贡献值明显为零或者趋近于零,说明Get_corr.py和get_feature.py还有压缩空间。我在实际项目里的做法是先把特征重要性从大到小排序,画一个累计重要性曲线,通常前 20 个特征就能覆盖 95% 以上的贡献。

这也直接关系到2_feture.csv的存在意义——它是特征筛选后的产物。你可以直接用这个精简后的文件训练一个模型,再跟全特征模型对比。如果精度没有明显下降,论文里可以写「通过相关性分析与特征重要性评估,剔除冗余特征,在降低计算开销的同时保持了检测性能」,这句话在毕设答辩里很加分。

3.3 机器学习路线的完整执行顺序

按照项目里的文件结构,我建议按下面这个顺序跑:

  1. 运行Lable_encoder.py,把字符串类型的类别特征转成数值。
  2. 运行Min-max.py,做数值归一化,保存 scaler 备用。
  3. 运行Get_corr.py,输出相关性矩阵,找出冗余特征。
  4. 运行get_feature.py,按特征重要性或相关性结果筛选特征,生成2_feture.csv。
  5. 运行Process_Imbanlce.py,观察类别分布并做平衡处理。
  6. 运行RFP.py和classify.py,训练并评估。

每一步的输出都最好保存下来,后面深度学习模型会用同一份预处理后的数据,避免两套流程导致对比不公平。

4. 深度学习路线:PyTorch 里的 CNN 与 LSTM

4.1cnn_pytorch.py的模型结构拆解

项目里有两个 CNN 相关入口:cnn_pytorch.py和cnn_train.py。前者应该是完整的 PyTorch 模型定义与训练脚本,后者可能是便于复跑的训练入口。入侵检测用 CNN 的思路是:把每条流量记录的特征向量视为一维序列,用一维卷积提取局部模式。虽然流量数据不像图片有空间结构,但相邻特征之间的交互关系,比如端口号与协议类型的组合、字节数与持续时间的关系,卷积核是可以捕捉到的。

一个典型的 CNN 分类器定义如下:

import torch.nn as nn class IDS_CNN(nn.Module): def __init__(self, input_dim, num_classes): super(IDS_CNN, self).__init__() self.conv1 = nn.Sequential( nn.Conv1d(1, 64, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool1d(2) ) self.conv2 = nn.Sequential( nn.Conv1d(64, 128, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool1d(2) ) self.fc = nn.Sequential( nn.Flatten(), nn.Linear(128 * (input_dim // 4), 128), nn.ReLU(), nn.Dropout(0.5), nn.Linear(128, num_classes) ) def forward(self, x): # x: (batch, input_dim) -> 增加通道维变成 (batch, 1, input_dim) x = x.unsqueeze(1) x = self.conv1(x) x = self.conv2(x) x = self.fc(x) return x

逻辑说明:输入维度input_dim是每条样本的特征数量,unsqueeze(1)把形状从(batch, input_dim)变成(batch, 1, input_dim),Conv1d 要求的输入格式是(batch, channels, length)。第一层卷积用 64 个卷积核,第二层增加到 128 个,捕获更高阶的特征组合。参数说明:kernel_size=3是卷积窗口长度,padding=1保持序列长度不变,MaxPool1d(2)把长度减半,所以全连接层输入维度是128 * (input_dim // 4),两次池化各除 2,所以要整除 4。如果你的input_dim不是偶数,池化后维度会出问题,做之前先检查特征数量。

4.2lstm_train.py里的序列建模思路

LSTM 和 CNN 的视角不一样。CNN 看的是相邻特征的局部组合,LSTM 则把特征顺序当作时间步来建模。每条流量记录展开成一个长度等于特征数量的序列,LSTM 按顺序读入,学习特征之间前后依赖的关系。某些攻击行为确实存在这种前序特征影响后续特征的模式,比如连接持续时间会影响随后记录的字节数分布。

项目里lstm_train.py和lstm_predict.py分别负责训练和推理,demo_2分类.py是简化版二分类演示。LSTM 训练较慢而且对超参数敏感,hidden_size和num_layers都要调,常见配置是hidden_size=64、num_layers=2。注意 LSTM 的输入格式是(batch, seq_len, input_size),如果直接把形状为(batch, input_dim)的特征矩阵传进去会报错,需要 reshape 成(batch, input_dim, 1)或者按固定窗口切分。

4.3 训练脚本里的损失函数、优化器与评估指标

cnn_train.py和lstm_train.py里大概率都用CrossEntropyLoss做多分类损失,优化器用Adam,学习率建议从 1e-3 起步。多分类入侵检测的评估不能只看 accuracy,因为正常流量占比高,模型就算把攻击流量全部误判,accuracy 也可能很难看。要同时关注 macro-F1 和各类别的召回率,尤其像 Backdoors 和 Shellcode 这类样本量极少的攻击类别。

到这里你会发现,深度学习路线能跑通不难,跑出有说服力的指标才是真正的分水岭。而指标垮掉的原因,往往不在模型结构上,而是前面预处理埋的雷。

5. 踩坑手册:特征泄漏、标签泄漏与不平衡数据

5.1 指标虚高到离谱:归一化放错了位置

现象:训练集 accuracy 和测试集 accuracy 都高达 98% 以上,接近完美,但拿到新数据上一测就垮。

原因:MinMaxScaler在切分训练/测试集之前就做了fit_transform,测试集的分布信息被混进了 scaler 的 min 和 max 参数里。测试集和训练集经过同一个缩放器的统计量去变换,相当于测试集信息提前泄漏给训练过程。

解决:把数据切分放在归一化之前,先fit只有训练集,再用同一个transform作用到训练集和测试集。写代码的固定顺序是:切分 → 训练集fit_transform→ 测试集transform。

5.2 召回率上不去:SMOTE 过采样打错了时间点

现象:预测结果里正常流量基本全对,但攻击类别的召回率很低,尤其是少数类攻击几乎全被预测成正常。

原因:类别不平衡处理做得太早,SMOTE 对整份数据做了过采样之后才拆训练集和测试集,导致合成样本同时出现在两边,模型「见过」测试集里少数类的特征,但真实场景里没见过这类攻击,于是表现很差。

解决:先切分,再只对训练集做 SMOTE。这也是Process_Imbanlce.py里最容易写错的顺序。如果在脚本里看到对全量数据做过采样,建议改成用imbalanced-learn的SMOTE配合train_test_split使用。

5.3 LSTM 训练曲线像心跳:shuffle 把序列顺序打乱了

现象:LSTM 训练时 loss 剧烈震荡,或者 val_loss 忽高忽低,完全没有收敛趋势。

原因:对时间序列数据做了随机shuffle。LSTM 依赖样本内部特征的顺序,但这个顺序在特征间是有逻辑关联的,跟时间窗口不一样。如果你把整条样本当成一个时间序列来处理,shuffle 样本批次会影响学习稳定性。

解决:shuffle 可以保留,但注意辨析。如果你构造的是跨时间步的序列样本,每个样本本身已经代表一段固定时间窗,shuffle 样本顺序没问题;如果你把一条流量记录的特征按序输入 LSTM,shuffle 多条样本的 batch 顺序也不影响内部特征顺序。真正需要注意的是,不要把特征本身的排列顺序打乱。先确认你的数据是「每条样本独立」还是「连续时间窗」,再去决定 shuffle 的粒度。

5.4 训练完的模型换个环境就不认了:PyTorch 版本不一致

现象:在自己机器上训练好的模型权重文件,换到另一台机器加载时报错,或者精度明显下降。

原因:PyTorch 不同版本的序列化格式不完全兼容,GPU 和 CPU 之间加载权重也可能因为map_location没指定而出错。

解决:加载时显式指定map_location='cpu',如果是在 GPU 上训练、到 CPU 上推理,加上torch.load(..., map_location=torch.device('cpu'))。项目里有requirements.txt,装环境时尽量照着锁版本,能少很多玄学问题。

5.5 测试集上表现异常好:查查有没有同样本泄漏

现象:模型在测试集上的 F1 比训练集还高,换到提交环境或新抓的数据上就现原形。

原因:数据切分前没有做去重。UNSW_NB15 原始数据里存在重复记录或近似重复记录,如果训练集和测试集是从同一个原始包里随机切出来的,重复样本会同时出现在两边。

解决:切分前先drop_duplicates(),或者按会话标识拆分。检查方法是跑一个 KNN 或者直接比对训练集和测试集的特征距离,距离为零的样本数量越多,泄漏越严重。

6. 几个立竿见影的迁移技巧:把指标和训练曲线变成拿得出手的结果

6.1 训练脚本的「记忆点」:把随机种子和实验参数写进文件名

同一个模型跑两遍结果不同,是深度学习最常见的质疑点。为了避免答辩时被人问「你这个结果能复现吗」,我在跑cnn_train.py这类脚本之前,习惯在代码开头固定随机种子:

import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed)

然后在保存模型和结果时,把关键超参数拼进文件名,例如cnn_bs64_lr1e-3_epoch30_acc92.1.pth。这样每一版实验结果都留有一份明确记录,翻车了也能快速定位是哪一组参数。

6.2 用学习率预热和早停控制训练

cnn_pytorch_acc.jpg这个文件大概率是训练过程中 accuracy 的截图。这类曲线能反映出模型的收敛情况和是否过拟合。我一般会在训练脚本里加两个控制逻辑:前 5 个 epoch 用预热学习率从 1e-5 线性升到目标学习率,之后按 epoch 衰减;同时监控验证集 loss,连续 10 个 epoch 不下降就保存当前最优权重并停止。

这样做的收益很明显:避免前期学习率太大导致 loss 爆炸,也能防止后期在验证集上出现过拟合。毕设论文里写上「采用 warmup 与 early stopping 策略,最终模型在第 22 个 epoch 达到最优验证指标」,比贴一张干巴巴的训练曲线有说服力得多。

6.3 结果展示的两个检查点

第一个检查点:看类别级别的召回率,而不是只看准确率。入侵检测的漏报代价远高于误报,重点攻击类别的召回率必须单独列出。第二个检查点:保存混淆矩阵图。答辩时展示 CNN 和 LSTM 在各类别上的混淆矩阵,比说十句话都管用。demo_2分类.py里应该有画图相关逻辑,跑完后把图片存下来放文档里。

以前调参的时候,我把归一化放在了切分数据之前,训练集测试集的 accuracy 都在 99% 上下,当时还挺得意。导师问了一句「你的归一化参数是怎么算的」,我打开代码一看才发现问题——从那以后,我每次写预处理脚本都强制按切分、拟合、变换的顺序走一遍,检查无误才开始训练。这种思路也值得你参考:先把数据流程理的滴水不漏,再去模型里调参,希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进