动手学深度学习pytorch版----3线性神经网络

3.1 线性回归

回归是能为一个或多个自变量与因变量之间关系建模的一类方法.

3.1.1 线性回归的基本元素

假设自变量x和因变量y之间的关系是线性的,即y可以表示为x中元素的加权和,这里通常允许包含观测值的一些噪音;假设噪音遵循正态分布。

以房屋销售为例,我们希望根据房屋的面积(平方英尺)和房龄(年)来估算房屋价格(美元)。为了开发一个能预测房价的模型,我们需要收集一个真实的数据集。 这个数据集包括了房屋的销售价格、面积和房龄。 在机器学习的术语中,该数据集称为训练数据集 或训练集。 每行数据(比如一次房屋交易相对应的数据)称为样本(sample), 也可以称为数据点数据样本。 我们把试图预测的目标(比如预测房屋价格)称为标签(label)或目标(target)。 预测所依据的自变量(面积和房龄)称为特征(feature)或协变量(covariate)。

线性假设是指目标(房屋价格)可以表示为特征(面积和房龄)的加权和。如下面的式子:

w_{area}w_{age}称为权重。权重决定了每个特征对预测值的影响。 b称为偏置、偏移量或截距。偏置指当所有特征都取值0时,预测值该为多少。如果没有偏置项,我们模型的表达能力将受到限制。上式是输入特征的一个仿射变换。仿射变换的特点是通过加权和对特征进行线性变换,并通过偏执项来平移。输出的预测值由输入特征通过线性模型的仿射变换决定,仿射变换由所选权重偏置确定。

当输入包含d个特征时,将预测结果y^(y的估计值)表示为:
 

将所有特征放到向量x(d维)中,权重放到w(d维),可以用点积形式简洁地表达模型。:

其中,向量x对应单个数据样本的特征,用矩阵X(n×d)可以方便地引用整个数据集n个样本。其中X每一行是一个样本,每一列是一种特征。用矩阵-向量乘法表示为:

此处求和会使用广播机制。线性回归的目标是找到一组权重向量w和偏置b: 当给定从X的同分布中取样的新样本特征时, 这组权重向量和偏置能够使得新样本预测标签的误差尽可能小。 

虽然我们相信给定x预测y的最佳模型会是线性的,但很难找到一个有n个样本的真实数据集,其中对于所有的1≤i≤n,yi完全等于wTxi + b。无论我们使用什么手段来观察特征X和标签y, 都可能会出现少量的观测误差。 因此,即使确信特征与标签的潜在关系是线性的, 我们也会加入一个噪声项来考虑观测误差带来的影响。

在寻找最好的模型参数w和b之前,还需要两个东西:一种模型质量的度量方式,一种能够更新模型以提高模型预测质量的方法。

在开始考虑如何用模型拟合数据之前,我们需要确定一个拟合程度的度量。损失函数能够量化目标的实际值预测值之间的差距,回归问题最常用的损失函数是平方误差函数。当样本 i 的预测值为y^(i),其相应的真实标签为y^(i)时,平方误差可以定义为:

由于平方误差函数中的二次方项, 估计值𝑦^(𝑖)和观测值𝑦(𝑖)之间较大的差异将导致更大的损失。 为了度量模型在整个数据集上的质量,我们需计算在训练集𝑛个样本上的损失均值(也等价于求和)。 

在训练模型时,我们希望寻找一组参数(𝐰∗,𝑏∗), 这组参数能最小化在所有训练样本上的总损失。如下式: 

线性回归的解可以用一个公式简单地表达出来,这类解叫作解析解。

梯度下降几乎可以优化所有深度学习模型。它通过不断在损失函数递减的方向上更新参数来降低误差。

梯度下降最简单的用法是计算损失函数(数据集中所有样本的损失均值) 关于模型参数的导数(在这里也可以称为梯度)。 但实际中的执行可能会非常慢:因为在每一次更新参数之前,我们必须遍历整个数据集。 因此,我们通常会在每次需要计算更新的时候随机抽取一小批样本, 这种变体叫做小批量随机梯度下降 。

