700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > 动手学深度学习v2 p2 线性神经网络 线性回归 softmax回归

动手学深度学习v2 p2 线性神经网络 线性回归 softmax回归

时间:2019-02-15 20:02:17

相关推荐

动手学深度学习v2 p2 线性神经网络 线性回归 softmax回归

3. 线性神经网络

回归(regression)是能为一个或多个自变量与因变量之间关系建模的一类方法。 在自然科学和社会科学领域,回归经常用来表示输入和输出之间的关系。

在机器学习领域中的大多数任务通常都与预测(prediction)有关。 当我们想预测一个数值时,就会涉及到回归问题。 常见的例子包括:预测价格(房屋、股票等)、预测住院时间(针对住院病人等)、 预测需求(零售销量等)。 但不是所有的预测都是回归问题。 在后面的章节中,我们将介绍分类问题。分类问题的目标是预测数据属于一组类别中的哪一个。

3.1. 线性回归

线性回归的基本元素

Here is the text extracted from the file:

3.1.1. 线性回归的基本元素

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

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

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

1.线性模型

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

中的warea和wage称为权重(weight),权重决定了每个特征对我们预测值的影响。b称为偏置(bias)、偏移量(offset)或截距(intercept)。偏置是指当所有特征都取值为0时,预测值应该为多少。即使现实中不会有任何房子的面积是0或房龄正好是0年,我们仍然需要偏置项。如果没有偏置项,我们模型的表达能力将受到限制。 严格来说,(3.1.1)是输入特征的一个仿射变换(affine transformation)。仿射变换的特点是通过加权和对特征进行线性变换(linear transformation), 并通过偏置项来进行平移(translation)。

给定一个数据集,我们的目标是寻找模型的权重w和偏置b:当给定从x的同分布中取样的新样本特征时,这组权重向量和偏置能够使得新样本预测标签的误差尽可能小。输出的预测值由输入特征通过线性模型的仿射变换决定,仿射变换由所选权重和偏置确定。

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

将所有特征放到向量x中,并将所有权重放到向量w中,我们可以用点积形式来简洁地表达模型:

在 (3.1.3)中,向量x对应于单个数据样本的特征。用符号表示的矩阵X可以很方便地引用我们整个数据集的n个样本。其中,X的每一行是一个样本,每一列是一种特征。

对于特征集合X,预测值y^(通常使用“尖角”符号表示y的估计值)可以通过矩阵-向量乘法表示为:

y^ = Xw + b

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

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

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

2.损失函数

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

常数1⁄2不会带来本质的差别,但这样在形式上稍微简单一些(因为当我们对损失函数求导后常数系数为1)。由于训练数据集并不受我们控制,所以经验误差只是关于模型参数的函数。为了进一步说明,来看下面的例子。 我们为一维情况下的回归问题绘制图像,如图3.1.1所示。

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

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

:

3.解析解

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

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

4.随机梯度下降

即使在我们无法得到解析解的情况下,我们仍然可以有效地训练模型。 在许多任务上,那些难以优化的模型效果要更好。 因此,弄清楚如何训练这些难以优化的模型是非常重要的。

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

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

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

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

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

公式(3.1.10)中的w和x都是向量。 在这里,更优雅的向量表示法比系数表示法(如w1, w2, ..., wd)更具可读性。|B|表示每个小批量中的样本数,这也称为批量大小(batch size)。 η表示学习率(learning rate)。 批量大小和学习率的值通常是手动预先指定,而不是通过模型训练得到的。 这些可以调整但不在训练过程中更新的参数称为超参数(hyperparameter)。 调参(hyperparameter tuning)是选择超参数的过程。 超参数通常是我们根据训练迭代结果来调整的, 而训练迭代结果是在独立的验证数据集(validation dataset)上评估得到的。

在训练了预先确定的若干迭代次数后(或者直到满足某些其他停止条件后), 我们记录下模型参数的估计值,表示为w^, b^。 但是,即使我们的函数确实是线性的且无噪声,这些估计值也不会使损失函数真正地达到最小值。 因为算法会使得损失向最小值缓慢收敛,但却不能在有限的步数内非常精确地达到最小值。

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

5.用模型进行预测

给定“已学习”的线性回归模型,现在我们可以通过房屋面积x1和房龄x2来估计一个(未包含在训练数据中的)新房屋价格。给定特征估计目标的过程通常称为预测(prediction)或推断(inference)。

本书将尝试坚持使用预测这个词。虽然推断这个词已经成为深度学习的标准术语,但其实推断这个词有些用词不当。在统计学中,推断更多地表示基于数据集估计参数。当深度学习从业者与统计学家交谈时,术语的误用经常导致一些误解。

2.矢量化加速

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

%matplotlib inlineimport mathimport timeimport numpy as npimport torchfrom d2l import torch as d2l# 为了说明矢量化为什么如此重要,我们考虑对向量相加的两种方法。 我们实例化两个全为1的10000维向量。 在一种方法中,我们将使用Python的for循环遍历向量; 在另一种方法中,我们将依赖对+的调用。# 由于在本书中我们将频繁地进行运行时间的基准测试,所以我们定义一个计时器:n = 100000a = 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()# 现在我们可以对工作负载进行基准测试。# 首先,我们使用for循环,每次执行一位的加法。c = torch.zeros(n)timer = Timer()for i in range(n):c[i] = a[i] + b[i]f'{timer.stop():.5f} sec'# '0.89696 sec'# 或者,我们使用重载的+运算符来计算按元素的和。timer.start()d = a + bf'{timer.stop():.5f} sec'# '0.00019 sec'# 结果很明显,第二种方法比第一种方法快得多。 矢量化代码通常会带来数量级的加速。 另外,我们将更多的数学运算放到库中,而无须自己编写那么多的计算,从而减少了出错的可能性。

直接张量之间运算

3.正态分布与平方损失

接下来,我们通过对噪声分布的假设来解读平方损失目标函数。

正态分布和线性回归之间的关系很密切。 正态分布(normal

distribution),也称为高斯分布(Gaussian distribution),

最早由德国数学家高斯(Gauss)应用于天文学研究。

简单的说,若随机变量x具有均值μ和方差σ^2(标准差σ),其正态分布概率密度函数如下:

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

def normal(x, mu, sigma):p = 1 / math.sqrt(2 * math.pi * sigma**2)return p * np.exp(-0.5 / sigma**2 * (x - mu)**2)# 再次使用numpy进行可视化x = np.arange(-7, 7, 0.01)# 均值和标准差对params = [(0, 1), (0, 2), (3, 1)]d2l.plot(x, [normal(x, mu, sigma) for mu, sigma in params], xlabel='x',ylabel='p(x)', figsize=(4.5, 2.5),legend=[f'mean {mu}, std {sigma}' for mu, sigma in params])

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

均方误差损失函数(简称均方损失)可以用于线性回归的一个原因是:

我们假设了观测中包含噪声,其中噪声服从正态分布。 噪声正态分布如下式:

, 其中,。

因此,我们现在可以写出通过给定的x观测到特定y的似然:

现在,根据极大似然估计法,参数w和b的最优值是使整个数据集的似然最大的值:

根据极大似然估计法选择的估计量称为极大似然估计量。

