深度学习实验(一)

news/2024/10/18 2:37:26/

线性回归与 softmax

线性回归

线性回归概要

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

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

线性模型

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

p r i c e = w a r e a ⋅ a r e a + w a g e ⋅ a g e + b \mathrm{price} = w_{\mathrm{area}} \cdot \mathrm{area} + w_{\mathrm{age}} \cdot \mathrm{age} + b price=wareaarea+wageage+b

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

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

y ^ = w 1 x 1 + . . . + w d x d + b . \hat{y} = w_1 x_1 + ... + 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 可以很方便地引用我们整个数据集的 n n n 个样本。其中, 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

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

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

损失函数

在我们开始考虑如何用模型拟合(fit)数据之前,我们需要确定一个拟合程度的度量。损失函数 能够量化目标的实际值与预测 值之间的差距。通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为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.

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

由于平方误差函数中的二次方项,估计值 y ^ ( i ) \hat{y}^{(i)} y^(i) 和观测值 y ( i ) y^{(i)} y(i) 之间较大的差异将贡献更大的损失。为了度量模型在整个数据集上的质量,我们需计算在训练集 n n 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 ∗ \mathbf{w}^*, b^* w,b),这组参数能最小化在所有训练样本上的总损失。如下式:

w ∗ , b ∗ = * ⁡ a r g m i n w , 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)。首先,我们将偏置 b b b 合并到参数 w \mathbf{w} w 中。合并方法是在包含所有参数的矩阵中附加一列。我们的预测问题是最小化 ∥ y − X w ∥ 2 \|\mathbf{y} - \mathbf{X}\mathbf{w}\|^2 yXw2。这在损失平面上只有一个临界点,这个临界点对应于整个区域的损失最小值。将损失关于 w \mathbf{w} w的导数设为0,得到解析解(闭合形式):

w ∗ = ( X ⊤ X ) − 1 X ⊤ 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 ∂ 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} 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} w x \mathbf{x} x 都是向量。在这里,更优雅的向量表示法比系数表示法(如 w 1 , w 2 , … , w d w_1, w_2, \ldots, w_d w1,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^,现在我们可以通过给定的房屋面积 x 1 x_1 x1 和房龄 x 2 x_2 x2来估计一个未包含在训练数据中的新房屋价格。给定特征估计目标的过程通常称为预测(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)]

正态分布与平方损失

正态分布和线性回归之间的关系很密切。
简单的说,若随机变量 x x x 具有均值 μ \mu μ 和方差 σ 2 \sigma^2 σ2(标准差 σ \sigma σ),其正态分布概率密度函数如下:
p ( x ) = 1 2 π σ 2 exp ⁡ ( − 1 2 σ 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])

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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观测到特定 y y y可能性(likelihood):

P ( y ∣ x ) = 1 2 π σ 2 exp ⁡ ( − 1 2 σ 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} w b b b 的最优值是使整个数据集的可能性最大的值:

P ( y ∣ X ) = ∏ i = 1 n p ( 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 = 1 n 1 2 log ⁡ ( 2 π σ 2 ) + 1 2 σ 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} w b b b。现在第二项除了常数 1 σ 2 \frac{1}{\sigma^2} σ21外,其余部分和前面介绍的平方误差损失是一样的。
幸运的是,上面式子的解并不依赖于 σ \sigma σ。因此,在高斯噪声的假设下,最小化均方误差等价于对线性模型的最大似然估计。

线性回归的从零开始实现

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

import random
import torch
from d2l import torch as d2l

生成数据集

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

你可以将 ϵ \epsilon ϵ 视为捕获特征和标签时的潜在观测误差。在这里我们认为标准假设成立,即 ϵ \epsilon ϵ 服从均值为 0 0 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) + by += 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 = 10for 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的矩阵-向量乘法后加上偏置 b b b。注意,上面的 X w \mathbf{Xw} Xw 是一个向量,而 b b b是一个标量。广播机制使得当我们用一个向量加一个标量时,标量会被加到向量的每个分量上。

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_sizeparam.grad.zero_()

训练

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

  1. 初始化参数

  2. 重复,直到完成

    计算梯度 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(\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_lossfor 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 d2ltrue_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 d2ltrue_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类,也称为平方 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)

训练

通过深度学习框架的高级 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个标量来表示权重(带下标的 w w w),3个标量来表示偏置(带下标的 b b b)。
下面我们为每个输入计算三个未归一化的预测(logits): o 1 o_1 o1 o 2 o_2 o2 o 3 o_3 o3
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 . \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回归也是一个单层神经网络。由于计算每个输出 o 1 o_1 o1 o 2 o_2 o2 o 3 o_3 o3取决于所有输入 x 1 x_1 x1 x 2 x_2 x2 x 3 x_3 x3 x 4 x_4 x4,所以softmax回归的输出层也是全连接层。

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

全连接层的参数开销

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

softmax运算

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

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

