线性回归与 softmax

线性回归

线性回归概要

线性回归 (linear regression)在回归的各种标准工具中最简单而且最流行。它可以追溯到19世纪初。线性回归基于几个简单的假设:首先,假设自变量 x\mathbf{x}x 和因变量 yyy 之间的关系是线性的,即yyy可以表示为 x\mathbf{x}x 中元素的加权和,这里通常允许包含观测值的一些噪声;其次,我们假设任何噪声都比较正常,如噪声遵循正态分布。

通常,我们使用 nnn 来表示数据集中的样本数。对索引为 iii 的样本,其输入表示为 x(i)=[x1(i),x2(i)]⊤\mathbf{x}^{(i)} = [x_1^{(i)}, x_2^{(i)}]^\topx(i)=[x1(i),x2(i)],其对应的标签是 y(i)y^{(i)}y(i)

线性模型

线性假设是指目标可以表示为特征的加权和,例如:

price=warea⋅area+wage⋅age+b\mathrm{price} = w_{\mathrm{area}} \cdot \mathrm{area} + w_{\mathrm{age}} \cdot \mathrm{age} + bprice=wareaarea+wageage+b

wareaw_{\mathrm{area}}wareawagew_{\mathrm{age}}wage 称为 权重(weight),bbb 称为 偏置(bias),或称为偏移量(offset)、截距(intercept)。权重决定了每个特征对我们预测值的影响。偏置是指当所有特征都取值为0时,预测值应该为多少。

给定一个数据集,我们的目标是寻找模型的权重 w\mathbf{w}w 和偏置 bbb,使得根据模型做出的预测大体符合数据里的真实价格。输出的预测值由输入特征通过线性模型的仿射变换决定,仿射变换由所选权重和偏置确定。在机器学习领域,我们通常使用的是高维数据集,建模时采用线性代数表示法会比较方便。当我们的输入包含 ddd 个特征时,我们将预测结果 y^\hat{y}y^(通常使用 “尖角” 符号表示估计值)表示为:

y^=w1x1+...+wdxd+b.\hat{y} = w_1 x_1 + ... + w_d x_d + b.y^=w1x1+...+wdxd+b.

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

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

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

对于特征集合 X\mathbf{X}X ,预测值 y^∈Rn\hat{\mathbf{y}} \in \mathbb{R}^ny^Rn 可以通过矩阵-向量乘法表示为:

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

这个过程中的求和将使用广播机制,给定训练数据特征 X\mathbf{X}X 和对应的已知标签 y\mathbf{y}y ,线性回归的目标是找到一组权重向量 w\mathbf{w}w 和偏置 bbb。当给定从X\mathbf{X}X的同分布中取样的新样本特征时,找到的权重向量和偏置能够使得新样本预测标签的误差尽可能小。

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

损失函数

在我们开始考虑如何用模型拟合(fit)数据之前,我们需要确定一个拟合程度的度量。损失函数 能够量化目标的实际值与预测 值之间的差距。通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为0。回归问题中最常用的损失函数是平方误差函数。当样本 iii 的预测值为 y^(i)\hat{y}^{(i)}y^(i),其相应的真实标签为 y(i)y^{(i)}y(i) 时,平方误差可以定义为以下公式:

l(i)(w,b)=12(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.

常数12\frac{1}{2}21不会带来本质的差别,但这样在形式上稍微简单一些,表现为当我们对损失函数求导后常数系数为1。由于训练数据集并不受我们控制,所以经验误差只是关于模型参数的函数。

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

L(w,b)=1n∑i=1nl(i)(w,b)=1n∑i=1n12(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∗\mathbf{w}^*, b^*w,b),这组参数能最小化在所有训练样本上的总损失。如下式:

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

解析解

线性回归刚好是一个很简单的优化问题。与我们将在本书中所讲到的其他大部分模型不同,线性回归的解可以用一个公式简单地表达出来,这类解叫作解析解(analytical solution)。首先,我们将偏置 bbb 合并到参数 w\mathbf{w}w 中。合并方法是在包含所有参数的矩阵中附加一列。我们的预测问题是最小化 ∥y−Xw∥2\|\mathbf{y} - \mathbf{X}\mathbf{w}\|^2yXw2。这在损失平面上只有一个临界点,这个临界点对应于整个区域的损失最小值。将损失关于w\mathbf{w}w的导数设为0,得到解析解(闭合形式):

w∗=(X⊤X)−1X⊤y.\mathbf{w}^* = (\mathbf X^\top \mathbf X)^{-1}\mathbf X^\top \mathbf{y}.w=(XX)1Xy.

像线性回归这样的简单问题存在解析解,但并不是所有的问题都存在解析解。解析解可以进行很好的数学分析,但解析解的限制很严格,导致它无法应用在深度学习里。

小批量随机梯度下降

本书中我们用到一种名为梯度下降(gradient descent)的方法,这种方法几乎可以优化所有深度学习模型。它通过不断地在损失函数递减的方向上更新参数来降低误差。

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

在每次迭代中,我们首先随机抽样一个小批量B\mathcal{B}B,它是由固定数量的训练样本组成的。然后,我们计算小批量的平均损失关于模型参数的导数(也可以称为梯度)。最后,我们将梯度乘以一个预先确定的正数η\etaη,并从当前参数的值中减掉。

我们用下面的数学公式来表示这一更新过程(∂\partial 表示偏导数):

(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∂wl(i)(w,b)=w−η∣B∣∑i∈Bx(i)(w⊤x(i)+b−y(i)),b←b−η∣B∣∑i∈B∂bl(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} wbwBηiBwl(i)(w,b)=wBηiBx(i)(wx(i)+by(i)),bBηiBbl(i)(w,b)=bBηiB(wx(i)+by(i)).

公式中的w\mathbf{w}wx\mathbf{x}x 都是向量。在这里,更优雅的向量表示法比系数表示法(如 w1,w2,…,wdw_1, w_2, \ldots, w_dw1,w2,,wd)更具可读性。 ∣B∣|\mathcal{B}|B 表示每个小批量中的样本数,这也称为批量大小(batch size)。η\etaη 表示 学习率(learning rate)。在训练了预先确定的若干迭代次数后(或者直到满足某些其他停止条件后),我们记录下模型参数的估计值,表示为w^,b^\hat{\mathbf{w}}, \hat{b}w^,b^。但是,即使我们的函数确实是线性的且无噪声,这些估计值也不会使损失函数真正地达到最小值。因为算法会使得损失向最小值缓慢收敛,但却不能在有限的步数内非常精确地达到最小值。

用学习到的模型进行预测

给定学习到的线性回归模型 w^⊤x+b^\hat{\mathbf{w}}^\top \mathbf{x} + \hat{b}w^x+b^,现在我们可以通过给定的房屋面积 x1x_1x1 和房龄 x2x_2x2来估计一个未包含在训练数据中的新房屋价格。给定特征估计目标的过程通常称为预测(prediction)或推断(inference)。

矢量化加速

在训练我们的模型时,我们经常希望能够同时处理整个小批量的样本。为了实现这一点,需要(我们对计算进行矢量化,从而利用线性代数库,而不是在Python中编写开销高昂的for循环)。

import math
import time
import numpy as np
import torch
from d2l import torch as d2l

为了说明矢量化为什么如此重要,我们考虑(对向量相加的两种方法)。
我们实例化两个全1的1000维向量。在一种方法中,我们将使用Python的for循环遍历向量。在另一种方法中,我们将依赖对 +++ 的调用。

n = 10000
a = torch.ones(n)
b = torch.ones(n)

定义计时器,便于比较‘

class Timer:  #@save
    """记录多次运行时间。"""
    def __init__(self):
        self.times = []
        self.start()

    def start(self):
        """启动计时器。"""
        self.tik = time.time()

    def stop(self):
        """停止计时器并将时间记录在列表中。"""
        self.times.append(time.time() - self.tik)
        return self.times[-1]

    def avg(self):
        """返回平均时间。"""
        return sum(self.times) / len(self.times)

    def sum(self):
        """返回时间总和。"""
        return sum(self.times)

    def cumsum(self):
        """返回累计时间。"""
        return np.array(self.times).cumsum().tolist()

基准测试

c = torch.zeros(n)
timer = Timer()
for i in range(n):
    c[i] = a[i] + b[i]
f'{timer.stop():.5f} sec'

或使用重载的 +++

timer.start()
d = a + b
f'{timer.stop():.5f} sec'

方法二要快很多

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RXfFpsO6-1636466983091)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\1.jpg)]

正态分布与平方损失

正态分布和线性回归之间的关系很密切。
简单的说,若随机变量 xxx 具有均值 μ\muμ 和方差 σ2\sigma^2σ2(标准差 σ\sigmaσ),其正态分布概率密度函数如下:
p(x)=12πσ2exp⁡(−12σ2(x−μ)2) p(x) = \frac{1}{\sqrt{2 \pi \sigma^2}} \exp\left(-\frac{1}{2 \sigma^2} (x - \mu)^2\right) p(x)=2πσ2 1exp(2σ21(xμ)2)

下面进行计算

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

