线性神经网络

线性回归

  • 线性模型

而在机器学习领域, 我们通常使用的是高维数据集, 建模时采用线性代数表示法会比较方便。当我们的输入 包含 d d d 个特征时,我们将预测结果 y ^ \hat{y} y^ (通常使用 “尖角”符号表示 y y y 的估计值)表示为:

y ^ = w 1 x 1 + … + w d x d + b . \hat{y}=w_1 x_1+\ldots+w_d x_d+b . y^=w1x1++wdxd+b.

将所有特征放到向量 x ∈ R d \mathbf{x} \in \mathbb{R}^d xRd 中,并将所有权重放到向量 w ∈ R d \mathbf{w} \in \mathbb{R}^d wRd中,我们可以用点积形式来简洁地表达模型 :

y ^ = w ⊤ x + b . \hat{y}=\mathbf{w}^{\top} \mathbf{x}+b . y^=wx+b.

向量 x \mathbf{x} x 对应于单个数据样本的特征。用符号表示的矩阵 X ∈ R n × d \mathbf{X} \in \mathbb{R}^{n \times d} XRn×d可以很方便地引用我们整个数 据集的 d d d个样本。其中, X \mathbf{X} X 的每一行是一个样本, 每一列是一种特征。
对于特征集合 x \mathbf{x} x, 预测值 y ^ ∈ R n \hat{\mathbf{y}} \in \mathbb{R}^n y^Rn可以通过矩阵-向量乘法表示为 :

y ^ = X w + b \hat{\mathbf{y}}=\mathbf{X} \mathbf{w}+b y^=Xw+b

在开始寻找最好的模型参数(model parameters)w和b之前,我们还需要两个东西:(1)⼀种模型质量的度量⽅式;(2)⼀种能够更新模型以提⾼模型预测质量的⽅法。

  • 损失函数

在我们开始考虑如何⽤模型拟合(fit)数据之前,我们需要确定⼀个拟合程度的度量。损失函数(loss function)能够量化⽬标的实际值与预测值之间的差距。

通常我们会选择⾮负数作为损失,且数值越⼩表⽰损失越⼩,完美预测时的损失为 0 。回归问题中最常用的损失函数是平方误差函数。当样本 i i i 的预测值为 y ^ ( i ) \hat{y}^{(i)} y^(i), 其相应的真 实标签为 y ( i ) y^{(i)} y(i)时,平方误差可以定义为以下公式:

l ( i ) ( w , b ) = 1 2 ( y ^ ( i ) − y ( i ) ) 2 . l^{(i)}(\mathbf{w}, b)=\frac{1}{2}\left(\hat{y}^{(i)}-y^{(i)}\right)^2 . l(i)(w,b)=21(y^(i)y(i))2.

为了度量模型在整个数据集上的质量,我们需计算在训练集n个样本上的损失均值(也等价于求和)。

L ( w , b ) = 1 n ∑ i = 1 n l ( i ) ( w , b ) = 1 n ∑ i = 1 n 1 2 ( w ⊤ x ( i ) + b − y ( i ) ) 2 . L(\mathbf{w}, b)=\frac{1}{n} \sum_{i=1}^n l^{(i)}(\mathbf{w}, b)=\frac{1}{n} \sum_{i=1}^n \frac{1}{2}\left(\mathbf{w}^{\top} \mathbf{x}^{(i)}+b-y^{(i)}\right)^2 . L(w,b)=n1i=1nl(i)(w,b)=n1i=1n21(wx(i)+by(i))2.

在训练模型时, 我们希望寻找一组参数 ( w ∗ , b ∗ ) \left(\mathbf{w}^*, b^*\right) (w,b), 这组参数能最小化在所有训练样本上的总损失。如下式 :

w ∗ , b ∗ = argmin ⁡ w , b L ( w , b ) . \mathbf{w}^*, b^*=\underset{\mathbf{w}, b}{\operatorname{argmin}} L(\mathbf{w}, b) . w,b=w,bargminL(w,b).

  • 解析解