虽然使许多指数函数的乘积最大化看起来很困难,

但是我们可以在不改变目标的前提下,通过最大化似然对数来简化。

由于历史原因,优化通常是说最小化而不是最大化。

我们可以改为最小化负对数似然-logP(y|X)。

由此可以得到的数学公式是:

现在我们只需要假设σ是某个固定常数就可以忽略第一项,

因为第一项不依赖于w和b。

现在第二项除了常数1/σ^2外,其余部分和前面介绍的均方误差是一样的。

幸运的是,上面式子的解并不依赖于σ。

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

4.从线性回归到深度网络

到目前为止,我们只谈论了线性模型。 尽管神经网络涵盖了更多更为丰富的模型,我们依然可以用描述神经网络的方式来描述线性模型, 从而把线性模型看作一个神经网络。 首先,我们用“层”符号来重写这个模型。

神经网络图

深度学习从业者喜欢绘制图表来可视化模型中正在发生的事情。 在图3.1.2中,我们将线性回归模型描述为一个神经网络。 需要注意的是,该图只显示连接模式,即只显示每个输入如何连接到输出,隐去了权重和偏置的值。

小结

机器学习模型中的关键要素是训练数据、损失函数、优化算法,还有模型本身。

矢量化使数学表达上更简洁,同时运行的更快。

最小化目标函数和执行极大似然估计等价。

线性回归模型也是一个简单的神经网络。

题目

假设我们有一些数据x1, ..., xn ∈ R。我们的目标是找到一个常数b,使得最小化∑i(xi - b)^2。 找到最优值b的解析解。这个问题及其解与正态分布有什么关系?

找到最优值b的解析解:

要找到使$\sum_i (x_i - b)^2$最小的$b$,可以将导数设为0:

$\frac{d}{db} \sum_i (x_i - b)^2 = -2 \sum_i (x_i - b) = 0$

解得:$b = \frac{1}{n}\sum_i x_i$,即$b$等于所有$x_i$的均值。

这个问题及其解与正态分布有关系:

$b$使残差平方和$\sum_i (x_i - b)^2$最小,等价于最大化观测数据$x_i$来自均值为$b$,方差为常数的正态分布的似然。

推导出使用平方误差的线性回归优化问题的解析解。为了简化问题,可以忽略偏置b(我们可以通过向X添加所有值为1的一列来做到这一点)。 用矩阵和向量表示法写出优化问题(将所有数据视为单个矩阵,将所有目标值视为单个向量)。计算损失对w的梯度。通过将梯度设为0、求解矩阵方程来找到解析解。什么时候可能比使用随机梯度下降更好?这种方法何时会失效?

优化问题表示:

计算梯度:

梯度设为0,解方程得:

当样本量少,计算解析解的代价小于随机梯度下降,否则随机梯度下降更有效。当$X^TX$不可逆时解析解会失效。

假定控制附加噪声ε的噪声模型是指数分布。也就是说,p(ε) = (1/2)exp(-|ε|) 写出模型-logP(y|X)下数据的负对数似然。请试着写出解析解。提出一种随机梯度下降算法来解决这个问题。哪里可能出错?(提示:当我们不断更新参数时,在驻点附近会发生什么情况)请尝试解决这个问题。

负对数似然:

没有解析解

可以使用随机梯度下降,但存在收敛到非最小点的风险。可以试试平方 Loss 作为替代。

下面是torch代码实现:

import torch# 生成数据X = torch.randn(100, 10) y = torch.randn(100)# 定义模型w = torch.randn(10, requires_grad=True)b = torch.randn(1, requires_grad=True)# 定义损失函数def loss_fn(y_pred, y):return torch.sum((y_pred - y)**2)# 训练optimizer = torch.optim.SGD([w, b], lr=1e-3)for iter in range(100):y_pred = X @ w + bloss = loss_fn(y_pred, y)loss.backward()optimizer.step()optimizer.zero_grad()# 输出训练后的参数 print(w) print(b)

这个代码实现了最小化平方损失来训练线性回归模型,并输出了训练后的权重w和偏置b。我们可以看到随机梯度下降可以用于求解这个线性回归问题。

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

在了解线性回归的关键思想之后,我们可以开始通过代码来动手实现线性回归了。在这一节中,我们将从零开始实现整个方法, 包括数据流水线、模型、损失函数和小批量随机梯度下降优化器。虽然现代的深度学习框架几乎可以自动化地进行所有这些工作,但从零开始实现可以确保我们真正知道自己在做什么。 同时,了解更细致的工作原理将方便我们自定义模型、自定义层或自定义损失函数。 在这一节中,我们将只使用张量和自动求导。 在之后的章节中,我们会充分利用深度学习框架的优势,介绍更简洁的实现方式。

1.生成数据集

为了简单起见,我们将根据带有噪声的线性模型构造一个人造数据集。 我们的任务是使用这个有限样本的数据集来恢复这个模型的参数。 我们将使用低维数据,这样可以很容易地将其可视化。 在下面的代码中,我们生成一个包含1000个样本的数据集, 每个样本包含从标准正态分布中采样的2个特征。 我们的合成数据集是一个矩阵X∈R1000×2。 我们使用线性模型参数w=[2, -3.4]、b=4.2 和噪声项ε生成数据集及其标签: y = Xw + b + ε. (3.2.1) ε可以视为模型预测和标签时的潜在观测误差。 在这里我们认为标准假设成立,即ε服从均值为0的正态分布。 为了简化问题,我们将标准差设为0.01。下面的代码生成合成数据集。

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

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

%matplotlib inlineimport randomimport torchfrom d2l import torch as d2ldef synthetic_data(w, b, num_examples): #@save # 定义合成数据生成函数synthetic_data# w: 权重参数的向量# b: 偏置量# num_examples: 生成的数据集样本数量 """生成y=Xw+b+噪声"""X = torch.normal(0, 1, (num_examples, len(w))) # 使用正态分布随机生成特征矩阵X,num_examples行,len(w)列y = torch.matmul(X, w) + b # 计算线性方程得到标签y的一部分,矩阵乘法计算X和w的乘积print('y.shape',y.shape)#y = X*w + b # X 和 w 的维度不匹配,不能直接相乘。X 是 (num_examples, len(w)) 的二维Tensor,w 是一维Tensor。y += torch.normal(0, 0.1, y.shape) # 添加正态噪声到标签y,噪声方差为0.01return X, y.reshape((-1, 1)) # 返回特征矩阵X和reshape后的标签y,reshape为num_examples行1列 -1 表示自适应这个维度的大小。也就是说,这一维的大小将根据其他维度来自动计算。1 表示TensorShape的第二个维度大小为1。true_w = torch.tensor([2, -3.4]) # 定义真实的参数权重向量true_w,2个元素true_b = 4.2 # 定义真实的偏置量true_b features, labels = synthetic_data(true_w, true_b, 1000) # 生成1000个样本的合成数据集,赋值到features和labels# true_w: 传入之前定义的真实权重 # true_b: 传入之前定义的真实偏置量# 1000: 生成样本数量print('features:', features[0],'\nlabel:', labels[0]) # 打印第一个样本的特征和标签d2l.set_figsize() d2l.plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1); # 绘制特征矩阵第2列和标签的散点图,点大小为1y.shape torch.Size([1000])features: tensor([0.0875, 0.5291]) label: tensor([2.5143])

