做入侵检测相关的工作,CIC-IDS2017 这个数据集基本是绕不开的。我最早用它的时候,光是把那 8 个 CSV 文件拼到一起读进内存就折腾了半天,后面又栽在数据清洗和评估指标上。这篇文章把我从下载原始数据、预处理、特征工程到模型训练测试的完整流程梳理出来,代码都是我自己跑过改过的,你可以直接拿去用。不管你是刚接触网络流量分析的新手,还是想快速验证一个检测模型的老手,这套流程都能帮你少踩几个坑。
1. 先搞清楚 CIC-IDS2017 到底是个什么数据集
很多人拿到 CIC-IDS2017 就直接开始读 CSV,结果发现列名带着空格、里面有 Infinity 和 NaN,训练出来的模型还莫名其妙的差。问题就出在没搞懂这个数据集的“出身”。它由加拿大网络安全研究所发布,目的是提供一份接近真实网络流量的入侵检测基准数据。跟老一代的 KDD99、NSL-KDD 相比,它的流量场景更丰富,特征也更贴近现代网络环境,所以现在学术界和工业界做 IDS 评估大多转到了这个数据集上。
1.1 数据集背景与 8 个 CSV 文件的对应关系
CIC-IDS2017 的原始数据是 5 天抓的 PCAP 包,分成了周一到周五。官方用 CICFlowMeter 工具从 PCAP 里提取出流特征,生成了一组 CSV 文件。很多资料只说“8个CSV”,但没告诉你这几个文件分别对应哪些攻击场景:
| 文件名称 | 对应时间 | 主要攻击类型 |
|---|---|---|
| Monday-WorkingHours.pcap_ISCX.csv | 周一上午、下午 | 仅有正常流量(BENIGN) |
| Tuesday-WorkingHours.pcap_ISCX.csv | 周二上午、下午 | FTP-Patator、SSH-Patator(暴力破解) |
| Wednesday-WorkingHours.pcap_ISCX.csv | 周三上午、下午 | DoS slowloris、DoS Slowhttptest、DoS Hulk、DoS GoldenEye、Heartbleed |
| Thursday-WorkingHours-Morning-WebAttacks.pcap_ISCX.csv | 周四上午 | Web Attack – Brute Force、Web Attack – XSS、Web Attack – Sql Injection |
| Thursday-WorkingHours-Afternoon-Infiltration.pcap_ISCX.csv | 周四下午 | Infiltration 内网渗透攻击 |
| Friday-WorkingHours-Morning.pcap_ISCX.csv | 周五上午 | Botnet 僵尸网络流量 |
| Friday-WorkingHours-Afternoon-PortScan.pcap_ISCX.csv | 周五下午 | PortScan 端口扫描 |
| Friday-WorkingHours-Afternoon-DDoS.pcap_ISCX.csv | 周五下午 | DDoS LOIT |
这些文件在官方压缩包里其实放在 MachineLearningCSV 目录下,下载后建议先解压到本地固定路径。注意周一的文件只有正常流量,是用来做基线训练的。真正做分类时,通常把多个文件拼起来。
1.2 特征维度与标签体系速览
CICFlowMeter 对每条双向流提取了 80 多个特征,常见列有 Flow Duration、Total Fwd Packets、Total Backward Packets、Fwd Packet Length Mean、Flow Bytes/s、Flow Packets/s、Average Packet Size 等等。最后一列是 Label,它的值直接是 BENIGN 或者具体的攻击名,比如 DoS Hulk、PortScan、DDoS。
这里有一个特别容易让人困惑的地方:CSV 第一行表头其实是带空格的。比如 " Destination Port" 前面有一个空格," Flow Duration" 前面也有空格。直接用 pandas 读取时列名会带着这些空格,处理起来极容易踩坑。我一般读取后直接 strip 一遍列名,省得后面每次写代码都出问题。
另外,同一份数据集在不同论文里报告的结果差异很大,原因之一就是有人把 15 类标签压缩成二分类(正常/异常),有人用多分类,还有人把少数攻击类合并到“其他攻击”。做实验前先想好自己的任务定义,不要拿别人的结果直接横向比较,否则会被误导。
1.3 为什么选这个数据集而不选 KDD99 或 NSL-KDD
KDD99 和 NSL-KDD 虽然经典,但它们的流量特征来源于 1998 年的网络环境,放到现在做实时入侵检测说服力不够。CIC-IDS2017 的 PCAP 采集为期 5 天,包含大量真实网络背景流量,攻击类型覆盖了暴力破解、Web 攻击、DoS、僵尸网络、端口扫描和 DDoS 等主流威胁。
它的缺点也很明显:数据量大,完整读入内存需要十几 GB RAM;类极度不平衡,BENIGN 占比很高,部分攻击样本很少;CSV 里有非法值。这些问题不是 bug,而是真实场景的产物,反而更接近落地部署会遇到的情况。所以我个人一直把它作为入侵检测算法验证的首选基准。
2. 搭建训练测试环境,把数据完整读进来
CIC-IDS2017 的 CSV 文件加起来有 8 个,单文件几百 MB 到 1GB 以上,直接 pd.read_csv() 全部拼起来会吃很多内存。我在 16GB 内存的老机器上跑过,拼完确实能读,但再做特征工程就容易卡死。建议有条件的话先把环境内存提到 32GB,或者用分批读取的思路。
2.1 推荐环境与依赖清单
我自己比较稳定的组合是这样的:
- Python 3.9 以上(3.10、3.11 都行)
- pandas 2.x
- numpy
- scikit-learn
- lightgbm 或 xgboost(二选一,后面训练会用到)
- matplotlib、seaborn(画混淆矩阵和特征分布用)
- imbalanced-learn(做采样处理时用)
如果只有 CPU,跑传统机器学习完全够用。深度模型可以后面再考虑 GPU,先把流程跑通最重要。
2.2 加载全部 CSV 的脚本模板与内存优化
我把加载脚本写成固定模板,每次打开新项目都是复制这个文件再改路径。核心逻辑是遍历目录下所有以 .csv 结尾的文件,pandas 读取后先把列名空格去掉,再加一列用来标记来源文件,最后用 concat 合并。
import pandas as pd import glob import os data_dir = "./MachineLearningCSV/MachineLearningCSV/" df_list = [] for csv_file in sorted(glob.glob(os.path.join(data_dir, "*.csv"))): print("正在读取:", csv_file) tmp_df = pd.read_csv(csv_file) # 去掉列名首尾空格,CIC-IDS2017的列名很多带前导空格 tmp_df.columns = [col.strip() for col in tmp_df.columns] df_list.append(tmp_df) df = pd.concat(df_list, ignore_index=True) print("合并后数据集形状:", df.shape) print("标签分布:") print(df["Label"].value_counts())这段代码跑完之后,你会看到一个很大的 DataFrame。首次加载时我建议先不要直接做任何操作,先打印 shape 和标签分布,确认 8 个文件都被正确读进来。如果出现某几个文件读不到,多半是路径问题,用 glob 输出所有匹配文件排查即可。
2.3 数据清洗:Infinity、NaN、类别标签编码
CIC-IDS2017 里有个老熟人:攻击样本的某些特征列会出现 Infinity 或者 NaN。这些值不处理掉,训练时 sklearn 直接报错,或者模型莫名其妙学出很差的边界。我的固定处理流程分四步:
- 把非数值列过滤掉,只保留 float64 / int64 类型列
- 把 Label 列单独拎出来
- 对剩余特征列用 np.isfinite 筛掉包含空值、无穷值的行
- 把类别标签做编码,同时记录类别列表方便后面画混淆矩阵
import numpy as np from sklearn.preprocessing import LabelEncoder # 先保存标签列 label_col = df["Label"] # 筛选数值型特征列 num_cols = df.select_dtypes(include=[np.number]).columns.tolist() feature_df = df[num_cols].copy() # 将无穷值和缺失值统一转为 NaN feature_df = feature_df.replace([np.inf, -np.inf], np.nan) feature_df = feature_df.dropna(axis=0, how="any") # 对应的标签行也要对齐 label_col = label_col.loc[feature_df.index] le = LabelEncoder() y = le.fit_transform(label_col) X = feature_df print("清洗后特征矩阵:", X.shape) print("类别映射:", dict(zip(le.classes_, le.transform(le.classes_))))注意一个细节:dropna(axis=0) 会删掉所有包含 NaN 的行,但特征里原本也有像 Flow Bytes/s 这种分母为 0 导致无穷大的列,务必先替换再删除。如果这里图省事不做清洗,后面随机森林跑起来虽然能出结果,但测试集的分数会变飘,很难复现。
3. 特征工程与样本切分:按训练集测试集的规矩来
特征工程这一步决定了你模型能力的上限。CIC-IDS2017 自带 80 多维特征,但并不是全部都有用。有些列是标识列,比如 Flow ID、Source IP、Destination IP,直接参与训练会带来严重的数据泄漏,模型在测试集上看着分数很高,实际部署时一塌糊涂。
3.1 特征选择思路与过滤非数值列
我在处理时固定过滤掉这几类列:Flow ID、Source IP、Destination IP、Timestamp 这几种对分类任务没有泛化意义的列。还有一类坑是 Source Port、Destination Port,端口本身不完全是噪声,特别是 PortScan 场景下目的端口很关键,但如果你的测试环境网络和训练环境差异很大,端口可能会导致过拟合。我的做法是先用全特征跑一个基准结果,再对比是否去掉端口列,根据验证集表现决定。
drop_cols = ["Flow ID", "Source IP", "Destination IP", "Timestamp"] X = X.drop(columns=[c for c in drop_cols if c in X.columns], errors="ignore")做完这步后特征数量通常在 78 左右。后续想压缩特征做快速验证时,可以用随机森林的 feature_importances_ 取 top 20,或者直接跑一个 LightGBM 看重要性排序。实测下来,Flow Duration、Fwd Packet Length Mean、Bwd Packet Length Mean、Flow Bytes/s、Average Packet Size 这几个特征的贡献度都比较靠前。
3.2 切分策略与标签平衡处理
CIC-IDS2017 有多种切分方式。最朴素的是随机切分:用 train_test_split(X, y, test_size=0.3, stratify=y) 保证训练集和测试集里各类别比例一致。但更严格的实验会按时间切,例如周一到周四做训练、周五做测试,这样更接近攻击检测的“未来数据”场景。
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) print("训练集形状:", X_train.shape) print("测试集形状:", X_test.shape)stratify 参数一定要带上,因为数据集极度不平衡。如果不做分层抽样,极端情况下测试集里某个攻击类别一条样本都没有,F1 分数会直接失真。另一个问题是大部分正常流量和攻击流量样本数量差距悬殊。我见过有人直接全部训练,结果模型在 DDoS 上表现不错,但在 Web Attack 上几乎没效果。先跑通流程可以不做采样,如果要追求更好的均衡指标,再考虑 SMOTE 或者 NearMiss。
3.3 标准化与避免数据泄漏
决策树、随机森林这类树模型不需要做标准化。但如果你用神经网络、逻辑回归或者 KNN,就必须对特征做标准化。这里有个非常关键的禁忌:fit 只能在训练集上做,然后再 transform 测试集,绝对不能用整个数据集去 fit,否则就是数据泄漏,测试结果偏高。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)如果你想要更高的一致性,可以把它封装进 sklearn 的 Pipeline。我自己的习惯是把预处理和模型一起做成 Pipeline,这样在调参和交叉验证时可以避免重复写清洗逻辑,也防止误用测试集信息。
4. 模型训练:从传统机器学习到深度网络
CIC-IDS2017 上表现好的模型不少。传统机器学习里,随机森林和 LightGBM 是性价比之王,训练快、效果稳。深度学习方面,简单的多层感知机(MLP)也能达到不错的效果,但需要更细调参。我的建议是先跑树模型拿到一个强基线,再决定要不要上深度网络。
4.1 随机森林与 XGBoost 的快速上手代码
先跑随机森林,用默认参数快速看效果:
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report rf_clf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1) rf_clf.fit(X_train, y_train) y_pred_rf = rf_clf.predict(X_test) print(classification_report(y_test, y_pred_rf, target_names=le.classes_))如果特征数量多,我建议直接上 LightGBM,训练速度快且准确率通常略高于随机森林:
import lightgbm as lgb lgb_clf = lgb.LGBMClassifier( n_estimators=200, learning_rate=0.1, num_leaves=31, random_state=42, n_jobs=-1 ) lgb_clf.fit(X_train, y_train) y_pred_lgb = lgb_clf.predict(X_test) print(classification_report(y_test, y_pred_lgb, target_names=le.classes_))这一步不需要刻意把参数调得太细。先看分类报告,确认哪些类别 Recall 低,再针对性地做特征或采样,比盲目调参高效得多。
4.2 简单 DNN 分类器的实现
树模型效果虽好,但很多时候项目要求用深度学习方案,便于后续做在线增量或部署到 GPU 服务上。我用 Keras 搭过一个简单的 MLP,结构是 128-64-32 的全连接层,加 Dropout,输出层 softmax。这里不展开太多调参细节,给一个能跑的模板。
import tensorflow as tf from tensorflow.keras import layers, models model = models.Sequential([ layers.Input(shape=(X_train_scaled.shape[1],)), layers.Dense(128, activation="relu"), layers.Dropout(0.3), layers.Dense(64, activation="relu"), layers.Dropout(0.3), layers.Dense(32, activation="relu"), layers.Dense(len(le.classes_), activation="softmax") ]) model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] ) history = model.fit( X_train_scaled, y_train, validation_split=0.2, epochs=20, batch_size=256, verbose=1 )注意这里 y_train 不需要做 one-hot,用 sparse_categorical_crossentropy 配合整数标签就行。跑 20 个 epoch 后观察验证损失曲线,如果验证损失开始反弹,说明已经过拟合,应该用 EarlyStopping 提前结束。
4.3 训练过程的监控、过拟合判断与调参心得
训练过程中我习惯记录三样东西:训练集损失、验证集损失、验证集 F1。对于树模型,直接看分类报告里各个类别的 Precision 和 Recall 是否均衡。对于神经网络,画 loss 曲线非常关键。如果训练 loss 不断下降但验证 loss 翘头,就是典型的过拟合。解决办法包括加大 Dropout、减小网络宽度、加 L2 正则、减少 epoch 或用早停。
调参时不要一上来就跑网格搜索。CIC-IDS2017 类多、样本多,全数据量下网格搜索非常耗时。先用小采样跑一组默认参数建立基线,然后用 Optuna 或 RandomizedSearchCV 在关键参数上搜索,比如 n_estimators、max_depth、learning_rate、num_leaves。这样既省时间,又能找到比较稳定的参数组合。
5. 测试报告与模型评估:别只看 Accuracy
很多初学者在 CIC-IDS2017 上跑完,一看 Accuracy 99%,就觉得模型无敌了。实际上这个数据集类别严重不平衡,BENIGN 占了绝大多数,如果你的模型把 BENIGN 全部判断对,把其他攻击大部分判断错,Accuracy 照样可能超过 95%。所以必须看细粒度的评估指标。
5.1 分类报告、混淆矩阵与 ROC-AUC
sklearn 的 classification_report 是必看的。它输出每个类别的 precision、recall、f1-score,还有一个 support,代表该类别在测试集里的样本数。我通常会重点看那些 support 很小的类,比如 Web Attack – Sql Injection、Infiltration、Heartbleed。这些类样本少、难识别,recall 低是常态,但如果在报告里直接消失,多半是模型把所有样本都预测成了别的类。
混淆矩阵能告诉你具体错在哪里。我用 seaborn 画热力图,把对角线之外的错误配对比看清楚:
import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import confusion_matrix cm = confusion_matrix(y_test, y_pred_lgb) plt.figure(figsize=(16, 12)) sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=le.classes_, yticklabels=le.classes_) plt.xticks(rotation=90) plt.yticks(rotation=0) plt.tight_layout() plt.savefig("confusion_matrix.png", dpi=150)ROC-AUC 在多分类场景下要按类别分别计算,然后做 macro 平均或 weighted 平均。但说实话,对入侵检测来说,我更关心的是“攻击样本到底有没有被漏掉”,所以 Recall 和 FPR 的参考价值往往比单一 AUC 更高。
5.2 多分类下的宏平均与加权平均
多分类评估指标有两个容易混淆的版本。macro average 是先计算每个类别的指标再做算术平均,它对小样本类别更敏感,如果你的模型把小攻击类学得很差,macro F1 会被拉低。weighted average 是按各类样本量加权平均,结果偏向大类,容易掩盖小类的问题。报告入侵检测结果时,我一般同时给出 macro F1 和 weighted F1,并单独列出关键攻击类的检测率。否则评审或者老板根本看不出你的模型对小攻击类的真实水平。
5.3 FPR 与检测率的真实含义
入侵检测常用的两个指标是检测率和误报率。检测率等价于召回率,也就是真实攻击样本中被正确识别出来的比例。误报率是正常流量被误判为攻击的比例,这个数值在实际部署中非常重要。假设误报率是 0.1%,但网络每秒几万条流量,一天下来误报数量就很可观,安全运营人员会被报警疲劳拖垮。
所以选择模型时我不会只盯着测试集上的 F1,而是用分类阈值进一步压低误报率。比如对 LightGBM,可以用 predict_proba 拿到每个类别的概率,再设定一个阈值,只有概率超过阈值才判定为攻击,否则归为 BENIGN。这样能灵活平衡检测率和误报率。
6. 常见问题排查与避坑实录
CIC-IDS2017 用久了,总会遇到一些重复出现的问题。我把踩过比较多的坑整理成一张速查表,方便你排查。
6.1 数据格式问题速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| pandas 读入后列名带空格 | CICFlowMeter 生成 CSV 的表头有前导空格 | 读取后对 columns 做 strip |
| 训练时报 "Input contains NaN" | 攻击样本的某些特征存在 NaN/Infinity | 先 replace inf 为 NaN,再用 dropna |
| 标签列读出来有前导空格 | 和列名问题同源 | 对 tag 列做 strip 或者统一 replace |
| 多文件拼接后样本重叠 | 不同时间段的 PCAP 理论上不会重叠,但注意去重 | 可用 df.drop_duplicates() 去重 |
| 内存不足直接死机 | CSV 文件太大,拼接后占用高 | 只读取需要的列,或使用 dtype 压缩 |
6.2 内存溢出与服务器环境建议
如果你只有 16GB 内存的笔记本,建议别一次性用 pd.concat 拼 8 个大 CSV。你可以用 chunked 读取,或者只读特定列。pandas read_csv 里可以用 usecols 参数指定需要的列,这样内存占用能降一半以上。
# 只需要读取数值特征和标签 cols_to_read = [" Destination Port", " Flow Duration", " Total Fwd Packets", " Label"] tmp_df = pd.read_csv(csv_file, usecols=cols_to_read)另外,如果是在服务器上训练,记得优先用磁盘空间充足的目录解压,/tmp 空间不够会直接导致写入失败。我踩过一次坑,解压到 /tmp 导致空间写满,后面所有 CSV 都读取失败。
6.3 样本不平衡引发的评估陷阱
CIC-IDS2017 的不平衡程度非常极端。BENIGN 样本是所有的攻击样本总量好几倍,而像 Web Attack – SQL Injection、Infiltration 这类攻击的样本数量可能只有几百条。这种情况下如果你不做采样,模型很难学到少数类特征。
最简单的处理办法是训练时给 sklearn 模型传 class_weight="balanced",LightGBM 里可以设置 is_unbalance=True 或 scale_pos_weight。对少数攻击类,我建议单独监督指标,观察每个攻击类的 recall 是否低到离谱。如果确实太低,可以尝试对少数类做 SMOTE 过采样,但也要小心过采样导致模型过拟合。最好用验证集而不是测试集来评估过采样效果,防止把测试集信息间接引入训练。
最后分享一个小技巧
我在跑了很多轮实验之后养成了一个习惯:每次训练前先随机抽 5000 条样本快速跑一遍完整流程,从加载、清洗、训练到评估都用最小的数据量过一遍,确认代码和指标没有明显问题后,再上全量数据。这个习惯帮我避免了无数次“熬夜等一个明显错误的结果”。
另一个非常值得做的事是保存每次实验的配置和结论。CIC-IDS2017 的数据量大,一次全量训练可能要几十分钟,如果你不记录参数和切分随机种子,后面复现结果会非常难受。我用 CSV 记录每组实验的数据集版本、特征列、模型参数、训练时间、验证集 F1 和测试集 F1,这样对比分析时一目了然。
如果你后续想把这套流程扩展到实时流量检测,可以在这个基础上引入滑动窗口重训练机制,用新抓取的流量持续更新模型。CIC-IDS2017 作为离线评测基准很成熟,但真实环境还要考虑加密流量和概念漂移,那是后话了。