线性回归的解可以⽤⼀个公式简单地表达出来,这类解叫作解析解(analytical solution)。

将损失关于w的导数设为0,得到解析解: w ∗ = ( X ⊤ X ) − 1 X ⊤ y \mathbf{w}^*=\left(\mathbf{X}^{\top} \mathbf{X}\right)^{-1} \mathbf{X}^{\top} \mathbf{y} w=(XX)1Xy

  • 随机梯度下降

即使在我们⽆法得到解析解的情况下,我们仍然可以有效地训练模型。我们⽤到⼀种名为**梯度下降(gradient descent)**的⽅法,这种⽅法⼏乎可以优化所有深度学习模型。它通过不断地在损失函数递减的⽅向上更新参数来降低误差。

梯度下降最简单的⽤法是计算损失函数(数据集中所有样本的损失均值)关于模型参数的导数(在这⾥也可以称为梯度)。但实际中的执⾏可能会⾮常慢:因为在每⼀次更新参数之前,我们必须遍历整个数据集。因此,我们通常会在每次需要计算更新的时候随机抽取⼀⼩批样本,这种变体叫做⼩批量随机梯度下降(minibatch stochastic gradient descent)
在每次迭代中,我们⾸先随机抽样⼀个⼩批量B,它是由固定数量的训练样本组成的。然后,我们计算⼩批量的平均损失关于模型参数的导数(也可以称为梯度)。最后,我们将梯度乘以⼀个预先确定的正数η,并从当前参数的值中减掉。

我们⽤下⾯的数学公式来表⽰这⼀更新过程(∂表⽰偏导数):

( w , b ) ← ( w , b ) − η ∣ B ∣ ∑ i ∈ B ∂ ( w , b ) l ( i ) ( w , b ) (\mathbf{w}, b) \leftarrow(\mathbf{w}, b)-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{(\mathbf{w}, b)} l^{(i)}(\mathbf{w}, b) (w,b)(w,b)BηiB(w,b)l(i)(w,b)

总结⼀下,算法的步骤如下:

(1)初始化模型参数的值,如随机初始化;

(2)从数据集中随机抽取⼩批量样本且在负梯度的⽅向上更新参数,并不断迭代这⼀步骤。对于平⽅损失和仿射变换,我们可以明确地写成如下形式:

w ← w − η ∣ B ∣ ∑ i ∈ B ∂ w l ( i ) ( w , b ) = w − η ∣ B ∣ ∑ i ∈ B x ( i ) ( w ⊤ x ( i ) + b − y ( i ) ) , b ← b − η ∣ B ∣ ∑ i ∈ B ∂ b l ( i ) ( w , b ) = b − η ∣ B ∣ ∑ i ∈ B ( w ⊤ x ( i ) + b − y ( i ) ) . \begin{aligned}& \mathbf{w} \leftarrow \mathbf{w}-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_{\mathbf{w}} l^{(i)}(\mathbf{w}, b)=\mathbf{w}-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)}\left(\mathbf{w}^{\top} \mathbf{x}^{(i)}+b-y^{(i)}\right), \\& b \leftarrow b-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \partial_b l^{(i)}(\mathbf{w}, b)=b-\frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}}\left(\mathbf{w}^{\top} \mathbf{x}^{(i)}+b-y^{(i)}\right) . \\&\end{aligned} wwBηiBwl(i)(w,b)=wBηiBx(i)(wx(i)+by(i)),bbBηiBbl(i)(w,b)=bBηiB(wx(i)+by(i)).

B|表⽰每个⼩批量中的样本数,这也称为批量⼤⼩(batch size)。η表⽰学习率(learning rate)。批量⼤⼩和学习率的值通常是⼿动预先指定,⽽不是通过模型训练得到的。这些可以调整但不在训练过程中更新的参数称为超参数(hyperparameter)。调参(hyperparameter tuning)是选择超参数的过程。超参数通常是我们根据训练迭代结果来调整的,⽽训练迭代结果是在独⽴的验证数据集(validation dataset)上评估得到的。

事实上,更难做到的是找到⼀组参数,这组参数能够在我们从未⻅过的数据上实现较低的损失,这⼀挑战被称为泛化(generalization)

