news 2026/10/8 7:43:18

从NSL-KDD到实时检测:入侵检测项目数据预处理与建模避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从NSL-KDD到实时检测:入侵检测项目数据预处理与建模避坑指南

简介:这是一份面向计算机专业学生与初学者的入侵检测项目完整工程,融合机器学习与深度学习两条技术路线,覆盖数据预处理、特征工程、类别不平衡处理、模型训练与评估等环节,可直接用于毕业设计、课程设计或期末大作业。压缩包采用zip格式,共31个文件,大小约26.58MB,主体为14个Python脚本,涵盖CNN、LSTM、随机森林等模型的训练与预测;5个txt与3个md说明文件帮助梳理目录与使用流程;另有2个zip数据压缩包、2个hdf5模型权重、3个csv数据集、1张结果展示图和doc参考论文。项目基于UNSW_NB15数据集,完整呈现数据归一化、标签编码、相关性分析、特征筛选与不平衡处理等关键步骤,并配有较为完整的运行说明。目前已有95人学习下载,适合需要高分范例、可运行完整代码的实战学习者。

1. 入侵检测项目源码到手,先判断能不能要:跑通只是第一步

拿到一份「python基于机器学习/深度学习实现的入侵检测项目源码+项目文档+参考论文+使用说明」,别急着pip install然后python train.py。这类项目最常见的坑不是代码报错,而是你把训练跑通、看到 99% 的准确率,就以为自己已经做完了一个入侵检测系统——换到真实流量上,立刻翻车。这个项目真正要解决的就两件事:第一,让网络流量或主机日志里的异常行为能被算法自动认出来;第二,让你有能力跟团队解释清楚,模型凭什么做判断、阈值怎么定、误报从哪来、换数据集之后哪些步骤要重来。它适合两类人:一个是拿来做课程设计或毕设的学生,照着项目文档复现、改参、写对比实验,拿分下限很高;另一个是刚接手安全数据分析的工程师,需要从这套源码里找到特征处理、窗口构造、模型选型的具体写法,再嫁接到自己的数据管道上。下面我按数据、建模、评估、部署这条线,把最容易翻车的地方逐个拆开。

2. 数据准备与预处理:先把 NSL-KDD 的坑填平再谈建模

2.1 读 NSL-KDD 前先看这四件事:文件格式、列名、标签与占比

课程设计和毕设里最常用的入侵检测数据集是 NSL-KDD。它小、标签明确、论文里的对比基线多。但很多人在第一步就没做对:拿pd.read_csv直接读,结果第一列变成了列名。NSL-KDD 的原始文件没有表头,需要按顺序给列命名。我一般会用下面这段脚本起手,先把数据读进来,顺便看一眼标签分布,这一步决定后面所有策略。

import pandas as pd # 常见做法:41 个特征 + 1 个标签列,按网络包字段顺序命名 columns = ["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("KDDTrain+.txt", header=None, names=columns) df["label"] = df["label"].str.strip().str.replace(".", "", regex=False) print(df["label"].value_counts())

这段代码的逻辑并不复杂,但有两个点容易被忽略:

  • 列名必须在读取时就给全,缺失或错位会导致后续独热编码时列数对不上,模型训练报维度错误。
  • 原始标签形如neptune.、normal.,末尾带着一个点。这里用str.strip()去掉空格、str.replace(".", "", regex=False)去掉末尾的点,否则normal和normal.会被当成两个类别。

读进来之后,先看value_counts()的分布,不要急着训练。如果发现某个攻击类别只有几十条样本,后面的 SMOTE 策略、评估指标选择都要跟着调整。这一步的输出也建议直接截图或者存成统计表,放进项目文档里,论文的“数据集描述”章节能直接用。

2.2 特征预处理脚本:数值编码、标准化与测试集只 transform 的约定

