PyTorch实现MNIST手写数字识别笔记 # 自用

发布于:2024-12-22 ⋅ 阅读:(14) ⋅ 点赞:(0)

从回归到分类

首先来对回归与分类进行一定的区分。

回归用于估计一个连续值,回归模型的输出是一个单一的实数值,可能是标量,也可能是多维向量,输出的范围为自然区间R

回归模型的训练目标是最小化预测值和真实值之间的差异。通过优化(如梯度下降),不断调整模型参数,从而最小化损失。

分类用于预测一个离散类别,分类任务的目标是预测一个离散的类别标签,输出结果通常是一个多维向量,每个维度对应一个类别的预测结果(置信度)。通常,置信度通过softmax函数计算,将原始得分转换为概率。

输出中置信度最高的类别被选为模型的预测结果,通常使用 softmax 函数来归一化置信度为概率。

可以这样概况:回归是问“多少”,而分类是问“哪一个”。

Softmax

而我们要利用回归实现分类问题,首先要对类别进行一位有效编码。

假如有n个类别Y,真实的类别为第i个,那么y_i为1,其他的为0。即向量中仅有一个1,其余为0。

y = [y_1, y_2,...,y_n]^T\\ y_i =\begin{cases} 1\ &if\ i=\ y\\ 0\ &otherwise \end{cases}

确定好类别后,利用均方损失进行训练。

将使置信度最大的 i 作为预测值的下标。同时要使其置信度远大于其他的类,能够与其他的类之间有足够的区分度。

\hat y\ =\ {argmax}_i\ o_i\\\\o_y-o_i\ge \bigtriangleup (y,i)

最后输出匹配概率和为1的非负数向量,做指数运算以转化为非负数。

\hat y=softmax(o)\\ \\ \hat y_i=\frac{exp(o_i)}{\sum_kexp(o_k)}

由此得到了预测值y_hat,接下来我们将真实概率y与y_hat之间的区别作为损失。

损失函数

交叉熵常用来衡量两个概率的区别,并将其作为损失。

假如 p,q 是两个离散概率,各有 n 个元素,令其每个元素做如下运算再求和

H(p,q)=\sum_i-p_ilog(q_i) \\ \\ l(y,\hat y)=-\sum_iy_ilog\hat y_i=-log\hat y_y

y是独热编码的实际概率,y_hat是softmax输出的预测概率。

由于 y 为独热编码,仅有一个元素为1,其余都为0,所以 l() 可以简写。

即真实类别对应的预测概率的负对数。

其对于 神经网络未经过softmax的输出 的梯度是真实概率与预测概率的区别。

\partial_{o_{i}} l(\mathbf{y}, \hat{\mathbf{y}})=\operatorname{softmax}(\mathbf{o})_{i}-y_{i}

总结

softmax回归是一个多类分类模型,使用softmax操作子得到每个类的预测置信度,置信度每个值都是非负的,且和为1,利用交叉熵来衡量预测和标号的区别。

以下是利用d2l库实现的代码

import os
import torch
import torchvision
from torch import nn
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2l
from matplotlib import pyplot as plt

def train(net, train_iter, test_iter, num_epochs, lr, save=False, save_path="./net.pth"):
    optimizer = torch.optim.SGD(net.parameters(), lr=lr)
    loss = nn.CrossEntropyLoss()
    animator = d2l.Animator(xlabel='epoch', xlim=[1, num_epochs], legend=['train loss', 'train acc', 'test acc'])
    for epoch in range(num_epochs):
        timer = d2l.Timer()
        #
        net.train()
        metric = d2l.Accumulator(3)
        for X, y in train_iter:
            y_hat = net(X)
            l = loss(y_hat, y)
            optimizer.zero_grad()
            l.backward()
            optimizer.step()
            metric.add(l * X.shape[0], (y_hat.argmax(dim=1) == y).sum().item(), y.numel())
        train_l = metric[0] / metric[2]
        train_acc = metric[1] / metric[2]
        #
        net.eval()
        metric = d2l.Accumulator(2)
        for X, y in test_iter:
            metric.add((net(X).argmax(dim=1) == y).sum().item(), y.numel())
            # 正确的数量, 样本总数
        test_acc =  metric[0] / metric[1]
        #
        animator.add(epoch + 1, (train_l, train_acc, test_acc))
        print(f'train loss {train_l:.3f}, train acc {train_acc:.3f}, ' f'test acc {test_acc:.3f}', f'{timer.stop():.1f} sec')
    if(save):
        torch.save(net.state_dict(), save_path)
    plt.show()

def show(imgs, labels, preds, n=8):
    fig, axes = plt.subplots(1, n, figsize=(12, 12 // n))
    for i in range(n):
        axes[i].imshow(imgs[i].reshape(28, 28).numpy(), cmap='gray')
        axes[i].set_title(f"Label: {labels[i]}\nPred: {preds[i]}")
        axes[i].axis('off')
    plt.show()
    
def load_model(net, load_path="model.pth"):
    """加载模型参数"""
    if os.path.exists(load_path):
        net.load_state_dict(torch.load(load_path))
        net.eval()
        print(f"Model loaded from {load_path}")
    else:
        print(f"Model file not found at {load_path}")

if __name__ == "__main__":
    batch_size = 256
    lr = 0.1
    epochs = 10
    trans = transforms.Compose([transforms.ToTensor(), transforms.Normalize(mean=(0.5), std=(0.5))])
    train_iter = data.DataLoader(torchvision.datasets.MNIST(root="./MNIST",
                                                            train=True, transform=trans, download=False), batch_size=batch_size, shuffle=True )
    test_iter = data.DataLoader(torchvision.datasets.MNIST(root="./MNIST", 
                                                           train=False, transform=trans, download=False), batch_size=batch_size, shuffle=True )
    net_1 = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
    net_2 = nn.Sequential(nn.Flatten(), nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10))
    net_3 = nn.Sequential(nn.Flatten(), nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 16), nn.ReLU(), nn.Linear(16, 10))
    net_4 = nn.Sequential(nn.Flatten(), nn.Linear(784, 512), nn.ReLU(), nn.Linear(512, 128), nn.ReLU(), nn.Linear(128, 32), nn.ReLU(), nn.Linear(32, 10))
    
    train(net_3, train_iter, test_iter, epochs, lr)
    X, y = next(iter(test_iter))
    X, y = X[:8], y[:8]
    Logits = net_3(X) # 这是训练完成的模型未经过softmax的预测值
    prob = torch.nn.functional.softmax(Logits, dim=1) # 令其softmax
    preds = prob.argmax(dim=1).numpy() # 预测结果
    show(X, y.numpy(), preds)

以上代码中训练时无需显式使用softmax,CrossEntropyLoss已经内置了softmax处理。

在一个回归训练框架中,主要有以下参数以及步骤需要实现。

batch_size # 批量大小
lr # 学习率
num_epochs # 训练轮数
train_iter, test_iter # 训练与测试迭代器
net # 神经网络模型
optimizer # 优化器
loss # 损失函数
X, y # 读入的数据与真实结果
# 传入参数
# 开启训练模式
net.train()
# 计算预测值
y_hat = net(X)
# 计算损失
l = loss(y_hat, y)
# 清空梯度
optimizer.zero_grad()
# 反向传播损失
l.backward()
# 更新优化器
optimizer.step()