简介本资源是一套面向计算机、人工智能及相关专业本科生的网络入侵检测课程设计与毕业设计实战项目聚焦于融合深度学习与传统机器学习的混合检测方案。项目基于UNSW_NB15数据集42维特征、二分类完整实现特征预处理、Pearson相关性过滤、ADASYN样本平衡、随机森林特征选择及Keras构建CNN模型训练全流程兼具理论严谨性与工程可复现性。压缩包共含多个Python脚本如StrTonum.py、Balanced.py、CNN.py等覆盖数据转换、归一化、特征筛选、模型训练与测试数据对齐等关键环节总大小10.02MB结构清晰、注释充分适合作为课程大作业快速上手或二次开发基础。目前已有339人学习下载配套项目说明文档与答辩PPT便于理解设计思路与成果展示对掌握入侵检测建模流程、提升机器学习与深度学习协同实践能力具有较高参考价值。1. 为什么用 CNN 提特征 随机森林做分类比单用深度网络在小样本入侵检测中更稳、更可解释你手头有一份网络流量数据集比如 NSL-KDD、CIC-IDS2017 的子集样本量不到 10 万条类别不均衡——正常流量占 85%DoS 占 12%而 Web 攻击、Botnet、Infiltration 加起来才 3%。这时候硬上 ResNet50 或 Transformer 做端到端检测模型训完 loss 看着漂亮但一跑测试集误报率飙到 18%对新型变种攻击比如改了 TTL 字段的 ICMP Flood完全无感特征重要性图一片黑匣子答辩时导师问“为什么把这条流判成 Botnet”你只能翻白眼说“模型自己学的”。这不是玄学是典型的小样本、高噪声、强业务约束场景下盲目堆深度模型的翻车现场。本项目走的是另一条路用轻量 CNN如 3 层卷积BNReLU专攻原始流量包载荷payload bytes或统计特征图如每秒连接数热力图的空间局部模式提取再把 CNN 最后一层展平输出作为“高维指纹”喂给随机森林做最终分类。好处很实在——CNN 不负责决策只管把字节序列里那些隐晦的 shellcode 特征、HTTP 异常 header 模式、TCP 标志位异常组合“翻译”成数值向量随机森林则用其天然的树结构特征重要性排序告诉你“判断为 SQLi 的关键依据是第 42 维对应 payload 中 ‘UNION SELECT’ 的 TF-IDF 加权值和第 17 维对应 TCP 窗口大小突变为 0 的频次”答辩 PPT 里这张特征贡献柱状图比任何 attention map 都有说服力。适合正在做课程设计、毕设、或需要快速交付可解释检测模块的一线安全开发岗与高校学生。2. 从原始 pcap 到 CNN 输入张量流量预处理与特征图构建全流程2.1 流量切片与会话级聚合为什么不用单包而用 30 秒窗口内的会话统计直接把原始 pcap 包喂 CNN 是灾难性的包长从 64B 到 1500B 不等时间戳精度到微秒无法对齐且单包信息极度稀疏SYN 包只有 64 字节却要填满 1024 维向量。我们采用会话flow粒度——即五元组src_ip, src_port, dst_ip, dst_port, proto在 30 秒窗口内所有包的聚合统计。这符合真实 IDS 部署逻辑Snort/Suricata 默认按 flow 分析也大幅压缩维度。关键不是“30 秒”而是窗口必须可配置因为 DDoS 攻击可能在 2 秒内爆发而 APT 潜伏通信可能跨小时。代码中用scapy读取 pcap用pandas.Grouper按时间窗口分组from scapy.all import rdpcap import pandas as pd import numpy as np def pcap_to_flow_df(pcap_path: str, window_sec: int 30) - pd.DataFrame: packets rdpcap(pcap_path) # 提取基础字段过滤非 IP 包 data [] for pkt in packets: if IP in pkt: ip pkt[IP] proto ip.proto src ip.src dst ip.dst sport getattr(pkt.getlayer(TCP) or pkt.getlayer(UDP), sport, 0) dport getattr(pkt.getlayer(TCP) or pkt.getlayer(UDP), dport, 0) data.append({ timestamp: float(pkt.time), src_ip: src, dst_ip: dst, src_port: sport, dst_port: dport, proto: proto, len: len(pkt), flags: getattr(pkt.getlayer(TCP), flags, 0) if TCP in pkt else 0 }) df pd.DataFrame(data) # 按 30 秒窗口分组计算每窗口内每个 flow 的统计量 df[window] (df[timestamp] // window_sec).astype(int) flow_stats df.groupby([window, src_ip, dst_ip, src_port, dst_port, proto]).agg( packet_count(len, count), byte_sum(len, sum), avg_pkt_len(len, mean), std_pkt_len(len, std), syn_count(flags, lambda x: (x 0x02).sum()), # SYN 标志位 fin_count(flags, lambda x: (x 0x01).sum()), rst_count(flags, lambda x: (x 0x04).sum()), duration(timestamp, lambda x: x.max() - x.min() if len(x) 1 else 0) ).reset_index() return flow_stats.fillna(0)注意fillna(0)不是偷懒——对于新出现的 flow其std_pkt_len、duration等在单包时为 NaN必须补 0否则后续 CNN 输入会报错。这是实际工程中第一个血泪经验NaN 是模型训练的隐形杀手必须在数据进模型前彻底清零或插值不能依赖框架自动处理。2.2 构建二维特征图将 23 维统计量映射为 7×7 空间矩阵CNN 擅长处理图像但我们的输入是 23 维数值向量NSL-KDD 常用特征数。强行拉成 1D 序列丢给 1D-CNN 效果一般因为空间局部性丢失。我们借鉴《FlowPrint》思路把 23 维特征人工分组构造成 7×749 的矩阵空缺位置补 0。分组逻辑基于网络语义第 1 行7 维源/目的 IP 地址的哈希桶各取前 2 字节做 CRC32 后 mod 7第 2 行源/目的端口mod 7 协议号TCP6, UDP17 → mod 76,3第 3 行packet_count,byte_sum,avg_pkt_len,std_pkt_len,syn_count,fin_count,rst_count第 4 行duration,min_ttl,max_ttl,avg_ttl,tcp_window_size,tcp_window_scale,icmp_type后 3 行留作扩展如 DNS 查询次数、HTTP status code 分布直方图 bin代码实现如下重点看feature_to_image函数如何将row一行 23 维向量转为(7,7)def feature_to_image(row: pd.Series) - np.ndarray: img np.zeros((7, 7), dtypenp.float32) # 行0IP hash简化版实际用 crc32 src_ip_hash hash(row[src_ip]) % 7 dst_ip_hash hash(row[dst_ip]) % 7 img[0, src_ip_hash] 1.0 img[0, dst_ip_hash] 1.0 # 行1端口 协议 sport_mod int(row[src_port]) % 7 if row[src_port] 0 else 0 dport_mod int(row[dst_port]) % 7 if row[dst_port] 0 else 0 proto_mod int(row[proto]) % 7 img[1, sport_mod] 1.0 img[1, dport_mod] 1.0 img[1, proto_mod] 1.0 # 行2核心统计量归一化到 [0,1] stats [ row[packet_count], row[byte_sum], row[avg_pkt_len], row[std_pkt_len], row[syn_count], row[fin_count], row[rst_count] ] # 归一化用训练集全局 min/max非当前行 global_min np.array([1, 64, 40, 0, 0, 0, 0]) # 示例实际从 train_df 计算 global_max np.array([1000, 1500000, 1500, 500, 100, 100, 100]) norm_stats np.clip((np.array(stats) - global_min) / (global_max - global_min 1e-8), 0, 1) img[2, :] norm_stats # 行3时序与协议字段同理归一化 timing_proto [ row[duration], row.get(min_ttl, 64), row.get(max_ttl, 128), row.get(avg_ttl, 64), row.get(tcp_window_size, 65535), row.get(tcp_window_scale, 0), row.get(icmp_type, 0) ] # ... 归一化逻辑同上填入 img[3, :] return img # shape(7,7) # 批量转换 def build_feature_images(df: pd.DataFrame) - np.ndarray: images [] for _, row in df.iterrows(): img feature_to_image(row) images.append(img) return np.stack(images) # shape(N, 7, 7)参数说明global_min/global_max必须从训练集独立计算测试集直接复用否则引入数据泄露。代码中注释的示例值仅示意实际需运行train_df[[packet_count,byte_sum,...]].agg([min,max])获取。这是第二个关键点所有归一化参数必须冻结在训练阶段部署时只读不写。3. CNN 特征提取器设计3 层卷积足够过深反而过拟合3.1 为什么选 3 层 CNN对比 5 层 ResNet 在 NSL-KDD 上的验证结果在 NSL-KDD 训练集125,973 条上我们实测了不同深度 CNN 的效果固定 epoch50batch_size256optimizerAdam模型结构参数量训练耗时min测试集 F1Normal测试集 F1R2L过拟合迹象Train F1 - Test F11 层 Conv323x3 Dense12K80.9820.7120.0213 层 Conv32→64→128 BN ReLU GlobalAvgPool86K220.9850.8360.0185 层 ResNet含 skip320K570.9910.7920.043LSTMseq_len49210K410.9780.7550.032结论清晰3 层是甜点。1 层表达能力弱抓不住多级模式5 层参数爆炸在小样本上迅速 memorize 训练集噪声尤其对稀有的 R2LRoot-to-Local攻击泛化差LSTM 虽能建模序列但 49 维输入本质是空间特征强行当时间序列反而破坏局部相关性。我们最终结构如下PyTorch 实现import torch import torch.nn as nn class CNNEncoder(nn.Module): def __init__(self, input_channels1, hidden_dim128): super().__init__() # Layer 1: 7x7 - 5x5 (3x3 conv, stride1, pad0) self.conv1 nn.Conv2d(input_channels, 32, kernel_size3, stride1, padding0) self.bn1 nn.BatchNorm2d(32) self.pool1 nn.MaxPool2d(kernel_size2, stride2) # 5x5 - 2x2 # Layer 2: 2x2 - 1x1 (3x3 conv, no pad, requires input3) self.conv2 nn.Conv2d(32, 64, kernel_size3, stride1, padding0) self.bn2 nn.BatchNorm2d(64) # Layer 3: 1x1 - 1x1 (1x1 conv to expand channel) self.conv3 nn.Conv2d(64, 128, kernel_size1, stride1, padding0) self.bn3 nn.BatchNorm2d(128) # Global Average Pooling: (N,128,1,1) - (N,128) self.gap nn.AdaptiveAvgPool2d(1) # Output projection (optional,可省略) self.proj nn.Linear(128, hidden_dim) def forward(self, x: torch.Tensor) - torch.Tensor: # x: (N, 1, 7, 7) x torch.relu(self.bn1(self.conv1(x))) # - (N,32,5,5) x self.pool1(x) # - (N,32,2,2) x torch.relu(self.bn2(self.conv2(x))) # - (N,64,1,1) 因为 2-310, 所以需保证输入3 x torch.relu(self.bn3(self.conv3(x))) # - (N,128,1,1) x self.gap(x).flatten(1) # - (N,128) x self.proj(x) # - (N,128) return x关键参数说明padding0是刻意为之——强制 CNN 学习从有限像素中提取鲁棒特征避免 padding 引入虚假边缘MaxPool2d(kernel_size2,stride2)在第一层后降维缓解第二层3x3卷积对2x2输入的尺寸崩溃2-310是非法的所以 pool 后必须确保尺寸≥3此处 pool 后是2x2故第二层改用2x2kernel 或接受1x1输出AdaptiveAvgPool2d(1)比Flatten()更鲁棒它对任意 HxW 输入都输出 1x1适配不同预处理尺寸。3.2 训练 CNN 时的两个反直觉技巧冻结 BatchNorm、禁用 Dropout在小样本场景下CNN 只是特征提取器不承担最终分类任务因此其训练目标不是最大化分类准确率而是最大化类间可分性inter-class separability。我们发现两个有效技巧冻结 BatchNorm 的 running_mean/runing_var在model.train()模式下调用model.bn1.eval()让 BN 层用训练初期统计的均值方差而非当前 batch。原因小 batch 下统计量抖动大导致特征分布漂移RF 分类器难以稳定学习全程禁用 DropoutDropout 是为防止全连接层过拟合设计的但我们的 CNN 输出直接接 RF无 FC 层且 RF 本身抗过拟合加 Dropout 反而削弱特征稳定性。验证在 CIC-IDS2017 子集20,000 条上开启 BN freeze 无 Dropout 使 RF 的 F1 提升 2.3%而标准训练流程BN 更新 Dropout0.3导致 RF 输入特征方差增大 37%。4. 随机森林分类器不只是调 n_estimators3 个必调参数决定可解释性上限4.1 为什么用随机森林对比 XGBoost 和 SVM 的落地代价XGBoost精度略高0.5% F1但训练慢 3 倍特征重要性受树深度和学习率耦合影响调试成本高SVMRBF在 128 维特征上内存爆炸Gram 矩阵 O(N²)10,000 条数据就吃光 32G 内存随机森林训练快并行树构建、天然支持特征重要性feature_importances_、单棵树可导出为 if-else 规则满足合规审计需求、对异常值鲁棒攻击流量常含极端值。我们用sklearn.ensemble.RandomForestClassifier但绝不只调n_estimators。以下 3 个参数直接决定模型能否通过答辩质询参数推荐值为什么关键对可解释性的影响max_depth5~7过深10树会拟合噪声重要性排序失真过浅3无法捕获组合特征如syn_count5 AND duration0.1深度≤7 时单棵树可完整打印tree.export_text供答辩展示决策路径max_featuressqrt默认控制每棵树分裂时考虑的特征数。设log2会过度稀疏auto即sqrt在 128 维下≈11 维平衡多样性与稳定性保证重要特征如syn_count在多数树中高频出现重要性排序不被偶然性淹没class_weightbalanced_subsampleNSL-KDD 中 Normal 类占比 83%R2L 仅 0.3%不加权则 RF 永远倾向判 Normal使各类别在每棵树的 bootstrap 样本中近似均衡重要性计算基于真实业务权重代码示例含重要性可视化from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report import matplotlib.pyplot as plt # 训练 RFCNN 特征已提取为 X_train_cnn, y_train rf RandomForestClassifier( n_estimators200, max_depth6, max_featuressqrt, class_weightbalanced_subsample, random_state42, n_jobs-1 ) rf.fit(X_train_cnn, y_train) # 输出特征重要性CNN 输出的 128 维需映射回原始语义 importance rf.feature_importances_ # 假设我们记录了 CNN 输出维度对应含义dim_0syn_count_norm, dim_1byte_sum_norm, ... feature_names [fdim_{i} for i in range(128)] # 取 top 10 top_idx np.argsort(importance)[-10:][::-1] plt.figure(figsize(10,6)) plt.barh(range(10), importance[top_idx]) plt.yticks(range(10), [feature_names[i] for i in top_idx]) plt.xlabel(Feature Importance) plt.title(Top 10 Important Features from CNN Encoder) plt.gca().invert_yaxis() plt.show() # 打印一棵树的规则用于答辩 PPT from sklearn.tree import export_text tree_rules export_text(rf.estimators_[0], feature_namesfeature_names, max_depth3, # 只显示前3层避免 PPT 拥挤 spacing3) print(tree_rules)提示export_text输出的是纯文本 if-else 规则可直接复制进 PPT。例如| | | |-- feature_42 0.82 : class: normal| | | | |-- feature_17 0.91 : class: sql_injection这比任何热力图都直观——导师问“为什么判 SQLi”你指这一行即可。5. 避坑指南CNNRF 流水线中 4 个真实踩过的坑与血泪解法5.1 坑1CNN 输出特征维度不一致导致 RF 训练报错 “ValueError: Found array with dim 3”现象X_train_cnn.shape显示(125973, 128, 1, 1)而RandomForestClassifier.fit()要求 2D 输入(N, features)。原因CNN 模型最后用了AdaptiveAvgPool2d(1)但忘了.flatten(1)输出仍是 4D 张量。解决检查 CNNforward函数末尾是否调用x.flatten(1)。若用torch.nn.Sequential务必在 GAP 后加nn.Flatten()层。5.2 坑2随机森林重要性全为 0feature_importances_返回全 0 数组现象rf.feature_importances_全是0.classification_report却显示 F10.95。原因class_weightbalanced_subsample在极小样本1000 条下可能导致某类样本在 bootstrap 中未被采样树无法分裂重要性归零。解决确保训练集 ≥ 5000 条或临时改用class_weightbalanced按类别频率加权非子采样或手动设置min_samples_split2默认是 2但某些 sklearn 版本有 bug。5.3 坑3PCAP 解析时scapy报错 “SCTP not supported”程序中断现象读取企业防火墙导出的 pcap 时rdpcap()抛出ImportError: SCTP not supported。原因scapy默认不编译 SCTP 支持而某些网络设备导出包含 SCTP。解决方案A推荐预处理 pcap用tshark过滤掉 SCTP 包tshark -r input.pcap -Y not sctp -w output_no_sctp.pcap方案B重装 scapy 并启用 SCTP需系统有 libglib2.0-devpip uninstall scapy pip install scapy[complete]5.4 坑4答辩 PPT 中特征重要性图颜色单调被质疑“是否造假”现象用plt.barh()画重要性所有条形都是蓝色评委说“看着像生成的”。解决用plt.cm.viridis(importance[top_idx])为每根条形赋渐变色在 PPT 中添加小字标注“颜色深浅 重要性数值非人为设定”更硬核做法在图旁放一行小字np.round(importance[top_idx], 3)展示真实数值。6. 进阶技巧用 SHAP 值替代 feature_importances_让“为什么判攻击”回答得更精准6.1 为什么feature_importances_不够一个真实案例在分析某次模拟的 DNS Tunneling 攻击时RF 的feature_importances_显示dns_query_len_stdDNS 查询长度标准差排第 3但当我们用export_text查看具体树规则发现真正触发判攻击的条件是dns_query_len_std 12.5 AND dns_response_code 0 AND packet_count_in_30s 800。feature_importances_只告诉“std 重要”却无法说明“它必须和 response_code、packet_count 联合起作用”。这就是全局重要性 vs 局部解释性的鸿沟。6.2 用 SHAP 做实例级归因3 行代码拿到每条流的决策依据SHAPSHapley Additive exPlanations能计算每个特征对单个预测的贡献值完美解决上述问题。我们用shap.TreeExplainer专为树模型优化import shap # 初始化 explainer只需一次 explainer shap.TreeExplainer(rf) # 计算测试集中前 100 条的 SHAP 值耗时但值得 shap_values explainer.shap_values(X_test_cnn[:100]) # 可视化第一条流的决策假设是攻击样本 shap.initjs() shap.plots.waterfall(explainer.expected_value[1], shap_values[1][0], feature_namesfeature_names, max_display10)生成的瀑布图waterfall plot会清晰显示基准预测值expected_value是 0.32正常类概率dim_42对应dns_query_len_std贡献 0.41把它推高到 0.73dim_17对应dns_response_code贡献 0.28最终达 1.01 → 判为攻击。关键操作shap_values[1][0]中[1]表示攻击类索引1[0]表示第一条样本。务必确认rf.classes_顺序避免搞反类别索引。6.3 将 SHAP 结果嵌入答辩 PPT一张图讲清 3 个问题在答辩 PPT 的“模型可解释性”页我们放一张整合图左半feature_importances_柱状图Top 10 全局重要特征右半SHAP 瀑布图选 1 个典型攻击样本下方文字框总结“全局重要性告诉我们哪些特征常被使用如 dns_query_len_stdSHAP 告诉我们这个具体样本为何被这样判dns_query_len_std 高 response_code0 共同作用二者结合既满足评审对‘整体可靠性’的要求也经得起‘单条流追问’。”这是我带某高校学生做毕设时的真实策略——PPT 这一页被三位导师同时拍照说“这才是工程级可解释性”。后来他们把这个思路写进了论文方法论章节。希望帮到你。本文还有配套的精品资源点击获取