PyTorch实战:用CrossEntropyLoss的weight和label_smoothing搞定类别不平衡和过拟合

发布时间:2026/5/19 8:48:10

PyTorch实战:用CrossEntropyLoss的weight和label_smoothing搞定类别不平衡和过拟合 PyTorch实战用CrossEntropyLoss的weight和label_smoothing搞定类别不平衡和过拟合当你面对一个真实世界的数据集时类别不平衡和模型过拟合可能是最令人头疼的两个问题。想象一下你正在构建一个医学影像分类系统但某些疾病的样本数量可能只有其他疾病的十分之一或者你在处理社交媒体文本分类时某些类别的样本量远远超过其他类别。这种不平衡会导致模型对多数类过度拟合而对少数类表现糟糕。同时模型可能会过于自信地预测训练集中的样本导致在未见数据上泛化能力下降。PyTorch中的nn.CrossEntropyLoss提供了两个强大的武器来应对这些挑战weight参数和label_smoothing参数。本文将带你深入理解这两个参数的工作原理并通过实际代码示例展示如何将它们应用到真实项目中。1. 理解类别不平衡问题及其解决方案类别不平衡是机器学习中一个普遍存在的问题。在实际应用中很少有数据集能够完美地保持各类别样本数量的均衡。以CIFAR-10数据集为例如果我们人为地创建一个不平衡版本比如让飞机类别的样本数量是汽车类别的10倍模型就会倾向于更多地预测飞机因为这样可以在不学习任何有用特征的情况下获得较高的准确率。1.1 类别权重的基本原理CrossEntropyLoss的weight参数允许我们为每个类别指定一个权重这个权重将用于调整损失计算。基本思想是给样本量少的类别更高的权重给样本量多的类别更低的权重。这样模型在训练时会更加关注少数类别的表现。计算类别权重的常见方法有逆类别频率权重与类别频率成反比平方根逆频率权重与类别频率的平方根成反比自定义权重根据业务需求手动设置import numpy as np from collections import Counter # 假设我们有以下类别标签 labels [0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2] # 计算每个类别的频率 class_counts Counter(labels) total_samples len(labels) num_classes len(class_counts) # 逆频率权重 weights torch.tensor([total_samples / class_counts[i] for i in range(num_classes)], dtypetorch.float32) weights weights / weights.sum() * num_classes # 归一化 print(逆频率权重:, weights)1.2 权重对模型训练的影响为了直观展示权重的作用我们可以比较使用和不使用权重时模型的训练过程。下面是一个简单的实验设置import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split # 创建一个不平衡的模拟数据集 X, y make_classification(n_samples1000, n_features20, n_classes3, n_informative4, weights[0.7, 0.2, 0.1], random_state42) X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, stratifyy, random_state42) # 转换为PyTorch张量 X_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.long) X_test_t torch.tensor(X_test, dtypetorch.float32) y_test_t torch.tensor(y_test, dtypetorch.long) # 计算类别权重 class_counts torch.bincount(y_train_t) weights (1. / class_counts) * len(y_train_t) / len(class_counts) weights weights.float() # 定义简单模型 class SimpleModel(nn.Module): def __init__(self, input_dim, num_classes): super().__init__() self.fc1 nn.Linear(input_dim, 64) self.fc2 nn.Linear(64, num_classes) def forward(self, x): x torch.relu(self.fc1(x)) x self.fc2(x) return x # 训练函数 def train_model(use_weightFalse): model SimpleModel(X_train.shape[1], len(class_counts)) optimizer optim.Adam(model.parameters(), lr0.01) if use_weight: criterion nn.CrossEntropyLoss(weightweights) else: criterion nn.CrossEntropyLoss() for epoch in range(100): optimizer.zero_grad() outputs model(X_train_t) loss criterion(outputs, y_train_t) loss.backward() optimizer.step() if epoch % 10 0: with torch.no_grad(): preds torch.argmax(model(X_test_t), dim1) acc (preds y_test_t).float().mean() print(fEpoch {epoch}, Loss: {loss.item():.4f}, Test Acc: {acc:.4f}) return model print(训练不使用类别权重:) model_no_weight train_model(use_weightFalse) print(\n训练使用类别权重:) model_with_weight train_model(use_weightTrue)在这个实验中你会观察到使用类别权重的模型在少数类别上的表现通常会更好尽管整体准确率可能略有下降。这正是我们想要的——在类别不平衡的情况下我们更关心的是所有类别的均衡表现而不是单纯的总体准确率。2. 标签平滑技术详解过拟合是深度学习中另一个常见问题。当模型对训练数据过度自信时它在测试数据上的表现往往会下降。标签平滑(Label Smoothing)是一种有效的正则化技术通过软化真实标签来防止模型变得过于自信。2.1 标签平滑的数学原理传统的分类任务使用硬标签one-hot编码即正确类别为1其他类别为0。标签平滑将这些硬标签替换为软标签y_i (1 - α) * y_i α / K其中y_i是原始one-hot标签α是平滑系数通常0.1-0.2K是类别数量y_i是平滑后的标签这种转换有两个主要效果正确类别的概率被略微降低错误类别的概率被略微提高def label_smoothing(y, alpha, num_classes): y: 原始标签形状为(N,) alpha: 平滑系数 num_classes: 类别数量 返回平滑后的标签形状为(N, num_classes) device y.device y_onehot torch.zeros(len(y), num_classes).to(device) y_onehot.scatter_(1, y.unsqueeze(1), 1) return (1 - alpha) * y_onehot alpha / num_classes # 示例 y torch.tensor([0, 1, 2]) smoothed label_smoothing(y, alpha0.1, num_classes3) print(平滑后的标签:\n, smoothed)2.2 标签平滑的实际效果为了理解标签平滑如何影响模型训练我们可以比较使用和不使用标签平滑时模型的预测置信度分布import matplotlib.pyplot as plt def train_with_smoothing(alpha0.0): model SimpleModel(X_train.shape[1], len(class_counts)) optimizer optim.Adam(model.parameters(), lr0.01) criterion nn.CrossEntropyLoss(label_smoothingalpha) for epoch in range(100): optimizer.zero_grad() outputs model(X_train_t) loss criterion(outputs, y_train_t) loss.backward() optimizer.step() return model # 训练不同平滑系数的模型 models { No smoothing: train_with_smoothing(alpha0.0), Smoothing α0.1: train_with_smoothing(alpha0.1), Smoothing α0.2: train_with_smoothing(alpha0.2) } # 分析预测置信度 def plot_confidence(models, X, y): plt.figure(figsize(15, 5)) for i, (name, model) in enumerate(models.items()): with torch.no_grad(): logits model(X) probs torch.softmax(logits, dim1) max_probs probs.max(dim1).values.numpy() plt.subplot(1, 3, i1) plt.hist(max_probs, bins20, range(0, 1)) plt.title(name) plt.xlabel(预测置信度) plt.ylabel(样本数量) plt.tight_layout() plt.show() plot_confidence(models, X_test_t, y_test_t)从直方图中可以看到不使用标签平滑时模型倾向于对许多样本做出非常自信的预测置信度接近1.0。而使用标签平滑后预测置信度分布更加分散表明模型对自己的预测不再那么自信这通常会带来更好的泛化性能。3. 综合应用同时处理不平衡和过拟合在实际项目中我们往往需要同时解决类别不平衡和过拟合问题。CrossEntropyLoss允许我们同时使用weight和label_smoothing参数来实现这一目标。3.1 参数组合策略当同时使用类别权重和标签平滑时需要注意以下几点类别权重通常基于训练集统计计算标签平滑系数α通常设置为0.1-0.2两种技术可以互补使用但可能需要调整学习率# 综合设置 class_counts torch.bincount(y_train_t) weights (1. / class_counts) * len(y_train_t) / len(class_counts) weights weights.float() alpha 0.1 # 定义损失函数 criterion nn.CrossEntropyLoss(weightweights, label_smoothingalpha) # 训练模型 model SimpleModel(X_train.shape[1], len(class_counts)) optimizer optim.Adam(model.parameters(), lr0.005) # 较小的学习率 for epoch in range(100): optimizer.zero_grad() outputs model(X_train_t) loss criterion(outputs, y_train_t) loss.backward() optimizer.step() if epoch % 10 0: with torch.no_grad(): preds torch.argmax(model(X_test_t), dim1) acc (preds y_test_t).float().mean() print(fEpoch {epoch}, Loss: {loss.item():.4f}, Test Acc: {acc:.4f})3.2 实际案例图像分类让我们将这些技术应用到一个更实际的图像分类任务中。我们将使用CIFAR-10数据集并人为创建一个不平衡版本import torchvision import torchvision.transforms as transforms from torch.utils.data import WeightedRandomSampler # 下载并加载CIFAR-10数据集 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) # 原始训练集 full_trainset torchvision.datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform) # 创建不平衡的训练集让类别0的样本是其他类别的10倍 indices [] class_counts [0] * 10 for idx, (_, label) in enumerate(full_trainset): if label 0 or (label ! 0 and idx % 10 0): indices.append(idx) class_counts[label] 1 imbalanced_trainset torch.utils.data.Subset(full_trainset, indices) trainloader DataLoader(imbalanced_trainset, batch_size64, shuffleTrue) # 测试集保持不变 testset torchvision.datasets.CIFAR10(root./data, trainFalse, downloadTrue, transformtransform) testloader DataLoader(testset, batch_size64, shuffleFalse) # 计算类别权重 weights torch.tensor([1.0 / count for count in class_counts], dtypetorch.float32) weights weights / weights.sum() * len(class_counts) # 定义CNN模型 class CNN(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(3, 32, 3, padding1) self.conv2 nn.Conv2d(32, 64, 3, padding1) self.pool nn.MaxPool2d(2, 2) self.fc1 nn.Linear(64 * 8 * 8, 256) self.fc2 nn.Linear(256, 10) def forward(self, x): x self.pool(torch.relu(self.conv1(x))) x self.pool(torch.relu(self.conv2(x))) x x.view(-1, 64 * 8 * 8) x torch.relu(self.fc1(x)) x self.fc2(x) return x # 训练函数 def train_cifar(use_weightFalse, use_smoothingFalse, alpha0.1): model CNN() optimizer optim.Adam(model.parameters(), lr0.001) if use_weight and use_smoothing: criterion nn.CrossEntropyLoss(weightweights, label_smoothingalpha) print(使用类别权重和标签平滑) elif use_weight: criterion nn.CrossEntropyLoss(weightweights) print(仅使用类别权重) elif use_smoothing: criterion nn.CrossEntropyLoss(label_smoothingalpha) print(仅使用标签平滑) else: criterion nn.CrossEntropyLoss() print(不使用特殊技术) for epoch in range(10): running_loss 0.0 for i, (inputs, labels) in enumerate(trainloader): optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 每个epoch结束后评估测试集 correct 0 total 0 with torch.no_grad(): for (inputs, labels) in testloader: outputs model(inputs) _, predicted torch.max(outputs.data, 1) total labels.size(0) correct (predicted labels).sum().item() print(fEpoch {epoch1}, Loss: {running_loss/len(trainloader):.4f}, Test Acc: {100*correct/total:.2f}%) return model # 比较不同配置 print( 基线模型 ) baseline train_cifar(use_weightFalse, use_smoothingFalse) print(\n 仅使用类别权重 ) weight_only train_cifar(use_weightTrue, use_smoothingFalse) print(\n 仅使用标签平滑 ) smoothing_only train_cifar(use_weightFalse, use_smoothingTrue) print(\n 使用类别权重和标签平滑 ) combined train_cifar(use_weightTrue, use_smoothingTrue)在这个实验中你会看到组合使用类别权重和标签平滑通常能取得最好的效果特别是在少数类别上的表现。虽然整体准确率可能不是最高的但各类别之间的性能差异会显著减小。4. 高级技巧与最佳实践掌握了基本用法后让我们来看一些高级技巧和实际应用中需要注意的事项。4.1 动态权重调整在某些情况下固定的类别权重可能不是最优选择。我们可以实现动态调整权重的策略class DynamicWeightedLoss(nn.Module): def __init__(self, initial_weights, update_interval100): super().__init__() self.weights nn.Parameter(initial_weights.clone(), requires_gradFalse) self.update_interval update_interval self.steps 0 self.class_counts torch.zeros_like(initial_weights) def forward(self, input, target): # 更新类别计数 unique, counts torch.unique(target, return_countsTrue) for cls, cnt in zip(unique, counts): self.class_counts[cls] cnt # 定期更新权重 self.steps 1 if self.steps % self.update_interval 0: with torch.no_grad(): new_weights (1. / self.class_counts) * len(target) / len(self.weights) new_weights new_weights / new_weights.sum() * len(self.weights) self.weights.copy_(new_weights) self.class_counts.zero_() # 计算损失 log_probs -torch.nn.functional.log_softmax(input, dim1) loss log_probs * self.weights[target].unsqueeze(1) return loss.mean() # 使用示例 initial_weights torch.ones(10) # 初始为均匀权重 criterion DynamicWeightedLoss(initial_weights)4.2 标签平滑与其他正则化技术的结合标签平滑可以与其他正则化技术如Dropout、权重衰减等一起使用。下面是一个综合应用的例子class RegularizedCNN(nn.Module): def __init__(self, dropout_prob0.5): super().__init__() self.conv1 nn.Conv2d(3, 32, 3, padding1) self.conv2 nn.Conv2d(32, 64, 3, padding1) self.pool nn.MaxPool2d(2, 2) self.dropout nn.Dropout(dropout_prob) self.fc1 nn.Linear(64 * 8 * 8, 256) self.fc2 nn.Linear(256, 10) def forward(self, x): x self.pool(torch.relu(self.conv1(x))) x self.pool(torch.relu(self.conv2(x))) x x.view(-1, 64 * 8 * 8) x self.dropout(x) x torch.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x # 训练设置 model RegularizedCNN(dropout_prob0.3) optimizer optim.Adam(model.parameters(), lr0.001, weight_decay1e-4) criterion nn.CrossEntropyLoss(weightweights, label_smoothing0.1) # 训练循环保持不变4.3 类别权重计算的变体除了简单的逆频率权重还有其他几种计算权重的方法值得尝试有效样本数量权重def effective_num_weight(class_counts, beta0.9): en (1 - beta**class_counts) / (1 - beta) weights 1.0 / en return weights / weights.sum() * len(weights) class_counts torch.bincount(y_train_t) weights effective_num_weight(class_counts)类别平衡焦点损失class BalancedFocalLoss(nn.Module): def __init__(self, class_counts, alpha0.25, gamma2): super().__init__() weights (1. / class_counts) * len(class_counts) self.weights weights / weights.sum() self.alpha alpha self.gamma gamma def forward(self, input, target): log_probs -torch.nn.functional.log_softmax(input, dim1) probs torch.exp(-log_probs) # 焦点损失项 focal (1 - probs) ** self.gamma # 类别平衡项 balanced self.weights[target].unsqueeze(1) loss self.alpha * balanced * focal * log_probs return loss.mean()在实际项目中我发现组合使用类别权重和标签平滑通常能取得最佳效果但需要仔细调整超参数。对于极度不平衡的数据集如1:100的比例可能需要更激进的重采样策略或专门设计的损失函数。标签平滑系数α0.1在大多数情况下表现良好但对于噪声较多的数据集可以尝试更大的值如0.2。

相关新闻