基于深度学习的网络入侵检测:从数据清洗到模型上线的完整实战
发布时间:2026/9/29 19:18:32来源:尧图网络
简介这份资源是面向高校学生与人工智能初学者的深度学习网络入侵检测完整项目包可作为毕业设计、课程设计或网络安全入门实践参考。项目围绕NIDS展开用CNN、RNN、LSTM等模型自动提取网络流量特征并识别恶意行为覆盖数据预处理、特征提取、模型训练与实时检测等模块并涉及联邦学习任务生成等分布式场景。压缩包共174个文件约83.58MB以97个Python脚本为核心辅以57个编译文件、4个Jupyter Notebook、5份Markdown说明及少量xml、pkl、txt等配置与数据文件便于复现与二次开发。已有67人学习下载。读者可获得可运行的检测流程、模型评估脚本、数据可视化代码与依赖清单快速理解从流量清洗到入侵判别的完整链路并在此基础上调整模型或迁移到自有数据集。1. 从一份「网络入侵检测.zip」说起深度学习到底能补上传统 IDS 的哪块短板很多人第一次拿到「基于深度学习的网络入侵检测.zip」这类项目第一反应是解压、装依赖、跑train.py然后被一堆报错劝退。但真正决定这个方向值不值得投入的不是代码能不能跑而是它到底解决了传统入侵检测系统的什么痛点。传统 IDS 靠 Snort、Suricata 这类规则引擎特征库更新滞后遇到变种流量、加密隧道里的异常行为、0day 利用就抓瞎而深度学习做入侵检测的核心价值是把「人工写规则」换成「从流量里学分布」——用 CNN 抓字节级局部模式用 LSTM/GRU 抓会话时序用自编码器做无监督异常重建。它适合谁适合已经懂一点 Python、想把手上的 NSL-KDD、CIC-IDS2017、UNSW-NB15 数据集跑出可复现指标的安全工程师也适合做毕设、想找一个能讲清楚「特征工程 模型选型 评估陷阱」的实战项目的人。这一篇不讲玄学只讲从数据到上线推理的完整链路以及那些让模型指标虚高、上线就翻车的坑。2. 数据先行NSL-KDD 与 CIC-IDS2017 的清洗、编码与切分2.1 为什么数据决定了一半的成败网络入侵检测的公开数据集有个共同特点类别极度不平衡正常流量占 80% 以上U2R、R2L 这类攻击样本可能只有几十条。如果你直接train_test_split然后看 accuracy99% 的准确率毫无意义——模型只要全预测成 Normal 就能拿到这个数。所以第一步不是建模是把数据分布摸清楚决定用哪种采样策略和评估指标。NSL-KDD 是 KDD99 的去重版每条记录 41 维特征含 protocol_type、service、flag 三个类别特征标签分 Normal、DoS、Probe、R2L、U2R 五类。CIC-IDS2017 更接近真实流量用 CICFlowMeter 提取了 80 多维流特征但原始 CSV 里有 NaN、无穷大、列名带空格等问题必须先清洗。2.2 可复现的清洗与编码脚本import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder, MinMaxScaler from sklearn.model_selection import train_test_split # 读取 NSL-KDD注意它没有表头列名需手动指定 col_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,label,difficulty] df pd.read_csv(KDDTrain.txt, namescol_names) df.drop(columns[difficulty], inplaceTrue) # 把 5 类攻击归成 5 个大类避免类别过细导致样本太少 attack_map { normal: Normal, back:DoS,land:DoS,neptune:DoS,pod:DoS,smurf:DoS,teardrop:DoS, ipsweep:Probe,nmap:Probe,portsweep:Probe,satan:Probe, ftp_write:R2L,guess_passwd:R2L,imap:R2L,multihop:R2L,phf:R2L, spy:R2L,warezclient:R2L,warezmaster:R2L, buffer_overflow:U2R,loadmodule:U2R,perl:U2R,rootkit:U2R } df[label] df[label].map(attack_map) # 类别特征做 LabelEncoder数值特征做 MinMax 归一化 cat_cols [protocol_type,service,flag] for c in cat_cols: df[c] LabelEncoder().fit_transform(df[c]) num_cols [c for c in df.columns if c not in cat_cols [label]] scaler MinMaxScaler() df[num_cols] scaler.fit_transform(df[num_cols]) # 分层切分保证每个类别在训练集和测试集比例一致 X df.drop(columns[label]).values.astype(np.float32) y LabelEncoder().fit_transform(df[label]) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42) print(train:, X_train.shape, test:, X_test.shape)这段脚本的关键点有三个。第一stratifyy必须加否则少数类可能在测试集里一条都没有评估直接失真。第二MinMaxScaler 只在训练集上 fit再 transform 测试集我这里为了简洁全量 fit 了严格做法应该scaler.fit(X_train)后分别 transform避免数据泄漏。第三LabelEncoder 对 service 这种高基数特征NSL-KDD 有 70 种取值会引入虚假的序关系更稳的做法是 One-Hot 或 Embedding但维度会涨到 120 多维树模型和 CNN 都能吃LSTM 建议先降维。2.3 CIC-IDS2017 的额外处理CIC-IDS2017 的 CSV 里Flow Bytes/s和Flow Packets/s两列经常出现inf直接喂给模型会得到 NaN 梯度。处理方式是先df.replace([np.inf, -np.inf], np.nan)再对缺失值填 0 或中位数。另外它的标签列叫Label里面除了 BENIGN 还有 Web Attack 的三种子类建议合并成 Web Attack 一类否则样本量小于 100 的类会让 macro-F1 剧烈波动。提示不要用随机过采样RandomOverSampler直接复制少数类样本那会让模型记住重复样本测试集指标虚高。优先用 SMOTE 在特征空间插值或者直接用 class_weight 让损失函数对少数类加权。3. 模型选型CNN、LSTM 与自编码器在流量特征上的分工3.1 三种主流结构的适用边界流量数据有两种视角一是把每条记录的 N 维特征当成一个向量二是把一次会话的多个包当成时间序列。CNN 适合前者——把 41 维特征 reshape 成 1×41 或 6×7 的「伪图像」用一维卷积核在特征维度上滑动能自动捕捉src_bytes和dst_bytes之间的局部组合模式。LSTM/GRU 适合后者——如果你有按时间排序的包序列用它抓会话内的时序依赖。自编码器则是无监督路线只用正常流量训练推理时看重建误差超过阈值就判异常适合没有标注数据的场景。我一般会先跑一个 CNN 做 baseline因为它在 NSL-KDD 上收敛快、调参少通常 10 个 epoch 内就能到 99% 左右的加权 F1。如果数据有时序结构再上 LSTM 对比。自编码器留到「只有正常流量标注」或者「想检测未知攻击」时用。3.2 一个可跑的 CNN 模型定义import torch import torch.nn as nn class IDSCNN(nn.Module): def __init__(self, n_features, n_classes): super().__init__() # 把 1D 特征当成单通道信号用两层一维卷积提局部模式 self.conv nn.Sequential( nn.Conv1d(1, 32, kernel_size3, padding1), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(32, 64, kernel_size3, padding1), nn.BatchNorm1d(64), nn.ReLU(), nn.AdaptiveAvgPool1d(1) # 全局平均池化避免展平后参数爆炸 ) self.fc nn.Sequential( nn.Linear(64, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, n_classes) ) def forward(self, x): # x: (batch, n_features) - (batch, 1, n_features) x x.unsqueeze(1) x self.conv(x).squeeze(-1) return self.fc(x)参数说明kernel_size3是流量特征建模的常用起点再大容易把不相关特征混进同一感受野BatchNorm1d在卷积后加能显著稳住训练尤其是特征量纲差异大时AdaptiveAvgPool1d(1)把每个通道压成一个值比直接 flatten 少了几十倍参数小数据集上更不容易过拟合Dropout(0.3)是经验值NSL-KDD 这种 12 万条的数据集上 0.2~0.5 都试过0.3 比较稳。训练时损失函数用CrossEntropyLoss(weightclass_weights)class_weights 用1 / 类别频率归一化后传入这样少数类 U2R 的梯度不会被 Normal 淹没。优化器 Adamlr1e-3batch_size256早停看验证集 macro-F1 而不是 accuracy。3.3 LSTM 版本什么时候值得上如果你的数据是 CIC-IDS2017 按时间戳排序的流序列或者你自己从 pcap 里按五元组切出了包序列LSTM 才有意义。否则把 41 维静态特征硬塞进 LSTM效果和 CNN 差不多还慢好几倍。LSTM 的隐藏层维度一般取 64 或 128层数 1~2 层足够再深容易过拟合。输入需要 reshape 成(batch, seq_len, n_features)如果每条记录只有一个时间步那 seq_len1等于退化成全连接不如直接用 MLP。注意不要迷信「模型越深越好」。入侵检测数据集规模有限超过 4 层的网络在 NSL-KDD 上几乎必然过拟合验证 loss 会在第 5 个 epoch 后反弹。血泪经验是先把 CNN 或 MLP 调到 macro-F1 0.95 以上再考虑加结构。4. 训练、评估与上线推理从脚本到可复现指标4.1 训练循环里必须记录的三个量很多人训练只打印 loss最后看测试集 accuracy这是翻车的根源。你至少要在每个 epoch 记录训练 loss、验证集 macro-F1、验证集混淆矩阵。macro-F1 对少数类敏感能暴露「模型是不是只学会了预测 Normal」。混淆矩阵能让你看到 U2R 被误判成了哪一类是数据问题还是模型容量问题。from sklearn.metrics import f1_score, confusion_matrix import numpy as np def evaluate(model, loader, device): model.eval() preds, labels [], [] with torch.no_grad(): for xb, yb in loader: xb xb.to(device) logits model(xb) preds.extend(logits.argmax(1).cpu().numpy()) labels.extend(yb.numpy()) macro_f1 f1_score(labels, preds, averagemacro) cm confusion_matrix(labels, preds) return macro_f1, cm # 训练循环片段 best_f1 0.0 for epoch in range(50): model.train() for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() optimizer.step() val_f1, cm evaluate(model, val_loader, device) if val_f1 best_f1: best_f1 val_f1 torch.save(model.state_dict(), best_ids.pt) print(fepoch {epoch} val_macro_f1{val_f1:.4f})这段代码里averagemacro是关键它把每个类别的 F1 等权平均Normal 占再多也不影响。保存 best 模型而不是最后一个因为最后一个往往已经过拟合。torch.save只存 state_dict不存整个模型对象部署时更灵活。4.2 上线推理的预处理一致性模型在测试集上 99%上线后一塌糊涂十有八九是预处理不一致。训练时用了 MinMaxScaler推理时新来一条流量你必须用同一个 scaler 的 min_ 和 scale_ 去 transform而不是重新 fit。类别特征的 LabelEncoder 映射表也要一起保存遇到训练时没见过的 service 值比如新协议要有兜底策略——映射到 unknown 类别而不是直接报错。import joblib # 训练结束后保存预处理器 joblib.dump(scaler, scaler.pkl) joblib.dump(le_dict, label_encoders.pkl) # le_dict 是每个类别列一个 LabelEncoder # 推理时加载 scaler joblib.load(scaler.pkl) le_dict joblib.load(label_encoders.pkl) def preprocess_one(raw_dict): # raw_dict 是单条流量的原始特征字典 for c, le in le_dict.items(): val raw_dict.get(c, unknown) if val not in le.classes_: val le.classes_[0] # 兜底到第一个已知类别 raw_dict[c] le.transform([val])[0] vec np.array([raw_dict[c] for c in feature_order], dtypenp.float32) return scaler.transform(vec.reshape(1, -1))feature_order必须和训练时的列顺序完全一致差一列结果就全错。建议把 feature_order 也存成 json推理服务启动时加载校验。4.3 阈值与告警策略分类模型输出的是 softmax 概率直接取 argmax 会把很多低置信度的正常流量误报成攻击。实际部署时我一般会加一个置信度阈值只有当攻击类概率超过 0.8 且高于 Normal 概率一定 margin 时才告警否则标记为「待人工复核」。这样能把误报率压下来代价是漏报略升具体阈值要在你的业务流量上跑一周再定。提示上线前一定用一段真实抓包数据做 shadow 测试把模型预测和现有 IDS 告警做对比看模型多报了哪些、漏了哪些。这一步能发现 80% 的预处理和阈值问题。5. 避坑与排查让指标虚高、上线翻车的五个真实原因5.1 现象测试集 99.9%混淆矩阵里 U2R 全是 0原因类别极度不平衡且没有用 class_weight 或重采样模型学会了全部预测 Normal 或 DoS。解决先看混淆矩阵确认少数类是否被完全忽略加CrossEntropyLoss(weight...)权重取总样本数 / (类别数 * 该类样本数)同时把评估指标从 accuracy 换成 macro-F1。5.2 现象训练 loss 一直降验证 loss 第 3 个 epoch 就开始涨原因模型容量相对数据量过大或者特征里有泄漏。常见泄漏是先把全量数据做了 MinMaxScaler 再切分测试集的 min/max 信息渗进了训练。解决切分后再 fit scaler减小模型宽度或加 Dropout用早停patience 设 5。5.3 现象离线指标很好上线后同一批流量预测结果和离线不一致原因预处理不一致最常见的是类别编码顺序变了、特征列顺序变了、scaler 重新 fit 了。解决把 scaler、LabelEncoder、feature_order 全部持久化推理服务启动时加载同一份文件写一个单元测试用训练集前 10 条数据跑推理结果必须和离线一致。5.4 现象CIC-IDS2017 训练时 loss 变成 NaN原因Flow Bytes/s等列存在 inf归一化后仍是 inf反向传播产生 NaN 梯度。解决读入后立刻replace([np.inf, -np.inf], np.nan)再fillna(0)或中位数填充检查所有数值列的 dtype 和取值范围。5.5 现象模型把某些正常业务流量持续误报为攻击原因训练集里的正常流量不包含这类业务模式模型没见过就判异常。解决收集误报样本加入训练集重新训练或者对这类流量做白名单规则前置过滤。纯靠模型解决所有误报不现实工程上一定是模型 规则 人工复核三层。6. 进阶技巧用自编码器做无监督异常检测与阈值调优有监督分类的前提是你有标注好的攻击样本但真实环境里新攻击层出不穷标注永远滞后。这时候自编码器Autoencoder是一条值得投入的路线只用正常流量训练让网络学会压缩重建正常模式推理时算重建误差MSE误差超过阈值就判异常。它的好处是不依赖攻击标注能抓到未知攻击代价是阈值难调且对正常流量里的罕见模式也会误报。结构上编码器和解码器各用两三层全连接即可瓶颈层维度取输入维度的 1/4 到 1/2。NSL-KDD 41 维输入瓶颈取 16 或 20 比较合适。训练时只喂 Normal 类损失用 MSE优化器 Adamlr1e-3训练到验证集重建 loss 不再下降为止。class IDSAE(nn.Module): def __init__(self, n_features, bottleneck16): super().__init__() self.encoder nn.Sequential( nn.Linear(n_features, 32), nn.ReLU(), nn.Linear(32, bottleneck), nn.ReLU() ) self.decoder nn.Sequential( nn.Linear(bottleneck, 32), nn.ReLU(), nn.Linear(32, n_features) ) def forward(self, x): z self.encoder(x) return self.decoder(z) # 只用正常流量训练 normal_X X_train[y_train normal_label] ae IDSAE(n_featuresnormal_X.shape[1]).to(device) opt torch.optim.Adam(ae.parameters(), lr1e-3) criterion nn.MSELoss() for epoch in range(100): ae.train() recon ae(torch.tensor(normal_X, devicedevice)) loss criterion(recon, torch.tensor(normal_X, devicedevice)) opt.zero_grad(); loss.backward(); opt.step() # 用验证集正常流量的重建误差分布定阈值 ae.eval() with torch.no_grad(): recon_val ae(torch.tensor(X_val_normal, devicedevice)) err ((recon_val - torch.tensor(X_val_normal, devicedevice)) ** 2).mean(1) threshold np.percentile(err.cpu().numpy(), 99) # 取 99 分位容忍 1% 正常误报阈值取 99 分位意味着允许 1% 的正常流量被误判为异常这个比例要根据你的告警容量调。如果每天只能处理 100 条告警就按流量总量反推分位点。推理时对每条流量算重建误差超过 threshold 就告警同时把误差值一起输出方便人工复核时排序。一个我踩过的坑自编码器对特征尺度非常敏感如果某个特征方差特别大重建误差会被它主导其他特征的异常就被淹没。所以训练前一定要做标准化StandardScaler 比 MinMaxScaler 更适合 AE并且可以按特征维度分别算误差再加权而不是直接取全局 MSE。验证自编码器效果时不要只看 AUC要把攻击样本的重建误差分布和正常样本画在一起看两者重叠区域有多大。重叠多说明阈值怎么调都是误报和漏报的权衡这时候要么加特征要么换有监督模型补位。我的习惯是自编码器负责「发现未知」有监督 CNN 负责「确认已知」两者输出一起送进告警聚合层实际跑下来比单模型稳得多。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网