在每次迭代中,首先随机抽样一个小批量β,它由固定数量的训练样本组成,然后计算小批量的平均损失关于模型参数的导数,最后将梯度乘以一个预先确定的正数η,并从当前参数的值中减掉。

 算法的步骤总结:(1)初始化模型参数的值,如随机初始化;(2)从数据集中随机抽取小批量样本且在负梯度的方向上更新参数,并不断迭代这一步骤。 对于平方损失和仿射变换,我们可以明确地写成如下形式:

其中𝐰和𝐱都是向量,丨β丨表示每个小批量中的样本数,这也称为批量大小(batch size)。 𝜂表示学习率(learning rate)。 批量大小和学习率的值通常是手动预先指定,而不是通过模型训练得到的。 些可以调整但不在训练过程中更新的参数称为超参数超参数通常是我们根据训练迭代结果来调整的, 而训练迭代结果是在独立的验证数据集上评估得到的。

线性回归恰好是一个在整个域只有一个最小值的学习问题。 但是对像深度神经网络这样复杂的模型来说,损失平面上通常包含多个最小值。事实上,更难做到的是找到一组参数,这组参数能够在我们从未见过的数据上实现较低的损失, 这一挑战被称为泛化(generalization)。

给定已学习的线性回归模型w^x + b,可以通过房屋面积、房龄来估计房价。给定特征估计目标的过程称为预测。

3.1.2 矢量化加速

在训练模型的时候,经常希望能够同时处理整个小批量的样本,为了实现这一点,需要对计算进行矢量化。

3.1.3 正态分布与平方损失

正态分布也称为高斯分布,若随机变量x具有均值μ和方差\sigma ^{2},其正态分布概率密度函数如下:

下面定义一个Python函数来计算正态分布。

def normal(x, mu, sigma):
    p = 1 / math.sqrt(2 * math.pi * sigma**2)
    return p * np.exp(-0.5 / sigma**2 * (x - mu)**2)

 现在可视化正态分布。

# 再次使用numpy进行可视化
x = np.arange(-7, 7, 0.01)

# 均值和标准差对
params = [(0, 1), (0, 2), (3, 1)]
d2l.plot(x, [normal(x, mu, sigma) for mu, sigma in params], xlabel='x',
         ylabel='p(x)', figsize=(4.5, 2.5),
         legend=[f'mean {mu}, std {sigma}' for mu, sigma in params])

如下图所示,改变均值会产生沿𝑥轴的偏移,增加方差将会分散分布、降低其峰值。 

均方误差可以用于线性回归的一个原因是:我们假设了观测中包含噪声,其中噪声服从正态分布,噪声正态分布如下式:

其中,\epsilon \sim N(0,\sigma ^{2})。

在高斯噪声的假设下,最小化均方误差等价于对线性模型的极大似然估计。

3.1.4 从线性回归到深度网络

将线性回归模型描述为一个神经网络,隐去了权重和偏置的值。如下图所示:

 在上图所示的神经网络中,输入层的输入数是d,输出层中的输出数是1。输入值都是已经给定的,并且只有一个计算神经元。 由于模型重点在发生计算的地方,所以通常我们在计算层数时不考虑输入层。即上图的神经网络的层数为1。可以将线性回归模型视为仅由单个人工神经元组成的神经网络,或称为单层神经网络

对于线性回归,每个输入都与每个输出(在本例中只有一个输出)相连, 我们将这种变换称为全连接层(fully-connected layer)。

3.2 线性回归的从零开始实现

%matplotlib inline
import random
import torch
from d2l import torch as d2l

3.2.1 生成数据集 

