简介:这份资源面向物联网安全与机器学习方向的学习者,围绕IoT23数据集提供网络流量异常检测与多分类的完整实验代码,覆盖朴素贝叶斯、决策树、随机森林、支持向量机与逻辑回归等经典算法,适合具备Python基础、希望上手入侵检测实战的中级读者。资源包共215个文件,以164张png结果图、31个py脚本、9个xlsx统计表及8个json评分文件为主,图片用于呈现混淆矩阵与指标对比,脚本承载数据预处理与模型训练流程,表格与json记录各场景评分,压缩包约12.14MB。已有1458人学习下载。读者可据此复现从数据清洗、特征处理到多模型训练评估的完整链路,借助现成的评分文件与可视化结果快速对比算法表现,并参考config配置与依赖说明搭建环境,省去从零摸索的时间。
1. 从一份 IoT23 流量日志说起:这个分类项目到底解决什么问题
手头有一份 IoT23 的网络流量抓包日志,几十万条连接记录,每条带着协议、字节数、持续时间、连接状态这些字段,标签列写着Benign、DoS、PartOfAHorizontalPortScan之类的类别。老板问一句「哪些设备在异常通信」,你总不能靠肉眼翻 CSV。这个项目干的就是这件事:把 IoT23 数据集喂给一组经典机器学习分类器,做多分类的流量异常检测,输出每条连接属于哪种攻击类型。技术栈很朴素——Python 加 scikit-learn,模型覆盖朴素贝叶斯、决策树、随机森林、SVM、逻辑回归五种,适合想搞懂 IoT 安全检测基线、又不想一上来就啃深度学习的开发者。它不追求 SOTA,追求的是「跑得通、看得懂、改得动」,拿来做课程设计、安全方向入门实验或者二次开发的起点都合适。
2. 项目结构与技术选型:五个分类器怎么组织起来
2.1 目录骨架与模块职责
这类项目通常不会搞得太重,常见做法是一个扁平结构,把数据处理、模型定义、训练评估拆成几个文件。我按这个场景下最合理的组织方式还原一下:
iot23-anomaly-classification/ ├── data/ │ ├── raw/ # 原始 IoT23 CSV,按设备/场景分文件 │ └── processed/ # 清洗、编码、归一化后的特征表 ├── src/ │ ├── preprocess.py # 读取、清洗、特征工程、划分数据集 │ ├── models.py # 五个分类器的定义与超参 │ ├── train.py # 训练入口,逐模型跑并落盘 │ └── evaluate.py # 混淆矩阵、分类报告、指标对比 ├── notebooks/ │ └── eda.ipynb # 类别分布、特征相关性探索 ├── requirements.txt └── README.mdpreprocess.py是整条链路的入口,负责把原始日志变成模型能吃的数值矩阵;models.py只做一件事——把五个分类器实例化好,参数集中管理;train.py串起训练流程;evaluate.py单独拎出来,方便换模型后只重跑评估。这种拆法的好处是改一个分类器不用动其他文件,坏处是文件间靠约定路径耦合,所以data/processed/的产物命名要固定。
2.2 为什么选这五个分类器
IoT23 的标签是多分类的,类别包括 Benign、DoS、DDoS、PortScan、BruteForce 等若干种,且类别极度不均衡——正常流量占大头,某些攻击类型样本很少。选这五个模型是有讲究的:
| 分类器 | 在项目里的角色 | 适合观察什么 |
|---|---|---|
| 朴素贝叶斯 | 基线,训练最快 | 特征独立假设下能到多少准确率 |
| 逻辑回归 | 线性基线 | 特征线性可分程度 |
| 决策树 | 可解释性最强 | 哪些特征切分贡献最大 |
| 随机森林 | 主力模型 | 集成后准确率与鲁棒性上限 |
| SVM | 对比模型 | 高维特征下的边界质量 |
朴素贝叶斯和逻辑回归当基线,跑完心里有个底;决策树用来解释特征重要性;随机森林通常是这类表格型流量数据上表现最稳的;SVM 在小样本子集上做对比。这套组合的价值不在单模型多强,而在于横向对比能让你看清「数据本身有多难分」。
2.3 依赖与运行环境
requirements.txt里核心就几行,版本不必锁死,但建议 Python 3.8 以上:
pip install pandas numpy scikit-learn matplotlib seabornpandas读 CSV 和处理缺失值,numpy做数值运算,scikit-learn提供全部五个分类器和评估工具,matplotlib/seaborn画混淆矩阵和类别分布。没有 GPU 依赖,普通笔记本就能跑,这也是它适合入门的原因。注意 IoT23 原始数据体量不小,全量加载前先确认内存,常见做法是先按设备或场景抽样。
3. 数据预处理:从原始日志到特征矩阵
3.1 读取与清洗
IoT23 的原始 CSV 字段多、类型杂,直接read_csv会踩类型推断的坑。我一般显式指定关键列类型,并把明显无用的列(如时间戳字符串、IP 地址)先剔掉:
import pandas as pd import numpy as np def load_and_clean(path): # 只读需要的列,减少内存占用 cols = ['proto', 'duration', 'orig_bytes', 'resp_bytes', 'orig_pkts', 'resp_pkts', 'conn_state', 'label'] df = pd.read_csv(path, usecols=cols, low_memory=False) # 数值列里的 '-' 是缺失标记,统一转 NaN for c in ['duration', 'orig_bytes', 'resp_bytes', 'orig_pkts', 'resp_pkts']: df[c] = pd.to_numeric(df[c], errors='coerce') # 缺失值用中位数填充,避免均值被极端流量拉偏 num_cols = df.select_dtypes(include=[np.number]).columns df[num_cols] = df[num_cols].fillna(df[num_cols].median()) # 标签列去掉空白和多余前缀 df['label'] = df['label'].astype(str).str.strip() return df这段的关键点在errors='coerce':IoT23 里数值列混着-这种占位符,不转成 NaN 后面填充会报错。中位数填充比均值稳,因为流量数据里存在大量长尾大值,均值容易被带偏。label列的清洗不能省,原始标签常带空格或层级前缀,不统一会导致同一个攻击类型被拆成两类。
3.2 类别编码与特征编码
标签是多分类字符串,需要映射成整数;proto、conn_state这类类别特征要做编码。这里有个选择:独热编码还是序数编码。conn_state取值有限且无序,独热更合适;但如果取值太多,独热会让特征维度爆炸,常见做法是只保留高频类别,低频归为other:
from sklearn.preprocessing import LabelEncoder def encode_features(df, max_categories=10): df = df.copy() # 类别特征:高频保留,低频合并 for col in ['proto', 'conn_state']: top = df[col].value_counts().nlargest(max_categories).index df[col] = df[col].where(df[col].isin(top), 'other') df = pd.get_dummies(df, columns=[col], prefix=col) # 标签单独编码,保存映射关系供反查 le = LabelEncoder() df['label_enc'] = le.fit_transform(df['label']) return df, lemax_categories=10是个经验值,conn_state的实际取值通常在这个量级内,设太大意义不大。get_dummies之后特征列会变多,这是正常的,随机森林和 SVM 都能处理。LabelEncoder的classes_属性保存了整数到原始标签的映射,评估阶段反查类别名全靠它,务必和模型一起持久化。
3.3 划分数据集与归一化
多分类且不均衡的数据,划分时要用分层抽样,保证训练集和测试集里各攻击类型比例一致:
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler def split_and_scale(df): X = df.drop(columns=['label', 'label_enc']) y = df['label_enc'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # 标准化只对距离敏感的模型有意义,但统一处理更省事 scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) return X_train, X_test, y_train, y_test, scalerstratify=y是必须的,否则某个稀有攻击类型可能整个落进测试集或训练集,模型直接学不到。StandardScaler只在训练集上fit,测试集只transform,这是防数据泄漏的铁律——很多新手在这里翻车,测试集准确率虚高往往就是归一化时把测试集信息带进了训练。树模型其实不需要标准化,但为了五个模型共用一套输入,统一处理更省心。
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 { 'naive_bayes': GaussianNB(), 'logistic': LogisticRegression(max_iter=1000, multi_class='multinomial'), 'decision_tree': DecisionTreeClassifier(max_depth=15, random_state=42), 'random_forest': RandomForestClassifier( n_estimators=200, max_depth=20, n_jobs=-1, random_state=42 ), 'svm': SVC(kernel='rbf', C=1.0, gamma='scale', probability=False), }几个参数值得说:LogisticRegression的max_iter=1000是因为多分类数据量大时默认迭代次数不够会报警告;multi_class='multinomial'让它做真正的多分类而非一对多。决策树限max_depth=15是防过拟合,不限深的话树会长到把训练集背下来。随机森林n_jobs=-1用满所有核,n_estimators=200是精度和耗时的平衡点。SVM 用 RBF 核,probability=False是因为开启概率估计会显著拖慢训练,评估阶段用decision_function就够。
4.2 训练循环与结果落盘
训练入口把预处理、建模、评估串起来,每个模型的结果单独存,方便对比:
import joblib from sklearn.metrics import classification_report, accuracy_score def train_all(X_train, X_test, y_train, y_test, le): models = build_models() results = {} for name, model in models.items(): print(f'=== training {name} ===') model.fit(X_train, y_train) y_pred = model.predict(X_test) acc = accuracy_score(y_test, y_pred) report = classification_report( y_test, y_pred, target_names=le.classes_, zero_division=0 ) results[name] = {'accuracy': acc, 'report': report} # 模型和标签映射一起存,反查类别名要用 joblib.dump({'model': model, 'label_encoder': le}, f'models/{name}.pkl') return resultszero_division=0是必须加的,某些稀有类别在预测里一次都没出现时,classification_report默认会警告甚至报错。joblib.dump把模型和LabelEncoder打包存,是因为预测新数据时要把整数结果翻译回DoS、PortScan这些名字,只存模型不存编码器等于白存。这个细节很多人第一次做会漏。
4.3 评估指标怎么读
多分类不均衡场景下,光看 accuracy 会骗人。假设正常流量占 90%,模型全预测正常也能拿 90% 准确率,但攻击一个没抓到。所以评估要看每个类别的 precision、recall、f1,尤其是稀有攻击类的 recall:
from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt def plot_confusion(y_test, y_pred, le, name): cm = confusion_matrix(y_test, y_pred) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=le.classes_, yticklabels=le.classes_) plt.xlabel('Predicted') plt.ylabel('True') plt.title(f'Confusion Matrix - {name}') plt.tight_layout() plt.savefig(f'reports/{name}_cm.png')混淆矩阵能直接看出模型把哪种攻击误判成了哪种。常见现象是 PortScan 和 DDoS 互相混淆,因为两者在连接数和包数特征上接近。看到这种模式,下一步就该回去补特征,而不是急着调参。评估报告建议按模型存成文本,横向对比时一目了然。
5. 避坑与排查:跑这个项目最容易栽的五个地方
5.1 内存爆掉,进程被系统杀掉
现象:read_csv读到一半进程消失,终端只留一个Killed。原因:IoT23 全量数据几十万到上百万行、上百列,pandas 默认按对象类型加载字符串列,内存翻好几倍。解决:用usecols只读需要的列,dtype显式指定数值列类型,或者先按设备/场景分文件抽样。我一般先nrows=10000试读,确认字段和类型再全量跑。
5.2 测试集准确率高得离谱
现象:随机森林测试准确率 99%,换真实新数据惨不忍睹。原因:归一化或特征编码时在全集上fit,测试集信息泄漏进训练。解决:StandardScaler、LabelEncoder、get_dummies的类别集合都只能在训练集上确定,测试集只做transform。独热编码尤其容易漏,因为get_dummies在合并后的表上做会把测试集类别带进来。
5.3 某个攻击类别 recall 为 0
现象:分类报告里某个稀有攻击类 precision、recall 全是 0。原因:样本太少,模型倾向于把它判成多数类。解决:先确认分层抽样生效;再考虑对少数类过采样(如 SMOTE)或给分类器设class_weight='balanced'。随机森林和逻辑回归都支持这个参数,改一行就能缓解,但别指望彻底解决,特征不够区分才是根因。
5.4 SVM 训练慢到无法接受
现象:前四个模型几分钟跑完,SVM 卡半小时。原因:RBF 核 SVM 的训练复杂度随样本量超线性增长,几十万行直接劝退。解决:在抽样后的子集上跑 SVM,或者换LinearSVC。这个项目里 SVM 的定位本来就是对比模型,不必全量训练,取 10% 分层样本足够看出趋势。
5.5 模型存了却加载报错
现象:joblib.load后预测报特征维度不匹配。原因:训练时的特征列顺序和预测时不一致,或者独热编码后的列集合不同。解决:把训练时的特征列名列表一起存进 pkl,预测前按这个列表重排、补缺失列。这个坑在二次开发换数据源时必踩,提前把列名固化能省很多调试时间。
6. 二次开发:换数据、加模型、做在线检测的改造思路
拿到这个项目后,最值钱的改造方向不是调参,而是把它从「离线实验」推到「能对新流量做判断」。我的做法分三步走。
第一步,把预处理固化成可复用的transform管道。现在preprocess.py里的函数是面向训练集的,二次开发时要抽出一个fit_transform(train_df)和一个transform(new_df),后者严格复用前者的列集合、编码映射、缩放参数。用sklearn.pipeline.Pipeline把编码、缩放、分类器串起来是最省事的,存一个 pkl 就是完整链路:
from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer pipe = Pipeline([ ('prep', ColumnTransformer([...])), # 编码 + 缩放 ('clf', RandomForestClassifier(n_estimators=200, n_jobs=-1)) ]) pipe.fit(X_train, y_train) joblib.dump(pipe, 'models/full_pipeline.pkl')这样新数据进来直接pipe.predict(new_df),不用手动对齐列,5.5 那个坑自然消失。
第二步,加模型时守住评估口径。想试 XGBoost 或 LightGBM,直接在build_models()里加一项,但评估必须复用同一份X_test、y_test和同一个LabelEncoder,否则横向对比没意义。我见过有人换模型时顺手重划了数据集,结果新模型「提升」了五个点,其实是测试集变了。
第三步,做在线检测的话,把批量预测包成一个按时间窗口滑动的函数:每积累 N 条连接记录,跑一次pipe.predict,把异常类别和置信度写进告警表。这里要注意模型漂移——IoT 设备的流量模式会随固件更新变化,固定模型跑几个月准确率会掉,常见做法是定期用新标注数据增量重训,或者至少监控预测类别的分布偏移。
改造时有个验证习惯我一直保持:任何改动后,先在一个固定的小样本集上跑一遍,确认输出和改动前一致或差异可解释,再上全量。有次我图快直接全量重训,结果特征列顺序被get_dummies悄悄改了,模型准确率掉了十几个点,排查了半天才发现是列对齐问题。从那以后我每次改预处理都强制走一遍小样本回归对比,确认列名和顺序没变才继续。希望这个习惯也能帮到你,少走点弯路。
本文还有配套的精品资源,点击获取