简介:这是一份基于Python的神经网络流量异常检测项目资源,面向信息安全与机器学习方向的学习者,适合毕业设计、课程设计或工程实训。项目基于CICIDS2017数据集,使用Pandas完成预处理与标准化,并通过TensorFlow内置Keras搭建DNN或LSTM模型,实现流量分类与异常识别,帮助读者掌握从数据清洗到模型优化的完整流程。压缩包共8个文件,包含5个CSV数据文件、2个Python脚本及1个说明文档,整体大小约10.58MB,数据、代码与说明分层清晰,便于对照学习。目前已有297人学习,资源可直接运行调试,也可作为算法对比与论文撰写的参考基础。
1. 从 CICIDS2017 到神经网络:流量异常检测到底在解决什么问题
公司 IDS 每天产生海量告警,其中大部分是误报。用 python 做神经网络流量异常检测,本质上是把每条网络流看成一行特征向量,让模型去回答「正常还是异常」。如果你手里刚拿到 CICIDS2017 数据集和两个基于 Keras 的神经网络脚本——DNN 和 LSTM——那么整个项目可以拆成清晰的四步:Pandas 预处理、建模、调参、评估。经历过完整流程的人会把重心放在三个地方:数据清洗、类别不平衡、评估指标的选择。这三个环节做好之后,无论写毕设做课程设计还是搭初期的工程原型,都能直接复用。本文按一条可复现的路线拆到底,所有代码在 TensorFlow 2.x 下可以直接跑。
2. 数据预处理:Pandas 清洗 CICIDS2017 的正确顺序
流量异常检测项目里,模型结构反而不是最耗时间的部分,数据预处理才是。CICIDS2017 是加拿大网络安全研究所 2017 年发布的基准数据集,其原始 pcap 经过 CICFlowMeter 特征提取后,得到 78 维流量统计特征加一个 Label 字段。
2.1 数据集结构与各 CSV 文件的对应关系
这个项目压缩包里的几个 CSV,分别对应不同的流量类别。BENIGN.csv 是正常流量,DDoS.csv 是分布式拒绝服务攻击,DoS Hulk.csv 是 Hulk 变种的拒绝服务攻击,PostScan-real.csv 是端口扫描流量,binary_classification.csv 则是把上述类别合并成「正常/异常」二分类之后的版本。
文件名和原始 CICIDS2017 官方命名略有出入,这是因为不同渠道下载后被人重新整理过,以你本地的实际文件名为准。处理这类多文件数据集的第一步,是先看清楚每个文件的列结构是否一致。CICIDS2017 每个 CSV 的表头都包含 Flow ID、Src IP、Dst Port、Protocol、Flow Duration、Packet Length Mean 等字段,但不同攻击流量的某些列取值范围差异很大,比如 DDoS 的 Flow Duration 可能集中在极短时间区间,而 PortScan 的连接数和标志位分布又不一样。
2.2 多文件合并与缺失值清洗
项目里 DNN_IDS.py 和 LSTM_IDS.py 都要复用同一份清洗逻辑,所以推荐把多文件合并和缺失值处理写在数据准备阶段。下面这段是常用的合并方式:
import pandas as pd import glob file_map = { 'benign': 'BENIGN.csv', 'ddos': 'DDoS.csv', 'hulk': 'DoS Hulk.csv', 'portscan': 'PostScan-real.csv', } frames = [] for label, path in file_map.items(): df = pd.read_csv(path) df['source'] = label # 记录来源,方便后续回溯 frames.append(df) df = pd.concat(frames, ignore_index=True) print(df.shape) print(df['source'].value_counts())pd.concat的ignore_index=True会把多个 CSV 的行索引重排,避免后续 train_test_split 时索引重叠。source字段用来追踪每行来自哪个文件,调试阶段很有用。合并之后先不要急着删列,先检查缺失值比例再决定策略。
CICIDS2017 里存在两种典型的脏数据:单元格里的NaN和Infinity。前者来自某些协议字段无法提取时留下的空位,后者来自除零计算产生的无穷值。处理它们时可以分两步走:
import numpy as np # 把无穷值替换为 NaN,方便统一处理 df = df.replace([np.inf, -np.inf], np.nan) # 统计每列的缺失比例 missing_ratio = df.isnull().mean() print(missing_ratio[missing_ratio > 0.01])注意这里用replace而不是先用dropna。如果直接把含Infinity的行删除,你删掉的行数大概率比预期多很多。实际项目里我通常会先用isnull().mean()看缺失比例分布,对缺失比例超过 50% 的特征列直接丢弃,对缺失比例很低的列才执行dropna()。这样既保留样本量,又不会让模型拟合到太多空值。
2.3 特征标准化:StandardScaler 与 MinMaxScaler 的选型
神经网络对输入特征的尺度很敏感。CICIDS2017 的特征里,Flow Duration 可能上百万微秒,而 Packet Length Min 可能只有几十字节,量纲差距达到四个数量级。如果不做标准化,梯度更新会被大数值特征主导,小数值特征几乎学不到东西。
对于流量统计特征,我一般默认选StandardScaler,它对均值方差归一化后,特征分布接近标准正态,配合 ReLU 激活函数收敛更稳定。MinMaxScaler更适合特征本身有明确上下界、且不要求分布形状的场景,比如已经归一化过的协议标志位。实际编码如下:
from sklearn.preprocessing import StandardScaler, LabelEncoder from sklearn.model_selection import train_test_split feature_cols = df.select_dtypes(include=['float64', 'int64']).columns.tolist() feature_cols.remove('source') # source 是字符串列,select_dtypes 不会选中 X = df[feature_cols].fillna(0).values y_raw = df['Label'].values # 统一编码:BENIGN -> 0,攻击类型 -> 1(或保留多分类) le = LabelEncoder() y_binary = (y_raw != 'BENIGN').astype(int) scaler = StandardScaler() X_scaled = scaler.fit_transform(X) X_train, X_test, y_train, y_test = train_test_split( X_scaled, y_binary, test_size=0.3, random_state=42, stratify=y_binary )fit_transform只能用在训练集上,测试集必须用scaler.transform(X_test),否则训练集和测试集的均值方差就不一致,相当于数据泄漏。stratify=y_binary保证切分后训练集和测试集中正常流量与攻击流量的比例与原始数据一致,这一点在类别不平衡的场景里尤其重要,CICIDS2017 的正常流量样本量远大于攻击流量,不 stratify 的话测试集里可能几乎全是正常样本。
2.4 标签处理:二分类还是多分类
压缩包里存在binary_classification.csv,说明项目作者已经把多类别标签合并成了二分类。实际做的时候要看你的目标是「检测是否有异常」还是「识别具体攻击类型」。后者需要把Label字段做多分类编码:
y_multi = le.fit_transform(y_raw) # 0=benign, 1=ddos, 2=hulk, 3=portscan多分类时输出层改成 softmax,损失函数改成categorical_crossentropy,其余流程不变。二分类是简化版,适合先跑通全流程;多分类是完整版,适合写论文时展示各类攻击的检测效果。从工程实践角度看,先用二分类拿到 baseline,再逐步扩展到多分类,踩坑会少很多。
3. DNN-IDS:用 Keras 搭建前馈神经网络做异常分类
流量异常检测里最基础的深度模型就是全连接神经网络,也就是项目里的 DNN_IDS.py。它结构简单、训练快,适合作为所有对比实验的基线模型。
3.1 为什么选 DNN 而不是 CNN
CICIDS2017 里的每条记录是 CICFlowMeter 聚合后的统计特征向量,而不是原始报文。特征之间没有天然的局部空域相关性,所以 CNN 卷积核的平移不变性在这里并没有明显优势。DNN 的每个神经元连接前一层的所有输出,能直接建模特征与特征之间的非线性组合关系,这是表格型数据建模最常见的选择。
另外不少人会拿 BP 神经网络来对比,BP 和这里说的 DNN 在结构上等价,只是 BP 强调误差反向传播的更新过程,DNN 强调多层堆叠的表达能力。在 Keras 里搭建时,你不需要手工实现反向传播,框架已经帮你处理了自动求导,你需要关注的是层数、神经元数量、激活函数和正则化策略。
3.2 DNN_IDS.py 的网络结构与 Dropout 作用
一个可以稳定跑通的结构是 128 → 64 → 32 的三层全连接。输入维度是数据清洗后的特征数,约 77 维(去掉标签列后)。第二层开始配合 BatchNormalization 和 Dropout,防止参数过多导致过拟合。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout, BatchNormalization from tensorflow.keras.optimizers import Adam model = Sequential([ Dense(128, activation='relu', input_shape=(X_train.shape[1],)), BatchNormalization(), Dropout(0.3), Dense(64, activation='relu'), Dropout(0.2), Dense(32, activation='relu'), Dense(1, activation='sigmoid') ])Dropout(0.3)表示训练时每一轮随机丢弃 30% 的神经元输出,这样模型不会过度依赖某一个特征组合。BatchNormalization放在卷积和全连接层之后,激活函数之前,能缓解梯度消失问题,也让学习率可以适当调大。
最后一层只有一个神经元,激活函数用sigmoid,输出值表示该样本属于攻击流量的概率。之后判断时以 0.5 作为默认阈值,但如果正常流量和攻击流量比例悬殊,0.5 往往不是最优阈值,后面第 5 章会讲怎么调。
3.3 编译与训练:学习率、batch size 与早停
model.compile(optimizer=Adam(learning_rate=0.001), loss='binary_crossentropy', metrics=['accuracy']) history = model.fit( X_train, y_train, validation_split=0.2, batch_size=256, epochs=50, callbacks=[EarlyStopping(patience=5, restore_best_weights=True)] )Adam是工程里默认选择,它对学习率的适应能力比 SGD 强很多,learning_rate=0.001是 Keras 的默认值,也是大多数表格建模的一个可靠起点。batch_size=256在 CICIDS2017 这种几十万行的数据集上比较合适,显存占用小且梯度估计足够稳定。
validation_split=0.2表示在训练数据中再划出 20% 作为验证集,用于监控每一轮的泛化表现,不影响模型参数更新。配合EarlyStopping(patience=5),当验证集损失连续 5 轮没有下降时,训练自动终止,restore_best_weights=True会把模型参数回滚到验证集效果最好的那一轮。这一点比手动保存权重再 reload 更省事。
3.4 多分类改动点与实践建议
如果要把二分类模型改成多分类,改动集中在三处:标签编码、损失函数、输出层。多分类输出层用Dense(num_classes, activation='softmax'),损失函数改为categorical_crossentropy,同时 y 标签要做 one-hot 编码,Keras 也支持直接用整数标签配合sparse_categorical_crossentropy。
初始化不同攻击类型数量过多时需要特别注意:某些攻击类型在数据集中本身样本量就很低,多分类下模型很容易把它忽略掉,全部预测成样本量最大的那一类。遇到这种情况,先把低频攻击类型合并成「other_attack」,或者改用第 5 章讲的加权损失会更实际。
4. LSTM-IDS:把流量当成时间序列来建模
项目里另一个模型是 LSTM_IDS.py。LSTM 引入了一个 DNN 没有的假设:流量样本之间可能有一定的时间顺序关系。
4.1 LSTM 用于流量异常检测的合理性
DDoS 攻击的过程往往有逐渐加剧的趋势,端口扫描更是天然具有时间序列特征,先探测端口,再尝试漏洞利用。如果只用单条流量的统计特征,模型看不到这个时间上下文。LSTM 通过门控机制保留历史信息,理论上能捕捉这种跨样本的时序模式。
但 CICIDS2017 的 CSV 数据没有显式的时间戳列,每条记录已经聚合成了独立特征。常见做法是把连续的多条流量记录拼成一个滑动窗口,用窗口内所有样本的特征来预测最后一条样本的标签。这样把表格数据转成了弱时序数据,保留了数据在文件中的原始排列顺序。
4.2 数据 reshape 成三维张量
LSTM 输入要求三维形状:(样本数, 时间步长, 特征维度)。下面用滑动窗口把上一章的X_scaled转换:
import numpy as np seq_len = 10 # 每个窗口包含 10 条连续流量记录 X_seq, y_seq = [], [] for i in range(len(X_scaled) - seq_len + 1): X_seq.append(X_scaled[i:i + seq_len]) y_seq.append(y_binary[i + seq_len - 1]) X_seq = np.array(X_seq) y_seq = np.array(y_seq)窗口生成后,第 i 个样本的特征是第 i 到 i+9 行流量记录,标签取窗口最后一行的标签。seq_len是超参数,取的太短学不到时序上下文,取太长会显著增加样本数并让相邻样本高度重叠,训练成本翻倍。项目里先用 10 跑通是合理的,后续可以调 5 和 20 做对比。
4.3 LSTM_IDS.py 主体结构与训练
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model = Sequential([ LSTM(64, return_sequences=True, input_shape=(X_seq.shape[1], X_seq.shape[2])), LSTM(32, return_sequences=False), Dropout(0.3), Dense(32, activation='relu'), Dense(1, activation='sigmoid') ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])第一层LSTM(64, return_sequences=True)返回所有时间步的隐状态,让第二层LSTM(32)继续处理时间维度的信息。最后一个 LSTM 不返回序列,只输出最终时序特征,再交给全连接层分类。input_shape必须显式指定时间步和特征数,Keras 会自动推断样本数。
训练时把X_seq和对应标签丢进model.fit,代码和上一章 DNN 的 fit 完全一样。注意 LSTM 参数数量远少于同层数的 DNN,但由于时序计算引入了循环依赖,实际训练耗时通常是 DNN 的 3 到 5 倍。数据量大时建议先用 5000 条样本验证流程能跑通,再全量训练。
4.4 DNN 与 LSTM 的适用边界
| 维度 | DNN | LSTM |
|---|---|---|
| 输入要求 | 二维特征向量 | 三维时间窗口 |
| 时序建模能力 | 无 | 有 |
| 训练速度 | 快 | 慢 |
| 适合场景 | 快速基线、特征独立 | 攻击过程有先后依赖 |
| 过拟合风险 | 较高,需 Dropout | 较低,但仍需正则化 |
大部分实际数据集里,DNN 和 LSTM 的最终准确率差距并不大,因为 CICIDS2017 的单条记录里已经包含了很多强区分特征,比如 Packet Length 的统计值、标志位组合等。LSTM 的价值在于展示另一种建模思路,以及在攻击流量确实存在时间模式时提供更好的召回表现。如果你发现 LSTM 结果比 DNN 差,不要急着调结构,先检查seq_len和数据是否乱序排列。
5. 超参数优化与模型评估:为什么准确率可能骗你
很多人在跑流量异常检测时,看到测试集准确率 98% 就以为完事了。但在 CICIDS2017 这类高度不平衡的数据上,准确率是很容易出现误导性的指标,需要结合更多工具综合判断。
5.1 准确率的陷阱
如果整个数据集里 85% 是正常流量,15% 是攻击流量,那么一个把所有样本全部预测为「正常」的模型,准确率也有 85%。模型可能什么都没学到,只是学会了复制数据集的分布。所以在训练完成后,评估阶段必须看混淆矩阵、精确率、召回率和 F1 分数,并额外关注攻击类别的召回率。
5.2 用 EarlyStopping 和 ModelCheckpoint 控制最优模型
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint callbacks = [ EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True), ModelCheckpoint('best_dnn.h5', monitor='val_auc', save_best_only=True, mode='max') ]EarlyStopping按验证损失决定何时停,ModelCheckpoint按验证集 AUC 保存最优权重,两者可以同时工作。注意回调函数监控指标要看模型编译时是否加入对应 metric:
from tensorflow.keras.metrics import AUC model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy', AUC(name='val_auc')])加了AUC之后,ModelCheckpoint的monitor='val_auc'才有效。mode='max'告诉回调函数 AUC 越大越好,如果不写这个参数,框架不知道该朝哪个方向保存权重。
5.3 评估代码:混淆矩阵与 ROC-AUC
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score y_proba = model.predict(X_test) y_pred = (y_proba > 0.5).astype(int) print(classification_report(y_test, y_pred, target_names=['benign', 'attack'])) print(confusion_matrix(y_test, y_pred)) print('AUC =', roc_auc_score(y_test, y_proba))classification_report会给出精确率、召回率、F1 值和各类别样本数,先看攻击类别的召回率。confusion_matrix返回[[TN, FP], [FN, TP]],重点关注左下角的FN即「把攻击流量误判为正常」的数量,漏报在安全场景里比误报更危险。最后一行AUC的好处是不依赖分类阈值,AUC 在 0.95 以上说明模型对正常和异常的概率分布区分度良好,但 AUC 高不代表 0.5 阈值下效果好,所以前面两个指标仍然要看。
5.4 超参数调优的实操顺序
超参数调优不要一上来就网格搜索,CICIDS2017 全量训练一轮可能就要几分钟,穷举组合很难受。我的习惯是三步走:
- 固定
batch_size=256,把learning_rate按 0.01、0.001、0.0001 各训练 10 轮,看验证集 loss 下降速度,选下降最快且不震荡的那一档。 - 固定学习率,把网络宽度和深度分别调整,比如隐藏层神经元从 128 变 256,或从三层加到四层,观察验证集与训练集 loss 的差距是否在扩大。差距扩大说明欠拟合,需要加容量;差距很小且两者都高,说明过拟合需要加 Dropout。
- 最后调
batch_size。32 和 128 的收敛轨迹差异明显,小 batch 收敛更慢但泛化更好,大 batch 速度更快但容易收敛到平坦的局部极值。在流量数据上,256 到 512 通常是在训练时间和效果之间的性价比区间。
6. 把模型部署到检测流程:模型导出、特征对齐与类别不平衡处理
训练的最终目的是把模型用起来,哪怕只是课程设计,也要让模型能对一条新产生的流量记录给出预测结果。这一章给出三条最常遇到的操作路径,每一条都直接影响线上效果。
6.1 模型导出与加载
训练完成后,把模型和标准化器同时导出。Keras 的 H5 格式单文件就能包含结构和权重,而StandardScaler也需要单独保存:
from tensorflow.keras.models import load_model import joblib model.save('dnn_ids.h5') joblib.dump(scaler, 'scaler.pkl')加载时注意一条容易踩的坑:load_model('dnn_ids.h5')默认会编译模型并保留训练时配置的优化器状态,但如果你用了自定义的AUC等指标,加载时可能会报错,需要加上compile=False参数,再用model.compile手动设置新的优化器策略。
6.2 新流量的特征对齐与标准化
线上预测时,新流量必须经过与训练时完全一致的特征提取和标准化步骤。CICIDS2017 原始特征是通过 CICFlowMeter 从 pcap 提取的,如果新流量数据的特征顺序和训练时不一致,神经网络的预测结果会完全失真。所以实操中建议提前把特征列名列表导出,预测前做一次列对齐:
expected_cols = feature_cols # 训练时保存的特征列名列表 def predict_flow(raw_row_df): missing = set(expected_cols) - set(raw_row_df.columns) if missing: raise ValueError(f'缺少特征列: {missing}') row = raw_row_df[expected_cols].fillna(0).values row_scaled = scaler.transform(row.reshape(1, -1)) prob = loaded_model.predict(row_scaled) return prob[0][0]这里的scaler必须是训练时保存的那份,不能线上重新fit,否则破坏了训练时的特征分布假设。
6.3 类别不平衡的两种处理方式
CICIDS2017 里正常流量和各类攻击样本比例悬殊,如果不做处理,模型对低频攻击类型的召回率往往偏低。最简单的方法是给 Keras 传入class_weight:
class_weight = {0: 1.0, 1: 10.0} model.fit(X_train, y_train, validation_split=0.2, class_weight=class_weight, epochs=30)此时模型在计算binary_crossentropy损失时,会把攻击类样本的损失乘以 10,相当于人为提高了攻击类的错分代价,梯度更新会更倾向于把攻击样本分对。class_weight 和 SMOTE 过采样可以叠加使用,但每次修改类别权重或采样策略之后,都要重新评估验证集上的混淆矩阵,防止模型从一个极端走到另一个极端,把所有流量都判成异常。
本文还有配套的精品资源,点击获取