根据带有噪声的线性模型构造一个人造数据集,我们的任务是使用这样有限样本的数据集,来恢复这个模型的参数。使用低维数据,很容易将其可视化。下面的代码生成一个包含1000个样本的数据集,没有样本包含从标准正态分布采样的2个特征。合成数据集是一个矩阵X(1000×2)

使用线性模型参数w=[2, -3, 4]T、b=4.2和噪声项ε生成数据集及其标签。

y = Xw + b + ε

ε视为模型预测和标签时的潜在观测误差。其服从均值为0的正态分布。将标准差设为0.01。下面的代码生成合成数据集。

def synthetic_data(w, b, num_examples):  #@save
    """生成y=Xw+b+噪声"""
    X = torch.normal(0, 1, (num_examples, len(w)))
    y = torch.matmul(X, w) + b
    y += torch.normal(0, 0.01, y.shape) # 添加随机噪声
    return X, y.reshape((-1, 1))

true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)

其中,features每行都包含一个二维数据样本,labels每行都包含一维标签值(一个标量)。

通过生成features的第二个特征features[:, 1](即所有行的第二列)和labels的散点图,可以直观观察到两者之间的线性关系。

d2l.set_figsize()
d2l.plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(), 1);
# torch有时需要从计算图中分离张量才能转换为Numpy数组
# 1表示图中点的大小

3.2.2 读取数据集 

需要定义一个函数能打乱数据集中的样本并以小批量方式获取数据。下面的代码定义一个data_iter函数,该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量。每个小批量包含一组特征和标签。

def data_iter(batch_size, features, labels):
    num_examples = len(features) # 行数,即所有的样本数量
    indices = list(range(num_examples))
    # 这些样本是随机读取的,没有特定的顺序
    random.shuffle(indices) # 打乱顺序
    for i in range(0, num_examples, batch_size): #0~batch_size-1,batch_size~....
        batch_indices = torch.tensor(
            indices[i: min(i + batch_size, num_examples)]) # 确保不会超过样本总数
        yield features[batch_indices], labels[batch_indices] # yield:返回迭代

通常,我们利用GPU并行运算的优势,处理合理大小的“小批量”。 每个样本都可以并行地进行模型计算,且每个样本损失函数的梯度也可以被并行计算。 

我们直观感受一下小批量运算:读取第一个小批量数据样本并打印。 每个批量的特征维度显示批量大小和输入特征数。 同样的,批量的标签形状与batch_size相等。

batch_size = 10

for X, y in data_iter(batch_size, features, labels):
    print(X, '\n', y)
    break
"""
tensor([[-1.2699, -0.0260],
        [-0.6117, -1.1171],
        [-0.1993,  0.3642],
        [-0.4954, -0.7317],
        [-0.6444,  0.1183],
        [ 0.0773, -0.6468],
        [-1.3608,  2.6203],
        [-0.8681,  0.2850],
        [ 0.5292,  0.7425],
        [-0.7476,  0.2808]]) 
 tensor([[ 1.7417],
        [ 6.7844],
        [ 2.5704],
        [ 5.6983],
        [ 2.5157],
        [ 6.5454],
        [-7.4277],
        [ 1.4845],
        [ 2.7418],
        [ 1.7358]])
"""

运行迭代时,会连续地获得不同的小批量,直至遍历完整个数据集。

3.2.3 初始化模型参数

使用小批量随机梯度下降优化模型参数之前,需要先有一些参数,下面的代码进行权重初始化,并将偏置初始化为0.

w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)

之后,我们的任务是更新这些参数,直至它们足够拟合我们的数据。每次更新都需要计算损失函数关于模型参数的梯度,根据这个梯度,向减小损失函数的方向更新参数。通过使用自动微分来计算梯度。

3.2.4 定义模型

定义模型将输入和参数同模型的输出关联起来。

def linreg(X, w, b):  #@save
    """线性回归模型"""
    return torch.matmul(X, w) + b

3.2.5 定义损失函数

