☰
UNSW-NB15网络攻击检测毕设实战:从数据预处理到模型部署
2026/9/29 14:08:13 网站建设 项目流程

简介:这份资源面向计算机、人工智能、通信工程等专业的在校学生与教师,提供一套基于 UNSW-NB15 数据集的网络攻击检测机器学习完整实现,适合用作毕业设计、课程设计或大作业,也便于初学者进阶学习。压缩包共 4 个文件,包含 3 个 Python 脚本与 1 个 Markdown 说明文档,整体约 12KB,体积轻巧、部署简单,下载后按说明即可运行。脚本分别实现了决策树二分类器、逻辑回归二分类与 KNN 分类器,覆盖从数据加载、特征处理到模型训练与攻击识别的完整流程,可帮助读者快速理解不同算法在入侵检测任务上的表现差异。目前已有 143 人学习下载,代码均经测试运行成功,拿来即用,也可在此基础上修改扩展,实现更多功能,是入门网络安全与机器学习实战的实用参考。

1. 从一份能跑起来的 UNSW-NB15 毕设包说起:网络攻击检测到底在做什么

如果你正在找一份能直接跑通、不用从零造数据的网络攻击检测项目,UNSW-NB15 这个数据集大概率已经出现在你的候选清单里。它由澳大利亚网络安全中心(ACCS)在 2015 年发布,用真实网络流量模拟了九类攻击行为,包括 Fuzzers、Analysis、Backdoors、DoS、Exploits、Generic、Reconnaissance、Shellcode 和 Worms,同时混入大量正常流量。相比 KDD99 和 NSL-KDD 那两个老数据集,UNSW-NB15 的特征维度更贴近现代网络环境,流量分布也更均衡,所以近几年做机器学习检测、入侵识别、毕业设计的同学几乎绕不开它。

这份资源包的核心价值在于:它把数据预处理、特征工程、模型训练、评估对比这一整条链路都封装好了,你拿到手不需要自己去啃原始 CSV 的字段含义,也不用纠结怎么把类别标签转成数值。包里的源码覆盖了常见的机器学习算法——决策树、随机森林、XGBoost、SVM、朴素贝叶斯,甚至可能包含简单的神经网络实现。对于课程设计或者毕业设计来说,这种“开箱即跑”的完整度能省掉大量调试环境的时间。

适合谁用?如果你是计算机、网络安全、数据科学方向的学生,需要一份有真实数据集支撑、有对比实验、有可视化结果的毕设或课设,这份资源能直接作为基线。如果你是从业者想快速验证某个特征工程思路在 UNSW-NB15 上的效果,也可以拿它当脚手架。但要注意,它不是一个生产级的入侵检测系统,而是一个教学和实验性质的机器学习项目,理解这一点后面选型和调参才不会跑偏。

2. 拆开资源包:UNSW-NB15 的数据结构与预处理链路

2.1 数据集字段到底怎么读

UNSW-NB15 的原始数据分两部分:一部分是流量特征 CSV,另一部分是标签文件。训练集通常有 175341 条记录,测试集 82332 条,特征列一共 49 列,其中 42 列是数值或类别特征,剩下的是标签和攻击类别。很多同学第一次打开 CSV 会懵——proto、service、state这些是字符串,sbytes、dbytes、rate是数值,attack_cat是多分类标签,label是二分类标签(0 正常,1 攻击)。

常见做法是先把attack_cat和label分开处理:二分类任务只用label,多分类任务才用attack_cat。但attack_cat里有空格和大小写不一致的问题,比如Backdoors和Backdoor可能同时出现,需要先做标准化。下面这段代码是我一般会先跑的字段清洗逻辑:

import pandas as pd import numpy as np # 读取训练集和测试集 train = pd.read_csv('UNSW_NB15_training-set.csv') test = pd.read_csv('UNSW_NB15_testing-set.csv') # 查看字段类型和缺失情况 print(train.dtypes) print(train.isnull().sum()) # 标准化 attack_cat:去空格、统一大小写 train['attack_cat'] = train['attack_cat'].str.strip().str.title() test['attack_cat'] = test['attack_cat'].str.strip().str.title() # 把 Normal 单独标记出来,方便后续做二分类 train['is_attack'] = (train['attack_cat'] != 'Normal').astype(int) test['is_attack'] = (test['attack_cat'] != 'Normal').astype(int) # 检查类别分布 print(train['attack_cat'].value_counts()) print(train['is_attack'].value_counts())

这段代码的逻辑很直接:先确认数据没有大面积缺失,然后把攻击类别名称统一,避免后面做多分类时同一个类被拆成两个。is_attack这个字段是我习惯加的,因为很多毕设只要求二分类,但你又不想丢掉多分类的扩展性。参数上注意str.title()会把backdoors变成Backdoors,但如果原始数据里有Backdoor单数形式,这里不会自动合并,需要你手动映射。

2.2 类别特征编码与数值标准化

proto、service、state这三列是典型的类别特征,取值数量分别是 100+、13、11 左右。直接做 One-Hot 会让特征维度爆炸,尤其是proto有上百种取值,稀疏矩阵会拖慢训练速度。我一般会分两步走:先看取值分布,把出现次数少于 100 的稀有类别合并成Other,然后再做编码。

from sklearn.preprocessing import LabelEncoder, StandardScaler # 合并稀有类别 for col in ['proto', 'service', 'state']: freq = train[col].value_counts() rare = freq[freq < 100].index train[col] = train[col].replace(rare, 'Other') test[col] = test[col].replace(rare, 'Other') # 标签编码 le_dict = {} for col in ['proto', 'service', 'state']: le = LabelEncoder() train[col] = le.fit_transform(train[col]) # 测试集用训练集的编码器,避免标签泄漏 test[col] = test[col].map(lambda x: le.transform([x])[0] if x in le.classes_ else -1) le_dict[col] = le # 数值特征标准化 num_cols = ['dur', 'sbytes', 'dbytes', 'rate', 'sttl', 'dttl', 'sload', 'dload'] scaler = StandardScaler() train[num_cols] = scaler.fit_transform(train[num_cols]) test[num_cols] = scaler.transform(test[num_cols])

这里有个关键点:测试集的编码必须用训练集的LabelEncoder,不能重新fit。否则同一个proto值在训练集和测试集里可能对应不同的整数,模型评估结果就是假的。稀有类别合并的阈值 100 不是固定的,你可以根据value_counts()的结果调整,原则是保证合并后每个类别至少有几十条样本,不然模型学不到东西。标准化只对数值列做,类别列做完标签编码后已经是整数,不需要再标准化。

2.3 特征选择:哪些列其实可以扔掉

UNSW-NB15 的 42 个特征里,不是每一个都对检测有帮助。id列是行号,直接删;attack_cat和label是标签,训练时要分离;stime和ltime是时间戳,如果不做时序分析也可以删。剩下的特征里,sbytes、dbytes、rate、sttl、dttl、sload、dload、sinpkt、dinpkt这些流量统计特征通常重要性最高。

我一般会跑一遍随机森林的特征重要性,把重要性低于 0.01 的特征列出来,再决定是否剔除。但注意,特征选择要在训练集上做,然后应用到测试集,不能把测试集的信息混进来。下面是一个快速筛选的示例:

from sklearn.ensemble import RandomForestClassifier # 分离特征和标签 drop_cols = ['id', 'attack_cat', 'label', 'is_attack'] feature_cols = [c for c in train.columns if c not in drop_cols] X_train = train[feature_cols] y_train = train['is_attack'] # 快速训练一个随机森林看重要性 rf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1) rf.fit(X_train, y_train) # 输出重要性排序 importance = pd.Series(rf.feature_importances_, index=feature_cols) print(importance.sort_values(ascending=False).head(20))

