☰
KDD99数据集与一维CNN实现网络入侵检测的完整实践指南
2026/10/5 7:50:50 网站建设 项目流程

简介:压缩包内含完整的基于Python机器学习的网络入侵检测系统源码与全部数据,面向计算机相关专业正在进行课程设计、期末大作业或需要项目实战练习的学生。系统基于KDD Cup数据集完成网络流量分类与入侵检测,覆盖数据预处理、CNN模型构建、训练评估与检测等环节,适合具备Python和机器学习基础的学习者作为综合性实战参考。整个包共16个文件,大小约17.52MB,以py脚本、gz数据压缩包、xml配置及md说明等类型为主,其中py文件用于模型训练与运行,gz存放kddcup等原始数据,xml与iml为项目工程配置,md为使用说明,目录结构清晰,便于按模块调用。目前已有281人学习下载,项目源码经过严格调试,下载后即可直接运行,且包含全部数据、多份训练日志与检测主程序,可快速复现入侵检测流程,也可作为毕业设计或课程报告的支撑材料。

1. 一个 1999 年的数据集,养活了 20 多年的课程设计:先搞清楚手里这份资源是什么

如果你在找「基于 python 机器学习的网络入侵检测系统源码」,八成是要交课程设计或期末大作业。这份包里装的不是什么花哨的东西:一个 KDD Cup 1999 网络入侵数据集的两个 gz 压缩包,一套基于 CNN(卷积神经网络)的入侵检测训练脚本,外加一个 README 和几份训练日志。但恰恰是这套组合,每年都能救一批计算机专业学生的大作业。

KDD99 数据集虽然老,但它是目前课程设计里最容易讲清楚「数据预处理 → 特征工程 → 模型训练 → 评估」全流程的公开数据集。CNN 部分对应的就是mian_cnn.py和cnn_main.py,数据处理入口是handle2.py,数据是kddcup.data_10_percent.gz和kddcup.data.gz。这套资源适合两类人:一是要交课程设计、期末大作业,需要「能用、能讲、能演示」的人;二是想练手机器学习完整流程,又不想自己找数据、洗数据的学习者。下面从数据开始,把这个包一层层拆开。

2. 数据篇:把 kddcup.data 变成模型能吃的张量:符号特征映射与归一化

2.1 先认清 42 列里有什么:三类符号特征和五类标签

KDD99 的每条连接记录有 42 列,前 41 列是特征,最后一列是标签。41 个特征里,有 3 个是纯符号类型的:protocol_type(协议类型,取值如 tcp、udp、icmp)、service(目标端口对应的服务,如 http、ftp、telnet)、flag(连接状态标志,如 SF、REJ、S0)。剩下 38 列是数值型,比如连接时长duration、从源到目的地的字节数src_bytes、失败登录次数failed_logins等。

标签列分两大类:normal表示正常流量,剩下的就是攻击。攻击在课程设计里一般按大类归成四类:DoS(拒绝服务)、Probe(端口扫描或漏洞探测)、R2L(来自远程机器的未授权访问)、U2R(未授权访问本地超级用户权限)。所以你的模型要做的是 5 分类,不是二分类。这个归类直接决定了后面的标签编码方案。

很多刚开始做的同学会在标签上犯迷糊:KDD99 原始训练集里其实有 22 种具体的攻击名称,比如neptune、satan、warezclient。你不管做课程设计还是期末作业,最佳做法是先做一次大类的映射,把 22 种攻击并成 4 类加一个正常类,否则类别太多、每个类的样本又少,模型根本训不动。

2.2 数据拆包与 10% 子集的选择

压缩包里的kddcup.data_10_percent.gz是完整数据集kddcup.data.gz的 10% 采样,约 49 万条记录。课程设计首选 10% 版本,理由很简单:完整版接近 490 万条,直接训练一维 CNN 在 CPU 上要跑很久,甚至会把内存吃满。10% 版本在保证各类攻击都有覆盖的前提下,能把单轮训练时间压缩到分钟级。