使用平方损失函数。其中真实值为y,预测值为y_hat。要使它们的形状相同。

def squared_loss(y_hat, y):  #@save
    """均方损失"""
    return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2

3.2.6 定义优化算法

小批量随机梯度下降,在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。 接下来,朝着减少损失的方向更新我们的参数。 下面的函数实现小批量随机梯度下降更新。函数接受模型参数集合学习速率批量大小作为输入。每 一步更新的大小由学习速率 lr 决定。因为我们计算的损失是一个批量样本的总和,所以我们用批量大小batch_size来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。

# params保存参数列表(权重, 偏置)
def sgd(params, lr, batch_size):  #@save
    """小批量随机梯度下降"""
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()

3.2.7 训练

在每一次迭代中,读取一小批量训练样本并通过模型来获得一组预测。计算完损失后,开始反向传播,存储每个参数的梯度。最后调用sgd来更新参数。

概括一下,我们将执行以下循环:

1.初始化参数

2.重复以下训练,直到完成。

在每个迭代周期(epoch),使用data_iter函数遍历整个数据集,将所有样本都使用一次。此处的迭代周期格式num_epochs和学习率lr都是超参数,分布设为3和0.03。设置超参数需要通过反复试验进行调整,现在暂时忽略这些细节。

lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss

for epoch in range(num_epochs):
    for X, y in data_iter(batch_size, features, labels):
        l = loss(net(X, w, b), y)  # X和y的小批量损失
        # 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
        # 并以此计算关于[w,b]的梯度
        l.sum().backward()
        sgd([w, b], lr, batch_size)  # 使用参数的梯度更新参数
    with torch.no_grad():
        train_l = loss(net(features, w, b), labels)
        print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
"""
epoch 1, loss 0.038757
epoch 2, loss 0.000153
epoch 3, loss 0.000051
"""

由于使用的是自己合成的数据集,所以我们知道真正的参数是什么,现在通过比较真实参数和通过训练得到的参数来评估训练的成功程度。

print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')

"""
w的估计误差: tensor([ 0.0008, -0.0008], grad_fn=<SubBackward0>)
b的估计误差: tensor([0.0009], grad_fn=<RsubBackward1>)
"""

3.3 线性回归的简洁实现

本节将介绍如何通过使用深度学习框架来简洁地实现上节的线性回归模型。

3.3.1 生成数据集

import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l

true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)

3.3.2 读取数据集

通过调用现有API来读取数据,将features和labels作为API的参数传递,通过数据迭代器知道batch_size。布尔值is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据。

def load_array(data_arrays, batch_size, is_train=True):  #@save
    """构造一个PyTorch数据迭代器"""
    dataset = data.TensorDataset(*data_arrays)
    return data.DataLoader(dataset, batch_size, shuffle=is_train)

batch_size = 10
data_iter = load_array((features, labels), batch_size)

为了验证是否正常工作,让我们读取并打印第一个小批量样本。

next(iter(data_iter))
"""
[tensor([[-1.1948, -0.9979],
         [ 1.3176, -0.9471],
         [ 0.2275,  1.0018],
         [-0.5488,  0.1131],
         [ 0.1650,  0.0070],
         [ 0.7202,  0.3726],
         [-0.6342, -0.2317],
         [ 0.4996,  0.1752],
         [ 1.3427, -1.2285],
         [ 0.3173,  0.1173]]),
 tensor([[ 5.1984],
         [10.0492],
         [ 1.2512],
         [ 2.7081],
         [ 4.5190],
         [ 4.3675],
         [ 3.7204],
         [ 4.6079],
         [11.0546],
         [ 4.4236]])]
"""

3.3.3 定义模型

对于标准深度学习模型,可以使用框架的预定义好的层,使得我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。首先定义一个模型变量net,它是一个Sequential类的实例,Sequential类将多个层串联在一起,当给定数据时,Sequential实例将数据传入第一层,然后将第一层的输出作为第二层的输入,以此类推。在下面的例子中,我们的模型只包含一个层,因此实际上不需要Sequential。但是由于以后几乎所有的模型都是多层的,在这里使用会让你熟悉“标准的流水线”。

