使用 PyTorch 训练的神经网络输出每个输入的平均值

neural network trained with PyTorch outputs the mean value for every input

我正在使用 PyTorch in order to get my neural network to recognize digits from the MNIST database

import torch
import torchvision

我想实现一个非常简单的设计,类似于 3Blue1Brown's video series about neural networks. The following design in particular achieved an error rate of 1.6% 中所示的设计。

class Net(torch.nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        self.layer1 = torch.nn.Linear(784, 800)
        self.layer2 = torch.nn.Linear(800, 10)

    def forward(self, x):
        x = torch.sigmoid(self.layer1(x))
        x = torch.sigmoid(self.layer2(x))
        return x

数据是使用 torchvision 收集的,并以小批量组织,每批包含 32 张图像。

batch_size = 32
training_set = torchvision.datasets.MNIST("./", download=True, transform=torchvision.transforms.ToTensor())
training_loader = torch.utils.data.DataLoader(training_set, batch_size=32)

我使用均方误差作为损失函数,使用学习率为 0.001 的随机梯度下降作为我的优化算法。

net = Net()
loss_function = torch.nn.MSELoss()
optimizer = torch.optim.SGD(net.parameters(), lr=0.001)

最后使用以下代码对网络进行训练和保存:

for images, labels in training_loader:
    optimizer.zero_grad()
    for i in range(batch_size):
        output = net(torch.flatten(images[i]))
        desired_output = torch.tensor([float(j == labels[i]) for j in range(10)])
        loss = loss_function(output, desired_output)
        loss.backward()
    optimizer.step()
torch.save(net.state_dict(), "./trained_net.pth")

但是,这里是一些测试图像的输出:

tensor([0.0978, 0.1225, 0.1018, 0.0961, 0.1022, 0.0885, 0.1007, 0.1077, 0.0994,
        0.1081], grad_fn=<SigmoidBackward>)
tensor([0.0978, 0.1180, 0.1001, 0.0929, 0.1006, 0.0893, 0.1010, 0.1051, 0.0978,
        0.1067], grad_fn=<SigmoidBackward>)
tensor([0.0981, 0.1227, 0.1018, 0.0970, 0.0979, 0.0908, 0.1001, 0.1092, 0.1011,
        0.1088], grad_fn=<SigmoidBackward>)
tensor([0.1061, 0.1149, 0.1037, 0.1001, 0.0957, 0.0919, 0.1044, 0.1022, 0.0997,
        0.1052], grad_fn=<SigmoidBackward>)
tensor([0.0996, 0.1137, 0.1005, 0.0947, 0.0977, 0.0916, 0.1048, 0.1109, 0.1013,
        0.1085], grad_fn=<SigmoidBackward>)
tensor([0.1008, 0.1154, 0.0986, 0.0996, 0.1031, 0.0952, 0.0995, 0.1063, 0.0982,
        0.1094], grad_fn=<SigmoidBackward>)
tensor([0.0972, 0.1235, 0.1013, 0.0984, 0.0974, 0.0907, 0.1032, 0.1075, 0.1001,
        0.1080], grad_fn=<SigmoidBackward>)
tensor([0.0929, 0.1258, 0.1016, 0.0978, 0.1006, 0.0889, 0.1001, 0.1068, 0.0986,
        0.1024], grad_fn=<SigmoidBackward>)
tensor([0.0982, 0.1207, 0.1040, 0.0990, 0.0999, 0.0910, 0.0980, 0.1051, 0.1039,
        0.1078], grad_fn=<SigmoidBackward>)

如您所见,网络似乎接近一种状态,其中每个输入的答案都是:

[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]

这个神经网络并不比猜测更好。我的设计或代码哪里出错了?

以下几点可能对您有用:

  • 乍一看你的模型没有学习,因为你的预测和随机猜测一样好。第一个举措是监控你的损失,这里你只有一个纪元。至少你可以根据看不见的数据评估你的模型:

    validation_set = torchvision.datasets.MNIST('./', 
        download=True, train=False, transform=T.ToTensor())
    
    validation_loader = DataLoader(validation_set, batch_size=32)
    
  • 您正在使用 MSE 损失(L2 范数)来训练 not the right tool for this kind of task. You could instead be using the negative log-likelihood. PyTorch offers nn.CrossEntropyLoss 的分类任务,其中包括一个模块中的 log-softmax 和负对数似然损失.可以通过添加以下内容来实现此更改:

    loss_function = nn.CrossEntropyLoss()
    

    并在应用 loss_function 时使用正确的目标形状(见下文)。由于损失函数将应用 log-softmax,因此您不应该模型输出上的激活函数。

  • 您正在使用 sigmoid 作为激活函数,中间非线性会更好地工作 ReLU (see related post)。 sigmoid 更适合二元分类任务。同样,由于我们使用的是 nn.CrossEntropyLoss,因此我们必须在 layer2.

    之后删除激活
    class Net(torch.nn.Module):
        def __init__(self):
            super(Net, self).__init__()
            self.flatten = nn.Flatten()
            self.layer1 = torch.nn.Linear(784, 800)
            self.layer2 = torch.nn.Linear(800, 10)
    
        def forward(self, x):
            x = self.flatten(x)
            x = torch.relu(self.layer1(x))
            x = self.layer2(x)
            return x
    
  • 一个不太重要的点是您可以推断整个批次的估计,而不是一次遍历每个批次一个元素。一个时期的典型训练循环如下所示:

    for images, labels in training_loader:
        optimizer.zero_grad()
        output = net(images)
        loss = loss_function(output, labels)
        loss.backward()
        optimizer.step()
    

通过这些类型的修改,您可以期望在一个 epoch 后获得大约 80% 的验证。