线性回归从零开始的实现

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

生成数据集

我们使用线性模型参数 w = [ 2 , − 3.4 ] ⊤ 、 b = 4.2 \mathbf{w}=[2,-3.4]^{\top} 、 b=4.2 w=[2,3.4]b=4.2和噪声项 ϵ \epsilon ϵ生成数据集及其标签:

y = X w + b + ϵ . \mathbf{y}=\mathbf{X} \mathbf{w}+b+\epsilon . y=Xw+b+ϵ.

你可以将 ϵ \epsilon ϵ 视为模型预测和标签时的潜在观测误差。在这里我们认为标准假设成立, 即 ϵ \epsilon ϵ 服从均值为 0 的正态分布。为了简化问题,我们将标准差设为 0.01 0.01 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中的每一行都包含一维标签值(一个标量)。

print('features:', features[0],'\nlabel:', labels[0])
features: tensor([-0.1413,  0.9253])
label: tensor([0.7524])

读取数据集

训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。 由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数, 该函数能打乱数据集中的样本并以小批量方式获取数据。

在下面的代码中,我们定义一个data_iter函数, 该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量。 每个小批量包含一组特征和标签。

def data_iter(batch_size, features, labels):
    num_examples = len(features) # python 矩阵中的len() 方法用于获取矩阵的行
    indices = list(range(num_examples)) 
		# range函数返回一个range对象实例。实例包含了计数的起始位置、终点位置和步长等信息。
		# list()函数是Python的内置函数。它可以将任何可迭代数据转换为列表类型,并返回转换后的列表。当参数为空时,list函数可以创建一个空列表。
    # 这些样本是随机读取的,没有特定的顺序
    random.shuffle(indices)
		# shuffle()方法将序列的所有元素随机排列
    for i in range(0, num_examples, batch_size):
        batch_indices = torch.tensor(
            indices[i: min(i + batch_size, num_examples)])
        yield features[batch_indices], labels[batch_indices] 

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

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

batch_size = 10

for X, y in data_iter(batch_size, features, labels):
    print(X, '\n', y)
    break
tensor([[-0.0929,  0.3136],
        [-0.4081,  0.5990],
        [ 1.2006, -0.8625],
        [ 2.8351,  1.2113],
        [ 0.4811,  1.6206],
        [-1.5946,  0.7590],
        [-0.7296,  2.0734],
        [ 1.4357, -0.4068],
        [-1.1405, -0.0359],
        [ 0.6749,  0.9677]])
 tensor([[ 2.9562],
        [ 1.3347],
        [ 9.5308],
        [ 5.7467],
        [-0.3549],
        [-1.5650],
        [-4.3218],
        [ 8.4510],
        [ 2.0353],
        [ 2.2612]])

初始化模型参数

在我们开始用小批量随机梯度下降优化我们的模型参数之前, 我们需要先有一些参数。 在下面的代码中,我们通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0。

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

在初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。 每次更新都需要计算损失函数关于模型参数的梯度。 有了这个梯度,我们就可以向减小损失的方向更新每个参数。 因为手动计算梯度很枯燥而且容易出错,所以没有人会手动计算梯度。 我们使用自动微分来计算梯度。

定义模型

接下来,我们必须定义模型,将模型的输入和参数同模型的输出关联起来。 回想一下,要计算线性模型的输出, 我们只需计算输入特征X和模型权重w的矩阵-向量乘法后加上偏置b。

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

定义损失函数

因为需要计算损失函数的梯度,所以我们应该先定义损失函数。 这里我们使用平方损失函数。 在实现中,我们需要将真实值y的形状转换为和预测值y_hat
的形状相同。

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

定义优化算法

这里我们介绍小批量随机梯度下降。

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

def sgd(params, lr, batch_size):  #@save
    """小批量随机梯度下降"""
    with torch.no_grad():  # 屏蔽梯度计算
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()

with torch.no_grad()和backward()