回顾上节中的单层网络架构, 这一单层被称为全连接层(fully-connected layer), 因为它的每一个输入都通过矩阵-向量乘法得到它的每个输出。

在PyTorch中,全连接层在Linear类中定义。将两个参数传递到nn.Linear,第一个指定输入特征形状,即2,第二个指定输出特征形状,为单个标量,因此为1。

from torch import nn

net = nn.Sequential(nn.Linear(2, 1))

3.3.4 初始化模型参数

深度学习框架通常有预定义的方法来初始化参数。 在这里指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采样, 偏置参数将初始化为零。

正如我们在构造nn.Linear时指定的输入和输出尺寸一样,现在可以直接访问参数以设定它们的初始值。通过net[0]选择网络的第一个图层,然后使用weight.data和bias.data方法访问参数。还可以使用替换方法normal_fill来重写参数值。

net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0) # tensor([0.])

3.3.5 定义损失函数

计算均方误差使用的是MSELoss类,也称为平方L_{2}范数。默认情况下,它返回所有样本损失的平均值

loss = nn.MSELoss()

3.3.6 定义优化算法

小批量随机梯度下降算法是一种优化神经网络的标准工具,PyTorch在optim模块中实现了该算法的许多变种。当我们实例化一个SGD实例时,要指定优化的参数,可通过(net.parameters()从我们的模型中获得)以及优化算法所需的超参数字典。 小批量随机梯度下降只需要设置lr,这里设置为0.03。

trainer = torch.optim.SGD(net.parameters(), lr=0.03)

3.3.7 训练

在每个迭代周期里,我们将完整遍历一次数据集(train_data), 不停地从中获取一个小批量的输入和相应的标签。 对于每一个小批量,我们会进行以下步骤:

1.通过调用net(X)生成预测并计算损失l(前向传播)。

2.通过进行反向传播来计算梯度。

3.通过调用优化器来更新模型参数。

为了更好的衡量训练效果,我们计算每个迭代周期后的损失,并打印它来监控训练过程。

