通透!过采样 vs 欠采样 !!

哈喽,大家好~

今儿和大家聊聊过采样 以及 欠采样。

在二分类问题中,设训练样本为,其中

如果类别分布不平衡(例如正类占比很低),标准训练可能导致模型过分偏向多数类,从而在少数类上表现糟糕(召回率低、漏报严重)。

我们通常在经验风险最小化框架下训练模型:

其中为模型,为损失函数(如对数损失、hinge损失等)。

当类别不平衡时,该经验风险的每类样本贡献不均衡,模型倾向于优化多数类的错误率。

从贝叶斯决策出发,最优分类器最小化期望代价。

设代价矩阵为

  • 错误分到的代价为(false positive代价);
  • 错误分到的代价为(false negative代价)。

贝叶斯决策规则(使用后验概率)为:

当下式成立时预测为1:

利用贝叶斯公式,,可得:

这意味着先验与代价比都会直接影响最优决策阈值。

在不平衡数据中,很小,直接训练的模型若近似后验,会偏向预测,导致少数类召回下降。

过采样 Oversampling

过采样通过 增加少数类样本的有效权重 来缓解不平衡。

实现方式会包括:

  • 随机过采样:简单地复制少数类样本;
  • SMOTE:在少数类样本与其近邻之间插值生成合成样本;
  • ADASYN:自适应生成少数类样本,聚焦于困难区域;
  • Borderline-SMOTE等变种:在决策边界附近生成样本,强化边界学习。

核心思想是让少数类在经验风险中得到更多加权,从而影响模型的学习方向与边界。

风险加权的等价视角

过采样(复制少数类样本)在许多凸模型中等价于使用样本权重。

将经验风险改写为加权形式:

若将所有少数类样本权重设为(相当于每条少数类样本复制次),渲染它们在优化中的影响力上升。

此加权与“随机过采样”的目标一致,但随机过采样对模型的方差与过拟合风险更敏感(特别是树模型与KNN)。

Logistic回归中的等价性与截距校正

以对数损失为例,二分类Logistic模型的对数似然为:

如果对少数类进行倍过采样,相当于将少数类样本权重变为,则目标变为:

著名的“病例-对照抽样(case-control sampling)”结论指出:在Logistic回归中,只要抽样不改变的条件分布形状,斜率参数对特征的作用估计往往不受影响(或影响较小),但截距会因为先验比例变化而发生偏移,需后处理校正。

校正公式为:

其中是训练集中(过采样后)的类别先验,而是目标部署环境或测试集的真实先验。这说明过采样可能改变模型的概率校准(calibration),需要用真实先验调整截距,或在决策时改变阈值。

若过采样通过复制将正类比例从改变为(过采样后),且复制因子为,则有:

上式直接影响训练的“有效先验”,进而影响模型拟合的基准偏置。

SMOTE生成公式

SMOTE通过在少数类样本及其近邻间插值:

其中为少数类样本,是其k近邻之一。

SMOTE增强了少数类的流形覆盖,在边界附近生成连续分布的合成点,减轻随机复制导致的过拟合。

过采样的侧重点与优缺点

侧重点:

  • 提升少数类召回(Recall)与F1;
  • 改善决策边界对少数类的覆盖;
  • 在保持多数类数据的情况下,改善少数类表征。

优点:

  • 不削减多数类信息;
  • SMOTE类方法能在边界附近加密样本,利于非线性模型。

缺点:

  • 随机过采样可能导致过拟合少数类(重复样本);
  • 生成样本方法需谨慎(可能引入噪声、不合理点);
  • 改变训练先验,可能损害概率校准,需要后续校正或阈值调节。

欠采样 Undersampling

欠采样通过“减少多数类样本的有效权重”来实现平衡。

常见方法包括:

  • 随机欠采样:随机丢弃多数类样本;
  • NearMiss:保留与少数类距离更近的多数类样本;
  • Tomek Links:删去边界上的成对样本,清理噪声;
  • 编辑法等:通过局部一致性剔除不代表多数类的点。

在加权风险视角下,欠采样相当于将多数类样本的权重降低或直接减少其数量。

欠采样对方差与边界的影响