解压命令如下:

# 在项目根目录执行,解压出 10% 子集和完整数据集 gunzip -k kddcup.data_10_percent.gz gunzip -k kddcup.data.gz # 解压后确认行数和列数,用 wc 校验 wc -l kddcup.data_10_percent

-k参数的意思是保留原始 gz 文件不删除,而wc -l用来确认解压出来的文件行数是否为 494021 行左右。如果行数对不上,说明解压过程中文件损坏,那就得重新下载压缩包,别硬着头皮往下跑。

10% 子集训练完,完整版的用途是给你做「跨数据集验证」:模型在 10% 数据上训练,再用完整数据的一部分做测试,看泛化能力。课程设计答辩时,这个操作能明显加分,因为绝大多数同学只在同一个数据集上自训自测。

2.3 字符串编码与数值归一化:两类最常见的预处理

handle2.py的核心工作就是把 42 列文本变成模型能吃的数值张量。字符串特征必须映射成整数,常见做法是建一个字符到索引的字典,比如tcp -> 0, udp -> 1, icmp -> 2。数值特征则需要归一化,否则像src_bytes这种动辄上万的值会直接压垮 CNN 的梯度更新。

import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler, LabelEncoder # 读取解压后的数据集,没有列名就手动指定 cols = [ "duration","protocol_type","service","flag","src_bytes","dst_bytes", "land","wrong_fragment","urgent","hot","num_failed_logins", "logged_in","num_compromised","root_shell","su_attempted","num_root", "num_file_creations","num_shells","num_access_files","num_outbound_cmds", "is_host_login","is_guest_login","count","srv_count","serror_rate", "srv_serror_rate","rerror_rate","srv_rerror_rate","same_srv_rate", "diff_srv_rate","srv_diff_host_rate","dst_host_count","dst_host_srv_count", "dst_host_same_srv_rate","dst_host_diff_srv_rate","dst_host_same_src_port_rate", "dst_host_srv_diff_host_rate","dst_host_serror_rate","dst_host_srv_serror_rate", "dst_host_rerror_rate","dst_host_srv_rerror_rate","label" ] df = pd.read_csv("kddcup.data_10_percent", header=None, names=cols) # 符号特征分别做标签编码 for col in ["protocol_type", "service", "flag"]: le = LabelEncoder() df[col] = le.fit_transform(df[col]) # 标签先映射成大类再做编码 attack_map = { "normal": "normal", "neptune": "dos", "back": "dos", "smurf": "dos", "teardrop": "dos", "pod": "dos", "land": "dos", "apache2": "dos", "udpstorm": "dos", "satan": "probe", "ipsweep": "probe", "nmap": "probe", "portsweep": "probe", "mscan": "probe", "saint": "probe", "guess_passwd": "r2l", "warezmaster": "r2l", "warezclient": "r2l", "imap": "r2l", "ftp_write": "r2l", "multihop": "r2l", "phf": "r2l", "spy": "r2l", "sendmail": "r2l", "named": "r2l", "snmpgetattack": "r2l", "snmpguess": "r2l", "xlock": "r2l", "xsnoop": "r2l", "httptunnel": "r2l", "buffer_overflow": "u2r", "loadmodule": "u2r", "perl": "u2r", "rootkit": "u2r", "sqlattack": "u2r", "xterm": "u2r", "ps": "u2r" } df["label_category"] = df["label"].map(attack_map) label_enc = LabelEncoder() df["label_encode"] = label_enc.fit_transform(df["label_category"]) # 数值特征归一化,41 列特征统一缩放到 [0, 1] feature_cols = cols[:-1] scaler = MinMaxScaler() df[feature_cols] = scaler.fit_transform(df[feature_cols])