num_epochs = 3
for epoch in range(num_epochs):
    for X, y in data_iter:
        l = loss(net(X) ,y) # net(X)表示预测出的标签,y表示真实标签
        trainer.zero_grad()
        l.backward()
        trainer.step() # 参数更新
    l = loss(net(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')
"""
epoch 1, loss 0.000318
epoch 2, loss 0.000101
epoch 3, loss 0.000101
"""

下面我们比较生成数据集的真实参数和通过有限数据训练获得的模型参数。要访问参数,首先从net访问所需的层,然后读取该层的权重和偏置。 

w = net[0].weight.data
print('w的估计误差:', true_w - w.reshape(true_w.shape))
b = net[0].bias.data
print('b的估计误差:', true_b - b)

"""
w的估计误差: tensor([-0.0010, -0.0003])
b的估计误差: tensor([-0.0003])
"""

nn模块定义了大量的神经网络层和常见损失函数。

3.4 softmax回归

3.4.1 分类问题

我们从一个图像分类问题开始。 假设每次输入是一个2×2的灰度图像。用一个标量表示每个像素值,每个图像对应四个特征𝑥1,𝑥2,𝑥3,𝑥4。 此外,假设每个图像属于类别“猫”“鸡”和“狗”中的一个。使用独热编码来表示标签,独热编码是一个向量,它的分量和类别一样多。 类别对应的分量设置为1,其他所有分量设置为0。 在我们的例子中,标签𝑦将是一个三维向量, 其中(1,0,0)对应于“猫”、(0,1,0)对应于“鸡”、(0,0,1)对应于“狗”:

3.4.2 网络架构

为了估计所有可能类别的概率,需要一个有多个输出的模型,每个类别对应一个输出。需要和输出一样多的仿射函数每个输出对应于它自己的仿射函数。在我们的例子中,由于我们有4个特征和3个可能的输出类别,将需要12个标量来表示权重, 3个标量来表示偏置。下面我们为每个输入计算三个未规范化的预测:𝑜1、𝑜2和𝑜3。

与线性回归一样,softmax回归也是一个单层神经网络。由于计算每个输出𝑜1、𝑜2和𝑜3取决于 所有输入𝑥1、𝑥2、𝑥3和𝑥4, 所以softmax回归的输出层也是全连接层。 

通过向量形式表达为𝐨=𝐖𝐱+𝐛。 由此,我们已经将所有权重放到一个3×4矩阵中。 对于给定数据样本的特征𝐱, 我们的输出是由权重与输入特征进行矩阵-向量乘法再加上偏置𝐛得到的。

3.4.3 全连接层的参数开销

对于任何具有𝑑个输入和𝑞个输出的全连接层, 参数开销为ο(dq),幸运的是,将𝑑个输入转换为𝑞个输出的成本可以减少到ο(dq/n), 其中超参数𝑛可以由我们灵活指定,以在实际应用中平衡参数节约和模型有效性。

3.4.4 softmax运算

为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。

我们希望模型的输出𝑦̂ 𝑗可以视为类别j的概率,然后选择具有最大输出值的类别argmaxxj yj作为我们的预测。例如,如果𝑦̂ 1、𝑦̂ 2和𝑦̂ 3分别为0.1、0.8和0.1, 那么我们预测的类别是2,在我们的例子中代表“鸡”。

我们不能将未规范化的预测o直接作为我们感兴趣的输出。因为将线性层的输出直接视为概率时存在一些问题:一方面,我们没有限制这些输出数字的总和为1。 另一方面,根据输入的不同,它们可以为负值

要将输出视为概率,必须保证在任何数据上的输出都是非负的且总和为1,此外还要有一个训练的目标函数,来激励模型精准地估计概率。

softmax函数能将未规范化的预测变换未非负数且总和为1,同时让模型保持可导的性质。

这里,对于所有的𝑗总有0≤𝑦̂ 𝑗≤1。 因此,𝐲̂ 可以视为一个正确的概率分布。 softmax运算不会改变未规范化的预测𝐨𝑜之间的大小次序,只会确定分配给每个类别的概率。 因此,在预测过程中,我们仍然可以用下式来选择最有可能的类别。

尽管softmax函数时一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定。因此softmax回归是一个线性模型

3.4.5 小批量样本的矢量化

为了提高效率充分利用GPU,通常会对小批量样本的数据执行矢量计算。设我们读取了一个批量的样本𝐗, 其中特征维度(输入数量)为𝑑,批量大小为𝑛。 此外,假设我们在输出中有𝑞个类别。 那么小批量样本的特征为𝐗(𝑛×𝑑), 权重为𝐖(𝑑×𝑞), 偏置为𝐛(1×𝑞)。 softmax回归的矢量计算表达式为:

小批量样本的矢量化加快了XW的矩阵-向量乘法。小批量的未规范化预测𝐎和输出概率𝑌^ 都是形状为𝑛×𝑞的矩阵。

3.4.6 损失函数

对任何标签y和模型预测y^,损失函数为:
 

 上式的损失函数被称为交叉熵损失。

3.4.7 信息论基础

3.4.8 模型预测和评估

在接下来的实验中,使用精度来评估模型的性能。精度等于正确预测数与预测总数之间的比率。

3.5 图像分类数据集

3.5.1 读取数据集

%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()

 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,
# 并除以255使得所有像素的数值均在0~1之间
trans = transforms.ToTensor()
mnist_train = torchvision.datasets.FashionMNIST(
    root="../data", train=True, transform=trans, download=True)
mnist_test = torchvision.datasets.FashionMNIST(
    root="../data", train=False, transform=trans, download=True)

每个输入图像的高度和宽度均为28像素。 数据集由灰度图像组成,其通道数为1。 

mnist_train[0][0].shape # torch.Size([1, 28, 28])

以下函数用于在数字标签索引及其文本名称之间进行转换。

def get_fashion_mnist_labels(labels):  #@save
    """返回Fashion-MNIST数据集的文本标签"""
    text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
                   'sandal', '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):  #@save
    """绘制图像列表"""
    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)))
