数据来源背景:
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,但是面对类别不平衡的数据,可以将阈值偏向类别较少的数据的标签