横沥网站建设公司,手机百度app下载,唯品会购物网站开发项目,网站设置文件夹权限设置3.1 线性回归
线性回归是对n维输入的加权#xff0c;外加偏差
线性回归可以看作是单层神经网络
回归问题中最常用的损失函数是平方误差函数。 平方误差可以定义为以下公式#xff1a; 常数1/2不会带来本质的差别#xff0c;但这样在形式上稍微简单一些 #xff08;因为当…3.1 线性回归
线性回归是对n维输入的加权外加偏差
线性回归可以看作是单层神经网络
回归问题中最常用的损失函数是平方误差函数。 平方误差可以定义为以下公式 常数1/2不会带来本质的差别但这样在形式上稍微简单一些 因为当我们对损失函数求导后常数系数为1
由于平方误差函数中的二次方项 估计值y-hat(i)和观测值y(i)之间较大的差异将导致更大的损失。 为了度量模型在整个数据集上的质量我们需计算在训练集个样本上的损失均值也等价于求和。 线性回归刚好是一个很简单的优化问题。 与我们将在本书中所讲到的其他大部分模型不同线性回归的解可以用一个公式简单地表达出来 这类解叫作解析解analytical solution。 首先我们将偏置b合并到参数w中合并方法是在包含所有参数的矩阵中附加一列。 我们的预测问题是最小化。 这在损失平面上只有一个临界点这个临界点对应于整个区域的损失极小点。 将损失关于w的导数设为0得到解析解
基础优化算法
一般而言损失函数很复杂参数空间庞大我们不知道它在何处能取得最小值。而通过巧妙地使用梯度来寻找函数最小值或者尽可能小的值的方法就是梯度法
梯度指示的方向是各点处的函数值减小最多的方向。因此无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际上在复杂的函数中梯度指示的方向基本上都不是函数值最小处。
梯度下降通过不断沿着反梯度方向更新参数求解
函数的极小值、最小值以及被称为鞍点saddle point的地方梯度为0。极小值是局部最小值也就是限定在某个范围内的最小值。虽然梯度法是要寻找梯度为0的地方但是那个地方不一定就是最小值也有可能是极小值或者鞍点。此外当函数很复杂且呈扁平状时学习可能会进入一个几乎平坦的地区陷入被称为“学习高原”的无法前进的停滞期。
虽然梯度的方向并不一定指向最小值但沿着它的方向能够最大限度地减小函数的值。因此在寻找函数的最小值或者尽可能小的值的位置的任务中要以梯度的信息为线索决定前进的方向。
此时梯度法就派上用场了。在梯度法中函数的取值从当前位置沿着梯度方向前进一定距离然后在新的地方重新求梯度再沿着新梯度方向前进如此反复不断地沿梯度方向前进。像这样通过不断地沿梯度方向前进逐渐减小函数值的过程就是梯度法主要是指梯度下降法gradient method。梯度法是解决机器学习中最优化问题的常用方法特别是在神经网络的学习中经常被使用 式4.7的 η表示更新量在神经网络的学习中称为学习率learning rate。学习率决定在一次学习中应该学习多少以及在多大程度上更新参数。式4.7是表示更新一次的式子这个步骤会反复执行。也就是说每一步都按式4.7更新变量的值通过反复执行此步骤逐渐减小函数值。
学习率需要事先确定为某个值比如0.01或0.001。一般而言这个值过大或过小都无法抵达一个“好的位置”。在神经网络的学习中一般会一边改变学习率的值一边确认学习是否正确进行了 如果学习率太小会导致多次计算梯度而计算梯度是很贵的 如果学习率太大会导致一直在震荡并没有真正的在下降
像学习率这样的参数称为超参数。这是一种和神经网络的参数权重和偏置性质不同的参数。相对于神经网络的权重参数是通过训练数据和学习算法自动获得的学习率这样的超参数则是人工设定的。一般来说超参数需要尝试多个值以便找到一种可以使学习顺利 进行的设定。
1.小批量随机梯度下降
小批量随机梯度下降是深度学习默认的求解算法
3.2 线性回归的从零开始实现
在这一节中我们将从零开始实现整个方法 包括数据流水线、模型、损失函数和小批量随机梯度下降优化器。
在这一节中我们将只使用张量和自动求导。 在之后的章节中我们会充分利用深度学习框架的优势介绍更简洁的实现方式。
%matplotlib inline # IPython 魔法命令它的作用是将生成的图表直接嵌入到 Notebook 的单元格输出中而不是在弹出的窗口中显示。
import random
import torch
from d2l import torch as d2l生成数据集
我们将根据带有噪声的线性模型构造一个人造数据集。 我们的任务是使用这个有限样本的数据集来恢复这个模型的参数。 我们将使用低维数据这样可以很容易地将其可视化。 在下面的代码中我们生成一个包含1000个样本的数据集 每个样本包含从标准正态分布中采样的2个特征。 我们的合成数据集是一个矩阵。
我们使用线性模型参数w[2, -3.4]^T、b4.2 和噪声项生成数据集及其标签 可以视为模型预测和标签时的潜在观测误差。 在这里我们认为标准假设成立即 服从均值为0的正态分布。 为了简化问题我们将标准差设为0.01。 下面的代码生成合成数据集。
def synthetic_data(w, b, num_examples): #save生成yXwb噪声X torch.normal(0, 1, (num_examples, len(w))) # 生成服从正态分布的特征矩阵Xy torch.matmul(X, w) b # 计算线性模型yXwby torch.normal(0, 0.01, y.shape) # 添加噪声噪声服从均值为0标准差为0.01的正态分布return X, y.reshape((-1, 1)) # 返回特征矩阵X和标签向量ytrue_w torch.tensor([2, -3.4])
true_b 4.2
features, labels synthetic_data(true_w, true_b, 1000)输入参数
w模型的权重形状为向量表示线性模型中每个特征的权重。b模型的偏置项截距这是一个标量。num_examples生成数据的样本数量。
X torch.normal(0, 1, (num_examples, len(w)))生成一个形状为 (num_examples, len(w)) 的特征矩阵 X其中每个元素都从均值为0、标准差为1的正态分布中采样。
y torch.matmul(X, w) b根据线性模型 y Xw b 计算标签。torch.matmul 是矩阵乘法w 是权重b 是偏置。 torch.matmul - Matrix product of two tensors. X 的维度是1000×2而 w 的维度是 2可以看作是 12的行向量。在进行矩阵乘法时torch.matmul(X, w) 会将 w 广播broadcasting成适合的维度进行相乘即21。
y torch.normal(0, 0.01, y.shape)给生成的标签 y 添加服从均值为0标准差为0.01的正态分布噪声模拟现实中的数据不完美性。
y.reshape((-1, 1))将一维张量 y 转换成二维张量使其形状为 (num_examples, 1)。因为-1是占位符会自动计算 在y.reshape((-1, 1)前y.shape是torch.Size([1000])也就是11000的行向量tensor中的一维张量而y.reshape((-1, 1)的shape是torch.Size([1000,1])也就是10001的列向量实际上在tensor中不算向量实际上是二维的张量
注意features中的每一行都包含一个二维数据样本 labels中的每一行都包含一维标签值一个标量。
print(features:, features[0],\nlabel:, labels[0])通过生成第二个特征features[:, 1]和labels的散点图 可以直观观察到两者之间的线性关系。
d2l.set_figsize()
d2l.plt.scatter(features[:, 1].detach().numpy(), labels.detach().numpy(), 1);读取数据集
回想一下训练模型时要对数据集进行遍历每次抽取一小批量样本并使用它们来更新我们的模型。 由于这个过程是训练机器学习算法的基础所以有必要定义一个函数 该函数能打乱数据集中的样本并以小批量方式获取数据。
在下面的代码中我们定义一个data_iter函数 该函数接收批量大小、特征矩阵和标签向量作为输入生成大小为batch_size的小批量。 每个小批量包含一组特征和标签。
def data_iter(batch_size, features, labels):num_examples len(features)indices list(range(num_examples)) # 创建一个列表内容是从 0 到 num_examples-1 的索引值。这些索引对应于数据集中每个样本的位置# 这些样本是随机读取的没有特定的顺序random.shuffle(indices) # 随机打乱索引顺序。这样每次迭代时样本的顺序都是随机的没有特定顺序。for i in range(0, num_examples, batch_size):# 每次从打乱后的索引列表中取出当前批量对应的索引并将这些索引转换成pytorch张量batch_indices torch.tensor(indices[i: min(i batch_size, num_examples)])# 使用 yield 返回当前批量的特征和标签。yield 是 Python 的生成器函数可以逐批返回数据而不是一次性返回所有数据节省内存。yield features[batch_indices], labels[batch_indices]yield就是 return 返回一个值并且记住这个返回的位置下次迭代就从这个位置后开始。
python中yield的用法详解
Python yield 使用浅析
通常我们利用GPU并行运算的优势处理合理大小的“小批量”。 每个样本都可以并行地进行模型计算且每个样本损失函数的梯度也可以被并行计算。 GPU可以在处理几百个样本时所花费的时间不比处理一个样本时多太多。
我们直观感受一下小批量运算读取第一个小批量数据样本并打印。 每个批量的特征维度显示批量大小和输入特征数。 同样的批量的标签形状与batch_size相等。
batch_size 10for X, y in data_iter(batch_size, features, labels):print(X, \n, y)break每次 for X, y in data_iter(batch_size, features, labels) 调用时它不会一次性返回所有数据而是分批次生成并返回数据。 当我们运行迭代时我们会连续地获得不同的小批量直至遍历完整个数据集。 上面实现的迭代对教学来说很好但它的执行效率很低可能会在实际问题上陷入麻烦。 例如它要求我们将所有数据加载到内存中并执行大量的随机内存访问。 在深度学习框架中实现的内置迭代器效率要高得多 它可以处理存储在文件中的数据和数据流提供的数据。
初始化模型参数
在我们开始用小批量随机梯度下降优化我们的模型参数之前 我们需要先有一些参数。 在下面的代码中我们通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重 并将偏置初始化为0。
w torch.normal(0, 0.01, size(2,1), requires_gradTrue)
b torch.zeros(1, requires_gradTrue)这行代码使用了 PyTorch 来创建一个可训练的标量张量并启用了自动求导功能。具体解析如下
torch.zeros(1)创建一个包含一个元素的张量并将其初始化为零。torch.zeros(1) 返回一个形状为 (1,) 的张量即一个包含单个元素的零张量。requires_gradTrue这个参数告诉 PyTorch 要对这个张量进行自动求导。这意味着 PyTorch 会跟踪对该张量的操作从而可以计算该张量的梯度。这个功能在优化过程中非常重要因为它允许你对参数进行反向传播即计算梯度并更新参数从而训练模型。
在初始化参数之后我们的任务是更新这些参数直到这些参数足够拟合我们的数据。 每次更新都需要计算损失函数关于模型参数的梯度。 有了这个梯度我们就可以向减小损失的方向更新每个参数。 因为手动计算梯度很枯燥而且容易出错所以没有人会手动计算梯度。 我们使用 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虽然它们两个元素个数是一样的但可能一个是向量另一个是行向量或者列向量
注意这里损失函数中没有求均值而是在下面的sgd中batch_size求均值了
定义优化算法
正如我们在 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_()小批量随机梯度下降SGDStochastic Gradient Descent优化算法 接受三个参数
params模型的参数列表通常是一个包含多个张量的迭代器每个张量代表模型的一个参数。lr学习率Learning Rate即更新参数时所乘的步长大小。学习率控制着每次参数更新的幅度。batch_size小批量的大小即每次使用多少个样本进行参数更新。通常与梯度计算相关联。
with torch.no_grad(): 使用 torch.no_grad() 上下文管理器表示在这个代码块中不需要计算梯度。这样可以避免在参数更新过程中误计算梯度因为此时只需要更新参数而不需要记录操作以供反向传播。
param - lr * param.grad / batch_size对每个参数执行参数更新
param.grad表示该参数的梯度。这个梯度通常是通过反向传播计算出来的表示损失函数对该参数的偏导数。lr * param.grad / batch_size首先将梯度乘以学习率然后除以批量大小。这相当于将梯度的平均值针对当前批量乘以学习率得到一个步长沿着梯度方向减小参数值从而最小化损失函数。param -用来更新参数即用当前的参数值减去计算得到的步长值。
param.grad.zero_() 将当前参数的梯度置零。因为 PyTorch 会累积梯度因此每次更新参数后需要将梯度清零以免下一次计算中混入之前的梯度值。
训练
现在我们已经准备好了模型训练所有需要的要素可以实现主要的训练过程部分了。 理解这段代码至关重要因为从事深度学习后 相同的训练过程几乎一遍又一遍地出现。 在每次迭代中我们读取一小批量训练样本并通过我们的模型来获得一组预测。 计算完损失后我们开始反向传播存储每个参数的梯度。 最后我们调用优化算法sgd来更新模型参数。
概括一下我们将执行以下循环 在每个迭代周期epoch中我们使用data_iter函数遍历整个数据集 并将训练数据集中所有样本都使用一次假设样本数能够被批量大小整除。 这里的迭代周期个数num_epochs和学习率lr都是超参数分别设为3和0.03。 设置超参数很棘手需要通过反复试验进行调整。 我们现在忽略这些细节以后会在 11节中详细介绍。
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(fepoch {epoch 1}, loss {float(train_l.mean()):f}){float(train_l.mean()):f} 这个部分是控制输出格式的。在 {} 内部的表达式 float(train_l.mean()) 会被计算然后用 :f 来指定浮点数的格式。 :f 表示将浮点数格式化为默认的六位小数。
每次 for X, y in data_iter(batch_size, features, labels) 调用时它不会一次性返回所有数据而是分批次生成并返回数据。 在每个 epoch 中for X, y in data_iter(batch_size, features, labels) 这个循环会持续调用 data_iter直到所有样本都被处理完。 data_iter 不会一次性返回所有数据而是每次生成一个批次返回一次直到样本全部处理完。 因为我们使用的是自己合成的数据集所以我们知道真正的参数是什么。 因此我们可以通过比较真实参数和通过训练学到的参数来评估训练的成功程度。 事实上真实参数和通过训练学到的参数确实非常接近。
print(fw的估计误差: {true_w - w.reshape(true_w.shape)})
print(fb的估计误差: {true_b - b})注意我们不应该想当然地认为我们能够完美地求解参数。 在机器学习中我们通常不太关心恢复真正的参数而更关心如何高度准确预测参数。 幸运的是即使是在复杂的优化问题上随机梯度下降通常也能找到非常好的解。 其中一个原因是在深度网络中存在许多参数组合能够实现高度精确的预测。
3.3 线性回归的简洁实现
在过去的几年里出于对深度学习强烈的兴趣 许多公司、学者和业余爱好者开发了各种成熟的开源框架。 这些框架可以自动化基于梯度的学习算法中重复性的工作。 在 3.2节中我们只运用了 1通过张量来进行数据存储和线性代数 2通过自动微分来计算梯度。 实际上由于数据迭代器、损失函数、优化器和神经网络层很常用 现代深度学习库也为我们实现了这些组件。
本节将介绍如何通过使用深度学习框架来简洁地实现 3.2节中的线性回归模型。
生成数据集
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来读取数据。 我们将features和labels作为API的参数传递并通过数据迭代器指定batch_size。 此外布尔值is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据。
def load_array(data_arrays, batch_size, is_trainTrue): #save构造一个PyTorch数据迭代器dataset data.TensorDataset(*data_arrays)return data.DataLoader(dataset, batch_size, shuffleis_train)batch_size 10
data_iter load_array((features, labels), batch_size)对于像数据特征和标签这样的输入不需要在函数内部修改它们因此使用元组可以提高安全性防止误操作导致的数据更改。并且因为元组是不可变的它的内存开销和性能通常比列表更高效。 在函数 load_array 中使用了参数拆包*data_arrays可以直接解包元组或列表。元组在这种解包操作中与列表同样适用但由于元组通常用于表达固定结构因此在这个场景下使用元组可能更加语义清晰。
load_array 函数的作用 该函数的主要作用是将输入的数据数组转换为 PyTorch 的 DataLoader使得在训练过程中可以更方便地进行小批量数据的迭代。
参数解析
data_arrays: 是输入的特征和标签一般由两个张量组成分别是特征张量 features 和标签张量 labels。*data_arrays 使用了可变参数的方式拆包操作符 *可以接受多个输入张量比如特征和标签。在函数内部它们会被拆包并传递给 TensorDataset。batch_size: 表示每次返回的数据批次大小。is_train: 一个布尔值表示是否在加载数据时随机打乱数据。默认值为 True即在训练时数据会被随机打乱。
构造 TensorDataset torch.utils.data.TensorDataset(*data_arrays)将传入的多个张量特征和标签打包成一个 Dataset 对象。TensorDataset 是 PyTorch 提供的用于包装数据的类可以同时包含特征和标签。 例如假设 data_arrays 包含两个张量 features 和 labelsTensorDataset(features, labels) 将它们组合成一个可迭代的数据集其中每个样本由一对 (feature, label) 组成。
返回 DataLoader torch.utils.data.DataLoader(dataset, batch_size, shuffleis_train)DataLoader 是 PyTorch 提供的用于加载数据的类它可以自动将 Dataset 以小批量形式加载出来并支持多线程加载、随机打乱数据等功能。 batch_size: 指定每次加载的样本数。 shuffleis_train: 如果 is_trainTrue则在每个 epoch 之前会随机打乱数据这对训练过程很重要可以提高模型的泛化能力。对测试集通常不打乱数据。
在 Python 中解包unpacking指的是将一个可迭代对象如元组或列表展开成单独的元素并传递给函数或赋值操作。在你的 load_array 函数中* data_arrays 是一种解包操作它用于将元组或列表中的元素展开并分别传递给函数。在 Python 中 * 操作符可以用于将一个可迭代对象如元组、列表解包为单独的元素并将这些元素作为单独的参数传递给函数。例如
def my_func(a, b):print(a, b)args (1, 2)
my_func(*args) # 等同于 my_func(1, 2)
在 load_array 函数中
data_arrays 是一个包含多个张量的元组 (features, labels)。*data_arrays 解包操作会将 data_arrays 元组中的每个元素即 features 和 labels作为单独的参数传递给 TensorDataset。TensorDataset 期望接收多个单独的张量而不是一个包含多个张量的容器如元组或列表。
使用data_iter的方式与我们在 3.2节中使用data_iter函数的方式相同。为了验证是否正常工作让我们读取并打印第一个小批量样本。 与 3.2节不同这里我们使用iter构造Python迭代器并使用next从迭代器中获取第一项。
next(iter(data_iter))定义模型
当我们在 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))2是features的列数1是label的列数
初始化模型参数
在使用net之前我们需要初始化模型参数。 如在线性回归模型中的权重和偏置。 深度学习框架通常有预定义的方法来初始化参数。 在这里我们指定每个权重参数应该从均值为0、标准差为0.01的正态分布中随机采样 偏置参数将初始化为零。
正如我们在构造nn.Linear时指定输入和输出尺寸一样 现在我们能直接访问参数以设定它们的初始值。 我们通过net[0]选择网络中的第一个图层 然后使用weight.data和bias.data方法访问参数。 我们还可以使用替换方法normal_和fill_来重写参数值。
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)net[0]是指容器里面的第一层也就是我们的linear
fill_同样是一个就地操作直接修改偏置的值。 定义损失函数
计算均方误差使用的是MSELoss类也称为平方L_2范数。 默认情况下它返回所有样本损失的平均值。
loss nn.MSELoss()定义优化算法
小批量随机梯度下降算法是一种优化神经网络的标准工具 PyTorch在optim模块中实现了该算法的许多变种。 当我们实例化一个SGD实例时我们要指定优化的参数 可通过net.parameters()从我们的模型中获得以及优化算法所需的超参数字典。 小批量随机梯度下降只需要设置lr值这里设置为0.03。
trainer torch.optim.SGD(net.parameters(), lr0.03)训练
通过深度学习框架的高级API来实现我们的模型只需要相对较少的代码。 我们不必单独分配参数、不必定义我们的损失函数也不必手动实现小批量随机梯度下降。 当我们需要更复杂的模型时高级API的优势将大大增加。 当我们有了所有的基本组件训练过程代码与我们从零开始实现时所做的非常相似。
回顾一下在每个迭代周期里我们将完整遍历一次数据集train_data 不停地从中获取一个小批量的输入和相应的标签。 对于每一个小批量我们会进行以下步骤:
通过调用net(X)生成预测并计算损失l前向传播。通过进行反向传播来计算梯度。通过调用优化器来更新模型参数。
为了更好的衡量训练效果我们计算每个迭代周期后的损失并打印它来监控训练过程。
num_epochs 3
for epoch in range(num_epochs):for X, y in data_iter:l loss(net(X) ,y)trainer.zero_grad()l.backward()trainer.step()l loss(net(features), labels)print(fepoch {epoch 1}, loss {l:f})没用with no_gard的原因是梯度清零的时机不一样一个在backward前一个在backward后
为什么省略l.sum():因为loss函数中已经有sum()操作了
下面我们比较生成数据集的真实参数和通过有限数据训练获得的模型参数。 要访问参数我们首先从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)3.4 softmax回归
通常机器学习实践者用分类这个词来描述两个有微妙差别的问题 1. 我们只对样本的“硬性”类别感兴趣即属于哪个类别 2. 我们希望得到“软性”类别即得到属于每个类别的概率。 这两者的界限往往很模糊。其中的一个原因是即使我们只关心硬类别我们仍然使用软类别的模型。 softmax回归 虽然名字中带有回归但实际上是一个分类问题
分类问题
我们从一个图像分类问题开始。 假设每次输入是一个2*2的灰度图像。 我们可以用一个标量表示每个像素值每个图像对应四个特征x_1,x_2,x_3,x_4。 此外假设每个图像属于类别“猫”“鸡”和“狗”中的一个。
接下来我们要选择如何表示标签。 我们有两个明显的选择最直接的想法是选择y属于{1,2,3} 其中整数分别代表{狗,猫,鸡}。 这是在计算机上存储此类信息的有效方法。 如果类别间有一些自然顺序 比如说我们试图预测{婴儿,儿童,青少年,青年人,中年人,老年人} 那么将这个问题转变为回归问题并且保留这种格式是有意义的。
但是一般的分类问题并不与类别之间的自然顺序有关。 幸运的是统计学家很早以前就发明了一种表示分类数据的简单方法独热编码one-hot encoding。 独热编码是一个向量它的分量和类别一样多。 类别对应的分量设置为1其他所有分量设置为0。 在我们的例子中标签 将是一个三维向量 其中(1,0,0)对应于“猫”、(0,1,0)对应于“鸡”、(0,0,1)对应于“狗”
网络架构
为了估计所有可能类别的条件概率我们需要一个有多个输出的模型每个类别对应一个输出。 为了解决线性模型的分类问题我们需要和输出一样多的仿射函数affine function。 每个输出对应于它自己的仿射函数。 在我们的例子中由于我们有4个特征和3个可能的输出类别 我们将需要12个标量来表示权重带下标的w 3个标量来表示偏置带下标的b。 下面我们为每个输入计算三个未规范化的预测logito1,o2,o3 我们可以用神经网络图 图3.4.1来描述这个计算过程。 与线性回归一样softmax回归也是一个单层神经网络。 由于计算每个输出o1,o2,o3取决于 所有输入x1,x2,x3,x4 所以softmax回归的输出层也是全连接层。 为了更简洁地表达模型我们仍然使用线性代数符号。 通过向量形式表达为oWxb 这是一种更适合数学和编写代码的形式。 由此我们已经将所有权重放到一个3*4矩阵中。 对于给定数据样本的特征x 我们的输出是由权重与输入特征进行矩阵-向量乘法再加上偏置b得到的。
全连接层的参数开销
正如我们将在后续章节中看到的在深度学习中全连接层无处不在。 然而顾名思义全连接层是“完全”连接的可能有很多可学习的参数。 具体来说对于任何具有d个输入和q个输出的全连接层 参数开销为O(dq)这个数字在实践中可能高得令人望而却步。 幸运的是将d个输入转换为q个输出的成本可以减少到O((dq)/n) 其中超参数n可以由我们灵活指定以在实际应用中平衡参数节约和模型有效性
softmax运算
现在我们将优化参数以最大化观测数据的概率。 为了得到预测结果我们将设置一个阈值如选择具有最大概率的标签。
我们希望模型的输出y-hat_j可以视为属于类j的概率 然后选择具有最大输出值的类别argmax_j{y_j}作为我们的预测。 例如如果y-hat_1,y-hat_2,y-hat_3分别为0.1、0.8和0.1 那么我们预测的类别是2在我们的例子中代表“鸡”。
然而我们能否将未规范化的预测o直接视作我们感兴趣的输出呢 答案是否定的。 因为将线性层的输出直接视为概率时存在一些问题 一方面我们没有限制这些输出数字的总和为1。 另一方面根据输入的不同它们可以为负值。 这些违反了 2.6节中所说的概率基本公理。
要将输出视为概率我们必须保证在任何数据上的输出都是非负的且总和为1。 此外我们需要一个训练的目标函数来激励模型精准地估计概率。 例如 在分类器输出0.5的所有样本中我们希望这些样本是刚好有一半实际上属于预测的类别。 这个属性叫做校准calibration。
社会科学家邓肯·卢斯于1959年在选择模型choice model的理论基础上 发明的softmax函数正是这样做的 softmax函数能够将未规范化的预测变换为非负数并且总和为1同时让模型保持 可导非负负的x - e^x - 正的输出总和为1分子相加等于分母的性质。 为了完成这一目标我们首先对每个未规范化的预测求幂这样可以确保输出非负。 为了确保最终输出的概率值总和为1我们再让每个求幂后的结果除以它们的总和。如下式 这里对于所有的j总有0 y-hat_j 1。 因此y-hat可以视为一个正确的概率分布。 softmax运算不会改变未规范化的预测o之间的大小次序只会确定分配给每个类别的概率。 因此在预测过程中我们仍然可以用下式来选择最有可能的类别。 尽管softmax是一个非线性函数但softmax回归的输出仍然由输入特征的仿射变换决定。 因此softmax回归是一个线性模型linear model。
小批量样本的矢量化
为了提高计算效率并且充分利用GPU我们通常会对小批量样本的数据执行矢量计算。 假设我们读取了一个批量的样本X 其中特征维度输入数量为d批量大小为n。 此外假设我们在输出中有q个类别。 那么小批量样本的特征为 X ∈ R n × d X \in R^{n \times d} X∈Rn×d 权重为 W ∈ R d × q W \in R^{d \times q} W∈Rd×q 偏置为 b ∈ R 1 × q b \in R^{1 \times q} b∈R1×q。 softmax回归的矢量计算表达式为 相对于一次处理一个样本 小批量样本的矢量化加快了X和W的矩阵-向量乘法。 由于X中的每一行代表一个数据样本 那么softmax运算可以按行rowwise执行 对于O的每一行我们先对所有项进行幂运算然后通过求和对它们进行标准化。 在 (3.4.5)中XWb 的求和会使用广播机制 小批量的未规范化预测O和输出概率Y-hat 都是形状为n*q的矩阵。
损失函数
接下来我们需要一个损失函数来度量预测的效果。 我们将使用最大似然估计这与在线性回归 3.1.3节 中的方法相同。
交叉熵损失 现在让我们考虑整个结果分布的情况即观察到的不仅仅是一个结果。 对于标签y我们可以使用与以前相同的表示形式。 唯一的区别是我们现在用一个概率向量表示如(0.1,0.2,0.7) 而不是仅包含二元项的向量(0,0,1)。 我们使用 (3.4.8)来定义损失l 它是所有标签分布的预期损失值。 此损失称为交叉熵损失cross-entropy loss它是分类问题最常用的损失之一。 使用softmax操作子得到每个类的预测置信度
使用交叉熵来衡量预测和标号的区别作为损失函数
3.5 图像分类数据集
MNIST数据集 (LeCun et al., 1998) 是图像分类中广泛使用的数据集之一但作为基准数据集过于简单。 我们将使用类似但更复杂的Fashion-MNIST数据集 (Xiao et al., 2017)。
%matplotlib inline
import torch
import torchvision
from torch.utils import data
from torchvision import transforms
from d2l import torch as d2ld2l.use_svg_display()读取数据集
我们可以通过框架中的内置函数将Fashion-MNIST数据集下载并读取到内存中。
# 通过ToTensor实例将图像数据从PIL类型变换成32位浮点数格式
# 并除以255使得所有像素的数值均在01之间
trans transforms.ToTensor()
mnist_train torchvision.datasets.FashionMNIST(root../data, trainTrue, transformtrans, downloadTrue)
mnist_test torchvision.datasets.FashionMNIST(root../data, trainFalse, transformtrans, downloadTrue)Fashion-MNIST由10个类别的图像组成 每个类别由训练数据集train dataset中的6000张图像 和测试数据集test dataset中的1000张图像组成。 因此训练集和测试集分别包含60000和10000张图像。 测试数据集不会用于训练只用于评估模型性能。
len(mnist_train), len(mnist_test)每个输入图像的高度和宽度均为28像素。 数据集由灰度图像组成其通道数为1。 为了简洁起见本书将高度h像素、宽度w像素图像的形状记为h*w或(h,w)
mnist_train[0][0].shape由于是黑白图片因此channel为1
Fashion-MNIST中包含的10个类别分别为t-shirtT恤、trouser裤子、pullover套衫、dress连衣裙、coat外套、sandal凉鞋、shirt衬衫、sneaker运动鞋、bag包和ankle boot短靴。 以下函数用于在数字标签索引及其文本名称之间进行转换。
def get_fashion_mnist_labels(labels): #save返回Fashion-MNIST数据集的文本标签text_labels [t-shirt, trouser, pullover, dress, coat,sandal, shirt, sneaker, bag, ankle boot]return [text_labels[int(i)] for i in labels]这个return语句是一个列表推导式它执行以下步骤
遍历输入的 labels其中 labels 是一个包含数字标签的列表或张量。对于每个 i先通过 int(i) 将其转换为整数索引以确保能够正确索引 text_labels 列表防止 i 是其他类型如张量。使用 text_labels[int(i)] 来获取对应的文本标签。最终返回一个包含所有文本标签的新列表。
我们现在可以创建一个函数来可视化这些样本。
def show_images(imgs, num_rows, num_cols, titlesNone, scale1.5): #save绘制图像列表figsize (num_cols * scale, num_rows * scale)_, axes d2l.plt.subplots(num_rows, num_cols, figsizefigsize)axes axes.flatten()for i, (ax, img) in enumerate(zip(axes, imgs)):if torch.is_tensor(img):# 图片张量ax.imshow(img.numpy())else:# PIL图片ax.imshow(img)ax.axes.get_xaxis().set_visible(False)ax.axes.get_yaxis().set_visible(False)if titles:ax.set_title(titles[i])return axes以下是训练数据集中前几个样本的图像及其相应的标签。
X, y next(iter(data.DataLoader(mnist_train, batch_size18)))
show_images(X.reshape(18, 28, 28), 2, 9, titlesget_fashion_mnist_labels(y));读取小批量
为了使我们在读取训练集和测试集时更容易我们使用内置的数据迭代器而不是从零开始创建。 回顾一下在每次迭代中数据加载器每次都会读取一小批量数据大小为batch_size。 通过内置数据迭代器我们可以随机打乱了所有样本从而无偏见地读取小批量。
batch_size 256def get_dataloader_workers(): #save使用4个进程来读取数据return 4train_iter data.DataLoader(mnist_train, batch_size, shuffleTrue,num_workersget_dataloader_workers())我们看一下读取训练数据所需的时间。
timer d2l.Timer()
for X, y in train_iter:continue
f{timer.stop():.2f} sec整合所有组件
现在我们定义load_data_fashion_mnist函数用于获取和读取Fashion-MNIST数据集。 这个函数返回训练集和验证集的数据迭代器。 此外这个函数还接受一个可选参数resize用来将图像大小调整为另一种形状。
def load_data_fashion_mnist(batch_size, resizeNone): #save下载Fashion-MNIST数据集然后将其加载到内存中trans [transforms.ToTensor()]if resize:trans.insert(0, transforms.Resize(resize))trans transforms.Compose(trans)mnist_train torchvision.datasets.FashionMNIST(root../data, trainTrue, transformtrans, downloadTrue)mnist_test torchvision.datasets.FashionMNIST(root../data, trainFalse, transformtrans, downloadTrue)return (data.DataLoader(mnist_train, batch_size, shuffleTrue,num_workersget_dataloader_workers()),data.DataLoader(mnist_test, batch_size, shuffleFalse,num_workersget_dataloader_workers()))这里首先创建了一个包含 transforms.ToTensor() 的列表 trans。 transforms.ToTensor()将输入的 PIL 图像或 NumPy 数组转换为 PyTorch 的张量格式同时将像素值从 [0, 255] 范围缩放到 [0, 1] 范围。这是模型训练和推理过程中常见的预处理步骤因为神经网络期望输入是浮点数张量。
这里检查是否传入了 resize 参数。 如果传入了 resize表示需要调整图像大小。resize 应该是一个整数或包含两个元素的元组指定图像的目标尺寸。 transforms.Resize(resize)这是一个图像预处理操作它会将输入图像调整为指定的尺寸resize 参数。它被插入到 trans 列表的开头insert(0, 表示在所有其他操作之前首先调整图像大小。 例如如果传入 resize(64, 64)那么图像会被调整为 64x64 像素。
transforms.Compose()将一系列预处理操作trans 列表中的每个操作组合成一个复合操作。在这里trans 列表可能包含两个操作 如果没有 resize只包含 ToTensor() 操作。 如果有 resize包含 Resize(resize) 和 ToTensor()图像会先被调整大小然后转换为张量。 Compose 函数的作用是按顺序依次执行这些操作。最终结果是一个由 transforms.Compose() 定义的预处理管道它会被应用于加载 Fashion-MNIST 数据集的每一张图像。
下面我们通过指定resize参数来测试load_data_fashion_mnist函数的图像大小调整功能。
train_iter, test_iter load_data_fashion_mnist(32, resize64)
for X, y in train_iter:print(X.shape, X.dtype, y.shape, y.dtype)break我们现在已经准备好使用Fashion-MNIST数据集便于下面的章节调用来评估各种分类算法。
3.6 softmax回归的从零开始实现
本节我们将使用刚刚在 3.5节中引入的Fashion-MNIST数据集 并设置数据迭代器的批量大小为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的图像。 本节将展平每个图像把它们看作长度为784的向量。 在后面的章节中我们将讨论能够利用图像空间结构的特征 但现在我们暂时只把每个像素位置看作一个特征。
回想一下在softmax回归中我们的输出与类别一样多。 因为我们的数据集有10个类别所以网络输出维度为10。 因此权重将构成一个78410的矩阵 偏置将构成一个110的行向量。 与线性回归一样我们将使用正态分布初始化我们的权重W偏置初始化为0。
num_inputs 784
num_outputs 10W torch.normal(0, 0.01, size(num_inputs, num_outputs), requires_gradTrue)
b torch.zeros(num_outputs, requires_gradTrue)定义softmax操作
在实现softmax回归模型之前我们简要回顾一下sum运算符如何沿着张量中的特定维度工作。 如 2.3.6节和 2.3.6.1节所述 给定一个矩阵X我们可以对所有元素求和默认情况下。 也可以只求同一个轴上的元素即同一列轴0或同一行轴1。 如果X是一个形状为(2, 3)的张量我们对列进行求和 则结果将是一个具有形状(3,)的向量。 当调用sum运算符时我们可以指定保持在原始张量的轴数而不折叠求和的维度。 这将产生一个具有形状(1, 3)的二维张量。
X torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
X.sum(0, keepdimTrue), X.sum(1, keepdimTrue)回想一下实现softmax由三个步骤组成
对每个项求幂使用exp对每一行求和小批量中每个样本是一行得到每个样本的规范化常数将每一行除以其规范化常数确保结果的和为1。
在查看代码之前我们回顾一下这个表达式 分母或规范化常数有时也称为配分函数其对数称为对数-配分函数。 该名称来自统计物理学中一个模拟粒子群分布的方程。
def softmax(X):X_exp torch.exp(X)partition X_exp.sum(1, keepdimTrue)return X_exp / partition # 这里应用了广播机制torch.exp(X)对 X 中的每个元素取指数值
X_exp.sum(1, keepdimTrue)对 X_exp 沿着第 1 维即列的方向求和得到每一行的总和。这个和称为 partition function它是 softmax 函数分母部分用于归一化每个类别的指数值。
X_exp / partition将 X_exp 中每一行的元素除以对应行的 partition这是 softmax 的归一化步骤。 在 X_exp / partition 这一行代码中严格来说并不是矩阵除法而是逐元素除法。 逐元素除法 vs 矩阵除法
逐元素除法每个元素会与另一个张量中的相应元素进行除法操作。这里的 X_exp / partition 就是逐元素的除法运算。矩阵除法如果是矩阵除法那我们通常会涉及到矩阵乘法或求逆但这并不是 softmax 函数中的情形。
在 softmax 函数中分母 partition 是每一行的和因此我们希望对每一行的每个元素都除以该行的总和。这是因为我们希望 softmax 的结果是一个概率分布即每行的值都归一化为 0 到 1 之间并且每行的总和为 1。
广播机制的作用
张量的形状 在广播之前 X_exp 的形状是 (batch_size, num_classes)即每个样本有 num_classes 个预测值。 partition 是通过 X_exp.sum(1, keepdimTrue) 得到的它的形状是 (batch_size, 1)即每一行有一个总和值。广播机制 广播机制允许形状不同的张量参与运算具体是将较小的张量扩展为与较大张量兼容的形状。广播不需要真的复制数据而是逻辑上扩展数据。
在这个例子中 partition 的形状是 (batch_size, 1)它的每一行只有一个数值。 当执行 X_exp / partition 时PyTorch 的广播机制会将 partition 的每个元素沿着列方向扩展变为形状 (batch_size, num_classes)即每个样本的所有类别都共享相同的分母。 具体过程 逐元素除法通过广播机制partition 的每一行的那个单一值被扩展使得 X_exp 的每一行都能与其对应的行总和值逐元素相除。 每一行的 X_exp[i, :] 都会除以 partition[i, 0]这相当于对行进行归一化运算最终确保每行的所有元素加起来等于 1。
正如上述代码对于任何随机输入我们将每个元素变成一个非负数。 此外依据概率原理每行总和为1。
X torch.normal(0, 1, (2, 5))
X_prob softmax(X)
X_prob, X_prob.sum(1)注意虽然这在数学上看起来是正确的但我们在代码实现中有点草率。 矩阵中的非常大或非常小的元素可能造成数值上溢或下溢但我们没有采取措施来防止这点。
定义模型
定义softmax操作后我们可以实现softmax回归模型。 下面的代码定义了输入如何通过网络映射到输出。 注意将数据传递到模型之前我们使用reshape函数将每张原始图像展平为向量。
def net(X):return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) b)X是[256,1,28,28], reshape后为[256,784]
定义损失函数
接下来我们实现 3.4节中引入的交叉熵损失函数。 这可能是深度学习中最常见的损失函数因为目前分类问题的数量远远超过回归问题的数量。
回顾一下交叉熵采用真实标签的预测概率的负对数似然。 这里我们不使用Python的for循环迭代预测这往往是低效的 而是通过一个运算符选择所有元素。 下面我们创建一个数据样本y_hat其中包含2个样本在3个类别的预测概率 以及它们对应的标签y。 有了y我们知道在第一个样本中第一类是正确的预测 而在第二个样本中第三类是正确的预测。 然后使用y作为y_hat中概率的索引 我们选择第一个样本中第一个类的概率和第二个样本中第三个类的概率。
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]y_hat[[0, 1], y] 使用了高级索引fancy indexing结合行索引和列索引来选择 y_hat 中的特定元素。实现的是利用索引来从 y_hat 张量中选取特定位置的元素。
[0, 1]这是行索引表示我们要选取 y_hat 中的第 0 行和第 1 行。 y这是列索引由张量 y 给出它包含 [0, 2] 因此y_hat[[0, 1], y] 的含义是
从 y_hat 的第 0 行选择索引为 0 的元素。从 y_hat 的第 1 行选择索引为 2 的元素。
现在我们只需一行代码就可以实现交叉熵损失函数。
def cross_entropy(y_hat, y):return - torch.log(y_hat[range(len(y_hat)), y])cross_entropy(y_hat, y)range(len(y_hat)) - [0, 1, …, len(y_hat)-1]
提示代码中的log实际上是ln
分类精度
给定预测概率分布y_hat当我们必须输出硬预测hard prediction时 我们通常选择预测概率最高的类。 许多应用都要求我们做出选择。如Gmail必须将电子邮件分类为“Primary主要邮件”、 “Social社交邮件”“Updates更新邮件”或“Forums论坛邮件”。 Gmail做分类时可能在内部估计概率但最终它必须在类中选择一个。
当预测与标签分类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(axis1)cmp y_hat.type(y.dtype) yreturn float(cmp.type(y.dtype).sum())if len(y_hat.shape) 1 and y_hat.shape[1] 1 这段代码的目的是判断 y_hat 是否是二维张量y_hat.shape这是 PyTorch 张量的形状属性它返回张量的维度即张量每一维的大小作为一个元组并且它是否包含多个类别的概率分布。如果 y_hat 是二维的且列数即 y_hat.shape[1]大于 1表示 y_hat 可能是类别的概率分布而不是类别索引。 例如假设 y_hat 是一个形状为 (batch_size, num_classes) 的张量表示每个样本的 num_classes 类别的预测概率分布。
y_hat y_hat.argmax(axis1) argmax(axis1)对 y_hat 的每一行每个样本的预测进行操作找出概率最大的类别索引。这样做的目的是将概率分布转换为类别的索引。 例如如果 y_hat 是 [[0.1, 0.3, 0.6], [0.2, 0.7, 0.1]]argmax(axis1) 会返回 [2, 1]因为每个样本的最大概率对应的类别索引分别是 2 和 1。
cmp y_hat.type(y.dtype) y 这一行代码的作用是将 y_hat 和 y 进行比较生成一个表示是否预测正确的布尔张量。
y_hat.type(y.dtype)确保 y_hat 和 y 的数据类型一致。y_hat 可能是整数型而 y 的数据类型可能不同例如 Long 类型。通过 type() 强制类型转换保证它们可以进行比较。 y逐元素比较 y_hat 和 y返回一个布尔张量表示每个样本的预测是否正确。 例如假设 y_hat [2, 1] 且 y [2, 0]比较的结果为 cmp [True, False]。
return float(cmp.type(y.dtype).sum()) cmp.type(y.dtype)将布尔张量 cmp 转换为与 y 相同的数据类型通常是整数。在布尔张量中True 转换为 1False 转换为 0。 .sum()对 cmp 中的所有元素求和得到预测正确的样本数量。 例如假设 cmp [True, False]转换后变为 [1, 0]求和结果为 1表示有 1 个样本预测正确。 float(…)将结果转换为浮点数确保返回的值是一个浮点数而不是整型。
我们将继续使用之前定义的变量y_hat和y分别作为预测的概率分布和标签。 可以看到第一个样本的预测类别是2该行的最大元素为0.6索引为2这与实际标签0不一致。 第二个样本的预测类别是2该行的最大元素为0.5索引为2这与实际标签2一致。 因此这两个样本的分类精度率为0.5。
accuracy(y_hat, y) / len(y)同样对于任意数据迭代器data_iter可访问的数据集 我们可以评估在任意模型net的精度。
def evaluate_accuracy(net, data_iter): #save计算在指定数据集上模型的精度if isinstance(net, torch.nn.Module):net.eval() # 将模型设置为评估模式metric Accumulator(2) # 正确预测数、预测总数with torch.no_grad():for X, y in data_iter:metric.add(accuracy(net(X), y), y.numel())return metric[0] / metric[1]if isinstance(net, torch.nn.Module): net.eval() # 将模型设置为评估模式 isinstance(net, torch.nn.Module)检查传入的 net 是否是 torch.nn.Module 的实例。如果是它表示这是一个 PyTorch 模型。 net.eval()将模型设置为评估模式。在评估模式下模型的行为会有所不同尤其是对 Dropout 和 BatchNorm 层它们在训练和推理时的表现是不一样的。
Dropout在训练时随机丢弃神经元在评估模式下关闭。BatchNorm在评估模式下使用训练时计算的均值和方差。
初始化计数器 metric Accumulator(2) # 正确预测数、预测总数 Accumulator(2)这是一个自定义的累加器用来保存和累加多个值。这里 Accumulator 的作用是累加两个值正确预测的数量和样本总数。2 表示累加器有两个部分分别用于统计这两个值。
禁用梯度计算 with torch.no_grad(): torch.no_grad()禁用梯度计算。因为我们只是在评估模型的准确率而不是进行训练因此不需要计算梯度。这样可以提高计算效率减少内存占用。
遍历数据集 for X, y in data_iter: 这里遍历 data_iter每次从数据迭代器中获取一小批数据 (X, y) X小批量输入数据。 y对应的真实标签。
更新计数器 metric.add(accuracy(net(X), y), y.numel()) net(X)将输入 X 喂给模型得到预测结果。 accuracy(net(X), y)调用前面定义的 accuracy 函数计算模型在当前批次数据上的准确预测数量。 y.numel()计算当前批次中样本的总数。numel() 返回张量中的元素数量对于分类任务而言这通常就是样本数。 metric.add()将准确预测的数量和样本总数累加到累加器 metric 中。
返回总体准确率 return metric[0] / metric[1] metric[0]累加器的第一个元素表示所有批次中正确预测的总数。 metric[1]累加器的第二个元素表示所有批次中样本的总数。 最后返回的是准确率即正确预测的总数除以样本总数。
这里定义一个实用程序类Accumulator用于在多个变量上累加数据。 在上面的evaluate_accuracy函数中 我们在Accumulator实例中创建了2个变量 分别用于存储正确预测的数量和预测的总数量。 当我们遍历数据集时两者都将随着时间的推移而累加。
class Accumulator: #save在n个变量上累加def __init__(self, n):# 创建了一个包含 n 个元素的列表 self.data所有元素初始值为 0.0。这个列表存储要累加的变量# 例如如果 n2那么 self.data 将是 [0.0, 0.0]# 在 Python 中列表乘以一个整数 n如这里的 2会将列表的内容重复 n 次从而生成一个包含 n 个相同元素的新列表self.data [0.0] * ndef add(self, *args):# 这个方法接收任意数量的参数 *args并将它们累加到 self.data 中对应的位置。# zip(self.data, args)将 self.data 和传入的参数 args 配对在一起。# a float(b)对 self.data 中的每个元素 a累加对应的 args 元素 b并将 b 转换为浮点数。然后将累加后的结果保存回 self.data。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)训练
在我们看过 3.2节中的线性回归实现 softmax回归的训练过程代码应该看起来非常眼熟。 在这里我们重构训练过程的实现以使其可重复使用。 首先我们定义一个函数来训练一个迭代周期。 请注意updater是更新模型参数的常用函数它接受批量大小作为参数。 它可以是d2l.sgd函数也可以是框架的内置优化函数。
def train_epoch_ch3(net, train_iter, loss, updater): #save训练模型一个迭代周期定义见第3章# 将模型设置为训练模式if isinstance(net, torch.nn.Module):net.train()# 训练损失总和、训练准确度总和、样本数metric Accumulator(3)# 逐批处理数据X 是输入数据y 是对应的标签for X, y in train_iter:# 计算梯度并更新参数y_hat net(X)l loss(y_hat, y)# 使用PyTorch内置的优化器和损失函数if isinstance(updater, torch.optim.Optimizer):updater.zero_grad() # 清除之前的梯度避免梯度累加。l.mean().backward() # 将损失的平均值反向传播计算出每个参数的梯度。updater.step() # 根据梯度更新模型的参数。# 使用定制的优化器和损失函数else:l.sum().backward() # 将损失的总和反向传播计算梯度。updater(X.shape[0]) # 使用自定义的更新函数其中 X.shape[0] 表示当前批次的样本数量。metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())# 返回训练损失和训练精度return metric[0] / metric[2], metric[1] / metric[2]在神经网络训练中反向传播是通过损失函数来计算每个参数的梯度然后使用优化器来更新参数。在实际操作中将损失的平均值反向传播和将损失的总和反向传播有一些细微的区别尤其是在处理不同的批次大小batch size时这会影响梯度的计算和模型的训练效果。
损失的总和反向传播 含义这里 l.sum() 是当前批次损失的总和。 影响这种方式会根据批次的大小样本数量直接累积损失然后计算出相应的梯度。 损失总和的梯度会随批次大小增加即大批次的损失较大反向传播计算的梯度也会较大。 对于较大的批次参数更新的幅度会更大对于较小的批次更新幅度较小。 换句话说梯度的规模会受到批次大小的直接影响因此需要谨慎调节学习率。损失的平均值反向传播 含义这里 l.mean() 是当前批次损失的平均值。 影响这种方式首先将每个样本的损失求平均值然后再进行反向传播。 平均损失的梯度不会直接受到批次大小的影响。无论批次大小是多少梯度的规模都会保持一致。 这样可以使得批次大小变化时梯度的尺度保持稳定因此模型对不同批次大小的适应性更好。 在小批次和大批次之间训练时学习率可以保持不变不需要因为批次大小调整学习率。
一般来说损失平均值反向传播更推荐用于标准化和稳定的训练流程。
在展示训练函数的实现之前我们定义一个在动画中绘制数据的实用程序类Animator 它能够简化本书其余部分的代码。
class Animator: #save在动画中绘制数据def __init__(self, xlabelNone, ylabelNone, legendNone, xlimNone,ylimNone, xscalelinear, yscalelinear,fmts(-, m--, g-., r:), nrows1, ncols1,figsize(3.5, 2.5)):# 增量地绘制多条线if legend is None:legend []d2l.use_svg_display()self.fig, self.axes d2l.plt.subplots(nrows, ncols, figsizefigsize)if nrows * ncols 1:self.axes [self.axes, ]# 使用lambda函数捕获参数self.config_axes lambda: d2l.set_axes(self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)self.X, self.Y, self.fmts None, None, fmtsdef add(self, x, y):# 向图表中添加多个数据点if not hasattr(y, __len__):y [y]n len(y)if not hasattr(x, __len__):x [x] * nif not self.X:self.X [[] for _ in range(n)]if not self.Y:self.Y [[] for _ in range(n)]for i, (a, b) in enumerate(zip(x, y)):if a is not None and b is not None:self.X[i].append(a)self.Y[i].append(b)self.axes[0].cla()for x, y, fmt in zip(self.X, self.Y, self.fmts):self.axes[0].plot(x, y, fmt)self.config_axes()display.display(self.fig)display.clear_output(waitTrue)接下来我们实现一个训练函数 它会在train_iter访问到的训练数据集上训练一个模型net。 该训练函数将会运行多个迭代周期由num_epochs指定。 在每个迭代周期结束时利用test_iter访问到的测试数据集对模型进行评估。 我们将利用Animator类来可视化训练进度。
def train_ch3(net, train_iter, test_iter, loss, num_epochs, updater): #save训练模型定义见第3章animator Animator(xlabelepoch, xlim[1, num_epochs], ylim[0.3, 0.9],legend[train loss, train acc, test acc])for epoch in range(num_epochs):# 调用函数 train_epoch_ch3 执行一次训练周期并返回训练损失和训练准确率。# train_metrics 是一个元组包含训练损失和训练准确率。train_metrics train_epoch_ch3(net, train_iter, loss, updater)# evaluate_accuracy(net, test_iter)调用 evaluate_accuracy 函数评估模型在测试集上的准确率返回 test_acc。test_acc evaluate_accuracy(net, test_iter)animator.add(epoch 1, train_metrics (test_acc,))# 在训练完成后使用断言来检查训练结果是否合理train_loss, train_acc train_metrics# 确保最终训练损失小于 0.5。如果不满足抛出错误并显示当前的 train_loss。assert train_loss 0.5, train_loss# 确保训练准确率在 (0.7, 1] 之间且不超过 1。如果不满足抛出错误并显示当前的 train_accassert train_acc 1 and train_acc 0.7, train_acc# 确保测试集上的准确率也在 (0.7, 1] 之间。如果不满足抛出错误并显示当前的 test_accassert test_acc 1 and test_acc 0.7, test_acc作为一个从零开始的实现我们使用 3.2节中定义的 小批量随机梯度下降来优化模型的损失函数设置学习率为0.1
lr 0.1def 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)预测
现在训练已经完成我们的模型已经准备好对图像进行分类预测。 给定一系列图像我们将比较它们的实际标签文本输出的第一行和模型预测文本输出的第二行。
def predict_ch3(net, test_iter, n6): #save预测标签定义见第3章for X, y in test_iter:breaktrues d2l.get_fashion_mnist_labels(y)preds d2l.get_fashion_mnist_labels(net(X).argmax(axis1))titles [true \n pred for true, pred in zip(trues, preds)]d2l.show_images(X[0:n].reshape((n, 28, 28)), 1, n, titlestitles[0:n])predict_ch3(net, test_iter)3.7 softmax回归的简洁实现
在 3.3节中 我们发现通过深度学习框架的高级API能够使实现
线性回归变得更加容易。 同样通过深度学习框架的高级API也能更方便地实现softmax回归模型。 本节如在 3.6节中一样 继续使用Fashion-MNIST数据集并保持批量大小为256。
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)初始化模型参数
如我们在 3.4节所述 softmax回归的输出层是一个全连接层。 因此为了实现我们的模型 我们只需在Sequential中添加一个带有10个输出的全连接层。 同样在这里Sequential并不是必要的 但它是实现深度模型的基础。 我们仍然以均值0和标准差0.01随机初始化权重。
# PyTorch不会隐式地调整输入的形状。因此
# 我们在线性层前定义了展平层flatten来调整网络输入的形状
# nn.Flatten()将多维输入展平为一维。例如输入的图像数据通常为2D或3D会被展平为1D张量以便输入到全连接层中。对于28x28的图像如MNIST数据集它将其展平为一个长度为784的向量。
# nn.Linear(784, 10)这是一个全连接层线性层输入大小为78428x28的展平图像输出大小为10表示10个类别的分类任务例如MNIST数据集中的0-9分类。
# 这一步定义了一个两层的神经网络其中第一层是展平操作第二层是一个线性层用于将输入的数据映射到10个类别上
net nn.Sequential(nn.Flatten(), nn.Linear(784, 10))# init_weights(m)这是一个自定义的函数用于初始化网络中的参数。
# m是网络中的某个层Module。在调用 net.apply(init_weights) 时PyTorch 会遍历网络中的每个层并将它传递给这个函数。
def init_weights(m):if type(m) nn.Linear: # 检查当前传递进来的层 m 是否是一个线性层 nn.Linear。只有当层是线性层时才会执行权重初始化。# 用均值为0标准差为0.01的正态分布对线性层的权重 m.weight 进行初始化。这意味着权重会随机初始化为接近零的小值。nn.init.normal_(m.weight, std0.01)# 这一行代码会遍历 net 中的所有层并对每一层调用 init_weights 函数。
net.apply(init_weights);重新审视Softmax的实现
在前面 3.6节的例子中 我们计算了模型的输出然后将此输出送入交叉熵损失。 从数学上讲这是一件完全合理的事情。 然而从计算角度来看指数可能会造成数值稳定性问题。
回想一下softmax函数 其中y-hat_j是预测的概率分布。 o_j是未规范化的预测o的第j个元素。 如果o_k中的一些数值非常大 那么exp(o_k)可能大于数据类型容许的最大数字即上溢overflow。 这将使分母或分子变为inf无穷大 最后得到的是0、inf或nan不是数字的y-hat_j。 在这些情况下我们无法得到一个明确定义的交叉熵值。
解决这个问题的一个技巧是 在继续softmax计算之前先从所有o_k中减去max(o_k)。 这里可以看到每个o_k按常数进行的移动不会改变softmax的返回值 在减法和规范化步骤之后可能有些o_j-max(o_k)具有较大的负值。 由于精度受限exp(o_j-max(o_k))将有接近零的值即下溢underflow。 这些值可能会四舍五入为零使y-hat_j为零 并且使得log(y-hat_j)的值为**-inf**。 反向传播几步后我们可能会发现自己面对一屏幕可怕的nan结果。
尽管我们要计算指数函数但我们最终在计算交叉熵损失时会取它们的对数。 通过将softmax和交叉熵结合在一起可以避免反向传播过程中可能会困扰我们的数值稳定性问题。 如下面的等式所示我们避免计算exp(o_j-max(o_k)) 而可以直接使用o_j-max(o_k)因为log(exp(~))被抵消了。 我们也希望保留传统的softmax函数以备我们需要评估通过模型输出的概率。 但是我们没有将softmax概率传递到损失函数中 而是在交叉熵损失函数中传递未规范化的预测并同时计算softmax及其对数 这是一种类似“LogSumExp技巧”的聪明方式。
# 交叉熵损失函数
loss nn.CrossEntropyLoss(reductionnone)优化算法
在这里我们使用学习率为0.1的小批量随机梯度下降作为优化算法。 这与我们在线性回归例子中的相同这说明了优化器的普适性。
trainer torch.optim.SGD(net.parameters(), lr0.1)训练
接下来我们调用 3.6节中 定义的训练函数来训练模型。
num_epochs 10
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)和以前一样这个算法使结果收敛到一个相当高的精度而且这次的代码比之前更精简了。