news 2026/10/3 1:05:34

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
随机秩次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, k=5, n_estimators=10, subsample_ratio=0.8, random_state=None): 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, metric='euclidean') # 取每个测试样本到该类最近 k 个样本的距离均值 k_eff = min(self.k, X_c.shape[0]) sorted_dist = np.sort(dist, axis=1)[:, :k_eff] scores[:, idx] = sorted_dist.mean(axis=1) 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, size=int(n * self.subsample_ratio), replace=False) 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, axis=0) return self.classes_[np.argmin(mean_scores, axis=1)]

这段代码的逻辑说明:_rank_score是核心,它对每个类别单独计算测试样本到该类训练样本的欧氏距离,排序后取前 k 个的均值作为该类的得分。得分越小,说明测试样本离这个类别越近。fit里做随机子采样,每次抽 80% 的样本训练一个基模型,这样每个基模型看到的样本分布略有不同,集成后能降低方差。predict把所有基模型的得分取平均,选最小得分对应的类别。

参数说明:k控制秩次窗口大小,默认 5,数据量大时可以调到 10 到 20;n_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_samples=5000, n_features=20, n_informative=10, n_redundant=5, weights=[0.95, 0.05], # 95% 多数类,5% 少数类 flip_y=0.01, random_state=42 ) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, stratify=y, random_state=42 ) print(f"训练集少数类占比: {y_train.sum() / len(y_train):.4f}") print(f"测试集少数类占比: {y_test.sum() / len(y_test):.4f}")

这里weights=[0.95, 0.05]模拟了典型的不平衡场景,flip_y=0.01加入少量标签噪声,更接近真实数据。stratify=y保证训练集和测试集的类别比例一致,避免评估偏差。运行后可以看到少数类占比在 5% 左右,这个比例下标准 kNN 的少数类召回通常很难超过 0.3。

3.2 训练与对比:标准 kNN、加权 kNN 和随机秩次 kNN

有了数据,把三个模型放在一起对比。标准 kNN 用 sklearn 的KNeighborsClassifier,加权 kNN 用weights='distance',随机秩次 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_neighbors=5) knn.fit(X_train_scaled, y_train) y_pred_knn = knn.predict(X_test_scaled) # 距离加权 kNN knn_dist = KNeighborsClassifier(n_neighbors=5, weights='distance') knn_dist.fit(X_train_scaled, y_train) y_pred_dist = knn_dist.predict(X_test_scaled) # 随机秩次 kNN rank_knn = RankKNN(k=5, n_estimators=20, subsample_ratio=0.8, random_state=42) 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, digits=4))

逻辑说明:标准化是必须的,因为 kNN 和秩次规则都依赖距离度量,特征尺度不统一会让距离计算失真。RankKNN的k和n_estimators先用默认值跑通,后续再调。classification_report会输出每个类别的精确率、召回率和 F1,重点看少数类(标签为 1)的召回率。

参数说明:n_neighbors=5是标准 kNN 的常用起点,weights='distance'让近邻的权重按距离倒数衰减,能在一定程度上缓解不平衡,但效果有限。RankKNN的n_estimators=20是集成规模,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_ratio=0.8时每次抽到的子集差异有限,如果n_estimators只有 5 到 10 个,平均后的得分不稳定。

解决:把n_estimators提到 30 以上,subsample_ratio降到 0.6 到 0.7,增加基模型之间的多样性。同时固定random_state做初步调试,确认稳定后再换种子验证泛化。

4.2 现象:训练集上表现很好,测试集上少数类召回暴跌

原因:过拟合。秩次规则对 k 值敏感,k 太小(比如 k=1)时,模型几乎是在记忆训练样本的局部结构,泛化能力差。

解决: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并设置algorithm='kd_tree'。如果数据维度超过 20,KD-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, axis=0) 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_estimators=200, learning_rate=0.05, num_leaves=31, scale_pos_weight=19, # 多数类/少数类比例减一,缓解不平衡 random_state=42 ) lgb_model.fit(X_train_aug, y_train) y_pred_lgb = lgb_model.predict(X_test_aug) print(f"LightGBM + 秩次特征 少数类 F1: {f1_score(y_test, y_pred_lgb, pos_label=1):.4f}")

这个思路的好处是:秩次特征提供了「相对位置」信息,树模型擅长捕捉特征交互和非线性边界,两者互补。scale_pos_weight设为多数类和少数类的比例减一,让 LightGBM 在训练时给少数类更高权重。实际跑下来,这种组合通常比单独用RankKNN或单独用 LightGBM 的少数类 F1 高 5 到 10 个百分点。

验证方法上,我习惯用分层交叉验证加 AUC-PR 作为主要指标,同时监控少数类召回和多数类精确率的权衡曲线。如果业务对误报容忍度低,就把阈值往精确率方向调;如果漏报代价高,就牺牲一点精确率换召回。

最后说个血泪教训:秩次规则对特征尺度极其敏感,我曾在一次项目里忘了做标准化,结果模型在训练集上 AUC 0.95,上线后直接崩到 0.6。后来养成习惯,只要用距离度量的算法,第一件事就是StandardScaler,没有后悔药可吃。希望帮到你。

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

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

运算器组成实验全解析:从ALU原理到硬件调试与Verilog仿真

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

作者头像 李华
网站建设 2026/10/3 1:04:20

鱼缸潜水泵EMC整改实战:KY32DS024与EPWM高集成驱动方案

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

作者头像 李华
网站建设 2026/10/3 1:03:55

Qt6工程落地实战:安装配置、模块声明与性能优化全路径

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

作者头像 李华
网站建设 2026/10/3 1:03:21

DRV8818PWPR+PIC18F87J11工业步进电机控制实战

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

作者头像 李华
网站建设 2026/10/3 1:02:53

脉冲判定窗与恢复时间:从振铃识别到采样率选型的工程实践

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

作者头像 李华
网站建设 2026/10/3 1:02:31

单细胞转录组入门:PBMC数据从读取到聚类全流程解析

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

作者头像 李华