简介:基于卷积神经网络实现网络入侵检测的完整项目代码包,面向希望掌握深度学习与网络安全结合应用的小白和进阶学习者,适合用于毕业设计、课程设计或工程实训。包内提供数据预处理脚本、一层全连接层对照代码和CNN主程序,配套KDD Cup数据集压缩包及TensorBoard训练日志,可复现99.5%的检测正确率。资源共16个文件,以Python脚本、gzip压缩数据集、项目配置XML和说明文档为主,整体大小约17.52MB,目录结构清晰。目前已有105人学习下载。通过阅读源码和训练日志,读者能理解CNN处理网络流量的思路,对比全连接层与卷积神经网络的性能差异,并根据准确率与损失曲线进行参数调整,是快速上手入侵检测项目的实用资料;包内还附工程配置与使用说明,便于直接导入项目并按需调整。
1. 网络入侵检测搭上卷积神经网络:为什么流量识别也要“看图”
网络入侵检测最麻烦的不是抓不到流量,而是不知道“正常”长什么样。规则引擎靠专家逐条写特征,遇到慢速扫描、低频爆破这类不刷屏的攻击经常漏;传统机器学习方案又极度依赖特征工程的质量。基于卷积神经网络的网络入侵检测走的是另一条路:不逐条定义攻击模式,而是把网络连接的特征向量重排成类似图像的二维矩阵,让卷积神经网络(CNN)自动抽取局部规律。在NSL-KDD这类公开基准集上,二分类正确率达到99.5%并不算夸张,真正的问题在于怎么复现它、怎么看这个数字、以及上线时怎么让它不翻车。这篇笔记围绕数据编码、模型搭建、参数设置和部署踩坑展开,适合流量分析、安全网关和IDS产品验证方向的人照着自己跑一遍。
2. 流量数据先变成张量:NSL-KDD选型、特征编码与二维重构
CNN吃进去的是矩阵,不是pcap文件,所以在谈模型之前,得先把“网络连接”这种结构化记录变成能放进卷积层的数字张量。做入侵检测的团队经常忽略这一步,结果模型结构再漂亮也救不回错误的数据表示。
2.1 数据集怎么选:NSL-KDD、UNSW-NB15和CICIDS 2017
数据集决定了你的实验能跑多快、结论能信多少。我一般先看NSL-KDD:训练集约12.5万条、测试集约2.2万条,每条样本41维特征,标签分Normal和4类攻击(DoS、Probe、R2L、U2R)。它去掉了KDD Cup 99里大量冗余重复记录,类别分布相对合理,整轮训练在普通GPU上几分钟完成,适合先验证网络结构和参数。
UNSW-NB15是比NSL-KDD更新一点的选项,49维特征、9类攻击,攻击面更贴近现代网络,但样本量达到百万级,预处理和训练都更慢。CICIDS 2017则是从原始PCAP重放出来的数据集,特征超过80维,最接近真实会话流,但需要自己决定怎么从抓包中抽特征,工程量明显更大。做第一版模型验证,NSL-KDD性价比最高。
| 数据集 | 样本规模 | 特征数 | 攻击类别 | 适合阶段 |
|---|---|---|---|---|
| NSL-KDD | 训练约12.5万,测试约2.2万 | 41 | 4类加正常 | 模型选型、参数迭代 |
| UNSW-NB15 | 百万级 | 49 | 9类加正常 | 跨数据集泛化验证 |
| CICIDS 2017 | 百万级 | 80以上 | 14类加正常 | 真实场景模拟、端到端验证 |
选好数据集后,下一个问题是:这些数据集里的特征列,CNN不能直接读。卷积神经网络原理上是在二维网格里找局部模式,把协议类型、连接时长、窗口统计这些字段按某种顺序排成矩阵,卷积核才能学到“相邻位置特征之间的组合关系”,这正是CNN相比全连接网络的一个优势——特征的空间排布天然带有抽象语义。
2.2 41维特征怎么编码成11x11矩阵
NSL-KDD的41维特征里,有3个是离散的:protocol_type(协议类型)、service(目标服务)、flag(连接状态)。剩下38个是连续数值,比如duration、src_bytes、dst_bytes,以及大量基于时间窗口的统计特征,像是count、serror_rate、dst_host_srv_count。离散字段不能直接当数值用——把tcp编码成1、udp编码成2,模型会莫名其妙学到“udp大于tcp”这种不存在的序关系。
常见做法是独热编码展开。protocol_type有3种取值、service约70种、flag约11种,三个离散字段展开后新增81个维度,加上38个连续特征,总维度变成122。图像格式一般选单通道灰度图,所以需要把122维向量重排成二维矩阵。12x10等于120维度还缺2个,11x11等于121维度缺1个,补零是最省事的方案;也有做法直接截断到120维,但我倾向补零,保留完整信息量,卷积核自己会学到边缘补零位置不重要。
2.3 reshape策略:特征排列顺序影响感受野
特征重排成矩阵后,排列顺序不能随便来。卷积神经网络结构图里最直观的一点是:3x3卷积核每次只看见相邻的9个格子。如果相邻位置放的是语义上完全无关的特征,卷积核学到的“局部模式”就没有物理含义,模型准确率会有波动,而且这种波动在调参阶段让你分不清是结构问题还是排列问题。
经验做法是把同一来源或同一时段的统计特征放在相邻位置。比如count、srv_count、serror_rate、srv_serror_rate这四个都描述“最近一段时间内连接行为”,把它们排在一起,卷积核更容易捕捉到“同一时段的连接数异常且错误率高”这种攻击模式。离散特征展开后的独热位放到矩阵边缘,连续数值特征集中在中间区域。下面这个预处理脚本可以直接套到NSL-KDD上:
import pandas as pd import numpy as np from sklearn.preprocessing import StandardScaler # NSL-KDD 的41列特征名,按原始顺序 FEATURES = [ 'duration', 'protocol_type', 'service', 'flag', 'src_bytes', 'dst_bytes', 'land', 'wrong_fragment', 'urgent', 'hot', 'num_failed_logins', 'logged_in', 'num_compromised', 'root_shell', 'su_attempted', 'num_root', 'num_file_creations', 'num_shells', 'num_access_files', 'num_outbound_cmds', 'is_host_login', 'is_guest_login', 'count', 'srv_count', 'serror_rate', 'srv_serror_rate', 'rerror_rate', 'srv_rerror_rate', 'same_srv_rate', 'diff_srv_rate', 'srv_diff_host_rate', 'dst_host_count', 'dst_host_srv_count', 'dst_host_same_srv_rate', 'dst_host_diff_srv_rate', 'dst_host_same_src_port_rate', 'dst_host_srv_diff_host_rate', 'dst_host_serror_rate', 'dst_host_srv_serror_rate', 'dst_host_rerror_rate', 'dst_host_srv_rerror_rate' ] def load_and_encode(train_df, test_df): # 合并train/test做独热展开,保证两边列集合完全一致 all_x = pd.concat([train_df.drop('label', axis=1), test_df.drop('label', axis=1)], axis=0) encoded = pd.get_dummies(all_x, columns=['protocol_type', 'service', 'flag']) train_enc = encoded.iloc[:len(train_df)].reset_index(drop=True) test_enc = encoded.iloc[len(train_df):].reset_index(drop=True) # 重点:scaler只fit训练集,测试集只做transform,避免信息泄漏 scaler = StandardScaler().fit(train_enc) X_train = scaler.transform(train_enc).astype('float32') X_test = scaler.transform(test_enc).astype('float32') # 122维补1个零到121维,再reshape成11x11x1 def to_image(X): n = X.shape[0] X_pad = np.pad(X, ((0, 0), (0, 1)), mode='constant') return X_pad.reshape(n, 11, 11, 1) return to_image(X_train), to_image(X_test)逻辑说明:先合并train/test做独热展开,是为了避免service字段在训练集和测试集中取值集合不一致,导致两边的列数对不上;StandardScaler用fit_transform和transform分开调用,是防止测试集的均值方差信息提前混进特征;最后np.pad在第122个位置补0,凑成121维,再reshape成11x11的单通道灰度图。
参数说明:缩放方式选了StandardScaler而不是MinMaxScaler,是因为网络流量统计特征有长尾分布,标准化后梯度更稳定。如果你用的是UNSW-NB15这类49维数据集,需要重新算独热展开后的维度,再调整reshape尺寸,比如补零到9x9或12x12都可行,原则是让语义相关的特征落在相邻格子里。
3. CNN入侵检测模型怎么搭:卷积核、池化与三个关键参数
数据变成张量后,模型部分反而简单。入侵检测是数据量相对小的分类任务,不需要套ResNet那种上百层的深度网络,两到三个卷积块加一个全连接层就能跑出很高的基准。真正拉开差距的是卷积核数量的设置、Dropout的位置、以及训练时的早停策略。
3.1 基础结构:两层卷积块加全连接,为什么不用深网络
我常用的CNN入侵检测结构长这样:输入一个11x11x1的灰度图,第一个卷积块包含32个3x3卷积核,卷积后接BatchNorm和ReLU,再做一次2x2最大池化;第二个卷积块换成64个3x3卷积核,同样接BatchNorm、ReLU和池化;然后Flatten压平,接一个128节点的全连接层,Dropout比例0.5,最后输出层按二分类或五分类接softmax。
第一层32个卷积核学习的是基础流量模式,比如连接数窗口统计、协议类型组合、错误率局部异常;第二层64个卷积核在更高层把前面学到的模式组合起来,对应“某个目标主机在时间窗口内出现大量不同源端口连接”这类攻击语义。MaxPooling把11x11降到5x5再降到2x2,既降了计算量,又保留了激活最强的特征。为什么不用更深的网络?NSL-KDD只有十几万训练样本,深层模型参数量上去了过拟合很快,而且加深一两层带来的准确率提升通常不到0.5%,不值得引入额外的调参和训练成本。
import tensorflow as tf from tensorflow.keras import layers, models def build_cnn(num_classes=2): model = models.Sequential([ layers.Input(shape=(11, 11, 1)), # 卷积块1:学基础流量局部模式 layers.Conv2D(32, 3, padding='same', activation='relu'), layers.BatchNormalization(), layers.MaxPooling2D(pool_size=2), # 卷积块2:在基础模式之上组合攻击语义 layers.Conv2D(64, 3, padding='same', activation='relu'), layers.BatchNormalization(), layers.MaxPooling2D(pool_size=2), layers.Flatten(), layers.Dense(128, activation='relu'), layers.Dropout(0.5), layers.Dense(num_classes, activation='softmax') ]) return model逻辑说明:Conv2D用3x3卷积核、padding='same',保证特征图边缘信息不提前丢失;BatchNorm放在卷积层后面、激活函数前面,对网络流量这种分布变化大的输入,能显著稳定训练过程;Dropout放在全连接层前,是CNN里对抗过拟合最有效的常规位置,卷积层本身有参数共享和池化做正则,不需要额外加Dropout。
参数说明:第一层卷积核32个、第二层64个是稳妥起步。如果验证集准确率上不去,可以试着提到64/128,但参数量会翻倍,在小数据集上收益有限。激活函数统一用ReLU,二分类输出层用softmax而不是sigmoid,这样后面分析概率阈值时更方便。
3.2 三个必调参数:卷积核数量、Dropout比例、学习率
CNN入侵检测模型跑起来后,真正影响落地效果的参数就三个。第一个是卷积核数量,32/64起步,模型欠拟合时加一倍,但不要反复加——加到128以上在NSL-KDD上几乎看不到提升,只增加了训练时间。第二个是Dropout比例,全连接层前我固定0.5;如果你发现验证集损失和训练集损失差得很大,把Dropout提到0.6试试。第三个是学习率,Adam优化器默认0.001基本不用改;收敛太慢时降到0.0003比直接改动网络结构更有效。这三个参数调到合理范围后,别急着继续调,先看数据有没有泄漏、类别是不是不平衡,那才是让准确率虚高的主因。
3.3 早停与学习率衰减:让训练自己停下来
early_stop = tf.keras.callbacks.EarlyStopping( monitor='val_loss', patience=10, restore_best_weights=True ) reduce_lr = tf.keras.callbacks.ReduceLROnPlateau( monitor='val_loss', factor=0.5, patience=5, min_lr=1e-6 )逻辑说明:EarlyStopping监控验证集损失,连续10轮不下降就停止训练,restore_best_weights会回滚到验证集损失最低时的权重,这个回滚很重要,否则保存下来的可能是过拟合后的模型。ReduceLROnPlateau在验证损失连续5轮不下降时把学习率减半,用来跨过局部平稳区域,min_lr=1e-6防止学习率衰减到完全学不动。
参数说明:epoch设置50就够,配合上面两个回调,实际训练通常20到30轮就停了。patience设太小容易在准确率还没爬上去时就早停,设太大浪费时间,10和5是我在NSL-KDD上比较稳的组合。
4. 复现99.5%的完整训练流程:数据加载、训练配置与指标评估
结构定了,参数定了,接下来就是端到端训练。这一章要解决一个核心问题:在什么时候、用什么配置,才能稳定复现出99.5%这个数字,以及这个数字到底该怎么读。
4.1 训练配置:固定随机种子和分层切分
复现实验结果最怕的是每次跑出来的准确率忽高忽低。差异主要来自两个地方:模型初始化时卷积核的随机权重、以及训练/验证切分时的样本分布。解决办法是固定随机种子,并且按类别比例分层切分。
import numpy as np import tensorflow as tf from sklearn.model_selection import train_test_split from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau np.random.seed(42) tf.random.set_seed(42) # 二分类标签:normal为0,所有攻击合并为1 y_train_bin = (train_df['label'] != 'normal').astype('int') y_test_bin = (test_df['label'] != 'normal').astype('int') # 如果不用官方测试集,按分层留10%做验证集 X_tr, X_val, y_tr, y_val = train_test_split( X_train_data, y_train_bin, test_size=0.1, stratify=y_train_bin, random_state=42 ) model = build_cnn(num_classes=2) model.compile( optimizer=tf.keras.optimizers.Adam(0.001), loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) history = model.fit( X_tr, y_tr, validation_data=(X_val, y_val), batch_size=64, epochs=50, callbacks=[ EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True), ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5, min_lr=1e-6) ], verbose=1 ) test_loss, test_acc = model.evaluate(X_test_data, y_test_bin, verbose=0) print(f'test acc: {test_acc:.4f}')逻辑说明:random_state=42和tf.random.set_seed(42)要在数据切分和模型构建之前完成,否则模型初始化顺序不同,结果仍会有细微差异。stratify=y_train_bin保证切出来的验证集里normal和attack比例和原始训练集一致,避免验证集里全是normal导致评估失真。
参数说明:batch_size=64在12万条样本上迭代速度合适,想更稳可以降到32;损失函数用sparse_categorical_crossentropy,配合整数标签,不需要手动做one-hot。评估时直接拿官方测试集跑,这样和别人论文里的结果有可比性。
4.2 正确率99.5%是怎么来的:混淆矩阵和分类报告比一个数字诚实
正确率本身是个很有迷惑性的指标。如果测试集里正常流量占大多数,把样本全部判成正常,正确率也会有九成以上。所以在宣称“正确率99.5%”之前,一定要把混淆矩阵和分类报告打出来看一眼。
from sklearn.metrics import confusion_matrix, classification_report y_pred = np.argmax(model.predict(X_test_data), axis=1) print(confusion_matrix(y_test_bin, y_pred)) # 输出格式:[[TN, FP], [FN, TP]] print(classification_report(y_test_bin, y_pred, target_names=['normal', 'attack'], digits=4))逻辑说明:混淆矩阵的四个格子能直接看出模型是偏向把正常流量误判成攻击(FP高),还是把攻击漏成正常(FN高)。分类报告里的precision和recall分别反映“报出来的攻击里有多少是真的”和“真正的攻击里有多少被抓住”,这两个数是上线前最该盯住的指标。
参数说明:digits=4让指标保留四位小数,方便对比实验间的细微差异。在NSL-KDD二分类任务里,模型通常能到0.99以上的precision和recall,这也是99.5%这个数字的主要来源。
4.3 二分类和五分类的路径差异:attention与class_weight
二分类是“攻击还是正常”,五分类则需要区分DoS、Probe、R2L、U2R这些攻击类型。五分类难度高不少,因为R2L和U2R在NSL-KDD里的样本量非常少,U2R整个训练集才几十条,模型很容易把它们学成噪声。遇到这种情况,class_weight比改网络结构更直接:
from sklearn.utils.class_weight import compute_class_weight classes = np.unique(y_train) weights = compute_class_weight('balanced', classes=classes, y=y_train) class_weight = dict(zip(classes, weights)) model.fit( X_tr, y_tr, validation_data=(X_val, y_val), class_weight=class_weight, batch_size=64, epochs=50, callbacks=[...], verbose=1 )逻辑说明:compute_class_weight会按样本量反比给每个类别分配权重,稀有类权重高,在损失函数里相当于多给几条样本,让模型不直接把它们忽略掉。加了class_weight后,R2L和U2R的recall能改善不少,但整体正确率通常会微降,这是正常现象——你是在拿一点点整体准确率换稀有类的检出率。
5. 高正确率背后的避坑清单:数据泄漏、类别不平衡与指标幻觉
99.5%的正确率看起来漂亮,实际踩坑时会发现,这个数字有一大半是数据分布给的,不是模型多聪明。这一章写清我在复现过程中遇到的5个典型问题,每个都按现象、原因、解决来拆。
5.1 归一化泄漏:为什么你复现出来的结果比论文还好
现象:训练集准确率99.9%,测试集准确率99.5%,怎么看都漂亮,但你隐约觉得哪里不对——换了一组真实流量数据后,准确率直接掉到85%。
原因:做特征缩放时,用StandardScaler对train和test合并后的全部数据做了fit_transform。测试集的均值和方差提前混进了特征缩放统计量,模型在测试时已经“见过”了测试集的分布信息,这不是真实的泛化能力。
解决:严格按本文2.2的写法,scaler只fit在训练集上,测试集单独调用transform。更严格一点,连独热编码的列集合都只从训练集导出。可以用一个简单方式自查:在训练脚本里打印scaler.mean_的长度和训练集特征数,如果对不上,说明编码阶段的数据范围就不对。
5.2 类别不平衡:U2R和R2L的recall为什么可能是0
现象:整体正确率95%,但打开分类报告,U2R那一行的recall是0.00,R2L只有0.12。所有U2R样本全部被预测成normal或DoS。
原因:NSL-KDD里U2R样本太少,模型在训练时几乎没见过这个类别,梯度更新被majority class主导。正确率是高了,但模型实际上是个偏科生。
解决:先看分类报告,不要只看acc。然后按4.3加class_weight,或者对稀有类做SMOTE过采样。如果业务必须检出U2R,还可以把问题重新组织成“正常vs四类攻击”的单分类器,把稀有类检出的优先级提到准确率之前。
5.3 切分数据时泄漏了同源会话
现象:随机切分一个训练/测试集后,准确率极高;换成按时间切分,同一个模型掉3到5个点。
原因:NSL-KDD里存在来自同一个网络会话的多个流量记录,随机切分会把同源记录同时分到训练集和测试集,相当于测试集里出现了训练样本的近亲副本。
解决:尽量使用官方预设的KDDTrain+和KDDTest+划分,不要自己随机切分。如果要自己切,先按源IP或会话ID分组,再对组做切分,避免同源记录跨集合。
5.4 固定了seed但换机器结果还是变
现象:同一份代码在同一台机器上跑,结果稳定。换一台机器或换一个TensorFlow版本,准确率掉了1到2个百分点。
原因:GPU算子浮点累加顺序、TF底层算子调度、cuDNN算法选择都会引入非确定性。seed只能控制Python层级的随机数,控制不了底层并行计算的微小差异。
解决:记录训练环境的TensorFlow版本、CUDA版本和GPU型号。如果项目对复现要求高,用CPU训练或开启TF的确定性算子开关,代价是训练慢一些。反正NSL-KDD数据集小,CPU跑也就几分钟,完全能接受。
5.5 指标幻觉:准确率99.5%上线后误报刷屏
现象:基准集上一切正常,接入真实网关后,正常业务流量被频繁判为攻击,安全运营一天收到上千条告警。
原因:真实网络流量的特征分布和NSL-KDD差别很大。基准集里的攻击流量是结构化样本,真实流量里大量P2P传输、视频流、长连接握手和内部扫描工具的特征,在模型眼里都和“异常”很像。正确率在两个分布下含义完全不同。
解决:上线前用真实业务流量回放,计算误报率而不是正确率。误报率的定义是“正常流量被判为攻击的比例”,按这个指标调阈值或加规则。另外,线上模型最好从二分类降级到多分类,只对明确攻击类型告警,模糊预测不告警。
6. 从实验台挪到网关:部署CNN入侵检测器的延迟与误报处理
模型在基准集上跑通只是第一步。把CNN入侵检测器接入真实链路时,要处理模型导出、在线推理延迟、误报压制三个问题。这一章讲我最常用的部署做法。
6.1 模型导出与推理管线的最小形态
# 保存Keras模型,供后续加载推理 model.save('cnn_nids_model.keras')在线推理管线一般长这样:实时抓包后按会话聚合连接记录,提取NSL-KDD同款41维特征,加载训练时保存的scaler做transform,补零后reshape成11x11x1,送入模型拿到softmax概率。单条样本推理在CPU上是毫秒级,瓶颈通常在会话聚合的等待时间。如果每秒要处理上千条连接,把推理改成batch模式跑,吞吐会高很多。
6.2 误报压制:滑动窗口多数投票和阈值调整
单条流量判异常并不等于告警。我上线时加了两层保护:第一层,取最近5个会话的预测概率做滑动窗口平均,超过告警阈值才触发;第二层,对新告警做同源IP聚合,短时间内同一源IP的多次告警合并成一条事件,避免告警风暴。阈值一般不用默认的0.5,可以按验证集P/R曲线选一个偏保守的值,比如0.7,让告警更精确。
6.3 上线前的一个验证习惯
我现在的习惯是:无论在基准集上跑出多高的正确率,正式上线前都会拿真实流量pcap回放一遍,统计误报率和单条延迟。98%以上的准确率只是入场券,能把误报率摁在业务能接受的范围内,才算真的落地。如果你正准备把这个方向带到生产环境,建议从NSL-KDD小步验证,再逐步换成UNSW-NB15和真实流量,一路把阈值、窗口和告警策略都磨一遍。希望帮到你。
本文还有配套的精品资源,点击获取