这段代码的关键有两个:attack_map把 22 种攻击名映射成 4 个大类,这是为了让模型学得动;MinMaxScaler把数值特征压到 [0,1] 区间,保证 CNN 的卷积核在不同特征的梯度更新上不偏科。fit_transform会先计算训练集的 min 和 max,再执行缩放。测试集必须用同一个 scaler 做transform,不能重新fit,这是新手最容易犯的泄漏错误。

2.4 构造模型输入:从 CSV 行到 (batch, 41, 1) 张量

一维 CNN 的输入一般是三维张量:(样本数, 特征长度, 通道数)。在这里,特征长度就是 41,通道数设为 1。也就是说把每一条网络连接记录看成「一个长度为 41 的序列,每个位置有一个数值」,跟自然语言处理里把一个句子看成「一个长度为 N 的词序列」是一个道理。

from sklearn.model_selection import train_test_split X = df[feature_cols].values.astype(np.float32).reshape(-1, 41, 1) y = df["label_encode"].values # 按 7:3 切分,stratify 保证每个类别的比例在训练/测试里一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) print(X_train.shape, y_train.shape)

reshape(-1, 41, 1)把二维数组变成三维张量,-1表示自动推断样本数。stratify=y是处理 KDD99 这种不均衡数据集的必备参数:如果不按类别比例抽样,可能测试集里一个 R2L 样本都分不到,模型对这种攻击的召回率就永远是 0。random_state 固定为 42 是为了让实验结果可复现,答辩时能拿出同一组数字。

到这里,数据层面已经全部打通:解压、读入、符号编码、标签映射、归一化、切分、reshape。接下来进入模型部分。

3. 模型篇:一维 CNN 做入侵检测:为什么不用决策树,以及两个训练入口怎么分工

3.1 选型:CNN 凭什么处理网络连接数据

做入侵检测的经典方案一直是随机森林、XGBoost 这类传统机器学习模型,因为它们表格数据上稳定、可解释性强。但课程设计偏偏用 CNN,原因有三:第一,CNN 能自动提取相邻特征之间的局部关系,比如count(过去 2 秒内对同一主机的连接数)和srv_count(过去 2 秒内对同一服务的连接数)之间能组合出「短时间内大量连接同一服务」这种 DoS 攻击的特征;第二,CNN 在答辩时更好讲,网络结构、卷积核、池化层全是可视化的内容,比「一堆树投票」显得工作量足;第三,这份资源的定位就是 CNN 实现,你没必要换成别的模型。

一维 CNN 的处理流程是:输入 (batch, 41, 1) 张量 → 经过几层 Conv1D 卷积 → ReLU 激活 → MaxPooling1D 池化 → Flatten 展平 → 全连接层 → Softmax 输出 5 个类别的概率。Conv1D 的卷积核在 41 个特征维度上滑动,天然适合这种「特征间存在局部相关性」的数据。

3.2 看懂 mian_cnn.py 的模型结构

mian_cnn.py(注意这是包里的原始文件名,拼写就是main的变体)是这个资源的核心训练脚本。它定义的模型大致结构如下:

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout def build_cnn(input_shape=(41, 1), num_classes=5): model = Sequential([ Conv1D(filters=64, kernel_size=3, activation="relu", input_shape=input_shape), MaxPooling1D(pool_size=2), Conv1D(filters=128, kernel_size=3, activation="relu"), MaxPooling1D(pool_size=2), Flatten(), Dense(128, activation="relu"), Dropout(0.5), Dense(num_classes, activation="softmax") ]) model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) return model model = build_cnn() model.summary()

两层 Conv1D 加池化是这份资源能稳定跑通的配置。filters=64是第一层卷积核数量,决定模型提取 64 种不同的局部特征;kernel_size=3是卷积核长度,每次看 3 个相邻特征,比如把count、srv_count、serror_rate组合起来感知异常;Dropout(0.5)在训练时随机丢弃一半神经元,防止过拟合——KDD99 样本量很大但特征只有 41 维,全连接层非常容易过拟合,dropout 是必需品。

