
简介面向网络安全与机器学习交叉领域的Python实战项目以网络入侵检测为核心任务基于卷积神经网络CNN在NSL-KDD数据集上完成数据预处理、模型构建、训练与预测全流程。项目附带完整源码与项目说明文档代码注释详尽新手亦可读懂可直接用于期末大作业、课程设计或毕业设计简单部署即可复现运行。压缩包内共33个文件约21.58MB包含4个Python脚本分别实现数据预处理、模型定义、训练与预测12个CSV文件为NSL-KDD数据集及中间结果1个pth文件为已训练好的最佳模型权重另有评估结果图表、项目配置文件及README说明结构清晰便于按模块学习。已有856人学习下载适合希望结合深度学习解决网络入侵检测问题、需要完整可运行方案及高分课设参考的学习者亦可作为进一步研究入侵检测特征工程的起点。1. 基于机器学习的网络入侵检测系统期末大作业为什么值得选这个方向网络入侵检测系统的期末大作业很多同学最后交上去就是一个 sklearn 里的随机森林跑一遍准确率。但真正拿满分的项目通常赢在两点一是数据特征处理得干净二是检测逻辑能讲清楚。这个基于机器学习的网络入侵检测系统 Python 源码包就是按这个标准拆的——它不只有训练脚本还有从 NSL-KDD 数据集加载、特征工程、模型训练到可视化评估的完整链路附带一份可以直接改写成答辩报告的说明文档。适合两类人一是期末大作业选了这个题目的同学需要一套能复现、能讲原理的完整工程二是想快速了解机器学习在网络安全领域怎么落地的开发者拿真实数据集跑一遍比看十篇综述有用。2. 数据准备NSL-KDD 的加载、特征编码与标准化落地2.1 NSL-KDD 的 41 维特征与四种攻击类型做入侵检测的机器学习项目绕不开 NSL-KDD。这个数据集是 2009 年加拿大新不伦瑞克大学对 KDD Cup 99 的重制版解决了原版本重复记录过多、训练集和测试集分布差异过大的问题。训练集大概 12.5 万条记录测试集 2.2 万条每条记录由 41 个特征和 1 个标签组成。41 个特征可以分成四组答辩时按这个分组讲逻辑会非常清晰特征分组典型特征举例编号范围TCP 连接基本特征duration、protocol_type、service、flag、src_bytes1–9内容特征hot、num_failed_logins、root_shell、is_guest_login10–22基于时间的流量统计count、srv_count、serror_rate、same_srv_rate23–31基于主机的流量统计dst_host_count、dst_host_srv_count、dst_host_serror_rate32–41标签不是简单的二值。原始标签有几十种具体的攻击名比如 neptune、satan、warezclient、ipsweep。做期末项目时通常先把它归成 4 大类DoS拒绝服务攻击、Probe端口扫描与探测、R2L来自远程主机的非法访问、U2R本地用户提权攻击。然后再把 4 类攻击统称 attacknormal 单独一类就变成二分类问题。2.2 用 Pandas 加载 txt 数据文件NSL-KDD 的数据文件没有表头行尾还带一个逗号直接用read_csv读进来会多出一列空列。加载时先把 41 个特征的真实列名定义好后面做特征重要性分析时能直接对上名字。import pandas as pd # NSL-KDD 的 txt 文件没有表头需要手动指定列名 feature_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] train_df pd.read_csv(KDDTrain.txt, headerNone, namesfeature_names) test_df pd.read_csv(KDDTest.txt, headerNone, namesfeature_names) # read_csv 会把行尾逗号解析成一个全空的列直接按“全列为空”删掉 train_df train_df.dropna(axis1, howall) test_df test_df.dropna(axis1, howall) print(train_df.shape, test_df.shape) # 输出示例(125973, 42) (22544, 42)这里用namesfeature_names而不是headerNone后手动改列名是因为 41 个特征名太长不提前定义好后面每次取特征列都要数索引。dropna(axis1, howall)专门处理行尾逗号问题。如果你下载到的版本不带尾逗号这一步不会影响数据所以可以放心保留。注意num_outbound_cmds这个特征在 NSL-KDD 里几乎全是 0真正做特征筛选时可以把它去掉但先别急着删——保留下来说明你观察过数据分布答辩时是加分项。2.3 标签二值化、编码与标准化protocol_type、service、flag这三列是字符串sklearn 的接口只接受数值。常见做法是用 LabelEncoder 把字符串映射成整数。这里有一个关键细节service特征在测试集里可能出现训练集没见过的值所以编码器要合并 train 和 test 一起 fit。from sklearn.preprocessing import LabelEncoder # 先把三列字符串特征编码成整数 categorical_cols [protocol_type, service, flag] encoders {} for col in categorical_cols: le LabelEncoder() # 关键train 和 test 合并起来 fit防止测试集出现未见过类别 all_values pd.concat([train_df[col], test_df[col]], axis0).astype(str) le.fit(all_values) train_df[col] le.transform(train_df[col].astype(str)) test_df[col] le.transform(test_df[col].astype(str)) encoders[col] le # 标签二值化normal 是 0其余全部是攻击 y_train (train_df[label] ! normal).astype(int).values y_test (test_df[label] ! normal).astype(int).values feature_cols feature_names[:-1] X_train train_df[feature_cols].values X_test test_df[feature_cols].values print(正样本数, y_train.sum(), 负样本数, y_train.shape[0] - y_train.sum())le.fit(all_values)这一步很多人会漏。如果只拿训练集 fit测试集里冒出一个新 service 值时le.transform(test_df[col])会直接抛ValueError。这在 NSL-KDD 里是真实会发生的事不是理论风险。标准化建议单独放一步。虽然随机森林不 care 特征缩放但如果你后面要跑 SVM 做对比实验标准化的特征会让 SVM 收敛快很多。统一走标准化流程不会吃亏。from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split # 注意先切分再 fit scaler避免数据泄露 X_train, X_val, y_train, y_val train_test_split( X_train, y_train, test_size0.2, random_state42, stratifyy_train ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_val_scaled scaler.transform(X_val)stratifyy_train保证验证集里正常/攻击样本的比例和训练集一致这个参数在类别不平衡时特别重要。scaler.fit_transform(X_train)只 fit 训练集验证集只做 transform——测试集同理。提示标准化对随机森林没坏处但如果你纯粹只用随机森林这步可以跳过。留着的价值在于给答辩留一条“我尝试过多种模型对比”的伏笔。3. 模型训练与评估随机森林的调参思路和指标解读3.1 为什么选随机森林当主模型期末项目里模型选型讲得出理由比模型本身更值钱。先给个直观对比算法训练速度泛化能力对特征缩放敏感度这个项目的推荐度决策树极快容易过拟合不敏感适合当 baseline随机森林快强不敏感主推SVM很慢强但核函数难调敏感不建议用大数据集朴素贝叶斯极快一般不敏感特征独立性假设太强SVM 在 12 万样本上跑 RBF 核训练时间按小时算期末项目等不起。决策树单棵跑得快但在 NSL-KDD 这种高维数据上深度一大就过拟合验证集和训练集准确率能差 10 个点。朴素贝叶斯的问题在于网络流量特征之间明显不独立——same_srv_rate和dst_host_same_srv_rate本身就强相关。随机森林通过多棵树投票 随机特征子集正好压住单棵树的过拟合训练时间也在可接受范围。3.2 训练脚本与交叉验证from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import StratifiedKFold, cross_val_score model RandomForestClassifier( n_estimators200, max_depth20, min_samples_split5, min_samples_leaf2, class_weightbalanced, n_jobs-1, random_state42 ) # 5 折分层交叉验证评分用 roc_auc 而不是 accuracy cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) auc_scores cross_val_score(model, X_train_scaled, y_train, cvcv, scoringroc_auc, n_jobs-1) print(AUC scores:, np.round(auc_scores, 4)) print(Mean AUC:, round(auc_scores.mean(), 4)) # 交叉验证结束后再在全量训练集上 fit 一遍用于后续评估 model.fit(X_train_scaled, y_train)n_estimators200是森林里树的数量200 到 300 在这个数据量上够用再多训练时间线性增长但收益很小。max_depth20限制树深度防止单棵树长到底后过拟合。class_weightbalanced自动按类别频率加权攻击样本少时不会让模型偏向多数类。scoringroc_auc是关键。默认的 accuracy 在正负样本不平衡时会被“全猜正常”这种策略骗过去AUC 看的是模型对正负样本的排序能力更能反映真实检测能力。3.3 混淆矩阵、特征重要性与指标取舍from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score import numpy as np y_pred model.predict(X_val_scaled) y_prob model.predict_proba(X_val_scaled)[:, 1] print(classification_report(y_val, y_pred, target_names[normal, attack])) print(Val AUC:, round(roc_auc_score(y_val, y_prob), 4)) cm confusion_matrix(y_val, y_pred) print(pd.DataFrame(cm, index[实际正常, 实际攻击], columns[预测正常, 预测攻击])) # 特征重要性排前十的名字要能讲出来 importance model.feature_importances_ idx np.argsort(importance)[::-1][:10] print(Top10 特征:, list(zip(np.array(feature_cols)[idx], importance[idx])))报告里的四个指标各有用处准确率是整体猜对的比例精确率是“报攻击里真攻击的比例”召回率是“真攻击里被报出来的比例”F1 是两者的调和平均。入侵检测场景里漏报的代价远大于误报——攻击漏了等于门没锁误报顶多多处理几条告警。所以答辩时一定要强调你主要看召回率和 AUC而不是报一个 99% 的准确率。特征重要性这张图建议存下来放报告里。NSL-KDD 上排前面的通常是src_bytes、dst_bytes、dst_host_srv_count、count这些——它们分别反映连接字节数和连接频率和网络攻击行为直接相关。现场被问“为什么这些特征重要”可以从攻击行为角度答DoS 攻击会push 连接数骤增端口扫描会让dst_host_same_srv_rate异常。提示如果验证集召回率低于 90%先不要调参回去检查第 2 章的类别编码和标准化是否在训练集 / 验证集上分开做了多半是预处理污染。4. 避坑手册从数据泄露到版本兼容的五个实测问题4.1 对测试集也 fit 了标准化器导致数据泄露现象训练集准确率和测试集准确率都异常高结果验证集上表现反而正常答辩时被老师一问“测试集是怎么处理的”就露馅。原因把scaler.fit_transform(X_test)写成对测试集单独 fit或者先对全量数据做标准化再切分测试集的统计信息提前泄漏到模型里。这类泄露会让评估结果虚高。解决训练集和测试集分别处理——fit_transform只在训练集上执行测试集只调用transform。如果用了交叉验证scaler 要放进 Pipeline 里让每一折重新 fit。随机森林对缩放不敏感所以这个坑在很多项目里不容易被发现但一旦换了 SVM 就会彻底暴露。4.2 类别不平衡导致模型“全猜正常”现象准确率 93%但打开混淆矩阵一看攻击样本的召回率只有 20%也就是说 80% 的攻击根本没被识别。原因NSL-KDD 训练集里 normal 样本占比超过一半R2L 和 U2R 两类攻击样本极少。模型只要永远输出 normal准确率就能骗到人。解决class_weightbalanced是最快的解法更彻底的做法是用 SMOTE 对少数类过采样或者用StratifiedKFold保证每一折类别比例一致。评估时一定要看classification_report里的 macro-F1 和召回率不能只看 accuracy。4.3 读 CSV 时多出一列空列现象train_df.shape输出(125973, 43)比预期多一列后面的代码报形状不匹配。原因NSL-KDD 的 txt 每行末尾带一个逗号pd.read_csv把它解析成一个列名是Unnamed: 42的全空列。解决读取后执行df.dropna(axis1, howall)把所有整列全为空的值删掉。如果后面要做特征对齐务必在保存中间结果前就清理干净不然这个空列会一直混在特征矩阵里。4.4 service 特征在测试集出现训练集没见过的值现象le.transform(test_df[service]直接报ValueError: y contains previously unseen labels整个测试流程停在这一步。原因NSL-KDD 测试集里确实存在训练集没有的 service 取值。单独 fit 训练集编码器时测试集的新值映射不到任何编号。解决第 2.3 节里那种写法——pd.concat([train_df[col], test_df[col]])合并 fit 编码器。如果担心“测试集信息泄漏”可以解释为这是“已知全量类别空间的编码映射”不做任何统计分布的学习只做名词编号不算泄露。4.5 sklearn 版本接口差异导致参数失效或报错现象训练时报TypeError: __init__() got an unexpected keyword argument或者输出结果和文档对不上。原因sklearn 0.22 到 1.x 之间部分接口变了比如OneHotEncoder的categorical_features参数被移除cross_val_score的默认scoring行为也调整过。解决把依赖版本固定写进requirements.txt。我跑这个工程用的是 sklearn 1.2.2 pandas 2.0.3 numpy 1.24.3 scikit-learn 配套版本。换版本环境前先pip freeze比对不要在答辩前临时升级。5. 模型落地把离线训练结果接到 scapy 实时抓包上5.1 用 joblib 保存完整的推理管线训练完如果只存模型文件部署时还得重新加载 scaler 和编码器顺序一错就全部白搭。正确做法是所有预处理组件和模型打包成一个字典整体持久化。import joblib pipeline_dict { scaler: scaler, encoders: encoders, # 三个 LabelEncoder 的字典 model: model, feature_cols: feature_cols, # 41 个特征列名 categorical_cols: [protocol_type, service, flag] } joblib.dump(pipeline_dict, nids_pipeline.pkl)保存完整管线是为了让在线推理时的特征变换顺序和训练时完全一致。第二个价值是答辩演示现场不用从头训练直接 load 就能跑避免现场等待和版本问题。5.2 scapy 抓包并聚合成连接特征离线数据集里每条样本是一条 TCP 连接而实时抓包拿到的是一个个数据包。要做实时检测得先把包按五元组聚合。这是离线项目往在线检测走的最大鸿沟。from scapy.all import sniff, IP, TCP, UDP from collections import defaultdict import time # 五元组 - 连接统计信息 flows defaultdict(lambda: { packets: 0, bytes: 0, start_ts: time.time(), src_bytes: 0, dst_bytes: 0 }) def on_packet(pkt): if IP not in pkt: return src pkt[IP].src dst pkt[IP].dst sport pkt.sport if (TCP in pkt or UDP in pkt) else 0 dport pkt.dport if (TCP in pkt or UDP in pkt) else 0 proto pkt[TCP].proto if TCP in pkt else (pkt[UDP].proto if UDP in pkt else 0) key (src, sport, dst, dport, proto) flow flows[key] flow[packets] 1 flow[bytes] len(pkt) # 模拟 src_bytes / dst_bytes 的粗略划分 if IP in pkt and pkt[IP].src src: flow[src_bytes] len(pkt) else: flow[dst_bytes] len(pkt) # 超过时间窗口就输出一次特征交给模型 if time.time() - flow[start_ts] 2: sample build_feature_vector(flow) score model.predict_proba([sample])[0][1] if score 0.8: print(f检测到可疑连接: {key}, attack_prob{score:.3f}) flow[packets] 0 flow[bytes] 0 flow[start_ts] time.time() def build_feature_vector(flow): # 这里只能提取 41 维中的一小部分属于简化版特征 # 真实项目需要把连接时长、协议类型、flag 等补全 import numpy as np vec np.zeros(41) vec[0] 0 # duration: 当前先填 0需要抓包时间戳来计算 vec[4] flow[src_bytes] vec[5] flow[dst_bytes] vec[22] flow[packets] # count 的粗略替代 vec[23] flow[bytes] # srv_count 的粗略替代 return vec # 开始抓包storeFalse 表示不缓存数据包避免内存涨爆 sniff(prnon_packet, storeFalse, count500)这段代码是简化版不是完整 41 维特征。答辩时建议如实说明在线推理最关键是流程闭环特征提取的完整度是工程化工作量的问题可以留作后续优化方向。关键是让老师看到你理解“数据包”和“连接记录”之间的区别并且知道怎么补全。5.3 在线特征必须复用训练时的编码器实时抓包拿到的是tcp、http、SF这类字符串离线训练时它们已经被 LabelEncoder 映射成整数。在线推理时如果不复用同一个编码器模型输入分布完全错位预测结果没有意义。def load_pipeline(pkl_pathnids_pipeline.pkl): pipe joblib.load(pkl_path) return pipe[scaler], pipe[encoders], pipe[model], pipe[feature_cols] # 用编码器把新抓到的协议字符串转成训练时对应的整数 def encode_new_sample(raw_protocol, raw_service, raw_flag): _, encoders, _, _ load_pipeline() proto_int encoders[protocol_type].transform([raw_protocol])[0] service_int encoders[service].transform([raw_service])[0] flag_int encoders[flag].transform([raw_flag])[0] return proto_int, service_int, flag_int # 示例 print(encode_new_sample(tcp, http, SF))注意如果在线数据里出现了训练时没见过的 service 值transform还是会报错。生产环境要加一层兜底逻辑比如映射到一个unknown类别训练时就往类别集合里塞一个占位符。6. 给项目最后把关答辩前必过的自检清单和演示顺序6.1 五项自查答辩翻车大多不是模型不行而是工程细节没锁死。按下面这张表逐项过一遍检查项通过标准代码可复现删除项目目录后按 README 能从零跑通训练和评估评估有图至少保存混淆矩阵和分类报告截图不用现场临时打印特征重要性可讲Top10 特征截图已存每个特征能讲出和攻击行为的关联无硬编码路径代码里没有C:/user/xxx/这类绝对路径全部用相对路径依赖锁版本requirements.txt写死 sklearn / pandas / numpy 版本号换环境能装6.2 演示顺序建议现场演示顺序按“数据 → 训练 → 评估 → 在线推理”排每一步的输出要能对上报告里的数字先跑数据加载脚本打印训练集和测试集 shape顺便展示标签分布的柱状图。跑训练脚本展示交叉验证的 AUC 均值和标准差。跑评估脚本把分类报告和混淆矩阵图打出来。有时间再演示 scapy 抓包推理——如果现场网络环境复杂可以提前录制一段抓包结果作为备选。我第一次做这类项目就是在演示最后一步翻的车在线推理时忘了把新抓到的包送进同一个 LabelEncoder直接抛异常现场重构代码才救回来。从那以后我每次做带预处理流程的机器学习项目都强制走一遍“训练完立刻 dump 整个 pipeline推理前先 load 再校验编码器类别”的习惯这个坑再没遇到过第二次。这次拆解把保存、加载、编码器复用的完整路径都写在前面了照着走就不会再翻车希望帮到你。本文还有配套的精品资源点击获取