训练

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

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

  • 初始化参数
  • 重复以下训练,直到完成
    • 计算梯度 g ← ∂ ( w , b ) 1 ∣ B ∣ ∑ i ∈ B l ( x ( i ) , y ( i ) , w , b ) \mathbf{g} \leftarrow \partial_{(\mathbf{w}, b)} \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} l\left(\mathbf{x}^{(i)}, y^{(i)}, \mathbf{w}, b\right) g(w,b)B1iBl(x(i),y(i),w,b)
    • 更新参数 ( w , b ) ← ( w , b ) − η g (\mathbf{w}, b) \leftarrow(\mathbf{w}, b)-\eta \mathbf{g} (w,b)(w,b)ηg
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.026352
epoch 2, loss 0.000093
epoch 3, loss 0.000054

线性回归的简洁实现

生成数据集

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)

读取数据集

我们可以调用框架中现有的API来读取数据。 我们将featureslabels作为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)

pytorch中Dataset,TensorDataset和DataLoader用法_鬼道2022的博客-CSDN博客_data.tensordataset

我们使用iter构造Python迭代器,并使用next从迭代器中获取第一项。

next(iter(data_iter))
[tensor([[ 0.7882, -0.7068],
         [ 0.5081,  0.2577],
         [-0.5769,  0.1545],
         [-0.3271, -0.6080],
         [-0.2716, -1.4628],
         [-1.1530, -1.4643],
         [ 0.1635, -0.2018],
         [-0.0753, -1.1161],
         [ 3.4251,  0.1953],
         [ 0.3589, -0.9478]]),
 tensor([[ 8.1742],
         [ 4.3357],
         [ 2.5157],
         [ 5.6106],
         [ 8.6395],
         [ 6.8726],
         [ 5.2155],
         [ 7.8377],
         [10.3918],
         [ 8.1590]])]

定义模型

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

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

# nn是神经网络的缩写
from torch import nn

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

初始化模型参数

在使用net之前,我们需要初始化模型参数。 如在线性回归模型中的权重和偏置。 深度学习框架通常有预定义的方法来初始化参数。 在这里,我们指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采样, 偏置参数将初始化为零。

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

PyTorch权重初始化的几种方法_鹊踏枝-码农的博客-CSDN博客_pytorch weight.data.normal_

深度学习基础知识(一)— 权重初始化_Teeyohuang的博客-CSDN博客_weight.data.normal_

定义损失函数

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

loss = nn.MSELoss()

定义优化算法

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

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

训练

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

  • 通过调用net(X)生成预测并计算损失l(前向传播)。
  • 通过进行反向传播来计算梯度。
  • 通过调用优化器来更新模型参数。

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