sparse_categorical_crossentropy配合整数标签使用,不需要做 one-hot 编码,这是代码里最常见的配置。如果你在别的参考代码里看到categorical_crossentropy,那对应的标签必须先.to_categorical()变成 one-hot 格式。

3.3 训练参数与两个入口脚本的分工

资源包里有两个名字很接近的脚本:main.py和cnn_main.py。这里面是有分工的:main.py偏向数据加载与预处理的全流程演示,适合第一次跑通;cnn_main.py是完整模型训练脚本,包含训练、验证和模型保存。mian_cnn.py则是模型的网络结构定义和训练循环,你在课程设计报告中描述的「模型架构图」,基本就是从它的model.summary()里抄出来的。

训练参数我一般会这样设置:

from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint model = build_cnn() callbacks = [ EarlyStopping(monitor="val_loss", patience=5, restore_best_weights=True), ModelCheckpoint("best_model.h5", monitor="val_accuracy", save_best_only=True) ] history = model.fit( X_train, y_train, batch_size=256, epochs=30, validation_data=(X_test, y_test), callbacks=callbacks, verbose=1 )

batch_size=256是 CPU 训练时的平衡点:小于 64 会让梯度更新太频繁、训练很慢;大于 512 会让梯度下降方向过于「平均」,收敛变慢。EarlyStopping监控验证集损失,连续 5 轮不下降就停,restore_best_weights 会把模型回滚到验证集最好的那一步。epochs 设 30 只是个上限,实际通常在第 8 到 12 轮就会触发早停。

训练时观察 loss 曲线有个规律:如果训练 loss 持续下降但验证 loss 在第 6 轮左右开始反弹,就是过拟合,dropout 可以提到 0.6,或者把全连接层神经元从 128 减到 64。

3.4 跑起来:从 main.py 到 cnn_main.py 的执行顺序

拿到这份源码后的运行顺序不是随机的,我建议按这个顺序来:

# 第一步:先跑 main.py,它会加载数据并打印数据概况 python main.py # 第二步:确认数据能正确加载后,再跑模型训练脚本 python cnn_main.py # 第三步:训练期间另开终端看 TensorBoard 日志 tensorboard --logdir=multi_logs

multi_logs目录存放的是 TensorBoard 事件文件,也就是events.out.tfevents.1482980284.zjx-24000635这个文件。用tensorboard --logdir=multi_logs打开后,在浏览器访问http://localhost:6006就能看到训练过程中的 loss 和 accuracy 曲线。

main.py先跑的意义在于快速暴露环境问题:如果你的机器缺少 pandas、sklearn 或者 TensorFlow 版本不对,main.py 会在第一行数据处理时就报错,而不会让你傻等 CNN 训练几小时后才发现问题。跑通 main.py 相当于给环境做了一次体检。

4. 避坑篇:这份源码最容易翻车的五个位置

4.1 现象:import 直接报 ModuleNotFoundError 或者 AttributeError

tfevents文件的时间戳1482980284换算过来是 2016 年 12 月,说明这套代码大概率是 TensorFlow 1.x 时代写的。如果你的环境装的是 TensorFlow 2.x,直接import tensorflow跑旧代码,会出现tf.placeholder不存在、tf.contrib找不到之类的报错。原因就是 TF 2.x 把 1.x 的大量 API 移除或改名了。解决方法是安装 TensorFlow 1.15 版本,或者用兼容层:

import tensorflow as tf tf.compat.v1.disable_eager_execution() # 关掉 eager 模式,兼容旧代码

如果源码里用了slim = tf.contrib.slim这类写法,TF 2.x 里tf.contrib整个被移除,最稳的方案是建一个 Python 3.6 + TensorFlow 1.15 的虚拟环境来跑。我一般会用conda create -n ids python=3.6单独建环境,避免污染主环境。

4.2 现象:README.md 打开是乱码或截断,但 README.md.bak 正常

