简介:基于Python与CNN卷积神经网络的网络入侵检测项目,面向高校课程设计、期末大作业和入门学习者,提供了从数据预处理、模型构建、训练到预测评估的完整流程。项目以NSL-KDD等公开入侵检测数据集为基础,代码包含详细注释,部署简单,适合快速复现并作为高分项目参考。压缩包共33个文件,大小仅21.58MB。核心为Python脚本与CSV数据文件,覆盖数据清洗、CNN模型定义、训练和预测等关键模块;同时包含XML工程配置、TXT说明、Markdown项目说明、PNG/JPG效果对比图以及训练好的PTH权重文件,结构清晰,便于按步骤学习和二次开发。目前已有117人学习下载。通过这套资源,读者可以掌握CNN应用于网络入侵检测的思路,获得可直接运行的模型与预训练权重,并借助准确率、精确率曲线和归一化对比图直观验证效果;项目说明还对关键步骤进行了梳理,适合课程设计、期末大作业参考,也能在此基础上继续改进算法或扩展数据集。
1. 基于Python+CNN的网络入侵检测:从告警洪流到一维卷积
做网络安全方向课程设计或研究生课题时,经常遇到一个尴尬场景:规则类IDS告警刷屏,一天上万条日志全是误报,真正有价值的事件被埋在里面。换个思路,把网络流量会话的特征数据交给深度学习模型去分类,让CNN自己从特征序列里找攻击模式,这就是基于Python+CNN的网络入侵检测算法要做的事。它把每个网络会话表示成一维特征向量,经过卷积层、池化层、全连接层,输出Normal、DoS、Probe、R2L、U2R这类分类结果。整条链路并不神秘:拿到公开数据集、清洗特征、把数据reshape成序列、塞进一维CNN训练、再拿分类报告评估。适合正在做课设、找创新点或想快速搭入侵检测基线的读者,下面按可复现的步骤把方案和踩过的坑一起讲明白。
2. 从数据集到训练样本:先解决特征和标签的问题
2.1 为什么第一版先用NSL-KDD而不是自己抓包
做网络入侵检测,数据是第一道坎。自己抓pcap包听起来很酷,但要把原始流量转成能训练的会话特征,中间涉及流量切分、协议解析、会话重组、特征提取,工作量和排错成本都不小,第一版不建议这么干。常见做法是用公开的NSL-KDD数据集起步,它是KDD Cup 99的改进版,去掉了大量重复记录,训练集和测试集划分固定,文献可比性强,项目答辩时能直接引用。
NSL-KDD里每条记录是一个网络会话的41维特征,包含时长duration、协议类型protocol_type、服务类型service、连接状态flag、字节数src_bytes和dst_bytes,以及统计类特征如count、serror_rate等。标签有五类,分别是normal、probe、dos、r2l、u2r。对于Python入门或首次接触CNN的读者,这个数据集量级适中,KDDTrain+约12.6万条样本,训练一轮几分钟内能跑完,很适合先把流程跑通再换复杂数据。
如果后续要贴近实战,可以换CICIDS2017之类的数据集,它包含真实流量捕获和更多攻击类型,但原始数据体积大、特征工程更繁琐。先拿NSL-KDD做基线,再迁移到CICIDS2017,是平滑的进阶路线。
2.2 用pandas读取无表头的CSV文件
从网络下载的NSL-KDD文件是txt格式,没有表头,需要手动把列名按顺序指定。41个特征名可以整理成列表,读取时通过header=None告诉pandas第一行不是列名。
import pandas as pd feature_names = [ "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" ] columns = feature_names + ["label", "difficulty"] train_df = pd.read_csv("KDDTrain+.txt", header=None, names=columns) test_df = pd.read_csv("KDDTest+.txt", header=None, names=columns) print(train_df.shape, test_df.shape) print(train_df["label"].value_counts())这段代码的关键在于列名顺序必须和文件里的字段顺序严格一致,错一列后面全乱。NSL-KDD官方文档里有对应关系表,建议对照着核对一遍。第42列是攻击类型标签,第43列是难度等级,这里统一读进来,训练时只会用到前41列和标签列。
2.3 标签编码与类别分布的现实问题
拿到标签后不能直接喂给神经网络,需要把字符串类别映射成整数ID。建立字典时有个细节:字典要在训练集上构造,测试集直接复用,避免两边映射不一致。
label_map = {"normal": 0, "probe": 1, "dos": 2, "r2l": 3, "u2r": 4} train_df["label_id"] = train_df["label"].map(label_map) test_df["label_id"] = test_df["label"].map(label_map) print(train_df["label_id"].value_counts())运行后会看到一个明显的类别不均衡问题:normal和dos占绝大多数,r2l和u2r样本量很少。这个现象在第5章会专门展开讲,现在只需记住,后续评估指标不能只看准确率。另外注意KDDTest+里包含训练集中没出现过的攻击子类型,但标签仍然能映射到五类,这正好用来检验模型的泛化能力。
3. 把表格特征变成CNN能吃的序列数据
3.1 离散特征的编码取舍:LabelEncoder还是one-hot
NSL-KDD的41维特征里,protocol_type、service、flag是离散字符串列,其余都是数值型。大部分教程会让你做one-hot编码,但我不建议在这类数据上无脑用。原因很简单:service种类有约70个,one-hot之后特征维度膨胀到120以上,而CNN在表格型稀疏特征上学习效率并不高。
常见做法是先对离散列做LabelEncoder,把类别映射成整数,再整体做归一化。这样做有一个值得知道的副作用:LabelEncoder会给类别强加一个大小顺序,比如tcp映射成0、udp映射成1、icmp映射成2,这个顺序本身没有物理意义。不过在实际实验里,一维卷积会通过卷积核组合相邻特征,编码顺序造成的影响有限。
from sklearn.preprocessing import LabelEncoder cat_cols = ["protocol_type", "service", "flag"] for col in cat_cols: le = LabelEncoder() le.fit(train_df[col]) # 只在训练集上fit train_df[col + "_enc"] = le.transform(train_df[col]) # 测试集可能出现训练集没见过的新类别,这里做个兜底 test_df[col + "_enc"] = test_df[col].map( lambda x: le.transform([x])[0] if x in le.classes_ else -1 )参数说明:LabelEncoder在训练集上fit,测试集用transform。测试集类别的兜底逻辑是必要的,因为NSL-KDD的测试集服务类型和训练集并非完全重合,直接transform会报错。映射成-1后,模型会把未知服务当成一个独立信号处理,虽然不完美,但比程序崩溃好得多。
3.2 归一化时最容易犯的数据泄漏错误
特征数值范围差异很大,src_bytes可能是几千,serror_rate是0到1之间的小数,不归一化直接训练会导致梯度更新被大数值特征主导。但归一化不是简单调一个MinMaxScaler就完事,关键是fit和transform的对象。
from sklearn.preprocessing import MinMaxScaler feat_cols = feature_names + ["protocol_type_enc", "service_enc", "flag_enc"] X_train_raw = train_df[feat_cols].copy() X_test_raw = test_df[feat_cols].copy() scaler = MinMaxScaler() X_train_scaled = scaler.fit_transform(X_train_raw) X_test_scaled = scaler.transform(X_test_raw) y_train = train_df["label_id"].values y_test = test_df["label_id"].values这里最容易翻车的写法是:先把所有数据拼在一起,再fit归一化器。那样的话,测试集的分布信息已经混进scaler的参数里,训练时等于提前看到了测试集,指标虚高,一到真实环境就掉链子。正确做法是scaler只在训练集上fit,测试集只做transform。
实际使用中还可以考虑用QuantileTransformer或RobustScaler,对离群值更鲁棒。网络流量里的字节数特征经常有极端大值,MinMaxScaler会把正常样本压到很窄的区间里,QuantileTransformer效果通常更好,初学者可以先跑通再精调。
3.3 reshape成(样本数, 时间步, 通道数)
CNN在图像任务上处理的是二维像素矩阵,但网络流量特征是一维的序列数据,所以这里要用Conv1D而不是Conv2D。输入形状定为(samples, steps, channels),把41个特征视作一个长度为41的序列,通道数设为1。
import numpy as np X_train_seq = X_train_scaled.reshape(X_train_scaled.shape[0], X_train_scaled.shape[1], 1) X_test_seq = X_test_scaled.reshape(X_test_scaled.shape[0], X_test_scaled.shape[1], 1) print(X_train_seq.shape) # (样本数, 41, 1)参数说明:reshape的第三个维度是通道数,类似图像里的RGB通道。1表示每个时间步只有一个数值。如果想要多通道,可以按特征含义分组,比如把TCP连接特征放一个通道、主机统计特征放另一个通道,这属于进阶玩法,需要你对数据集特征分组的语义有足够理解,初版不必强求。
4. 用Keras搭一维CNN模型并完成训练
4.1 一维卷积为什么适合流量特征分类
CNN最擅长的就是从局部输入里提取模式,一维卷积核沿着特征维度滑动时,每次覆盖kernel_size个相邻特征。这个机制和入侵检测的直觉是对得上的:某个攻击行为往往会在相邻特征上同时表现出异常,比如连接失败率的升高和源字节数的骤减就是一组局部相关的信号。卷积核像扫描仪一样滑过整条特征序列,把局部组合模式提取出来,再经过池化层压缩冗余,最后交给全连接层做分类。
和传统机器学习方法对比,CNN的好处是省去手工特征组合,卷积核自动学习特征之间的交互关系。但要注意,CNN在表格型数据上并不总是优于XGBoost或随机森林,它的优势更多体现在特征量足够大、局部模式明显的场景。做这个项目时不必神化CNN,把它当作一个能提取局部模式的分类器即可。
4.2 模型结构:两层Conv1D加池化和Dropout
下面用Keras的Sequential模型搭建。第一个卷积层输入形状是(41, 1),经过卷积、批归一化、池化后,第二个卷积层进一步提取特征,最后展平接全连接层。
from tensorflow.keras import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout, BatchNormalization model = Sequential([ Conv1D(filters=64, kernel_size=3, activation="relu", input_shape=(41, 1)), BatchNormalization(), MaxPooling1D(pool_size=2), Conv1D(filters=32, kernel_size=3, activation="relu"), BatchNormalization(), MaxPooling1D(pool_size=2), Flatten(), Dense(64, activation="relu"), Dropout(0.5), Dense(5, activation="softmax") ]) model.compile( optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"] )每层输出尺寸可以推算:第一层卷积后序列长度从41变成39(41减kernel_size 3加1),池化后变成19;第二次卷积后从19变成17,池化后变成8。最后展平是32乘以8等于256个节点。参数说明:filters决定卷积核数量,64个卷积核意味着学习64种不同的局部特征组合;kernel_size设为3表示每次看3个相邻特征;Dropout(0.5)表示全连接层的神经元在训练时有50%概率随机失活,这是对抗过拟合最直接的手段。
loss使用sparse_categorical_crossentropy,配合整数标签使用,不用手动转one-hot编码,代码更简洁。
4.3 训练参数设置与回调函数
训练时batch_size、epochs、学习率是三个最需要关心的超参数。这里给一组能稳定收敛的配置,并加上EarlyStopping防止过拟合。
from tensorflow.keras.callbacks import EarlyStopping early_stop = EarlyStopping( monitor="val_loss", patience=5, restore_best_weights=True ) history = model.fit( X_train_seq, y_train, validation_data=(X_test_seq, y_test), batch_size=128, epochs=30, callbacks=[early_stop], verbose=1 )batch_size设128是因为训练集约12.6万条样本,128是最常见的折中方案,既不会让单轮时间过长,也能保证梯度估计相对平稳。epochs设30并配合EarlyStopping,实际训练中通常跑到十轮左右就收敛了。Adam优化器默认学习率0.001就能工作,不需要手动调整。
参数速查表如下:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| filters | 64 / 32 | 卷积核数量,先大后小逐步压缩 |
| kernel_size | 3 | 卷积核覆盖的相邻特征数 |
| pool_size | 2 | 池化窗口大小 |
| dropout | 0.5 | 全连接层随机失活比例 |
| batch_size | 128 | 每批样本数 |
| learning_rate | 0.001 | Adam默认学习率 |
| epochs | 30 | 配合EarlyStopping使用 |
4.4 评估模型不能只看准确率
训练完成后,用测试集评估并打印分类报告。
from sklearn.metrics import classification_report, confusion_matrix y_pred = model.predict(X_test_seq) y_pred_ids = np.argmax(y_pred, axis=1) print(classification_report(y_test, y_pred_ids, target_names=list(label_map.keys()))) print(confusion_matrix(y_test, y_pred_ids))这里强调不看单个accuracy值,而是看每个类别的precision、recall、F1-score,尤其关注r2l和u2r。准确率会被normal和dos的样本量带跑,全部预测成normal也能拿很高的准确率,但这在网络入侵检测场景里毫无用处。classification_report能直接暴露模型对少数类别的表现,下一步的调参方向也由此决定。
5. 网络入侵检测项目里的五个常见坑与排查方法
5.1 数据泄漏:归一化器偷看了测试集
现象:训练时验证集准确率98%,测试集评估也接近满分,但拿到新数据上表现稀烂。
原因:最常见的操作是把所有数据拼在一起再fit归一化器,或者用pd.concat合并后再做preprocessing。归一化器记录了全局最小值和最大值,这些统计量里包含了测试集的信息,训练过程中模型相当于提前做过真题。
解决:严格做到scaler.fit(X_train)后,再scaler.transform(X_test)。同理适用于数据清洗、缺失值填充和特征选择,凡是涉及数据分布的步骤都只在训练集上计算参数。我已经在3.2节给了正确写法,这里再强调一次:先切分,再预处理。
5.2 类别不均衡让模型把少数类当成噪声
现象:训练完成后打印classification_report,r2l和u2r的precision、recall全是0,模型把这几个类别直接忽略。
原因:NSL-KDD训练集中u2r只有52条样本,r2l不到1000条,而normal有6.7万条。交叉熵损失函数以大类别为主导,把少数类预测错对总loss的影响微乎其微,模型自然选择躺平。
解决:最简单的方式是在model.fit里传class_weight参数,给少数类更高的权重。
class_weights = { 0: 1.0, 1: 1.0, 2: 1.0, 3: 5.0, 4: 10.0 } history = model.fit( X_train_seq, y_train, validation_data=(X_test_seq, y_test), batch_size=128, epochs=30, class_weight=class_weights, callbacks=[early_stop], verbose=1 )权重值需要根据实际类别比例调试,初始可以按“多数类样本数除以少数类样本数”的比例来设,跑完看u2r的recall有没有从0抬起来。也可以在训练前对少数类做SMOTE过采样,但要注意SMOTE必须在训练集上单独进行,而且生成样本有时会带来噪声。项目答辩时诚实说明少数类性能是业界公认难题,比硬吹效果好。
5.3 训练准确率99%,测试集却掉到76%
现象:训练集loss一路降到0.1以下,测试集loss在某个epoch后开始反弹,典型的过拟合曲线。
原因:CNN的参数量远大于传统机器学习模型,而NSL-KDD的特征维度只有41,特征量不算大,模型容易把训练集里攻击模式的细节背下来,而不是学出泛化模式。特别是在训练集和测试集的攻击子类型有差异时,过拟合的表现会更极端。
解决:先看全连接层是不是过大。把Dense(128)甚至Dense(256)换成Dense(64)或Dense(32),参数数量立刻降一个量级。再确认Dropout已经加在Flatten层之后,这是最容易漏的位置。最后用EarlyStopping在验证集loss开始上升时及时截停,restore_best_weights=True可以回滚到最优epoch的权重。这个组合拳能从模型复杂度、正则化、训练轮次三个方向压住过拟合。
5.4 特征列顺序错位导致模型学了个寂寞
现象:训练时loss下降很慢,测试集准确率一直在40%附近徘徊,怎么调参都没用。
原因:NSL-KDD特征列顺序和代码里的feature_names列表对不上。有的教程自己定义了特征名,有的直接按位置读取,如果你参考的代码和数据版本不一致,列就错位了。更隐蔽的是,自己加的LabelEncoder编码列拼在特征列表末尾时,如果后面reshape操作拿错了列顺序,模型输入里有实际意义的特征全是错位的。
解决:训练前打印X_train_seq里前几行数据,和CSV文件对应行的原始值做比对,确认第0列是duration而不是protocol_type。写代码时把特征名列表定义成常量放在文件头部,增加或删减特征时只改一处。这类问题靠肉眼检查比靠调参有效得多。
5.5 全数据集乱序切分制造虚假验证
现象:训练集和验证集都来自同一个CSV,从某一行硬切,模型效果极好,但换个时间段的数据集就崩。
原因:NSL-KDD的记录本身是独立会话,但如果有教程把数据集按行顺序切成前80%后20%,而原始文件里攻击类型是有聚集的,那验证集的标签分布会和训练集有系统性差异。反过来,如果数据本身顺序泄漏了时间信息,硬切分就会让模型偷看到未来信息。
解决:用sklearn的train_test_split并设置stratify参数,按标签比例分层采样。固定random_state,让每次复现结果一致。这里有个容易忽略的点:网络流量数据集的最佳实践是按时间窗口切分(前段训练、后段测试),而不是随机切分,因为真实攻击是随时间演进的。课程设计阶段用stratify随机切分是能接受的,论文或生产环境一定要按时间分组。
6. 验证方法与交付思路:让模型不只是跑在Jupyter Notebook里
6.1 用混淆矩阵找模型的真实短板
分类报告会告诉你每个类别的precision和recall,但想知道模型具体把哪两类搞混了,还得看混淆矩阵。打开confusion_matrix的输出,大多数情况下你会发现r2l被分到dos或normal里。这时候可以按攻击类别逐个检查:如果u2r大概率被预测成normal,说明这类攻击的特征在训练样本里太少,卷积核根本没有学到足够强的信号。针对具体混淆方向去补充数据或调整特征,比盲目加深网络更有价值。建议把混淆矩阵保存成图片,项目答辩时直接展示,比口头说准确率高有说服力得多。
6.2 从训练脚本到可交付的检测流程
模型训练完后,把权重和预处理对象都保存下来,这样推理脚本不依赖训练环境就能独立工作。
model.save("nids_cnn_model.keras") import joblib joblib.dump(scaler, "scaler.pkl") joblib.dump(label_encoders, "label_encoders.pkl")推理时重新加载模型和scaler,对一条新会话做同样的离散编码、归一化、reshape,然后预测。
from tensorflow.keras.models import load_model model = load_model("nids_cnn_model.keras") def predict_session(session_features, scaler, label_encoders, model): processed = [] for i, col in enumerate(feat_cols): if col in label_encoders: le = label_encoders[col] val = le.transform([session_features[i]])[0] if session_features[i] in le.classes_ else -1 processed.append(val) else: processed.append(session_features[i]) scaled = scaler.transform([processed]) seq = scaled.reshape(1, len(feat_cols), 1) prob = model.predict(seq, verbose=0) return int(np.argmax(prob[0])), float(np.max(prob[0]))这里有个现实要认清:把CSV特征喂给CNN只是入侵检测的一环,真实环境里你还需要从抓包文件提取这些特征,这涉及流量解析和特征计算,那部分是另一个工程问题。课程设计和毕业项目里,先把这个训练推理闭环跑通,再考虑特征提取的自动化。我自己做这类项目时最大的教训是急着堆模型复杂度,后来发现先把评价指标定对、把复现流程钉死,比多加一层卷积重要得多。希望帮到你。
本文还有配套的精品资源,点击获取