Skip to content

线性回归

线性回归

回归(regression)是能为一个或多个自变量与因变量之间关系建模的一类方法。在我们开始考虑如何用模型拟合(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.\)

由于平方误差函数中的二次方项,估计值\(\hat{y}^{(i)}\)和观测值\(y^{(i)}\)之间较大的差异将导致更大的损失。为了度量模型在整个数据集上的质量,我们需计算在训练集\(n\)个样本上的损失均值(也等价于求和)。 在训练模型时,我们希望寻找一组参数(\(\mathbf{w}^*, b^*\)),这组参数能最小化在所有训练样本上的总损失。如下式: 那么\(L(\mathbf{w}, b)\)就是一个关于\(\mathbf{w}, b\)这两个矩阵的函数, 那么就可以对这两个矩阵求导, 求出的导数将用于更行两个参数.

我们用到一种名为梯度下降(gradient descent)的方法,这种方法几乎可以优化所有深度学习模型。它通过不断地在损失函数递减的方向上更新参数来降低误差。但实际中的执行可能会非常慢:因为在每一次更新参数之前,我们必须遍历整个数据集。因此,我们通常会在每次需要计算更新的时候随机抽取一小批样本,这种变体叫做小批量随机梯度下降(minibatch stochastic gradient descent)。

怎么理解上面这段话? 求出导数在某个确切值的矩阵处的数值矩阵毕竟还是需要代入数据集的数据的. 与其更新一次w, b矩阵需要遍历整个数据集, 不如直接小批量随机梯度下降, 一小部分数据就能更新一次参数, 效率就高多了! 效率高就是节省计算资源!

在每次迭代中,我们首先随机抽样一个小批量\(\mathcal{B}\),它是由固定数量的训练样本组成的。然后,我们计算小批量的平均损失关于模型参数的导数(也可以称为梯度)。最后,我们将梯度乘以一个预先确定的正数\(\eta\),并从当前参数的值中减掉。我们用下面的数学公式来表示这一更新过程(\(\partial\)表示偏导数): 总结一下,算法的步骤如下:

(1)初始化模型参数的值,如随机初始化;

(2)从数据集中随机抽取小批量样本且在负梯度的方向上更新参数,并不断迭代这一步骤。

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

正太分布与平方损失

接下来,我们通过对噪声分布的假设来解读平方损失目标函数。正态分布和线性回归之间的关系很密切。正态分布(normal distribution),也称为高斯分布(Gaussian distribution),简单的说,若随机变量\(x\)具有均值\(\mu\)和方差\(\sigma^2\)(标准差\(\sigma\)),其正态分布概率密度函数如下: 均方误差损失函数(简称均方损失)可以用于线性回归的一个原因是:我们假设了观测中包含噪声,其中噪声服从正态分布。噪声正态分布:\(\(y = \mathbf{w}^\top \mathbf{x} + b + \epsilon,\)\) 其中,\(\epsilon \sim \mathcal{N}(0, \sigma^2)\)。因此, y也是正态分布的随机变量, 分布可以写成\(y∼N(w⊤x+b,σ^2)\)

因此,我们现在可以写出通过给定的\(\mathbf{x}\)观测到特定\(y\)似然(likelihood): 这个表达式描述了在给定输入 \(\mathbf{x}\)的条件下,观测到输出y的概率分布。

极大似然估计法(Maximum Likelihood Estimation, 简称 MLE)是一种估计模型参数的统计方法。它的目标是找到使观测数据在某一假设的概率模型下出现的概率(即似然)最大的参数值。在给定一组观测数据和一个参数化的统计模型的前提下,极大似然估计法通过最大化观测数据在该模型中的似然函数,从而得到最符合数据的模型参数。换句话说,极大似然估计法找到的是那些使得观测数据最有可能发生的参数值。

现在,根据极大似然估计法,参数\(\mathbf{w}\)\(b\)的最优值是使整个数据集的似然最大的值(由于样本之间是独立的(独立同分布假设,i.i.d.),整个数据集 \(\mathbf{y}\) 在给定输入 \(\mathbf{X}\) 下的联合概率就是每个样本条件概率的乘积): 虽然使许多指数函数的乘积最大化看起来很困难,但是我们可以在不改变目标的前提下,通过最大化似然对数来简化。我们可以改为最小化负对数似然\(-\log P(\mathbf y \mid \mathbf X)\)。由此可以得到的数学公式是: 现在我们只需要假设\(\sigma\)是某个固定常数就可以忽略第一项,因为第一项不依赖于\(\mathbf{w}\)\(b\)。现在第二项除了常数\(\frac{1}{\sigma^2}\)外,其余部分和前面介绍的均方误差是一样的。幸运的是,上面式子的解并不依赖于\(\sigma\)。因此,在高斯噪声的假设下,最小化均方误差等价于对线性模型的极大似然估计

从零开始的线性回归

在这一节中,我们将只使用张量和自动求导。为了简单起见,我们将[根据带有噪声的线性模型构造一个人造数据集。]我们的任务是使用这个有限样本的数据集来恢复这个模型的参数。我们将使用低维数据,这样可以很容易地将其可视化。在下面的代码中,我们生成一个包含1000个样本的数据集,每个样本包含从标准正态分布中采样的2个特征。我们的合成数据集是一个矩阵\(\mathbf{X}\in \mathbb{R}^{1000 \times 2}\)