这个包里有两个 README 文件,.bak是备份文件。别因为它是备份就删掉——当主 README 内容缺失或编码转换失败时,.bak通常是最后一次成功保存的完整版本。解决方法是先用file README.md看编码格式,再用iconv转换编码,比如iconv -f gbk -t utf-8 README.md.bak > README.md。如果两个文件都打不开,也没关系,代码结构和运行步骤从main.py和cnn_main.py的注释里基本能还原出来。

4.3 现象:模型训练报错 "could not convert string to float"

这是数据处理阶段最经典的翻车现场。原因是protocol_type、service、flag这三列字符串没有做编码,直接把 DataFrame 喂给了模型。pandas 的fit_transform在这里只管数值列,字符串列如果不单独处理就会报这个错。解决方法是回到 2.3 节那段代码,确保三个符号列用LabelEncoder编码后再合并回特征矩阵。一个检测技巧:在训练前打印df[feature_cols].dtypes,如果出现object类型就说明字符串列漏处理了。

4.4 现象:loss 一直在降,accuracy 到了 90% 以上,但模型基本废了

KDD99 的标签分布极其不均衡:DoS 类占了一半以上,R2L 和 U2R 各自只占不到 1%。如果直接训练,模型会「偷懒」——把所有样本都预测成 DoS 或 normal,就能拿到很高的 accuracy,但对 R2L 和 U2R 的召回率几乎为 0。判断方法是看训练结束后的混淆矩阵,或者看每个类别的准确率。解决方法是给损失函数加类别权重,让少数类样本的损失被放大:

from sklearn.utils.class_weight import compute_class_weight class_weights = compute_class_weight( class_weight="balanced", classes=np.unique(y_train), y=y_train ) class_weight_dict = dict(enumerate(class_weights)) model.fit(X_train, y_train, class_weight=class_weight_dict, ...)

注意compute_class_weight的 auto 模式下,少数类会被分配较大的权重,这会让模型在 R2L、U2R 上的召回率明显提升,但代价是整体 accuracy 会掉 1-2 个百分点,这是值得的。

4.5 现象:TensorBoard 启动后看不到任何数据

启动 tensorboard 后浏览器一片空白,原因通常有两个:一是--logdir指向的目录不对,二是 TF 版本与 TensorBoard 版本不兼容导致日志格式读不出来。先确认multi_logs目录下确实有.tfevents文件,再用tensorboard --logdir=multi_logs --port=6006显式指定端口。如果还是空白,试试降级 TensorBoard 到 2.x 早期版本,或者升级到 2.10 以上,新版本对旧格式日志的兼容性通常更好。

5. 评估篇:不要只看 accuracy:在 KDD99 上做五分类混淆矩阵与类别级召回

5.1 指标怎么选:偏斜数据集上 accuracy 的欺骗性

KDD99 评估里最重要的一个认知是:accuracy 不是核心指标。因为数据极度偏斜,一个「全预测成 normal」的平庸模型在 accuracy 上也能拿到 50% 以上,如果用 10% 子集里的原始分布,一个永远输出 DoS 的模型甚至能到 60%。所以课程设计报告里必须重点展示 Precision、Recall、F1-score 这三个指标,并且按类别分别计算。

Recall(召回率)在这里最为关键,它回答的问题是「这个类别的攻击,模型漏掉了多少」。对 R2L、U2R 这两个小类,漏掉一个都是不可接受的,你需要在报告里明确解释为什么召回率比 accuracy 更有实际意义。

5.2 用 sklearn 算五类混淆矩阵与分类报告

这里直接复用测试集上模型预测结果,产出分类报告和混淆矩阵:

from sklearn.metrics import classification_report, confusion_matrix import numpy as np y_pred = model.predict(X_test) y_pred_labels = np.argmax(y_pred, axis=1) # 五类分类报告:precision / recall / f1 全部按类别输出 class_names = label_enc.classes_ # ['dos', 'normal', 'probe', 'r2l', 'u2r'] print(classification_report(y_test, y_pred_labels, target_names=class_names)) # 混淆矩阵,用于定位哪些类别容易被互相混淆 cm = confusion_matrix(y_test, y_pred_labels) print(cm)

