机器学习网络入侵检测:从数据集到99.5%正确率的完整链路 简介基于机器学习实现的网络入侵检测源码项目面向高校学生和入门开发者覆盖数据处理、模型构建到性能评估的完整流程可直接用于课程设计或期末大作业正确率可达99.5%。压缩包共十六个文件总大小为17.51MB核心为四个脚本文件配合经典入侵检测数据集压缩包、训练日志、项目配置与说明文档目录结构清楚便于按模块阅读。代码带有详细注释新手也能看懂简单部署即可运行同时支持根据实际环境调整参数或数据集进行二次改造。项目包含主程序和辅助程序展示了卷积神经网络在入侵检测中的应用涵盖数据读取、特征处理、训练测试等环节可复现实验效果并对比不同设置。已有109人学习下载适合需要快速搭建高准确率入侵检测系统或希望参考完整工程实现的学习者。1. 网络入侵检测不是玄学99.5%正确率从哪来、能信几分先泼一盆冷水在入侵检测Network Intrusion Detection这个场景下一个「99.5%正确率」的数字如果只报正确率而不报误报率这个数字基本没有工程参考价值。二分类甚至多分类的入侵检测类别极度不平衡——正常流量可能占 90% 以上模型只要无脑判「正常」就能拿到接近 90% 的正确率再调一调阈值就能把分数刷到 99%。所以拿到这份基于机器学习实现的网络入侵检测python源码项目说明正确率可达99.5%第一件事不是跑通它而是搞清楚它的 99.5% 是在哪个数据集、哪个评价指标、哪类攻击样本下算出来的。但这不代表这套源码不值得看。恰恰相反基于机器学习的入侵检测是网络安全里少有的「数据驱动、可复现、能持续迭代」的方向。它的核心路径很清晰抓流量 → 提特征 → 训练分类器 → 上线评估 → 回流再训练。Python 生态里scikit-learn、pandas、imbalanced-learn几乎把整个流程的工具链都备齐了一个人完全可以在本地跑通从原始数据集到检测模型的全流程。这篇文章就顺着标题里的关键词——机器学习、网络入侵检测、Python 源码、正确率把这条链路拆开讲数据怎么选、特征怎么做、模型怎么调、那个 99.5% 怎么验证以及哪些地方最容易翻车。适合手里已经有 Python 基础、想往安全方向落地的工程师也适合学生拿这套思路做毕设或课程设计时避开那些一看就是凑数的坑。2. 先选数据再做模型入侵检测的数据集、标签与特征工程2.1 KDD Cup 99 与 NSL-KDD为什么老数据集仍是首选网络入侵检测方向绕不开两个经典数据集KDD Cup 99 和它的清洗版 NSL-KDD。KDD Cup 99 是 1998 年林肯实验室采集的模拟军事网络流量后来被做成了带标签的 CSV一共包含 41 个特征和 4 类攻击标签DoS、Probe、R2L、U2R加一个 normal。这套数据的问题很明显训练集和测试集分布不一致、重复样本太多、部分攻击类别样本极少直接拿它跑出来的准确率可以被刷到非常夸张。NSL-KDD 是后来对原始数据的去重和重采样版本去掉了冗余记录并把每类的样本数压到合理范围是目前做毕设和论文最常用的替代品。我一般建议直接下载 NSL-KDD 的KDDTrain.txt和KDDTest.txt来用因为KDDTrain_20Percent.txt只是训练集的 20% 子集用于快速调参可以用于最终训练会浪费数据。下载地址可以从 UNB加拿大新不伦瑞克大学的官方页面拿到文件是裸的文本格式无需解压列之间用逗号分隔。# 用 pandas 读取 NSL-KDD 训练集 import pandas as pd 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 ] train_df pd.read_csv(KDDTrain.txt, headerNone, namescolumns) print(train_df.shape) # 样本数与特征数 print(train_df[label].value_counts()) # 标签分布这段代码做了两层事情。第一层是定义 41 个特征加 1 个标签列的列名因为 NSL-KDD 的原始文件没有表头不指定名字的话后续所有操作都得靠iloc按位置取列代码会非常难维护。第二层是看标签分布——这一步直接决定后面的模型策略如果攻击类别占比极低比如 R2L 和 U2R 加起来不到 1%那常规的分类器几乎不可能学出有效的识别边界。value_counts()能立刻暴露数据不平衡程度比任何「先跑模型再说」的思路都省时间。2.2 协议类型与服务名把字符串特征转成机器能理解的量NSL-KDD 的特征不是全数值的protocol_type、service、flag这三列是字符串。protocol_type只有 tcp、udp、icmp 三种service有 70 种左右flag有 11 种状态。常见做法是直接用LabelEncoder把它们编码成整数但对service这种高基数类别特征直接整数编码等于强加了一层根本不存在的序关系——tcp0、udp1、icmp2 会让模型误以为 udp 和 icmp 的「距离」比 tcp 和 udp 更近。对树模型来说这样还能忍因为树模型本身对特征值只做阈值切分但对逻辑回归或 SVM 就完全不行。更稳的做法是分两步先用LabelEncoder做基础编码保证流程能跑通然后把编码后的结果送进模型里跑一次记录性能再用one-hot重做一遍对比两次的差异。# 字符串特征编码 基础切分 from sklearn.preprocessing import LabelEncoder # 复制一份避免改动原始数据 df train_df.copy() # 把攻击标签映射成二分类1 表示攻击0 表示正常 df[label_binary] (df[label] ! normal).astype(int) # 字符串特征先做 LabelEncoder先跑通流程用 for col in [protocol_type, service, flag]: le LabelEncoder() df[col _enc] le.fit_transform(df[col]) print(df[service_enc].nunique()) # 编码后的类别数用于判断是否该走 one-hot这里有个很容易被忽略的参数LabelEncoder拟合用的数据是df[col]全量数据也就是说fit_transform用的是训练集本身的取值集合。如果后续加载KDDTest.txt测试集里可能会出现服务名在训练集里没出现过的情况——此时直接transform会抛异常处理方式是给LabelEncoder传一个classes_白名单或者干脆用pandas的factorize配合union合并类别集合。这个问题在 NSL-KDD 里实际会出现因为测试集中的service种类比训练集多属于必踩的坑。做完之后建议看一眼service_enc.nunique()的输出。如果类别数超过 50且后续模型是线性模型就直接改用pd.get_dummies做 one-hot代价是特征维度会从 41 涨到 120 左右但换来的是模型不会做出「服务 A 和 服务 B 的数值差距有意义」这种错误假设。如果用的是随机森林或 XGBoostLabelEncoder通常是够用的树模型对序关系不敏感。2.3 特征数值化之后标准化、相关性过滤与流量时序特征的选择做完编码之后数值型特征直接拼进来时有一个细节src_bytes和dst_bytes这类特征的数值范围可能从 0 到几千万而serror_rate这类比率型特征恒在 0 到 1 之间。逻辑回归和 KNN 这一类依赖距离度量的算法如果不做标准化大数值特征会直接淹没小数值特征的贡献。决策树和随机森林不受此影响但为了后面模型对比时的公平性我一般会统一先做标准化再用StandardScaler的transform应用到测试集。from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split feature_cols [ duration, src_bytes, dst_bytes, 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, protocol_type_enc, service_enc, flag_enc ] X df[feature_cols].values y df[label_binary].values X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.3, random_state42, stratifyy ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_val_scaled scaler.transform(X_val)上面这段代码里的stratifyy是专门为不平衡分类准备的。如果不加它随机切分有可能把原本就稀少的攻击样本几乎全切进测试集导致训练集类别分布进一步恶化模型彻底学不到攻击模式。加了这个参数后训练集和验证集里 normal 和 attack 的比例会保持和原始数据一致。random_state42固定随机种子是为了后面每次跑出来的结果可复现——调参时如果不固定两次实验间的性能差异会混合随机性根本没法判断是参数改的功劳还是运气。做完标准化之后可以用SelectKBest或者看随机森林的feature_importances_做一轮粗筛但这步不是必须的。对 NSL-KDD 这种 41 维特征来说特征维度不算高树模型加正则完全能自己筛只有在你准备用逻辑回归且发现收敛很慢、或者准备上深度学习网络时才需要认真做特征选择。手工筛选时重点看src_bytes、dst_bytes、count、srv_count、serror_rate这五个——它们是连接层的计数特征和协议级比率是当前流量行为最直接的数值投影也是后续任何模型里重要性排在最前的特征。3. 从训练到检测模型选型、参数设置与那份 Python 源码该有的结构3.1 为什么随机森林是入侵检测的及格线而逻辑回归是第一版好基线在入侵检测方向选模型我个人的经验是逻辑回归做第一版基线随机森林做第二版主力XGBoost 做决赛版刷分。这三个顺序能帮你把「数据问题」和「模型问题」分开排查——如果逻辑回归的准确率已经到 97%说明特征工程质量够好如果逻辑回归只有 88%先回头查特征别急着上 XGBoost 加戏。逻辑回归的优势是可解释性和稳定性。它的输出可以直接读成概率调阈值时不需要改模型权重而且 41 维特征下训练只要几秒钟。随机森林的优势是对特征尺度不敏感、能自动捕捉非线性边界且不容易过拟合到噪声特征上。在 NSL-KDD 上随机森林配合 100~300 棵树几乎不需要调参就能到 97% 以上的准确率这一点是 SVM 和朴素贝叶斯给不了的。这块源码按最常见的落地结构应该是data_loader.py读原始文件、feature_engineering.py编码与标准化、train.py训练主脚本、evaluate.py算指标与输出混淆矩阵再加一份requirements.txt和项目说明文档。# train.py 核心训练流程 from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import (accuracy_score, precision_score, recall_score, f1_score, confusion_matrix) clf RandomForestClassifier( n_estimators200, max_depth20, min_samples_leaf2, class_weightbalanced_subsample, random_state42, n_jobs-1 ) clf.fit(X_train_scaled, y_train) y_pred clf.predict(X_val_scaled) print(f准确率: {accuracy_score(y_val, y_pred):.4f}) print(f精确率: {precision_score(y_val, y_pred):.4f}) print(f召回率: {recall_score(y_val, y_pred):.4f}) print(fF1: {f1_score(y_val, y_pred):.4f})这里class_weightbalanced_subsample是给不平衡数据专门设的参数。它会在每棵树的随机抽样中按类别比例自动调整权重相当于让攻击样本的误判代价更大模型会更用力去学攻击类。如果不用这个参数模型的精确率可能很高但召回率极低——也就是它把所有正常流量都认对把攻击流量大量漏掉。n_jobs-1表示用满所有 CPU 核心如果训练时发现内存不足先降n_estimators再降max_depth。这段代码里我特意保留了「用标准化后的特征喂随机森林」的写法。严格来说随机森林不需要标准化但这样写的好处是后面你换逻辑回归或 KNN 做对比实验时不用改数据预处理环节直接换分类器实例就行。数据处理的统一性比省一次fit更重要。3.2 精确率与召回率的博弈以及那个 99.5% 的真相单独看准确率在入侵检测里没有工程意义因为攻击流量在真实世界里的占比远低于正常流量。一个把所有流量都判为正常的模型在 NSL-KDD 上准确率也能到 80% 以上。评估一份源码是不是注水关键是看它对 R2L 和 U2R 这两个少量类别的表现。KDD Cup 99 里的远程登录攻击与用户提权攻击样本极少很多模型为了刷总分几乎放弃识别它们——准确率看着很高但这两类攻击的召回率常年在 10% 以下。# 按攻击子类分组的召回率分析 from sklearn.metrics import classification_report # 这里需要把原始 multi-class 标签的测试集单独传入 y_val_full df.loc[train_val_split_idx, label] # 示意代码真实执行时按实际索引取 report classification_report(y_val_full, y_pred_multi, zero_division0) print(report) # 计算仅攻击样本上的召回率 attack_mask (y_val 1) if attack_mask.sum() 0: attack_recall recall_score(y_val[attack_mask], y_pred[attack_mask]) print(f攻击样本上的召回率: {attack_recall:.4f})zero_division0很关键——当某个类别在预测里完全没出现时metrics模块会抛 undefined 警告这个参数让它在死报时输出 0 而不是崩掉。这也是源码里常见的坑项目说明只写「准确率 99.5%」但附带的classification_report里 U2R 的 f1-score 是 0.00。所以拿到任何声称 99.5% 的项目按上面这段代码把报告打出来先看 macro avg 的 f1再看每类的 recall最后再决定这个项目值不值得往下投入。提示对入侵检测模型一个可上线的正检系统至少需要同时给出精确率和召回率两个指标且签收标准一般看「攻击类别的召回率」是否够高。准确率只适合作为对外汇报的辅助数字。3.3 多分类与二分类的选择源码里的标签体系决定了一切拿到的源码如果只做了二分类normal vs attack那它的 99.5% 更好理解二分类任务简单类间区分度大随机森林轻松可以刷到接近 99%。但这在真实部署里意义有限——安全运营人员需要知道攻击是哪一类是 DoS 还是探测这决定了下一步该封 IP、重启服务还是隔离主机。多分类的做法需要把label列从二值改回五类。NSL-KDD 的原始标签是normal、back、neptune、satan等 20 多种具体攻击名按大类映射成normal、DoS、Probe、R2L、U2R五类。# 攻击类型映射到四个大类 def map_attack_label(label): attack_groups { DoS: [back, land, neptune, pod, smurf, teardrop, apache2, udpstorm, processtable, worm], Probe: [satan, ipsweep, nmap, portsweep, mscan, saint], R2L: [guess_passwd, ftp_write, imap, phf, multihop, warezmaster, warezclient, spy, snmpgetattack, named, snmpguess, xlock, xsnoop, sendmail], U2R: [buffer_overflow, loadmodule, rootkit, perl, sqlattack, xterm, ps, httptunnel] } if label normal: return normal for attack_type, names in attack_groups.items(): if label in names: return attack_type return unknown df[label_multi] df[label].apply(map_attack_label)map_attack_label里每个攻击名都属于一个明确的大类这个映射是 KDD 数据集的标准划分不同论文略有出入但影响不大。多分类时也建议用stratify按五类标签切分且评优优先看macro avg而非weighted avg——macro 平均对每个类一视同仁不会被样本量大的 DoS 类带偏。场景上多分类的价值在于「可行动性」检测到 DoS 时可以直接上丢包限速规则检测到 R2L 时该看认证日志检测到 U2R 时必须马上查权限变更。只做二分类的模型中所有这些后续动作无法区分安全分析师拿到报警还得重新回溯流量整体效率会被拖到原地的程度。4. 避坑那些让入侵检测源码在测试集上翻车的四个陷阱4.1 特征泄露把测试集的特征值混进了训练流程现象训练时准确率 99.5%模型文件也保存了但部署到新流量上一塌糊涂连正常流量都开始误报。原因代码在切分训练集与测试集之前就做了fit_transform——也就是LabelEncoder或StandardScaler先在整个数据集上拟合再切分。这样测试集的分布信息已经偷偷进入训练阶段测试评估变成了一场开卷考试。解决把预处理对象编码器、scaler全部拆成fit与transform两步先切分再只对训练集fit_transform对测试集只transform。关于这一点源码里如果出现StandardScaler().fit_transform(X)之后才做train_test_split的代码基本都是这个问题。4.2 不平衡样本下的正确率虚高被 DoS 类撑起的总分现象项目说明吹「整体正确率 99.5%」但你一看混淆矩阵R2L 和 U2R 的召回率低到等于没识别U2R 直接被分类器无视。原因攻击类别之间样本量差异极其悬殊。NSL-KDD 训练集里 DoS 类有 45000 条样本U2R 只有 50 条左右。模型只要对 DoS 学得好全局准确率就能到 99%代价是对稀有类完全放弃。解决训练阶段用class_weight或对少数类做SMOTE过采样。不要只看准确率核心指标看每类召回率。真实场景里 U2R 哪怕一条被漏可能意味着一个内网权限已经丢了一周这种代价不是准确率能衡量的。注意imbalanced-learn库的 SMOTE 只支持纯数值输入用之前必须完成所有特征编码和标准化且应在训练集上拟合、只对训练集生成合成样本测试集永远保持原始分布。4.3 对训练集做随机欠采样时把时间顺序弄丢现象测试集结果勉强正常但模拟在线检测时报警的节奏和真实流量完全不匹配误报在某个时间窗口内扎堆。原因入侵检测的流量特征中像count和srv_count这类特征本身就携带「最近 N 条连接内有多少次相同目标」的时序信息。如果做欠采样时直接random_state42随机丢样本这些特征的时间上下文被破坏模型学到的是被随机重排后的流量统计规律。解决办法如果一定要降采样按标签分组后在组内做「前 N 条保留其余随机丢弃」的顺序采样或者干脆用TimeSeriesSplit代替普通 K 折交叉验证。在线评估时严格按流量到达的时间顺序切训练集与测试集不要洗牌。4.4 数据集不匹配拿 KDD Cup 99 训练的模型去测 CICIDS 的流量现象项目说明里的源码在作者环境上跑出 99.5%你拉下来在自己的机器上换了个数据集立刻掉到 80%然后开始怀疑人生。原因KDD Cup 99 / NSL-KDD 的特征是基于早期网络的协议统计信息它和现代恶意流量的形态差别极大。现代数据集如 CICIDS 2017 包含更多 TLS 加密流量特征、包间隔特征、流量方向统计特征维度从 41 变成 80 多。旧的模型在这些数据上基本无法迁移。解决方案是明确源码只服务于「教学演示」和「方法论验证」实际部署时必须在自己的流量镜像上重新做特征工程与模型训练。把数据集的适配层写好保证换数据时只需要改data_loader.py和feature_engineering.py——这样才叫人能在新数据上跑起来。5. 把 99.5% 变成可交付的检测能力三个验证技巧和一条落地路径验证一份源码是不是能真正落地比跑通它重要得多。这里有三个我常用的技巧对任何基于机器学习实现的网络入侵检测 python 源码都适用。技巧一是画 PR 曲线而非只看 ROC 曲线。ROC 在类别极端不平衡时会有「假阳性率被大量负样本稀释」的乐观偏置PR 曲线直接反映精确率与召回率的权衡适合入侵检测。做法是用predict_proba输出概率配合precision_recall_curve扫一遍阈值找到「精确率下降不明显、召回率快速上升」的拐点把那个位置对应的阈值写进 config。注意阈值也属于模型参数要保存下来不能只在训练脚本里出现。from sklearn.metrics import precision_recall_curve import numpy as np prob_pos clf.predict_proba(X_val_scaled)[:, 1] precisions, recalls, thresholds precision_recall_curve(y_val, prob_pos) # 找精确率和召回率差值最小的阈值 dist np.abs(precisions[:-1] - recalls[:-1]) best_idx np.argmin(dist) best_threshold thresholds[best_idx] print(f推荐阈值: {best_threshold:.4f}此点精确率{precisions[best_idx]:.4f}召回率{recalls[best_idx]:.4f})技巧二是在训练脚本里留一个--evaluate-only开关方便加载已保存的模型做增量评估。常见做法是训练完成后用joblib.dump保存模型与标准化器再写一个单独的评估脚本加载它们。这个习惯能让你在拿到新流量数据时不用重训就能快速看到旧模型的跨时间表现——这是安全场景里最重要的维度。技巧三是把源码跑出的模型导出成通用格式如 ONNX 或 PMML再接一个简单的规则引擎。常见的做法是当模型输出的攻击概率超过阈值时就触发告警而在告警聚合层加「同源地址 5 分钟告警超过 20 次再升级」的规则削峰。纯 ML 的入侵检测误报率在真实环境里通常有 1% 到 5%没有规则层配合安全运营人员会在第二天就关掉这个系统。这一步是让整套系统从一个「论文分数」变成「真正有人用的工具」的最后工序。我自己在本地复现这类项目时每次拿到源码都会先做一个动作把训练脚本里的random_state固定下来然后重新跑一遍看那个声称的 99.5% 是否能稳定复现到小数点后两位。接着把confusion_matrix打印出来看具体是哪个类别在撑分数。这个习惯帮我看穿了不少包装精美的项目——有几份源码只在某一个特定随机种子下跑出高分换一个种子直接掉两个点。最后补一句我在整条落地路径上最深的体会入侵检测这个方向数据的质量决定了模型性能的上限而特征工程决定了下限——模型只是把这两件事的成果兑现出来。不要在数据没洗干净之前就去调参。希望帮到你。本文还有配套的精品资源点击获取