我们使用线性模型参数\(\mathbf{w} = [2, -3.4]^\top\)\(b = 4.2\)和噪声项\(\epsilon\)生成数据集及其标签:\(\mathbf{y}= \mathbf{X} \mathbf{w} + b + \mathbf\epsilon.\)

def synthetic_data(w, b, num_examples): 
    X = torch.normal(0, 1, (num_examples, len(w)))
    y = torch.matmul(X, w) + b
    y += torch.normal(0, 0.01, y.shape)
    return X, y.reshape((-1, 1))
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
print(features.shape, labels.shape)
# torch.Size([1000, 2]) torch.Size([1000, 1])

注意,[features中的每一行都包含一个二维数据样本,labels中的每一行都包含一维标签值(一个标量]。数据已经创建好了, 接下来读取的就是数据集了:

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

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

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

在这段代码中,yield 是 Python 中生成器函数的关键字之一。与普通的 return 不同,yield 不会立即结束函数的执行,而是会暂停函数的执行,并返回一个值。当再次调用这个生成器函数时,函数会从上次暂停的地方继续执行。

在下面的代码中,我们通过从均值为0、标准差为0.01的正态分布中采样随机数来初始化权重,并将偏置初始化为0。

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

在初始化参数之后,我们的任务是更新这些参数,直到这些参数足够拟合我们的数据。每次更新都需要计算损失函数关于模型参数的梯度。有了这个梯度,我们就可以向减小损失的方向更新每个参数。

接下来,我们必须[定义模型,将模型的输入和参数同模型的输出关联起来。]

回想一下,要计算线性模型的输出,我们只需计算输入特征\(\mathbf{X}\)和模型权重\(\mathbf{w}\)的矩阵-向量乘法后加上偏置\(b\)。注意,上面的\(\mathbf{Xw}\)是一个向量,而\(b\)是一个标量。而当我们用一个向量加一个标量时,标量会被加到向量的每个分量上。因为需要计算损失函数的梯度,所以我们应该先定义损失函数。这里我们使用平方损失函数(MSELoss)。

def linreg(X, w, b): 
    """线性回归模型"""
    return torch.matmul(X, w) + b
def squared_loss(y_hat, y):  
    """均方损失"""
    return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2

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

在每一步中,使用从数据集中随机抽取的一个小批量,然后根据参数计算损失的梯度。接下来,朝着减少损失的方向更新我们的参数。下面的函数实现小批量随机梯度下降更新。该函数接受模型参数集合、学习速率和批量大小作为输入。每一步更新的大小由学习速率lr决定。因为我们计算的损失是一个批量样本的总和,所以我们用批量大小(batch_size)来规范化步长,这样步长大小就不会取决于我们对批量大小的选择。

def sgd(params, lr, batch_size): 
    """小批量随机梯度下降"""
    # 现在情况是已经params计算过一次梯度了,然后需要更新并清空储存的梯度
    with torch.no_grad(): 
    # 在模型推理(inference)阶段或在不需要计算梯度的情况下使用这个上下文管理器
    # 以节省内存和提高计算速度
        for param in params: 
            param -= lr * param.grad / batch_size
            param.grad.zero_() # 清空

现在我们已经准备好了模型训练所有需要的要素,可以实现主要的[训练过程]部分了。在每次迭代中,我们读取一小批量训练样本,并通过我们的模型来获得一组预测。计算完损失后,我们开始反向传播,存储每个参数的梯度。最后,我们调用优化算法sgd来更新模型参数。

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

lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss
for epoch in range(num_epochs):
    for X, y in data_iter(batch_size, features, labels):
        l = loss(net(X, w, b), y)  # X和y的小批量损失
        # 因为l形状是(batch_size,1),而不是一个标量。l中的所有元素被加到一起,
        # 并以此计算关于[w,b]的梯度
        l.sum().backward()
        sgd([w, b], lr, batch_size)  # 使用参数的梯度更新参数
    with torch.no_grad():
        train_l = loss(net(features, w, b), labels)
        print(f'epoch {epoch + 1}, loss {float(train_l.mean()):f}')
print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')
# w的估计误差: tensor([ 0.0004, -0.0011], grad_fn=<SubBackward0>)
# b的估计误差: tensor([0.0013], grad_fn=<RsubBackward1>)

我们发现拟合出来的参数和原来的十分接近!

简介实现

使用torch高级的API工具:

  1. *data_arrays 的作用是将 data_arrays 中的所有元素作为单独的参数传递给 data.TensorDataset 构造函数. 这里使用*是因为有两个对象: feature与labels需要制作iterator.
  2. 布尔值is_train表示是否希望数据迭代器对象在每个迭代周期内打乱数据
  3. 在使用net之前,我们需要初始化模型参数
  4. 正如我们在构造nn.Linear时指定输入和输出尺寸一样,现在我们能直接访问参数以设定它们的初始值。我们通过net[0]选择网络中的第一个图层,然后使用weight.databias.data方法访问参数。我们还可以使用替换方法normal_fill_来重写参数值。
  5. 不论输入的形状是怎样,nn.MSELoss() 最后都会返回一个标量,因为它将所有样本的损失值进行求平均(或求和),从而得到一个单一的损失值。这种设计是为了便于优化算法在训练过程中使用。
import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l
from torch import nn
# 手搓数据集
true_w = torch.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)

def load_array(data_arrays, batch_size, is_train=True): 
    """构造一个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)
net = nn.Sequential(nn.Linear(2, 1))
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
loss = nn.MSELoss()
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
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}')