简介:这是一份基于UNSW-NB15数据集开展网络攻击检测的机器学习毕业设计资源,面向计算机相关专业的学生、老师及企业开发者,涵盖决策树、逻辑回归、KNN等典型分类算法的Python实现,可直接用于毕设、课程设计或初期项目演示。压缩包解开后共4个文件,核心为3个Python脚本和1个说明文档,整体仅12KB,结构清楚,部署简单,能够快速复现二分类检测流程并观察各算法表现。目前已有143人学习下载,适合希望以现成源码快速启动实验、在此基础上修改或扩展功能的中初级学习者。资源内附的说明与应用逻辑便于使用者理解从数据读入、模型训练到结果输出的完整过程,可用于论文复现、答辩演示或作为同类入侵检测课题的起步模板。
1. 用 UNSW-NB15 数据集做网络攻击检测:毕设里性价比最高的选题之一
当你在 UNSW-NB15 数据集上把随机森林跑到 90% 以上准确率时,真正值得注意的不是那个数字,而是测试集上的 F1 波动。标题里这个项目——机器学习算法 + UNSW-NB15 数据集的网络攻击检测——几乎是毕设选题里的“标准答案”:数据集公开、标签齐全、场景明确,一套流程跑下来预处理、算法对比、图形化评估全都有,论文里的图表基本齐了。
它能解决的核心问题,是用机器学习判断一条网络流量是正常还是攻击,并进一步识别攻击类型。适合两类人:毕业设计或课程设计需要“能运行、能讲解、有图表”完整方案的在校生,以及刚进入入侵检测方向、想拿真实数据集练手的从业者。价值在于链路完整,从 CSV 原始数据到混淆矩阵每一步都有明确输出,不是那种跑完就扔的黑匣子 Demo。
先提醒一个常见的认知偏差:很多人以为模型一 fit、打印个准确率就算完工,但这个数据集有特有的坑,比如训练测试分布不一致、类别不平衡、字符特征编码顺序错位。接下来把数据加载、预处理、训练、评估全路线,以及最容易翻车的节点一次讲透。
2. 数据预处理与特征工程:把 49 列原始流量变成模型能吃的矩阵
UNSW-NB15 的原始 CSV 并不是拿来就能直接喂给 sklearn 的。官方发布的数据集已经划分好了训练集和测试集,但里面混着 id、源 IP、端口这类标识列,还有 proto、service、state 这类字符列,不处理直接 fit 会当场报错。这一章把数据加载、清洗、编码、归一化整条流水线过一遍,后面训练脚本才能一跑就通。
2.1 加载数据:用官方划分,不要自己随机切分
这个数据集的官方发布页提供了训练集和测试集两个 CSV,训练集约 17.5 万条,测试集约 8.2 万条,每条 49 列。毕设里最稳的做法是直接用官方划分,不要自己切。因为论文评审会关注测试集出处,官方划分的测试集在同方向的对比实验里就是基准,自己随机切分反而容易被质疑结果不可比。
import pandas as pd train = pd.read_csv("UNSW_NB15_training-set.csv") test = pd.read_csv("UNSW_NB15_testing-set.csv") print("训练集:", train.shape) print("测试集:", test.shape) print(train["label"].value_counts()) # 正常流量 label=0,攻击流量 label=1逻辑说明:read_csv 不传额外参数就能成功读入,说明文件完整。shape 和 value_counts 用来确认数据规模与正负样本比例,这两步是后续所有处理的前提,先确认数据没问题再往下走。
参数说明:label 列只有 0/1 两个取值,0 是正常流量,1 是攻击流量。如果 value_counts 打印出来比例严重偏离,比如攻击样本只有几千条,说明下载的文件可能不完整,要回到数据源重新获取。
2.2 剔除标识列并把字符特征编码
49 列里,id、srcip、sport、dstip、dsport 是网络流量的标识信息,不能进模型。剩下的特征里,proto、service、state 是字符型,sklearn 不接受字符串,必须先编码。这里用 LabelEncoder 而不是对所有 object 列做 OneHot,原因是 service 列基数很大,OneHot 会把特征列扩展到几百维,训练和预测都变慢,而树模型对 LabelEncoder 产生的整数顺序并不敏感。
drop_cols = ["id", "srcip", "sport", "dstip", "dsport"] feature_cols = [c for c in train.columns if c not in drop_cols + ["attack_cat", "label"]] from sklearn.preprocessing import LabelEncoder def encode_categorical(data, encoders): X = data[feature_cols].copy() for col in feature_cols: if X[col].dtype == object: le = encoders.get(col, LabelEncoder()) X[col] = le.fit_transform(X[col].astype(str)) encoders[col] = le return X train_encoders = {} X_train_raw = encode_categorical(train, train_encoders) X_test_raw = encode_categorical(test, train_encoders) # 复用整套编码器逻辑说明:encode_categorical 用传入的 encoders 字典,第一次 fit 生成映射,测试集只做 transform 不重新 fit。这样保证同一个字符取值在训练和测试里被映射成同一个数字,这是最容易出错的地方:如果在测试集上重新 fit 一套编码,特征含义就完全错位,模型评估结果直接失真。
参数说明:astype(str) 是为了避免某列里混入 NaN 导致 fit_transform 报错。LabelEncoder 遇到训练集没出现过的字符串会抛“未知类别”错误,这个在避坑第 5.2 节会专门处理。
2.3 缺失值填充与特征顺序固定
这个数据集里部分列确实存在 null。常见处理是 fillna(0),网络流量里时长、负载长度缺失时填 0,语义上等价于“该属性无记录”,对树模型的切分影响很小。另一个值得留意的细节是把 feature_cols 存成 pickle,训练和预测共用同一份列顺序,否则会出现特征错位这种极难排查的问题。
import joblib X_train_raw = X_train_raw.fillna(0) X_test_raw = X_test_raw.fillna(0) feature_order = list(X_train_raw.columns) joblib.dump(feature_order, "feature_order.pkl") # 预测阶段还要用 print("特征数量:", len(feature_order)) print("训练集剩余 NaN 数量:", int(X_train_raw.isna().sum().sum()))逻辑说明:fillna(0) 放在编码之后统一执行,比一列一列处理省事。feature_order.pkl 是后悔药:模型训练完放几天再回来做预测,只要加载这个文件,列顺序永远和训练时保持一致。
参数说明:如果 fillna(0) 之后 isna().sum() 仍然大于 0,说明有列的 dtype 是 object 但填充的是数字,需要回到 2.2 的编码逻辑检查是否把全部 object 列都过了一遍。
提示:特征清单一定要落盘保存。常见情况是有人把 feature_cols 写在内存变量里,重启内核后丢失,预测时只能靠回忆补列,结果一整排特征全部错位,F1 直接掉十几个点。
2.4 归一化:staler 只允许 fit 训练集
逻辑回归、SVM 这类基于距离的模型必须做归一化,随机森林可以不归一化,但统一做也没坏处。关键在于 scaler 只能 fit 训练集。曾经有人在拼接后的全量数据上做标准化,测试集的信息提前进入训练流程,评估结果虚高,论文数据被导师一句“数据泄漏”打回重做。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train = scaler.fit_transform(X_train_raw) X_test = scaler.transform(X_test_raw) y_train = train["label"].values y_test = test["label"].values print("X_train:", X_train.shape, "X_test:", X_test.shape)逻辑说明:fit_transform 在训练集上计算均值和标准差,transform 在测试集上只做标准化,不重新计算。这一步保证了测试集信息完全不进入训练流程,也是后面所有评估结果可信的前提。
参数说明:如果后面要用随机森林,可以保留一份未归一化的 X_train_raw 做对照,树模型通常不受归一化影响;但为了评估流程统一,这里直接全部走同一套预处理,避免“为什么两个模型输入不同”的额外解释。
3. 算法选型与最小训练脚本:从逻辑回归到随机森林的对照实验
毕设里算法部分的典型写法是“两个以上算法做对比,一张表格加一张折线图”。常见选型是逻辑回归当基线、随机森林当主力、XGBoost 当提分项。这一章先讲清楚选型逻辑,再给一份能直接跑通的完整脚本,最后给硬件耗时预期。
3.1 选型逻辑:三个标准决定用哪些算法
选算法先看三个标准。第一,能否在 10 分钟内出结果,毕涉周期内反复调参是常态,跑一轮要半小时的算法会拖垮节奏;第二,能否在答辩时讲清楚原理,老师最常问的就是“为什么用这个算法,它为什么有效”;第三,能否输出可视化结果。同时满足这三个条件的经典组合就是逻辑回归、随机森林、XGBoost。
逻辑回归是 baseline,结果再差也是个参照系;随机森林是主力,参数可解释性强,特征重要性直接出图;XGBoost 是提分项,同一份数据上通常比随机森林高 1 到 3 个百分点。SVM 不建议在 17.5 万条样本上用,训练时间是非线性增长,效果还不一定超过随机森林,性价比太低。
3.2 最小训练脚本:能直接跑通的对照实验
下面这份脚本把训练、计时、评估封装成一个函数,三个算法统一入口。跑完会打印每个模型的训练耗时和完整分类报告,这些内容可以直接整理进论文的实验章节。
import time from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report def train_and_eval(name, model): start = time.time() model.fit(X_train, y_train) elapsed = time.time() - start y_pred = model.predict(X_test) print(f"\n===== {name} | 训练耗时 {elapsed:.1f} 秒 =====") print(classification_report(y_test, y_pred, digits=3)) return model models = { "LogisticRegression": LogisticRegression(max_iter=500, C=1.0, n_jobs=-1), "RandomForest": RandomForestClassifier( n_estimators=100, max_depth=20, min_samples_leaf=5, n_jobs=-1, random_state=42 ), } results = {} for name, model in models.items(): results[name] = train_and_eval(name, model)逻辑说明:train_and_eval 统一封装训练、计时、评估三件事。classification_report 一次输出精确率、召回率、F1,够论文表格直接用。训练耗时打印出来写进“实验环境与耗时对比”一节,是评审喜欢看到的细节。
参数说明:LogisticRegression 的 max_iter=500 保证收敛,如果输出警告说未收敛,继续加大到 1000;C=1.0 是默认正则强度,调小会更强调防过拟合。RandomForest 的 max_depth=20 是刻意限制,避免在 17.5 万样本上无限制生长导致过拟合和内存暴涨;min_samples_leaf=5 让叶子节点至少 5 个样本,降低方差;random_state=42 固定随机种子,结果可复现。
3.3 加上 XGBoost 提分,以及硬件预期
XGBoost 是大多数毕设项目里实际效果最好的单个模型,前提是没调坏参数。tree_method="hist" 用直方图近似,在大样本上比默认的精确贪心算法快很多,8GB 内存笔记本就能跑。
from xgboost import XGBClassifier xgb_model = XGBClassifier( n_estimators=300, max_depth=5, learning_rate=0.1, subsample=0.8, colsample_bytree=0.8, tree_method="hist", eval_metric="logloss", verbosity=0 ) xgb_model = train_and_eval("XGBoost", xgb_model)逻辑说明:subsample 和 colsample_bytree 都设 0.8,每次建树只用 80% 的样本和 80% 的特征,是 XGBoost 最常用的防过拟合组合。verbosity=0 关掉训练日志,输出清爽。
参数说明:n_estimators=300 配合 learning_rate=0.1,如果想更稳,把 n_estimators 加到 500、learning_rate 降到 0.05,训练时间会翻倍但通常 F1 还能再涨一点。eval_metric="logloss" 是二分类场景的默认评估指标,这里不参与训练,只用于内部日志监控。
| 算法 | 预期训练耗时(普通笔记本) | 预期 F1 区间 | 定位 |
|---|---|---|---|
| 逻辑回归 | 1 分钟以内 | 0.72-0.80 | 基线对照 |
| 随机森林 | 5-10 分钟 | 0.86-0.91 | 毕设主力 |
| XGBoost | 5-15 分钟 | 0.88-0.93 | 提分项 |
这张表是经验区间,不是承诺。具体数值取决于预处理细节,有人字符编码顺序处理不同,随机森林直接差三个点。所以先跑通流程,再对照区间看自己落在哪里,偏低就回去查预处理。
4. 模型评估与调参:准确率、F1、混淆矩阵的搭配用法
classification_report 打印完就收工的做法太糙。UNSW-NB15 的攻击类别分布很不均匀,Generic 和 Exploits 样本多,Worms、Analysis、Shellcode 样本少。只看准确率,小类别全分错也可能有 85% 以上的分数,这数字在论文里站不住脚。
4.1 准确率会骗人:优先看 F1 和召回率
二分类场景里,准确率高往往只是因为多数类被分对了。UNSW-NB15 训练集里攻击样本比正常流量多,比例大约 1.3 比 1。如果模型把攻击类分对、正常流量误报严重,准确率依然好看,但实际检测系统的可用性很差。所以评估顺序应该是 F1、召回率,最后才是准确率。
from sklearn.metrics import precision_recall_fscore_support y_pred = results["RandomForest"].predict(X_test) p, r, f1, _ = precision_recall_fscore_support(y_test, y_pred, average="binary") print(f"精确率: {p:.4f} | 召回率: {r:.4f} | F1: {f1:.4f}")逻辑说明:average="binary" 以 1(攻击)为正类,报告的是攻击类的精确率、召回率和 F1。这组数放进论文表格,配合准确率一起看,才不会被一句“准确率虚高”问住。
参数说明:随机森林在这个数据上的 F1 通常会落在 0.85 到 0.91 区间,低于这个区间先查预处理,高于不必惊喜,说明特征工程和调参到位了。
4.2 混淆矩阵:定位哪类攻击被漏掉
混淆矩阵是答辩图上最直观的东西。除了 Normal 和 Attack 的二分类矩阵,还可以进一步用 attack_cat 列做十类别分析。这里先给出二分类版本,代码同样适用多分类。
import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from sklearn.metrics import ConfusionMatrixDisplay disp = ConfusionMatrixDisplay.from_predictions( y_test, y_pred, display_labels=["Normal", "Attack"], normalize="true", cmap="Blues" ) plt.savefig("cm.png", dpi=150, bbox_inches="tight")逻辑说明:normalize="true" 按行归一化,每个格子的数字表示该真实类别的样本有多少比例被分到了这个格子,比纯计数更容易看出漏检率。savefig 直接输出 PNG,论文插图不用截屏。
参数说明:Agg 后端适合无图形界面的服务器环境;如果本地有桌面窗口,可以把 savefig 换成 plt.show(),但保存图片这一行建议保留,后面改图方便。
4.3 GridSearchCV 调参:小网格、子样本,避免一夜跑不完
网格搜索是毕设调参的标准动作,但直接在 17.5 万行全量数据上做三折交叉验证非常慢。常见做法是从训练集抽 3 万条做快速搜索,定下关键参数后再用全量数据训练最终模型,效率和效果兼顾。
import numpy as np from sklearn.model_selection import GridSearchCV rng = np.random.default_rng(42) idx = rng.choice(len(X_train), size=30000, replace=False) X_sub, y_sub = X_train[idx], y_train[idx] param_grid = { "max_depth": [15, 20, 25], "min_samples_leaf": [2, 5, 10], } gs = GridSearchCV( RandomForestClassifier(n_estimators=100, n_jobs=-1, random_state=42), param_grid, cv=3, scoring="f1", n_jobs=-1 ) gs.fit(X_sub, y_sub) print("最优参数:", gs.best_params_, "最优 F1:", round(gs.best_score_, 4))逻辑说明:3 万条子样本、3×3 的网格、3 折交叉验证,组合起来几分钟出结果。拿到 best_params_ 后,再用全量训练集按最优参数重训一次,就是最终模型。
参数说明:scoring="f1" 让搜索以 F1 为标准而不是准确率,理由见 4.1。cv=3 在 3 万样本上开销可控,如果只有双核 CPU,n_jobs=-1 反而可能拖慢整体,改成 1 或 2 更稳定。
5. 实战避坑:用 UNSW-NB15 训练检测模型的 5 个常见问题
这一章都是血泪经验。每一条按现象、原因、解决的顺序写,照这个思路排查,能省下大半天调试时间。这里的每个问题都在真实环境中出现过,不是理论推演。
5.1 训练集 F1 很高,测试集掉到 0.7 附近
现象是随机森林在训练集上评估 F1 接近 0.99,换到测试集直接跌到 0.7,甚至不同 random_state 跑出的结果波动明显。原因是过拟合叠加数据集分布差异。随机森林不限制深度时会在 17.5 万样本里逐点记忆噪声,而 UNSW-NB15 的训练集和测试集来自不同流量时间段,分布本身有差异,训练集上习得的规律在测试集上不能完全成立。
解决方法是把 max_depth 限制在 15 到 25,加上 min_samples_leaf 不小于 5,同时用交叉验证替代单次划分估计真实水平。如果掉点仍然严重,回到 2.4 检查 scaler 是否在拼接数据上 fit 过,测试集信息一旦提前泄漏,测试集表现就会虚高,换到真实场景立刻原形毕露。
# 如果测试集 F1 偏低,先改这里再重跑 RandomForestClassifier( n_estimators=100, max_depth=20, min_samples_leaf=10, n_jobs=-1, random_state=42 )5.2 直接 fit 报错 could not convert string to float
现象是代码在 model.fit(X_train, y_train) 处报 ValueError,提示某个字段无法转换成 float。最常见原因是 id、srcip、proto 这些列没被剔除也没编码,数据里混着字符串,sklearn 无法解析。这个报错信息比较直白,但很多人第一反应是去查数据格式,而不是查特征列是否干净。
解决方法是强制走特征白名单:feature_cols = [c for c in train.columns if c not in drop_cols + ["attack_cat", "label"]],再确认所有 object 列都过了一遍 LabelEncoder。排查时打印 X_train.dtypes,只要还有 object 类型就回到 2.2 补编码。
# 排查:确认训练矩阵里没有 object 列 print(X_train.dtypes.value_counts()) # 如果 object 数量大于 0,说明有字符列漏编码5.3 预测时提示特征名不匹配
现象是 predict 时出现 sklearn 警告,说训练时有 45 个特征而预测时有 43 个,或者 Feature names mismatch 的提示。原因是训练和预测走了两条预处理路径,列顺序发生了变化。典型场景是把预处理脚本复制了一份,后来在训练脚本里加了 fillna,预测脚本里没加,或者反过来。
解决方法是预测端加载第 2.3 节存下来的 feature_order.pkl,把测试特征按这个顺序重排:X_test_reordered = X_test_raw[feature_order]。再走同一套编码器和 scaler 完成变换,这样无论预处理顺序怎么改,送入模型的列始终和训练时一致。
import joblib feature_order = joblib.load("feature_order.pkl") X_pred_raw = X_pred_raw[feature_order] # 列顺序强制对齐 X_pred = scaler.transform(X_pred_raw.fillna(0))5.4 Worms、Shellcode、Analysis 的召回率接近 0
现象是 classification_report 里这三个类别 F1 只有 0.0x,甚至全部被识别成其他攻击或正常流量。原因是类别严重不平衡,这三个攻击类别在数据集中样本量极少,模型学到的主要是 Fuzzers、Generic、Exploits 这些大类的规律,小类别几乎没有机会被正确分类。
解决方法是二分类时给模型加 class_weight="balanced",让少数类在损失函数里获得更高权重。做多分类时,对 Worms、Shellcode 等小类做上采样,或者直接在论文结论里如实写“样本量过小的攻击类型识别效果有限”。诚实写结论比硬调参更有说服力,评审反而认可你对数据局限性的判断。
RandomForestClassifier( n_estimators=100, max_depth=20, class_weight="balanced", n_jobs=-1, random_state=42 )5.5 训练时内存占满或进程被杀
现象是跑 RandomForest 时内存飙到 10GB 以上,训练中断,严重时直接死机。原因是两个叠加:树不加深度限制,且全量数据参与。随机森林默认建树到纯节点,每棵树都要保存大量中间索引,175341×45 的矩阵本身不大,但树的递归结构非常吃内存。
解决方法是限制 max_depth 和 min_samples_leaf,n_estimators 先给 50 做验证。如果机器只有 4GB 内存,先把训练数据采样到 8 万条跑通流程,再上全量。内存不足不是代码错误,是资源配置问题,调整参数就能解决。
# 低内存机器可以先这样跑通 X_sample, y_sample = X_train[:80000], y_train[:80000] RandomForestClassifier( n_estimators=50, max_depth=15, min_samples_leaf=10, n_jobs=2, random_state=42 ).fit(X_sample, y_sample)6. 进阶技巧:从二分类扩展到逐类别攻击识别
6.1 把 label 换成 attack_cat 做 10 分类
二分类只能回答“是不是攻击”,但论文里往往需要回答“是哪类攻击”。把训练目标从 y_train 换成 train["attack_cat"],预处理代码完全不用动,随机森林一样能训。唯一要处理的是 attack_cat 里的字符串,先做一套独立的 LabelEncoder。
from sklearn.preprocessing import LabelEncoder le_cat = LabelEncoder() y_train_cat = le_cat.fit_transform(train["attack_cat"]) y_test_cat = le_cat.transform(test["attack_cat"]) model_cat = RandomForestClassifier( n_estimators=200, max_depth=20, min_samples_leaf=5, n_jobs=-1, random_state=42 ) model_cat.fit(X_train, y_train_cat) print(classification_report(y_test_cat, model_cat.predict(X_test), target_names=le_cat.classes_))参数说明:le_cat 必须在训练集 fit、测试集 transform,新类别出现时检查 classes_。评估输出带上 target_names,混淆矩阵标签就不再是一堆数字。
6.2 看特征重要性,给论文加一张关键图
随机森林的 feature_importances_ 直接输出每个特征对分类的贡献。训练完取出 Top 20 特征,画条形图并顺手用这 20 个特征重训一个模型,通常准确率掉不到 1%,但训练时间再降一大截。这一步能把随机森林从黑匣子变成可解释的模型。
import pandas as pd imp = pd.Series(model.feature_importances_, index=feature_cols) imp.sort_values(ascending=False).head(20).plot.bar(figsize=(10, 6)) plt.title("Top-20 Feature Importance") plt.tight_layout() plt.savefig("feature_importance.png", dpi=150)这段代码直接出图,配合一段“哪些特征对攻击检测贡献最大”的文字,就是论文里很扎实的一节。我的习惯是训练后必看特征重要性,既能发现无效列,也能在答辩时讲清楚模型在关注什么。
6.3 用概率阈值控制误报
生产场景里,误报比漏报更难处理。sklearn 默认以 0.5 作为判定阈值,但对不平衡数据,可以把阈值抬到 0.6 或 0.7,让模型只在把握较大时报出攻击。验证方法直接:加载测试集,算 predict_proba 的概率列,扫一遍阈值,画一条误报率随阈值变化的曲线,选业务可接受的点。
proba = model.predict_proba(X_test)[:, 1] for thr in [0.5, 0.6, 0.7, 0.8]: thr_pred = (proba >= thr).astype(int) f1 = precision_recall_fscore_support(y_test, thr_pred, average="binary")[2] print(f"阈值 {thr}: F1={f1:.4f}")这个小实验做完,结论页里就能写出“在误报率不超过 X% 的前提下,最优阈值是 Y”,比单纯汇报准确率有说服力得多。整个项目到这里就是从数据、模型到阈值决策的完整闭环。做毕设时留好每一步中间产物——特征清单、编码器、scaler、模型文件和阈值曲线——后面改任何一版都能快速回溯,这是我从多次返工里养成的习惯。希望帮到你。
本文还有配套的精品资源,点击获取