NSL-KDD 里有三列是符号类型:protocol_type(tcp/udp/icmp)、service(http/ftp/ssh 等几十种)、flag(连接状态标志)。这三列不能直接喂进 sklearn 或 PyTorch。常见做法是:决策树类的模型可以用LabelEncoder转成整数;神经网络模型建议做独热编码。但无论哪种,都必须只对训练集进行拟合,再对测试集做转换。

from sklearn.preprocessing import LabelEncoder, StandardScaler # 符号列:LabelEncoder 转成整数索引 encode_cols = ["protocol_type", "service", "flag"] for col in encode_cols: le = LabelEncoder() df[col] = le.fit_transform(df[col]) # 特征列与标签列分离 X = df.drop(columns=["label"]).values y = (df["label"] != "normal").astype(int).values # 二分类:0=正常, 1=攻击 # 划分训练集和测试集,注意 stratify from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y) # 标准化:只对训练集 fit,测试集只 transform scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test)

这里最需要记住的是scaler.fit_transform(X_train)和scaler.transform(X_test)的区分。很多复现项目把StandardScaler().fit_transform(X)放在整份数据上做,看起来测试集也被“规范”了,但这是标准的数据泄露:测试集的均值和方差已经参与到了训练特征的构造里,测试分数会虚高。真正的项目里,这条约定必须写进文档和代码注释里,因为换一个人来接着做,大概率会在这一步把管道改坏。

stratify=y也是必须的参数。入侵检测数据天然不平衡,如果不按类别分层抽样,切出来的测试集可能全是正常流量,模型评估直接失真。带上这个参数之后,训练集和测试集里正常/攻击的比例会大致保持一致。

2.3 样本不均衡怎么办:先看分布再 SMOTE,参数不能乱给

许多人在跑完value_counts()看到少数类样本很少之后,第一反应就是上 SMOTE。但入侵检测里的不均衡问题比较特殊:像neptune这种拒绝服务攻击,样本数量其实很多,真正少的是 U2R、R2L 这类提权和远程漏洞利用攻击。对这些过少的类别做 SMOTE,合成的样本很可能落在特征空间的真实模式之外,反而是把噪声喂进模型。

我的习惯是:一开始不做任何采样,先训练一个随机森林看各类别的召回率,尤其是少数类的召回率。如果少数类召回率低到不可接受,再对少数的类别做 SMOTE,而不是对所有非 normal 类别一股脑全采样。

from imblearn.over_sampling import SMOTE # 只有当少数类样本量足够时才用 SMOTE,k_neighbors 要调小 smote = SMOTE(random_state=42, k_neighbors=3, sampling_strategy="auto") X_train_res, y_train_res = smote.fit_resample(X_train, y_train) print("SMOTE 之后各类别数量:", pd.Series(y_train_res).value_counts().to_dict())

k_neighbors默认是 5,但少数类只有几十条样本时,5 个近邻里可能挤满了不同类别的样本,生成的插值样本就会“跨界”。有人直接代码复制默认参数,跑完发现模型效果反而变差,就是这个原因。sampling_strategy="auto"表示把每个类别都补齐到最多类别的数量,如果你只想提高某一个少数类,可以传入一个字典,比如sampling_strategy={1: 5000},只把攻击类别补到 5000 条,这样能减少合成样本带来的噪声。

还要注意采样和标准化的先后顺序。如果先标准化再 SMOTE,合成样本是在标准化后的空间里线性插值,还原到原空间后可能产生不合理的数值。更稳的顺序是:原始特征上做 SMOTE,再做标准化。

3. 两类检测模型怎么选:传统机器学习与深度学习的对比实现

3.1 传统机器学习路线:随机森林脚本、参数与过拟合边界

在绝大多数入侵检测项目里,随机森林是最稳的基线模型。它对表格特征、非线性关系和类别不平衡都有不错的容忍度,训练速度快,还能输出特征重要性,方便写进论文里做分析。下面这个脚本可以作为第一个跑通的模型。

from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report rf = RandomForestClassifier( n_estimators=200, max_depth=18, min_samples_leaf=2, max_features="sqrt", class_weight="balanced", random_state=42, n_jobs=-1 ) rf.fit(X_train, y_train) print(classification_report(y_test, rf.predict(X_test)))

