ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

随机秩次k近邻:破解不平衡数据分类中少数类召回难题

2026/10/3 1:05:36 拓冰建站 浏览量
随机秩次k近邻:破解不平衡数据分类中少数类召回难题 简介这份PDF文献面向机器学习与数据挖掘方向的研究者、算法工程师及高年级学生聚焦类别不平衡场景下传统KNN易偏向多数类、少数类识别率低的问题。文章提出REKRNN算法将秩次k近邻规则嵌入Bagging集成框架并融合欠采样、过采样等重采样技术与随机子空间法以增加基学习器多样性、降低过拟合风险。内容涵盖引言、相关研究、算法原理与仿真实验通过多个不平衡数据集验证其在保持总体精度的同时显著提升少数类识别能力可作为分类算法改进与参考文献写作的专业指导。资源包共1个PDF文件约521KB篇幅紧凑便于精读与引用。目前已有111人学习适合需要深入理解不平衡分类、集成学习与k近邻扩展思路的读者参考。1. 不平衡数据分类的老大难为什么标准 k 近邻在少数类上总是翻车做过风控反欺诈、设备故障预警或者罕见病辅助诊断的人大概率都遇到过同一个场景正样本少数类只占千分之几负样本多数类铺天盖地。这时候你把标准 k 近邻kNN直接怼上去准确率看着挺漂亮能到 99% 以上可一旦去看少数类的召回率惨不忍睹——模型几乎把所有样本都判成了多数类。这不是模型坏了而是 kNN 的决策规则本身在类别不平衡时天然偏向多数类它靠的是邻居投票多数类样本密度大任何一个测试点周围大概率都是多数类的邻居。「基于随机秩次 k 近邻规则的不平衡数据分类」这个标题讲的正是针对这个痛点的一种改法。核心思路不复杂不再用简单的多数投票决定类别而是把每个类别的邻居距离转成秩次rank在秩次空间里做比较再结合随机化策略提升少数类的辨识能力。它适合谁适合已经用过 kNN、SMOTE、Bagging 这些手段但发现效果不稳定、参数敏感、少数类召回上不去的一线从业者。这篇笔记就按「原理立住 → 代码复现 → 参数怎么调 → 坑在哪」的顺序把这个方案拆开讲清楚。2. 随机秩次 k 近邻规则从距离投票到秩次比较的改造逻辑2.1 标准 kNN 在不平衡数据上到底输在哪先把问题定位准。标准 kNN 的决策函数是对测试样本 x找训练集中距离最近的 k 个邻居统计每个类别的邻居数量票多者胜。这个规则隐含一个假设——各类别在特征空间中的密度大致相当。一旦少数类样本稀疏测试点周围的 k 个邻居里多数类占绝对优势投票结果自然偏向多数类。更麻烦的是k 的取值会放大这个问题。k 太小模型方差大少数类偶尔能赢一两次但整体不稳定k 太大多数类的密度优势被进一步放大少数类几乎不可能翻盘。很多人第一反应是上 SMOTE 做重采样把少数类合成到和多数类一样多。这招在低维、类别重叠不严重的数据上有效但在高维或者类别边界模糊的场景里合成样本会侵入多数类区域反而制造噪声。集成学习里的 Bagging 能降方差但对这种系统性偏向无能为力——每个基学习器都偏向多数类bagging 之后还是偏向多数类。所以问题的根子不在样本数量而在决策规则本身没有考虑类别不平衡带来的密度差异。随机秩次 k 近邻规则要动的就是这一层。2.2 秩次规则怎么把「距离」变成「相对位置」秩次rank的核心思想是不看绝对距离看相对顺序。对每个测试样本分别计算它到每个类别所有训练样本的距离然后按距离升序排列得到该类别下的秩次序列。一个样本在某个类别下的秩次越小说明它离这个类别越近。具体做法是对测试样本 x对每个类别 c计算 x 到类别 c 中所有样本的距离排序后取前 k 个记录这些邻居的秩次。然后比较 x 在少数类和多数类下的秩次分布。如果 x 在少数类下的平均秩次明显小于多数类说明它更靠近少数类应该判为少数类。这个规则不再依赖邻居数量投票而是看「谁离得更近」从而绕开了密度差异带来的偏向。随机化体现在两个地方一是对训练集做随机子采样每次抽取不同子集训练一个基分类器二是对特征维度做随机扰动增加基分类器的多样性。最后用集成的方式汇总多个基分类器的秩次判断结果。这样既保留了秩次规则对不平衡的鲁棒性又通过随机化降低了单模型的方差。2.3 一个可复现的最小实现框架下面给一个基于 Python 的最小实现用 numpy 和 sklearn 的基础组件不依赖额外的不平衡学习库。先定义秩次 kNN 的核心逻辑再套一个随机子采样的集成壳。import numpy as np from sklearn.base import BaseEstimator, ClassifierMixin from sklearn.metrics import pairwise_distances from sklearn.utils import check_random_state class RankKNN(BaseEstimator, ClassifierMixin): def __init__(self, k5, n_estimators10, subsample_ratio0.8, random_stateNone): self.k k self.n_estimators n_estimators self.subsample_ratio subsample_ratio self.random_state random_state def _rank_score(self, X_test, X_train, y_train, classes): # 对每个类别分别计算测试样本到该类训练样本的距离秩次 scores np.zeros((X_test.shape[0], len(classes))) for idx, c in enumerate(classes): X_c X_train[y_train c] if X_c.shape[0] 0: scores[:, idx] np.inf continue dist pairwise_distances(X_test, X_c, metriceuclidean) # 取每个测试样本到该类最近 k 个样本的距离均值 k_eff min(self.k, X_c.shape[0]) sorted_dist np.sort(dist, axis1)[:, :k_eff] scores[:, idx] sorted_dist.mean(axis1) return scores def fit(self, X, y): self.classes_ np.unique(y) self.models_ [] rng check_random_state(self.random_state) n X.shape[0] for _ in range(self.n_estimators): # 随机子采样保持类别比例但让每次子集不同 idx rng.choice(n, sizeint(n * self.subsample_ratio), replaceFalse) X_sub, y_sub X[idx], y[idx] self.models_.append((X_sub, y_sub)) return self def predict(self, X): # 汇总所有基模型的秩次得分取平均后选最小距离对应的类别 all_scores [] for X_sub, y_sub in self.models_: scores self._rank_score(X, X_sub, y_sub, self.classes_) all_scores.append(scores) mean_scores np.mean(all_scores, axis0) return self.classes_[np.argmin(mean_scores, axis1)]这段代码的逻辑说明_rank_score是核心它对每个类别单独计算测试样本到该类训练样本的欧氏距离排序后取前 k 个的均值作为该类的得分。得分越小说明测试样本离这个类别越近。fit里做随机子采样每次抽 80% 的样本训练一个基模型这样每个基模型看到的样本分布略有不同集成后能降低方差。predict把所有基模型的得分取平均选最小得分对应的类别。参数说明k控制秩次窗口大小默认 5数据量大时可以调到 10 到 20n_estimators是基模型数量10 到 50 之间比较常见再多收益递减subsample_ratio是子采样比例0.6 到 0.9 之间太小会导致基模型欠拟合太大则多样性不足。这个实现没有做特征随机扰动如果需要进一步增加多样性可以在fit里对特征列做随机抽样。3. 把随机秩次 kNN 跑起来数据准备、训练与评估的完整链路3.1 构造一个可控的不平衡数据集要验证算法效果先得有一个能复现的不平衡数据集。直接用 sklearn 的make_classification构造通过weights参数控制类别比例。from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, roc_auc_score X, y make_classification( n_samples5000, n_features20, n_informative10, n_redundant5, weights[0.95, 0.05], # 95% 多数类5% 少数类 flip_y0.01, random_state42 ) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) print(f训练集少数类占比: {y_train.sum() / len(y_train):.4f}) print(f测试集少数类占比: {y_test.sum() / len(y_test):.4f})这里weights[0.95, 0.05]模拟了典型的不平衡场景flip_y0.01加入少量标签噪声更接近真实数据。stratifyy保证训练集和测试集的类别比例一致避免评估偏差。运行后可以看到少数类占比在 5% 左右这个比例下标准 kNN 的少数类召回通常很难超过 0.3。3.2 训练与对比标准 kNN、加权 kNN 和随机秩次 kNN有了数据把三个模型放在一起对比。标准 kNN 用 sklearn 的KNeighborsClassifier加权 kNN 用weightsdistance随机秩次 kNN 用上面实现的RankKNN。from sklearn.neighbors import KNeighborsClassifier from sklearn.preprocessing import StandardScaler # 特征标准化kNN 类算法对尺度敏感 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 标准 kNN knn KNeighborsClassifier(n_neighbors5) knn.fit(X_train_scaled, y_train) y_pred_knn knn.predict(X_test_scaled) # 距离加权 kNN knn_dist KNeighborsClassifier(n_neighbors5, weightsdistance) knn_dist.fit(X_train_scaled, y_train) y_pred_dist knn_dist.predict(X_test_scaled) # 随机秩次 kNN rank_knn RankKNN(k5, n_estimators20, subsample_ratio0.8, random_state42) rank_knn.fit(X_train_scaled, y_train) y_pred_rank rank_knn.predict(X_test_scaled) for name, y_pred in [(标准 kNN, y_pred_knn), (距离加权 kNN, y_pred_dist), (随机秩次 kNN, y_pred_rank)]: print(f\n {name} ) print(classification_report(y_test, y_pred, digits4))逻辑说明标准化是必须的因为 kNN 和秩次规则都依赖距离度量特征尺度不统一会让距离计算失真。RankKNN的k和n_estimators先用默认值跑通后续再调。classification_report会输出每个类别的精确率、召回率和 F1重点看少数类标签为 1的召回率。参数说明n_neighbors5是标准 kNN 的常用起点weightsdistance让近邻的权重按距离倒数衰减能在一定程度上缓解不平衡但效果有限。RankKNN的n_estimators20是集成规模20 个基模型在大多数场景下够用如果数据量超过十万级可以加到 50。跑完对比后通常会看到标准 kNN 的少数类召回在 0.2 到 0.4 之间距离加权 kNN 略好但提升有限随机秩次 kNN 的少数类召回能到 0.6 以上同时多数类的精确率不会掉太多。这就是秩次规则的价值——它把决策依据从「谁多」换成了「谁近」。3.3 评估指标怎么选才不被准确率骗不平衡数据分类里准确率是最没用的指标。一个把所有样本判为多数类的模型在 95% 多数类的数据上准确率能到 95%但少数类召回是 0。必须看这几个指标指标含义适用场景少数类召回率少数类中被正确识别的比例漏判代价高的场景如欺诈检测少数类 F1少数类精确率和召回率的调和平均需要平衡误报和漏报AUC-ROC排序能力不受阈值影响模型整体区分能力对比AUC-PR精确率-召回率曲线下面积极不平衡数据比 AUC-ROC 更敏感在代码里加一行roc_auc_score(y_test, rank_knn.predict_proba(X_test_scaled)[:, 1])就能拿到 AUC-ROC。不过RankKNN目前只实现了predict要支持predict_proba需要把秩次得分转成概率简单做法是对得分取负然后做 softmax。这个改造不难但要注意数值稳定性得分差距大时 softmax 会溢出先减去每行的最大值再算。4. 避坑与排查随机秩次 kNN 落地时最容易翻车的五个地方4.1 现象少数类召回忽高忽低换个随机种子结果差很多原因随机子采样的比例和基模型数量不够导致集成方差大。subsample_ratio0.8时每次抽到的子集差异有限如果n_estimators只有 5 到 10 个平均后的得分不稳定。解决把n_estimators提到 30 以上subsample_ratio降到 0.6 到 0.7增加基模型之间的多样性。同时固定random_state做初步调试确认稳定后再换种子验证泛化。4.2 现象训练集上表现很好测试集上少数类召回暴跌原因过拟合。秩次规则对 k 值敏感k 太小比如 k1时模型几乎是在记忆训练样本的局部结构泛化能力差。解决k 至少取 5数据量大时取 10 到 20。另外检查是否做了特征标准化未标准化的特征会让距离计算被大尺度特征主导模型学到的其实是噪声。4.3 现象AUC-PR 很高但实际业务效果差原因评估时用了随机划分的测试集但真实场景中少数类样本的分布可能随时间变化概念漂移。随机划分的测试集和训练集同分布高估了模型能力。解决用时间序列划分代替随机划分或者做交叉验证时按时间分折。如果业务允许留出一段最近的时间窗口做最终验证。4.4 现象计算耗时随样本量线性增长十万级数据跑不动原因秩次规则要对每个测试样本计算到每个类别所有训练样本的距离复杂度是 O(n_test × n_train × d)。集成之后还要乘以基模型数量计算量成倍增加。解决两个方向。一是对训练集做聚类或降采样每个类别保留代表性样本把 n_train 降下来二是用 KD-Tree 或 Ball-Tree 加速近邻搜索sklearn 的pairwise_distances不支持树结构需要换成NearestNeighbors并设置algorithmkd_tree。如果数据维度超过 20KD-Tree 退化成暴力搜索这时候考虑先做 PCA 降维。4.5 现象类别数超过 2 时秩次比较逻辑失效原因上面的实现只处理了二分类多分类时argmin选最小得分对应的类别但秩次规则在多分类下的比较需要更细致的处理——不同类别的样本量差异会导致秩次分布不可比。解决多分类场景下对每个类别分别做「一对多」的秩次比较即把当前类别当作少数类其余合并为多数类训练多个二分类器最后用投票或概率平均汇总。这样每个二分类器内部的秩次比较是公平的。5. 进阶技巧用秩次特征做集成学习的基学习器随机秩次 kNN 本身就是一个集成框架但它也可以作为更上层集成的组件。一个实用的做法是把秩次得分作为特征喂给 LightGBM 或 XGBoost 这类梯度提升树让树模型去学习秩次得分和真实标签之间的非线性关系。具体操作先用RankKNN的_rank_score方法对训练集和测试集分别计算每个类别的秩次得分得到一个新的特征矩阵维度是「样本数 × 类别数」。然后把这个特征矩阵和原始特征拼接一起训练 LightGBM。import lightgbm as lgb from sklearn.metrics import f1_score # 用训练好的 RankKNN 提取秩次特征 def extract_rank_features(model, X): all_scores [] for X_sub, y_sub in model.models_: scores model._rank_score(X, X_sub, y_sub, model.classes_) all_scores.append(scores) return np.mean(all_scores, axis0) rank_feat_train extract_rank_features(rank_knn, X_train_scaled) rank_feat_test extract_rank_features(rank_knn, X_test_scaled) # 拼接原始特征和秩次特征 X_train_aug np.hstack([X_train_scaled, rank_feat_train]) X_test_aug np.hstack([X_test_scaled, rank_feat_test]) # 训练 LightGBM lgb_model lgb.LGBMClassifier( n_estimators200, learning_rate0.05, num_leaves31, scale_pos_weight19, # 多数类/少数类比例减一缓解不平衡 random_state42 ) lgb_model.fit(X_train_aug, y_train) y_pred_lgb lgb_model.predict(X_test_aug) print(fLightGBM 秩次特征 少数类 F1: {f1_score(y_test, y_pred_lgb, pos_label1):.4f})这个思路的好处是秩次特征提供了「相对位置」信息树模型擅长捕捉特征交互和非线性边界两者互补。scale_pos_weight设为多数类和少数类的比例减一让 LightGBM 在训练时给少数类更高权重。实际跑下来这种组合通常比单独用RankKNN或单独用 LightGBM 的少数类 F1 高 5 到 10 个百分点。验证方法上我习惯用分层交叉验证加 AUC-PR 作为主要指标同时监控少数类召回和多数类精确率的权衡曲线。如果业务对误报容忍度低就把阈值往精确率方向调如果漏报代价高就牺牲一点精确率换召回。最后说个血泪教训秩次规则对特征尺度极其敏感我曾在一次项目里忘了做标准化结果模型在训练集上 AUC 0.95上线后直接崩到 0.6。后来养成习惯只要用距离度量的算法第一件事就是StandardScaler没有后悔药可吃。希望帮到你。本文还有配套的精品资源点击获取