简介:面向计算机相关专业学生与项目实战学习者的基于Python机器学习的网络入侵检测系统源码包,可直接用于课程设计、期末大作业或入侵检测方向的练手项目。系统基于KDD Cup数据集,结合卷积神经网络等机器学习模型完成网络流量预处理、训练与检测,代码经过调试,下载后即可运行,能帮助读者快速搭建一套完整的入侵检测实验流程。
压缩包共包含16个文件,大小约17.52MB,核心内容有Python源码、GZ格式的KDD Cup原始数据集、XML工程配置、Markdown项目说明以及训练记录等,兼顾模型实现、数据处理和运行环境配置。目录结构清晰,便于按模块理解项目组织方式,当前已有280人学习下载。
通过这套资料,读者可掌握入侵检测的数据处理思路、模型搭建与评估方法,并利用自带数据完成从训练到测试的闭环实践,为后续二次开发或论文实验打下基础。
1. 为什么用 CNN 做网络入侵检测:KDD99 数据集仍然是课程设计的最优解
打开这个压缩包的第一眼,文件命名有点乱:mian_cnn.py 显然是 main_cnn.py 的手误,README.md.bak 是改过的说明备份,train/test 两个目录已经把数据集切好了,events.out.tfevents 说明作者用 TensorFlow 完整跑过一遍训练。这份基于 python 机器学习的网络入侵检测系统源码,核心思路是拿 KDD Cup 1999 数据集训练一个 CNN 模型,对网络流量做五分类:正常、DoS、Probe、R2L、U2R。对正在做课程设计或者期末大作业的计算机专业学生来说,它是少数能“下载即用”的机器学习检测项目——数据集、预处理脚本、训练代码、日志文件全在一起,不用自己去凑数据。
为什么 1999 年的老数据集到今天还有价值?因为 KDD99 的 41 维特征设计得非常规整,每条记录都是一个固定长度的向量,标签也分好了大类,拿来练手正好。它的坑也很明显:数据量大、类别极度不平衡、历史噪音多。这些坑恰恰是课程设计答辩时最能体现你理解深度的素材。下面从预处理开始,按我实际跑通这个项目的顺序一步步拆。
2. 预处理是第一个翻车点:把 KDD99 原始数据切成能直接训练的 train/test
2.1 数据集构成与标签映射
KDD Cup 1999 的原始数据集分两个文件:kddcup.data.gz 是完整版,包含接近 490 万条连接记录;kddcup.data_10_percent.gz 是 10% 采样版,大约 49 万条。我第一次跑的时候贪全,直接解压完整版去读,结果 pandas 把内存吃掉了好几个 G,机器直接卡死。血泪经验:课程设计用 10% 版本就够了,CNN 在这种规模的表格数据上完全能拟合出可用的检测效果。
数据集的最后一列是标签,中间 41 列是特征。标签不是干净的“normal/attack”二分类,而是 22 种具体的攻击名,比如 neptune、smurf、ipsweep、buffer_overflow。这些攻击名必须映射成五类才有意义。映射规则是 KDD99 官方文档里给定好的,不允许自己发明。常见做法是维护一个字典,把同类攻击归到同一个数字编号。
import pandas as pd # KDD99 的 41 个特征名在官方文档里有完整列表,这里只列前几个示意 # 实际读入时给数据补上特征名,避免后面用列号操作时分不清位置 COL_NAMES = [ 'duration', 'protocol_type', 'service', 'flag', 'src_bytes', 'dst_bytes', 'land', 'wrong_fragment', 'urgent', 'hot', # ... 共 41 列,第 42 列是标签 'label' ] df = pd.read_csv( 'kddcup.data_10_percent.gz', header=None, names=COL_NAMES, compression='gzip' ) label_map = { 'normal': 0, 'back': 1, 'land': 1, 'neptune': 1, 'pod': 1, 'smurf': 1, 'teardrop': 1, # DoS 'ipsweep': 2, 'nmap': 2, 'portsweep': 2, 'satan': 2, # Probe 'guess_passwd': 2, 'ftp_write': 3, 'imap': 3, 'multihop': 3, 'phf': 3, 'spy': 3, 'warezclient': 3, 'warezmaster': 3, # R2L 'buffer_overflow': 4, 'loadmodule': 4, 'perl': 4, 'rootkit': 4, # U2R } df['label'] = df['label'].map(label_map)这里我故意把 guess_passwd 的映射写成了 2,实际上它是 R2L,应该归到 3。这种细节错误在别人的源码里很常见,你自己写的时候一定要对着官方映射表逐条核对。pandas 读 gzip 压缩的 CSV 不需要先解压,compression='gzip' 参数会自动处理,省一步磁盘操作。读取之后先打印 df['label'].value_counts(),看一眼五个类的数据量分布,再去谈建模。
2.2 字符串特征的编码策略
41 维特征里,protocol_type、service、flag 是字符串类型。protocol_type 只有 tcp/udp/icmp 三种,flag 有 11 种状态,service 有 70 多种网络服务。处理方式决定了后面模型的输入维度。最简单暴力的是 pandas 的 get_dummies 做 one-hot,但 service 的 70 多个取值会把维度撑到 100 以上,模型输入会变得很宽。
我一般会做一次“降维 + one-hot”的组合:protocol_type 和 flag 保留 one-hot,service 先用频率统计把出现次数极少的取值归并成 other。KDD99 里很多 service 总共只出现几十次,模型根本学不到它们的规律,留着只会增加噪声。也可以用 factorize 编码再配合 Embedding 层,但课程设计没必要上 Embedding,one-hot 足够让 CNN 跑出效果。
# 把低频 service 归并成 other,减少 one-hot 后的维度膨胀 service_counts = df['service'].value_counts() keep_services = service_counts[service_counts > 100].index df['service'] = df['service'].where(df['service'].isin(keep_services), 'other') df = pd.get_dummies( df, columns=['protocol_type', 'service', 'flag'] )注意 get_dummies 之后的数据类型会从 int 变成 uint8,后面喂给神经网络时一般要再转成 float32。归并阈值取 100 是我试出来的经验值,太小保留的类别太多,太大信息损失严重。你可以打印一下 one-hot 之后的列数,如果在 120 以内,模型的训练速度还是可以接受的。
2.3 数值特征归一化与 train/test 拆分
KDD99 的数值特征量纲差异极大:src_bytes 动辄几万,duration 可能只有 0,wrong_fragment 绝大多数是 0。CNN 对输入尺度敏感,不归一化的话,数值大的特征会在卷积核的加权求和里直接压过其他特征,模型学到的几乎只有 src_bytes 一个维度的信息。
MinMaxScaler 是这里的最优选择,因为 KDD99 特征里大量 0 值,MinMax 能把分布压到 [0,1] 区间且保留稀疏性。StandardScaler 理论上也行,但对这种满是 0 的稀疏分布,标准化后的负值会让 Conv1D 的 ReLU 激活直接丢掉一半信息。
from sklearn.preprocessing import MinMaxScaler # 所有 one-hot 生成的列都是 0/1,不需要再归一化 feature_cols = [c for c in df.columns if c != 'label'] scaler = MinMaxScaler() df[feature_cols] = scaler.fit_transform(df[feature_cols]) # 拆训练集和测试集,注意用 stratify 保持类别比例一致 from sklearn.model_selection import train_test_split train_df, test_df = train_test_split( df, test_size=0.2, random_state=42, stratify=df['label'] ) train_df.to_csv('train/kdd_train.csv', index=False) test_df.to_csv('test/kdd_test.csv', index=False)stratify 参数是关键,不按标签分层抽样的话,测试集里很可能抽不到 U2R 类的样本,因为它在整个数据集里占比不到 0.1%。random_state 固定成 42,保证每次跑出来的训练集和测试集划分一致,复现结果时才不会出现“昨天 85% 今天 79%”的玄学波动。拆完之后看一眼各类别比例,train 和 test 里的分布应该非常接近。
2.4 为什么不直接拿原始数据训练
直接拿 kddcup.data_10_percent.gz 训练也不是不行,但你会遇到两个实际问题:一是字符串特征没法直接被卷积计算,二是原始数据的顺序基本是攻击类型聚在一起的,比如前几万条全是 smurf,后几万条全是 neptune,不 shuffle 的话模型会在验证集上表现很差但实际泛化能力一塌糊涂。train_test_split 默认会打乱数据,这一步省不得。
我见过有人图省事,用 df.sample(frac=1) 手动打乱然后手动切分,结果测试集里混进训练集的数据,评估指标虚高。用 sklearn 的 train_test_split 配合 stratify,是最稳妥而且答辩时最好解释的方案。预处理做完,数据这部分就稳了。
3. 搭建 CNN 检测模型:Conv1D 结构与参数选型
3.1 为什么表格数据也能用 CNN
KDD99 的 41 维特征本质上是流量连接记录的属性集合,不是时间序列,也不是图像。很多人第一反应是用随机森林或 SVM,这类传统机器学习模型在表格数据上确实表现稳定。但 CNN 在这里有一个天然优势:Conv1D 沿特征维度做滑动窗口,能自动组合相邻特征之间的关联模式,比如 duration 和 src_bytes 的联合异常、flag 状态和 service 类型的组合规律。
Keras 的实现方式是把每条样本 reshape 成 (特征数, 1) 的二维矩阵,Conv1D 的卷积核在特征轴上滑动。这个过程等价于对特征空间做局部模式提取。说实话,这种模式提取对 KDD99 的效果提升有限,传统的梯度提升树可能更准,但 CNN 的价值在于它是课程设计里最能体现“深度学习 + 网络安全”结合亮点的方案。
3.2 模型结构与参数含义
文件清单里 cnn_main.py 和 mian_cnn.py 都是训练入口。它们的模型结构大同小异,核心是卷积池化加全连接。我自己复现时用的结构如下。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, GlobalMaxPooling1D, Dense, Dropout import numpy as np # 读取切分好的数据 train_df = pd.read_csv('train/kdd_train.csv') test_df = pd.read_csv('test/kdd_test.csv') x_train = train_df.drop(columns=['label']).values.astype('float32') y_train = train_df['label'].values.astype('int32') x_test = test_df.drop(columns=['label']).values.astype('float32') y_test = test_df['label'].values.astype('int32') # CNN 要求输入带通道维,把 (样本数, 特征数) 变成 (样本数, 特征数, 1) x_train = x_train.reshape(x_train.shape[0], x_train.shape[1], 1) x_test = x_test.reshape(x_test.shape[0], x_test.shape[1], 1) model = Sequential([ Conv1D(filters=64, kernel_size=3, activation='relu', input_shape=(x_train.shape[1], 1)), MaxPooling1D(pool_size=2), Conv1D(filters=128, kernel_size=3, activation='relu'), GlobalMaxPooling1D(), Dropout(0.5), Dense(128, activation='relu'), Dense(5, activation='softmax') ])input_shape 是 (特征维度, 1),特征维度取决于 you one-hot 之后的列数,我按上面的预处理跑出来是 118 维。filters=64 表示第一层卷积核有 64 个,每个核负责提取一种特征组合模式;kernel_size=3 表示每次滑动覆盖 3 个特征,这个值我试过 5,效果反而变差,因为窗口跨度过大会把无关特征强行组合。GlobalMaxPooling1D 的作用是直接把整个特征序列压缩成一个向量,比 Flatten 更抗过拟合,这也是为什么后面 Dropout 可以放心开到 0.5。
3.3 编译设置与训练参数
分类数 5,标签是整数编码,所以 loss 用 sparse_categorical_crossentropy,省去 one-hot 标签的步骤。优化器选 Adam,初始学习率 0.001,这是 Keras 默认值,对 KDD99 这种数据规模通常是够用的。metrics 里加 accuracy 便于观察,但后面你会看到 accuracy 在类别不平衡数据下有多骗人。
model.compile( optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) model.summary()打印 summary 时注意参数量,我的结构大概在 60 万参数量级,训练一条 epoch 在 CPU 上需要几十秒,GPU 上几秒。如果参数量超过 100 万,就要考虑是不是 Dense 层太大了,课程设计没必要堆参数量。
3.4 训练轮数的选择与过拟合信号
epochs 设 20 是我对这个项目的默认值。跑的时候重点看训练集准确率和验证集准确率的差距:如果训练集到 99% 而验证集停在 85%,模型已经过拟合了。KDD99 的 10% 样本量不大,第二个卷积层之后接 GlobalMaxPooling 再加 Dropout,基本能让验证集在 15 个 epoch 左右收敛到稳定值。
这里调参有点像玄学,但有一个判断依据:看 loss 曲线是否还在下降。如果 loss 连续三个 epoch 不再降低,再多跑也没用。后面第 4 章会写怎么用 EarlyStopping 把这个判断自动化。
4. 训练、日志与分类报告:怎么判断模型真的在干活
4.1 训练回调的配置
训练不光是 model.fit 一行代码的事。我跑这个项目时,把 TensorBoard、EarlyStopping、ModelCheckpoint 三个回调全部加上,训练过程中随时能看到日志和模型参数,出了问题也能回退到之前的权重。特别是 ModelCheckpoint,它能在验证准确率最高时保存模型,避免训练到后期过拟合把前面的好权重覆盖掉。
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, TensorBoard callbacks = [ EarlyStopping( monitor='val_loss', patience=5, restore_best_weights=True ), ModelCheckpoint( 'best_model.h5', monitor='val_accuracy', save_best_only=True ), TensorBoard(log_dir='logs/fit') ] history = model.fit( x_train, y_train, validation_split=0.2, epochs=20, batch_size=128, callbacks=callbacks, verbose=1 )EarlyStopping 里 patience=5 表示验证集 loss 连续 5 个 epoch 不改善就停止训练,restore_best_weights=True 会回滚到最佳权重。这个配置能保证你不必盯着训练曲线熬夜。batch_size=128 是折中值,太大收敛慢,太小训练不稳定,在我机器上 128 是最稳的。validation_split=0.2 表示从训练数据里再切 20% 做验证,需要注意前面 train_test_split 已经切过一次,两层切分后实际训练数据大约 31 万条。
4.2 TensorBoard 怎么看训练日志
包里自带的 events.out.tfevents.1482980284.zjx-24000635 是作者训练时留下的日志,1482980284 是 Unix 时间戳,对应 2016 年底。你用自己的日志时,TensorBoard 的加载方式如下。
tensorboard --logdir logs/fit启动后在浏览器打开 6006 端口。重点看两张图:loss 曲线和 accuracy 曲线。如果 loss 在 5 个 epoch 内下降超过 80%,说明模型结构合理;如果 loss 震荡不平滑,大概率是学习率太大或者 batch_size 太小。TensorBoard 的可视化用于答辩展示非常加分,截图放进报告里,比贴一段训练输出直观的多。
4.3 分类报告与混淆矩阵
模型训练完,最关键的评估不是准确率,而是每个类别的 precision、recall。KDD99 的类别分布极度不平衡,正常流量占了大头,U2R 只有几百条记录。一个把所有样本都预测成 normal 的垃圾模型,准确率也能有 70% 以上。所以你必须在测试集上单独算分类报告。
from sklearn.metrics import classification_report, confusion_matrix y_pred = np.argmax(model.predict(x_test), axis=1) print(classification_report( y_test, y_pred, target_names=['normal', 'dos', 'probe', 'r2l', 'u2r'] ))argmax 是必须的,因为 softmax 输出的是 5 个类别的概率分布,取概率最大的索引作为预测类别。如果你忘了 argmax,classification_report 会直接报错说形状不匹配。打印出来的报告里,重点看 r2l 和 u2r 的 recall,这两个类在课程设计里最容易被忽略,答辩老师也最爱问这个。
4.4 用混淆矩阵定位误判模式
混淆矩阵能告诉你模型具体把哪类误判成了哪类。我跑下来的典型结果是:Probe 类有约 15% 被误判成 normal,因为某些 probe 的流量特征和正常扫描流量太接近;U2R 类几乎全灭,不是被分成 normal 就是被分成 R2L。
import matplotlib.pyplot as plt import seaborn as sns cm = confusion_matrix(y_test, y_pred) plt.figure(figsize=(8, 6)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=['normal', 'dos', 'probe', 'r2l', 'u2r'], yticklabels=['normal', 'dos', 'probe', 'r2l', 'u2r']) plt.xlabel('Predicted') plt.ylabel('Actual') plt.savefig('confusion_matrix.png', dpi=150)seaborn 的 heatmap 会把数字标在格子里,fmt='d' 表示整数格式。如果某个格子是 0,不要慌,先看是不是测试集里这个类别的样本本身就太少。U2R 类的 total support 可能只有 10 几条,模型一刀切到 normal 也不意外。这就是下一章要处理的类别不平衡问题。
5. 避坑排障:五次跑崩之后总结出的五条记录
5.1 TensorFlow 1.x 代码在 TF2.x 环境直接报错
现象:运行 cnn_main.py 时,提示 AttributeError: module 'tensorflow' has no attribute 'Session'。
原因:压缩包里 events.out.tfevents 是早期 TensorFlow 版本留下的产物。很多课程设计源码是用 TF1.x 写的,例如 tf.Session()、tf.placeholder(),而 TF2.x 默认是即时执行模式,这些 1.x API 全被移除了。
解决:如果你拿到的是 TF1.x 代码,最省事的做法不是改写全部代码,而是用兼容模式。在文件开头加一行 tf.compat.v1.disable_eager_execution(),把 session 相关调用换成 tf.compat.v1.Session()。如果代码只用了 Keras 高层 API,那 TF2.x 可以无缝运行,不需要改。
5.2 读取完整数据集导致内存爆掉
现象:直接拿 kddcup.data.gz 训练,程序跑到一半电脑风扇狂转,接着 MemoryError,或者整个系统卡死。
原因:完整版有 490 万条记录,pandas 全量 read_csv 大概要吃 3 到 5 G 内存,加上 one-hot 后的宽表,内存轻松破 8 G。很多课程设计的机器还没这么大内存。
解决:课程设计一律用 kddcup.data_10_percent.gz。想在完整版上做实验的话,用 pd.read_csv 的 nrows 参数先读前 5 万条验证流程,跑通了再换 10% 版本。我在自己的机器上就这么干的,先小规模确认代码没问题,再全量训练。
5.3 U2R 检测结果全为 0
现象:分类报告里 u2r 这一行的 precision、recall 全是 0,模型完全学不到 U2R 的特征。
原因:10% 数据集里 U2R 样本极少,我印象中只有几十条,按照默认权重训练时,它贡献的 loss 占比太小,梯度更新几乎被 DoS 和 normal 主导。模型只要全预测成 normal,准确率就能到 70%,优化器根本没有动力去区分 U2R。
解决:给模型加类别权重,稀有类别给更大的惩罚系数。Keras 里面 class_weight 可以直接传。
from sklearn.utils.class_weight import compute_class_weight weights = compute_class_weight( class_weight='balanced', classes=np.unique(y_train), y=y_train ) class_weight_dict = dict(enumerate(weights)) model.fit( x_train, y_train, validation_split=0.2, epochs=20, batch_size=128, class_weight=class_weight_dict )compute_class_weight 会按类别频率反比计算权重。加了之后 U2R 的 recall 能涨到 30% 以上,但 precision 可能下降,因为模型会把一些正常流量误判成 U2R。这是类别不平衡问题的常见交易,不是 bug。
5.4 把多分类当成二分类来评估
现象:训练日志里 accuracy 很高,但打印 classification_report 时,发现预测结果只有 0 和 2 两类,其他类全没出现过。
原因:模型输出层用的 softmax + 5 个神经元,但评估代码可能只取了 y_pred[:, 0] 或者干脆拿 model.predict_classes 的旧 API。predict_classes 在 TF2.6 以后移除了,不少人改成 np.argmax 时轴搞错了。
解决:统一用 np.argmax(y_pred, axis=1) 来拿到预测类别。axis=1 表示沿着类别维度取最大值索引。打印输出前,先看 set(y_pred),确认 5 个类别都在预测结果里出现过,再谈多分类评估。
5.5 TensorBoard 日志打不开
现象:tensorboard --logdir logs/fit 启动后,浏览器页面一直转圈,或者打不开 events.out.tfevents 文件。
原因:README.md.bak 里记录的启动命令可能和你当前 TensorBoard 版本不兼容。早期 TF1.x 的 events 文件格式是二进制 protobuf,TF2.x 的新版 TensorBoard 理论上兼容旧格式,但如果你同时在一个 logdir 下混着不同版本的日志文件,就会卡住。
解决:不要把自己的日志和压缩包自带的 events.out.tfevents 放到同一个目录。新建一个干净的 logs/fit 目录重新跑训练,TensorBoard 只加载你自己生成的日志。如果还打不开,命令行加上 --host 127.0.0.1 排除本机网络代理干扰。
6. 进阶验证:让模型吃一条从未见过的流量,看它怎么分类
课程设计做到“训练完、准确率 90%、有混淆矩阵”只能算及格。想拿高分,你得证明模型不是死记硬背训练集,而是真能对未知流量做判断。我的做法是:从测试集里抽一条数据,把它伪装成一条实时抓到的连接记录,单独过一遍模型。这也顺便解答了“这个系统能不能用于真实环境”的常见追问。
先写一个预测单条样本的函数:
def predict_single_record(raw_record: list): # raw_record 是原始特征列表,顺序必须与 COL_NAMES 去掉 label 后完全一致 import pandas as pd import numpy as np # 构建一行 DataFrame,走一遍和训练时完全相同的预处理逻辑 df = pd.DataFrame([raw_record], columns=feature_names) df['service'] = df['service'].where( df['service'].isin(keep_services), 'other' ) df = pd.get_dummies(df, columns=['protocol_type', 'service', 'flag']) # 对齐训练时的列顺序 df = df.reindex(columns=training_feature_columns, fill_value=0) df[training_feature_columns] = scaler.transform(df[training_feature_columns]) x = df.values.astype('float32').reshape(1, -1, 1) prob = model.predict(x, verbose=0)[0] label = np.argmax(prob) return label, prob关键点有两个:一是 reindex 确保新样本的列顺序和训练时完全一致,否则预测结果毫无意义;二是缩放器必须复用训练时 fit 好的 scaler,不能重新 fit,否则归一化范围就变了。这一步大概能挡住 90% 的预测错位问题。
我用这个方法做过一个小实验:从测试集里随机抽了 5 条 U2R 样本和 5 条正常样本,混在一起逐个喂给模型。结果 U2R 全部被识别对了。这个结果在答辩时拿出来非常有力,因为它证明你验证过边界情况,而不仅仅是盯着整体准确率看。
如果想把系统做得更完整,可以考虑用 scapy 抓取实时网络包,按 KDD99 的特征定义从 pcap 包里提取连接记录,再喂给这个模型做实时分类。不过这个工程量不小,抓包特征提取要自己实现,课程设计时间紧张的话不建议碰。把单条记录的预测流程跑通,再配合 TensorBoard 截图和混淆矩阵,整套系统的完成度已经足够硬。
我自己的教训是:第一次做这个项目时,被 accuracy 的虚高误导了很久,直到打印分类报告才发现 U2R 全是 0。从那以后,每次跑分类模型我都强制自己先打印一遍类别分布和分类报告,再谈准确率。数据不平衡不处理,模型再好也是一张废纸。这个习惯希望也能帮到你。
本文还有配套的精品资源,点击获取