几个参数的调整逻辑,按优先级排是这样的:

  • max_depth是第一个要调的。默认不限制深度,随机森林会逐棵长到完全拟合训练集,在入侵检测这种噪声不少的数据集上过拟合很快。常见做法是从 8 开始,按 2 的倍数向上扫,看到测试集 F1 不再涨就停。
  • min_samples_leaf设成 2 或 3,避免叶子节点只覆盖一个样本,减少异常点的记忆。
  • class_weight="balanced"让少数类的误分代价更高,这比用 SMOTE 更省事,而且不会引入合成样本。
  • max_features="sqrt"是表格数据的默认好习惯,每次分裂只随机看一部分特征,树与树之间的多样性更好。

判断模型有没有过拟合,不看训练集准确率,而是对比训练集和测试集的加权 F1。如果训练集加权 F1 在 0.99,测试集只有 0.82,先回去调max_depth和min_samples_leaf,而不是急着换模型或堆数据。这个判断方法在参考论文里也常被用来支撑“本文方法优于基线”的论述。

3.2 深度学习路线:把单条样本变成窗口序列,再喂进 LSTM

深度学习在入侵检测里的落地方式,最容易犯的错误是“拿着表格数据硬套 LSTM”。NSL-KDD 的每一条样本是一个独立的网络连接记录,样本与样本之间本来没有先后顺序,直接把每一行当序列喂给 LSTM,模型学到的只是特征的死记硬背,效果通常还打不过随机森林。常见做法是做窗口化:按时间或会话顺序,把相邻的一批样本拼接成一个窗口,让模型去学窗口内特征的变化模式。

import numpy as np def make_windows(X, y, window=32, stride=16): """把单条样本按顺序切成重叠窗口,标签取窗口最后一条样本的标签""" Xs, ys = [], [] for i in range(0, len(X) - window + 1, stride): Xs.append(X[i:i + window]) ys.append(y[i + window - 1]) return np.array(Xs), np.array(ys) X_train_w, y_train_w = make_windows(X_train, y_train, window=32, stride=16) X_test_w, y_test_w = make_windows(X_test, y_test, window=32, stride=16)

window=32表示模型每次看 32 条连续样本,stride=16表示窗口每次滑动 16 条,窗口之间有重叠。这样样本量会增加,但也引入了相邻窗口之间的相关性,后面评估时要注意这一点。窗口标签取最后一条样本的标签,含义是“根据过去 32 条连接的上下文,预测当前这条连接是否异常”,这种做法不会让未来信息泄漏进当前预测。

有了窗口数据之后,再用 PyTorch 搭一个简单的 LSTM 分类器:

import torch import torch.nn as nn class LstmDetector(nn.Module): def __init__(self, input_dim, hidden_dim=64, num_layers=1): super().__init__() self.lstm = nn.LSTM(input_size=input_dim, hidden_size=hidden_dim, num_layers=num_layers, batch_first=True) self.head = nn.Linear(hidden_dim, 2) def forward(self, x): out, _ = self.lstm(x) # 取窗口最后一个时间步的输出做分类 return self.head(out[:, -1, :])

batch_first=True让输入形状变成(batch, window, features),符合直觉。hidden_dim=64在这个量级的数据上够用,加大到 128 以上只会拖慢训练,对 F1 提升非常有限。num_layers=1是刻意为之的,两层以上的 LSTM 在这个数据规模上几乎必然过拟合。训练时建议用 AdamW,学习率设 1e-3,跑 20 个 epoch 后看测试 F1,而不是盯着 loss 曲线。

3.3 对比实验怎么设计:同一个评估脚本,三个训练种子,结果才敢写进论文

这类项目里,参考论文和项目文档最值钱的部分是“模型对比表”。很多项目对比结果写得没法看,是因为不同模型用了不同的数据切分、不同的预处理顺序,甚至不同的标签定义。对比实验的唯一原则是:除了模型本身,其他东西全部保持不变。