num_epochs = 3
for epoch in range(num_epochs):
    for X, y in data_iter:
        l = loss(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.000157
epoch 2, loss 0.000094
epoch 3, loss 0.000094

softmax 回归

softmax运算

统计学家很早以前就发明了一种表示分类数据的简单方法:独热编码(one-hot encoding)。 独热编码是一个向量,它的分量和类别一样多。 类别对应的分量设置为1,其他所有分量设置为0。

与线性回归一样,softmax回归也是一个单层神经网络。 由于计算每个输出 o 1 , o 2 , o 3 , o 4 o_1,o_2,o_3,o_4 o1,o2,o3,o4取决于 所有输入 x 1 , x 2 , x 3 , x 4 x_1,x_2,x_3,x_4 x1,x2,x3,x4, 所以softmax回归的输出层也是全连接层。
动手学深度学习(2)—— 线性神经网络-LMLPHP
softmax函数能够将未规范化的预测变换为非负数并且总和为1,同时让模型保持 可导的性质。 为了完成这一目标,我们首先对每个未规范化的预测求幂,这样可以确保输出非负。 为了确保最终输出的概率值总和为1,我们再让每个求幂后的结果除以它们的总和。如下式:

y ^ = softmax ⁡ ( o )  其中  y ^ j = exp ⁡ ( o j ) ∑ k exp ⁡ ( o k ) \hat{\mathbf{y}}=\operatorname{softmax}(\mathbf{o}) \quad \text { 其中 } \quad \hat{y}_j=\frac{\exp \left(o_j\right)}{\sum_k \exp \left(o_k\right)} y^=softmax(o) 其中 y^j=kexp(ok)exp(oj)

这里, 对于所有的 j j j总有 0 ≤ y ^ j ≤ 1 0 \leq \hat{y}_j \leq 1 0y^j1 。 因此, y ^ \hat{\mathbf{y}} y^可以视为一个正确的概率分布。 softmax运算不会改变末规范化的预测 o \mathbf{o} o之间的大小次序, 只会确定分配给每个类别的概 率。因此, 在预测过程中, 我们仍然可以用下式来选择最有可能的类别。

argmax ⁡ j y ^ j = argmax ⁡ j o j . \underset{j}{\operatorname{argmax}} \hat{y}_j=\underset{j}{\operatorname{argmax}} o_j . jargmaxy^j=jargmaxoj.

交叉熵损失

softmax函数给出了一个向量 y ^ \hat{\mathbf{y}} y^, 我们可以将其视为 “对给定任意输入 x \mathbf{x} x 的每个类的条件概率”。可以将估计值与实际值进行比较:

假设整个数据集 X , Y {X,Y} X,Y具有 n n n个样本,其中索引i的样本由特征向量 x ( i ) x^{(i)} x(i)和独热标签向量 y ( i ) y^{(i)} y(i)组成

P ( Y ∣ X ) = ∏ i = 1 n P ( y ( i ) ∣ x ( i ) ) . P(\mathbf{Y} \mid \mathbf{X})=\prod{i=1}^n P\left(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}\right) . P(YX)=i=1nP(y(i)x(i)).

根据最大似然估计, 我们最大化 P ( Y ∣ X ) P(\mathbf{Y} \mid \mathbf{X}) P(YX), 相当于最小化负对数似然:

− log ⁡ P ( Y ∣ X ) = ∑ i = 1 n − log ⁡ P ( y ( i ) ∣ x ( i ) ) = ∑ i = 1 n l ( y ( i ) , y ^ ( i ) ) , -\log P(\mathbf{Y} \mid \mathbf{X})=\sum_{i=1}^n-\log P\left(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}\right)=\sum_{i=1}^n l\left(\mathbf{y}^{(i)}, \hat{\mathbf{y}}^{(i)}\right), logP(YX)=i=1nlogP(y(i)x(i))=i=1nl(y(i),y^(i)),

其中,对于任何标签 y \mathbf{y} y 和模型预测 y ^ \hat{\mathbf{y}} y^ ,损失函数为:

l ( y , y ^ ) = − ∑ j = 1 q y j log ⁡ y ^ j . l(\mathbf{y}, \hat{\mathbf{y}})=-\sum_{j=1}^q y_j \log \hat{y}_j . l(y,y^)=j=1qyjlogy^j.

上式中的损失函数通常被称为交叉嫡损失 (cross-entropy loss)。

图像分类数据集

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

# 通过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)

Fashion-MNIST由10个类别的图像组成, 每个类别由训练数据集(train dataset)中的6000张图像 和测试数据集(test dataset)中的1000张图像组成。 因此,训练集和测试集分别包含60000和10000张图像。 测试数据集不会用于训练,只用于评估模型性能。

len(mnist_train), len(mnist_test)
# (60000, 10000)

每个输入图像的高度和宽度均为28像素。 数据集由灰度图像组成,其通道数为1。 为了简洁起见,本书将高度h像素、宽度w像素图像的形状记为 h × w h\times w h×w或者 ( h , w ) (h,w) (h,w)

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

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

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

动手学深度学习(2)—— 线性神经网络-LMLPHP

读取小批量

在每次迭代中,数据加载器每次都会读取一小批量数据,大小为batch_size
。 通过内置数据迭代器,我们可以随机打乱了所有样本,从而无偏见地读取小批量。

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

整合所有的组件

现在我们定义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

Softmax回归从零开始实现

本节我们将使用Fashion-MNIST数据集, 并设置数据迭代器的批量大小为256。

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)

