☰
基于深度学习的网络入侵检测:从数据清洗到模型上线的完整实战
2026/9/29 19:18:27 网站建设 项目流程

简介:这份资源是面向高校学生与人工智能初学者的深度学习网络入侵检测完整项目包,可作为毕业设计、课程设计或网络安全入门实践参考。项目围绕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然后看 accuracy,99% 的准确率毫无意义——模型只要全预测成 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", names=col_names) df.drop(columns=["difficulty"], inplace=True) # 把 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_size=0.2, stratify=y, random_state=42) print("train:", X_train.shape, "test:", X_test.shape)

这段脚本的关键点有三个。第一,stratify=y必须加,否则少数类可能在测试集里一条都没有,评估直接失真。第二,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_size=3, padding=1), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(32, 64, kernel_size=3, padding=1), 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_size=3是流量特征建模的常用起点,再大容易把不相关特征混进同一感受野;BatchNorm1d在卷积后加能显著稳住训练,尤其是特征量纲差异大时;AdaptiveAvgPool1d(1)把每个通道压成一个值,比直接 flatten 少了几十倍参数,小数据集上更不容易过拟合;Dropout(0.3)是经验值,NSL-KDD 这种 12 万条的数据集上 0.2~0.5 都试过,0.3 比较稳。

训练时损失函数用CrossEntropyLoss(weight=class_weights),class_weights 用1 / 类别频率归一化后传入,这样少数类 U2R 的梯度不会被 Normal 淹没。优化器 Adam,lr=1e-3,batch_size=256,早停看验证集 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_len=1,等于退化成全连接,不如直接用 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, average="macro") 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(f"epoch {epoch} val_macro_f1={val_f1:.4f}")

这段代码里average="macro"是关键,它把每个类别的 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], dtype=np.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,优化器 Adam,lr=1e-3,训练到验证集重建 loss 不再下降为止。

class IDSAE(nn.Module): def __init__(self, n_features, bottleneck=16): 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_features=normal_X.shape[1]).to(device) opt = torch.optim.Adam(ae.parameters(), lr=1e-3) criterion = nn.MSELoss() for epoch in range(100): ae.train() recon = ae(torch.tensor(normal_X, device=device)) loss = criterion(recon, torch.tensor(normal_X, device=device)) opt.zero_grad(); loss.backward(); opt.step() # 用验证集正常流量的重建误差分布定阈值 ae.eval() with torch.no_grad(): recon_val = ae(torch.tensor(X_val_normal, device=device)) err = ((recon_val - torch.tensor(X_val_normal, device=device)) ** 2).mean(1) threshold = np.percentile(err.cpu().numpy(), 99) # 取 99 分位,容忍 1% 正常误报

阈值取 99 分位意味着允许 1% 的正常流量被误判为异常,这个比例要根据你的告警容量调。如果每天只能处理 100 条告警,就按流量总量反推分位点。推理时对每条流量算重建误差,超过 threshold 就告警,同时把误差值一起输出,方便人工复核时排序。

一个我踩过的坑:自编码器对特征尺度非常敏感,如果某个特征方差特别大,重建误差会被它主导,其他特征的异常就被淹没。所以训练前一定要做标准化(StandardScaler 比 MinMaxScaler 更适合 AE),并且可以按特征维度分别算误差再加权,而不是直接取全局 MSE。

验证自编码器效果时,不要只看 AUC,要把攻击样本的重建误差分布和正常样本画在一起,看两者重叠区域有多大。重叠多说明阈值怎么调都是误报和漏报的权衡,这时候要么加特征,要么换有监督模型补位。我的习惯是自编码器负责「发现未知」,有监督 CNN 负责「确认已知」,两者输出一起送进告警聚合层,实际跑下来比单模型稳得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询