seeds = [42, 2024, 2025] for seed in seeds: X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=seed, stratify=y) # 随机森林、LSTM、其他任何模型,都基于同一份切分结果跑 # 预处理脚本单独封装,保证每个模型看到的是同一份训练集和测试集 # 记录每个 seed 下的 precision、recall、f1,最后输出均值 ± 标准差

三个种子分别跑完之后,报告平均值和标准差,这一组数字才经得起追问。只跑一个随机种子、又不使用stratify,测试集分布一偏,后面的结论全都不成立。项目文档里也应当把这三个种子下的结果都附上,参考论文的复现实验部分往往就是这么写的。

4. 效果验证与避坑:为什么别人论文 99%,你复现只有 85%

4.1 评估指标:准确率在入侵检测里为什么会骗人

入侵检测数据天然类别不平衡,攻击样本占比通常不高。就以 NSL-KDD 为例,一个什么都不做、把所有样本都判成正常流量的“模型”,也能拿到相当高的准确率。但这显然没有任何检测能力。所以看实验结果,第一眼一定要看classification_report输出里攻击类别的recall和f1-score。召回率代表攻击样本能被抓出来的比例,在 IDS 场景里漏报往往比误报更危险。F1 是精确率和召回率的调和平均,用来衡量模型在“别漏报”和“别乱报”之间的综合表现。如果项目文档里只给了 accuracy 没给每类的 precision/recall,这份结果基本可以判定为不可信。

4.2 数据泄露的三个重灾区:重叠样本、归一化时机、特征穿越

入侵检测项目里“效果好到不真实”的结果,多半是数据泄露造成的。我整理了三处最常见的泄露点:

  • 重叠样本。NSL-KDD 测试集和训练集里存在不少同类型甚至同源特征的样本。如果不用官方划分、而是自己train_test_split,测试集里很可能混进与训练样本高度相似的“熟人”。解决方法是使用官方提供的训练/测试文件划分,或者在按会话切分时确保同一批网络连接只出现在一个集合里。
  • 归一化时机。前面已经提过,先在全量数据上fit_transform再切分,测试集的均值和方差就泄漏到了训练过程里。正确的顺序永远是:先划分,再训练集上fit,测试集只transform。
  • 特征穿越。窗口化构造序列数据时,如果标签取的是窗口中间位置的样本,而特征包含了窗口后面的样本,就相当于让模型拿“未来”预测“过去”。正确做法是标签取窗口最后一条,或者保证特征只取自当前时刻之前的信息。

4.3 复现论文结果前先核对四个配置项

拿到一套源码或者参考论文,想复现结果,先别急着跑。往下核对这四个配置项,任何一个对不上,结果就会有明显偏差:

配置项需要核对的内容对不上会怎样
数据集版本NSL-KDD 的 20% 子集和全量集合差异很大模型指标完全不同
分类口径二分类(正常/攻击)还是多分类(按攻击类型细分)指标不可比,少数类召回率剧烈变化
采样策略与位置有没有用 SMOTE?是在切分前还是切分后采样的?采样后再切分会造成重叠样本,指标虚高
评估口径用的是 macro-F1 还是 weighted-F1?是否去掉了召回率为 0 的类别“99%” 可能只是加权后的假象

