PyTorch nn.CrossEntropyLoss 实战:3种权重设置与标签平滑对比(附代码)

📅 2026/7/10 0:00:24 👁️ 阅读次数 📝 编程学习
PyTorch nn.CrossEntropyLoss 实战:3种权重设置与标签平滑对比(附代码)

PyTorch nn.CrossEntropyLoss 实战:3种权重设置与标签平滑对比(附代码)

交叉熵损失函数是深度学习分类任务中最核心的组件之一。PyTorch框架中的nn.CrossEntropyLoss不仅实现了标准的交叉熵计算,还提供了weightlabel_smoothing两个关键参数来解决实际训练中的类别不平衡和过拟合问题。本文将深入解析这些高级参数的实现原理,并通过完整的代码对比展示不同配置下的训练效果差异。

1. 交叉熵损失函数核心原理回顾

在进入高级参数实战之前,我们需要明确几个关键概念:

  • Logits:模型最后一层线性层的原始输出值(未经过softmax)
  • Softmax:将logits转换为概率分布的函数:$p_i = \frac{e^{z_i}}{\sum_j e^{z_j}}$
  • 交叉熵:衡量预测概率分布与真实分布的差异:$H(p,q) = -\sum p_i \log q_i$

PyTorch的nn.CrossEntropyLoss实际上同时完成了softmax和交叉熵计算,其数学表达式为:

loss(x, class) = -log(exp(x[class]) / sum(exp(x[i]))) = -x[class] + log(sum(exp(x[i])))

这种实现方式在数值稳定性上优于分开计算softmax和交叉熵。

2. 权重参数的三种设置方式

类别不平衡是现实数据中的常见问题。weight参数允许我们为不同类别分配不同的损失权重,其核心公式变为:

$$ loss(x, class) = weight[class] \times (-x[class] + \log(\sum_j e^{x[j]})) $$

2.1 手动指定权重

最直接的方式是根据领域知识手动设置各类别权重。例如在医疗诊断场景中,罕见病类别的权重通常更高:

import torch import torch.nn as nn # 假设3分类问题:类别0、1、2的权重分别为1.0, 2.5, 1.8 weights = torch.tensor([1.0, 2.5, 1.8]) criterion = nn.CrossEntropyLoss(weight=weights) # 示例计算 logits = torch.tensor([[2.0, 1.0, 0.5], [0.5, 2.0, 1.5]]) # 2个样本的logits targets = torch.tensor([0, 1]) # 真实类别 loss = criterion(logits, targets) print(f"加权损失值: {loss.item():.4f}")

2.2 自动计算类别权重

更科学的方式是根据训练集统计自动计算权重,常用方法是反比于类别频率:

def calculate_weights(train_labels): class_counts = torch.bincount(train_labels) num_classes = len(class_counts) weights = 1. / (class_counts.float() / class_counts.float().sum()) return weights / weights.sum() * num_classes # 归一化 # 假设训练集中3个类别的样本数分别为100, 30, 20 train_labels = torch.cat([ torch.zeros(100), torch.ones(30), torch.full((20,), 2) ]).long() weights = calculate_weights(train_labels) print(f"自动计算权重: {weights}")

2.3 类别平衡权重

在极端不平衡场景下,可以使用更激进的平衡策略:

def balanced_weights(train_labels, beta=0.9): class_counts = torch.bincount(train_labels) effective_num = 1.0 - torch.pow(beta, class_counts) weights = (1.0 - beta) / effective_num return weights / weights.sum() * len(weights) weights = balanced_weights(train_labels) print(f"平衡权重: {weights}")

三种权重计算方式对比表:

方法类型优点缺点适用场景
手动指定灵活可控依赖领域知识类别重要性明确
自动计算数据驱动对极端少数类敏感一般不平衡数据
平衡权重强调少数类可能过度补偿极端不平衡数据

3. 标签平滑技术解析与实现

标签平滑(Label Smoothing)是一种正则化技术,通过"软化"one-hot编码来防止模型对标签的过度自信。其数学表达为:

$$ y' = (1 - \alpha) \times y + \alpha / K $$

其中$K$是类别数,$\alpha$是平滑系数。

3.1 基础实现

PyTorch 2.0+直接支持label_smoothing参数:

criterion = nn.CrossEntropyLoss(label_smoothing=0.1) # 等效手动实现 def smooth_one_hot(labels, alpha, num_classes): smoothed = torch.full((labels.size(0), num_classes), alpha / (num_classes-1)) smoothed.scatter_(1, labels.unsqueeze(1), 1.0 - alpha) return smoothed smoothed_targets = smooth_one_hot(targets, 0.1, 3)

3.2 与权重的组合使用

标签平滑可以与权重参数协同工作:

criterion = nn.CrossEntropyLoss( weight=torch.tensor([1.0, 2.0, 1.5]), label_smoothing=0.1 )

3.3 不同平滑系数的效果对比

我们通过一个简单的实验观察不同$\alpha$值的影响:

import matplotlib.pyplot as plt def test_smoothing_effects(logits, targets): alphas = [0, 0.05, 0.1, 0.2, 0.3] losses = [] for alpha in alphas: criterion = nn.CrossEntropyLoss(label_smoothing=alpha) loss = criterion(logits, targets) losses.append(loss.item()) plt.plot(alphas, losses, marker='o') plt.xlabel('Smoothing Alpha') plt.ylabel('Loss Value') plt.title('Label Smoothing Effect on Loss') plt.grid(True) plt.show() test_smoothing_effects(logits, targets)

典型情况下,损失值会随$\alpha$增大而先降后升,最优值通常在0.05-0.2之间。

4. 完整训练案例对比

现在我们实现一个完整的图像分类训练流程,对比不同参数配置的效果。使用CIFAR-10数据集,人为制造类别不平衡:

import torchvision from torch.utils.data import WeightedRandomSampler # 创建不平衡的CIFAR-10数据集 transform = torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize((0.5,0.5,0.5), (0.5,0.5,0.5)) ]) dataset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) # 人为制造不平衡:各类别样本数指数递减 class_counts = [5000] + [5000 // (2**i) for i in range(1,10)] print("各类别样本数:", class_counts) # 创建带权重的DataLoader weights = 1. / torch.tensor(class_counts, dtype=torch.float) samples_weights = weights[torch.tensor(dataset.targets)] sampler = WeightedRandomSampler(samples_weights, len(samples_weights)) train_loader = torch.utils.data.DataLoader(dataset, batch_size=64, sampler=sampler)

定义测试函数:

def train_and_evaluate(criterion, epochs=10): model = torchvision.models.resnet18(num_classes=10) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) train_losses = [] accuracies = [] for epoch in range(epochs): model.train() total_loss = 0 for inputs, labels in train_loader: optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() # 评估 model.eval() correct = 0 total = 0 with torch.no_grad(): for inputs, labels in test_loader: outputs = model(inputs) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() acc = correct / total train_losses.append(total_loss/len(train_loader)) accuracies.append(acc) print(f"Epoch {epoch+1}: Loss={train_losses[-1]:.4f}, Acc={acc:.4f}") return train_losses, accuracies

配置对比实验:

# 实验配置 configs = [ {"name": "Baseline", "criterion": nn.CrossEntropyLoss()}, {"name": "Weighted", "criterion": nn.CrossEntropyLoss(weight=weights)}, {"name": "Smoothing(0.1)", "criterion": nn.CrossEntropyLoss(label_smoothing=0.1)}, {"name": "Weighted+Smoothing", "criterion": nn.CrossEntropyLoss(weight=weights, label_smoothing=0.1)} ] # 运行实验 results = {} for config in configs: print(f"\nRunning {config['name']}...") losses, accs = train_and_evaluate(config["criterion"]) results[config["name"]] = {"loss": losses, "acc": accs}

可视化结果:

plt.figure(figsize=(12,5)) plt.subplot(1,2,1) for name, res in results.items(): plt.plot(res["loss"], label=name) plt.xlabel("Epoch") plt.ylabel("Training Loss") plt.legend() plt.subplot(1,2,2) for name, res in results.items(): plt.plot(res["acc"], label=name) plt.xlabel("Epoch") plt.ylabel("Test Accuracy") plt.legend() plt.tight_layout() plt.show()

典型实验结果会显示:

  • 加权方法能显著提升少数类的识别率
  • 标签平滑有助于提高最终准确率
  • 组合使用可能获得最佳平衡

5. 高级技巧与注意事项

5.1 权重归一化

当使用较大权重值时,建议进行归一化以避免梯度爆炸:

weights = weights / weights.sum() * len(weights)

5.2 动态权重调整

在训练过程中可以动态调整权重:

def dynamic_weight_adjustment(epoch, max_epoch): base_weights = torch.tensor([...]) # 基础权重 # 随训练进程逐渐降低权重影响 factor = 1.0 - min(epoch / max_epoch, 0.8) return base_weights * factor

5.3 标签平滑与MixUp的结合

MixUp数据增强与标签平滑有协同效应:

def mixup_data(x, y, alpha=0.2): lam = np.random.beta(alpha, alpha) batch_size = x.size(0) index = torch.randperm(batch_size) mixed_x = lam * x + (1 - lam) * x[index] y_a, y_b = y, y[index] return mixed_x, y_a, y_b, lam # 在训练循环中 inputs, targets_a, targets_b, lam = mixup_data(inputs, targets) outputs = model(inputs) loss = lam * criterion(outputs, targets_a) + (1 - lam) * criterion(outputs, targets_b)

5.4 类别权重与学习率

对于高权重类别,可以相应降低学习率:

optimizer = torch.optim.SGD([ {'params': model.base.parameters()}, {'params': model.classifier.parameters(), 'lr': 1e-3 * weights.mean()} ], lr=1e-3)