2.读取数据集

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

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

通常,我们利用GPU并行运算的优势,处理合理大小的“小批量”。 每个样本都可以并行地进行模型计算,且每个样本损失函数的梯度也可以被并行计算。 GPU可以在处理几百个样本时,所花费的时间不比处理一个样本时多太多。

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

def data_iter(batch_size, features, labels): # batch_size: 每次迭代返回的特征和标签数目num_examples = len(features) # features和labels的数目indices = list(range(num_examples)) # 生成索引列表# 这些样本是随机读取的,没有特定顺序random.shuffle(indices) # 打乱索引顺序for i in range(0, num_examples, batch_size): # 从索引中循环读取特征和标签batch_indices = torch.tensor(indices[i: i + batch_size]) # 本次取出的样本索引# a=[1,2,3]# print(a[:5]) #不会报错的yield features[batch_indices], labels[batch_indices] # 按照索引取出特征和标签a=list(range(10))random.shuffle(a)print(a)batch_size = 10for X, y in data_iter(batch_size, features, labels):print(X, '\n', y)break[6, 2, 9, 4, 0, 7, 3, 5, 8, 1]tensor([[ 0.4282, -0.6740],[-0.3198, -0.1568],[-0.8597, 0.6003],[-0.6630, 0.4034],[ 0.1831, -0.5057],[ 0.0660, 0.9484],[-0.6181, 1.1546],[ 2.1839, -0.3741],[-0.8864, 0.3907],[ 1.1164, -0.7436]]) tensor([[ 7.3615],[ 4.1884],[ 0.3505],[ 1.5880],[ 6.2715],[ 1.2592],[-0.9332],[ 9.8630],[ 1.0079],[ 8.7573]])

当我们运行迭代时,我们会连续地获得不同的小批量,直至遍历完整个数据集。 上面实现的迭代对教学来说很好,但它的执行效率很低,可能会在实际问题上陷入麻烦。 例如,它要求我们将所有数据加载到内存中,并执行大量的随机内存访问。 在深度学习框架中实现的内置迭代器效率要高得多, 它可以处理存储在文件中的数据和数据流提供的数据。

3.初始化模型参数、定义模型、损失函数、优化算法

初始化模型参数

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

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

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

定义模型

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

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

损失函数

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

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

优化算法

正如我们在3.1节中讨论的,线性回归有解析解。 尽管线性回归有解析解,但本书中的其他模型却没有。 这里我们介绍小批量随机梯度下降。

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

7.训练

现在我们已经准备好了模型训练所有需要的要素,可以实现主要的训练过程部分了。 理解这段代码至关重要,因为从事深度学习后, 相同的训练过程几乎一遍又一遍地出现。 在每次迭代中,我们读取一小批量训练样本,并通过我们的模型来获得一组预测。 计算完损失后,我们开始反向传播,存储每个参数的梯度。 最后,我们调用优化算法sgd来更新模型参数。

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

初始化参数

重复以下训练,直到完成

计算梯度

更新参数

在每个迭代周期(epoch)中,我们使用data_iter函数遍历整个数据集, 并将训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。 这里的迭代周期个数num_epochs和学习率lr都是超参数,分别设为3和0.03。 设置超参数很棘手,需要通过反复试验进行调整。 我们现在忽略这些细节,以后会在11节中详细介绍。

w = torch.normal(0, 0.01, size=(2,1), requires_grad=True) # 初始化w,均值为0,标准差为0.01,大小为(2,1),需要梯度 b = torch.zeros(1, requires_grad=True) # 初始化b,值为0,需要梯度b = torch.zeros(1, requires_grad=True)def linreg(X, w, b): #@save _"""线性回归模型"""_return torch.matmul(X, w) + b # X是输入,w和b是模型参数,matmul做矩阵乘法def squared_loss(y_hat, y): #@save _"""均方损失"""_return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2 # y_hat是预测,y是标签,计算均方误差def sgd(params, lr, batch_size): #@save _"""小批量随机梯度下降"""_with torch.no_grad():for param in params:param -= lr * param.grad / batch_size # 对每个参数param,按照lr和batch_size更新param.grad.zero_() # 梯度清零lr = 0.03num_epochs = 10net = linregloss = 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}')print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')print(f'b的估计误差: {true_b - b}')epoch 1, loss 0.038583epoch 10, loss 0.005485w的估计误差: tensor([-0.0028, -0.0045], grad_fn=<SubBackward0>)b的估计误差: tensor([-0.0045], grad_fn=<RsubBackward1>)

因为我们使用的是自己合成的数据集,所以我们知道真正的参数是什么。 因此,我们可以通过比较真实参数和通过训练学到的参数来评估训练的成功程度。 事实上,真实参数和通过训练学到的参数确实非常接近。

注意,我们不应该想当然地认为我们能够完美地求解参数。 在机器学习中,我们通常不太关心恢复真正的参数,而更关心如何高度准确预测参数。 幸运的是,即使是在复杂的优化问题上,随机梯度下降通常也能找到非常好的解。 其中一个原因是,在深度网络中存在许多参数组合能够实现高度精确的预测。

小结、练习

我们学习了深度网络是如何实现和优化的。在这一过程中只使用张量和自动微分,不需要定义层或复杂的优化器。

这一节只触及到了表面知识。在下面的部分中,我们将基于刚刚介绍的概念描述其他模型,并学习如何更简洁地实现其他模型

练习

如果我们将权重初始化为零,会发生什么。算法仍然有效吗

在进行梯度下降优化时,所有样本对权重更新的贡献会完全相同。这会导致权重无法得到有效的更新,算法将无法正常工作

true_w = torch.tensor([0.,0.]) # 定义真实的参数权重向量true_w,2个元素print(X.shape,w.shape)torch.Size([1000, 2]) torch.Size([2])

假设试图为电压和电流的关系建立一个模型。自动微分可以用来学习模型的参数吗

可以使用自动微分来学习建模电压和电流关系的参数。我们可以定义模型,然后使用自动微分计算损失函数关于模型参数的梯度,通过梯度下降迭代参数以拟合数据。import torch# 模型class Model(torch.nn.Module):def __init__(self):super().__init__()self.linear = torch.nn.Linear(1, 1)def forward(self, x):return self.linear(x)# 生成数据 X = torch.rand(20, 1) y = 3*X + 0.1*torch.randn(20, 1)# 构建模型model = Model()# 定义损失函数和优化器criterion = torch.nn.MSELoss()optimizer = torch.optim.SGD(model.parameters(), lr=0.01)# 训练for epoch in range(100):y_pred = model(X)loss = criterion(y_pred, y)optimizer.zero_grad() loss.backward() optimizer.step()

能基于普朗克定律使用光谱能量密度来确定物体的温度吗?

可以基于普朗克定律使用光谱能量密度计算物体的温度。根据普朗克定律,某一温度下的黑体辐射与波长的光谱能量密度可决定其温度。所以如果测量得到光谱能量密度,可以反算出物体的温度。