就像我们所看到的,改变均值会产生沿 xxx 轴的偏移,增加方差将会分散分布、降低其峰值。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8nTkycvN-1636466983093)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\2.jpg)]

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

y=w⊤x+b+ϵ where ϵ∼N(0,σ2) y = \mathbf{w}^\top \mathbf{x} + b + \epsilon \text{ where } \epsilon \sim \mathcal{N}(0, \sigma^2) y=wx+b+ϵ where ϵN(0,σ2)

因此,我们现在可以写出通过给定的x\mathbf{x}x观测到特定yyy可能性(likelihood):

P(y∣x)=12πσ2exp⁡(−12σ2(y−w⊤x−b)2) P(y \mid \mathbf{x}) = \frac{1}{\sqrt{2 \pi \sigma^2}} \exp\left(-\frac{1}{2 \sigma^2} (y - \mathbf{w}^\top \mathbf{x} - b)^2\right) P(yx)=2πσ2 1exp(2σ21(ywxb)2)

现在,根据最大似然估计法,参数 w\mathbf{w}wbbb 的最优值是使整个数据集的可能性最大的值:

P(y∣X)=∏i=1np(y(i)∣x(i)) P(\mathbf y \mid \mathbf X) = \prod_{i=1}^{n} p(y^{(i)}|\mathbf{x}^{(i)}) P(yX)=i=1np(y(i)x(i))

根据最大似然估计法选择的估计量称为最大似然估计量
虽然使许多指数函数的乘积最大化看起来很困难,但是我们可以在不改变目标的前提下,通过最大化似然对数来简化。
由于历史原因,优化通常是说最小化而不是最大化。我们可以改为 最小化负对数似然 −log⁡P(y∣X)-\log P(\mathbf y \mid \mathbf X)logP(yX)。由此可以得到的数学公式是:

−log⁡P(y∣X)=∑i=1n12log⁡(2πσ2)+12σ2(y(i)−w⊤x(i)−b)2 -\log P(\mathbf y \mid \mathbf X) = \sum_{i=1}^n \frac{1}{2} \log(2 \pi \sigma^2) + \frac{1}{2 \sigma^2} \left(y^{(i)} - \mathbf{w}^\top \mathbf{x}^{(i)} - b\right)^2 logP(yX)=i=1n21log(2πσ2)+2σ21(y(i)wx(i)b)2
现在我们只需要假设σ\sigmaσ是某个固定常数就可以忽略第一项,因为第一项不依赖于 w\mathbf{w}wbbb。现在第二项除了常数1σ2\frac{1}{\sigma^2}σ21外,其余部分和前面介绍的平方误差损失是一样的。
幸运的是,上面式子的解并不依赖于 σ\sigmaσ。因此,在高斯噪声的假设下,最小化均方误差等价于对线性模型的最大似然估计。

线性回归的从零开始实现

在这一节中,我们将只使用张量和自动求导。在之后的章节中,我们会充分利用深度学习框架的优势,介绍更简洁的实现方式。

import random
import torch
from d2l import torch as d2l
生成数据集

在下面的代码中,我们生成一个包含1000个样本的数据集,每个样本包含从标准正态分布中采样的2个特征。我们的合成数据集是一个矩阵 X∈R1000×2\mathbf{X}\in \mathbb{R}^{1000 \times 2}XR1000×2

你可以将 ϵ\epsilonϵ 视为捕获特征和标签时的潜在观测误差。在这里我们认为标准假设成立,即 ϵ\epsilonϵ 服从均值为 000 的正态分布。
为了简化问题,我们将标准差设为 0.010.010.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)

print('features:', features[0], '\nlabel:', labels[0])
# 通过生成第二个特征 `features[:, 1]` 和 `labels` 的散点图,可以直观地观察到两者之间的线性关系。
d2l.set_figsize()
d2l.plt.scatter(features[:, (1)].detach().numpy(),
                labels.detach().numpy(), 1);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QR6WI788-1636466983095)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\3.jpg)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDvdR12m-1636466983097)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\4.jpg)]

读取数据集

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

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):
        batch_indices = torch.tensor(indices[i:min(i +
                                                   batch_size, num_examples)])
        yield features[batch_indices], labels[batch_indices]
        
batch_size = 10

for X, y in data_iter(batch_size, features, labels):
    print(X, '\n', y)
    break

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aXE9GIwo-1636466983098)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\5.jpg)]

初始化模型参数

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

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

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

定义参数

接下来,我们必须定义模型,将模型的输入和参数同模型的输出关联起来。
回想一下,要计算线性模型的输出,我们只需计算输入特征 X\mathbf{X}X 和模型权重w\mathbf{w}w的矩阵-向量乘法后加上偏置bbb。注意,上面的Xw\mathbf{Xw}Xw 是一个向量,而bbb是一个标量。广播机制使得当我们用一个向量加一个标量时,标量会被加到向量的每个分量上。

def linreg(X, w, b):  #@save
    """线性回归模型。"""
    return torch.matmul(X, w) + b
定义损失函数

因为要更新模型。需要计算损失函数的梯度,所以我们应该先定义损失函数。

def squared_loss(y_hat, y):  #@save
    """均方损失。"""
    return (y_hat - y.reshape(y_hat.shape))**2 / 2
定义优化算法

正如我们在 :numref:sec_linear_regression 中讨论的,线性回归有解析解。然而,这是一本关于深度学习的书,而不是一本关于线性回归的书。
由于这本书介绍的其他模型都没有解析解,下面我们将在这里介绍小批量随机梯度下降的工作示例。

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

    计算梯度 g←∂(w,b)1∣B∣∑i∈Bl(x(i),y(i),w,b)\mathbf{g} \leftarrow \partial_{(\mathbf{w},b)} \frac{1}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} l(\mathbf{x}^{(i)}, y^{(i)}, \mathbf{w}, b)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

在每个迭代周期(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}')

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LG204kOC-1636466983099)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\6.jpg)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rc3Paho4-1636466983100)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\7.jpg)]

线性回归的简洁实现

生成数据集

首先生成数据集

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)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)
next(iter(data_iter))

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LNQzOfWY-1636466983100)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\8.jpg)]

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

定义模型

对于标准操作,我们可以使用框架的预定义好的层。这使我们只需关注使用哪些层来构造模型,而不必关注层的实现细节。我们首先定义一个模型变量net,它是一个 Sequential 类的实例。 Sequential 类为串联在一起的多个层定义了一个容器。当给定输入数据, Sequential 实例将数据传入到第一层,然后将第一层的输出作为第二层的输入,依此类推。在下面的例子中,我们的模型只包含一个层,因此实际上不需要Sequential。但是由于以后几乎所有的模型都是多层的,在这里使用Sequential会让你熟悉标准的流水线。这一单层被称为 全连接层(fully-connected layer),因为它的每一个输入都通过矩阵-向量乘法连接到它的每个输出。

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

from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
初始化模型参数

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

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

net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
定义损失函数

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

loss = nn.MSELoss()
定义优化算法

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

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

通过深度学习框架的高级 API 来实现我们的模型只需要相对较少的代码。我们不必单独分配参数、不必定义我们的损失函数,也不必手动实现小批量随机梯度下降。当我们需要更复杂的模型时,高级 API 的优势将大大增加。当我们有了所有的基本组件,训练过程代码与我们从零开始实现时所做的非常相似。

回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(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)
        trainer.zero_grad()
        l.backward()
        trainer.step()
    l = loss(net(features), labels)
    print(f'epoch {epoch + 1}, loss {l:f}')

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CGJlgRQH-1636466983101)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\9.jpg)]

比较生成数据集的真实参数和通过有限数据训练获得的模型参数,要访问参数,我们首先从 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)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-njOOEwi5-1636466983102)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\10.jpg)]

小结

  • 我们可以使用 PyTorch 的高级 API更简洁地实现模型。
  • 在 PyTorch 中,data 模块提供了数据处理工具,nn 模块定义了大量的神经网络层和常见损失函数。
  • 我们可以通过_ 结尾的方法将参数替换,从而初始化参数。

softmax回归

softmax 综述

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

通常,机器学习实践者用分类这个词来描述两个有微妙差别的问题:

  1. 我们只对样本的硬性类别感兴趣,即属于哪个类别;
  2. 我们希望得到软性类别,即得到属于每个类别的概率。这两者的界限往往很模糊。其中的一个原因是,即使我们只关心硬类别,我们仍然使用软类别的模型。
网络结构

为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。
为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数(affine function)。
每个输出对应于它自己的仿射函数。
在我们的例子中,由于我们有4个特征和3个可能的输出类别,我们将需要12个标量来表示权重(带下标的www),3个标量来表示偏置(带下标的bbb)。
下面我们为每个输入计算三个未归一化的预测(logits):o1o_1o1o2o_2o2o3o_3o3
o1=x1w11+x2w12+x3w13+x4w14+b1,o2=x1w21+x2w22+x3w23+x4w24+b2,o3=x1w31+x2w32+x3w33+x4w34+b3. \begin{aligned} o_1 &= x_1 w_{11} + x_2 w_{12} + x_3 w_{13} + x_4 w_{14} + b_1,\\ o_2 &= x_1 w_{21} + x_2 w_{22} + x_3 w_{23} + x_4 w_{24} + b_2,\\ o_3 &= x_1 w_{31} + x_2 w_{32} + x_3 w_{33} + x_4 w_{34} + b_3. \end{aligned} o1o2o3=x1w11+x2w12+x3w13+x4w14+b1,=x1w21+x2w22+x3w23+x4w24+b2,=x1w31+x2w32+x3w33+x4w34+b3.