show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y));

3.5.2 读取小批量

使用内置的数据迭代器,可以随机打乱所有样本,无偏见地读取小批量。

batch_size = 256

def get_dataloader_workers():  #@save
    """使用4个进程来读取数据"""
    return 4

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

3.5.3 整合所有组件

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

def load_data_fashion_mnist(batch_size, resize=None):  #@save
    """下载Fashion-MNIST数据集,然后将其加载到内存中"""
    trans = [transforms.ToTensor()]
    if resize:
        trans.insert(0, transforms.Resize(resize))
    trans = transforms.Compose(trans)
    mnist_train = torchvision.datasets.FashionMNIST(
        root="../data", train=True, transform=trans, download=True)
    mnist_test = torchvision.datasets.FashionMNIST(
        root="../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

3.6 softmax回归的从零开始实现

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)

3.6.1 初始化模型参数

每个样本都将用固定长度的向量表示。 原始数据集中的每个样本都是28×28的图像。本节将展平每个图像,把它们看作长度为784的向量

在softmax回归中,我们的输出与类别一样多。 因为我们的数据集有10个类别,所以网络输出维度为10因此,权重将构成一个784×10的矩阵, 偏置将构成一个1×10的向量。 与线性回归一样,我们将使用正态分布初始化我们的权重W,偏置初始化为0。

num_inputs = 784
num_outputs = 10

W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)

3.6.2 定义softmax操作

实现softmax由三个步骤组成:

1.对每个项求幂(使用exp);

2.对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;

3.将每一行除以其规范化常数,确保结果的和为1。

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

 上述代码,对于任何随机输入,将每个元素变成一个非负数。每行总和为1。

3.6.3 定义模型

def net(X):
    return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)

3.6.4 定义损失函数

使用交叉熵损失函数,创建一个数据样本y_hat,其中包含2个样本在3个类别的预测概率, 以及它们对应的标签y。

def cross_entropy(y_hat, y):
    return - torch.log(y_hat[range(len(y_hat)), y])

3.6.5 分类精度

当预测与标签分类y一致时,即是正确的。分类精度即正确预测数量与总预测数量之比。因为精度计算不可导,所以直接优化精度很困难。为了计算精度,执行以下操作。最后返回正确预测的数量。

def accuracy(y_hat, y):  #@save
    """计算预测正确的数量"""
    if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
        y_hat = y_hat.argmax(axis=1)
    cmp = y_hat.type(y.dtype) == y
    return float(cmp.type(y.dtype).sum())

对于任意数据迭代器data_iter可访问的数据集, 我们可以评估在任意模型net的精度。

def evaluate_accuracy(net, data_iter):  #@save
    """计算在指定数据集上模型的精度"""
    if isinstance(net, torch.nn.Module):
        net.eval()  # 将模型设置为评估模式
    metric = Accumulator(2)  # 正确预测数、预测总数
    with torch.no_grad():
        for X, y in data_iter:
            metric.add(accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]

3.6.6 训练

首先定义一个函数来训练一个迭代周期,updater是更新参数的常用函数。它接受批量大小作为参数。 它可以是d2l.sgd函数,也可以是框架的内置优化函数。