计算二阶导数时可能会遇到什么问题?这些问题可以如何解决?

计算二阶导数时,可能会遇到数值不稳定的问题。这是因为通过数值近似计算二阶导数时,误差会被放大。可以通过一些技巧来缓解这一问题,例如使用中心差分法、调整步长等。

为什么在squared_loss函数中需要使用reshape函数?

在squared_loss函数中需要reshape是为了将预测输出和标签矢量展平,以便可以进行按元素相减并计算平方差。

尝试使用不同的学习率,观察损失函数值下降的快慢。

使用不同的学习率会影响损失函数下降的快慢。学习率越大,损失函数下降越快,但是也容易越过最小值;学习率越小,虽然不易越过最小值但收敛速度减慢。

如果样本个数不能被批量大小整除,data_iter函数的行为会有什么变化?

如果样本个数不能被批量大小整除,最后一个批次的样本数将小于批量大小。这会造成最后一个批次输入数据的形状与前面不同。我们可以补充0或舍弃最后一个批次来保证形状一致。

常见问题

loss.sum().backward()中对于sum()的理解

对其中的的 l.sum().backward() 并不是很理解,对损失函数求导即可,为什么要先求和再求导。

其实在2.5节,沐神已经明确的讲解了这个问题,只不过当时我并没有get到这个点。

import torchx = torch.arange(4.0)x.requires_grad_(True)y = 2 * torch.dot(x, x)y.backward()x.grad# 输出: tensor([ 0., 4., 8., 12.])x.grad.zero_()y = x.sum()y.backward()x.grad# 输出: tensor([1., 1., 1., 1.])x.grad.zero_()y = x * x# 等价于y.backward(torch.ones(len(x)))y.sum().backward()x.grad#输出: tensor([0., 2., 4., 6.])

第一段代码: y = 2 * torch.dot(x,x) 的结果为28为一个标量

第二段代码: y = x.sum 进行反向传播后所得的各方向梯度均为1,而结果为8一个标量

第三段代码: y = x * x 的结果为 [0 , 1, 4, 9] 为一个向量

y不是标量时,向量y关于向量x的导数的最自然解释是一个矩阵。对于高阶和高维的yx,求导的结果可以是一个高阶张量。

然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括深度学习中),但当我们调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。这里,我们的目的不是计算微分矩阵,而是批量中每个样本单独计算的偏导数之和。

引用自 2.5.自动求导

一个向量是不进行backward操作的,而sum()后,由于梯度为1,所以对结果不产生影响。反向传播算法一定要是一个标量才能进行计算。

对于末尾处的 l.sum().backward() 不是很理解。backward是通过反向传播函数来求梯度,在查阅了一些资料后,通过以下例子即可很好理解。

import torcha = torch.randn(size=(), requires_grad=True)b = torch.randn(size=(), requires_grad=True)c = torch.randn(size=(), requires_grad=True)c = a * bc.backward()print( a.grad == b,a)print( b.grad == a,b)output:tensor(True) tensor(-0.7874, requires_grad=True)tensor(True) tensor(0.0025, requires_grad=True)

若在 torch 中 对定义的变量 requires_grad 的属性赋为 True ,那么此变量即可进行梯度以及导数的求解,在以上代码中,a,b,c 都可以理解为数学中的x,y,z进行运算,c 分别对 a,b 求导的结果为 b,a。

当c.backward() 语句执行后,会自动对 c 表达式中 的可求导变量进行方向导数的求解,并将对每个变量的导数表达存储到 变量名.grad 中。

可如此理解 c.backward() = a.gradI+ b.gradj

3.3.线性回归的简洁实现

在3.2节中,我们只运用了:(1)通过张量来进行数据存储和线性代数; (2)通过自动微分来计算梯度。 实际上,由于数据迭代器、损失函数、优化器和神经网络层很常用, 现代深度学习库也为我们实现了这些组件。

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

1.生成数据集、读取数据集

我们可以调用框架中现有的API来读取数据。 我们将featureslabels作为API的参数传递,并通过数据迭代器指定batch_size。 此外,布尔值is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据。

import numpy as npimport torchfrom torch.utils import datafrom d2l import torch as d2l# 生成数据集true_w = torch.tensor([2, -3.4])true_b = 4.2features, labels = d2l.synthetic_data(true_w, true_b, 1000)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 = 10data_iter = load_array((features, labels), batch_size)next(iter(data_iter))[tensor([[ 3.0515, -1.9356],[-1.7380, 1.0954],[-1.7617, -0.2488],[ 0.2510, 2.2884],[ 1.1146, 0.1146],[-0.2739, -0.0878],[ 1.5473, -0.0908],[-0.9345, -0.7377],[-0.3061, 0.7877],[-0.5368, -1.5448]]),tensor([[16.8753],[-2.9899],[ 1.5094],[-3.0853],[ 6.0264],[ 3.9463],[ 7.6005],[ 4.8626],[ 0.9270],[ 8.3821]])]

使用data_iter的方式与我们在3.2节中使用data_iter函数的方式相同。为了验证是否正常工作,让我们读取并打印第一个小批量样本。 与3.2节不同,这里我们使用iter构造Python迭代器,并使用next从迭代器中获取第一项。

3.定义模型、初始化模型参数、定义损失函数、优化算法

当我们在3.2节中实现线性回归时, 我们明确定义了模型参数变量,并编写了计算的代码,这样通过基本的线性代数运算得到输出。 但是,如果模型变得更加复杂,且当我们几乎每天都需要实现模型时,自然会想简化这个过程。 这种情况类似于为自己的博客从零开始编写网页。 做一两次是有益的,但如果每个新博客就需要工程师花一个月的时间重新开始编写网页,那并不高效。

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

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

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

# nn是神经网络的缩写from torch import nnnet = nn.Sequential(nn.Linear(2, 1))# 4. 初始化模型参数# 在使用net之前,我们需要初始化模型参数。 如在线性回归模型中的权重和偏置。 深度学习框架通常有预定义的方法来初始化参数。 在这里,我们指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采样, 偏置参数将初始化为零。# 正如我们在构造nn.Linear时指定输入和输出尺寸一样, 现在我们能直接访问参数以设定它们的初始值。 我们通过net[0]选择网络中的第一个图层, 然后使用weight.data和bias.data方法访问参数。 我们还可以使用替换方法normal_和fill_来重写参数值。#list of layers Sequential一个有序的容器,神经网络模块将按照在传入构造器的顺序依次被添加到计算图中执行, # 同时以神经网络模块为元素的有序字典也可以作为传入参数。net[0].weight.data.normal_(0, 0.01)print(net[0].bias.data.fill_(0))# 5. 定义损失函数 计算均方误差使用的是MSELoss类,也称为l2平方范数。 默认情况下,它返回所有样本损失的平均值。loss = nn.MSELoss()# 6. 定义优化算法 小批量随机梯度下降算法是一种优化神经网络的标准工具, PyTorch在optim模块中实现了该算法的许多变种。 当我们实例化一个SGD实例时,我们要指定优化的参数 (可通过net.parameters()从我们的模型中获得)以及优化算法所需的超参数字典。 小批量随机梯度下降只需要设置lr值,这里设置为0.03。trainer = torch.optim.SGD(net.parameters(), lr=0.03)tensor([0.])

