深度强化学习入侵检测实战:DQN状态动作奖励设计、调参与测试集排坑指南
简介基于深度强化学习A3C算法的网络入侵检测系统Python源码附带KDD Cup数据集面向计算机、信息安全、人工智能等专业在校学生与开发者适合作为毕业设计、期末大作业或初期项目立项演示。资源打包为zip格式共50个文件涵盖16个py源码模块包括数据预处理、自定义环境构建、A3C训练与测试、策略监控等核心环节20个txt文件提供数据集及说明文档8个data文件为处理后的样本数据另有4个eps实验结果图和2个bat批处理脚本包体大小约19.06MB。项目代码结构清晰可直接运行测试能够完整呈现深度强化学习在网络入侵检测任务中的建模与训练流程帮助读者理解A3C算法、状态空间设计及攻击类型识别思路具备较高的学习借鉴价值。目前已有343人学习下载适合需要快速上手强化学习与网络安全交叉方向的读者使用。1. 基于深度强化学习的网络入侵检测系统这份源码到底值不值得吃透做安全方向的人对入侵检测应该都不陌生规则引擎堆了一堆特征误报还是压不下去传统机器学习模型跑得挺快遇到没见过攻击变种又容易抓瞎。这份基于深度强化学习开发的网络入侵检测系统Python源码把检测逻辑训练成一个能根据环境反馈不断调整策略的智能体随包带数据集解压后就能跑通从训练到评估的完整链路。它适合两类人一类是拿深度强化学习算法做毕设或练手、但不想只跑老掉牙CartPole的Python工程师另一类是正在做异常流量识别、苦于没有安全工作台可参考的安全研发或运维。核心价值在于把“怎么检测攻击”从写规则变成训决策后面所有改代码的动作都围绕这一点展开。2. 把入侵检测建模成强化学习问题状态、动作、奖励三个核心设计拿到这份源码第一件该做的事不是急着跑训练而是看懂作者或者你自己要延续的建模方式。强化学习和普通监督学习最大的区别是它没有静态的“标签-预测”对只有“状态-动作-奖励-下一状态”的交互闭环。入侵检测要怎么放进这个闭环决定了后面所有代码怎么改。2.1 状态表示网络流特征与原始报文两种喂法把网络流量喂给强化学习智能体最常见的路线是特征级表示。直接拿入侵检测数据集里每条连接记录的特征做状态。NSL-KDD这条路线最有代表性每条记录41个字段其中protocol_type、service、flag这类枚举特征需要做one-hot展开数值特征做归一化。这个方案的优点是状态维度小、训练快、可解释性强缺点是特征工程做得好不好直接决定智能体的上限。这类基于深度强化学习开发的网络入侵检测系统默认走的就是这条路——压缩包里的数据集解压后就是这种“一行样本代表一条网络连接记录”的格式。第二条路是报文级表示。不提取特征直接把原始流量的字节序列、包长序列或会话前N个包丢给CNN或LSTM做状态编码。这个做法上限更高因为特征由模型自己学但训练成本和数据量要求都上去了单机跑起来非常吃力。我一般建议新人先把特征级流程跑通再考虑报文级。状态向量就是一行样本特征智能体每走一步看到的是一个维度固定的数组这一步的action、reward和下一步的state构成一条经验记录。这里有一个直接影响训练效果的点state的数值尺度。protocol_type被one-hot成0/1但duration可能到几百src_bytes可能到几十万。这种尺度差异会让DQN的Q值更新非常不稳定所以代码里统一用MinMaxScaler把所有列压到[0,1]区间。这是新手最容易跳过的步骤之一也是第五章排查单里最高发的黑匣子之一。2.2 动作空间二分类、多分类与特征选择型动作动作空间决定智能体能做什么它的设计比网络结构更影响成败。常见设定有三种。第一种是二分类动作0正常动作1攻击。最简单训练最稳适合只关心“有没有问题”的监控场景。第二种是多分类按数据集标注粒度分出DoS、Probe、R2L、U2R等攻击族每个族一个动作。检出的同时能拿到攻击类型但类不平衡问题立刻变严重训练曲线也抖得厉害。第三种是特征选择型动作智能体每次从特征全集里挑一部分“看”再决定是否报警。这种设计适合解决特征多、冗余高的场景但训练复杂度高不建议作为第一个版本。包里常见的实现是二分类或四分类。四分类对应NSL-KDD里最常见的normal/DoS/Probe/R2LU2R合并因为R2L和U2R样本量极少拆太细根本训不起来。我自己做类似项目时也沿用这个取舍把样本量少的攻击族合并成一个大类先保证检出率再做家族细分。动作空间与网络输出的对应关系是DQN网络最后一层神经元个数等于动作个数输出每个动作的Q值训练时取最大Q值对应的动作作为决策。修改动作空间只需要改action_dim这一个参数以及y标签的映射关系网络结构和奖励函数都不用动这对想拿这份代码去跑自己数据的人来说很友好。2.3 奖励函数检测率、误报率与生存时长的权衡强化学习里一切都由奖励驱动所以奖励函数是整份源码里最该仔细读的地方。最朴素的写法是判别正确给1判别错误给-1。一步一奖励智能体很快能学到“跟着多数类走”——测试集里Normal占比高全部猜Normal也有很高的准确率但攻击检出率趋近于0。这就是奖励设计失衡的典型翻车现场。稍微好一点的写法是引入不对称奖励攻击被检出给1正常流量判对给0.1误报给-2漏报给-3。目的是让智能体把“漏报攻击”当成代价最大的错误因为入侵检测里漏报的代价远高于误报。# 常用不对称奖励函数直接放在训练循环里 if action label: reward 1.0 if label 1 else 0.1 else: reward -2.0 if action 1 else -3.0这段代码的思路是只有攻击类别的正确判定有高奖励正常流量的正确判定给一个小奖励维持温和信号误报和漏报都扣分但漏报扣得更狠。参数怎么调取决于业务场景——如果你的场景里误报会淹没日志系统就把误报的-2改到-5如果攻击漏掉会直接造成财产损失就把漏报的-3继续加大。还有一个细节奖励的绝对数值会影响DQN的训练稳定性。奖励值如果超过±10Q值更新很容易震荡。把这四条奖励控制在[-5, 1]区间内是我做这个方向多年踩坑换来的经验。你在自己的环境里调奖励时宁可先小幅度调也别直接从几百的惩罚开始试。3. 用Python跑通最小DRL-NIDS从数据加载到训练这一章直接给你一套可复现的最小实现。全程用深度强化学习算法里的DQNDeep Q-Network做载体因为它在NIDS这类离散动作场景下最稳、最容易调试。代码按三个小节拼起来就是一个能跑通的完整训练脚本。3.1 环境准备与数据集加载先建独立环境避免把系统Python弄脏。这一步相当于Python安装教程里的常规操作但对这种带PyTorch依赖的项目很关键。cd 项目目录 python -m venv drl-nids-env source drl-nids-env/bin/activate # Windows用 drl-nids-env\Scripts\activate pip install torch pandas numpy scikit-learn matplotlib然后加载数据集。NSL-KDD打开后是KDDTrain.txt这种格式每行41个特征加一个标签。import pandas as pd import numpy as np from sklearn.preprocessing import MinMaxScaler from sklearn.model_selection import train_test_split # NSL-KDD 的41个字段名按官方顺序排列 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] df pd.read_csv(data/KDDTrain.txt, headerNone, namescol_names) # 标签形如 normal. / neptune. / warezclient.去掉点号后统一 df[label] df[label].str.rstrip(.) # 二分类映射normal为0其余攻击族统一合并为1 attack_map {normal: 0} df[label] df[label].apply(lambda x: attack_map.get(x, 1)) # 枚举特征one-hot展开数值特征交给scaler df pd.get_dummies(df, columns[protocol_type, service, flag]) X df.drop(label, axis1).values.astype(np.float32) y df[label].values.astype(np.int64) # 关键先切分再fit scaler防止数据泄漏 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy) scaler MinMaxScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) # 复用同一个scaler不能重新fit上面这段有两个容易忽略的参数。train_test_split里的stratifyy是按标签比例分层切分保证训练集和测试集里攻击样本占比一致否则随机切分可能把攻击样本全切到一边训练直接废掉。random_state42固定切分结果保证每次跑代码拿到的训练集测试集完全一样后面调参才有可比性。3.2 DQN智能体结构三层MLP就够NIDS的特征级状态维度不高一般几十到一百多用三层MLP做Q网络足够。不用上来就堆Transformer那只会让训练变慢效果未必更好。import torch import torch.nn as nn import random from collections import deque class DQN(nn.Module): def __init__(self, state_dim, action_dim, hidden64): super().__init__() self.net nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, action_dim) # 输出每个动作的Q值 ) def forward(self, x): return self.net(x) class ReplayBuffer: def __init__(self, capacity20000): self.buf deque(maxlencapacity) def push(self, s, a, r, s2, done): self.buf.append((s, a, r, s2, done)) def sample(self, batch_size): batch random.sample(self.buf, batch_size) s_b torch.FloatTensor([e[0] for e in batch]) a_b torch.LongTensor([e[1] for e in batch]).unsqueeze(1) r_b torch.FloatTensor([e[2] for e in batch]).unsqueeze(1) s2_b torch.FloatTensor([e[3] for e in batch]) d_b torch.FloatTensor([e[4] for e in batch]).unsqueeze(1) return s_b, a_b, r_b, s2_b, d_b def __len__(self): return len(self.buf)hidden64是我在41维特征输入下的默认值。特征one-hot展开后维度会涨到一百上下64个隐藏神经元足够拟合Q值。如果你的数据集特征更多可以调到128但不要一上来就256以上过拟合会让测试集效果反而变差。ReplayBuffer用deque(maxlencapacity)实现容量满了自动丢弃最旧样本这是DQN经验回放的标准做法。3.3 训练循环经验回放、目标网络与epsilon衰减这是整份源码的核心骨架。训练循环要同时做四件事按epsilon-greedy策略选动作、算奖励、存经验、从经验里采样更新网络。我按episode组织训练每个episode从训练集随机起点连续走256步这样训练成本可控也符合“一段会话过程”的直觉。action_dim 2 # 正常/攻击 state_dim X_train.shape[1] device torch.device(cuda if torch.cuda.is_available() else cpu) dqn DQN(state_dim, action_dim, hidden64).to(device) target_net DQN(state_dim, action_dim, hidden64).to(device) target_net.load_state_dict(dqn.state_dict()) # 初始权重同步 optimizer torch.optim.Adam(dqn.parameters(), lr1e-3) loss_fn nn.MSELoss() replay ReplayBuffer(capacity20000) batch_size 64 gamma 0.99 # 折扣因子衡量未来奖励的重要程度 epsilon 1.0 # 初始探索率 epsilon_min 0.05 # 最低探索率 epsilon_decay 0.995 # 每个episode结束后衰减 step_per_episode 256 target_update_freq 10 num_episodes 300 for episode in range(num_episodes): start np.random.randint(0, len(X_train) - step_per_episode) state X_train[start] for t in range(start, start step_per_episode): # epsilon-greedy探索期随机选动作后期选当前Q值最大的动作 if np.random.rand() epsilon: action np.random.randint(action_dim) else: with torch.no_grad(): q dqn(torch.FloatTensor(state).unsqueeze(0).to(device)) action q.argmax().item() # 不对称奖励攻击判对1正常判对0.1误报-2漏报-3 label y_train[t] if action label: reward 1.0 if label 1 else 0.1 else: reward -2.0 if action 1 else -3.0 done (t start step_per_episode - 1) # 本episode最后一步视为终止 next_state X_train[t 1] if not done else state replay.push(state, action, reward, next_state, done) state next_state # 经验足够后开始训练 if len(replay) batch_size: s_b, a_b, r_b, s2_b, d_b replay.sample(batch_size) s_b, a_b, r_b, s2_b, d_b (s_b.to(device), a_b.to(device), r_b.to(device), s2_b.to(device), d_b.to(device)) q_pred dqn(s_b).gather(1, a_b) # 当前Q值 with torch.no_grad(): q_next target_net(s2_b).max(1, keepdimTrue)[0] q_target r_b (1 - d_b) * gamma * q_next # 目标Q值 loss loss_fn(q_pred, q_target) optimizer.zero_grad() loss.backward() optimizer.step() # 每个episode结束后衰减探索率定期同步目标网络 epsilon max(epsilon_min, epsilon * epsilon_decay) if episode % target_update_freq 0: target_net.load_state_dict(dqn.state_dict()) if episode % 20 0: print(fepisode {episode} | epsilon {epsilon:.3f} | loss {loss.item():.4f})这段代码里有三个设计要说明。第一目标Q值里(1 - d_b) * gamma * q_next终止状态直接拿奖励做目标不再往后看未来Q值这是DQN终止状态的标准处理。第二目标网络每10个episode同步一次目的是让训练目标不要每步都变Q值更新才稳定。如果每步都用最新网络算目标Q值容易发散成一条乱跳的曲线。第三done标志只在episode最后一步置True中间步骤next_state取下一行样本等于把数据集的连接记录当作一个时间序列在看这是简化的处理方式真要在生产环境用应该按会话ID和时间戳重组顺序而不是用数据集里的原始行序。4. 让模型训得稳的四个参调动作学习率、epsilon、回放容量与奖励尺度很多人拿到源码第一件事是跑默认参数损失能降但测试集指标一般。问题不在代码在于DQN的默认参数是为游戏环境调的入侵检测的数据分布和奖励密度都不同必须做针对性调整。这一章我把最影响结果也最容易踩坑的四个参数讲透。4.1 学习率和epsilon衰减必须配合调学习率决定Q网络参数每次更新的步长。1e-3在CartPole这类简单环境里没问题但在NIDS这种奖励稀疏、样本不平衡的场景下1e-3经常导致训练中期Q值震荡loss曲线哗哗抖。我一般先把学习率降到5e-4如果发现loss下降明显变缓再提回1e-3。epsilon衰减决定智能体从“满世界探索”到“专心利用所学策略”的速度。衰减太快模型还来不及见全攻击样本就已经进入纯利用模式很多攻击族一局都没探索到衰减太慢后期还在大量随机乱试训练不收敛。经验值是初始1.0每个episode乘以0.995300个episode后大约降到0.05这个节奏配合学习率5e-4在NSL-KDD上通常能稳定收敛。这两个参数必须配合调的原因是它们互相牵制学习率大但探索率高网络会被连续的高方差样本带偏探索率低但学习率小模型收敛速度慢训练时间翻倍还不见得好。我的习惯是固定epsilon_decay先试三个学习率画出训练曲线选抖动最小的那组再回过来微调衰减系数。4.2 经验回放容量不是越大越好经验回放的直观理解是“让模型从历史经验里反复学习”。容量越大模型越能抵抗样本间相关性但代价是旧经验在缓冲区里停留时间过长。NIDS的流量模式在变攻击样本分布也在变如果缓冲区塞满了三个月前的旧经验新出现的攻击模式会被旧经验淹没模型对当前流量形态的反应变慢。源码里ReplayBuffer默认20000这个数值对NSL-KDD这种万级样本足够。换到CICIDS2017这种百万级数据集时我建议把容量调大到50000左右但不要超过100000否则每次采样都从海量历史里挑训练速度明显变慢收益却不明显。判断标准很简单如果训练后期loss仍然波动剧烈先把容量调小而不是调大多数情况能更快稳住。4.3 奖励裁剪防止Q值膨胀DQN一个出名的问题是Q值高估。NIDS场景下特别容易发生攻击样本少奖励稀疏部分状态的Q值被网络记忆扭曲越训越高。奖励裁剪是最直接的抑制手段把每步奖励限制在固定区间防止异常大奖励把整个Q值分布带飞。实践中我坚持三条线攻击检出最高1漏报最低-3单步奖励绝对值不超过3同时把所有中间取值也压在这个范围。不要用10、-50这种惩罚那会让Q值loss在个别样本上暴涨训练被迫靠降低学习率来“硬扛”最终两头不讨好。加上奖励裁剪后如果Q值仍然膨胀检查是否忘了把目标网络每N步同步回来。4.4 特征缩放与数据切分方式最后是数据层面最容易出问题的地方。特征缩放用MinMax还是Standard对DQN影响不大但一定要在train_test_split之后再fit否则scaler在训练阶段看到了测试集的统计量属于数据泄漏测试指标会虚高。一旦你带着这个虚高指标去上线真实环境效果直接打折。数据切分方式上普通随机切分即可但需要stratifyy保证攻击样本在两边比例一致。如果要模拟真实场景的时间泛化能力就要按时间顺序切分拿前80%时间段数据训练后20%时间段数据测试。这种做法对NIDS更重要因为攻击模式会随时间变化随机切分会让测试集混入同一时期的相关样本指标好看但没有实际参考价值。这组参数建议整理成表参数默认值推荐范围调参方向learning_rate1e-33e-4 ~ 1e-3训练震荡就降低epsilon_decay0.9950.98 ~ 0.999收敛慢就调大replay容量2000010000 ~ 50000数据集大就调大batch_size6432 ~ 128训练不稳定就调大奖励区间[-3, 1]绝对值不超过5收敛差就缩小target更新频率105 ~ 20Q值不稳就调小5. DRL入侵检测为何总在测试集翻车五个常见问题与排查这一章写的是我这个方向反复踩过的坑每条都按“现象 → 原因 → 解决”的顺序展开。如果你跑这份源码发现测试集指标不对劲先对照下面五条排查一遍。5.1 训练Loss下降但F1低——智能体学到了“怎么做都输”现象训练过程loss从几百降到个位数看起来“收敛了”但测试集F1不到0.3甚至不如随机猜。原因loss下降只代表Q值预测稳定不代表策略判别能力提高。类不平衡下攻击样本占比低智能体把每条流量都判成normal也能拿很高的累积奖励所以Q网络学到了“最优策略是全部判normal”。这是DRL-NIDS最常见的黑匣子不是代码bug是目标函数和数据分布错位。解决改用recall和F1作为主指标不要盯loss奖励函数按2.3节改成不对称设计训练前对训练集做攻击样本加权采样让智能体每个episode都能遇到足够的攻击样本。5.2 类不平衡导致智能体躺平现象模型到了训练后期epsilon降到最低输出几乎全是normal攻击检出率等于零。原因攻击样本在NSL-KDD里占比不高随机探索时命中攻击样本的次数太少Q网络学到的“normal优势”越来越大最后彻底压过攻击动作的Q值。解决给每个episode的起点加类别加权——按标签分布采样起点提高攻击样本进入状态序列的概率。代码里把start np.random.randint(0, len(X_train) - step_per_episode)改成先按类别概率选类别、再在类别内随机选起点。这样每个episode至少能看到一段攻击样本序列智能体对攻击状态才会有真实的Q值估计。5.3 数据泄漏scaler的fit被测试集复用现象训练指标正常测试指标高得离谱AUC 0.99以上但一换真实环境就崩。原因对全量X做fit_transform后才切分训练集测试集scaler在训练阶段看到了测试集的统计量测试集信息提前泄露给模型。更隐蔽的另一种情况有些人先切分但误把测试集也单独fit_transform了一遍等于测试集用了两套不同尺度模型根本没法泛化。解决办法就是第3.1节的顺序先train_test_split再X_train scaler.fit_transform(X_train)最后X_test scaler.transform(X_test)。测试集scaler只做transform不做第二次fit。这是所有NIDS深度实验的底线踩一次就知道它多阴险。5.4 奖励函数让智能体走捷径现象智能体学会固定输出攻击所有流量都被报警实际是检测系统整个废掉。原因奖励函数里攻击检出给1误报只扣-1智能体很容易算出一笔账把所有样本判为攻击获得的期望奖励比认真区分高得多。奖励设计不权衡智能体必然钻空子这不是模型笨是目标函数本身有漏洞。解决把误报惩罚加大到比正常判对奖励高一个量级比如误报-2正常判对0.1。再观察策略是否还躺平如果仍然全判攻击说明漏报惩罚相对误报又偏重了继续调平衡。我推荐直接把奖励映射到F1的变化量上比拍脑袋设计多档奖励更稳。5.5 seed不固定导致训练差异巨大现象同一份代码换个机器或换一次运行测试集F1差5到10个百分点。原因DQN回放采样的随机性、PyTorch参数初始化随机性、数据集切分随机性三处随机源叠加结果差异被放大。Python的random、numpy、torch各自持有全局随机状态只固定其中一个没用。解决训练脚本开头一次性固定三处seed再固定数据集切分的random_state。import random, numpy as np, torch random.seed(42) np.random.seed(42) torch.manual_seed(42)固定seed之后同环境下可以复现同一套结果。但要注意CPU和GPU的浮点运算顺序仍有微小差异F1有1-2个点的波动属于正常范围超过5个点就要怀疑代码里还有未固定的随机源比如ReplayBuffer的random.sample有没有用全局随机状态。这个问题的玄学程度很高我见过有人调了一周参数最后发现是忘了固定numpy的seed白费功夫。6. 验证与进阶换数据集、补基线、留好复现配置训练脚本跑通之后下一步是确认这套方案是不是真的比传统方法强。只在一个数据集上出指标说明不了问题至少要做两件事换数据集验证补传统基线对比。6.1 换数据集验证泛化把训练脚本里的数据加载换成UNSW-NB15或CICIDS2017改动点集中在三处列名映射换成新数据集的字段、枚举特征的one-hot列名要重新对齐、标签映射按攻击族重新合并。其余DQN网络和训练循环完全不用动这就是特征级DRL-NIDS的迁移优势。from sklearn.metrics import classification_report # 测试集评估训练结束后单独跑 y_pred [] with torch.no_grad(): for i in range(len(X_test)): q dqn(torch.FloatTensor(X_test[i]).unsqueeze(0).to(device)) y_pred.append(q.argmax().item()) print(classification_report(y_test, y_pred, target_names[normal, attack]))如果换到UNSW-NB15后F1掉得不多说明智能体学到的是通用的攻击判别逻辑而不是记住了NSL-KDD的样本形态。这一步是对这份源码价值最直接的检验。6.2 基线对比怎么写对比实验至少要有两个基线随机森林或XGBoost以及一个简单的单步监督学习模型。随机森林在同样的训练集上很快就能出一个F1如果DRL模型连随机森林都打不过就得回头审视奖励设计或状态构建而不是继续堆训练轮数。基线代码用sklearn即可几十行搞定这里不展开但对比时注意和DRL模型共用同一个切分后的数据保证公平。6.3 留一个训练配置记录习惯我做这类项目最大的后悔药是每次训练把关键参数和结果一并落盘。用json记录learning_rate、epsilon_decay、replay容量、reward四档数值、seed、F1和recall文件名带时间戳。否则调试两周后回看结果根本不知道哪组参数跑出哪个指标所有实验全部作废。血泪经验说明模型能复现和模型效果好同样重要。现在再回头看这份基于深度强化学习开发的网络入侵检测系统Python源码你要做的不是追求某个花哨的算法变体而是把状态、动作、奖励三个闭环设计清楚再按第四、五章的调参与排查把训练稳下来。跑通它只是第一步能换数据集、补基线、留好配置你才算真正吃透了这套检测方案。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →