这是《动手学深度学习》(PyTorch版)(Dive-into-DL-PyTorch)的学习笔记,里面有一些代码是我自己拓展的。

其他笔记在专栏 深度学习 中。

这并不是简单的复制原文内容,而是加入了自己的理解,里面的代码几乎都有详细注释!!!

6 线性神经网络——softmax回归

6.1 softmax回归

6.1.1 概念

事实上,我们经常对分类感兴趣:不是问“多少”,而是问“哪一个”:

(1)硬性类别:即属于哪个类别;

(2)软性类别:即得到属于每个类别的概率。

一种表示分类数据的简单方法:**独热编码(one-hot encoding)**是一个向量,它的分量和类别一样多。

  • softmax回归有多个输入、多个输出(每个类别对应一个输出),需要和输出一样多的仿射函数(affine function)。 每个输出对应于它自己的仿射函数。
  • softmax回归是一个单层神经网络。
  • softmax回归的输出层是全连接层(每个输出取决于所有输入)。
  • softmax回归的输出仍然由输入特征的仿射变换决定,是一个线性模型。

6.1.2 softmax运算

(1)运算公式

我们必须保证在任何数据上的输出都是非负(对每个未归一化的预测求幂)的且总和为1(对每个求幂后的结果除以它们的总和):

(2)交叉熵(cross-entropy loss)损失

  • 交叉熵常用来衡量两个概率的区别 H ( p , q ) = ∑ i − p i log ⁡ ( q i )
  • 将它作为损失
    l(y,y^)=j=1qyjlogexp(oj)k=1qexp(ok)=j=1qyjlogk=1qexp(ok)j=1qyjoj=logk=1qexp(ok)j=1qyjoj
  • 其梯度是真实概率和预测概率的区别

拓展:几种常见的损失函数


当预测值与实际值相差较大时,是绝对值误差;相差较小时,是均方误差。

6.2 图像分类数据集(Fashion-MNIST)

图像分类数据集中最常用的是手写数字识别数据集MNIST。但大部分模型在MNIST上的分类精度都超过了95%。为了更直观地观察算法之间的差异,我们将使用一个图像内容更加复杂的数据集Fashion-MNIST(这个数据集也比较小,只有几十M,没有GPU的电脑也能吃得消)。

我们先引入一个多类图像分类数据集,以方便我们观察比较算法之间在模型精度和计算效率上的区别。

本节我们将使用torchvision包,它是服务于PyTorch深度学习框架的,主要用来构建计算机视觉模型。torchvision主要由以下几部分构成:

  1. torchvision.datasets: 一些加载数据的函数及常用的数据集接口;
  2. torchvision.models: 包含常用的模型结构(含预训练模型),例如AlexNet、VGG、ResNet等;
  3. torchvision.transforms: 常用的图片变换,例如裁剪、旋转等;
  4. torchvision.utils: 其他的一些有用的方法。

首先导入需要的包或模块。

%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2l

d2l.use_svg_display()  #使用svg来显示图片,清晰度更高

6.2.1 读取数据集

通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中。

下面的代码中:

  • transforms.ToTensor()将尺寸为 (H x W x C) 且数据位于[0, 255]的PIL图片或者数据类型为np.uint8的NumPy数组转换为尺寸为(C x H x W)且数据类型为torch.float32且位于[0.0, 1.0]的Tensor。
  • mnist_train和mnist_test都是torch.utils.data.Dataset的子类,所以我们可以用len()来获取该数据集的大小,还可以用下标来获取具体的一个样本。
  • 从torchvision.datasets里面把FashionMNIST拿出来下载到root中。
  • train=True表示下载的是训练数据集;train=False表示下载的是测试集。
  • transform=trans表示拿出来的是对应的PyTorch的数据集,而不是一堆图片。
  • download=True表示默认从网上下载(也可以事先下载好放在data中,就不用这个命令了)。

输出的结果(60000, 10000)表示mnist_train大概需要60000个图片,mnist_test大概需要10000个图片(训练集中和测试集中的每个类别的图像数分别为6,000和1,000。因为有10个类别,所以训练集和测试集的样本数分别为60,000和10,000)。

trans = transforms.ToTensor()  #把图片转成Tensor,最简单的一个预处理,如果不进行转换则返回的是PIL图片
mnist_train = torchvision.datasets.FashionMNIST(
    root="C:/Users/xinyu/Desktop/myjupyter/data", train=True, transform=trans, download=True)  #训练数据集
mnist_test = torchvision.datasets.FashionMNIST(
    root="C:/Users/xinyu/Desktop/myjupyter/data", train=False, transform=trans, download=True)  
#测试数据集,只用来评价模型的表现,并不用来训练模型

len(mnist_train), len(mnist_test)
(60000, 10000)
mnist_train[0][0].shape  
torch.Size([1, 28, 28])

Fashion-MNIST数据集同时包含图形和标签,第一个[0]表示第0个example(图片数据),第二个[0]表示取图片,如果是[1]则是取标签。

上面的结果表示:

  • 首先这个图片是一个torch的tensor
  • 尺寸是 (C × H × W) 的,第一维是通道数,后面两维分别是图像的高和宽
  • 数据集由灰度图像组成,其通道数为1
  • 每个输入图像的高度和宽度均为28像素

Fashion-MNIST中包含的10个类别分别为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。以下函数用于在数字标签索引及其文本名称之间进行转换。

def get_fashion_mnist_labels(labels):
    text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat', 'sanda', 'shirt', 'sneaker', 'bag', 'ankle boot']
    return [text_labels[int(i)] for i in labels]

'''下面定义一个可以在一行里画出多张图像和对应标签的函数(可视化样本)'''
def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):
    figsize = (num_cols * scale, num_rows * scale)
    _, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize)  #这里的_表示我们忽略(不使用)的变量
    axes = axes.flatten()
    for i, (ax, img) in enumerate(zip(axes, imgs)):
        if torch.is_tensor(img):  #图片张量
            ax.imshow(img.numpy())
        else:  #PIL图片
            ax.imshow(img)
        ax.axes.get_xaxis().set_visible(False)
        ax.axes.get_yaxis().set_visible(False)
        if titles:
            ax.set_title(titles[i])
    return axes

X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))  #next表示拿到第一个小批量;用iter构造出一个iterate(迭代)
show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y))  #18个样本,2行9列排布,宽和高为28(这里不能擅自改动宽高)

array([<AxesSubplot:title={'center':'ankle boot'}>,
       <AxesSubplot:title={'center':'t-shirt'}>,
       <AxesSubplot:title={'center':'t-shirt'}>,
       <AxesSubplot:title={'center':'dress'}>,
       <AxesSubplot:title={'center':'t-shirt'}>,
       <AxesSubplot:title={'center':'pullover'}>,
       <AxesSubplot:title={'center':'sneaker'}>,
       <AxesSubplot:title={'center':'pullover'}>,
       <AxesSubplot:title={'center':'sanda'}>,
       <AxesSubplot:title={'center':'sanda'}>,
       <AxesSubplot:title={'center':'t-shirt'}>,
       <AxesSubplot:title={'center':'ankle boot'}>,
       <AxesSubplot:title={'center':'sanda'}>,
       <AxesSubplot:title={'center':'sanda'}>,
       <AxesSubplot:title={'center':'sneaker'}>,
       <AxesSubplot:title={'center':'ankle boot'}>,
       <AxesSubplot:title={'center':'trouser'}>,
       <AxesSubplot:title={'center':'t-shirt'}>], dtype=object)
 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZXJ44zMW-1637287819985)(output_6_1.svg)]

6.2.2 读取小批量

使用内置的数据迭代器读取一小批量数据,大小为batch_size。

我们将在训练数据集上训练模型,并将训练好的模型在测试数据集上评价模型的表现。前面说过,mnist_train是torch.utils.data.Dataset的子类,所以我们可以将其传入torch.utils.data.DataLoader来创建一个读取小批量数据样本的DataLoader实例。、

在实践中,数据读取经常是训练的性能瓶颈,特别当模型较简单或者计算硬件性能较高时。PyTorch的DataLoader中一个很方便的功能是允许使用多进程来加速数据读取。这里我们通过参数num_workers来设置4个进程读取数据。

batch_size = 256

num_workers = 4  #使用4个进程来读取数据。一般来说图片可能是放在硬盘上(现在我们的已经在内存中),需要多个进程来读取数据。

train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True, num_workers=get_dataloader_workers())

timer = d2l.Timer()
for X, y in train_iter:
    continue
f'{timer.stop():.2f} sec'
'3.19 sec'

6.2.3 整合所有组件

现在我们定义load_data_fashion_mnist函数,用于获取和读取Fashion-MNIST数据集。它返回训练集和验证集的数据迭代器。此外,它还接受一个可选参数,用来将图像大小调整为另一种形状。

