简介:这份资源是面向计算机、通信、人工智能、自动化等专业学生与从业者的网络入侵检测实战项目包,可作为毕业设计、课程设计或期末大作业的完整参考方案。项目基于UNSW_NB15官方数据集,围绕42列特征与二分类标签,完整实现了特征处理、Pearson相关性过滤、随机森林特征选择以及Keras搭建CNN网络训练的全流程,并配套项目说明与答辩PPT,便于理解整体设计思路与实验结论。压缩包为zip格式,大小约10.02MB,内含源码、说明文档与演示材料,代码按StrTonum、Min-max、Balanced、Pearson、Find_feature、Pro_Testdata、CNN等模块划分,运行流程清晰,并附有requirements.txt便于快速配置环境。目前已有339人学习关注,适合希望借鉴特征工程与深度学习融合思路、快速上手复现或进行二次开发的读者参考使用。
1. 从一份课设说起:CNN 加随机森林做入侵检测,到底能跑出什么结果
如果你正在为机器学习课程大作业或者毕业设计找方向,网络入侵检测是个很稳的选题——数据集公开、评价指标清晰、答辩时也好讲。但真正动手时你会发现,从原始流量特征到能跑出一个像样的准确率,中间隔着特征清洗、样本不平衡、维度灾难好几道坎。这份资源给了一条完整的流水线:用 UNSW_NB15 官方数据集,先做 Pearson 相关性过滤,再用随机森林做特征选择,最后把筛出来的特征送进 Keras 搭的 CNN 做二分类。42 列特征一列标签,二分类问题,代码按StrTonum.py到CNN.py的顺序串起来就能跑。它适合计算机、通信、人工智能方向的学生直接拿来做期末课设或毕设,也适合想快速了解「传统特征工程 + 深度学习」这套组合拳的从业者。下面我把这份源码拆开,按实际运行顺序讲清楚每一步在干什么、参数怎么调、哪里容易翻车。
2. 数据预处理流水线:从字符串到归一化,每一步都在解决什么
2.1 为什么第一步是 StrTonum.py 而不是直接读 CSV
UNSW_NB15 数据集里混着数值型和分类型特征。像dur、sbytes、dbytes这些是数值,但proto、service、state是字符串类别。CNN 的输入层只认数字,所以第一步必须把字符串映射成整数编码。StrTonum.py干的就是这件事——遍历所有 object 类型的列,用LabelEncoder或者自定义映射表转成数值。常见做法是先把训练集和测试集拼在一起做编码,避免测试集出现训练集没见过的类别导致编码越界。
import pandas as pd from sklearn.preprocessing import LabelEncoder # 读取原始训练集和测试集 train = pd.read_csv('UNSW_NB15_training-set.csv') test = pd.read_csv('UNSW_NB15_testing-set.csv') # 合并后统一编码,保证类别映射一致 full = pd.concat([train, test], axis=0, ignore_index=True) cat_cols = full.select_dtypes(include=['object']).columns.tolist() # 排除标签列,标签单独处理 if 'attack_cat' in cat_cols: cat_cols.remove('attack_cat') le = LabelEncoder() for col in cat_cols: full[col] = le.fit_transform(full[col].astype(str)) # 标签列二值化:0 正常,1 攻击 full['label'] = full['label'].astype(int) # 重新切分 train = full.iloc[:len(train), :].copy() test = full.iloc[len(train):, :].copy()这段代码的关键点在于合并编码。如果分别对训练集和测试集做LabelEncoder,同一个proto值可能在两边被编成不同数字,模型直接学废。参数上注意attack_cat是多分类标签,二分类任务里用不到,要提前剔除,否则它会干扰特征选择。另外LabelEncoder对无序类别是安全的,但如果你后续想换 One-Hot,特征维度会从 42 膨胀到上百,CNN 训练时间成倍增加,课设阶段不建议。
2.2 Min-max 归一化和 Balanced.py 的样本平衡
数值特征量纲差异极大,sbytes可能上万,dur只有零点几秒。不做归一化,CNN 的梯度会被大量纲特征主导,收敛慢甚至不收敛。Min-max 把每个特征线性映射到 [0,1],公式是(x - min) / (max - min)。注意 min 和 max 必须从训练集算,然后应用到测试集,否则就是数据泄露。
from sklearn.preprocessing import MinMaxScaler # 分离特征和标签 feature_cols = [c for c in train.columns if c not in ['label', 'attack_cat']] X_train = train[feature_cols].values y_train = train['label'].values X_test = test[feature_cols].values y_test = test['label'].values # 只在训练集上 fit scaler = MinMaxScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) # 用训练集的 min/maxBalanced.py处理的是另一类问题:UNSW_NB15 里正常流量和攻击流量比例失衡,直接训练会让模型偏向多数类,准确率看着高但召回率惨淡。代码里参考 ADASYN 算法做过采样,核心思想是根据少数类样本的分布密度自适应生成新样本——密度低的区域多生成,密度高的区域少生成。实际运行时如果 ADASYN 太慢,可以退化成 SMOTE,效果差距在课设场景下可以接受。平衡之后正负样本接近 1:1,后续 CNN 的 loss 才不会一边倒。
提示:归一化和平衡的顺序不能反。先平衡再归一化,新生成的样本会参与 min/max 计算,导致测试集归一化基准偏移。正确顺序是先在训练集上归一化,再对归一化后的数据做过采样。
3. 特征选择双保险:Pearson 过滤加随机森林,把 42 维压到多少维合适
3.1 Pearson.py 在筛什么:线性相关性的边界
Pearson 相关系数衡量的是两个连续变量之间的线性关系,取值 -1 到 1,绝对值越接近 1 越相关。Pearson.py计算的是特征与特征之间、特征与标签之间的相关性。特征之间相关性过高说明信息冗余,留着只会增加计算量还可能过拟合;特征与标签相关性太低说明这个特征对分类没贡献,应该丢掉。
import numpy as np import seaborn as sns import matplotlib.pyplot as plt # 计算特征间相关系数矩阵 corr_matrix = pd.DataFrame(X_train, columns=feature_cols).corr(method='pearson') # 找出相关性高于阈值的特征对 threshold = 0.9 upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool)) to_drop = [column for column in upper.columns if any(upper[column].abs() > threshold)] print(f"因高相关拟剔除的特征: {to_drop}")阈值 0.9 是常见起点,但别死守。UNSW_NB15 里sbytes和dbytes这类流量统计特征相关性天然偏高,如果一刀切到 0.8 以下,可能把有用信息也砍了。我的习惯是先跑一遍看分布,如果大部分特征对都在 0.95 以上才考虑降阈值。另外 Pearson 只抓线性关系,对非线性关联无能为力,所以它只是第一道粗筛,不能替代后面的随机森林。
3.2 Find_feature.py 用随机森林做特征重要性排序
随机森林做特征选择的逻辑很直接:训练多棵决策树,每棵树在分裂节点时选最优特征,最后统计每个特征在所有树中被选为分裂点的次数或者带来的不纯度下降总量,归一化后就是特征重要性。Find_feature.py通常会把重要性从高到低排序,然后让你选一个累计贡献率阈值,比如前 15 个特征已经覆盖 95% 的重要性,后面的就可以扔了。
from sklearn.ensemble import RandomForestClassifier # 用训练集训练随机森林 rf = RandomForestClassifier( n_estimators=100, # 树的数量,100 是精度和速度的平衡点 max_depth=None, # 不限制深度,让树充分生长 random_state=42, # 固定随机种子,保证结果可复现 n_jobs=-1 # 用满所有 CPU 核心 ) rf.fit(X_train, y_train) # 获取特征重要性 importances = rf.feature_importances_ indices = np.argsort(importances)[::-1] # 按累计贡献率选特征 sorted_imp = importances[indices] cumsum = np.cumsum(sorted_imp) n_selected = np.searchsorted(cumsum, 0.95) + 1 # 累计到 95% selected_features = [feature_cols[i] for i in indices[:n_selected]] print(f"选中 {n_selected} 个特征: {selected_features}")n_estimators设 100 是课设级别的稳妥选择,再往上加精度提升有限但训练时间线性增长。random_state必须固定,不然每次跑出来的特征排序都不一样,答辩时没法复现。累计贡献率 95% 是我一般会用的值,但如果你发现选出来的特征数还是个位数,可以放宽到 90% 看看是不是漏掉了弱相关但有用的特征。选完之后记得把selected_features存下来,Pro_Testdata.py要用同一套特征列来对齐测试集。
3.3 Pro_Testdata.py 的对齐逻辑:训练集筛了什么,测试集就得跟什么
这一步最容易被忽略,但错了整个评估就废了。训练集经过 Pearson 和随机森林筛完之后只剩一部分特征,测试集必须用完全相同的特征列和顺序。Pro_Testdata.py做的就是按selected_features列表重新索引测试集,确保送入 CNN 的输入维度一致。
# 按训练集选出的特征对齐测试集 X_test_selected = test[selected_features].values # 重新做归一化,注意 scaler 要用训练集 fit 好的那个 X_test_selected = scaler.transform(X_test_selected) # 确认维度一致 assert X_train_selected.shape[1] == X_test_selected.shape[1], "特征维度不匹配"如果跳过这一步直接拿原始 42 维测试集去评估,模型输入维度对不上会直接报错;更隐蔽的情况是维度碰巧一样但特征顺序不同,模型照样能跑但准确率会莫名其妙掉一大截。这种坑我在早期项目里踩过,排查了半天才发现是列顺序问题。
4. CNN.py 搭网络:Keras 结构、参数和训练过程里的几个关键决策
4.1 一维卷积处理表格特征,输入形状怎么定
网络入侵检测的特征是一维向量,不是图像,所以用Conv1D而不是Conv2D。输入形状是(n_features, 1),把每个特征当成一个时间步或者一个通道。Keras 里Conv1D的filters参数决定卷积核数量,kernel_size决定一次看几个相邻特征。对于 15 到 20 维的特征向量,kernel_size设 3 比较合适,太大感受野覆盖过多反而模糊了局部模式。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout from tensorflow.keras.optimizers import Adam n_features = X_train_selected.shape[1] model = Sequential([ # 输入 reshape 成 (n_features, 1) Conv1D(filters=32, kernel_size=3, activation='relu', input_shape=(n_features, 1), padding='same'), MaxPooling1D(pool_size=2), Conv1D(filters=64, kernel_size=3, activation='relu', padding='same'), MaxPooling1D(pool_size=2), Flatten(), Dense(64, activation='relu'), Dropout(0.5), # 丢弃一半神经元,防过拟合 Dense(1, activation='sigmoid') # 二分类输出 ]) model.compile( optimizer=Adam(learning_rate=0.001), loss='binary_crossentropy', metrics=['accuracy'] ) model.summary()两层卷积加两层池化是课设级别的标准配置。filters从 32 到 64 递增,让网络先抓局部细节再组合高层模式。Dropout(0.5)在全连接层之前加,是因为全连接层参数量最大、最容易过拟合。padding='same'保证卷积后长度不变,池化层再降维,避免特征维度掉太快。输出层用sigmoid是因为二分类,如果改成多分类攻击类型识别,要换成softmax并且 loss 改成categorical_crossentropy。
4.2 训练参数和 TensorFlow 与 Keras 的版本对应
训练时batch_size设 64 或 128,epochs先跑 30 看收敛曲线。如果验证集 loss 在 10 轮之后还在降,可以加到 50;如果 5 轮就平了,说明学习率或者网络容量有问题。validation_split=0.2从训练集里切 20% 做验证,不要动测试集。
# reshape 输入 X_train_cnn = X_train_selected.reshape(-1, n_features, 1) X_test_cnn = X_test_selected.reshape(-1, n_features, 1) history = model.fit( X_train_cnn, y_train, batch_size=128, epochs=30, validation_split=0.2, verbose=1 ) # 评估 loss, acc = model.evaluate(X_test_cnn, y_test, verbose=0) print(f"测试集准确率: {acc:.4f}")TensorFlow 和 Keras 的版本对应是个血泪坑。TensorFlow 2.x 已经把 Keras 集成进去了,from tensorflow.keras是正确写法。如果你环境里同时装了独立的keras包,版本不匹配会报ImportError或者某些层参数不识别。requirements.txt里一般会锁版本,但如果你自己配环境,记住 TensorFlow 2.6 以上对应 Keras 2.6 以上,TensorFlow 2.10 之后独立 Keras 包不再需要单独安装。跑之前用pip list | grep tensorflow和pip list | grep keras确认一下。
注意:如果训练时 loss 一直是
nan,先检查输入数据里有没有 NaN 或者无穷大。归一化之前用np.isnan(X_train).sum()扫一遍,有缺失值就填 0 或者均值。
5. 避坑与排查:这份源码跑不起来时先看这五条
5.1 报错ValueError: Input 0 is incompatible with layer conv1d
现象是模型fit的时候直接抛维度不匹配。原因通常是Pro_Testdata.py没跑或者跑的顺序不对,测试集还是 42 维,而模型输入是按筛选后的维度建的。解决办法是确认selected_features列表在训练和测试阶段是同一个,并且X_test_selected的列顺序和X_train_selected完全一致。可以在 reshape 之前打印两个数组的 shape 对比。
5.2 准确率虚高到 99% 但召回率极低
现象是测试集 accuracy 很好看,但混淆矩阵里少数类几乎全被预测成多数类。原因是Balanced.py没执行或者过采样只对训练集做了但评估时用了原始分布。解决方法是确认过采样只作用于训练集,测试集保持原始分布,并且评估时看classification_report里的 recall 和 f1-score,不要只盯 accuracy。
5.3pip install -r requirements.txt卡在 TensorFlow 下载
现象是安装依赖时 TensorFlow 包下载极慢或者超时。原因是 TensorFlow 包体积大,默认源在国外。解决办法是换国内镜像源,比如pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple。如果还是慢,可以单独先装 TensorFlow,再装其他依赖。
5.4 随机森林特征重要性每次跑出来不一样
现象是Find_feature.py两次运行选出的特征数量不同。原因是RandomForestClassifier的random_state没固定。解决办法是在实例化时显式传入random_state=42,并且确保n_estimators不要太小,100 以上结果基本稳定。
5.5 答辩 PPT 里的准确率和代码跑出来的对不上
现象是 PPT 上写的 98.5%,自己跑出来只有 95% 左右。原因是 PPT 里的结果可能是在特定随机种子或者特定预处理参数下得到的,而你的环境有细微差异。解决办法是不要改random_state,不要改归一化范围,不要改特征选择阈值,按源码默认参数跑一遍。如果还是对不上,检查数据集版本是否一致,UNSW_NB15 官方数据集有过更新。
6. 进阶玩法:把二分类改成多分类,以及怎么验证模型不是靠猜
6.1 从二分类扩展到攻击类型识别
原始代码做的是二分类:正常 vs 攻击。但 UNSW_NB15 的attack_cat列有 9 种攻击类型,改成多分类只需要动三个地方。第一,标签处理从label列换成attack_cat列,用LabelEncoder编成 0 到 8。第二,输出层从Dense(1, activation='sigmoid')改成Dense(9, activation='softmax')。第三,loss 从binary_crossentropy改成sparse_categorical_crossentropy,因为标签是整数不是 one-hot。
from tensorflow.keras.utils import to_categorical # 多分类标签 le_attack = LabelEncoder() y_train_multi = le_attack.fit_transform(train['attack_cat']) y_test_multi = le_attack.transform(test['attack_cat']) n_classes = len(le_attack.classes_) # 输出层改 softmax model_multi = Sequential([ Conv1D(32, 3, activation='relu', input_shape=(n_features, 1), padding='same'), MaxPooling1D(2), Conv1D(64, 3, activation='relu', padding='same'), MaxPooling1D(2), Flatten(), Dense(64, activation='relu'), Dropout(0.5), Dense(n_classes, activation='softmax') ]) model_multi.compile( optimizer=Adam(learning_rate=0.001), loss='sparse_categorical_crossentropy', metrics=['accuracy'] )多分类的难点在于类别极度不平衡,某些攻击类型样本只有几十条。这时候Balanced.py里的 ADASYN 要针对每个少数类分别做,不能只对二分类的正类做。另外评估指标要看 macro f1-score,它给每个类别同等权重,不会被多数类拉高。
6.2 验证模型不是靠猜:混淆矩阵和特征重要性交叉检查
跑出高准确率之后,怎么确认模型学到的是真实模式而不是数据泄露或者随机猜测?我一般会做两件事。第一,画混淆矩阵看每一类的误判方向。如果正常流量被大量误判成某一种攻击,说明这个攻击的特征和正常流量重叠严重,需要回去看特征选择是不是漏了关键维度。第二,把随机森林选出的 top 特征和 CNN 第一层卷积核的激活模式对照,如果 CNN 关注的特征和随机森林重要性高的特征高度重合,说明两条路径互相验证了,结果可信度更高。
from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns y_pred = (model.predict(X_test_cnn) > 0.5).astype(int) cm = confusion_matrix(y_test, y_pred) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues') plt.xlabel('Predicted') plt.ylabel('Actual') plt.title('Confusion Matrix') plt.show() print(classification_report(y_test, y_pred, target_names=['Normal', 'Attack']))从那以后我每次跑完入侵检测模型,都强制走一遍混淆矩阵加分类报告,不看 accuracy 就下结论。这份源码把数据预处理、特征选择、CNN 训练串成了一条完整的线,课设答辩够用,想往深了做也有扩展空间。希望帮到你。
本文还有配套的精品资源,点击获取