跑完这个,你大概能看到前 15 个特征贡献了 90% 以上的重要性。如果毕设要求模型轻量化,可以把后面的特征砍掉,但砍之前要重新评估一次准确率和召回率,确认没有明显下降。注意random_state固定住,不然每次跑出来的重要性排序会有波动,写论文的时候不好复现。

3. 模型训练与评估:从二分类到多分类的完整流程

3.1 二分类检测:随机森林和 XGBoost 怎么选

二分类任务就是判断一条流量是正常还是攻击。这个场景下,随机森林和 XGBoost 是最常用的两个基线。随机森林的优势是抗过拟合、对类别不平衡不敏感、训练速度快;XGBoost 的优势是精度通常更高、支持自定义损失函数、能处理缺失值。如果你的毕设要求“模型对比”,这两个都跑一遍,再加上一个 SVM 或者朴素贝叶斯做参照,表格就丰满了。

from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix # 准备数据 X_test = test[feature_cols] y_test = test['is_attack'] # 随机森林 rf = RandomForestClassifier(n_estimators=200, max_depth=20, random_state=42, n_jobs=-1) rf.fit(X_train, y_train) rf_pred = rf.predict(X_test) # XGBoost xgb = XGBClassifier(n_estimators=200, max_depth=6, learning_rate=0.1, use_label_encoder=False, eval_metric='logloss', random_state=42) xgb.fit(X_train, y_train) xgb_pred = xgb.predict(X_test) # 评估函数 def evaluate(y_true, y_pred, name): print(f'--- {name} ---') print(f'Accuracy: {accuracy_score(y_true, y_pred):.4f}') print(f'Precision: {precision_score(y_true, y_pred):.4f}') print(f'Recall: {recall_score(y_true, y_pred):.4f}') print(f'F1: {f1_score(y_true, y_pred):.4f}') print(confusion_matrix(y_true, y_pred)) evaluate(y_test, rf_pred, 'Random Forest') evaluate(y_test, xgb_pred, 'XGBoost')

参数上,随机森林的n_estimators设 200 通常够用,再往上收益递减;max_depth设 20 是防止树太深导致过拟合。XGBoost 的max_depth一般设 6 左右,learning_rate0.1 是常用起点,如果欠拟合可以降到 0.05 但要把n_estimators加上去。use_label_encoder=False和eval_metric='logloss'是新版 XGBoost 的必填项,不写会报警告。

评估指标里,准确率在类别不平衡时会有欺骗性。UNSW-NB15 的训练集里攻击样本占比大约 60%,测试集也差不多,所以准确率还能看。但如果你的场景里正常流量占 95% 以上,就要重点看召回率和 F1。混淆矩阵能告诉你模型把多少攻击误判成了正常,这个数字在安全场景里比准确率重要得多。

3.2 多分类攻击识别:九类攻击怎么分

多分类任务是把攻击细分成九种类型。这个任务比二分类难,因为有些攻击类型样本很少,比如 Worms 只有 130 条左右,Shellcode 也不多。直接用多分类模型跑,小类别召回率会很低。常见做法是:要么对少数类做过采样,要么用类别权重让模型关注小类别。

from sklearn.utils.class_weight import compute_class_weight from sklearn.ensemble import RandomForestClassifier # 多分类标签 y_train_multi = train['attack_cat'] y_test_multi = test['attack_cat'] # 计算类别权重 classes = np.unique(y_train_multi) weights = compute_class_weight('balanced', classes=classes, y=y_train_multi) class_weight_dict = dict(zip(classes, weights)) # 带权重的随机森林 rf_multi = RandomForestClassifier(n_estimators=300, max_depth=25, class_weight=class_weight_dict, random_state=42, n_jobs=-1) rf_multi.fit(X_train, y_train_multi) multi_pred = rf_multi.predict(X_test) # 多分类评估 from sklearn.metrics import classification_report print(classification_report(y_test_multi, multi_pred))