4.4 高频踩坑清单:五条现象、原因与解决办法

  • 现象:训练时准确率接近 99%,测试集只剩 72%。原因:训练集和测试集样本重叠,模型记住了训练样本而不是学到了泛化规律。解决:改用官方训练/测试划分,检查两个集合里相同特征模式的样本是否被重复切分。
  • 现象:加了 SMOTE 之后效果反而变差。原因:对标准化之后的数据做插值,合成样本脱离了真实特征分布。解决:先采样再标准化;把k_neighbors从 5 降到 3;只对真正的少数类采样,而不是所有类别无脑补齐。
  • 现象:LSTM 训练时 loss 不降,准确率一直在基线附近。原因:输入还是单条样本,没有做窗口化,模型学不到序列关系。解决:检查输入形状是否是(batch, window, features),用make_windows构造窗口数据。
  • 现象:复现论文代码时标签预测全是 0,一个攻击样本都没检出来。原因:标签列里的.后缀没有去掉,normal.和normal被分成两个类别,模型把“不带点”的标签学成了正常样本。解决:读取标签后统一strip()并去掉特殊符号,打印value_counts()确认类别数。
  • 现象:把模型部署到自己的流量数据上,报特征列数不匹配的错误。原因:新数据的协议类型或服务名出现训练集里没见过的新取值,独热编码产生了新列。解决:保存训练时的StandardScaler和编码器,对未知类别做兜底映射;统一列名后再进入标准化管道。

5. 落地成可用的检测脚本:滑动窗口与阈值告警的最小实现

5.1 把离线模型变成在线检测器:滑动窗口 + 阈值告警的最小实现

离线训练做完,项目文档也写好了,接下来是常见的进阶需求:把模型变成一个能持续接收流量特征、实时判别的检测脚本。最简做法是滑动窗口加概率阈值。从流量队列里取新到的特征行,拼到已有窗口末尾,每次只预测最新窗口的输出概率,超过阈值就告警。

threshold = 0.85 # 先跑一段正常流量,取预测分数的 95 分位作为初值 def predict_stream(model, scaler, encoder, window=64, stride=32): buffer = [] while True: rows = collect_next_rows(stride) # 从流量队列取新的一批 features = preprocess(rows, scaler, encoder) # 只 transform,不 fit buffer.extend(features) if len(buffer) < window: continue window_data = np.array(buffer[-window:]) proba = model.predict_proba(window_data.reshape(1, -1))[0][1] if proba > threshold: alert(proba, rows[-1])

threshold是最值得花时间调的东西。我见过有人拍脑袋定成 0.9,结果夜间流量一波动,告警刷屏。正确的做法是先用模型跑几天的正常流量,画出预测分数的分布,取 95 分位作为初值,再根据误报率上下调整。这个“先找分布、再定阈值”的过程,比换模型更能解决真实部署里的误报问题。

5.2 误报压不住时的兜底:先调阈值,再写规则,别急着换模型

如果告警还是压不住,不要马上换模型,先做规则兜底。我的做法是:对概率落在阈值附近(比如 0.8 到 0.95 之间)的样本,再用几条确定性规则过滤一次,比如单包负载大小是否异常、目标端口是否有对应服务响应、连接是否在一秒内反复建立。模型负责给出一个宽泛的怀疑范围,规则负责把“模棱两可”的样本快速否决掉。这套组合在误报率和漏报率之间会留出更从容的调节空间。项目文档里也应该把阈值设定过程记录清楚,说明它来自多少天、多少条流量的统计,而不是经验值。这样别人接手时,至少知道该从哪里下手调。

我最开始做这类系统时,习惯一上来就追求模型指标的极致,却忽略了阈值和规则兜底这些“脏活”。结果是在论文里指标漂亮,部署到真实环境之后每天被误报消耗注意力,最后还是回来老老实实调阈值。后来我把“先跑几天看分数分布、再定阈值、最后用规则兜底”写成了固定流程,翻车次数才明显减少。如果你也正在做入侵检测或者打算用这份源码改自己的方案,希望这个流程能帮你少走一段弯路,希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 7:43:12

CTF杂项解题exe工具链全攻略:从文件识别到内存取证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 7:43:09

基于CNN的NSL-KDD网络入侵检测实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 7:42:04

工业级电源路径保护:TPS259483与R7FA4C1BD3CFP协同设计实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 7:41:38

C# Socket网络通讯实战:上位机TCP粘包拆包与异步收发源码解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 7:41:01

Python人脸签到系统:生产级部署的5大硬指标

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 7:40:24

工业嵌入式电源路径保护设计实战:eFuse与MCU协同方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华