如何处理数据类别不平衡的问题

数据来源背景:

1.医疗诊断 -- 疾病检测、医疗影像分类

2.欺诈检测 -- 信用卡欺诈检测、金融反欺诈

3.自然语言处理 -- 垃圾邮件分类、情感分析

产生这种数据类别不平衡的原因是有以下几点:

1.数据采集方式:现实世界中,某些事件本身发生频率低,如欺诈交易、疾病病例。

2.标注成本高:罕见类别的人工标注成本高,如医学影像、交通事故数据

3.自然分布:在信用评分中,大部分用户是正常的,违约用户较少

如何应对这一类问题,有一下几点的解决方法:

数据方面:

1.数据过采样(Oversampling) -- 增加少类样本的数量

①随机过采样 -- 复制少数类样本

def random_oversample(X, y, sampling_rate=0.1):
    """
    随机复制指定类别的样本以实现过采样。

    参数:
    - X: 特征数据,ndarray格式
    - y: 标签数据,ndarray格式
    - target_class: 需要过采样的类别,默认为1
    - sampling_rate: 采样率,表示增加原始数据集0.1倍样本

    返回:
    - oversampled_X: 过采样后的特征数据
    - oversampled_y: 过采样后的标签数据
    """
    # 获取目标类别样本的索引
    target_indices = np.where(y == 1)[0]
    n_target_samples = len(X)

    # 计算需要增加的样本数量
    n_additional_samples = int(n_target_samples * sampling_rate)

    # 随机选择目标类别样本进行复制
    sampled_indices = np.random.choice(target_indices, size=n_additional_samples, replace=True)
    sampled_data = X[sampled_indices]

    # 合并原始数据和过采样数据
    oversampled_X = np.vstack((X, sampled_data))
    oversampled_y = np.concatenate((y, np.ones(n_additional_samples)))

    return oversampled_X, oversampled_y

②SMOTE(合成少数类样本)

def smote(X_train, y_train, k=5, sampling_rate=1.0):
    '''
    X_train:训练集的特征数据,形状为 (n_samples, n_features)。
    y_train:训练集的标签,形状为 (n_samples, ),其中 1 表示少数类,0 表示多数类。
    k=5:选择 5 个最近邻样本(用于生成合成样本)。
    sampling_rate=1.0:过采样比例
    '''
    class_1_indices = np.where(y_train == 1)[0]
    n_samples = len(class_1_indices)
    n_synthetic_samples = int(n_samples * sampling_rate)  # 计算需要合成的样本数量
    synthetic_samples = []

    for i in range(n_synthetic_samples):
        # 计算当前样本与所有样本的欧几里得距离,并选取 k 个最近邻
        nn_indices = np.argsort(np.linalg.norm(X_train - X_train[class_1_indices[i % n_samples]], axis=1))[1:k+1]
        nn_index = np.random.choice(nn_indices)
        diff = X_train[nn_index] - X_train[class_1_indices[i % n_samples]]
        gap = np.random.rand()
        synthetic_sample = X_train[class_1_indices[i % n_samples]] + gap * diff
        synthetic_samples.append(synthetic_sample)

    # 数据拼接
    synthetic_samples = np.array(synthetic_samples)
    synthetic_labels = np.ones(n_synthetic_samples)

    X_train_balanced = np.concatenate([X_train, synthetic_samples], axis=0)
    y_train_balanced = np.concatenate([y_train, synthetic_labels], axis=0)

    X_train_balanced, y_train_balanced = shuffle(X_train_balanced, y_train_balanced)

    return X_train_balanced, y_train_balanced

优点:增加少数类样本,防止模型只学到多数类

缺点:会引入过拟合(尤其是随机过采样)

2.数据欠采样(Undersampling)

①随机删除部分多数类样本

