
简介面向计算机相关专业学生这份基于KNN算法的入侵检测系统源码是一套适合期末大作业、课程设计与毕业设计的完整项目。系统结合PyTorch与KNN算法实现入侵检测流程涵盖数据预处理、训练评估与结果展示等环节源码经由导师指导并在评审中获得99分代码完整、可直接运行即使是初学者也能自行部署。资源包共58个文件总体积约15.18MB。核心包含Java源码、HTML页面与PNG图片等前端展示内容同时提供XML、Properties、YAML等配置文件以及KDD CUP 10%数据集的多种预处理版本和运行脚本。项目采用Maven工程结构源码、测试、配置与部分说明分区存放目录层次清楚便于按模块阅读和二次修改。当前已有53人学习下载适合需要获取完整项目方案、梳理入侵检测系统设计思路或进行项目实战练习的人群。除可运行代码外还附带有数据清洗后的处理文件、前端页面以及构建运行所需配置能够帮助使用者较快地理解从数据输入到检测结果输出的完整流程也可作为进一步扩展的起始工程。1. 为什么期末大作业选 PyTorch 写 KNN 入侵检测很多人在做安全方向的期末大作业时第一反应是上 CNN 或 LSTM但 KNNK Nearest Neighbors在入侵检测里从没被淘汰。攻击流量在特征空间里往往有明显的聚团性一个样本被哪些邻居包围比一个复杂的决策边界更能直接说明问题。这个项目用 PyTorch 来实现 KNN而不是直接调 sklearn核心目的是把距离计算、张量索引、多数投票这一整条链路都放在自己手里课程答辩时每一步都能讲出底层原理。选 PyTorch 还有一个现实原因49 万条 KDD Cup 99 记录展开成距离矩阵后显存占用和计算效率会立刻暴露问题这正好是理解向量化优化的最佳教材。项目里的kddcup.data_10_percent_corrected_NOString_NOString是已经去掉字符串标签的数值化版本适合直接做张量运算。这套代码适合正在做课设、需要交完整实验报告、以及想搞明白 KNN 在安全数据上怎么落地的人新手能跟着跑通老手也能在距离矩阵实现里看到可优化的细节。2. KDD Cup 99 数据集清洗与特征数值化2.1 原始数据里哪些列要处理KDD Cup 99 原始数据集的每条记录包含 41 个特征和 1 个标签其中前 9 个特征里有 3 个是符号型分别是协议类型protocol_type、服务类型service、连接状态标志flag。符号型特征不能直接参与欧氏距离计算所以项目里用的文件是NOString版本也就是已经把这些列映射成数字后的结果标签也从normal、neptune这类字符串变成了数值通常 0 表示正常非 0 表示攻击。除了字符串映射原始数据里还有几个特征的值域差异特别大例如src_bytes可能是 0也可能是上百万而count这类特征只在 0~511 之间。如果不做标准化KNN 的距离计算会被大数值特征完全主导最终结果几乎只看源字节数这一个字段。因此拿到NOString文件后第一件事不是训练而是逐列检查最大值、最小值和缺失值。head -5 kddcup.data_10_percent_corrected_NOString_NOString.txt awk -F, {print NF} kddcup.data_10_percent_corrected_NOString_NOString.txt | sort -u第一行能看到每列是不是纯数字第二行确认列数是否为 4241 个特征加 1 个标签。如果某个字段出现非数值内容需要在进入 PyTorch 之前就改掉。这里不能直接读进 torch 再处理因为torch.load遇到字符串会直接报错数据类型的清洗应该放在numpy阶段完成。项目里的test.txt_marked_corrected这类文件是已经标记过的测试集通常用于最后验证模型泛化能力不要拿它参与训练。工程里还包含mark_marked和aa.txt_marked这看起来是作者做数据标注时留下的临时输出实际使用时可以忽略。2.2 从文本到张量的预处理脚本先读取 CSV然后分离特征和标签最后做标准化。下面这段代码可以直接替换成preprocess.py在训练之前执行import pandas as pd import numpy as np import torch DATA_PATH kddcup.data_10_percent_corrected_NOString_NOString.txt def load_and_preprocess(pathDATA_PATH, test_size0.2): df pd.read_csv(path, headerNone, sep,) X df.iloc[:, :-1].values.astype(np.float32) y df.iloc[:, -1].values.astype(np.int64) # 手动切分避免随机化带来的标签泄漏争议 split int(len(df) * (1 - test_size)) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] # 标准化用训练集的均值方差测试集只做转换 mean X_train.mean(axis0, keepdimsTrue) std X_train.std(axis0, keepdimsTrue) std[std 0] 1.0 # 防止常量特征除零 X_train (X_train - mean) / std X_test (X_test - mean) / std return (torch.from_numpy(X_train), torch.from_numpy(y_train), torch.from_numpy(X_test), torch.from_numpy(y_test))这里有几个关键决策切分时没有打乱数据因为 KDD Cup 99 原始记录本身就是按时间顺序排列的前面是早期攻击类型后面是后期攻击类型保持顺序可以让测试集更接近现实中的时间分布标准化参数只从训练集计算测试集沿用训练集的mean和std这是最容易出错的地方。如果先对整个数据集做标准化再切分等于让模型提前看到了测试集的分布信息实验报告里写出的指标会虚高。常量特征的标准差为 0不处理的话除零之后会得到 NaN所以必须手动替换成 1.0。astype(np.float32)也很重要。原始文件按双精度读入时内存占用会翻倍49 万行 41 列的数据不算小float32 在 PyTorch 里正好对应默认的torch.float32后续张量运算不需要额外转换。3. PyTorch 向量化实现 KNN 分类器3.1 欧氏距离矩阵的显式展开KNN 的核心是求每个测试样本与全部训练样本之间的距离。朴素写法是两层 for 循环测试集 5 万个样本、训练集 40 万个样本循环次数是 200 亿在课程设计展示现场能跑到你怀疑人生。正确做法是直接用张量广播展开成距离矩阵。对两个形状分别为(M, D)和(N, D)的矩阵欧氏距离的平方可以展开成sum(|x-y|^2) sum(x^2) sum(y^2) - 2*x^T y。用 PyTorch 写出来就是def pairwise_distances(x, y): # x: (M, D), y: (N, D) # 返回 (M, N)dist[i][j] 是 x[i] 到 y[j] 的平方距离 x_norm (x ** 2).sum(dim1, keepdimTrue) # (M, 1) y_norm (y ** 2).sum(dim1, keepdimTrue) # (N, 1) xy torch.mm(x, y.t()) # (M, N) dist_sq x_norm y_norm.t() - 2 * xy dist_sq torch.clamp(dist_sq, min0) # 浮点误差可能产生极小负数 return dist_sq这里没有直接开平方因为k个最近邻居的排序结果在平方距离下完全一致开平方只会增加计算量。keepdimTrue保留了维度让x_norm能跟(M, N)矩阵直接广播相加clamp的作用是避免浮点误差导致负数的平方根。如果后续要换用曼哈顿距离或余弦距离只需要替换这个函数KNN 的主体结构不需要改。在实际运行时会发现(M, N)距离矩阵的体积是 M 乘以 N。以 40 万训练样本和 5 万测试样本为例float32 距离矩阵占用的内存是400000 * 50000 * 4 / 1024 / 1024 / 1024 ≈ 74.5 GB这是个人电脑无论如何都扛不住的。所以工程代码里一般不会一次把全部样本放进距离矩阵而是把测试集分块每块 1000 或 2000 条循环完成预测。这个分块大小是内存和速度的平衡点。3.2 Top-k 索引与多数投票拿到距离矩阵后对每一行取最小的 k 个距离对应的训练集索引然后看这些索引对应的标签用torch.bincount做多数投票。def knn_predict_block(x_block, X_train, y_train, k5): dist_sq pairwise_distances(x_block, X_train) _, topk_idx torch.topk(dist_sq, kk, dim1, largestFalse) topk_labels y_train[topk_idx] # (block_size, k) preds torch.zeros(x_block.size(0), dtypetorch.int64) for i in range(x_block.size(0)): counts torch.bincount(topk_labels[i]) preds[i] torch.argmax(counts) return preds def knn_predict(X_test, X_train, y_train, k5, chunk_size1000): preds [] for start in range(0, X_test.size(0), chunk_size): x_block X_test[start:start chunk_size] preds.append(knn_predict_block(x_block, X_train, y_train, kk)) return torch.cat(preds)torch.topk用的是快速选择算法不需要对整个距离矩阵做全量排序复杂度远低于sort。我这里用了一个 Python 循环来对每行做 bincount老实说这不是最高效的写法但胜在直观答辩时讲起来很清楚。如果你在意速度可以对topk_labels做 one-hot 展开后用矩阵乘法一次性完成投票def knn_predict_block_fast(x_block, X_train, y_train, k5, num_classes2): dist_sq pairwise_distances(x_block, X_train) _, topk_idx torch.topk(dist_sq, kk, dim1, largestFalse) topk_labels y_train[topk_idx] one_hot torch.nn.functional.one_hot(topk_labels, num_classes) votes one_hot.sum(dim1) return torch.argmax(votes, dim1)这种写法把循环里的 bincount 变成了张量求和GPU 上能快 20 倍左右。num_classes在二分类问题里是 2如果 KDD 原始标签已经映射成了 20 多种攻击类型就要改成对应类别数。3.3 基于张量操作的完整类封装为了跟 sklearn 风格保持一致方便后续做网格搜索我一般会把 KNN 包装成一个类class TorchKNN: def __init__(self, k5, chunk_size1000): self.k k self.chunk_size chunk_size self.X_train None self.y_train None def fit(self, X_train, y_train): self.X_train X_train.contiguous() self.y_train y_train.contiguous() return self def predict(self, X_test): preds [] for start in range(0, X_test.size(0), self.chunk_size): block X_test[start:start self.chunk_size] dist_sq pairwise_distances(block, self.X_train) _, idx torch.topk(dist_sq, kself.k, dim1, largestFalse) labels self.y_train[idx] one_hot torch.nn.functional.one_hot(labels, self.y_train.max() 1) votes one_hot.sum(dim1) preds.append(torch.argmax(votes, dim1)) return torch.cat(preds)fit里没有真正学习参数只做了引用保存这正好说明 KNN 是惰性学习模型。contiguous()是为了确保后续矩阵乘法不会因为张量不连续而报错在把切片传给模型时尤其重要。chunk_size这个参数决定了距离矩阵的峰值内存如果测试时显存溢出优先把它调小而不是调 k。4. K 值选择、评估指标与交叉验证4.1 评估指标检测率与误报率怎么算入侵检测不是简单的准确率游戏因为原始数据集中正常流量和攻击流量比例大约是 3:7哪怕把所有流量都判成攻击准确率也有 70%。课程设计如果只摆准确率评委一眼就能看出你没理解业务。实际项目里更要关注的是检测率召回率和误报率。正常样本记为正类还是负类不同论文里习惯不一样我这里按常见做法把攻击样本作为正类正常样本作为负类指标公式含义检测率 DRTP / (TP FN)真实攻击中被检出的比例越高越好误报率 FARFP / (FP TN)正常流量被判为攻击的比例越低越好F12 * P * R / (P R)查准率与检测率的调和平均准确率 ACC(TP TN) / 总数全局正确率只作参考KNN 在 KDD Cup 99 上做到 90% 以上的检测率并不难难的是把误报率压到 1% 以下。因为不少攻击类型比如 U2R、R2L在特征空间里跟正常流量混在一起邻居投票容易出现平票。这时候单纯调 k 效果有限应该去看特征重要性把没有区分度的特征删掉。项目源码里test.txt_marked_corrected这个文件就带了详细标签可以直接统计每个攻击类别的检出情况。4.2 网格搜索 K 值与距离度量K 值太小容易受单个噪声样本影响K 值太大会把类别边界抹平。在 49 万条数据上K 通常在 3 到 15 之间比较合理。我用一个在线验证的流程来选 Kdef evaluate_model(model, X_val, y_val): preds model.predict(X_val) tp ((preds 1) (y_val 1)).sum().item() fp ((preds 1) (y_val 0)).sum().item() fn ((preds 0) (y_val 1)).sum().item() dr tp / (tp fn) far fp / (fp tp) return dr, far for k in [3, 5, 7, 9, 11, 15]: model TorchKNN(kk, chunk_size1000) model.fit(X_train, y_train) dr, far evaluate_model(model, X_val, y_val) print(fk{k}, DR{dr:.4f}, FAR{far:.4f})k1时训练集上的准确率永远是 100%因为每个样本的最近邻就是它自己但这没有任何意义不能用训练集误差来选 K。上面代码里单独留了验证集k的最终选择可以画一条 FAR 随 K 变化的曲线。如果曲线在某个 K 值之后开始上升说明进入了过平滑区间再增大 K 只会让误报持续恶化。4.3 训练/测试划分与标准化陷阱很多课程设计在做划分时直接random_state42打乱但这在入侵检测里会引入两个问题一是同一时间窗口内的流量被拆到训练集和测试集会造成数据泄漏二是 KDD Cup 99 的测试集刻意包含了训练集中从未出现过的攻击类型随机采样会掩盖模型对未知攻击的泛化能力评估。本项目文件里保留了原始顺序的kddcup.data_10_percent_corrected_NOString_NOString按前 80% 后 20% 切分虽然简单但至少避免了不同时间段样本混洗的争议。标准化参数只从训练集统计。如果对全量数据标准化再切分会让测试集的均值方差反过来影响训练样本的数值范围KNN 的距离计算结果会轻微失真。我看到很多开源代码是先StandardScaler().fit_transform(X_all)再做train_test_split这是错误示范。正确顺序必须先切分再 fit再 transform。5. 工程结构与调参边界5.1 工程文件说明与 Maven 包装器的作用解压项目后你会看到pom.xml和mvnw这不是多余文件。很多入侵检测课程设计会把流量预处理或者规则引擎部分写成 Java再用 Maven 管理模型部分用 Python 和 PyTorch 实现。mvnw是 Maven Wrapper它会在没有全局安装 Maven 的情况下自动下载指定版本保证环境可复现。实际运行前执行./mvnw package可以把 Java 端的预处理工具打好包然后再跑 Python 端训练脚本。工程目录里的aa.txt_marked、mark_marked这类文件是中间标注产物可以忽略不影响模型训练。真正核心的就是两个部分kddcup.data_10_percent_corrected_NOString_NOString数据文件和几个.py脚本。我习惯把数据文件放在项目根目录Python 脚本放在src/main/python下跟 Maven 的src/main/java分开这样评审老师一眼能看出技术栈边界。5.2 样本量、数据维度对 KNN 的性能影响KNN 的时间复杂度是O(M * N * D)M 是测试样本数N 是训练样本数D 是特征维度。在原始 41 维特征上10% 数据集的训练样本约 39 万如果全量测试单次预测的矩阵乘法也要跑很久。我实测下来chunk_size 设为 2000 时5 万测试样本的预测时间大约在 8 到 12 分钟具体取决于 CPU 还是 GPU。如果做交叉验证这个时间会翻倍所以课程设计里一般只做一次验证集划分而不是 5 折。维度的影响更直接。41 维到 20 维距离计算量减少一半但如果用 PCA 降到过低维度攻击流量之间的区分度也会下降。可以先跑一遍全量特征再对比去掉src_bytes、dst_bytes这两个极值特征后的结果。很多情况下删掉这两个特征后误报率反而下降因为极值特征让少数大流量样本成了孤立的邻居。5.3 常见误用用训练集统计量归一化测试集我们前面已经强调过标准化参数泄漏这里再补一个容易踩的坑测试集在做标准化之前必须去掉标签列。如果读入时把标签一起标准化标签值 0/1 会被缩放到 -0.2 到 1.2 之类的范围预测阶段索引训练标签时会出现越界。源码里的NOString_NOString文件格式就是特征和标签全为纯数字分离标签时建议用iloc[:, :-1]不要手写索引列号。另一个常见问题是距离矩阵溢出。float32 在x_norm y_norm.t()阶段可能出现数值放大训练样本上亿次累加后平方和可能达到 10^8 量级减法之后仍然在 float32 表示范围内但开平方应避免。如果你发现预测结果全是同一个类别先打印一下dist_sq的最小值和最大值看看是不是标准化没有生效。6. 部署时的验证技巧与加载新流量数据课程设计交上去之前我建议把整个流程固化成一个main.py从数据加载到预测一条新记录都能在命令行里复现。重点不是写漂亮的 API而是保证第二天打开电脑还能跑出同样的指标。def preprocess_new_record(raw_values, mean, std): arr np.array(raw_values, dtypenp.float32).reshape(1, -1) arr (arr - mean) / std return torch.from_numpy(arr) def predict_single(model, x_new): dist pairwise_distances(x_new, model.X_train) _, idx torch.topk(dist, kmodel.k, dim1, largestFalse) labels model.y_train[idx] return torch.bincount(labels[0]).argmax().item()这里的关键是把训练时算出的mean和std存成文件下次加载新流量时直接复用不要重新计算。KNN 没有显式的模型文件但距离计算依赖完整的训练集所以部署时必须把X_train和y_train一起保存。用torch.save保存训练集会被认为太原始常见做法是把TorchKNN对象序列化但 KNN 对象里包含整个训练矩阵文件体积跟数据本身一样大这是惰性学习模型的固有代价。验证整条链路时取训练集中的最后 1000 条样本先跑一遍预测确认准确率跟实验报告里的偏差小于 0.5%。然后随便构造一条[0, 0, 0, ...]全零记录此时标准化后的向量完全落在所有特征的均值位置预测结果应该是数量占优的那一类如果输出跟多数类一致说明代码逻辑没串联错。最后再检查一次内存当训练样本数翻倍时chunk_size 必须同步减半否则距离矩阵占用会超过物理内存表现为 PyTorch 报CUDA out of memory或者系统 swap 急剧增长。调小 chunk_size 是最简单可靠的兜底方案。本文还有配套的精品资源点击获取