def train_epoch_ch3(net, train_iter, loss, updater):  #@save
    """训练模型一个迭代周期(定义见第3章)"""
    # 将模型设置为训练模式
    if isinstance(net, torch.nn.Module):
        net.train()
    # 训练损失总和、训练准确度总和、样本数
    metric = Accumulator(3)
    for X, y in train_iter:
        # 计算梯度并更新参数
        y_hat = net(X)
        l = loss(y_hat, y)
        if isinstance(updater, torch.optim.Optimizer):
            # 使用PyTorch内置的优化器和损失函数
            updater.zero_grad()
            l.mean().backward()
            updater.step()
        else:
            # 使用定制的优化器和损失函数
            l.sum().backward()
            updater(X.shape[0])
        metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
    # 返回训练损失和训练精度
    return metric[0] / metric[2], metric[1] / metric[2]

在展示训练函数的实现之前,我们定义一个在动画中绘制数据的实用程序类Animator, 它能够简化本书其余部分的代码。

class Animator:  #@save
    """在动画中绘制数据"""
    def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
                 ylim=None, xscale='linear', yscale='linear',
                 fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
                 figsize=(3.5, 2.5)):
        # 增量地绘制多条线
        if legend is None:
            legend = []
        d2l.use_svg_display()
        self.fig, self.axes = d2l.plt.subplots(nrows, ncols, figsize=figsize)
        if nrows * ncols == 1:
            self.axes = [self.axes, ]
        # 使用lambda函数捕获参数
        self.config_axes = lambda: d2l.set_axes(
            self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
        self.X, self.Y, self.fmts = None, None, fmts

    def add(self, x, y):
        # 向图表中添加多个数据点
        if not hasattr(y, "__len__"):
            y = [y]
        n = len(y)
        if not hasattr(x, "__len__"):
            x = [x] * n
        if not self.X:
            self.X = [[] for _ in range(n)]
        if not self.Y:
            self.Y = [[] for _ in range(n)]
        for i, (a, b) in enumerate(zip(x, y)):
            if a is not None and b is not None:
                self.X[i].append(a)
                self.Y[i].append(b)
        self.axes[0].cla()
        for x, y, fmt in zip(self.X, self.Y, self.fmts):
            self.axes[0].plot(x, y, fmt)
        self.config_axes()
        display.display(self.fig)
        display.clear_output(wait=True)

接下来实现一个训练函数,他会在train_iter访问到的训练集上训练一个模型net,利用Animator类来可视化训练进度。

def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater):  #@save
    """训练模型(定义见第3章)"""
    animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
                        legend=['train loss', 'train acc', 'test acc'])
    for epoch in range(num_epochs):
        train_metrics = train_epoch_ch3(net, train_iter, loss, updater)
        test_acc = evaluate_accuracy(net, test_iter)
        animator.add(epoch + 1, train_metrics + (test_acc,))
    train_loss, train_acc = train_metrics
    assert train_loss < 0.5, train_loss
    assert train_acc <= 1 and train_acc > 0.7, train_acc
    assert test_acc <= 1 and test_acc > 0.7, test_acc
lr = 0.1

def updater(batch_size):
    return d2l.sgd([W, b], lr, batch_size)

num_epochs = 10
train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, updater)

3.6.7 预测

给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。

def predict_ch3(net, test_iter, n=6):  #@save
    """预测标签(定义见第3章)"""
    for X, y in test_iter:
        break
    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)]
    d2l.show_images(
        X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])

predict_ch3(net, test_iter)

3.7 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)

3.7.1 初始化模型参数

softmax回归的输出层是一个全连接层,因此,为了实现模型,只需要在Sequential中添加一个带有10个输出的全连接层。

# PyTorch不会隐式地调整输入的形状。因此,
# 我们在线性层前定义了展平层(flatten),来调整网络输入的形状
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10))

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);

3.7.2 重新审视softmax的实现

loss = nn.CrossEntropyLoss(reduction='none')

3.7.3 优化算法

trainer = torch.optim.SGD(net.parameters(), lr=0.1)

3.7.4 训练

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值