欠采样的优势是加速训练、缓解数据规模问题,同时更突出边界附近的样本(若使用NearMiss/Tomek等)。然而,减少多数类样本会增大估计方差,可能导致欠拟合或不稳定的边界,尤其当多数类中存在重要的结构信息时。

从优化角度看,欠采样改变了优化目标中的多数类分布,使得模型更重视少数类错误,但也可能丢失多数类的全局结构(例如多个簇或长尾模式),降低整体泛化性能。

欠采样的侧重点与优缺点

侧重点:

  • 快速训练,降低计算成本;
  • 强化类间边界对称性;
  • 在多数类冗余较多时,减少冗余并提升少数类表现。

优点:

  • 简单高效,适合海量多数类场景;
  • 边界清理方法(如Tomek)可以提升边界纯净度。

缺点:

  • 舍弃多数类信息,增大估计方差,可能欠拟合;
  • 若多数类结构复杂,欠采样会损害总体性能与稳定性。

过采样 vs 欠采样:核心区别与等价性

在凸模型(如Logistic回归)中,过采样与欠采样都可以视为“样本权重调整”,它们在理论上都相当于改变先验或代价结构(或者说改变经验风险的权重)。

但过采样倾向于保留多数类完整结构,欠采样倾向于牺牲多数类结构换取平衡。

两者都可能改变训练集的“有效先验比例”,导致概率校准的偏移。需要用真实先验或阈值后处理来校正。

在非线性模型(如树、核SVM、KNN)中,两者影响不再简单等价。过采样可能更易过拟合少数类(特别是随机复制),SMOTE类方法更合理但需调参。欠采样可能提升边界纯净度但损失多数类的多样性与全局结构。

算法侧重点:

  • 过采样:保留多数类、增强少数类,偏重提高召回与边界覆盖;
  • 欠采样:降低规模与冗余、加快训练,偏重边界平衡但牺牲多数类信息。

指标与采样策略的影响

不平衡场景下仅看准确率(Accuracy)不可靠。

常用指标定义:

准确率(Accuracy):

召回率(Recall, TPR):

精确率(Precision):

F1:

平衡准确率(Balanced Accuracy):

MCC(Matthews Correlation Coefficient):

ROC曲线(TPR vs FPR),PR曲线(Precision vs Recall):

ROC曲线衡量阈值变化下TPR与FPR关系,PR曲线更敏感于的极小先验情况,通常在强不平衡下更能反映少数类性能。

过采样往往提升Recall与F1,欠采样在某些情况下提升Balanced Accuracy,但若过度欠采样可能损害整体Acc与MCC。两者均会影响ROC/PR曲线形状与AUC。

完整案例

我们依旧使用虚拟数据集构造一个二分类、显著不平衡的任务。

我们代码中会训练三种模型:

  • 基线模型(不采样);
  • 过采样(SMOTE);
  • 欠采样(RandomUndersampler)。

并在同一张图中,详细说明图的意义与要点。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.metrics import roc_curve, auc, precision_recall_curve, average_precision_score, confusion_matrix
from imblearn.over_sampling import SMOTE
from imblearn.under_sampling import RandomUnderSampler
import seaborn as sns

# 1. 生成虚拟数据(不平衡)
X, y = make_classification(
    n_samples=2500,
    n_features=2,
    n_informative=2,
    n_redundant=0,
    n_clusters_per_class=2,
    weights=[0.900.10],  # 强不平衡:90%多数类,10%少数类
    class_sep=1.25,        # 类间分离度
    flip_y=0.03,           # 加入噪声
    random_state=42
)

# 划分训练/测试
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.4, stratify=y, random_state=42
)

# 2. 标准化(先拟合训练集)
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s = scaler.transform(X_test)

# 3. 三种训练集方案:原始、不采样;SMOTE过采样;随机欠采样
smote = SMOTE(sampling_strategy=0.5, k_neighbors=7, random_state=42)  # 将少数类提升到50%相对比例
rus = RandomUnderSampler(sampling_strategy=0.5, random_state=42)     # 将多数类降低到相对50%

X_train_sm, y_train_sm = smote.fit_resample(X_train_s, y_train)
X_train_ru, y_train_ru = rus.fit_resample(X_train_s, y_train)