def load_data_fashion_mnist(batch_size, resize=None):  #@save
    #resize表示是否更改宽高
    """下载Fashion-MNIST数据集,然后将其加载到内存中。"""
    trans = [transforms.ToTensor()]
    if resize:
        trans.insert(0, transforms.Resize(resize))
    trans = transforms.Compose(trans)
    mnist_train = torchvision.datasets.FashionMNIST(
        root="C:/Users/xinyu/Desktop/myjupyter/data", train=True, transform=trans, download=True)
    mnist_test = torchvision.datasets.FashionMNIST(
        root="C:/Users/xinyu/Desktop/myjupyter/data", train=False, transform=trans, download=True)
    return (data.DataLoader(mnist_train, batch_size, shuffle=True,
                            num_workers=get_dataloader_workers()),
            data.DataLoader(mnist_test, batch_size, shuffle=False,
                            num_workers=get_dataloader_workers()))

'''我们通过指定resize参数来测试load_data_fashion_mnist函数的图像大小调整功能'''
train_iter, test_iter = load_data_fashion_mnist(32, resize=64)
for X, y in train_iter:
    print(X.shape, X.dtype, y.shape, y.dtype)
    break
torch.Size([32, 1, 64, 64]) torch.float32 torch.Size([32]) torch.int64

6.3 softmax回归的从零开始实现

6.3.1 获取和读取数据

import torch
from IPython import display
from d2l import torch as d2l

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)  #每一次随机读256张图片返回训练集和测试集的迭代器

6.3.2 初始化模型参数

把图像拉长成向量。每个样本输入是高和宽均为28像素的图像,模型的输入向量的长度是 28×28=784:该向量的每个元素对应图像中每个像素。

由于图像有10个类别,单层神经网络输出层的输出个数为10,因此softmax回归的权重和偏差参数分别为784×10和1×10的矩阵。

num_inputs = 784  
num_outputs = 10

W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)  
#权重,初始化为均值为0,方差为0.01的高斯随机分布,形状是784×10
b = torch.zeros(num_outputs, requires_grad=True)  #偏移,形状是1×10

6.3.3 实现softmax运算

对多维Tensor按维度操作:给定一个Tensor矩阵X。我们可以只对其中同一列(dim=0)或同一行(dim=1)的元素求和,并在结果中保留行和列这两个维度(keepdim=True)。

X = torch.tensor([[1, 2, 3], 
                  [4, 5, 6]])
X.sum(0, keepdim=True), X.sum(1, keepdim=True)
#这是一个2×3矩阵,其中,sum后面接0表示把X中的第0个元素化为1,即X为1×3矩阵;sum后面接1表示把X中的第1个元素化为1,即X为2×1矩阵
(tensor([[5, 7, 9]]),
 tensor([[ 6],
         [15]]))

实现softmax

def softmax(X):
    X_exp = torch.exp(X)
    partition = X_exp.sum(1, keepdim=True)
    return X_exp / partition  #这里应用了广播机制

'''验证一下softmax函数'''
X = torch.normal(0, 1, (2, 5))  #创建一个均值为0,方差为1的2×5的矩阵
X_prob = softmax(X)
X_prob, X_prob.sum(1), X_prob.sum(1, keepdim=True)
(tensor([[0.0784, 0.4247, 0.3255, 0.0969, 0.0746],
         [0.1576, 0.1277, 0.2073, 0.1265, 0.3810]]),
 tensor([1.0000, 1.0000]),
 tensor([[1.0000],
         [1.0000]]))

6.3.4 实现softmax回归模型

def net(X):
    return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)  
#X是一个n×d(批量大小×输入数量)的矩阵,上面的-1对应多少个样本(即batch_size), W.shape[0]就是784

6.3.5 计算损失函数

得到标签的预测概率:

y = torch.tensor([0, 2])  #表示2个样本(样本序号是0、1)的标签类别:第0类和第2类.torch.Size([2])
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])  #这2个样本在3个类别(第0、1、2类)的预测概率,torch.Size([2, 3])
y_hat[[0, 1], y]  #取出第0、1个样本的预测概率。y[0]=0,所以取出0.1;y[1]=2,所以取出0.5
                  #y_hat[0, y[0]]=tensor(0.1000)  y_hat[1, y[1]]=tensor(0.5000)
                  #上面相当于取出y_hat的第0行,第y[0]列。y_hat[[0, 1], y]就是把这两个索引合起来 