初始化模型参数

原始数据集中的每个样本都是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)

定义softmax操作

在实现softmax回归模型之前,我们简要回顾一下sum运算符如何沿着张量中的特定维度工作。

X = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
X.sum(0, keepdim=True), X.sum(1, keepdim=True)
(tensor([[5., 7., 9.]]),
 tensor([[ 6.],
         [15.]]))

回想一下,实现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。

X = torch.normal(0, 1, (2, 5))
X_prob = softmax(X)
X_prob, X_prob.sum(1)
(tensor([[0.0456, 0.1734, 0.0443, 0.2028, 0.5339],
         [0.0648, 0.0213, 0.0681, 0.6248, 0.2211]]),
 tensor([1., 1.]))

定义模型

定义softmax操作后,我们可以实现softmax回归模型。 下面的代码定义了输入如何通过网络映射到输出。 注意,将数据传递到模型之前,我们使用reshape
函数将每张原始图像展平为向量。

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

定义损失函数

y = torch.tensor([0, 2])
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y_hat[[0, 1], y]

我们只需一行代码就可以实现交叉熵损失函数。

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

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

分类精度

为了计算精度,我们执行以下操作。 首先,如果y_hat是矩阵,那么假定第二个维度存储每个类的预测分数。 我们使用argmax获得每行中最大元素的索引来获得预测类别。 然后我们将预测类别与真实y元素进行比较。 由于等式运算符“==”对数据类型很敏感, 因此我们将y_hat的数据类型转换为与y的数据类型一致。 结果是一个包含0(错)和1(对)的张量。 最后,我们求和会得到正确预测的数量。

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

我们将继续使用之前定义的变量y_haty分别作为预测的概率分布和标签。 可以看到,第一个样本的预测类别是2(该行的最大元素为0.6,索引为2),这与实际标签0不一致。 第二个样本的预测类别是2(该行的最大元素为0.5,索引为2),这与实际标签2一致。 因此,这两个样本的分类精度率为0.5。

accuracy(y_hat, y) / len(y)
# 0.5

同样,对于任意数据迭代器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]

这里定义一个实用程序类Accumulator,用于对多个变量进行累加。 在上面的evaluate_accuracy函数中, 我们在Accumulator实例中创建了2个变量, 分别用于存储正确预测的数量和预测的总数量。 当我们遍历数据集时,两者都将随着时间的推移而累加。

class Accumulator:  #@save
    """在n个变量上累加"""
    def __init__(self, n):
        self.data = [0.0] * n

    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0.0] * len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]

由于我们使用随机权重初始化net模型, 因此该模型的精度应接近于随机猜测。 例如在有10个类别情况下的精度为0.1。

evaluate_accuracy(net, test_iter)
# 0.1012

训练

首先,我们定义一个函数来训练一个迭代周期。 请注意,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。 该训练函数将会运行多个迭代周期(由num_epochs指定)。 在每个迭代周期结束时,利用test_iter访问到的测试数据集对模型进行评估。 我们将利用Animator类来可视化训练进度。

小批量随机梯度下降来优化模型的损失函数,设置学习率为0.1。

lr = 0.1

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

现在,我们训练模型10个迭代周期。 请注意,迭代周期(num_epochs)和学习率(lr)都是可调节的超参数。 通过更改它们的值,我们可以提高模型的分类精度。

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

动手学深度学习(2)—— 线性神经网络-LMLPHP

预测

现在训练已经完成,我们的模型已经准备好对图像进行分类预测。 给定一系列图像,我们将比较它们的实际标签(文本输出的第一行)和模型预测(文本输出的第二行)。

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)

动手学深度学习(2)—— 线性神经网络-LMLPHP

softmax回归的简洁实现

继续使用Fashion-MNIST数据集,并保持批量大小为256。

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),来调整网络输入的形状
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);
  • Softmax实现
loss = nn.CrossEntropyLoss(reduction='none')
  • 优化算法
trainer = torch.optim.SGD(net.parameters(), lr=0.1)
  • 训练
num_epochs = 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
12-05 10:50