与线性回归一样,softmax回归也是一个单层神经网络。由于计算每个输出o1o_1o1o2o_2o2o3o_3o3取决于所有输入x1x_1x1x2x_2x2x3x_3x3x4x_4x4,所以softmax回归的输出层也是全连接层。

为了更简洁地表达模型,我们仍然使用线性代数符号。
通过向量形式表达为 o=Wx+b\mathbf{o} = \mathbf{W} \mathbf{x} + \mathbf{b}o=Wx+b,这是一种更适合数学和编写代码的形式。我们已经将所有权重放到一个 3×43 \times 43×4 矩阵中。对于给定数据样本的特征 x\mathbf{x}x,我们的输出是由权重与输入特征进行矩阵-向量乘法再加上偏置b\mathbf{b}b得到的。

全连接层的参数开销

正如我们将在后续章节中看到的,在深度学习中,全连接层无处不在。
然而,顾名思义,全连接层是“完全”连接的,可能有很多可学习的参数。
具体来说,对于任何具有ddd个输入和qqq个输出的全连接层,参数开销为O(dq)\mathcal{O}(dq)O(dq),在实践中可能高得令人望而却步。
幸运的是,将ddd个输入转换为qqq个输出的成本可以减少到O(dqn)\mathcal{O}(\frac{dq}{n})O(ndq),其中超参数nnn可以由我们灵活指定,以在实际应用中平衡参数节约和模型。

softmax运算

在这里要采取的主要方法是将模型的输出视作为概率。我们将优化参数以最大化观测数据的概率。为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。

我们希望模型的输出 y^j\hat{y}_jy^j 可以视为属于类 jjj 的概率。然后我们可以选择具有最大输出值的类别*⁡argmaxjyj\operatorname*{argmax}_j y_j*argmaxjyj作为我们的预测。例如,如果 y^1\hat{y}_1y^1y^2\hat{y}_2y^2y^3\hat{y}_3y^3 分别为 0.1、0.8 和 0.1,那么我们预测的类别是2。

为了将未归一化的预测变换为非负并且总和为1,同时要求模型保持可导。我们首先对每个未归一化的预测求幂,这样可以确保输出非负。为了确保最终输出的总和为1,我们再对每个求幂后的结果除以它们的总和。如下式:
y^=softmax(o)其中y^j=exp⁡(oj)∑kexp⁡(ok) \hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{o})\quad \text{其中}\quad \hat{y}_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)} y^=softmax(o)其中y^j=kexp(ok)exp(oj)
容易看出对于所有的 jjj 总有 0≤y^j≤10 \leq \hat{y}_j \leq 10y^j1。因此,y^\hat{\mathbf{y}}y^ 可以视为一个正确的概率分布。softmax 运算不会改变未归一化的预测 o\mathbf{o}o 之间的顺序,只会确定分配给每个类别的概率。因此,在预测过程中,我们仍然可以用下式来选择最有可能的类别。

*⁡argmaxjy^j=*⁡argmaxjoj \operatorname*{argmax}_j \hat y_j = \operatorname*{argmax}_j o_j *argmaxjy^j=*argmaxjoj
尽管 softmax 是一个非线性函数,但 softmax 回归的输出仍然由输入特征的仿射变换决定。因此,softmax 回归是一个线性模型。

小批量样本的矢量化

为了提高计算效率并且充分利用GPU,我们通常会针对小批量数据执行矢量计算。假设我们读取了一个批量的样本 X\mathbf{X}X ,其中特征维度(输入数量)为ddd,批量大小为nnn。此外,假设我们在输出中有 qqq 个类别。那么小批量特征为 X∈Rn×d\mathbf{X} \in \mathbb{R}^{n \times d}XRn×d ,权重为 W∈Rd×q\mathbf{W} \in \mathbb{R}^{d \times q}WRd×q,偏置为 b∈R1×q\mathbf{b} \in \mathbb{R}^{1\times q}bR1×q。softmax回归的矢量计算表达式为:

O=XW+b,Y^=softmax(O). \begin{aligned} \mathbf{O} &= \mathbf{X} \mathbf{W} + \mathbf{b}, \\ \hat{\mathbf{Y}} & = \mathrm{softmax}(\mathbf{O}). \end{aligned} OY^=XW+b,=softmax(O).
相对于一次处理一个样本,小批量样本的矢量化加快了 X和W\mathbf{X}和\mathbf{W}XW 的矩阵-向量乘法。由于 X\mathbf{X}X 中的每一行代表一个数据样本,所以softmax运算可以按行(rowwise)执行:对于O\mathbf{O}O的每一行,我们先对所有项进行幂运算,然后通过求和对它们进行标准化。
XW+b\mathbf{X} \mathbf{W} + \mathbf{b}XW+b 的求和会使用广播,小批量的未归一化预测 O\mathbf{O}O 和输出概率 Y^\hat{\mathbf{Y}}Y^ 都是形状为 n×qn \times qn×q 的矩阵。

损失函数

接下来,我们需要一个损失函数来度量预测概率的效果。我们将依赖最大似然估计,这与我们在为线性回归(中的均方误差目标提供概率证明时遇到的概念完全相同。

对数似然

softmax函数给出了一个向量 y^\hat{\mathbf{y}}y^,我们可以将其视为给定任意输入 x\mathbf{x}x的每个类的估计条件概率。例如,y^1\hat{y}_1y^1 = P(y=猫∣x)P(y=\text{猫} \mid \mathbf{x})P(y=x)。假设整个数据集 {X,Y}\{\mathbf{X}, \mathbf{Y}\}{X,Y} 具有 nnn 个样本,其中索引 iii 的样本由特征向量 x(i)\mathbf{x}^{(i)}x(i) 和独热标签向量 y(i)\mathbf{y}^{(i)}y(i) 组成。我们可以将估计值与实际值进行比较:

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

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

−log⁡P(Y∣X)=∑i=1n−log⁡P(y(i)∣x(i))=∑i=1nl(y(i),y^(i)), -\log P(\mathbf{Y} \mid \mathbf{X}) = \sum_{i=1}^n -\log P(\mathbf{y}^{(i)} \mid \mathbf{x}^{(i)}) = \sum_{i=1}^n l(\mathbf{y}^{(i)}, \hat{\mathbf{y}}^{(i)}), 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=1qyjlog⁡y^j. l(\mathbf{y}, \hat{\mathbf{y}}) = - \sum_{j=1}^q y_j \log \hat{y}_j. l(y,y^)=j=1qyjlogy^j.
在本节稍后的内容会讲到, :eqref:eq_l_cross_entropy 中的损失函数通常被称为 交叉熵损失(cross-entropy loss)。由于 y\mathbf{y}y 是一个长度为 qqq 的独热编码向量,所以除了一个项以外的所有项 jjj 都消失了。由于所有 y^j\hat{y}_jy^j 都是预测的概率,所以它们的对数永远不会大于 000
因此,如果正确地预测实际标签,即,如果实际标签 P(y∣x)=1P(\mathbf{y} \mid \mathbf{x})=1P(yx)=1,则损失函数不能进一步最小化。
注意,这往往是不可能的。例如,数据集中可能存在标签噪声(某些样本可能被误标),或输入特征没有足够的信息来完美地对每一个样本分类。

softmax及其导数

由于softmax和相关的损失函数很常见,因此值得我们更好地理解它的计算方式。将eq_softmax_y_and_o 代入损失 :eqref:eq_l_cross_entropy 中。利用softmax的定义,我们得到:

l(y,y^)=−∑j=1qyjlog⁡exp⁡(oj)∑k=1qexp⁡(ok)=∑j=1qyjlog⁡∑k=1qexp⁡(ok)−∑j=1qyjoj=log⁡∑k=1qexp⁡(ok)−∑j=1qyjoj. \begin{aligned} l(\mathbf{y}, \hat{\mathbf{y}}) &= - \sum_{j=1}^q y_j \log \frac{\exp(o_j)}{\sum_{k=1}^q \exp(o_k)} \\ &= \sum_{j=1}^q y_j \log \sum_{k=1}^q \exp(o_k) - \sum_{j=1}^q y_j o_j\\ &= \log \sum_{k=1}^q \exp(o_k) - \sum_{j=1}^q y_j o_j. \end{aligned} l(y,y^)=j=1qyjlogk=1qexp(ok)exp(oj)=j=1qyjlogk=1qexp(ok)j=1qyjoj=logk=1qexp(ok)j=1qyjoj.

为了更好地理解发生了什么,考虑相对于任何未归一化的预测 ojo_joj 的导数。我们得到:

∂ojl(y,y^)=exp⁡(oj)∑k=1qexp⁡(ok)−yj=softmax(o)j−yj. \partial_{o_j} l(\mathbf{y}, \hat{\mathbf{y}}) = \frac{\exp(o_j)}{\sum_{k=1}^q \exp(o_k)} - y_j = \mathrm{softmax}(\mathbf{o})_j - y_j. ojl(y,y^)=k=1qexp(ok)exp(oj)yj=softmax(o)jyj.

换句话说,导数是我们模型分配的概率(由softmax得到)与实际发生的情况(由独热标签向量表示)之间的差异。从这个意义上讲,与我们在回归中看到的非常相似,其中梯度是观测值yyy和估计值y^\hat{y}y^之间的差异。

模型预测和评估

在训练softmax回归模型后,给出任何样本特征,我们可以预测每个输出类别的概率。通常我们使用预测概率最高的类别作为输出类别。如果预测与实际类别(标签)一致,则预测是正确的。在接下来的实验中,我们将使用 准确率 来评估模型的性能。准确率等于正确预测数与预测的总数之间的比率。

  • softmax运算获取一个向量并将其映射为概率。
  • softmax回归适用于分类问题。它使用了softmax运算中输出类别的概率分布。
  • 交叉熵是一个衡量两个概率分布之间差异的很好的度量。它测量给定模型编码数据所需的比特数。

softmax回归的从零开始实现

我们使用刚刚在 sec_fashion_mnist 中引入的 Fashion-MNIST 数据集,并设置数据迭代器的批量大小为 256256256

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×2828 \times 2828×28 的图像。在本节中,我们将展平每个图像,把它们看作长度为784的向量。

在softmax回归中,我们的输出与类别一样多。。因此,权重将构成一个 784×10784 \times 10784×10 的矩阵,偏置将构成一个 1×101 \times 101×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)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-44PlA1tN-1636466983103)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\11.jpg)]

