简介:本资源面向物联网安全与机器学习方向的学习者,提供基于IoT23数据集的网络流量异常检测与分类完整实验项目,覆盖数据预处理、多分类建模与结果可视化等环节,适合具备Python基础、希望上手入侵检测实战的中级读者。压缩包共215个文件,以164张png图表、31个py脚本为主,辅以9个xlsx结果表、8个json评分文件及说明文档,整体约12.14MB,图表与脚本对应清晰,便于按实验阶段检索复现。项目采用朴素贝叶斯、决策树、支持向量机、随机森林与逻辑回归等多种分类器进行对比,并给出各场景的评分输出,可帮助读者理解不同模型在IoT流量异常识别中的表现差异。已有1458人学习下载,适合作为课程实验、毕业设计或安全分析入门的参考案例。
1. 从一份被反复引用的流量数据集说起
手里有一份 IoT23 流量抓包,几十个 CSV 摊在目录里,每个文件对应一类设备、一类攻击或一类正常行为,标签列藏在最后一列。想验证一个分类器到底能不能把异常流量挑出来,第一件事不是调参,而是把这份数据读进来、对齐特征、处理类别不平衡。这个项目做的就是这件事:把 IoT23 数据集上的多分类异常检测流程完整跑通,用朴素贝叶斯、决策树、支持向量机、随机森林、逻辑回归五种经典分类器做横向对比,输出准确率、混淆矩阵这类能直接看的指标。适合两类人:一是刚接触机器学习分类任务、想找一个端到端可复现例子的开发者;二是做物联网安全方向、需要一个基线实验框架来快速验证想法的人。它不追求 SOTA,追求的是流程清晰、每一步都能改。
2. 项目结构与技术选型:为什么是这五个分类器
2.1 目录组织与数据流走向
这类项目通常不会搞复杂的工程分层,核心就是「数据加载 → 预处理 → 模型训练 → 评估」四段。按常见做法,目录大致长这样:
iot23-anomaly-classification/ ├── data/ # 原始 CSV,按设备/攻击类型分文件 ├── src/ │ ├── load_data.py # 读取并合并多个 CSV │ ├── preprocess.py # 缺失值、编码、归一化、划分 │ ├── models.py # 五个分类器的定义与训练封装 │ └── evaluate.py # 指标计算与混淆矩阵输出 ├── notebooks/ │ └── eda.ipynb # 探索性分析,看类别分布 ├── requirements.txt └── main.py # 串起全流程的入口数据流是单向的:load_data把散落的 CSV 拼成一个 DataFrame,preprocess负责把字符串特征编码成数值、把标签列单独拎出来,models拿到X_train, y_train后逐个训练,evaluate在测试集上算指标。这种结构的好处是每一段都能单独替换——想换数据集只动load_data,想加模型只动models。
2.2 五个分类器的选型逻辑
为什么偏偏是这五个?它们覆盖了从简单到复杂的谱系,方便看「模型复杂度对结果的影响」:
| 分类器 | 特点 | 在这个场景里的角色 |
|---|---|---|
| 朴素贝叶斯 | 假设特征独立,训练极快 | 下限基线,看数据本身可分性 |
| 逻辑回归 | 线性决策边界,可解释 | 线性模型基线 |
| 决策树 | 非线性、可解释、易过拟合 | 看单树能力 |
| 随机森林 | 集成、抗过拟合 | 主力候选 |
| SVM | 核方法、小样本友好 | 对比核方法表现 |
IoT23 的特征维度不算高,样本量中等,这五个模型跑起来都不吃力。选型的核心考虑是:先用朴素贝叶斯和逻辑回归快速拿到一个基线数字,再用决策树和随机森林看非线性带来的提升,最后用 SVM 验证核方法是否值得。这套组合拳在多数表格型分类任务里都适用,不是这个项目独有的。
2.3 依赖与运行环境
依赖很轻,requirements.txt里常见的是:
pandas numpy scikit-learn matplotlib seaborn没有深度学习框架,没有 GPU 要求,一台普通笔记本就能跑。Python 版本建议 3.8 以上,scikit-learn 用较新的稳定版即可。安装和运行:
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt python main.pymain.py里一般会有一个DATA_DIR常量指向data/目录,跑之前确认 CSV 已经放进去。如果数据量大,第一次运行会慢一些,主要是读文件和编码耗时,训练本身很快。
3. 数据加载与预处理:把散落的 CSV 拼成能训练的表
3.1 多文件合并与标签列处理
IoT23 的数据是按场景拆成多个 CSV 的,每个文件里既有特征列也有标签列。加载时要做的第一件事是遍历目录、逐个读取、纵向拼接:
import os import pandas as pd def load_all_csv(data_dir): frames = [] for fname in os.listdir(data_dir): if not fname.endswith(".csv"): continue path = os.path.join(data_dir, fname) # 低内存模式读取,避免大文件撑爆内存 df = pd.read_csv(path, low_memory=False) # 记录来源文件,方便后续按场景分析 df["source_file"] = fname frames.append(df) return pd.concat(frames, ignore_index=True)这里有两个细节值得说。low_memory=False是为了避免 pandas 分块推断类型时给出混合类型的警告,代价是内存占用高一些;source_file列是血泪经验——后面发现某类攻击识别率特别低时,能快速定位是哪个文件贡献的样本。标签列通常是最后一列或名为label的列,合并后要单独拎出来:
def split_label(df, label_col="label"): y = df[label_col] X = df.drop(columns=[label_col, "source_file"]) return X, y参数说明:label_col要按实际列名改,不同版本的 IoT23 清洗脚本列名可能不一样,跑之前先df.columns看一眼。
3.2 类别不平衡与特征编码
IoT23 的类别分布极度不均,正常流量占大头,某些攻击类型样本很少。直接训练会让模型偏向多数类。常见做法是先看分布,再决定是否做重采样:
from sklearn.utils import resample def balance_by_undersample(X, y, random_state=42): df = pd.concat([X, y], axis=1) majority = df[df[y.name] == df[y.name].value_counts().idxmax()] minorities = df[df[y.name] != df[y.name].value_counts().idxmax()] majority_down = resample(majority, replace=False, n_samples=len(minorities), random_state=random_state) return pd.concat([majority_down, minorities]).sample(frac=1, random_state=random_state)这段是欠采样版本,把多数类降到和少数类总量相当。注意它会让总样本量大幅缩水,如果少数类本身很少,更稳妥的是用class_weight="balanced"让模型自己加权,而不是硬砍数据。特征编码方面,类别型特征用LabelEncoder或pd.get_dummies,数值特征做标准化:
from sklearn.preprocessing import StandardScaler, LabelEncoder def encode_and_scale(X): X = X.copy() for col in X.select_dtypes(include="object").columns: X[col] = LabelEncoder().fit_transform(X[col].astype(str)) scaler = StandardScaler() X_scaled = scaler.fit_transform(X) return X_scaled标准化对 SVM 和逻辑回归影响明显,对树模型无所谓,但统一处理省事。注意fit_transform只能在训练集上做,测试集要用训练集的 scaler 做transform,否则数据泄漏,指标会虚高——这是新手最容易翻车的地方之一。
3.3 训练测试划分的坑
划分时一定要分层采样,保证每个类别在训练集和测试集里比例一致:
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X_scaled, y, test_size=0.2, stratify=y, random_state=42 )stratify=y是关键参数,少了它,稀有类别可能全被分到一边,测试集里根本没有该类样本,混淆矩阵会出现整行零。random_state固定住,保证每次跑结果可复现,调参对比时才有意义。
4. 五个分类器的训练与评估:一条流水线跑完
4.1 统一训练接口
五个模型如果各写各的训练代码,后面加模型会很痛苦。常见做法是用字典把模型名映射到实例,循环训练:
from sklearn.naive_bayes import GaussianNB from sklearn.linear_model import LogisticRegression from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import RandomForestClassifier from sklearn.svm import SVC def build_models(): return { "NaiveBayes": GaussianNB(), "LogisticRegression": LogisticRegression(max_iter=1000, class_weight="balanced"), "DecisionTree": DecisionTreeClassifier(random_state=42, class_weight="balanced"), "RandomForest": RandomForestClassifier(n_estimators=100, random_state=42, class_weight="balanced"), "SVM": SVC(kernel="rbf", class_weight="balanced"), }参数说明:max_iter=1000是给逻辑回归留足收敛轮数,默认值在多分类时经常不收敛报警告;class_weight="balanced"让模型按类别频率自动加权,缓解不平衡;随机森林的n_estimators=100是性价比不错的默认值,加到 300 提升有限但耗时翻倍。SVM 用 RBF 核,样本量大时会慢,如果跑不动可以换LinearSVC。
4.2 训练循环与指标收集
from sklearn.metrics import accuracy_score, f1_score, classification_report def train_and_evaluate(models, X_train, y_train, X_test, y_test): results = {} for name, model in models.items(): model.fit(X_train, y_train) pred = model.predict(X_test) results[name] = { "accuracy": accuracy_score(y_test, pred), "f1_macro": f1_score(y_test, pred, average="macro"), "report": classification_report(y_test, pred, zero_division=0), } return results这里同时收accuracy和f1_macro两个指标。只看准确率在不平衡数据上会被多数类带偏,f1_macro对每个类别一视同仁,更能反映稀有类的识别情况。zero_division=0是防止某类完全没预测出来时除零报错。跑完把结果打成表对比:
import pandas as pd summary = pd.DataFrame({ name: {"accuracy": r["accuracy"], "f1_macro": r["f1_macro"]} for name, r in results.items() }).T print(summary.sort_values("f1_macro", ascending=False))4.3 混淆矩阵与结果解读
指标数字之外,混淆矩阵能看出「谁被误判成谁」:
from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt def plot_confusion(model, X_test, y_test, labels): pred = model.predict(X_test) cm = confusion_matrix(y_test, pred, labels=labels) sns.heatmap(cm, annot=True, fmt="d", xticklabels=labels, yticklabels=labels, cmap="Blues") plt.xlabel("Predicted") plt.ylabel("True") plt.tight_layout() plt.savefig("confusion.png", dpi=150)解读时重点看两处:对角线外的亮点集中在哪两类之间,说明这两类特征太像;某一行整体偏暗,说明该类样本太少或特征区分度不够。随机森林通常在这份数据上 f1_macro 最高,朴素贝叶斯最低,这个排序本身就是一个可复现的结论,换数据后排序可能变,但对比方法不变。
5. 避坑与排查:跑不通时先看这几条
5.1 内存撑爆或读取极慢
现象:pd.concat时进程被系统杀掉,或读文件卡住几分钟。原因:IoT23 原始 CSV 单个文件可能上百万行,全部读进内存再拼接,峰值内存是数据量的两三倍。解决:分块读取后聚合,或先抽样再合并:
chunks = pd.read_csv(path, chunksize=100000, low_memory=False) df = pd.concat(chunks, ignore_index=True)如果只是验证流程,先每个文件抽 10% 样本跑通,再上全量。
5.2 标签列编码后含义错乱
现象:混淆矩阵的类别名变成 0、1、2,对不上原始攻击类型。原因:对标签也做了LabelEncoder,但没保存映射关系。解决:编码前存一份le.classes_,画图时用le.inverse_transform还原,或者干脆不对标签编码,sklearn 多数分类器支持字符串标签。
5.3 SVM 训练长时间不结束
现象:前四个模型几秒跑完,SVM 卡了十几分钟。原因:RBF 核 SVM 的训练复杂度随样本量超线性增长,全量数据下非常慢。解决:对 SVM 单独做子采样,或换LinearSVC,或调小C参数。我一般会在build_models里给 SVM 单独配一个采样后的训练集,不跟其他模型共用全量数据。
5.4 测试集指标异常高
现象:准确率 0.99 以上,高得不真实。原因:标准化或编码时在划分之前对全量数据做了fit,测试集信息泄漏进训练过程。解决:严格按「先划分、再在训练集上 fit、测试集只 transform」的顺序,把预处理封装进Pipeline能从根本上避免:
from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler pipe = Pipeline([("scaler", StandardScaler()), ("clf", RandomForestClassifier(random_state=42))]) pipe.fit(X_train, y_train) # scaler 只在训练集上 fit5.5 类别权重设了但没生效
现象:加了class_weight="balanced",稀有类召回率还是接近零。原因:某些模型(如 GaussianNB)不支持class_weight参数,传了也不报错但被忽略。解决:查一下模型文档确认支持情况,朴素贝叶斯要靠重采样而不是权重来平衡。
6. 二次开发:换数据集、加模型、做对比实验
拿到这个项目后最常做的改造有三类,按投入产出排序说。
第一类是换数据集验证流程通用性。把load_data.py里的读取逻辑换掉,保持输出X, y两个对象不变,后面所有代码不用动。我一般会先写一个check_schema函数,打印列名、dtype、类别分布,确认新数据和原流程兼容再往下跑。这一步花五分钟,能省掉后面半小时的报错排查。
第二类是加新模型做横向对比。在build_models字典里加一项就行,比如梯度提升:
from sklearn.ensemble import GradientBoostingClassifier "GradientBoosting": GradientBoostingClassifier( n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42 )注意新模型如果对特征尺度敏感(如 KNN、神经网络),要确认预处理已经覆盖;如果训练慢,单独给它配采样策略。加完后跑一遍train_and_evaluate,结果表自动多一行,对比成本几乎为零。
第三类是把单次实验变成可复现的对比框架。常见做法是把random_state、采样比例、模型超参抽成配置字典,用循环跑多组配置,结果落成 CSV:
import itertools configs = list(itertools.product([0.1, 0.2, 0.3], [42, 7, 2024])) rows = [] for test_size, seed in configs: X_tr, X_te, y_tr, y_te = train_test_split(X_scaled, y, test_size=test_size, stratify=y, random_state=seed) res = train_and_evaluate(build_models(), X_tr, y_tr, X_te, y_te) for name, r in res.items(): rows.append({"model": name, "test_size": test_size, "seed": seed, "f1_macro": r["f1_macro"]}) pd.DataFrame(rows).to_csv("sweep_results.csv", index=False)这样跑出来的表能看出模型排名是否稳定——如果随机森林在多个 seed 下都领先,结论才站得住;如果排名随 seed 乱跳,说明差异在噪声范围内,别过度解读。
验证改造是否成功,我习惯看三个信号:流程能从头跑到尾不报错、指标量级和原项目一致(换数据集除外)、混淆矩阵的类别标签能对上原始含义。三个都过,才算改对了。
有个教训值得说:早期我做这类对比实验时,图省事在划分前就对全量数据做了编码和标准化,结果随机森林 f1 冲到 0.98,换了个 seed 又掉到 0.7,折腾半天才发现是泄漏导致的虚高。从那以后我每次改预处理代码,都强制先跑一遍「只用训练集 fit」的检查,确认 Pipeline 里没有提前 fit 的步骤再继续。希望这个习惯也能帮到你,少走一段弯路。
本文还有配套的精品资源,点击获取