为了将未归一化的预测变换为非负并且总和为1,同时要求模型保持可导。我们首先对每个未归一化的预测求幂,这样可以确保输出非负。为了确保最终输出的总和为1,我们再对每个求幂后的结果除以它们的总和。如下式:
y ^ = s o f t m a x ( o ) 其中 y ^ j = exp ⁡ ( o j ) ∑ k exp ⁡ ( o k ) \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)
容易看出对于所有的 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 之间的顺序,只会确定分配给每个类别的概率。因此,在预测过程中,我们仍然可以用下式来选择最有可能的类别。

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

小批量样本的矢量化

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

O = X W + b , Y ^ = s o f t m a x ( 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的每一行,我们先对所有项进行幂运算,然后通过求和对它们进行标准化。
X W + b \mathbf{X} \mathbf{W} + \mathbf{b} XW+b 的求和会使用广播,小批量的未归一化预测 O \mathbf{O} O 和输出概率 Y ^ \hat{\mathbf{Y}} Y^ 都是形状为 n × q n \times q n×q 的矩阵。

损失函数

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

对数似然

softmax函数给出了一个向量 y ^ \hat{\mathbf{y}} y^,我们可以将其视为给定任意输入 x \mathbf{x} x的每个类的估计条件概率。例如, y ^ 1 \hat{y}_1 y^1 = P ( y = 猫 ∣ x ) P(y=\text{猫} \mid \mathbf{x}) P(y=x)。假设整个数据集 { X , Y } \{\mathbf{X}, \mathbf{Y}\} {X,Y} 具有 n n n 个样本,其中索引 i i i 的样本由特征向量 x ( i ) \mathbf{x}^{(i)} x(i) 和独热标签向量 y ( i ) \mathbf{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(\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 = 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(\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 = 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.
在本节稍后的内容会讲到, :eqref:eq_l_cross_entropy 中的损失函数通常被称为 交叉熵损失(cross-entropy loss)。由于 y \mathbf{y} y 是一个长度为 q q q 的独热编码向量,所以除了一个项以外的所有项 j j j 都消失了。由于所有 y ^ j \hat{y}_j y^j 都是预测的概率,所以它们的对数永远不会大于 0 0 0
因此,如果正确地预测实际标签,即,如果实际标签 P ( y ∣ x ) = 1 P(\mathbf{y} \mid \mathbf{x})=1 P(yx)=1,则损失函数不能进一步最小化。
注意,这往往是不可能的。例如,数据集中可能存在标签噪声(某些样本可能被误标),或输入特征没有足够的信息来完美地对每一个样本分类。

softmax及其导数

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

l ( y , y ^ ) = − ∑ j = 1 q y j log ⁡ exp ⁡ ( o j ) ∑ k = 1 q exp ⁡ ( o k ) = ∑ j = 1 q y j log ⁡ ∑ k = 1 q exp ⁡ ( o k ) − ∑ j = 1 q y j o j = log ⁡ ∑ k = 1 q exp ⁡ ( o k ) − ∑ j = 1 q y j o j . \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.

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

∂ o j l ( y , y ^ ) = exp ⁡ ( o j ) ∑ k = 1 q exp ⁡ ( o k ) − y j = s o f t m a x ( o ) j − y j . \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得到)与实际发生的情况(由独热标签向量表示)之间的差异。从这个意义上讲,与我们在回归中看到的非常相似,其中梯度是观测值 y y y和估计值 y ^ \hat{y} y^之间的差异。

模型预测和评估

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

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

softmax回归的从零开始实现

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

import torch
from IPython import display
from d2l import torch as d2lbatch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

初始化模型参数

和之前线性回归的例子一样,这里的每个样本都将用固定长度的向量表示。原始数据集中的每个样本都是 28 × 28 28 \times 28 28×28 的图像。在本节中,我们将展平每个图像,把它们看作长度为784的向量。

在softmax回归中,我们的输出与类别一样多。。因此,权重将构成一个 784 × 10 784 \times 10 784×10 的矩阵,偏置将构成一个 1 × 10 1 \times 10 1×10 的行向量。与线性回归一样,我们将使用正态分布初始化我们的权重 W,偏置初始化为0。

num_inputs = 784
num_outputs = 10W = 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。

s o f t m a x ( X ) i j = exp ⁡ ( X i j ) ∑ k exp ⁡ ( X i k ) . \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) == yreturn 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] * ndef 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_metricsassert train_loss < 0.5, train_lossassert train_acc <= 1 and train_acc > 0.7, train_accassert 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:breaktrues = 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 d2lbatch_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 ⁡ ( o j ) ∑ k exp ⁡ ( o k ) \hat y_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)} y^j=kexp(ok)exp(oj),其中 y ^ j \hat y_j y^j是预测的概率分布。 o j o_j oj是未归一化的预测 o \mathbf{o} o的第 j j j个元素。如果 o k o_k ok中的一些数值非常大,那么 exp ⁡ ( o k ) \exp(o_k) exp(ok) 可能大于数据类型容许的最大数字(即 上溢(overflow))。这将使分母或分子变为inf(无穷大),我们最后遇到的是0、infnan(不是数字)的 y ^ j \hat y_j y^j。在这些情况下,我们不能得到一个明确定义的交叉熵的返回值。

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

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

