线性回归与softmax回归
线性回归
我们希望根据房屋的面积(平方英尺)和房龄(年)来估算房屋价格(美元)。 为了开发一个能预测房价的模型,我们需要收集一个真实的数据集。 这个数据集包括了房屋的销售价格、面积和房龄。 在机器学习的术语中,该数据集称为训练数据集(training data set) 或训练集(training set)。 每行数据(比如一次房屋交易相对应的数据)称为样本(sample), 也可以称为数据点(data point)或数据样本(data instance)。 我们把试图预测的目标(比如预测房屋价格)称为标签(label)或目标(target)。 预测所依据的自变量(面积和房龄)称为特征(feature)或协变量(covariate)。
线性模型
线性假设是指目标(房屋价格)可以表示为特征(面积和房龄)的加权和。$$PRICE=W_{area}*area+W_{age}*age+b$$w是权重,b是偏移量。给定一个数据集,我们的目标是寻找模型的权重w和偏置b, 使得根据模型做出的预测大体符合数据里的真实价格。
损失函数
在我们开始考虑如何用模型拟合(fit)数据之前,我们需要确定一个拟合程度的度量。 损失函数(loss function)能够量化目标的实际值与预测值之间的差距。 通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为0。
回归问题中最常用的损失函数是平方误差函数。当样本$$i$$的预测值为$$\hat{y}^{(i)}$$, 其相应的真实标签为$$y{(i)}$$时,平方误差为$$l{(i)}(\mathbf{w}, b) = \frac{1}{2} \left(\hat{y}^{(i)} - y{(i)}\right)2$$。
由于平方误差函数中的二次方项, 估计值和观测值之间较大的差异将导致更大的损失。 为了度量模型在整个数据集上的质量,我们需计算在训练集n个样本上的损失均值(也等价于求和)。
解析解
在线性回归中,解析解是一种计算最优解的方法,也称为闭式解或正规方程法。其基本思想是通过对数据集进行矩阵运算,求出使得误差最小的权重矩阵(即最优参数),从而得到一个全局最优解。令 表示回归系数矩阵, 表示全部特征向量组合的矩阵, 表示标记列向量组成的矩阵。则线性回归的解析解可表示为:
公式 可以通过求导和极值原理来推导得到。下面假设输入样本矩阵 的每一个实例均为 维向量,输出结果为:
假设我们想要通过矩阵 来预测输出结果 ,我们假定预测值为 。为了最小化预测值 和实际值 之间的差距,我们可以通过最小二乘法来确定最佳的权重向量 。最小二乘法就是通过最小化误差平方和的方法得到 。
定义误差 为第 个样本预测值 与 实际值 之间的差距,即 。则所有样本的误差平方和可以表示为:
其中, 表示第 个输入样本的特征向量, 表示整个训练集的特征矩阵, 表示我们要求解的权重向量, 表示整个训练集的目标向量。
为了找到使误差平方和最小的权重向量,我们需要对 求导。为了简化求导的过程,我们可以将 写成如下形式:
其中,我们省略了常数项,并利用了 的关系式。
对 求导可以得到:
令导数为 0,则可以得到:
因此, 的最优解可由矩阵 的逆矩阵和 相乘而得。这就是线性回归模型的闭式解(即解析解)
随机梯度下降
即使在我们无法得到解析解的情况下,我们仍然可以有效地训练模型。 随机梯度下降这种方法几乎可以优化所有深度学习模型。 它通过不断地在损失函数递减的方向上更新参数来降低误差。
梯度下降最简单的用法是计算损失函数(数据集中所有样本的损失均值) 关于模型参数的导数(在这里也可以称为梯度)。 但实际中的执行可能会非常慢:因为在每一次更新参数之前,我们必须遍历整个数据集。 因此,我们通常会在每次需要计算更新的时候随机抽取一小批样本, 这种变体叫做小批量随机梯度下降(minibatch stochastic gradient descent)。
在每次迭代中,我们首先随机抽样一个小批量$$\mathcal{B}$$, 它是由固定数量的训练样本组成的。 然后,我们计算小批量的平均损失关于模型参数的导数(也可以称为梯度)。 最后,我们将梯度乘以一个预先确定的正数$$\eta,$$并从当前参数的值中减掉。
总结一下,算法的步骤如下: (1)初始化模型参数的值,如随机初始化; (2)从数据集中随机抽取小批量样本且在负梯度的方向上更新参数,并不断迭代这一步骤。 对于平方损失和仿射变换,我们可以明确地写成如下形式:
\begin{split}\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}\end{split}
|\mathcal{B}|$$表示每个小批量中的样本数,这也称为*批量大小*(batch size)。$$\eta$$表示*学习率*(learning rate)。 ### 预测 给定“已学习”的线性回归模型, 现在我们可以通过房屋面积x1和房龄x2来估计一个(未包含在训练数据中的)新房屋价格。 给定特征估计目标的过程通常称为*预测*(prediction)或*推断*(inference)。 #### 加速 矢量化代码通常会带来数量级的加速。 另外,我们将更多的数学运算放到库中,而无须自己编写那么多的计算,从而减少了出错的可能性。 #### 正态分布 若随机变量$$ X$$具有均值$$\mu$$和方差$$\sigma^2$$(标准差$$\sigma$$),其正态分布概率密度函数如下: $$p(x) = \frac{1}{\sqrt{2 \pi \sigma^2}} \exp\left(-\frac{1}{2 \sigma^2} (x - \mu)^2\right)
改变均值会产生沿x轴的偏移,增加方差将会分散分布、降低其峰值。
从线性回归到深度网络
该图只显示连接模式,即只显示每个输入如何连接到输出,隐去了权重和偏置的值。
输入数为d,神经网络层数为1。我们可以将线性回归模型视为仅由单个人工神经元组成的神经网络,或称为单层神经网络。对于线性回归,每个输入都与每个输出(在本例中只有一个输出)相连, 我们将这种变换称为全连接层(fully-connected layer)或称为稠密层(dense layer)。
线性回归实现过程
生成数据集
生成一个包含1000个样本的数据集, 每个样本包含从标准正态分布中采样的2个特征。
我们使用线性模型参数$$\mathbf{w} = [2, -3.4]^\top$$、b = 4.2和噪声项$$\epsilon$$生成数据集及其标签:
def synthetic_data(w, b, num_examples): #@save
"""生成y=Xw+b+噪声"""
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b
y += torch.normal(0, 0.01, y.shape)
return X, y.reshape((-1, 1))
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
features: tensor([-0.3679, -1.8471])
label: tensor([9.7361])
注意,features
中的每一行都包含一个二维数据样本, labels
中的每一行都包含一维标签值(一个标量)。
通过生成第二个特征features[:, 1]
和labels
的散点图, 可以直观观察到两者之间的线性关系。
读取数据集
回想一下,训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。 由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数, 该函数能打乱数据集中的样本并以小批量方式获取数据。
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 表示每次迭代所选取的样本数量,features 和 labels 分别表示输入特征和对应的标签。
- 通过 features 的长度获取数据集的样本数,在 indices 列表中记录所有样本的下标。
- 对 indices 进行随机重排,将样本打乱,以保证每个 batch 中的样本具有随机性。
- 在 for 循环内,从 0 到 num_examples 循环,每次取出 batch_size 个样本,并将这些样本的下标通过 torch.tensor 转换为 Tensor 类型。
- 使用 yield 关键字将每次迭代生成的小批量样本(features 和 labels)返回给调用者。这里使用了 Python 的迭代器来实现,与常规的列表返回方法不同,它可以逐步生成数据,在大数据量的情况下节省了内存空间
初始化模型参数
从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0。在初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。 每次更新都需要计算损失函数关于模型参数的梯度。 有了这个梯度,我们就可以向减小损失的方向更新每个参数。
定义模型
当我们用一个向量加一个标量时,标量会被加到向量的每个分量上。
定义损失函数
平方损失函数
定义优化算法
小批量随机梯度下降
在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。 接下来,朝着减少损失的方向更新我们的参数。 下面的函数实现小批量随机梯度下降更新。 该函数接受模型参数集合、学习速率和批量大小作为输入。每 一步更新的大小由学习速率lr
决定。 因为我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size
) 来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。
def sgd(params, lr, batch_size): #@save
"""小批量随机梯度下降"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
更新其值为减去学习率乘以参数的梯度均值。
训练
- 初始化参数
- 重复以下训练,直到完成
- 计算梯度$$\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)$$
- 更新参数$$(\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g}$$
在每个迭代周期(epoch)中,我们使用data_iter
函数遍历整个数据集, 并将训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。 这里的迭代周期个数num_epochs
和学习率lr
都是超参数,分别设为3和0.03。
lr = 0.03 # 学习率
num_epochs = 3 # 迭代周期数
net = linreg # 线性回归模型函数
loss = squared_loss # 损失函数
for epoch in range(num_epochs): # 迭代 num_epochs 次
for X, y in data_iter(batch_size, features, labels): # 遍历数据集,每次随机读取 batch_size 个样本
l = loss(net(X, w, b), y) # 计算模型预测值与真实值之间的损失,使用的损失函数是二次损失
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}') # 打印输出模型在整个训练集上的损失
epoch 1, loss 0.043705
epoch 2, loss 0.000172
epoch 3, loss 0.000047
用深度学习框架实现线性回归
读取数据集
将features
和labels
作为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)
这是一段用于构造 PyTorch 数据迭代器的代码。下面是对每一行代码的解释:
def load_array(data_arrays, batch_size, is_train=True):
"""构造一个PyTorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays) # 使用TensorDataset封装输入数据
return data.DataLoader(dataset, batch_size, shuffle=is_train) # 使用DataLoader加载数据集并返回迭代器
batch_size = 10 # 批量大小
data_iter = load_array((features, labels), batch_size) # 构造一个PyTorch数据迭代器
这段代码主要用于将数据集加载到 PyTorch 数据迭代器中。load_array()
函数将 PyTorch 中的 Tensor
数据类型封装到 TensorDataset
中,并将其作为数据集传递到 PyTorch 的 DataLoader
中,从而能够使用 PyTorch 的支持便捷的数据加载方式。
在这段代码中,data_arrays
是由特征数据和标签数据构成的元组,batch_size
是批量大小,is_train
表示是否为训练集。DataLoader
对象将数据集分批加载,然后返回一个 Python iterable
,通过 __next__
方法可以依次得到批量数据。
通过这段代码可以方便地将数据集转换为 PyTorch 数据迭代器,从而提高模型训练的效率。
定义模型
在PyTorch中,全连接层在Linear
类中定义。 值得注意的是,我们将两个参数传递到nn.Linear
中。 第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。
# nn是神经网络的缩写
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
初始化模型参数
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
定义损失函数
计算均方误差使用的是MSELoss
类,也称为平方$$L_2$$范数。 默认情况下,它返回所有样本损失的平均值。
$ MSE = \frac{1}{n}\sum_{i=1}^{n} (y_{i}-\hat{y_{i}})^{2} $
loss = nn.MSELoss()
优化算法
小批量随机梯度下降算法是一种优化神经网络的标准工具, PyTorch在optim
模块中实现了该算法的许多变种。 当我们实例化一个SGD
实例时,我们要指定优化的参数 (可通过net.parameters()
从我们的模型中获得)以及优化算法所需的超参数字典。
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
训练
回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(train_data
), 不停地从中获取一个小批量的输入和相应的标签。 对于每一个小批量,我们会进行以下步骤:
- 通过调用
net(X)
生成预测并计算损失l
(前向传播)。 - 通过进行反向传播来计算梯度。
- 通过调用优化器来更新模型参数。
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
l = loss(net(X) ,y)
trainer.zero_grad()
l.backward()
trainer.step()
l = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l:f}')
softmax回归
独热编码是一个向量,它的分量和类别一样多。 类别对应的分量设置为1,其他所有分量设置为0。
为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。
为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数(affine function)。 每个输出对应于它自己的仿射函数。 在我们的例子中,由于我们有4个特征和3个可能的输出类别, 我们将需要12个标量来表示权重(带下标的w), 3个标量来表示偏置(带下标的b)。 下面我们为每个输入计算三个未规范化的预测(logit):$$O_1、O_2$$和$$O_3$$。
\begin{split}\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}\end{split}
与线性回归一样,softmax回归也是一个单层神经网络。每个输出取决于所有输入,所以也是全连接层。
现在我们将优化参数以最大化观测数据的概率。 为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。
我们希望模型的输出$$\hat{y}_j$$可以视为属于类j的概率, 然后选择具有最大输出值的类别$$\operatorname*{argmax}_j y_j$$作为我们的预测。 例如,如果$$\hat{y}_1、\hat{y}_2$$和$$\hat{y}_3$$分别为0.1、0.8和0.1, 那么我们预测的类别是2,在我们的例子中代表“鸡”。
要将输出视为概率,我们必须保证在任何数据上的输出都是非负的且总和为1。->softmax函数
尽管softmax是一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定。 因此,softmax回归是一个线性模型(linear model)。
接下来,我们需要一个损失函数来度量预测的效果。 我们将使用最大似然估计。
损失函数(对数似然)
SOFTMAX及其导数
\begin{split}\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}\end{split}
换句话说,导数是我们softmax模型分配的概率与实际发生的情况(由独热标签向量表示)之间的差异。
SOFTMAX从零实现
初始化
num_inputs = 784
num_outputs = 10
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)
定义softmax
回想一下,实现softmax由三个步骤组成:
- 对每个项求幂(使用
exp
); - 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;
- 将每一行除以其规范化常数,确保结果的和为1。
定义模型
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
定义损失函数
训练
定义一个函数来训练一个迭代周期。
def train_epoch_ch3(net, train_iter, loss, updater): #@save
"""训练模型一个迭代周期(定义见第3章)"""
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module):
net.train()
# 训练损失总和、训练准确度总和、样本数
metric = Accumulator(3)
for X, y in train_iter:
# 计算梯度并更新参数
y_hat = net(X)
l = loss(y_hat, y)
if isinstance(updater, torch.optim.Optimizer):
# 使用PyTorch内置的优化器和损失函数
updater.zero_grad()
l.mean().backward()
updater.step()
else:
# 使用定制的优化器和损失函数
l.sum().backward()
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2]
预测
def predict_ch3(net, test_iter, n=6): #@save
"""预测标签(定义见第3章)"""
for X, y in test_iter:
break
trues = d2l.get_fashion_mnist_labels(y)
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
d2l.show_images(
X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])
predict_ch3(net, test_iter)
线性回归
我们希望根据房屋的面积(平方英尺)和房龄(年)来估算房屋价格(美元)。 为了开发一个能预测房价的模型,我们需要收集一个真实的数据集。 这个数据集包括了房屋的销售价格、面积和房龄。 在机器学习的术语中,该数据集称为训练数据集(training data set) 或训练集(training set)。 每行数据(比如一次房屋交易相对应的数据)称为样本(sample), 也可以称为数据点(data point)或数据样本(data instance)。 我们把试图预测的目标(比如预测房屋价格)称为标签(label)或目标(target)。 预测所依据的自变量(面积和房龄)称为特征(feature)或协变量(covariate)。
线性模型
线性假设是指目标(房屋价格)可以表示为特征(面积和房龄)的加权和。$$PRICE=W_{area}*area+W_{age}*age+b$$w是权重,b是偏移量。给定一个数据集,我们的目标是寻找模型的权重w和偏置b, 使得根据模型做出的预测大体符合数据里的真实价格。
损失函数
在我们开始考虑如何用模型拟合(fit)数据之前,我们需要确定一个拟合程度的度量。 损失函数(loss function)能够量化目标的实际值与预测值之间的差距。 通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为0。
回归问题中最常用的损失函数是平方误差函数。当样本$$i$$的预测值为$$\hat{y}^{(i)}$$, 其相应的真实标签为$$y{(i)}$$时,平方误差为$$l{(i)}(\mathbf{w}, b) = \frac{1}{2} \left(\hat{y}^{(i)} - y{(i)}\right)2$$。
由于平方误差函数中的二次方项, 估计值和观测值之间较大的差异将导致更大的损失。 为了度量模型在整个数据集上的质量,我们需计算在训练集n个样本上的损失均值(也等价于求和)。
解析解
在线性回归中,解析解是一种计算最优解的方法,也称为闭式解或正规方程法。其基本思想是通过对数据集进行矩阵运算,求出使得误差最小的权重矩阵(即最优参数),从而得到一个全局最优解。令 表示回归系数矩阵, 表示全部特征向量组合的矩阵, 表示标记列向量组成的矩阵。则线性回归的解析解可表示为:
公式 可以通过求导和极值原理来推导得到。下面假设输入样本矩阵 的每一个实例均为 维向量,输出结果为:
假设我们想要通过矩阵 来预测输出结果 ,我们假定预测值为 。为了最小化预测值 和实际值 之间的差距,我们可以通过最小二乘法来确定最佳的权重向量 。最小二乘法就是通过最小化误差平方和的方法得到 。
定义误差 为第 个样本预测值 与 实际值 之间的差距,即 。则所有样本的误差平方和可以表示为:
其中, 表示第 个输入样本的特征向量, 表示整个训练集的特征矩阵, 表示我们要求解的权重向量, 表示整个训练集的目标向量。
为了找到使误差平方和最小的权重向量,我们需要对 求导。为了简化求导的过程,我们可以将 写成如下形式:
其中,我们省略了常数项,并利用了 的关系式。
对 求导可以得到:
令导数为 0,则可以得到:
因此, 的最优解可由矩阵 的逆矩阵和 相乘而得。这就是线性回归模型的闭式解(即解析解)
随机梯度下降
即使在我们无法得到解析解的情况下,我们仍然可以有效地训练模型。 随机梯度下降这种方法几乎可以优化所有深度学习模型。 它通过不断地在损失函数递减的方向上更新参数来降低误差。
梯度下降最简单的用法是计算损失函数(数据集中所有样本的损失均值) 关于模型参数的导数(在这里也可以称为梯度)。 但实际中的执行可能会非常慢:因为在每一次更新参数之前,我们必须遍历整个数据集。 因此,我们通常会在每次需要计算更新的时候随机抽取一小批样本, 这种变体叫做小批量随机梯度下降(minibatch stochastic gradient descent)。
在每次迭代中,我们首先随机抽样一个小批量$$\mathcal{B}$$, 它是由固定数量的训练样本组成的。 然后,我们计算小批量的平均损失关于模型参数的导数(也可以称为梯度)。 最后,我们将梯度乘以一个预先确定的正数$$\eta,$$并从当前参数的值中减掉。
总结一下,算法的步骤如下: (1)初始化模型参数的值,如随机初始化; (2)从数据集中随机抽取小批量样本且在负梯度的方向上更新参数,并不断迭代这一步骤。 对于平方损失和仿射变换,我们可以明确地写成如下形式:
\begin{split}\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}\end{split}
|\mathcal{B}|$$表示每个小批量中的样本数,这也称为*批量大小*(batch size)。$$\eta$$表示*学习率*(learning rate)。 ### 预测 给定“已学习”的线性回归模型, 现在我们可以通过房屋面积x1和房龄x2来估计一个(未包含在训练数据中的)新房屋价格。 给定特征估计目标的过程通常称为*预测*(prediction)或*推断*(inference)。 #### 加速 矢量化代码通常会带来数量级的加速。 另外,我们将更多的数学运算放到库中,而无须自己编写那么多的计算,从而减少了出错的可能性。 #### 正态分布 若随机变量$$ X$$具有均值$$\mu$$和方差$$\sigma^2$$(标准差$$\sigma$$),其正态分布概率密度函数如下: $$p(x) = \frac{1}{\sqrt{2 \pi \sigma^2}} \exp\left(-\frac{1}{2 \sigma^2} (x - \mu)^2\right)
改变均值会产生沿x轴的偏移,增加方差将会分散分布、降低其峰值。
从线性回归到深度网络
该图只显示连接模式,即只显示每个输入如何连接到输出,隐去了权重和偏置的值。
输入数为d,神经网络层数为1。我们可以将线性回归模型视为仅由单个人工神经元组成的神经网络,或称为单层神经网络。对于线性回归,每个输入都与每个输出(在本例中只有一个输出)相连, 我们将这种变换称为全连接层(fully-connected layer)或称为稠密层(dense layer)。
线性回归实现过程
生成数据集
生成一个包含1000个样本的数据集, 每个样本包含从标准正态分布中采样的2个特征。
我们使用线性模型参数$$\mathbf{w} = [2, -3.4]^\top$$、b = 4.2和噪声项$$\epsilon$$生成数据集及其标签:
def synthetic_data(w, b, num_examples): #@save
"""生成y=Xw+b+噪声"""
X = torch.normal(0, 1, (num_examples, len(w)))
y = torch.matmul(X, w) + b
y += torch.normal(0, 0.01, y.shape)
return X, y.reshape((-1, 1))
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
features: tensor([-0.3679, -1.8471])
label: tensor([9.7361])
注意,features
中的每一行都包含一个二维数据样本, labels
中的每一行都包含一维标签值(一个标量)。
通过生成第二个特征features[:, 1]
和labels
的散点图, 可以直观观察到两者之间的线性关系。
读取数据集
回想一下,训练模型时要对数据集进行遍历,每次抽取一小批量样本,并使用它们来更新我们的模型。 由于这个过程是训练机器学习算法的基础,所以有必要定义一个函数, 该函数能打乱数据集中的样本并以小批量方式获取数据。
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 表示每次迭代所选取的样本数量,features 和 labels 分别表示输入特征和对应的标签。
- 通过 features 的长度获取数据集的样本数,在 indices 列表中记录所有样本的下标。
- 对 indices 进行随机重排,将样本打乱,以保证每个 batch 中的样本具有随机性。
- 在 for 循环内,从 0 到 num_examples 循环,每次取出 batch_size 个样本,并将这些样本的下标通过 torch.tensor 转换为 Tensor 类型。
- 使用 yield 关键字将每次迭代生成的小批量样本(features 和 labels)返回给调用者。这里使用了 Python 的迭代器来实现,与常规的列表返回方法不同,它可以逐步生成数据,在大数据量的情况下节省了内存空间
初始化模型参数
从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重, 并将偏置初始化为0。在初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。 每次更新都需要计算损失函数关于模型参数的梯度。 有了这个梯度,我们就可以向减小损失的方向更新每个参数。
定义模型
当我们用一个向量加一个标量时,标量会被加到向量的每个分量上。
定义损失函数
平方损失函数
定义优化算法
小批量随机梯度下降
在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。 接下来,朝着减少损失的方向更新我们的参数。 下面的函数实现小批量随机梯度下降更新。 该函数接受模型参数集合、学习速率和批量大小作为输入。每 一步更新的大小由学习速率lr
决定。 因为我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size
) 来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。
def sgd(params, lr, batch_size): #@save
"""小批量随机梯度下降"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
更新其值为减去学习率乘以参数的梯度均值。
训练
- 初始化参数
- 重复以下训练,直到完成
- 计算梯度$$\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)$$
- 更新参数$$(\mathbf{w}, b) \leftarrow (\mathbf{w}, b) - \eta \mathbf{g}$$
在每个迭代周期(epoch)中,我们使用data_iter
函数遍历整个数据集, 并将训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。 这里的迭代周期个数num_epochs
和学习率lr
都是超参数,分别设为3和0.03。
lr = 0.03 # 学习率
num_epochs = 3 # 迭代周期数
net = linreg # 线性回归模型函数
loss = squared_loss # 损失函数
for epoch in range(num_epochs): # 迭代 num_epochs 次
for X, y in data_iter(batch_size, features, labels): # 遍历数据集,每次随机读取 batch_size 个样本
l = loss(net(X, w, b), y) # 计算模型预测值与真实值之间的损失,使用的损失函数是二次损失
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}') # 打印输出模型在整个训练集上的损失
epoch 1, loss 0.043705
epoch 2, loss 0.000172
epoch 3, loss 0.000047
用深度学习框架实现线性回归
读取数据集
将features
和labels
作为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)
这是一段用于构造 PyTorch 数据迭代器的代码。下面是对每一行代码的解释:
def load_array(data_arrays, batch_size, is_train=True):
"""构造一个PyTorch数据迭代器"""
dataset = data.TensorDataset(*data_arrays) # 使用TensorDataset封装输入数据
return data.DataLoader(dataset, batch_size, shuffle=is_train) # 使用DataLoader加载数据集并返回迭代器
batch_size = 10 # 批量大小
data_iter = load_array((features, labels), batch_size) # 构造一个PyTorch数据迭代器
这段代码主要用于将数据集加载到 PyTorch 数据迭代器中。load_array()
函数将 PyTorch 中的 Tensor
数据类型封装到 TensorDataset
中,并将其作为数据集传递到 PyTorch 的 DataLoader
中,从而能够使用 PyTorch 的支持便捷的数据加载方式。
在这段代码中,data_arrays
是由特征数据和标签数据构成的元组,batch_size
是批量大小,is_train
表示是否为训练集。DataLoader
对象将数据集分批加载,然后返回一个 Python iterable
,通过 __next__
方法可以依次得到批量数据。
通过这段代码可以方便地将数据集转换为 PyTorch 数据迭代器,从而提高模型训练的效率。
定义模型
在PyTorch中,全连接层在Linear
类中定义。 值得注意的是,我们将两个参数传递到nn.Linear
中。 第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。
# nn是神经网络的缩写
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
初始化模型参数
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
定义损失函数
计算均方误差使用的是MSELoss
类,也称为平方$$L_2$$范数。 默认情况下,它返回所有样本损失的平均值。
$ MSE = \frac{1}{n}\sum_{i=1}^{n} (y_{i}-\hat{y_{i}})^{2} $
loss = nn.MSELoss()
优化算法
小批量随机梯度下降算法是一种优化神经网络的标准工具, PyTorch在optim
模块中实现了该算法的许多变种。 当我们实例化一个SGD
实例时,我们要指定优化的参数 (可通过net.parameters()
从我们的模型中获得)以及优化算法所需的超参数字典。
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
训练
回顾一下:在每个迭代周期里,我们将完整遍历一次数据集(train_data
), 不停地从中获取一个小批量的输入和相应的标签。 对于每一个小批量,我们会进行以下步骤:
- 通过调用
net(X)
生成预测并计算损失l
(前向传播)。 - 通过进行反向传播来计算梯度。
- 通过调用优化器来更新模型参数。
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
l = loss(net(X) ,y)
trainer.zero_grad()
l.backward()
trainer.step()
l = loss(net(features), labels)
print(f'epoch {epoch + 1}, loss {l:f}')
softmax回归
独热编码是一个向量,它的分量和类别一样多。 类别对应的分量设置为1,其他所有分量设置为0。
为了估计所有可能类别的条件概率,我们需要一个有多个输出的模型,每个类别对应一个输出。
为了解决线性模型的分类问题,我们需要和输出一样多的仿射函数(affine function)。 每个输出对应于它自己的仿射函数。 在我们的例子中,由于我们有4个特征和3个可能的输出类别, 我们将需要12个标量来表示权重(带下标的w), 3个标量来表示偏置(带下标的b)。 下面我们为每个输入计算三个未规范化的预测(logit):$$O_1、O_2$$和$$O_3$$。
\begin{split}\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}\end{split}
与线性回归一样,softmax回归也是一个单层神经网络。每个输出取决于所有输入,所以也是全连接层。
现在我们将优化参数以最大化观测数据的概率。 为了得到预测结果,我们将设置一个阈值,如选择具有最大概率的标签。
我们希望模型的输出$$\hat{y}_j$$可以视为属于类j的概率, 然后选择具有最大输出值的类别$$\operatorname*{argmax}_j y_j$$作为我们的预测。 例如,如果$$\hat{y}_1、\hat{y}_2$$和$$\hat{y}_3$$分别为0.1、0.8和0.1, 那么我们预测的类别是2,在我们的例子中代表“鸡”。
要将输出视为概率,我们必须保证在任何数据上的输出都是非负的且总和为1。->softmax函数
尽管softmax是一个非线性函数,但softmax回归的输出仍然由输入特征的仿射变换决定。 因此,softmax回归是一个线性模型(linear model)。
接下来,我们需要一个损失函数来度量预测的效果。 我们将使用最大似然估计。
损失函数(对数似然)
SOFTMAX及其导数
\begin{split}\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}\end{split}
换句话说,导数是我们softmax模型分配的概率与实际发生的情况(由独热标签向量表示)之间的差异。
SOFTMAX从零实现
初始化
num_inputs = 784
num_outputs = 10
W = torch.normal(0, 0.01, size=(num_inputs, num_outputs), requires_grad=True)
b = torch.zeros(num_outputs, requires_grad=True)
定义softmax
回想一下,实现softmax由三个步骤组成:
- 对每个项求幂(使用
exp
); - 对每一行求和(小批量中每个样本是一行),得到每个样本的规范化常数;
- 将每一行除以其规范化常数,确保结果的和为1。
定义模型
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
定义损失函数
训练
定义一个函数来训练一个迭代周期。
def train_epoch_ch3(net, train_iter, loss, updater): #@save
"""训练模型一个迭代周期(定义见第3章)"""
# 将模型设置为训练模式
if isinstance(net, torch.nn.Module):
net.train()
# 训练损失总和、训练准确度总和、样本数
metric = Accumulator(3)
for X, y in train_iter:
# 计算梯度并更新参数
y_hat = net(X)
l = loss(y_hat, y)
if isinstance(updater, torch.optim.Optimizer):
# 使用PyTorch内置的优化器和损失函数
updater.zero_grad()
l.mean().backward()
updater.step()
else:
# 使用定制的优化器和损失函数
l.sum().backward()
updater(X.shape[0])
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回训练损失和训练精度
return metric[0] / metric[2], metric[1] / metric[2]
预测
def predict_ch3(net, test_iter, n=6): #@save
"""预测标签(定义见第3章)"""
for X, y in test_iter:
break
trues = d2l.get_fashion_mnist_labels(y)
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
titles = [true +'\n' + pred for true, pred in zip(trues, preds)]
d2l.show_images(
X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])
predict_ch3(net, test_iter)