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


    线性神经网络

    线性回归

    • 线性模型

    而在机器学习领域, 我们通常使用的是高维数据集, 建模时采用线性代数表示法会比较方便。当我们的输入 包含 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 ) ) .

    wwη|B|iBwl(i)(w,b)=wη|B|iBx(i)(wx(i)+by(i)),bbη|B|iBbl(i)(w,b)=bη|B|iB(wx(i)+by(i))." role="presentation">wwη|B|iBwl(i)(w,b)=wη|B|iBx(i)(wx(i)+by(i)),bbη|B|iBbl(i)(w,b)=bη|B|iB(wx(i)+by(i)).
    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
    
    • 1
    • 2
    • 3
    • 4

    生成数据集

    我们使用线性模型参数 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

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

    读取数据集

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

    在下面的代码中,我们定义一个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] 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

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

    batch_size = 10
    
    for X, y in data_iter(batch_size, features, labels):
        print(X, '\n', y)
        break
    
    • 1
    • 2
    • 3
    • 4
    • 5
    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]])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    初始化模型参数

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

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

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

    定义模型

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

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

    定义损失函数

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

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

    定义优化算法

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

    在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。 接下来,朝着减少损失的方向更新我们的参数。 下面的函数实现小批量随机梯度下降更新。 该函数接受模型参数集合、学习速率和批量大小作为输入。每 一步更新的大小由学习速率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_()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    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}')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    epoch 1, loss 0.026352
    epoch 2, loss 0.000093
    epoch 3, loss 0.000054
    
    • 1
    • 2
    • 3

    线性回归的简洁实现

    生成数据集

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    读取数据集

    我们可以调用框架中现有的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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

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

    next(iter(data_iter))
    
    • 1
    [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]])]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    定义模型

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

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

    # nn是神经网络的缩写
    from torch import nn
    
    net = nn.Sequential(nn.Linear(2, 1))
    
    • 1
    • 2
    • 3
    • 4

    初始化模型参数

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

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

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

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

    定义损失函数

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

    loss = nn.MSELoss()
    
    • 1

    定义优化算法

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

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

    训练

    回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(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}')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    epoch 1, loss 0.000157
    epoch 2, loss 0.000094
    epoch 3, loss 0.000094
    
    • 1
    • 2
    • 3

    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回归的输出层也是全连接层。
    在这里插入图片描述
    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    len(mnist_train), len(mnist_test)
    # (60000, 10000)
    
    • 1
    • 2

    每个输入图像的高度和宽度均为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])
    
    • 1
    • 2

    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]
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们现在可以创建一个函数来可视化这些样本。

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    以下是训练数据集中前几个样本的图像及其相应的标签。

    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));
    
    • 1
    • 2

    在这里插入图片描述

    读取小批量

    在每次迭代中,数据加载器每次都会读取一小批量数据,大小为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())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    整合所有的组件

    现在我们定义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()))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    下面,我们通过指定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
    
    • 1
    • 2
    • 3
    • 4
    torch.Size([32, 1, 64, 64]) torch.float32 torch.Size([32]) torch.int64
    
    • 1

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    初始化模型参数

    原始数据集中的每个样本都是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)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    定义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)
    
    • 1
    • 2
    (tensor([[5., 7., 9.]]),
     tensor([[ 6.],
             [15.]]))
    
    • 1
    • 2
    • 3

    回想一下,实现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
    • 2
    • 3
    • 4

    正如你所看到的,对于任何随机输入,我们将每个元素变成一个非负数。 此外,依据概率原理,每行总和为1。

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

    定义模型

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

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

    定义损失函数

    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]
    
    • 1
    • 2
    • 3

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

    def cross_entropy(y_hat, y):
        return - torch.log(y_hat[range(len(y_hat)), y])
    
    cross_entropy(y_hat, y)
    
    • 1
    • 2
    • 3
    • 4
    tensor([2.3026, 0.6931])
    
    • 1

    分类精度

    为了计算精度,我们执行以下操作。 首先,如果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())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    accuracy(y_hat, y) / len(y)
    # 0.5
    
    • 1
    • 2

    同样,对于任意数据迭代器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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里定义一个实用程序类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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

    evaluate_accuracy(net, test_iter)
    # 0.1012
    
    • 1
    • 2

    训练

    首先,我们定义一个函数来训练一个迭代周期。 请注意,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]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在展示训练函数的实现之前,我们定义一个在动画中绘制数据的实用程序类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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    接下来我们实现一个训练函数, 它会在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)
    
    • 1
    • 2
    • 3
    • 4

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

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

    在这里插入图片描述

    预测

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

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 初始化模型参数
    # 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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • Softmax实现
    loss = nn.CrossEntropyLoss(reduction='none')
    
    • 1
    • 优化算法
    trainer = torch.optim.SGD(net.parameters(), lr=0.1)
    
    • 1
    • 训练
    num_epochs = 10
    d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)
    
    • 1
    • 2
  • 相关阅读:
    注解@EnableAutoConfiguration的作用以及如何使用
    SQL 语句执行过程
    使springAOP生效不一定要加@EnableAspectJAutoProxy注解
    stm32之30.DMA
    第十三届蓝桥杯B组c++国赛
    springboot236基于springboot在线课程管理系统的设计与实现
    算法系列五:十大经典排序算法之——选择排序
    el-popover放在el-table中点击无反应问题
    【云原生--Kubernetes】配置管理
    ​力扣解法汇总1796. 字符串中第二大的数字
  • 原文地址:https://blog.csdn.net/zyw2002/article/details/128174039