softmax 由三个步骤组成:

  1. 对每个项求幂(使用exp);
  2. 对每一行求和(小批量中每个样本是一行),得到每个样本的归一化常数;
  3. 将每一行除以其归一化常数,确保结果的和为1。

softmax(X)ij=exp⁡(Xij)∑kexp⁡(Xik). \mathrm{softmax}(\mathbf{X})_{ij} = \frac{\exp(\mathbf{X}_{ij})}{\sum_k \exp(\mathbf{X}_{ik})}. softmax(X)ij=kexp(Xik)exp(Xij).

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)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vI06H2ho-1636466983103)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\12.jpg)]

定义模型

在将数据传递到我们的模型之前,我们使用 reshape 函数将每张原始图像展平为向量。

def net(X):
    return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
定义损失函数

接下来,我们需要实现 sec_softmax 中引入的交叉熵损失函数。这可能是深度学习中最常见的损失函数,因为目前分类问题的数量远远超过回归问题。

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]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yv6bpASA-1636466983104)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\13.jpg)]

交叉熵损失函数

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

cross_entropy(y_hat, y)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FFGnMb5V-1636466983104)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\14.jpg)]

分类准确率

给定预测概率分布 y_hat,当我们必须输出硬预测(hard prediction)时,我们通常选择预测概率最高的类。当预测与标签分类 y 一致时,它们是正确的。分类准确率即正确预测数量与总预测数量之比。虽然直接优化准确率可能很困难(因为准确率的计算不可导),但准确率通常是我们最关心的性能衡量标准,我们在训练分类器时几乎总是会报告它。

为了计算准确率,我们执行以下操作。首先,如果 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)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zPKmeb2c-1636466983105)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\15.jpg)]

def evaluate_accuracy(net, data_iter):  #@save
    """计算在指定数据集上模型的精度。"""
    if isinstance(net, torch.nn.Module):
        net.eval()  # 将模型设置为评估模式
    metric = Accumulator(2)  # 正确预测数、预测总数
    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)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uUXlEuK8-1636466983105)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\16.jpg)]

训练

如果你看过 sec_linear_scratch 中的线性回归实现,softmax回归的训练过程代码应该看起来非常熟悉。在这里,我们重构训练过程的实现以使其可重复使用。首先,我们定义一个函数来训练一个迭代周期。请注意,updater 是更新模型参数的常用函数,它接受批量大小作为参数。它可以是封装的d2l.sgd函数,也可以是框架的内置优化函数。

def train_epoch_ch3(net, train_iter, loss, updater):  #@save
    # 将模型设置为训练模式
    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.backward()
            updater.step()
            metric.add(
                float(l) * len(y), accuracy(y_hat, y),
                y.size().numel())
        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]