tensor([0.1000, 0.5000])

实现交叉熵损失函数:

def cross_entropy(y_hat, y):
    return -torch.log(y_hat[range(len(y_hat)), y])  #与上面的y_hat[[0, 1], y]类似,取出对于真实标号的概率

cross_entropy(y_hat, y)
tensor([2.3026, 0.6931])

6.3.6 计算分类准确率

给定一个类别的预测概率分布y_hat,我们把预测概率最大的类别作为输出类别。如果它与真实类别y一致,说明这次预测是正确的。分类准确率即正确预测数量与总预测数量之比。下面的代码中:

  • y_hat.argmax(dim=1)返回矩阵y_hat每行中最大元素的索引,且返回结果与变量y形状相同。
  • 相等条件判断式(y_hat.argmax(dim=1) == y)是一个类型为ByteTensor的Tensor,我们用float()将其转换为值为0(相等为假)或1(相等为真)的浮点型Tensor。
def accuracy(y_hat, y):  #@save
    return (y_hat.argmax(dim=1) == y).float().mean().item()

print(accuracy(y_hat, y))  
0.5

上面,我们预测的第0、1个样本分别是第2类和第2类,实际上,是第0类和第2类,也就是50%的准确率,所以结果是0.5。

类似地,我们可以评价模型net在数据迭代器data_iter上的准确率。

def evaluate_accuracy(data_iter, net):  #@save
    acc_sum, n = 0.0, 0
    for X, y in data_iter:
        acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
        n += y.shape[0]
    return acc_sum / n

print(evaluate_accuracy(test_iter, net))
0.0338
 

6.3.7 训练模型

训练softmax回归的实现跟线性回归的从零开始实现一节介绍的线性回归中的实现非常相似。我们同样使用小批量随机梯度下降来优化模型的损失函数。在训练模型时,迭代周期数num_epochs和学习率lr都是可以调的超参数。改变它们的值可能会得到分类更准确的模型。

num_epochs, lr = 5, 0.1

def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
              params=None, lr=None, optimizer=None):  #@save
    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X, y in train_iter:
            y_hat = net(X)
            l = loss(y_hat, y).sum()

            # 梯度清零
            if optimizer is not None:
                optimizer.zero_grad()
            elif params is not None and params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()

            l.backward()
            if optimizer is None:
                d2l.sgd(params, lr, batch_size)
            else:
                optimizer.step()  # “softmax回归的简洁实现”一节将用到


            train_l_sum += l.item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
            n += y.shape[0]
        test_acc = evaluate_accuracy(test_iter, net)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
              % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))

train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)

epoch 1, loss 0.7865, train acc 0.749, test acc 0.788
epoch 2, loss 0.5697, train acc 0.814, test acc 0.812
epoch 3, loss 0.5261, train acc 0.826, test acc 0.820
epoch 4, loss 0.5019, train acc 0.832, test acc 0.825
epoch 5, loss 0.4847, train acc 0.837, test acc 0.828

6.3.8 预测

X, y = iter(test_iter).next()

trues = d2l.get_fashion_mnist_labels(y)
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
titles = [true +'\n' + pred for true, pred in zip(trues, preds)]

n = 6
d2l.show_images(X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])
array([<AxesSubplot:title={'center':'ankle boot\nankle boot'}>,
       <AxesSubplot:title={'center':'pullover\npullover'}>,
       <AxesSubplot:title={'center':'trouser\ntrouser'}>,
       <AxesSubplot:title={'center':'trouser\ntrouser'}>,
       <AxesSubplot:title={'center':'shirt\nshirt'}>,
       <AxesSubplot:title={'center':'trouser\ntrouser'}>], dtype=object)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J09N4ce1-1637287819990)(output_32_1.svg)]

6.4 softmax回归的简洁实现

import torch
from torch import nn
from d2l import torch as d2l


batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
# Flatten()是把任意维度的tensor转换为二维的,保留第0维度,剩下的全部展成一个向量
# nn.Linear(784, 10)定义线性层,输入是784,输出是10
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))
 
def init_weights(m):  #生成W
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights)
Sequential(
  (0): Flatten(start_dim=1, end_dim=-1)
  (1): Linear(in_features=784, out_features=10, bias=True)
)
loss = nn.CrossEntropyLoss()
trainer = torch.optim.SGD(net.parameters(), lr=0.1)

num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I7GXkAuB-1637287819994)(output_35_0.svg)]