7.训练

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

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

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

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

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

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

num_epochs = 3for 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}')

下面我们比较生成数据集的真实参数和通过有限数据训练获得的模型参数。 要访问参数,我们首先从net访问所需的层,然后读取该层的权重和偏置。 正如在从零开始实现中一样,我们估计得到的参数与生成数据的真实参数非常接近。

w = net[0].weight.dataprint('w的估计误差:', true_w - w.reshape(true_w.shape))b = net[0].bias.dataprint('b的估计误差:', true_b - b)w的估计误差: tensor([-0.0003, 0.0002])b的估计误差: tensor([0.0008])

8.小结、练习

我们可以使用PyTorch的高级API更简洁地实现模型。

在PyTorch中,data模块提供了数据处理工具,nn模块定义了大量的神经网络层和常见损失函数。

我们可以通过_结尾的方法将参数替换,从而初始化参数。

如果将小批量的总损失替换为小批量损失的平均值,需要如何更改学习率?

# 5. 定义损失函数 计算均方误差使用的是MSELoss类,也称为l2平方范数。 默认情况下,它返回所有样本损失的平均值。loss = nn.MSELoss(reduction='sum')# 6. 定义优化算法 小批量随机梯度下降算法是一种优化神经网络的标准工具, PyTorch在optim模块中实现了该算法的许多变种。 当我们实例化一个SGD实例时,我们要指定优化的参数 (可通过net.parameters()从我们的模型中获得)以及优化算法所需的超参数字典。 小批量随机梯度下降只需要设置lr值,这里设置为0.03。trainer = torch.optim.SGD(net.parameters(), lr=0.03/batch_size) #指定需要优化的参数 还有学习率num_epochs = 3for epoch in range(num_epochs):for X, y in data_iter:l = loss(net(X), y) #这里的net返回输入x经过定义的网络所计算出的值trainer.zero_grad() #清除上一次的梯度值 避免叠加上下面的数据所带来的梯度l.backward() #损失函数进行反向传播 求参数的梯度trainer.step() #trainer步进 根据指定的优化算法进行参数的寻优l = loss(net(features), labels) #根据上面的参数优化结果计算参数对整个数据集的拟合状态 以loss进行反映print(f'epoch {epoch + 1}, loss {l:f}')

def **init**(self, size\_average: Any = None,reduce: Any = None,reduction: str = 'mean') -> None创建一个评估输入x和目标y之间的每一个元素的均方误差(平方L2范数)的准则。未约简的(即reduction设置为'none')损失可以表示为:l(x, y) = L = {l1, ..., lN}^T, ln = (xn - yn)^2,其中N是批量大小。如果reduction不是'none'(默认为'mean'),则:l(x, y) = ⎧⎨⎩\\frac{1}{N}\\sum\_n ln, if reduction = 'mean' \\sum\_n ln, if reduction = 'sum'x和y是具有n个元素的任意形状的张量。mean操作仍在所有元素上进行运算,并除以n。如果将reduction设置为'sum',可以避免除以n的操作。形状:输入:(\*),其中\*表示任意数量的维度。 目标:与输入相同的形状(\*)。例子:\>>> loss = nn.MSELoss() \>>> input = torch.randn(3, 5, requires_grad=True)\>>> target = torch.randn(3, 5)\>>> output = loss(input, target)\>>> output.backward()参数:size_average – 弃用(见reduction)。默认情况下,损失在批处理中的每个损失元素上平均。注意,对于某些损失,每个样本有多个元素。如果将size_average字段设置为False,则损失将为每个小批量求和。reduce为False时将被忽略。默认值为True。reduce – 弃用(见reduction)。默认情况下,根据size_average,损失在每个小批量的观测结果上平均或求和。当reduce为False时,返回每个批处理元素的损失,而不是忽略size_average。默认值为True。reduction – 指定应用于输出的减少方式:'none' | 'mean' | 'sum'。'none':不会应用约简;'mean':输出的总和将被输出元素数量除;'sum':输出将被求和。注:size_average和reduce正在被弃用,与此同时,指定这两个参数中的任何一个都将覆盖reduction。默认值为'mean'。

查看深度学习框架文档,它们提供了哪些损失函数和初始化方法?用Huber损失代替原损失,即

def Huber(pred , true , sigma = 0.005):error = abs(pred.detach().numpy() - true.detach().numpy())return torch.tensor(np.where(error < sigma , error - sigma / 2 , 0.5 * sigma * error ** 2) , requires_grad= True).mean()loss=Huberepoch 1, loss 0.078279epoch 2, loss 0.078279epoch 3, loss 0.078279例如TensorFlow中常用的损失函数有:tf.losses.mean_squared_error: 平方误差损失tf.losses.categorical_crossentropy: 交叉熵损失tf.losses.huber_loss: Huber损失初始化方法有:tf.initializers.zeros:全0初始化tf.initializers.ones:全1初始化tf.initializers.random_normal:随机正态初始化

如何访问线性回归的梯度?

通过loss函数直接得到每个样本的损失。然后对损失调用.backward()可以自动计算梯度,梯度保存在模型参数的grad属性中

常见问题

相关概念

net - 模型变量,用于定义我们的神经网络模型Sequential - PyTorch中的一个类,用于顺序地堆叠多个网络层parameters - 模型可学习的参数cuda - 将张量移动到GPU上的方法optim - 优化器模块SGD - 随机梯度下降优化器 SGD:stochastic gradient descent:随机梯度下降lr - 学习率loss - 损失函数CrossEntropyLoss - 交叉熵损失函数train - 训练模式backward - 反向传播step - 优化器执行一步优化更新的方法eval - 切换到评估模式no_grad - 关闭autograd的 gradient tracking

数据并行 batch size/机器数量

LSH(LocalitySensitive Hashing),中文叫做局部敏感哈希",它是一种针对海量高维数据的快速最近查找算法。在信息检索、数据挖掘以及推荐系统等应用中我们经常会遇到的一个问题就是面临着海量的高维数据,查找最近邻。如果使用线性查找,那么对于低维数据效率尚可,而对于高维数据,就显得非常耗时了。为了解决这样的问题,人们设计了一种特殊的hash函数,使得2个相似度很高的数据以较高的概率映射成同一个hash值,而令2个相似度很低的数据以极低的概率映射成同一个hash值。我们把这样的函数,叫LSH(局部敏感哈希)。汇SH最根本的作用,就是能高效处理海量高维数据的最近邻问题。

3.4.softmax回归

在3.1节中我们介绍了线性回归。 随后,在3.2节中我们从头实现线性回归。 然后,在3.3节中我们使用深度学习框架的高级API简洁实现线性回归。

回归可以用于预测多少的问题。 比如预测房屋被售出价格,或者棒球队可能获得的胜场数,又或者患者住院的天数。

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

某个电子邮件是否属于垃圾邮件文件夹?

某个用户可能注册或不注册订阅服务?

某个图像描绘的是驴、狗、猫、还是鸡?

某人接下来最有可能看哪部电影?