def random_undersampling(X, y, sampling_rate=1.0, random_state=None):
    """
    随机欠采样(Undersampling),减少多数类样本数量,使类别更平衡
    
    参数:
    X: 特征数据 (pd.DataFrame 或 np.ndarray)
    y: 标签数据 (pd.Series 或 np.ndarray)
    sampling_rate: 欠采样比例,例如 0.5 表示多数类减少 50%
    random_state: 随机种子,保证结果可复现
    
    返回:
    X_resampled, y_resampled: 经过欠采样后的数据
    """
    np.random.seed(random_state)  # 保证可复现
    df = pd.DataFrame(X)  # 转换为 DataFrame 方便操作
    df['label'] = y        # 添加标签列
    
    # 统计类别数量
    class_counts = df['label'].value_counts()
    minority_class = class_counts.idxmin()  # 取少数类标签
    majority_class = class_counts.idxmax()  # 取多数类标签

    n_minority = class_counts[minority_class]  # 少数类样本数量
    n_majority = int(class_counts[majority_class] * sampling_rate)  # 欠采样后多数类数量
    
    # 随机采样多数类
    majority_samples = df[df['label'] == majority_class].sample(n=n_majority, random_state=random_state)
    minority_samples = df[df['label'] == minority_class]  # 保留所有少数类样本

    # 组合数据
    df_resampled = pd.concat([majority_samples, minority_samples]).sample(frac=1, random_state=random_state)  # 打乱数据
    X_resampled = df_resampled.drop(columns=['label']).values
    y_resampled = df_resampled['label'].values
    
    return X_resampled, y_resampled

②近似聚类(K-means)去除冗余数据

import numpy as np
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.metrics import pairwise_distances_argmin_min

def approximate_clustering_deduplication(X, n_clusters=10, random_state=42):
    """
    通过 KMeans 进行近似聚类,并去除冗余样本,保留每个簇的代表性样本。
    
    参数:
    X: np.ndarray 或 pd.DataFrame, 特征矩阵
    n_clusters: int, 设定的聚类数量
    random_state: int, 随机种子,保证结果可复现
    
    返回:
    X_selected: np.ndarray, 去冗余后的数据
    selected_indices: list, 选出的样本索引
    """
    # 进行 KMeans 聚类
    kmeans = KMeans(n_clusters=n_clusters, random_state=random_state, n_init=10)
    labels = kmeans.fit_predict(X)
    
    # 计算每个类别的中心点,并选出距离中心最近的样本
    selected_indices = []
    for cluster in range(n_clusters):
        cluster_indices = np.where(labels == cluster)[0]  # 取出当前簇的样本索引
        cluster_center = kmeans.cluster_centers_[cluster].reshape(1, -1)  # 当前簇的中心
        closest, _ = pairwise_distances_argmin_min(cluster_center, X[cluster_indices])  # 找到离中心最近的点
        selected_indices.append(cluster_indices[closest[0]])  # 记录索引
    
    # 取出筛选后的样本
    X_selected = X[selected_indices]
    
    return X_selected, selected_indices

优点:让模型更关注少数类

缺点:可能丢失多数类的有用信息

3.数据增强(Data Augmentation)

①.图像分类:数据旋转、翻转、噪声、颜色变换等来实现数据增强

transform = transforms.Compose([
    transforms.RandomResizedCrop(224),            # 随机裁剪并调整大小
    transforms.Grayscale(num_output_channels=3),  # 将灰度图转为 3 通道
    transforms.RandomHorizontalFlip(),            # 随机水平翻转
    transforms.RandomRotation(30),                # 随机旋转
    transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.2),  # 随机颜色变换
    transforms.ToTensor(),                     # 转换为Tensor
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),  # 标准化
])

②文本分类:替换同义词、回译、词序调整

优点:能增加模型的泛化能力

缺点:如果增强方式不合理,可能会影响数据的真实分布

模型层面:

1.调整损失函数

①加权交叉熵(Weighted Cross-Entropy)

