You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyTorch单隐藏层MLP无法训练问题排查求助

手动实现梯度下降训练MNIST MLP时损失停滞的排查建议

问题背景

手动实现梯度下降(不使用PyTorch内置优化器)训练单隐藏层MLP,针对MNIST数据集训练时,损失值始终维持在2.3左右,无法有效下降。相关代码如下:

import torch
import numpy as np
import torchvision.datasets as datasets
from torchvision.transforms import ToTensor

# download train and test set
train = datasets.MNIST(root='.data', train=True, download=True, transform=ToTensor())
test = datasets.MNIST(root='.data', train=False, download=True, transform=ToTensor())

# calculate mean and std
mean = train.data.double().mean()
std = train.data.double().std()
print(f"Mean: {mean}, std: {std}")

# standardize data
train.data = (train.data - mean) / std
test.data = (test.data - mean) / std

from torch.utils.data import DataLoader

train_loader1 = DataLoader(train, shuffle=True, batch_size=1)
train_loader2 = DataLoader(train, shuffle=True, batch_size=256)

device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Device: {device}")

import torch.nn as nn

class SimpleNN(nn.Module):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten() # flatten the 28x28 images to 784 features vectors
        self.in_to_hidden = nn.Linear(784, 784) # input to 784 neurons hidden layer
        self.hidden_to_out = nn.Linear(784, 10) # 784 to 10 neuron output layer
        self.softmax = nn.Softmax(dim=1) # make the output into a probability distribution
        
    def forward(self, x):
        activation_fn = nn.Tanh()
        x = self.flatten(x)
        x = activation_fn(self.in_to_hidden(x))
        x = self.softmax(self.hidden_to_out(x))
        return x
    
    def train_single_epoch(self, data_loader, loss_fn, lr, device):
        for x, y in data_loader:
            x, y = x.to(device), y.to(device)

            # calculate loss
            pred = self(x)
            loss = loss_fn(pred, y)

            # backpropagate error and update weights
            loss.backward()
            with torch.no_grad(): # the gradient mustn't be calculated for the weight updates
                for weights in self.parameters():
                    weights -= lr * weights.grad
                    weights.grad.zero_()

        print(f"loss: {loss.item()}")

net = SimpleNN().to(device)
for i in range(2):
    net.train_single_epoch(train_loader2, nn.CrossEntropyLoss(), 0.001, device)

关键排查点及修复方案

1. CrossEntropyLoss与Softmax的冲突

CrossEntropyLoss内部已经整合了LogSoftmax和NLLLoss的计算逻辑,额外添加输出层的Softmax会导致损失计算出现数值稳定性问题,进而引发梯度消失。
修复:移除self.softmax,让hidden_to_out输出的原始logits直接传入损失函数:

def forward(self, x):
    activation_fn = nn.Tanh()
    x = self.flatten(x)
    x = activation_fn(self.in_to_hidden(x))
    x = self.hidden_to_out(x)  # 直接输出logits
    return x

2. 数据标准化未生效

直接修改train.data和test.data的方式不会作用到DataLoader取出的数据上——ToTensor()已将数据转换为0-1的张量,而train.data是原始uint8格式数据,两者存储独立。
修复:用transforms.Normalize将标准化集成到预处理流程:

from torchvision import transforms

# 计算0-1范围内的均值和标准差
mean = train.data.double().mean() / 255
std = train.data.double().std() / 255

transform = transforms.Compose([
    ToTensor(),
    transforms.Normalize(mean=[mean], std=[std])
])

# 重新创建数据集
train = datasets.MNIST(root='.data', train=True, download=True, transform=transform)
test = datasets.MNIST(root='.data', train=False, download=True, transform=transform)

3. 梯度清零时机错误

在更新每个参数后立刻调用weights.grad.zero_(),会导致后续参数更新时使用的梯度已被清零,参数更新不完整。
修复:在每个batch的前向传播前统一清零所有梯度:

def train_single_epoch(self, data_loader, loss_fn, lr, device):
    for x, y in data_loader:
        x, y = x.to(device), y.to(device)
        
        # 先清零所有梯度
        self.zero_grad()
        
        # 前向传播计算损失
        pred = self(x)
        loss = loss_fn(pred, y)
        
        # 反向传播
        loss.backward()
        
        # 更新参数
        with torch.no_grad():
            for param in self.parameters():
                param -= lr * param.grad

4. 学习率与激活函数适配问题

Tanh激活函数容易出现饱和导致梯度消失,当前设置的学习率0.001偏小,不足以驱动参数更新。
修复:将学习率调整到0.01,或者替换为ReLU激活函数:

# 替换激活函数示例
def forward(self, x):
    activation_fn = nn.ReLU()  # 改用ReLU
    x = self.flatten(x)
    x = activation_fn(self.in_to_hidden(x))
    x = self.hidden_to_out(x)
    return x

5. 参数初始化优化

默认的Linear层初始化可能不适合Tanh激活,容易导致初始梯度过小。
修复:使用Xavier初始化适配Tanh:

def __init__(self):
    super().__init__()
    self.flatten = nn.Flatten()
    self.in_to_hidden = nn.Linear(784, 784)
    self.hidden_to_out = nn.Linear(784, 10)
    
    # Xavier初始化
    nn.init.xavier_uniform_(self.in_to_hidden.weight)
    nn.init.zeros_(self.in_to_hidden.bias)
    nn.init.xavier_uniform_(self.hidden_to_out.weight)
    nn.init.zeros_(self.hidden_to_out.bias)

内容的提问来源于stack exchange,提问作者A_Pumpkin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 21:30:51