compute_class_weight('balanced')会根据每个类别的样本数自动算权重,样本越少权重越高。这样模型在训练时会更关注 Worms、Shellcode 这些小类别。但注意,权重过高可能导致模型把正常流量误判成小类别攻击,所以跑完要看classification_report里每个类别的 precision 和 recall,不能只看 overall accuracy。

如果过采样,可以用 SMOTE,但 SMOTE 对高维稀疏数据效果一般,而且容易生成不真实的样本。我一般优先用类别权重,实在不行再考虑过采样。另外,多分类的标签编码要用LabelEncoder统一,确保训练集和测试集的类别顺序一致。

3.3 交叉验证与超参数搜索

单次划分训练集和测试集的结果有随机性,写毕设的时候导师可能会问“你这个结果稳定吗”。交叉验证能给出更可靠的评估。常见做法是 5 折或 10 折交叉验证,配合网格搜索找最优参数。

from sklearn.model_selection import GridSearchCV, StratifiedKFold # 定义参数网格 param_grid = { 'n_estimators': [100, 200, 300], 'max_depth': [15, 20, 25], 'min_samples_split': [2, 5, 10] } # 分层交叉验证,保证每折的类别比例一致 cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) rf_cv = RandomForestClassifier(random_state=42, n_jobs=-1) grid = GridSearchCV(rf_cv, param_grid, cv=cv, scoring='f1', n_jobs=-1, verbose=1) grid.fit(X_train, y_train) print(f'Best params: {grid.best_params_}') print(f'Best F1: {grid.best_score_:.4f}')

StratifiedKFold比普通KFold更适合类别不平衡的数据,因为它保证每一折里各类别的比例和整体一致。scoring='f1'是因为安全场景更看重 F1,如果你更关心召回率可以改成recall。网格搜索比较耗时,参数组合多的话可以先用RandomizedSearchCV粗筛,再用GridSearchCV细调。

4. 避坑与排查:UNSW-NB15 实战中容易翻车的地方

4.1 标签泄漏:测试集信息混进了训练过程

现象:模型在测试集上准确率 99%,但换一批数据就掉到 70% 以下。原因:预处理时对全体数据做了标准化或编码,测试集的统计信息泄漏到了训练阶段。解决:所有fit操作只能在训练集上做,测试集只能用transform。标准化、编码、特征选择都要遵守这个原则。

4.2 类别不平衡导致小类别召回率为零

现象:多分类报告里 Worms 和 Shellcode 的 recall 是 0.00。原因:这两个类别样本太少,模型直接把它们归到了大类。解决:用class_weight='balanced'或手动设置权重,也可以对小类别做过采样。如果还是不行,考虑把极稀有类别合并成Other,或者只做二分类不做多分类。

4.3 特征编码顺序不一致

现象:训练时proto列编码后tcp是 3,测试时变成了 5。原因:测试集重新fit了LabelEncoder,导致编码映射不同。解决:训练集fit后保存编码器,测试集用同一个编码器transform。如果测试集出现了训练集没见过的类别,映射为 -1 或单独处理。

4.4 随机种子不固定导致结果无法复现

现象:每次跑代码准确率都不一样,论文里的数字对不上。原因:random_state没设或者设了但没传给所有随机过程。解决:在train_test_split、模型初始化、交叉验证里都固定random_state=42。如果用了 XGBoost,还要注意n_jobs并行时可能有微小随机性,可以设n_jobs=1保证完全可复现。

4.5 内存不足:数据量不大但特征维度爆炸

现象:做 One-Hot 编码后内存直接爆了。原因:proto列有上百个取值,One-Hot 后特征维度从 42 涨到 200 多,稀疏矩阵存储不当会占大量内存。解决:用标签编码代替 One-Hot,或者用pd.get_dummies时加sparse=True。如果一定要 One-Hot,先合并稀有类别,把proto的取值控制在 20 个以内。

5. 进阶技巧:用 SHAP 解释模型和保存可复用的推理管道