接下来我们实现一个训练函数,它会在train_iter 访问到的训练数据集上训练一个模型net。该训练函数将会运行多个迭代周期(由num_epochs指定)。在每个迭代周期结束时,利用 test_iter 访问到的测试数据集对模型进行评估。我们将利用 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

作为一个从零开始的实现,我们使用 :numref:sec_linear_scratch 中定义的小批量随机梯度下降来优化模型的损失函数,设置学习率为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)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n3zTaKxX-1636466983106)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\17.jpg)]

预测
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)
小结
  • 借助 softmax 回归,我们可以训练多分类的模型。
  • softmax 回归的训练循环与线性回归中的训练循环非常相似:读取数据、定义模型和损失函数,然后使用优化算法训练模型。正如你很快就会发现的那样,大多数常见的深度学习模型都有类似的训练过程。

softmax 回归的简洁实现

通过深度学习框架的高级API也能更方便地实现分类模型。

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)
初始化模型参数

如我们在 :numref:sec_softmax 所述,[softmax 回归的输出层是一个全连接层]。因此,为了实现我们的模型,我们只需在 Sequential 中添加一个带有10个输出的全连接层。同样,在这里,Sequential 并不是必要的,但我们可能会形成这种习惯。因为在实现深度模型时,Sequential将无处不在。我们仍然以均值0和标准差0.01随机初始化权重。

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 的实现

回想一下,softmax 函数 y^j=exp⁡(oj)∑kexp⁡(ok)\hat y_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}y^j=kexp(ok)exp(oj),其中y^j\hat y_jy^j是预测的概率分布。ojo_joj是未归一化的预测o\mathbf{o}o的第jjj个元素。如果oko_kok中的一些数值非常大,那么 exp⁡(ok)\exp(o_k)exp(ok) 可能大于数据类型容许的最大数字(即 上溢(overflow))。这将使分母或分子变为inf(无穷大),我们最后遇到的是0、infnan(不是数字)的 y^j\hat y_jy^j。在这些情况下,我们不能得到一个明确定义的交叉熵的返回值。

解决这个问题的一个技巧是,在继续softmax计算之前,先从所有oko_kok中减去max⁡(ok)\max(o_k)max(ok)。你可以证明每个 oko_kok 按常数进行的移动不会改变softmax的返回值。在减法和归一化步骤之后,可能有些 ojo_joj 具有较大的负值。由于精度受限, exp⁡(oj)\exp(o_j)exp(oj) 将有接近零的值,即 下溢(underflow)。这些值可能会四舍五入为零,使 y^j\hat y_jy^j 为零,并且使得 log⁡(y^j)\log(\hat y_j)log(y^j) 的值为 -inf。反向传播几步后,我们可能会发现自己面对一屏幕可怕的nan结果。

尽管我们要计算指数函数,但我们最终在计算交叉熵损失时会取它们的对数。
通过将softmax和交叉熵结合在一起,可以避免反向传播过程中可能会困扰我们的数值稳定性问题。如下面的等式所示,我们避免计算exp⁡(oj)\exp(o_j)exp(oj),而可以直接使用ojo_joj。因为log⁡(exp⁡(⋅))\log(\exp(\cdot))log(exp())被抵消了。

log⁡(y^j)=log⁡(exp⁡(oj)∑kexp⁡(ok))=log⁡(exp⁡(oj))−log⁡(∑kexp⁡(ok))=oj−log⁡(∑kexp⁡(ok)). \begin{aligned} \log{(\hat y_j)} & = \log\left( \frac{\exp(o_j)}{\sum_k \exp(o_k)}\right) \\ & = \log{(\exp(o_j))}-\log{\left( \sum_k \exp(o_k) \right)} \\ & = o_j -\log{\left( \sum_k \exp(o_k) \right)}. \end{aligned} log(y^j)=log(kexp(ok)exp(oj))=log(exp(oj))log(kexp(ok))=ojlog(kexp(ok)).

我们也希望保留传统的softmax函数,以备我们需要评估通过模型输出的概率。

loss = nn.CrossEntropyLoss()
优化算法

在这里,我们使用学习率为0.1的小批量随机梯度下降作为优化算法。这与我们在线性回归例子中的相同,这说明了优化器的普适性。

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-LB0Yb4np-1636466983106)(C:\Users\Lunatic\Desktop\深度学习实验报告\实验1\18.jpg)]

小结
  • 使用高级 API,我们可以更简洁地实现 softmax 回归。
  • 从计算的角度来看,实现softmax回归比较复杂。在许多情况下,深度学习框架在这些著名的技巧之外采取了额外的预防措施,来确保数值的稳定性。这使我们避免了在实践中从零开始编写模型时可能遇到的陷阱。
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