# 定义类别权重,少数类(1)赋予更高权重
class_weights = torch.tensor([0.1, 0.9], dtype=torch.float).to(device)

# 加权交叉熵损失
criterion = nn.CrossEntropyLoss(weight=class_weights)

②Focal Loss(聚焦损失)

Focal Loss 通过给容易分类的样本较小的权重,让模型更关注难分类的样本

class FocalLoss(nn.Module):
    def __init__(self, alpha=0.25, gamma=2):
        """
        alpha: 控制正负样本的权重(适用于类别不平衡)
        gamma: 控制模型对困难样本的关注度
        """
        super(FocalLoss, self).__init__()
        self.alpha = alpha
        self.gamma = gamma

    def forward(self, logits, targets):
        probs = F.softmax(logits, dim=1)  # 计算 softmax 概率
        targets_one_hot = F.one_hot(targets, num_classes=logits.shape[1]).float()  # 转换为 one-hot
        
        # 计算 focal loss
        ce_loss = -targets_one_hot * torch.log(probs + 1e-8)
        focal_weight = self.alpha * (1 - probs) ** self.gamma
        focal_loss = focal_weight * ce_loss

        return focal_loss.sum()

2.改进采样策略 -- 平衡采样(Balanced Batch Sampling)

确保每个 batch 中少数类和多数类的比例均衡

# 计算采样权重
class_counts = torch.bincount(y)
weights = 1.0 / class_counts[y]  # 计算每个样本的采样权重

# 使用 WeightedRandomSampler 进行采样
sampler = WeightedRandomSampler(weights, len(weights))

# 创建 DataLoader
dataset = TensorDataset(X, y)
balanced_loader = DataLoader(dataset, batch_size=16, sampler=sampler)

3.使用更鲁棒的模型

集成学习(Ensemble Learning)

结合多个模型,提高分类效果,减少类别不平衡带来的偏差

优化评估指标层面

1.使用适合不平衡数据的评价指标

①F1-score(综合Precision和Recall)

②AUC-ROC(衡量模型区分类别能力)

③PR 曲线(Precision-Recall Curve)

④Quadratic Weighted Kappa(QWK)

from sklearn.metrics import f1_score, precision_score, recall_score
from sklearn.metrics import roc_auc_score, roc_curve
from sklearn.metrics import precision_recall_curve, auc
from sklearn.metrics import cohen_kappa_score

# --------------------------------------------------------------------------#
# 计算 F1-score、Precision、Recall
f1 = f1_score(y_true, y_pred)  # F1 分数
precision = precision_score(y_true, y_pred)  # 精准率
recall = recall_score(y_true, y_pred)  # 召回率

# --------------------------------------------------------------------------#
# 计算 AUC-ROC
auc_roc = roc_auc_score(y_true, y_scores)

# 绘制 ROC 曲线
fpr, tpr, _ = roc_curve(y_true, y_scores)
plt.plot(fpr, tpr, label=f'AUC = {auc_roc:.4f}')
plt.plot([0, 1], [0, 1], linestyle='--', color='gray')  # 随机分类参考线
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve')
plt.legend()
plt.show()

# --------------------------------------------------------------------------#
# 计算 Precision-Recall 曲线
precision, recall, _ = precision_recall_curve(y_true, y_scores)
pr_auc = auc(recall, precision)

# 绘制 PR 曲线
plt.plot(recall, precision, label=f'PR AUC = {pr_auc:.4f}')
plt.xlabel('Recall')
plt.ylabel('Precision')
plt.title('Precision-Recall Curve')
plt.legend()
plt.show()

# --------------------------------------------------------------------------#
# 计算 Quadratic Weighted Kappa(QWK)
qwk_score = cohen_kappa_score(y_true, y_pred, weights='quadratic')

2.后处理 -- 调整阈值

通常阈值定为0.5,但是面对类别不平衡的数据,可以将阈值偏向类别较少的数据的标签

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值