5.1 用 SHAP 看模型到底学了什么

毕设答辩的时候,导师经常会问“你这个模型为什么判断这条流量是攻击”。SHAP 能给出每个特征对单条预测的贡献值,比单纯看特征重要性更有说服力。下面是对随机森林做 SHAP 解释的代码:

import shap # 用训练集的一个子集做背景,加速计算 explainer = shap.TreeExplainer(rf) shap_values = explainer.shap_values(X_test.iloc[:500]) # summary plot:全局特征重要性 shap.summary_plot(shap_values[1], X_test.iloc[:500], feature_names=feature_cols) # force plot:单条预测的解释 shap.force_plot(explainer.expected_value[1], shap_values[1][0], X_test.iloc[0], feature_names=feature_cols)

TreeExplainer对树模型的计算速度很快,500 条样本几秒钟就能出结果。shap_values[1]取的是正类(攻击)的 SHAP 值,summary_plot会画出每个特征对预测的影响方向和大小。如果某个特征在攻击样本里普遍推高预测值,说明模型学到了合理的模式。注意 SHAP 计算量随特征数和样本数增长,全量测试集可能跑很久,取子集就够了。

5.2 保存完整的推理管道

训练完模型不能只保存权重,预处理步骤也要一起保存,不然部署的时候还得手动复现编码和标准化逻辑。用joblib把编码器、标准化器、模型打包成一个字典:

import joblib # 打包所有需要复用的组件 pipeline = { 'label_encoders': le_dict, 'scaler': scaler, 'feature_cols': feature_cols, 'model': rf, 'model_type': 'RandomForest' } joblib.dump(pipeline, 'unsw_nb15_pipeline.pkl') print('Pipeline saved.') # 加载并推理 loaded = joblib.load('unsw_nb15_pipeline.pkl') model = loaded['model'] le_dict = loaded['label_encoders'] scaler = loaded['scaler'] # 对新数据做同样的预处理 def preprocess_new(df, le_dict, scaler, feature_cols): for col in ['proto', 'service', 'state']: le = le_dict[col] df[col] = df[col].map(lambda x: le.transform([x])[0] if x in le.classes_ else -1) num_cols = ['dur', 'sbytes', 'dbytes', 'rate', 'sttl', 'dttl', 'sload', 'dload'] df[num_cols] = scaler.transform(df[num_cols]) return df[feature_cols] # 假设 new_data 是一条新流量 # X_new = preprocess_new(new_data, le_dict, scaler, feature_cols) # prediction = model.predict(X_new)

这个管道的好处是,你换一台机器、换一个环境,只要加载这个 pkl 文件,就能对新流量做完全一致的预处理和预测。le_dict里存的是每个类别列的LabelEncoder对象,scaler是训练好的标准化器,feature_cols是特征顺序。注意preprocess_new里对未见过的类别映射为 -1,这个值在训练时没出现过,模型可能会给出异常预测,所以实际部署时要加一层判断,遇到 -1 就标记为“未知类别”人工处理。

5.3 一个我踩过的坑:特征顺序不能乱

有一次我把训练好的模型拿去推理,准确率直接掉到 50%。排查了半天才发现,新数据的列顺序和训练时的feature_cols不一致。sklearn的模型对特征顺序是敏感的,列顺序变了,模型看到的输入就完全变了。从那以后我每次保存管道都会把feature_cols一起存下来,推理前强制df = df[feature_cols]重排。这个习惯帮我省了很多次“玄学掉点”的排查时间。

另外,如果你用 XGBoost 保存模型,joblib和save_model两种方式都可以,但joblib能把预处理组件一起打包,更省事。如果毕设要求部署成 API,可以用 Flask 或 FastAPI 包一层,把preprocess_new和model.predict串起来,输入 JSON 输出预测结果。这样答辩的时候演示效果会好很多,导师也能直观看到模型怎么用。

希望帮到你。

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

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

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

立即咨询