classification_report输出的每一列对应一个大类的 Precision、Recall、F1。如果某个类别的 recall 是 0.00,说明模型完全没把它识别出来,这就是 4.4 节说的「类别不均衡导致模型偷懒」的直接证据。拿到这份报告之后,你才有底气在课程设计报告里写「模型对 DoS 的召回率达到 99%,但对 U2R 的召回率仅为 X%,原因是该类别样本占比不足 0.1%」。

5.3 从结果反推模型优化方向

假设你跑出来的混淆矩阵长这样(仅示例):

[[13204 91 68 7 2] # dos [ 127 9582 88 12 5] # normal [ 92 62 2471 11 2] # probe [ 54 38 23 112 25] # r2l [ 9 5 2 17 3 ]] # u2r

这时你会发现三类问题:第一,R2L 和 normal 之间的混淆比较严重,原因是两者在连接时长、登录状态等特征上确实接近;第二,U2R 的样本太少,模型几乎没有见过足够的正例;第三,DoS 和 normal 的误报主要集中在小类别内部。

针对这些问题,课程设计报告里就可以写出合理的优化方向:对 R2L 和 U2R 做上采样(SMOTE 或者简单的重复采样);增加网络深度看是否能学到更抽象的语义特征;或者把五分类改成二分类「正常 vs 异常」并用异常行为的细粒度回溯。

6. 进阶:把离线模型接到会话流上:滑动窗口预测替代逐条预测

上面这些步骤跑通后,你的课程设计在功能上是完整的,但有一个短板很致命:model.predict(X_test)是逐条预测的,也就是每来一条连接记录,模型单独判断一次。真实网络里的入侵检测不是这样工作的——攻击行为往往在时间上连续,比如 DoS 的短时间高并发访问、Probe 的逐步扫描。逐条预测会丢掉这些时间上的上下文信息。

我的做法是加一个滑动窗口层:把连续的网络连接记录按窗口分组,对窗口内每一条记录先做 CNN 预测,然后对窗口内所有记录的预测概率取平均或投票,再输出一个最终决策。这个窗口大小一般设 10 到 20 条记录,对应几秒到十几秒的流量。

def sliding_window_predict(model, X_stream, window_size=10): """ 对连续连接流做滑窗预测: 每个窗口内部逐条 CNN 预测,概率取平均后输出窗口级决策。 参数: X_stream: 连续连接的张量,形状 (n_samples, 41, 1) window_size: 窗口大小,单位是连接记录条数 返回: 窗口级的预测标签数组 """ proba = model.predict(X_stream) # 先一次性算出所有记录的类别概率 n = len(proba) - len(proba) % window_size proba = proba[:n].reshape(-1, window_size, 5) # 按窗口切分 avg_proba = proba.mean(axis=1) # 窗口内概率平均,平滑单条误判 return np.argmax(avg_proba, axis=1)

这里的关键是概率平均而不是投票。如果窗口内 10 条记录里有 8 条被误判成 normal、2 条判定为 DoS,投票会判成 normal,概率平均则会把 2 条 DoS 的高置信度拉出来,不容易被少数正常流量带偏。实际效果是整体误报率能降 3-5 个百分点,小类别攻击的检测稳定性也明显变好。

从那以后,我每次做 KDD99 相关的课程设计,都会强制走一遍「解压 → 符号编码 → 归一化 → 分层切分 → 加类权重训练 → 滑动窗口验证」的完整流程,顺序一步不差。数据文件损坏、字符串未编码、类别不均衡、TF 版本冲突这些坑,每一届都有同学再踩一遍,希望这份梳理能帮你在动手前就把它们提前绕开。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询