简介:面向计算机相关专业毕业设计、课程设计与期末大作业场景,这份基于机器学习的入侵检测系统项目包含完整可运行的Python源码与配套技术文档。项目曾获导师认可与99分评审高分,覆盖数据预处理、特征提取、模型训练与预测等环节,并分别提供CNN、LSTM及CNN-PyTorch实现;除训练与预测外,预处理部分也覆盖相关性分析、不平衡样本处理、归一化及标签编码等常用步骤,适合需要实战参考的本科生与入门学习者。压缩包共31个文件,以14个Python脚本为核心,辅以CSV数据集、HDF5模型权重、Markdown/TXT说明文档、技术方案Word文档和结构图,整体约26.58MB,目录按CNN、LSTM、ML、data等模块划分,便于对照研读。资源内包含UNSW_NB15数据集处理脚本及打包数据,可直接复现二分类入侵检测流程。目前已有68人学习下载,对希望短时间搭建同类型系统或快速理解检测思路的读者有较高参考价值。
1. 把入侵检测当分类问题:UNSW-NB15 与 CNN/LSTM 的落地组合
入侵检测系统听起来像个黑匣子,但把这套源码拆开之后你会发现,它的核心就是一句话:给一条网络流量记录,判断它是正常还是攻击。UNSW-NB15 提供了 49 维特征和九类攻击行为,CNN 负责提局部模式,LSTM 负责建模顺序关系,两者都是可以直接训练的分类器。这套基于机器学习的入侵检测系统 Python 源码把数据处理、模型训练、预测验证串成了一条完整链路,数据、脚本、文档、结果图片都齐,适合做机器学习课设、期末大作业或者毕设起步。想快速跑通全流程的,从数据管道往下走就行。
2. 数据管道先行:特征提取、编码归一化与类别不平衡处理
数据处理是整个项目里最容易翻车也最花时间的环节。机器学习里的数据处理做不好,后面模型调参全是白费。这套源码把这一步拆成了四个脚本:Get_corr.py、get_feature.py、Lable_encoder.py、Min-max.py,加上一个 Process_Imbanlce.py 处理样本不平衡,顺序对上了,模型训练才能顺。
2.1 特征工程:Get_corr.py 与 get_feature.py 在筛什么
UNSW-NB15 解压之后,你会看到一个列数很多的 CSV。原始数据集有 49 个特征,包含像 srcip、dstip 这样的 IP 字符串列,还有 sport、dsport 端口号,以及协议类型 proto。这些列里,IP 地址是纯类别信息,模型直接读会当成巨大的数值,端口号则会被 80、443 这种大数字干扰特征尺度。所以拿到原始表第一步不是训练,而是先把特征筛一遍。
项目里 Get_corr.py 做的是特征相关性分析,输出各特征与标签的相关性排序,用来判断哪些列对"是否攻击"有区分度。get_feature.py 则把筛选结果落地,生成后续真正喂给模型的特征矩阵。我一般会在这一步把 IP 段、端口这类高基数类别列先剔除,再看相关性。
# 特征筛选:剔除字符串列后按相关系数排序 import pandas as pd df = pd.read_csv('../data/UNSW_NB15.csv', encoding='utf-8') drop_cols = ['id', 'srcip', 'dstip', 'sport', 'dsport'] df = df.drop(columns=[c for c in drop_cols if c in df.columns]) # 相关性排序(二分类标签列通常叫 label) corr = df.corr()['label'].abs().sort_values(ascending=False) print(corr.head(20))逻辑说明:corr() 只能处理数值列,所以先把字符串列剔除干净,否则会抛异常或产生一堆 NaN。相关系数取绝对值,正负只代表方向,绝对值越大说明该特征与标签的线性关系越强。排序后你通常能看到 service、ct_state_ttl、ct_srv_dst 这类连接状态特征排在最前面,这和网络安全领域的经验判断是一致的。
参数说明:相关性阈值取多少没有定论。特征多的时候我习惯先按 0.05 卡一条线,把低于这个值的弱相关特征丢掉,再在训练时对比丢之前的精度。如果算出来大部分特征相关性都很低,说明原始数据里脏列太多,先回头检查有没有没剔除的字符串列。
2.2 编码与归一化:Lable_encoder.py 和 Min-max.py 的配合顺序
数据里 attack_cat 列是字符串形式的攻击类型,比如 "Normal"、"Fuzzers"、"Generic"。模型要的是数值,所以 Lable_encoder.py 会用 LabelEncoder 或 pandas 的 factorize 把它映射成 0、1、2 这样的整数。二分类场景下更常见的做法是只保留 0 和 1:所有攻击类别合并成 1,正常为 0;多分类场景才保留 0 到 9 的十类编码。
归一化这块,Min-max.py 对应 sklearn 的 MinMaxScaler。为什么选 MinMax 而不是 StandardScaler?CNN 和 LSTM 的激活函数对输入尺度敏感,MinMax 把特征压到 0~1 区间,保留原始分布形状;StandardScaler 会把特征变成均值 0 方差 1 的分布,产生负值,ReLU 对负输入直接截断,梯度信号就丢了。不过在特征存在明显长尾分布的情况下,MinMax 会让大多数样本挤在 0 附近,这时候 StandardScaler 反而更好——但这个项目里先按 Min-max.py 跑通。
# 标签编码 + MinMax 归一化的完整顺序 import pandas as pd from sklearn.preprocessing import LabelEncoder, MinMaxScaler df = pd.read_csv('../data/UNSW_NB15.csv', encoding='utf-8') # 1) 先对攻击类别做标签编码 le = LabelEncoder() df['attack_cat'] = le.fit_transform(df['attack_cat'].astype(str)) # 2) 二分类标签:所有攻击类合并为 1,正常为 0 df['label'] = (df['attack_cat'] != le.transform(['Normal'])[0]).astype(int) # 3) 数值特征列统一做 MinMax 归一化 feature_cols = [c for c in df.columns if c not in ['attack_cat', 'label', 'id']] scaler = MinMaxScaler() df[feature_cols] = scaler.fit_transform(df[feature_cols]) df.to_csv('../data/2_feture.csv', index=False)逻辑说明:先编码再归一化,这个顺序不能换。LabelEncoder 要求输入全是字符串,归一化要求输入全是数值。如果先把整个 DataFrame 丢给 MinMaxScaler,字符串列会直接报错;如果先归一化再编码,标签列混在特征矩阵里,归一化会把标签也压一遍,等于提前泄露了目标信息。处理完的 2_feture.csv 就是后续所有模型共用的特征文件。
参数说明:MinMaxScaler 的 feature_range 默认是 (0,1)。如果训练时发现模型收敛慢,可以改成 (-1,1),但配套的激活函数和输入维度要重新核对。注意 fit 只能用训练集,测试集只做 transform,否则测试集的信息会提前混进训练流程。
2.3 类别不平衡:Process_Imbanlce.py 处理的是分布不是样本量
UNSW-NB15 的类别分布很不均匀。Generic 和 Exploits 样本很多,Shellcode 和 Worms 很少,有的类别可能只有几百条,而正常流量有几万条。直接拿原始分布训练,模型会学成"无脑预测大类",整体准确率看着很高,但小类攻击几乎全漏。这就是 Process_Imbanlce.py 存在的意义——它处理的不是样本数量本身,而是类别分布。
常见做法是下采样或者过采样。下采样把大类抽稀,训练速度快但会丢信息;过采样用 SMOTE 合成少类样本,信息保留好但可能产生噪声。Process_Imbanlce.py 从命名看走的是后一条路,用 imbalanced-learn 库做重采样。
# 用 SMOTE 做类别不平衡处理,先切分再重采样 from imblearn.over_sampling import SMOTE from sklearn.model_selection import train_test_split X = df[feature_cols].values y = df['label'].values X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) smote = SMOTE(random_state=42) X_train_res, y_train_res = smote.fit_resample(X_train, y_train) print(pd.Series(y_train_res).value_counts().to_dict())逻辑说明:先切分再重采样是硬性要求。如果先对整个数据集 SMOTE 再切训练测试集,合成样本可能同时出现在两边,造成数据泄露,测试指标虚高得离谱。stratify=y 保证切分后训练集和测试集里的类别比例跟原始数据一致,这一步在类别不平衡时尤其重要。
参数说明:SMOTE 的 k_neighbors 默认 5,对样本只有几百条的少数类,建议调低到 3,否则合成样本会和大量邻居重叠。重采样后模型训练时长会明显增加,尤其是 LSTM。如果机器性能吃紧,可以考虑先下采样大类到两倍于少类,再做 SMOTE。
2.4 数据切分:train.csv 与 test.csv 的划分思路
data 目录里已经有 train.csv 和 test.csv,省了手动划分的步骤。但你需要知道它是怎么切的,这直接影响你对外汇报评估结果的底气。入侵检测领域的惯例是按时间切分:原始记录按时间排序,前 80% 时间段做训练,后 20% 做测试。这样模拟的是"用过去的流量发现未来的攻击",比随机切分更接近真实部署场景。
如果作者用的是随机切分,那评估结果会偏乐观一点,因为训练集和测试集来自同一分布。不管哪种方式,至少确认测试集里每条攻击类别都出现了,别让 Shellcode 这种少类在测试集里一条都见不到。
# 按时间戳切分,避免随机切分带来的乐观偏差 df = pd.read_csv('../data/UNSW_NB15.csv', parse_dates=['time']) df = df.sort_values('time') split_idx = int(len(df) * 0.8) train_df = df.iloc[:split_idx].copy() test_df = df.iloc[split_idx:].copy() print(f'train: {len(train_df)}, test: {len(test_df)}')逻辑说明:parse_dates 会把 time 列解析成时间类型,sort_values 按时间升序排列。取前 80% 做训练、后 20% 做测试,保证测试集在时间上严格晚于训练集。如果后续发现测试集效果比随机切分差,这是正常现象,按时间切分的评估结果通常更保守。
3. 双模型落地:CNN 与 LSTM 在 UNSW-NB15 上的训练路径
数据管道跑通之后,进入模型环节。这套资源给了两条路:cnn 目录下的 CNN 方案,lstm 目录下的 LSTM 方案。两者读取的是同一份 2_feture.csv,差异在于怎么理解"一条网络流记录"——CNN 把它当成局部特征组合,LSTM 把它当成特征顺序序列。
3.1 CNN 的训练入口与模型结构:cnn_pytorch.py 与 cnn_train.py 怎么配合
cnn 目录下有两个关键脚本:cnn_pytorch.py 负责模型定义,cnn_train.py 负责训练流程。从 PyTorch 的实现来看,这种表格数据用的是一维卷积 Conv1d。每条样本是一个特征向量,可以理解成单通道的一维信号,卷积核沿着特征维度滑动,提取相邻特征之间的局部模式。比如端口号和协议类型之间的组合关系,可能就藏在这种局部结构里。
# 基于 PyTorch 的 1D-CNN 模型核心结构(对应 cnn_pytorch.py) import torch import torch.nn as nn class IDSCNN(nn.Module): def __init__(self, n_features, num_classes=2): super().__init__() self.conv_block = nn.Sequential( nn.Conv1d(1, 64, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(64, 128, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool1d(2), ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128 * (n_features // 4), 128), nn.ReLU(), nn.Dropout(0.5), nn.Linear(128, num_classes), ) def forward(self, x): x = x.unsqueeze(1) # (batch, n_features) -> (batch, 1, n_features) x = self.conv_block(x) return self.classifier(x)逻辑说明:Conv1d 的输入形状是 (batch, channels, length),特征向量先 unsqueeze(1) 变成单通道一维信号。kernel_size=3 表示卷积核每次看相邻 3 个特征,padding=1 保持卷积后的长度不变。MaxPool1d(2) 把特征长度减半,两次池化后长度变为原来的四分之一,所以全连接层第一个 Linear 的输入维度要用 n_features 除以 4。如果特征数不是 4 的整数倍,这里维度就对不上,会直接报错。
训练脚本 cnn_train.py 的流程是标准的 PyTorch 训练循环:读 train.csv,转成 Tensor 和 DataLoader,每个 epoch 里算 loss、反向传播、更新权重,并定时在验证集上评估。二分类场景建议用 CrossEntropyLoss,配合最后一层输出的 logits,数值上比 BCELoss 加 Sigmoid 稳定。优化器用 Adam,学习率先给 1e-3,如果 loss 震荡再降到 1e-4。
# cnn_train.py 训练主循环的简化结构 from torch.utils.data import DataLoader, TensorDataset dataset = TensorDataset(torch.tensor(X_train, dtype=torch.float32), torch.tensor(y_train, dtype=torch.long)) loader = DataLoader(dataset, batch_size=64, shuffle=True) model = IDSCNN(n_features=feature_cols长度, num_classes=2) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss() for epoch in range(epochs): for X_batch, y_batch in loader: optimizer.zero_grad() out = model(X_batch) loss = criterion(out, y_batch) loss.backward() optimizer.step()逻辑说明:batch_size=64 是表格数据常见起点,数据量不大时 32 也行。shuffle=True 打乱样本顺序,避免模型学到样本排布规律。每个 batch 里 optimizer.zero_grad() 必须放在 forward 之前,否则梯度会累加。训练完之后模型权重会存到 cnn/res 目录下,供 cnn_predict.py 调用。
3.2 LSTM 的序列建模与参数:lstm_train.py 在建模什么
LSTM 在入侵检测里的作用有两种理解。一种是把同一源 IP 的多条流量按时间组织成真实序列,另一种是把单条流量的特征向量按特征顺序逐位读入,每个特征值当作一个时间步。lstm_train.py 走的是后一种思路,代码上更简单:一条样本是一个 49 维向量,LSTM 按顺序读完 49 个特征,最后输出分类结果。
# LSTM 模型核心结构(对应 lstm_train.py 的实现思路) import torch.nn as nn class IDSLSTM(nn.Module): def __init__(self, n_features, hidden_size=64, num_layers=2, num_classes=2): super().__init__() self.lstm = nn.LSTM( input_size=1, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, bidirectional=True, ) self.fc = nn.Linear(hidden_size * 2, num_classes) def forward(self, x): x = x.unsqueeze(-1) # (batch, n_features, 1) out, _ = self.lstm(x) out = out[:, -1, :] # 取最后一个时间步的输出 return self.fc(out)逻辑说明:input_size=1 表示每个时间步只读一个特征值,n_features 个特征就是 n_features 个时间步。bidirectional=True 让 LSTM 从两个方向读特征序列,最后一个时间步的输出包含了整个序列的信息。hidden_size*2 是因为双向 LSTM 会把正向和反向的隐藏状态拼接起来。
参数说明:hidden_size=64 是起步值,数据集规模大时可以调到 128。num_layers=2 增加网络深度,但小数据集上超过两层容易过拟合。如果训练太慢,把 num_layers 降到 1,或者 hidden_size 降到 32,先跑通再调大。LSTM 比 CNN 慢不少,CPU 机器上要有心理准备,这也是为什么很多入门项目先用 CNN 出基线结果。
3.3 预测与评估:cnn_predict.py 与 lstm_predict.py 的结果怎么解读
预测脚本读测试集,加载训练好的模型权重文件,输出每条样本的预测类别。cnn_predict.py 和 lstm_predict.py 的逻辑几乎一样,只是加载的模型类不同。输出除了预测标签,一般还会附带混淆矩阵和分类报告。
# 预测脚本核心逻辑(对应 cnn_predict.py) import torch from sklearn.metrics import classification_report, confusion_matrix model.load_state_dict(torch.load('cnn/res/best_model.pt')) model.eval() preds, labels = [], [] with torch.no_grad(): for X_batch, y_batch in test_loader: out = model(X_batch) pred = out.argmax(dim=1) preds.extend(pred.numpy()) labels.extend(y_batch.numpy()) print(confusion_matrix(labels, preds)) print(classification_report(labels, preds, digits=4))逻辑说明:model.eval() 会关闭 Dropout 和 BatchNorm 的训练行为,这一步忘了的话预测结果不稳定。argmax(dim=1) 取每个样本概率最高的类别作为输出。classification_report 给出精确率、召回率、F1 和每个类别的样本数,这些比整体 accuracy 更值得关注,因为入侵检测场景下漏报攻击的代价远高于误报正常流量。
如果预测脚本报找不到权重文件,先回训练目录确认训练真的跑完并且保存了 .pt 文件。root 目录下的 cnn_pytorch_acc.jpg 是训练过程的准确率曲线截图,对照它就能判断自己训练出的模型有没有达到预期水平——如果曲线明显没收敛,说明数据管道或超参有问题,往下看避坑章节。
4. 避坑实录:数据格式、依赖版本与运行环境的五个典型问题
这套源码我在复现的时候踩过不少坑。下面这五条基本覆盖了新手运行时会碰到的绝大多数问题,按现象、原因、解决的顺序写,可以直接对照排查。
4.1 现象一:ModuleNotFoundError: No module named 'torch'
原因:虚拟环境没建,或者 requirements.txt 没安装。很多拿源码直接跑的人用的是系统 Python,里面只装了 pip 和几个基础库。CNN 和 LSTM 方案都依赖 PyTorch,数据处理依赖 scikit-learn 和 imbalanced-learn,少一个都跑不起来。
解决:先建虚拟环境,再执行 pip install -r requirements.txt。Windows 下安装 PyTorch 如果从默认 PyPI 拉包很慢,可以加国内镜像源。装完后进入 Python 验证一下 import torch 和 import imblearn 是否正常,两个都通过再跑训练脚本。
4.2 现象二:训练 loss 不降,accuracy 一直在 50% 附近震荡
原因:输入数据没有归一化,或者标签没编码干净。CNN 和 LSTM 对输入量级非常敏感,源端口、包长度这种大数值列会把梯度推向错误方向。另一个隐蔽原因是训练集和测试集用了不同的特征列顺序,比如从 dict 构建 DataFrame 时列顺序不稳定,模型在训练时看到的特征排列和测试时不一致。
解决:回到 Min-max.py 把特征压到 0~1,确认 attack_cat 列已经被编码成 0/1 而不是保留原始字符串。特征列顺序统一用同一份 feature_cols 列表来取,不要靠记忆写列名。检查方式是打印训练集和测试集的列名列表,逐列比对。
4.3 现象三:准确率 95% 以上,但预测结果全是正常类
原因:这是类别不平衡最典型的翻车现场。UNSW-NB15 里正常流量占比大,模型学到"全部预测正常"这种偷懒策略就能拿到很高准确率。如果 Process_Imbanlce.py 没跑,或者跑完重采样之后没有 stratify 切分,测试集里攻击样本太少,评估结果就更具有欺骗性。
解决:训练前用 SMOTE 重采样训练集,但测试集保持原始分布,不要对测试集做任何重采样。评估时看 classification_report 里每个攻击类别的召回率,而不是只看整体 acc。如果召回率还是上不去,把 SMOTE 的采样策略改成 0.5 比例,减少合成样本的噪声干扰。
4.4 现象四:Windows 下路径带中文或反斜杠报错
原因:pandas 的 read_csv 在 Windows 上遇到中文目录或反斜杠转义会出问题,或者文件编码不是 UTF-8。UNSW-NB15 原始 CSV 某些字段里带有特殊字符,用 gbk 编码读取会崩或者产生乱码列。
解决:全程用正斜杠路径,比如 '../data/UNSW_NB15.csv',文件统一放到英文目录下。读取时指定 encoding='utf-8',实在不行加 engine='python' 兜底。data 目录里的 UNSW_NB15.zip 解压后建议重命名为 UNSW_NB15.csv 再读,避免文件名里带中文。
4.5 现象五:预测脚本报找不到模型权重文件
原因:训练脚本和预测脚本的路径约定不一致。比如训练时把权重存到了 cnn/res/ 下,预测脚本却默认去根目录找;或者训练中断过,权重文件根本没生成。还有一种情况是脚本里写了 .cuda(),但本地机器没有 GPU,代码没有判断 torch.cuda.is_available() 就直接崩了。
解决:先检查训练目录下有没有生成 .pt 文件,确认训练真的跑完。再看预测脚本里的路径是否和保存路径一致,不一致就改成相对路径。GPU 相关的代码改成条件判断:device = torch.device('cuda' if torch.cuda.is_available() else 'cpu'),然后模型和数据都 to(device),这样 CPU 和 GPU 都能跑。
5. 从二分类 demo 到自有多分类检测器:最后这一步怎么改
如果你已经跑通了 demo_2分类.py,下一步最值得做的事就是把二分类改成 UNSW-NB15 完整的多分类。原始数据集有十个类别,一个 Normal 加上 Fuzzers、Analysis、Backdoors、DoS、Exploits、Generic、Reconnaissance、Shellcode、Worms 九种攻击。多分类能让你看清楚哪些攻击容易互相混淆,这在答辩里比单个准确率更有说服力。
改成多分类只需要动三个地方。第一,标签编码保留 0 到 9 的完整映射,不要把所有攻击合并成 1;第二,模型输出层的 num_classes 从 2 改成 10;第三,评估部分用多分类版本的 classification_report。具体改动如下:
# 二分类:合并所有攻击为 1 df['label'] = (df['attack_cat'] != 'Normal').astype(int) # 多分类:保留完整类别映射 le = LabelEncoder() df['label'] = le.fit_transform(df['attack_cat']) # 此时 label 范围为 0~9,对应原始十个类别这两行代码的差别就是整个改造的核心。改完之后,把 CNN 和 LSTM 的输出层都调成 10,其余结构不用动。观察训练时的 loss 曲线,如果前几个 epoch 下降缓慢,说明类别间区分度比二分类低很多,这是正常的——Generic 和 Exploits 的特征重叠度本来就高。
验证技巧也很关键:多分类之后不要只看整体 acc,把混淆矩阵打印出来逐行看。Shellcode 和 Worms 样本少,如果被大面积误判成其他攻击,说明少数类的训练样本仍然不够,可以考虑对这两个类单独加大 SMOTE 的采样比例。我习惯把混淆矩阵存成图片,答辩时直接展示哪类攻击被谁混淆了,比一句"准确率 98%"有说服力得多。
我刚拿到这套源码时,在 4.3 那个坑里栽过跟头——第一次跑出 96% 的准确率兴奋了半天,点开混淆矩阵才发现攻击样本漏了一半。从那以后我每次复现流程都强制走一遍:先看数据均衡情况,再训练,最后先看混淆矩阵再看 acc。做二分类 demo 先跑通完整链路,再改多分类,这条路花的时间最少。希望帮到你。
本文还有配套的精品资源,点击获取