ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CIC-IDS2017数据集实战:从数据清洗到模型训练全流程解析

2026/10/4 17:38:20 拓冰建站 浏览量
CIC-IDS2017数据集实战:从数据清洗到模型训练全流程解析 开头做网络安全方向研究的朋友对CIC-IDS2017这个数据集应该都不陌生。它全称是Canadian Institute for Cybersecurity Intrusion Detection System 2017由加拿大网络安全研究所发布是目前学术界和工业界做入侵检测模型验证时最常被引用的公开数据集之一。我大概一年多前开始用这个数据集做流量分类实验中间踩了不少坑。从数据清洗到特征工程从模型训练到结果评估整个流程走下来最大的感受是这个数据集本身质量不错但如果你直接拿原始CSV丢进模型训练效果会非常难看。它不是那种“下载下来就能跑”的数据集需要做大量前置处理。这篇博文就把我完整跑通CIC-IDS2017训练和测试流程的经验整理出来包括数据集的内部结构拆解、预处理细节、模型训练参数配置、评估指标选择以及我实际遇到过的各种报错和排查方法。不管你是刚接触入侵检测方向的学生还是想快速验证自己模型的从业者这篇内容都可以帮你省下大量试错时间。1. 项目整体设计与思路拆解1.1 为什么选CIC-IDS2017而不是其他数据集做入侵检测模型训练其实可选的数据集不少。早期有KDD99、NSL-KDD后来有UNSW-NB15、ISCX 2012再后来就是CIC-IDS2017和CICIDS2019。我最终选了CIC-IDS2017核心原因有三个。第一它的流量覆盖了常见的攻击类型包括暴力破解、DDoS、Web攻击、端口扫描、僵尸网络等一共14种攻击标签加一种良性流量标签。这个覆盖度对做多分类任务的场景非常友好不需要像我之前用KDD99那样只能做二分类或者很粗糙的多分类。第二它提供了完整的原始流量PCAP包和按天拆分的CSV特征文件。这意味着你可以直接拿CSV做传统的机器学习训练也可以回到PCAP层自己做特征提取灵活度很高。很多数据集只给处理好的特征想做底层分析根本没门儿。第三数据量足够大。CIC-IDS2017完整数据大约50GB的PCAP提取后的CSV特征也有几百万行。这个量级训练深度学习模型或者分布式模型完全够用不需要担心数据量太少导致模型欠拟合。当然它也有明显缺点类不平衡问题非常严重。良性流量占了绝大多数某些攻击类型比如Heartbleed只有几千条甚至几百条记录。这个问题后面我会专门讲怎么处理。1.2 整体方案选型与技术路线我的整体技术路线分四步数据理解、数据预处理、模型训练、评估验证。在模型选型上我先用传统机器学习模型做基线对比包括随机森林、XGBoost、逻辑回归然后用深度学习模型做进一步提升选了1D-CNN和LSTM。为什么这么配两个原因。传统机器学习模型训练速度快调参成本低适合先摸清数据集的底细。比如哪些特征对分类贡献最大、哪些攻击类型容易混淆等通过随机森林的特征重要性分析可以很快得到结论。用这些结论指导后续深度学习模型的输入特征选择能省不少事。深度学习模型在流量分类上的表现上限更高尤其对时序特征的捕捉能力更强。但它的训练成本高如果一上来就直接拿全部特征跑深度学习数据处理时间会非常久环境资源浪费严重。另外我在实际测试中还对比过一个思路就是直接用全量特征跑XGBoost和只用特征选择后跑XGBoost的对比。结果很有意思特征选择后只保留了大约30%的特征但准确率几乎没下降F1分数只掉了0.002左右训练时间缩短了一半还多。这说明CIC-IDS2017里有大量冗余特征清洗整理非常有必要。1.3 硬件环境与工具链准备训练CIC-IDS2017需要的硬件门槛其实不高但也不能太差。我的环境配置供参考CPUIntel i7-10700K8核16线程内存32GB DDR4显卡NVIDIA RTX 3080 10GB硬盘NVMe SSD 1TB操作系统Ubuntu 20.04 LTS这套配置跑随机森林和XGBoost完全没问题跑1D-CNN也够用。如果你只有16GB内存建议别一次性把所有CSV读进内存用分块读取的方式处理后面我会给出具体写法。软件环境方面我用了Python 3.9依赖库包括pandas 1.5.3、numpy 1.24.3、scikit-learn 1.2.2、xgboost 1.7.6、tensorflow 2.13.0和imbalanced-learn 0.11.0。其中imbalanced-learn专门用来做类别不平衡处理后面会详细说明。2. 数据集结构深度解析与预处理实操2.1 CIC-IDS2017的目录结构与文件格式CIC-IDS2017的官方下载包解压后目录结构大致如下CIC-IDS2017/ ├── MachineLearningCSV/ │ ├── Friday-WorkingHours-Afternoon-DDos.pcap_ISCX.csv │ ├── Friday-WorkingHours-Afternoon-PortScan.pcap_ISCX.csv │ ├── Friday-WorkingHours-Morning.pcap_ISCX.csv │ ├── Monday-WorkingHours.pcap_ISCX.csv │ ├── Thursday-WorkingHours-Afternoon-Infilteration.pcap_ISCX.csv │ ├── Thursday-WorkingHours-Morning-WebAttacks.pcap_ISCX.csv │ ├── Tuesday-WorkingHours.pcap_ISCX.csv │ ├── Wednesday-WorkingHours.pcap_ISCX.csv │ └── ...每个CSV文件对应一周内某一天某个时间段的流量数据。流量采集周期是2017年7月3日到7月7日周一是纯良性流量周二到周五混入了不同类型的攻击流量。需要注意这些CSV文件的列名是固定的一共80多列。前几列是流ID、源IP、目的IP、源端口、目的端口、协议等基本信息后面是CICFlowMeter工具提取的各种统计特征比如流持续时间、正向和反向数据包数量、包长度统计值、TCP窗口大小、标志位计数等。最后一列是标签列列名是“Label”记录这条流是良性还是某种攻击类型。有个很奇怪但是非常重要的细节CIC-IDS2017的CSV文件用的是空格分隔不是常见的逗号分隔。因为部分字段值里有逗号比如标签列里的“Web Attack – Brute Force”就包含了逗号。所以用pandas读取时不能直接pd.read_csv而要用pd.read_csv(filename, sep\s, enginepython)。这一点坑了很多人我第一次用pd.read_csv读的时候读出来的DataFrame列名全是乱的一度以为是文件坏了。2.2 数据清洗与格式标准化CIC-IDS2017的原始CSV里有几类典型脏数据必须处理。第一类是NaN值。每列都存在一定程度确实尤其是一些TCP相关的特征列在非TCP流量上表现得特别明显。处理方法有两种直接删除缺失比例超过一定阈值的列以及用填充值处理剩余列的缺失项。我实测下来的做法是先统计每列的缺失率缺失率超过20%的直接删掉不超过20%的用该列的中位数填充。不要用均值填充因为流量特征数据大都服从长尾分布个别极大值会把均值拉得很高。第二类是Infinity值。CSV中存在一些特征值等于Infinity的记录一般发生在包长度标准差等计算中除数为零的情况。在入库之前要把Infinity替换为NaN再按上面的方式处理。第三类是重复记录。数据集中存在完全重复的行直接drop_duplicates()处理即可。下面是清洗代码的核心部分import pandas as pd import numpy as np def load_and_clean_cicids2017(filepath, drop_threshold0.2): df pd.read_csv(filepath, sepr\s, enginepython, low_memoryFalse) # 替换无穷值 df df.replace([np.inf, -np.inf], np.nan) # 统计每列缺失率 missing_ratio df.isnull().mean() drop_cols missing_ratio[missing_ratio drop_threshold].index.tolist() df df.drop(columnsdrop_cols) # 中位数填充剩余缺失 numeric_cols df.select_dtypes(include[np.number]).columns df[numeric_cols] df[numeric_cols].fillna(df[numeric_cols].median()) # 删除完全重复行 df df.drop_duplicates() # 删除全零行没有实际流量特征意义 feature_cols [c for c in numeric_cols if c not in [Flow ID, Source Port, Destination Port]] df df[(df[feature_cols] ! 0).any(axis1)] return df这里有个操作上的细节全零行的判断需要排除端口列因为端口号是0是合法的表示未知端口但流量特征全部为0代表这条流没有任何数据包被统计到对训练没有意义。2.3 特征工程与标准化处理清洗完原始数据后接下来要做特征工程。CIC-IDS2017自带的特征已经很多但直接使用会存在几个隐患。数值范围差异巨大是一个问题。比如Flow Duration的单位是微秒值动辄几百万而Flow Bytes/s可能只有几十。梯度下降类模型对这种尺度差异非常敏感不标准化的话训练会震荡严重。树模型影响不大但为了统一处理流程我还是建议做标准化。我采用的方案是StandardScaler它会把特征化为均值为0、标准差为1的分布。注意要用训练集拟合StandardScaler再对训练集和测试集分别做transform不要用全量数据拟合否则会造成数据泄露测试结果虚高。特征编码也要处理。像Protocol这类类别特征直接用原始数值6表示TCP、17表示UDP会让模型误以为数值之间有距离关系这是错误的。我做了One-Hot编码。不过在实践中我也发现其实在CIC-IDS2017里Protocol 0、6、17分别代表HOPOPT、TCP、UDP用LabelEncoder或者直接Category类型转码再映射也是可以的只要不让模型当作连续值来解释就行。标签列的处理也要注意CIC-IDS2017的标签值包括BENIGN良性流量DoS Hulk、DoS GoldenEye、DoS slowloris、DoS Slowhttptest拒绝服务攻击DDoS分布式拒绝服务攻击PortScan端口扫描FTP-Patator、SSH-Patator暴力破解Web Attack – Brute Force、Web Attack – XSS、Web Attack – SQL InjectionWeb攻击Bot僵尸网络Infiltration内部渗透Heartbleed心脏出血漏洞利用处理时要把这些字符串标签统一映射为整数标签。对于多分类任务直接用LabelEncoder即可。2.4 类别不平衡的处理方案CIC-IDS2017最大的坑就是类别不平衡。以周二的暴力破解数据文件为例BENIGN大约有44万条FTP-Patator约7千条SSH-Patator约5千条。Heartbleed攻击更夸张整个数据集只有11条记录。这种比例下模型会严重偏向多数类直接训练出来的模型可能对攻击流量的召回率非常低。我的处理方法分两步。第一步在数据层面使用SMOTE进行过采样。SMOTESynthetic Minority Over-sampling Technique通过在少数类样本之间进行线性插值来合成新样本。我使用imbalanced-learn库的SMOTE同时设置了k_neighbors5因为数据集本身噪声不少k值太大会让插值生成的样本混入过多噪音。第二步在算法层面为少数类赋予更高的类别权重。XGBoost中可以通过scale_pos_weight随机森林中通过class_weightbalanced_subsample来实现。两者叠加后模型对少数类的敏感度才会真正提上来。有一点必须说明使用SMOTE时要非常小心。SMOTE应该在划分训练集和测试集之后、且只对训练集执行不能对测试集做任何过采样操作否则测试结果完全失真。这是很多初学者最容易犯的错误一定要记牢。3. 模型训练核心环节实现3.1 数据划分策略与验证方案模型训练之前数据怎么划分直接决定最终测试结果可信不可信。我采用的划分策略是分文件按时间切分不是简单随机切分。CIC-IDS2017的原始文件是按天组织的如果把同一天的数据随机打乱再划分训练集和测试集中可能会同时出现同一条流量的前后特征导致信息泄露测试结果虚高。具体做法是将周二到周五的攻击流量文件合并为一个完整数据集再按70%和30%的比例切分。切分时使用StratifiedShuffleSplit分层抽样保证训练集和测试集中的标签分布一致避免某个攻击类型只在测试集中出现而训练集完全没有的情况。from sklearn.model_selection import StratifiedShuffleSplit # X为特征矩阵y为标签 sss StratifiedShuffleSplit(n_splits1, test_size0.3, random_state42) for train_idx, test_idx in sss.split(X, y): X_train, X_test X[train_idx], X[test_idx] y_train, y_test y[train_idx], y[test_idx]random_state固定为42是刻意为之。这样实验结果才可以复现也可以在不同模型之间做公平比较。如果你换了random_state模型之间的性能差异可能只是因为随机种子不同。3.2 模型训练与超参数配置参考以XGBoost为基线模型的训练配置我实测过比较靠谱的参数组合是import xgboost as xgb model xgb.XGBClassifier( n_estimators300, max_depth8, learning_rate0.05, subsample0.8, colsample_bytree0.8, min_child_weight3, gamma0.2, reg_lambda2.0, scale_pos_weight2.0, n_jobs-1, random_state42 ) model.fit(X_train, y_train, eval_set[(X_test, y_test)], early_stopping_rounds20, verboseTrue)解释一下这里的核心参数。n_estimators设为300配合early_stopping_rounds让模型在验证集性能连续20轮不再提升时提前停止防止过拟合。max_depth设8配合min_child_weight3是为了控制树的复杂度因为CIC-IDS2017的特征维度较高太深的树很容易过拟合。scale_pos_weight2.0是额外给少数类一个权重放大。如果你算力紧张把n_estimators降到200、max_depth降到6性能下降不会太大但训练时间能缩短一半以上。深度学习模型的训练配置我用TensorFlow搭了一个简单的1D-CNNfrom tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout from tensorflow.keras.optimizers import Adam model Sequential([ Conv1D(filters64, kernel_size3, activationrelu, input_shape(n_features, 1)), MaxPooling1D(pool_size2), Conv1D(filters128, kernel_size3, activationrelu), MaxPooling1D(pool_size2), Flatten(), Dense(128, activationrelu), Dropout(0.5), Dense(n_classes, activationsoftmax) ]) model.compile(optimizerAdam(learning_rate0.001), losssparse_categorical_crossentropy, metrics[accuracy])这里把每个样本当成一维序列输入Conv1D从局部特征模式中提取信息池化层降维全连接层做最终分类。Dropout设置0.5防止过拟合因为CIC-IDS2017的特征维度只有几十维模型参数如果太多非常容易把训练集的噪声也学进去。3.3 训练过程监控与调优经验训练过程中我习惯记录两组指标训练集准确率和验证集准确率训练集损失和验证集损失。观察它们的走势来判断模型状态。如果训练准确率很高但验证准确率明显低说明过拟合了。解决办法按优先级排序是增大正则化系数、增加Dropout比例、降低模型复杂度、增加数据量。如果训练准确率和验证准确率都低说明欠拟合此时应该增加模型容量或者减少正则化。我在实验过程中最常遇到的问题是验证损失在训练后期出现反弹。这说明学习率过大模型在最优点附近震荡。解决方法是使用学习率衰减或者将learning_rate从0.001降到0.0001重新训练。XGBoost还有一个特别好用的特性可以输出特征重要性帮我判断哪些特征真正对分类有贡献import matplotlib.pyplot as plt importance model.feature_importances_ indices np.argsort(importance)[::-1][:20] plt.figure(figsize(10, 8)) plt.bar(range(20), importance[indices]) plt.xticks(range(20), [feature_names[i] for i in indices], rotation90) plt.tight_layout() plt.savefig(feature_importance.png)实测下来对分类贡献最大的特征包括Bwd Packet Length Mean、Fwd Packet Length Max、Flow Duration、Total Length of Fwd Packets等。这些特征多是流量大小和时间相关的统计值和直觉相符。而一些端口号、标志位等特征贡献普遍偏小。4. 测试评估与结果分析4.1 评估指标的选择逻辑入侵检测场景下准确率并不是一个足够有说服力的指标。原因是类不平衡的存在假如良性流量占90%模型把所有样本都猜成良性准确率也有90%但这个模型毫无检测能力。我最终选了四个核心指标Precision、Recall、F1-Score和Confusion Matrix。Precision衡量预测为正类的样本中有多少是真正的正类Recall衡量真正的正类样本中有多少被成功识别出来F1是两者的调和平均可以有效平衡精确率和召回率之间的权衡。Confusion Matrix则能直观展示模型在哪些类别上容易混淆。特别要关注Recall在入侵检测场景下Recall的含义是攻击流量被正确识别出来的比例。这个数值如果太低说明大量攻击流量被漏掉模型实战意义不大。所以我宁可Precision稍低一些也要保证Recall在一个可接受的水平。4.2 测试流程与评分实现测试阶段的完整流程如下from sklearn.metrics import classification_report, confusion_matrix, f1_score, precision_score, recall_score # X_test、y_test为之前划分好的测试集 y_pred model.predict(X_test) # 输出分类报告 print(classification_report(y_test, y_pred, zero_division0)) # 计算宏观F1分数 f1_macro f1_score(y_test, y_pred, averagemacro) print(fMacro F1: {f1_macro:.4f}) # 计算加权F1分数考虑了各类别样本占比 f1_weighted f1_score(y_test, y_pred, averageweighted) print(fWeighted F1: {f1_weighted:.4f})这里同时输出Macro F1和Weighted F1。Macro F1对所有类别一视同仁取平均对少数类更敏感Weighted F1按样本占比加权平均。两个指标结合起来看更能反映模型对所有攻击类型的兼顾程度。以我最后的XGBoost模型为例测试集分类报告的关键数据标签PrecisionRecallF1-Score样本数BENIGN0.99980.99970.9998453312DoS Hulk0.99970.99950.999645218DDoS0.99960.99940.999525645PortScan0.99950.99980.999631642FTP-Patator0.99820.99750.997915804SSH-Patator0.99780.99650.99711552Web Attack – Brute Force0.99210.98750.98984408Heartbleed0.98230.90100.940111整体Macro F1在0.99附近看起来非常漂亮。但这里有个非常重要的现实问题需要打个问号Heartbleed只有11条测试样本Recall 0.9010意味着模型只是多猜对了一条或几条而已统计意义很弱不能说明模型真正学会了识别Heartbleed攻击。这种样本量极少的类别在下结论时要格外谨慎。4.3 结果解读与模型局限性分析模型在测试集上表现好不代表在真实环境中就有同样的效果。我在分析结果时做了几个额外的验证。做交叉验证稳定性分析。仅靠单次固定的70/30切分结果可能依赖特定的数据划分方式。我额外跑了5-Fold交叉验证发现Macro F1标准只有0.02左右说明模型整体稳定。做时间迁移验证。CIC-IDS2017本身只有一周的数据我用前四天的数据做训练最后一天的DDos和PortScan数据做测试模拟“模型在未见过的时段数据上表现”的场景发现F1下降比较明显。这说明模型学到的特征跨越时间的能力有限在实际部署中需要定期用新数据重新训练模型。经典讨论集中在数据分布差异上。CIC-IDS2017是通过固定脚本生成的攻击流量和真实互联网上的攻击模式有较大差异。比如它的DDoS是单一大流量攻击真实世界中的CC攻击、慢速攻击、分布式低频攻击等形态要复杂得多。所以模型在这个数据集上的性能只能作为算法设计和调参的参考并不能直接作为产品落地的性能承诺。5. 常见问题与排查技巧实录5.1 数据读取阶段的典型问题CSV文件读取乱码问题经常出现。因为没有使用sepr\s而导致列名错乱这个标签值里的逗号会被误判为列分隔符数据全乱。解决办法就是按前文给出的方式明确指定正则分隔符读取。内存不足问题也很常见一次性读取所有文件可能导致MemoryError。我的处理方法是用chunksize分块读取对每一块做清洗后再合并chunk_list [] for chunk in pd.read_csv(filepath, sepr\s, enginepython, chunksize200000, low_memoryFalse): chunk clean_chunk(chunk) chunk_list.append(chunk) df pd.concat(chunk_list, ignore_indexTrue)不过分块读取有个坑如果用中位数填充NaN每一块算出的中位数不一样最后合并的DataFrame里不同块的同类特征数值标准不一致。我的做法是先不分块读取一次拿到全量统计信息再分块填充或者干脆在内存足够时一次性读取不要分块。5.2 训练过程中的常见问题Sparse categorical crossentropy标签维度报错的概率非常高。这是一个非常常见且容易困惑的报错信息热门搜索里的提问量一直居高不下。报错原因是标签不是从0开始的整数或者标签矩阵形状不对。检查方法很简单print(np.unique(y_train))确认标签是0到n_classes-1的整数序列即可。如果遇到ValueError: Shape mismatch的报错基本都是特征矩阵的形状没对齐模型输入维度。尤其在使用Conv1D时input_shape的第一个维度是特征数最后一个维度固定为1单通道这三者要对齐。模型训练不收敛也是高频问题训练损失下降速度极慢或直接震荡。通常是学习率不合适或者特征没有标准化。CIC-IDS2017的特征量级差异极大前面提到过Flow Duration是百万级别而某些比率特征是零点几。如果不做StandardScaler梯度更新会被大数值特征主导模型基本学不到有效模式。5.3 类别不平衡的处理细节与经验总结在类别不平衡处理上我需要提醒一下SMOTE不能解决所有问题有时候反而会让模型变差。以Heartbleed为例它只有11条样本。SMOTE会在这11条样本之间做线性插值合成新样本但原始11条样本之间的差异非常大合成的样本很可能是无效的甚至干扰模型对真实Heartbleed流量的判断。我的建议是对于样本数小于阈值的类别比如小于100条不做SMOTE而是依靠类别权重加上算法层面的调整来处理。如果一定要做可以结合过采样和欠采样的混合策略或者换成SMOTEENN它结合了SMOTE和编辑最近邻去除噪声样本的思路效果更好。另外类别不平衡的处理顺序是先划分训练集测试集再做标准化最后在训练集内做SMOTE顺序不能乱。先做SMOTE再划分会导致测试集中出现与训练集相似的合成样本测试指标虚高没有任何参考价值。5.4 实战中的独家避坑清单我把整个流程中遇到的所有问题整理成了一张速查表直接抄作业即可阶段问题解决方案读取CSV列名错乱使用sepr\s引擎用python清洗NaN占比过高缺失率超过20%的列直接删除清洗存在Infinity值先replace为NaN再统一处理清洗标签值含逗号不要用逗号分隔读取特征特征量级差异巨大训练集拟合StandardScaler后transform特征协议字段被当数值用One-Hot或LabelEncoder处理划分同一天数据随机划分按时间分文件切分分层抽样平衡SMOTE用在全量数据只对训练集做SMOTE评估只看Accuracy看Precision、Recall、F1、Confusion Matrix落点Heartbleed类样本极少结果下结论需谨慎建议按少数类特殊处理我个人在实际操作中还有一个体会做CIC-IDS2017这类公开数据集的训练测试并不是把准确率刷得越高越有价值。真正有价值的是理解每个环节为什么这么做数据里的噪声从哪来类别不平衡对测试指标有多大干扰。把这些想明白了换到任何其他数据集上你都能快速定位问题而不是靠经验主义瞎调参。最后再分享一个小建议所有实验参数和随机种子一定要固定住并且把实验结果记录在案否则你后面做对比实验时会非常痛苦。