从零实现NLL损失函数PyTorch实战图像分类任务刚接触机器学习的同学一定对负对数似然损失这个术语不陌生但真正理解它如何在实际代码中发挥作用的人却不多。今天我们不谈复杂的数学推导而是直接动手用PyTorch实现一个完整的分类任务让你亲眼看到NLLLoss是如何工作的。很多教程一上来就抛出NLL的数学公式让人望而生畏。其实理解一个概念最好的方式就是亲手实现它。我们将从数据加载开始一步步构建模型、定义损失函数直到完成训练循环。在这个过程中你会遇到几个常见的坑比如忘记添加LogSoftmax层或者混淆了NLLLoss和CrossEntropyLoss的区别——别担心我都会带你一一解决。1. 环境准备与数据加载首先确保你已经安装了最新版的PyTorch。如果你使用conda环境可以通过以下命令安装conda install pytorch torchvision -c pytorch我们将使用经典的MNIST手写数字数据集作为示例。这个数据集包含60,000张28x28像素的手写数字图像非常适合用来理解分类任务的基本原理。import torch from torchvision import datasets, transforms # 定义数据转换 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 加载训练集和测试集 train_dataset datasets.MNIST(./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(./data, trainFalse, transformtransform) # 创建数据加载器 train_loader torch.utils.data.DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader torch.utils.data.DataLoader(test_dataset, batch_size1000, shuffleTrue)提示MNIST数据集中的图像已经被标准化到0-1范围我们进一步使用均值0.1307和标准差0.3081进行归一化这有助于模型更快收敛。2. 构建神经网络模型接下来我们定义一个简单的卷积神经网络(CNN)来处理MNIST图像。虽然模型结构不是本文的重点但理解各层的作用对调试NLLLoss很有帮助。import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() self.conv1 nn.Conv2d(1, 10, kernel_size5) self.conv2 nn.Conv2d(10, 20, kernel_size5) self.fc1 nn.Linear(320, 50) self.fc2 nn.Linear(50, 10) def forward(self, x): x F.relu(F.max_pool2d(self.conv1(x), 2)) x F.relu(F.max_pool2d(self.conv2(x), 2)) x x.view(-1, 320) x F.relu(self.fc1(x)) x self.fc2(x) return F.log_softmax(x, dim1)注意模型最后一层的输出我们使用了F.log_softmax而不是普通的softmax。这是使用NLLLoss的关键前提——NLLLoss期望接收的是对数概率(log probabilities)而不是原始概率。3. 理解NLLLoss的工作原理现在来到核心部分负对数似然损失函数。在PyTorch中它由nn.NLLLoss类实现。让我们先看看它的数学本质假设我们的模型对某个样本的输出概率分布为[0.1, 0.8, 0.1]真实标签是1第二类。那么取正确类别的概率0.8计算其对数log(0.8) ≈ -0.2231取负值0.2231这就是NLLLoss的计算过程。当正确类别的预测概率越高损失值就越小。在代码中实现这一点非常简单model SimpleCNN() criterion nn.NLLLoss() optimizer torch.optim.SGD(model.parameters(), lr0.01, momentum0.5)注意常见的错误是忘记在模型最后添加LogSoftmax层或者错误地使用了普通的softmax。NLLLoss必须与LogSoftmax配合使用如果使用普通softmax会导致计算错误。4. 训练循环与结果分析让我们把前面准备好的组件组合起来实现完整的训练过程def train(epoch): model.train() for batch_idx, (data, target) in enumerate(train_loader): optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step() if batch_idx % 100 0: print(fTrain Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} f({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}) def test(): model.eval() test_loss 0 correct 0 with torch.no_grad(): for data, target in test_loader: output model(data) test_loss criterion(output, target).item() pred output.argmax(dim1, keepdimTrue) correct pred.eq(target.view_as(pred)).sum().item() test_loss / len(test_loader.dataset) print(f\nTest set: Average loss: {test_loss:.4f}, fAccuracy: {correct}/{len(test_loader.dataset)} f({100. * correct / len(test_loader.dataset):.0f}%)\n) for epoch in range(1, 10): train(epoch) test()运行这段代码你会看到类似下面的输出Train Epoch: 1 [0/60000 (0%)] Loss: 2.312423 Train Epoch: 1 [6400/60000 (11%)] Loss: 0.876543 ... Test set: Average loss: 0.0023, Accuracy: 9234/10000 (92%)5. NLLLoss与CrossEntropyLoss的关系很多初学者会困惑为什么PyTorch同时提供了NLLLoss和CrossEntropyLoss它们之间有什么区别实际上CrossEntropyLoss LogSoftmax NLLLoss。也就是说# 这两种方式是等价的 loss1 nn.CrossEntropyLoss()(model_output, target) # 等价于 log_probs F.log_softmax(model_output, dim1) loss2 nn.NLLLoss()(log_probs, target)那么为什么PyTorch要提供两种实现呢主要有两个原因灵活性有时你可能需要在LogSoftmax和NLLLoss之间插入其他操作历史原因这两个概念在数学上是分开的分开实现更符合理论定义在实际应用中如果你只是需要一个标准的分类损失函数直接使用CrossEntropyLoss更为方便。但理解NLLLoss的工作原理对于调试模型和实现自定义损失函数非常有帮助。6. 常见问题与调试技巧在使用NLLLoss时你可能会遇到以下几个典型问题问题1损失值出现负数这通常意味着你的模型输出没有经过LogSoftmax处理。NLLLoss期望输入是对数概率如果直接传入原始分数可能会计算出无意义的结果。问题2损失值下降但准确率不提高检查你的LogSoftmax是否应用在了正确的维度上。对于分类任务通常应该在最后一个维度(dim1)上应用。问题3损失值突然变成NaN这可能是由于数值不稳定导致的。尝试减小学习率添加梯度裁剪检查数据中是否有异常值# 梯度裁剪示例 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)7. 扩展应用自定义NLLLoss理解了基本原理后我们可以尝试实现自己的NLLLoss。这不仅能加深理解还能根据需要添加特殊功能class MyNLLLoss(nn.Module): def __init__(self): super(MyNLLLoss, self).__init__() def forward(self, input, target): # input是log probabilities # target是类别索引 loss -input[range(target.shape[0]), target].mean() return loss这个自定义实现与PyTorch内置的NLLLoss功能相同但代码更加透明。你可以在此基础上添加权重、忽略特定类别等功能。8. 实际项目中的最佳实践在真实项目中使用NLLLoss时有几个经验值得分享始终验证输入形状确保你的log probabilities和targets的形状匹配# log_probs形状应为[N, C]targets形状应为[N] assert log_probs.shape[0] targets.shape[0] assert log_probs.shape[1] num_classes考虑类别不平衡如果某些类别样本很少可以使用weight参数# 假设类别0的样本是类别1的2倍 weight torch.tensor([1.0, 2.0]) criterion nn.NLLLoss(weightweight)与LogSoftmax的配合确保只在训练时使用LogSoftmax推理时直接取argmax即可学习率调整NLLLoss对学习率比较敏感建议使用学习率调度器scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.1)在图像分类任务中经过适当调参使用NLLLoss的简单CNN模型在MNIST上可以达到98%以上的准确率。这证明了即使不依赖复杂的数学推导通过实践也能很好地理解和应用这一重要概念。
别再死记硬背NLL公式了!用PyTorch手把手带你复现一个分类任务(附完整代码)
发布时间:2026/5/23 17:14:03
从零实现NLL损失函数PyTorch实战图像分类任务刚接触机器学习的同学一定对负对数似然损失这个术语不陌生但真正理解它如何在实际代码中发挥作用的人却不多。今天我们不谈复杂的数学推导而是直接动手用PyTorch实现一个完整的分类任务让你亲眼看到NLLLoss是如何工作的。很多教程一上来就抛出NLL的数学公式让人望而生畏。其实理解一个概念最好的方式就是亲手实现它。我们将从数据加载开始一步步构建模型、定义损失函数直到完成训练循环。在这个过程中你会遇到几个常见的坑比如忘记添加LogSoftmax层或者混淆了NLLLoss和CrossEntropyLoss的区别——别担心我都会带你一一解决。1. 环境准备与数据加载首先确保你已经安装了最新版的PyTorch。如果你使用conda环境可以通过以下命令安装conda install pytorch torchvision -c pytorch我们将使用经典的MNIST手写数字数据集作为示例。这个数据集包含60,000张28x28像素的手写数字图像非常适合用来理解分类任务的基本原理。import torch from torchvision import datasets, transforms # 定义数据转换 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) # 加载训练集和测试集 train_dataset datasets.MNIST(./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(./data, trainFalse, transformtransform) # 创建数据加载器 train_loader torch.utils.data.DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader torch.utils.data.DataLoader(test_dataset, batch_size1000, shuffleTrue)提示MNIST数据集中的图像已经被标准化到0-1范围我们进一步使用均值0.1307和标准差0.3081进行归一化这有助于模型更快收敛。2. 构建神经网络模型接下来我们定义一个简单的卷积神经网络(CNN)来处理MNIST图像。虽然模型结构不是本文的重点但理解各层的作用对调试NLLLoss很有帮助。import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() self.conv1 nn.Conv2d(1, 10, kernel_size5) self.conv2 nn.Conv2d(10, 20, kernel_size5) self.fc1 nn.Linear(320, 50) self.fc2 nn.Linear(50, 10) def forward(self, x): x F.relu(F.max_pool2d(self.conv1(x), 2)) x F.relu(F.max_pool2d(self.conv2(x), 2)) x x.view(-1, 320) x F.relu(self.fc1(x)) x self.fc2(x) return F.log_softmax(x, dim1)注意模型最后一层的输出我们使用了F.log_softmax而不是普通的softmax。这是使用NLLLoss的关键前提——NLLLoss期望接收的是对数概率(log probabilities)而不是原始概率。3. 理解NLLLoss的工作原理现在来到核心部分负对数似然损失函数。在PyTorch中它由nn.NLLLoss类实现。让我们先看看它的数学本质假设我们的模型对某个样本的输出概率分布为[0.1, 0.8, 0.1]真实标签是1第二类。那么取正确类别的概率0.8计算其对数log(0.8) ≈ -0.2231取负值0.2231这就是NLLLoss的计算过程。当正确类别的预测概率越高损失值就越小。在代码中实现这一点非常简单model SimpleCNN() criterion nn.NLLLoss() optimizer torch.optim.SGD(model.parameters(), lr0.01, momentum0.5)注意常见的错误是忘记在模型最后添加LogSoftmax层或者错误地使用了普通的softmax。NLLLoss必须与LogSoftmax配合使用如果使用普通softmax会导致计算错误。4. 训练循环与结果分析让我们把前面准备好的组件组合起来实现完整的训练过程def train(epoch): model.train() for batch_idx, (data, target) in enumerate(train_loader): optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step() if batch_idx % 100 0: print(fTrain Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} f({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}) def test(): model.eval() test_loss 0 correct 0 with torch.no_grad(): for data, target in test_loader: output model(data) test_loss criterion(output, target).item() pred output.argmax(dim1, keepdimTrue) correct pred.eq(target.view_as(pred)).sum().item() test_loss / len(test_loader.dataset) print(f\nTest set: Average loss: {test_loss:.4f}, fAccuracy: {correct}/{len(test_loader.dataset)} f({100. * correct / len(test_loader.dataset):.0f}%)\n) for epoch in range(1, 10): train(epoch) test()运行这段代码你会看到类似下面的输出Train Epoch: 1 [0/60000 (0%)] Loss: 2.312423 Train Epoch: 1 [6400/60000 (11%)] Loss: 0.876543 ... Test set: Average loss: 0.0023, Accuracy: 9234/10000 (92%)5. NLLLoss与CrossEntropyLoss的关系很多初学者会困惑为什么PyTorch同时提供了NLLLoss和CrossEntropyLoss它们之间有什么区别实际上CrossEntropyLoss LogSoftmax NLLLoss。也就是说# 这两种方式是等价的 loss1 nn.CrossEntropyLoss()(model_output, target) # 等价于 log_probs F.log_softmax(model_output, dim1) loss2 nn.NLLLoss()(log_probs, target)那么为什么PyTorch要提供两种实现呢主要有两个原因灵活性有时你可能需要在LogSoftmax和NLLLoss之间插入其他操作历史原因这两个概念在数学上是分开的分开实现更符合理论定义在实际应用中如果你只是需要一个标准的分类损失函数直接使用CrossEntropyLoss更为方便。但理解NLLLoss的工作原理对于调试模型和实现自定义损失函数非常有帮助。6. 常见问题与调试技巧在使用NLLLoss时你可能会遇到以下几个典型问题问题1损失值出现负数这通常意味着你的模型输出没有经过LogSoftmax处理。NLLLoss期望输入是对数概率如果直接传入原始分数可能会计算出无意义的结果。问题2损失值下降但准确率不提高检查你的LogSoftmax是否应用在了正确的维度上。对于分类任务通常应该在最后一个维度(dim1)上应用。问题3损失值突然变成NaN这可能是由于数值不稳定导致的。尝试减小学习率添加梯度裁剪检查数据中是否有异常值# 梯度裁剪示例 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)7. 扩展应用自定义NLLLoss理解了基本原理后我们可以尝试实现自己的NLLLoss。这不仅能加深理解还能根据需要添加特殊功能class MyNLLLoss(nn.Module): def __init__(self): super(MyNLLLoss, self).__init__() def forward(self, input, target): # input是log probabilities # target是类别索引 loss -input[range(target.shape[0]), target].mean() return loss这个自定义实现与PyTorch内置的NLLLoss功能相同但代码更加透明。你可以在此基础上添加权重、忽略特定类别等功能。8. 实际项目中的最佳实践在真实项目中使用NLLLoss时有几个经验值得分享始终验证输入形状确保你的log probabilities和targets的形状匹配# log_probs形状应为[N, C]targets形状应为[N] assert log_probs.shape[0] targets.shape[0] assert log_probs.shape[1] num_classes考虑类别不平衡如果某些类别样本很少可以使用weight参数# 假设类别0的样本是类别1的2倍 weight torch.tensor([1.0, 2.0]) criterion nn.NLLLoss(weightweight)与LogSoftmax的配合确保只在训练时使用LogSoftmax推理时直接取argmax即可学习率调整NLLLoss对学习率比较敏感建议使用学习率调度器scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.1)在图像分类任务中经过适当调参使用NLLLoss的简单CNN模型在MNIST上可以达到98%以上的准确率。这证明了即使不依赖复杂的数学推导通过实践也能很好地理解和应用这一重要概念。