通常,机器学习实践者用分类这个词来描述两个有微妙差别的问题: 1. 我们只对样本的“硬性”类别感兴趣,即属于哪个类别; 2. 我们希望得到“软性”类别,即得到属于每个类别的概率。 这两者的界限往往很模糊。其中的一个原因是:即使我们只关心硬类别,我们仍然使用软类别的模型。

1.分类问题

我们从一个图像分类问题开始。 假设每次输入是一个2×2的灰度图像。 我们可以用一个标量表示每个像素值,每个图像对应四个特征x1,x2,x3,x4。 此外,假设每个图像属于类别“猫”“鸡”和“狗”中的一个。

接下来,我们要选择如何表示标签。 我们有两个明显的选择:最直接的想法是选择y∈{1,2,3}, 其中整数分别代表狗猫鸡{狗,猫,鸡}。 这是在计算机上存储此类信息的有效方法。 如果类别间有一些自然顺序, 比如说我们试图预测 {婴儿儿童青少年青年人中年人老年人{婴儿,儿童,青少年,青年人,中年人,老年人}, 那么将这个问题转变为回归问题,并且保留这种格式是有意义的。

但是一般的分类问题并不与类别之间的自然顺序有关。 幸运的是,统计学家很早以前就发明了一种表示分类数据的简单方法:独热编码(one-hot encoding)。 独热编码是一个向量,它的分量和类别一样多。 类别对应的分量设置为1,其他所有分量设置为0。 在我们的例子中,标签y将是一个三维向量, 其中(1,0,0)对应于“猫”、(0,1,0)对应于“鸡”、(0,0,1)对应于“狗”:

2.网络架构

为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。 为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数(affine function)。 每个输出对应于它自己的仿射函数。 在我们的例子中,由于我们有4个特征和3个可能的输出类别, 我们将需要12个标量来表示权重(带下标的w), 3个标量来表示偏置(带下标的b)。 下面我们为每个输入计算三个未规范化的预测(logit):o1、o2和o3。

我们可以用神经网络图图3.4.1来描述这个计算过程。 与线性回归一样,softmax回归也是一个单层神经网络。 由于计算每个输出o1、o2和o3取决于 所有输入x1、x2、x3和x4, 所以softmax回归的输出层也是全连接层。

图3.4.1softmax回归是一种单层神经网络

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

3.全连接层的参数开销

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

4.softmax运算

现在我们将优化参数以最大化观测数据的概率。 为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。

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

然而我们能否将未规范化的预测o直接视作我们感兴趣的输出呢? 答案是否定的。 因为将线性层的输出直接视为概率时存在一些问题: 一方面,我们没有限制这些输出数字的总和为1。 另一方面,根据输入的不同,它们可以为负值。 这些违反了2.6节中所说的概率基本公理。

要将输出视为概率,我们必须保证在任何数据上的输出都是非负的且总和为1。 此外,我们需要一个训练的目标函数,来激励模型精准地估计概率。 例如, 在分类器输出0.5的所有样本中,我们希望这些样本是刚好有一半实际上属于预测的类别。 这个属性叫做校准(calibration)。

社会科学家邓肯·卢斯于1959年在选择模型(choice model)的理论基础上 发明的softmax函数正是这样做的: softmax函数能够将未规范化的预测变换为非负数并且总和为1,同时让模型保持 可导的性质。 为了完成这一目标,我们首先对每个未规范化的预测求幂,这样可以确保输出非负。 为了确保最终输出的概率值总和为1,我们再让每个求幂后的结果除以它们的总和。如下式:

其中

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

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

5.小批量样本的矢量化

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

相对于一次处理一个样本, 小批量样本的矢量化加快了和X和W的矩阵-向量乘法。 由于X中的每一行代表一个数据样本, 那么softmax运算可以按行(rowwise)执行: 对于O的每一行,我们先对所有项进行幂运算,然后通过求和对它们进行标准化。 在(3.4.5)中,XW+b的求和会使用广播机制, 小批量的未规范化预测O和输出概率Y^都是形状为n×q的矩阵。

6.损失函数、交叉熵

接下来,我们需要一个损失函数来度量预测的效果。 我们将使用最大似然估计,这与在线性回归 (3.1.3节) 中的方法相同。

6.1.对数似然

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

根据最大似然估计,我们最大化,相当于最小化负对数似然:

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

在本节稍后的内容会讲到,(3.4.8)中的损失函数 通常被称为交叉熵损失(cross-entropy loss)。 由于y是一个长度为q的独热编码向量, 所以除了一个项以外的所有项j都消失了。 由于所有y^j都是预测的概率,所以它们的对数永远不会大于0。 因此,如果正确地预测实际标签,即如果实际标签P(y∣x)=1, 则损失函数不能进一步最小化。 注意,这往往是不可能的。 例如,数据集中可能存在标签噪声(比如某些样本可能被误标), 或输入特征没有足够的信息来完美地对每一个样本分类。

6.2.softmax及其导数

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

考虑相对于任何未规范化的预测oj的导数,我们得到:

换句话说,导数是我们softmax模型分配的概率与实际发生的情况(由独热标签向量表示)之间的差异。 从这个意义上讲,这与我们在回归中看到的非常相似, 其中梯度是观测值y和估计值y^之间的差异。 这不是巧合,在任何指数族分布模型中 (参见本书附录中关于数学分布的一节), 对数似然的梯度正是由此得出的。 这使梯度计算在实践中变得容易很多。

6.3.交叉熵损失

现在让我们考虑整个结果分布的情况,即观察到的不仅仅是一个结果。 对于标签y,我们可以使用与以前相同的表示形式。 唯一的区别是,我们现在用一个概率向量表示,如(0.1,0.2,0.7), 而不是仅包含二元项的向量(0,0,1)。 我们使用(3.4.8)来定义损失l, 它是所有标签分布的预期损失值。 此损失称为交叉熵损失(cross-entropy loss),它是分类问题最常用的损失之一。 本节我们将通过介绍信息论基础来理解交叉熵损失。 如果想了解更多信息论的细节,请进一步参考本书附录中关于信息论的一节

7.信息论基础

信息论(information theory)涉及编码、解码、发送以及尽可能简洁地处理信息或数据。

7.1.熵

信息论的核心思想是量化数据中的信息内容。 在信息论中,该数值被称为分布P的熵(entropy)。可以通过以下方程得到:

信息论的基本定理之一指出,为了对从分布p中随机抽取的数据进行编码, 我们至少需要H[P]“纳特(nat)”对其进行编码。 “纳特”相当于比特(bit),但是对数底为e而不是2。因此,一个纳特是比特。

7.2.信息量

压缩与预测有什么关系呢? 想象一下,我们有一个要压缩的数据流。 如果我们很容易预测下一个数据,那么这个数据就很容易压缩。 为什么呢? 举一个极端的例子,假如数据流中的每个数据完全相同,这会是一个非常无聊的数据流。 由于它们总是相同的,我们总是知道下一个数据是什么。 所以,为了传递数据流的内容,我们不必传输任何信息。也就是说,“下一个数据是xx”这个事件毫无信息量。

但是,如果我们不能完全预测每一个事件,那么我们有时可能会感到“惊异”。 克劳德·香农决定用信息量来量化这种惊异程度。 在观察一个事件j时,并赋予它(主观)概率P(j)。 当我们赋予一个事件较低的概率时,我们的惊异会更大,该事件的信息量也就更大。 在(3.4.11)中定义的熵, 是当分配的概率真正匹配数据生成过程时的信息量的期望。

.3.重新审视交叉熵

如果把熵H(P)想象为“知道真实概率的人所经历的惊异程度”,那么什么是交叉熵? 交叉熵从P到Q,记为H(P,Q)。 我们可以把交叉熵想象为“主观概率为Q的观察者在看到根据概率P生成的数据时的预期惊异”。 当P=Q时,交叉熵达到最低。 在这种情况下,从P到Q的交叉熵是H(P,P)=H(P)。

简而言之,我们可以从两方面来考虑交叉熵分类目标: (i)最大化观测数据的似然;(ii)最小化传达标签所需的惊异。

8.模型预测和评估

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

9.小结、练习

softmax运算获取一个向量并将其映射为概率。

softmax回归适用于分类问题,它使用了softmax运算中输出类别的概率分布。

交叉熵是一个衡量两个概率分布之间差异的很好的度量,它测量给定模型编码数据所需的比特数。

我们可以更深入地探讨指数族与softmax之间的联系。

计算softmax交叉熵损失l(y,y^)的二阶导数。

设预测输出为o,真实标签为y,则

import torchimport torch.nn as nnloss = nn.CrossEntropyLoss()y_pred = torch.randn(3, 5, requires_grad=True) y = torch.empty(3, dtype=torch.long).random_(5)loss(y_pred, y).backward()print(y_pred.grad.shape) # (3, 5) 二阶导数矩阵

可以看出,二阶导数是一个3x5的矩阵,与预测输出o的形状相同。

计算softmax(o)给出的分布方差,并与上面计算的二阶导数匹配。

设softmax输出为p,则方差为:

mean = p.mean() # 分布均值var = p*(1-p) # 分布方差

这个方差恰好就是softmax交叉熵损失的二阶导数。

假设我们有三个类发生的概率相等,即概率向量是(13,13,13)。

如果我们尝试为它设计二进制代码,有什么问题?

请设计一个更好的代码。提示:如果我们尝试编码两个独立的观察结果会发生什么?如果我们联合编码n个观测值怎么办?

(1) 如果使用二进制编码,如000, 001, 010来表示三类,则这三个代码词的概率不再相等,000的概率为1/8,其他两个为3/8。

(2) 更好的编码是使用三进制编码,即0, 1, 2来表示三类。这样三个代码词的概率仍然相等,为1/3。

softmax是对上面介绍的映射的误称(虽然深度学习领域中很多人都使用这个名字)。真正的softmax被定义为RealSoftMax(a,b)=log⁡(exp⁡(a)+exp⁡(b))。

证明RealSoftMax(a,b)>max(a,b)。

证明λ−1RealSoftMax(λa,λb)>max(a,b)成立,前提是λ>0。

证明对于λ→∞,有λ−1RealSoftMax(λa,λb)→max(a,b)。

soft-min会是什么样子?

将其扩展到两个以上的数字。

import torchdef real_softmax(a, b):return torch.log(torch.exp(a) + torch.exp(b))a = torch.tensor(1.)b = torch.tensor(2.)print(real_softmax(a, b) > max(a, b)) # Truelambda_ = 2.print(lambda_**-1 * real_softmax(lambda_*a, lambda_*b) > max(a, b)) # True lambda_ = 1000.print(lambda_**-1 * real_softmax(lambda_*a, lambda_*b) - max(a, b)) # ~0Soft-min类似地取最小值。多个变量情况可以类似推广。

常见问题

Softmax回归损失函数的梯度公式推导

常用损失函数

均方误差(Mean Squared Error, MSE) l2loss(平均平方误差(MSE)/L2 loss:预测值与真实值的平方差的平均值。回归任务常用。)

或者

平均绝对误差(MAE)/L1 loss:预测值与真实值的绝对差的平均值。对异常值敏感度较低。

或者

最大以然函数属于数理统计中参数估计的用样本估计总体

Huber loss:在误差小于阈值δ时,类似MSE;大于δ时类似MAE。综合MSE和MAE的特点。

3.5.图像分类数据集

MNIST数据集(LeCunet al., 1998)是图像分类中广泛使用的数据集之一,但作为基准数据集过于简单。 我们将使用类似但更复杂的Fashion-MNIST数据集(Xiaoet al., )。

1.读取数据集

%matplotlib inline# 导入PyTorch的torch和torchvision模块import torchimport torchvision# 从torch.utils模块中导入data组件from torch.utils import data# 从torchvision模块中导入transforms组件,用于进行图像数据转换from torchvision import transforms# 从d2l模块中导入torch组件,并将其重命名为d2lfrom d2l import torch as d2l# 使用d2l模块提供的use_svg_display函数设置matplotlib后端为svg显示d2l.use_svg_display()# 3.5.1. 读取数据集# 我们可以通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中。# 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式,# 并除以255使得所有像素的数值均在0~1之间# 创建一个ToTensor实例,将图像数据从PIL类型转换为32位浮点数格式# 同时除以255将像素值归一化到0-1范围内trans = transforms.ToTensor()# 使用内置函数从FashionMNIST数据集中获取训练数据# 设置训练数据的根目录、是否为训练集、图像转换方式、是否下载数据mnist_train = torchvision.datasets.FashionMNIST(root="../data", train=True, transform=trans, download=True)# 使用内置函数从FashionMNIST数据集中获取测试数据# 设置测试数据的根目录、是否为训练集、图像转换方式、是否下载数据mnist_test = torchvision.datasets.FashionMNIST(root="../data", train=False, transform=trans, download=True)# Fashion-MNIST由10个类别的图像组成, 每个类别由训练数据集(train dataset)中的6000张图像 和测试数据集(test dataset)中的1000张图像组成。 因此,训练集和测试集分别包含60000和10000张图像。 测试数据集不会用于训练,只用于评估模型性能。# 打印训练数据和测试数据的样本数量# 训练数据包含60000张图像,测试数据包含10000张图像print(len(mnist_train), len(mnist_test))# 60000 10000# 每个输入图像的高度和宽度均为28像素。 数据集由灰度图像组成,其通道数为1。 为了简洁起见,本书将高度h像素、宽度w像素图像的形状记为h×w或(h,w)。# 打印第一个样本的形状# 每张图像为28x28大小的单通道灰度图print(mnist_train[0][0].shape)# torch.Size([1, 28, 28])# Fashion-MNIST中包含的10个类别,分别为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。 以下函数用于在数字标签索引及其文本名称之间进行转换。# 定义一个函数,将数字标签转换为相应的文本标签# FashionMNIST包含10个类别,分别对应不同的服装类型def get_fashion_mnist_labels(labels):"""返回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]# 我们现在可以创建一个函数来可视化这些样本。# 定义一个函数绘制多张图像# imgs: 要绘制的图像列表# num_rows: 图像排列的行数# num_cols: 图像排列的列数# titles: 每张图像的标题# scale: 图像大小放大倍数def show_images(imgs, num_rows, num_cols, titles=None, scale=1.5):"""绘制图像列表"""figsize = (num_cols * scale, num_rows * scale) # 计算绘制图片的总尺寸_, axes = d2l.plt.subplots(num_rows, num_cols, figsize=figsize) # 创建子图axes = axes.flatten() # 将子图组打平为1维for i, (ax, img) in enumerate(zip(axes, imgs)): # 遍历图像和子图的zip对象if torch.is_tensor(img):ax.imshow(img.numpy()) # 图片张量 如果是tensor,转成numpyarray显示else:ax.imshow(img) # 如果是PIL图像,直接显示ax.axes.get_xaxis().set_visible(False) # 隐藏x轴ax.axes.get_yaxis().set_visible(False) # 隐藏y轴if titles:ax.set_title(titles[i]) # 设置标题return axes# 以下是训练数据集中前几个样本的图像及其相应的标签。# 取训练集的前18张样本,获取图像和标签X, y = next(iter(data.DataLoader(mnist_train, batch_size=18)))# 绘制这些图像,传入标签作为标题show_images(X.reshape(18, 28, 28), 2, 9, titles=get_fashion_mnist_labels(y));

2.读取小批量

# 2. 读取小批量# 为了使我们在读取训练集和测试集时更容易,我们使用内置的数据迭代器,而不是从零开始创建。 回顾一下,在每次迭代中,数据加载器每次都会读取一小批量数据,大小为batch_size。 通过内置数据迭代器,我们可以随机打乱了所有样本,从而无偏见地读取小批量。batch_size = 256 # 定义批量大小# def get_dataloader_workers(): #@save#"""使用4个进程来读取数据"""#return 4workers=8 # 定义使用的进程数# 构造训练数据迭代器,每次返回一个批量train_iter = data.DataLoader(mnist_train, batch_size, shuffle=True,num_workers=workers)# 我们看一下读取训练数据所需的时间。# 计时timer = d2l.Timer()# 迭代训练数据,但不执行任何处理for X, y in train_iter:continueprint(f'{timer.stop():.2f} sec') # 打印迭代一遍训练数据花费的时间# '2.58 sec'# 3. 整合所有组件# 现在我们定义load_data_fashion_mnist函数,用于获取和读取Fashion-MNIST数据集。 这个函数返回训练集和验证集的数据迭代器。 此外,这个函数还接受一个可选参数resize,用来将图像大小调整为另一种形状。def load_data_fashion_mnist(batch_size, resize=None): #@save # 定义一个函数,实现下载和加载FashionMNIST数据"""下载Fashion-MNIST数据集,然后将其加载到内存中"""trans = [transforms.ToTensor()] # 定义图像转换if resize:trans.insert(0, transforms.Resize(resize))trans = pose(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=workers),data.DataLoader(mnist_test, batch_size, shuffle=False,num_workers=workers))# 下面,我们通过指定resize参数来测试load_data_fashion_mnist函数的图像大小调整功能。train_iter, test_iter = load_data_fashion_mnist(32, resize=64) # 使用定义的函数加载数据for X, y in train_iter: # 打印第一个批量的形状和类型print(X.shape, X.dtype, y.shape, y.dtype)break# torch.Size([32, 1, 64, 64]) torch.float32 torch.Size([32]) torch.int64# 我们现在已经准备好使用Fashion-MNIST数据集,便于下面的章节调用来评估各种分类算法。

4.小结

Fashion-MNIST是一个服装分类数据集,由10个类别的图像组成。我们将在后续章节中使用此数据集来评估各种分类算法。

我们将高度h像素,宽度w像素图像的形状记为h×w或(h,w)。

数据迭代器是获得更高性能的关键组件。依靠实现良好的数据迭代器,利用高性能计算来避免减慢训练过程。

5.练习

减少batch_size(如减少到1)是否会影响读取性能?

会,在256时,需要2秒,减少之后,变成10.39 sec

数据迭代器的性能非常重要。当前的实现足够快吗?探索各种选择来改进它。

当前的数据迭代器性能还可以通过几个方法改进: (1) 使用更大的batch_size,减少迭代次数,从而减少开销。但batch_size不要设置过大,可能导致OOM。 (2) 使用更多的进程数num_workers来加速读取。但进程数不要过多,否则进程间通信的开销会增大。 (3) 将数据放在内存中,避免每次读取都访问硬盘,可以大幅提升读取速度。 (4) 使用更高效的数据格式,如内存映射memmap,可以减少数据复制开销。

查阅框架的在线API文档。还有哪些其他数据集可用?

LSUN: Large-scale Scene Understanding dataset,包含Various categories的大规模场景图像。ImageFolder: 通用的数据加载器,以文件夹分类好的图像数据集。CIFAR10: 10类小图片数据集,常用的图像分类数据集。CIFAR100: 100类小图片数据集,常用的图像分类数据集。FashionMNIST: 10类服饰图像数据集,常用的图像分类数据集。MNIST: 10类数字图像数据集,常用的图像分类数据集。STL10: 10类图像数据集,包含train/test分离的数据集。SVHN: Google Street View House Numbers,街景数字图像数据集。ImageNet: 大规模图像分类数据集,包含上千万张图片和数万类。VOC: PASCAL VOC数据集,常用于目标检测、语义分割任务。COCO: 大规模对象检测、分割、字幕数据集。Kinetics: 人类行为视频数据集,用于行为识别研究。HMDB51: 人类运动视频数据集,用于行为识别。UCF101: 101类行为识别数据集。Places365:场景识别数据集,包含365种场景类别。LFW: 人脸识别和验证数据集。KITTI: 自动驾驶相关数据集,包含视觉和点云数据。

3.6.softmax回归的从零开始实现

就像我们从零开始实现线性回归一样, 我们认为softmax回归也是重要的基础,因此应该知道实现softmax回归的细节。 本节我们将使用刚刚在3.5节中引入的Fashion-MNIST数据集, 并设置数据迭代器的批量大小为256。

1.初始化模型参数

和之前线性回归的例子一样,这里的每个样本都将用固定长度的向量表示。 原始数据集中的每个样本都是28×28的图像。 本节将展平每个图像,把它们看作长度为784的向量。 在后面的章节中,我们将讨论能够利用图像空间结构的特征, 但现在我们暂时只把每个像素位置看作一个特征。

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

import torchfrom IPython import displayfrom d2l import torch as d2lbatch_size = 256train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)num_inputs = 784# 定义输出数量为10(MNIST数据集分类类别数)num_outputs = 10# 初始化权重张量,均值为0,标准差为0.01# 大小为(num_inputs, num_outputs),需要计算梯度W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)# 初始化偏置为0,大小为(num_outputs),需要计算梯度b = torch.zeros(num_outputs, requires_grad=True)# 权重张量大小是(784, 10),784个输入特征,10个输出类别# 偏置大小是(10),10个类别的偏置# 两者都需要计算梯度,以便进行反向传播优化参数

参考资料:3.1. 线性回归 — 动手学深度学习 2.0.0 documentation

loss.sum().backward()中对于sum()的理解 - 知乎

pytorch backward() 的一点简单的理解 - 知乎

局部敏感哈希(LSH)秦睿哲_哔哩哔哩_bilibili

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。