# 4. 训练三个SVM模型(RBF核)
svc_base = SVC(kernel='rbf', probability=True, C=1.0, gamma='scale', random_state=42)
svc_smote = SVC(kernel='rbf', probability=True, C=1.0, gamma='scale', random_state=42)
svc_rus = SVC(kernel='rbf', probability=True, C=1.0, gamma='scale', random_state=42)

svc_base.fit(X_train_s, y_train)
svc_smote.fit(X_train_sm, y_train_sm)
svc_rus.fit(X_train_ru, y_train_ru)

# 5. 决策边界绘制网格(在原始坐标系构造网格后再标准化供预测)
x_min, x_max = X[:,0].min()-1.0, X[:,0].max()+1.0
y_min, y_max = X[:,1].min()-1.0, X[:,1].max()+1.0
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 400),
                     np.linspace(y_min, y_max, 400))
grid = np.c_[xx.ravel(), yy.ravel()]
grid_s = scaler.transform(grid)  # 标准化后用于模型预测

# 获取决策函数或预测概率用于等高线
Z_base = svc_base.decision_function(grid_s).reshape(xx.shape)
Z_smote = svc_smote.decision_function(grid_s).reshape(xx.shape)
Z_rus = svc_rus.decision_function(grid_s).reshape(xx.shape)

# 6. 测试集评估:ROC与PR
y_score_base = svc_base.predict_proba(X_test_s)[:,1]
y_score_smote = svc_smote.predict_proba(X_test_s)[:,1]
y_score_rus = svc_rus.predict_proba(X_test_s)[:,1]

fpr_base, tpr_base, _ = roc_curve(y_test, y_score_base)
fpr_smote, tpr_smote, _ = roc_curve(y_test, y_score_smote)
fpr_rus, tpr_rus, _ = roc_curve(y_test, y_score_rus)
auc_base = auc(fpr_base, tpr_base)
auc_smote = auc(fpr_smote, tpr_smote)
auc_rus = auc(fpr_rus, tpr_rus)

prec_base, rec_base, _ = precision_recall_curve(y_test, y_score_base)
prec_smote, rec_smote, _ = precision_recall_curve(y_test, y_score_smote)
prec_rus, rec_rus, _ = precision_recall_curve(y_test, y_score_rus)
ap_base = average_precision_score(y_test, y_score_base)
ap_smote = average_precision_score(y_test, y_score_smote)
ap_rus = average_precision_score(y_test, y_score_rus)