log ⁡ ( y ^ j ) = log ⁡ ( exp ⁡ ( o j ) ∑ k exp ⁡ ( o k ) ) = log ⁡ ( exp ⁡ ( o j ) ) − log ⁡ ( ∑ k exp ⁡ ( o k ) ) = o j − log ⁡ ( ∑ k exp ⁡ ( o k ) ) . \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回归比较复杂。在许多情况下,深度学习框架在这些著名的技巧之外采取了额外的预防措施,来确保数值的稳定性。这使我们避免了在实践中从零开始编写模型时可能遇到的陷阱。

http://www.ppmy.cn/news/214481.html

相关文章

我转行程序员的那一年(五)

前端方向初定成&#xff0c;奔赴西安求前程 春节 坐在火车上&#xff0c;我给老板发了一段内容比较长的微信&#xff0c;分析了我认为App发展到这个地步的原因&#xff0c;也很希望以后能力达到的时候能把App做下去。尽管我这时已经开始学前端&#xff0c;但对未来做IT只是个保…

华为否认鸿蒙为噱,华为否认鸿蒙为噱头 鸿蒙系统手机会上市吗

近段时间&#xff0c;华为自主研发的鸿蒙操作系统&#xff0c;无疑是社会关注的焦点&#xff0c;但在华为一系列的操作以后&#xff0c;有关于鸿蒙系统手机的消息却越来越少&#xff0c;甚至有媒体表示鸿蒙系统只是一个噱头。近日&#xff0c;华为否认鸿蒙为噱头的说法&#xf…

华为鸿蒙系统多久上市,华为鸿蒙系统手机上市了吗 鸿蒙OS已达到安卓70-80%水平...

描述 (许云圣和蛋蛋说娱综合整理) 鸿蒙系统消息出来已经很久了&#xff0c;真正的鸿蒙手机上市了吗&#xff1f;其结果是没有。 华为CEO余承东曾透露过鸿蒙OS落地手机的情况&#xff0c;在去年八月份的时候&#xff0c;鸿蒙2.0就被推出来了&#xff0c;虽然鸿蒙OS智能机还没出来…

华为鸿蒙系统搭载在什么手机上,[财经]华为鸿蒙手机什么时候上市?搭载鸿蒙系统手机发布时间或在17日...

10月12日消息&#xff0c;继Mate 30系列之后&#xff0c;华为手机将再度预告&#xff0c;10月17日推出一款更具创新的全面屏手机&#xff0c;从目前的情况看&#xff0c;其应该是采用了屏下隐藏式前置摄像头设计&#xff0c;同时还有可能搭载鸿蒙系统。 从华为晒出的预告图来看…

华为手机什么时候更新鸿蒙系统_华为什么时候用鸿蒙系统 鸿蒙OS上市时间与支持机型...

华为鸿蒙系统什么时候出? 华为消费者业务首届开发者大会今日举行(8月9日)。华为首次公布了自研操作系统“鸿蒙”。余承东表示&#xff0c;手机会优先安卓系统&#xff0c;但如果不能用&#xff0c;随时可以启用鸿蒙&#xff0c;从安卓系统迁移到鸿蒙OS非常便捷&#xff0c;只需…

华为鸿蒙手机什么时候上市5g,5g手机什么时候上市 华为5g手机何时上市?

5g商用了&#xff0c;5g移动套餐也出来了&#xff0c;5g手机什么时候上市呢&#xff1f;咱们来看看各大手机品牌商的表态吧。不过小编建议大家看看就好啦&#xff0c;新出的5g手机比较贵&#xff0c;而且5g网络还在建设中&#xff0c;没必要花大价钱买一个不能用5g网络的5g手机…

华为鸿蒙啥时发布,华为鸿蒙OS手机Beta版什么时候上市-发布时间

今日&#xff0c;华为官宣了鸿蒙OS手机Beta版的发布时间&#xff0c;该系统已确认将会采用全新的语言设计&#xff0c;接下来小编就为大家带来了最新信息&#xff0c;一起看看吧&#xff01; 一、什么时候发布 根据新浪科技的最新报道来看&#xff0c;华为软件部副总裁毛玉敏表…

华为鸿蒙2.0什么时候上市,华为鸿蒙2.0上市时间什么时候

现在大部分智能手机的操作系统主要就是用的安卓和苹果的IOS了&#xff0c;我们国家一直未有自己的操作系统可用。早前华为曾透露已经在自主研发鸿蒙这个操作系统了&#xff0c;在昨日的开发者大会上&#xff0c;华为消费者业务CEO余承东发表主题演讲&#xff0c;并正式发布鸿蒙…