# 7. 构建可视化颜色与风格
cmap_bg = ListedColormap([""""])
colors_classes = {0""1""}     # 多数类红、少数类蓝
boundary_colors = {"base""""smote""""rus"""}

# 8. 将SMOTE与欠采样后的数据逆标准化以便在原始坐标系绘制
X_train_sm_orig = scaler.inverse_transform(X_train_sm)
X_train_ru_orig = scaler.inverse_transform(X_train_ru)

# 9. 可视化分析
plt.figure(figsize=(1612), dpi=120)

# 子图1:原始训练数据 + 基线模型决策边界
ax1 = plt.subplot(221)
ax1.contourf(xx, yy, Z_base > 0, alpha=0.12, cmap=cmap_bg)
ax1.contour(xx, yy, Z_base, levels=[0], colors=[boundary_colors["base"]], linewidths=2.5)

# 绘制原始训练集散点
mask0 = (y_train==0)
mask1 = (y_train==1)
ax1.scatter(X_train[mask0,0], X_train[mask0,1], s=18, c=colors_classes[0], label="Train Majority(0)", edgecolor="white", alpha=0.8)
ax1.scatter(X_train[mask1,0], X_train[mask1,1], s=30, c=colors_classes[1], label="Train Minority(1)", marker="^", edgecolor="black", alpha=0.9)

ax1.set_title("子图1:原始训练集 + 基线SVM决策边界", fontweight="bold")
ax1.set_xlabel("Feature 1")
ax1.set_ylabel("Feature 2")
ax1.legend(loc="best", frameon=True)

# 子图2:SMOTE过采样后数据 + 决策边界
ax2 = plt.subplot(222)
ax2.contourf(xx, yy, Z_smote > 0, alpha=0.12, cmap=cmap_bg)
ax2.contour(xx, yy, Z_smote, levels=[0], colors=[boundary_colors["smote"]], linewidths=2.5)

mask0_sm = (y_train_sm==0)
mask1_sm = (y_train_sm==1)
ax2.scatter(X_train_sm_orig[mask0_sm,0], X_train_sm_orig[mask0_sm,1], s=15, c=colors_classes[0], label="SMOTE Majority(0)", alpha=0.7)
ax2.scatter(X_train_sm_orig[mask1_sm,0], X_train_sm_orig[mask1_sm,1], s=25, c=colors_classes[1], label="SMOTE Minority(1)", marker="^", edgecolor="black", alpha=0.85)

ax2.set_title("子图2:SMOTE过采样后的训练集 + SVM决策边界", fontweight="bold")
ax2.set_xlabel("Feature 1")
ax2.set_ylabel("Feature 2")
ax2.legend(loc="best", frameon=True)

# 子图3:随机欠采样后数据 + 决策边界
ax3 = plt.subplot(223)
ax3.contourf(xx, yy, Z_rus > 0, alpha=0.12, cmap=cmap_bg)
ax3.contour(xx, yy, Z_rus, levels=[0], colors=[boundary_colors["rus"]], linewidths=2.5)

mask0_ru = (y_train_ru==0)
mask1_ru = (y_train_ru==1)
ax3.scatter(X_train_ru_orig[mask0_ru,0], X_train_ru_orig[mask0_ru,1], s=22, c=colors_classes[0], label="Under Majority(0)", edgecolor="white", alpha=0.8)
ax3.scatter(X_train_ru_orig[mask1_ru,0], X_train_ru_orig[mask1_ru,1], s=30, c=colors_classes[1], label="Under Minority(1)", marker="^", edgecolor="black", alpha=0.9)

ax3.set_title("子图3:随机欠采样后的训练集 + SVM决策边界", fontweight="bold")
ax3.set_xlabel("Feature 1")
ax3.set_ylabel("Feature 2")
ax3.legend(loc="best", frameon=True)

# 子图4:ROC与PR曲线对比(同一子图叠加两类曲线)
ax4 = plt.subplot(224)
# ROC曲线
ax4.plot(fpr_base, tpr_base, color=boundary_colors["base"], lw=2.0, label=f"Baseline ROC (AUC={auc_base:.3f})")
ax4.plot(fpr_smote, tpr_smote, color=boundary_colors["smote"], lw=2.0, label=f"SMOTE ROC (AUC={auc_smote:.3f})")
ax4.plot(fpr_rus, tpr_rus, color=boundary_colors["rus"], lw=2.0, label=f"Under ROC (AUC={auc_rus:.3f})")
ax4.plot([0,1],[0,1], color="", lw=1.0, linestyle="--", alpha=0.6)
ax4.set_xlim([0.01.0])
ax4.set_ylim([0.01.05])
ax4.set_xlabel("FPR")
ax4.set_ylabel("TPR")
ax4.set_title("子图4:ROC与PR曲线叠加对比", fontweight="bold")

# 使用副坐标轴叠加PR曲线
ax4_twin = ax4.twinx()
ax4_twin.plot(rec_base, prec_base, color=boundary_colors["base"], lw=2.0, linestyle=":", label=f"Baseline PR (AP={ap_base:.3f})")
ax4_twin.plot(rec_smote, prec_smote, color=boundary_colors["smote"], lw=2.0, linestyle=":", label=f"SMOTE PR (AP={ap_smote:.3f})")
ax4_twin.plot(rec_rus, prec_rus, color=boundary_colors["rus"], lw=2.0, linestyle=":", label=f"Under PR (AP={ap_rus:.3f})")
ax4_twin.set_ylim([0.01.05])
ax4_twin.set_ylabel("Precision")

# 组合图例:把主轴与副轴的图例合并
lines_main, labels_main = ax4.get_legend_handles_labels()
lines_twin, labels_twin = ax4_twin.get_legend_handles_labels()
ax4.legend(lines_main + lines_twin, labels_main + labels_twin, loc="lower right", frameon=True)

plt.tight_layout()
plt.show()
通透!过采样 vs 欠采样 !!图1

图1,原始训练集 + 基线决策边界:背景浅色块表示分类区域;绿色轮廓线是基线SVM的决策边界(等高线)。

散点中红色为多数类,蓝色尖角标记为少数类。可以观察在不平衡情况下,决策边界更偏向多数类的一侧,少数类点在相当区域被预测为多数类。

这体现了经验风险不平衡导致的偏置:尽管整体准确率可能看起来不错,但少数类漏报较多。

图2,SMOTE过采样 + 决策边界

黄色轮廓线为SMOTE训练后的决策边界。过采样增强了少数类在训练中的权重与覆盖,决策边界更靠近真实类间分隔,少数类区域被更好地识别。

合成样本在少数类局部流形上形成“连续带”,缓解随机复制过拟合的问题。通常这会提升Recall与PR曲线的表现,尤其在少数类极小先验情况下。

图3,随机欠采样 + 决策边界:紫色轮廓线为欠采样训练后的边界。通过减少多数类样本,训练过程更加“公平”地关注少数类。但可能牺牲多数类的整体结构信息,边界在全局上可能不够稳定,具体需要依赖数据分布复杂度与采样策略。

欠采样常见于多数类极其庞大的场景,降低训练时长与内存占用,同时在边界平衡上有所改善。但过度欠采样潜在地引入方差上升与欠拟合风险。

图4,ROC与PR曲线叠加:主轴展示ROC曲线(TPR vs FPR),辅助轴展示PR曲线(Precision vs Recall),并显示AUC与AP指标。使用鲜艳颜色区分三种方案。

通常在强不平衡时,PR/AP的区别更能体现少数类性能;SMOTE往往能提升AP与Recall,ROC AUC也可能改善。欠采样的改善取决于多数类冗余程度与采样比例。

通过这四幅图,我们都可以直观比较三种策略对边界形状、少数类覆盖以及整体评估指标的影响,从而帮助大家理解过采样与欠采样的本质差异与适用场景。

在实际的实验中,采样一般来说,仅在训练集进行,测试集必须保持原始真实分布,以评估模型在真实先验下的泛化表现。

交叉验证时,应在每个fold的训练部分进行采样,避免数据泄漏。

若使用Logistic等概率模型并在训练阶段改变先验,部署时应考虑截距/阈值校正。

SMOTE类方法需要考虑k近邻数、边界强化(Borderline-SMOTE)等超参数对效果的影响。欠采样比例不宜过度,以免引起方差飙升与信息丢失;可结合边界清理方法提升纯净度。

总体而言,过采样与欠采样是解决不平衡学习的两条互补路径:前者强调“补齐”少数类表示、后者强调“削减”多数类冗余。

最佳实践通常结合采样、代价敏感、阈值校正与合理的模型调参,以达到在真实部署环境下稳健且具有业务意义的性能。

最后

最近准备了的总结,完整的机器学习小册,免费领取~
通透!过采样 vs 欠采样 !!图2
领取:备注「算法小册」即可~
通透!过采样 vs 欠采样 !!图3

声明:内容取材于网络,仅代表作者观点,如有内容违规问题,请联系处理。 
more
首发2000万台?消息称苹果首款折叠iPhone 屏幕供应链、组装厂已定
专为端侧大模型而生:解密AI计算加速“秘密武器”Arm China周易X3 NPU IP
苹果2nm A20 性能太强了!曝:苹果将跳过 19 直推 iPhone 20
融资超20亿!北京AI芯片创企筹备IPO
突破!深圳诺奖实验室发布量产级RISC-V处理器IP
TypeScript超越Python成GitHub上使用最广语言,AI是主要驱动力
小智一周要闻 | 宇树科技完成IPO辅导;智元机器人完成股改
1899 元「iPhone 袜子」上架秒光;传华为 Mate80 上 20GB 内存;微信点赞苹果小程序政策 | 极客早知道
Sora 想做的 IP 内容生态,这个国产 AI 已经先走了一步
NeurIPS 2025 Oral | 1个Token零成本,REG让Diffusion训练收敛快20倍!
Copyright © 2025 成都区角科技有限公司
蜀ICP备2025143415号-1
  
川公网安备51015602001305号