线性回归

2025-08-26

线性回归

线性回归指的是自变量x和因变量y是线性关系,可能有多个自变量或者一些噪声影响y的结果。

1.1 线性模型

线性假设是指目标可以表示为特征的加权和,如下面的式子: \(\widehat{y} = w_1x_1 + ...+w_dx_d +b.\) 我们可以用点积形式来简洁地表达模型: \(\widehat{y} = w^Tx + b\) 可以通过矩阵-向量乘法表示为: \(\widehat{y} = Xw + b\) 在开始寻找最好的模型参数(model parameters)和之前, 我们还需要两个东西: (1)一种模型质量的度量方式; (2)一种能够更新模型以提高模型预测质量的方法。

1.2 损失函数

损失函数(loss function)能够量化目标的实际值与预测值之间的差距。 通常我们会选择非负数作为损失,且数值越小表示损失越小,完美预测时的损失为0。 回归问题中最常用的损失函数是平方误差函数。 当样本的预测值$\widehat{y}^{(i)}$,其相应的真实标签为时$\widehat{y}$, 平方误差可以定义为以下公式: \(l^{(i)}(w, b) = \frac{1}{2}(\widehat{y}^{(i)} - \widehat{y})^2\) 常数$\frac{1}{2}$不会带来本质的差别,但这样在形式上稍微简单一些 (因为当我们对损失函数求导后常数系数为1)。

由于平方误差函数中的二次方项, 估计值和观测值之间较大的差异将导致更大的损失。 为了度量模型在整个数据集上的质量,我们需计算在训练集$n$个样本上的损失均值(也等价于求和)。 \(L(w, b) = \frac{1}{n}\sum_{i =1}^n\frac{1}{2}(w^Tx^{(i)} + b - y{(i)})^2\) 在训练模型时,我们希望寻找一组参数$(w^, b^)$, 这组参数能最小化在所有训练样本上的总损失。如下式: \(w^*, b^* = argmin L(w, b)\)

1.3 解析解

线性回归的解可以用一个公式简单地表达出来, 这类解叫作解析解(analytical solution)。 首先,我们将偏置$b$合并到参数$w$中,合并方法是在包含所有参数的矩阵中附加一列。 我们的预测问题是最小化$||y - Xw||^2$。 这在损失平面上只有一个临界点,这个临界点对应于整个区域的损失极小点。 将损失关于$w$的导数设为0,得到解析解: \(w^* = (X^TX)^{-1}X^Ty\) 像线性回归这样的简单问题存在解析解,但并不是所有的问题都存在解析解。 解析解可以进行很好的数学分析,但解析解对问题的限制很严格,导致它无法广泛应用在深度学习里。

1.4 随机梯度下降

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

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

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

在每次迭代中,我们首先随机抽样一个小批量$\beta$, 它是由固定数量的训练样本组成的。 然后,我们计算小批量的平均损失关于模型参数的导数(也可以称为梯度)。 最后,我们将梯度乘以一个预先确定的正数$\eta$,并从当前参数的值中减掉。我们用下面的数学公式来表示这一更新过程($\delta$表示偏导数): \((w,b) \leftarrow (w, b) -\frac{\eta}{|\beta|}\sum_{i\in\beta}\delta_{(w,b)}l^{(i)}(w, b)\) 总结一下,算法的步骤如下: (1)初始化模型参数的值,如随机初始化; (2)从数据集中随机抽取小批量样本且在负梯度的方向上更新参数,并不断迭代这一步骤。 对于平方损失和仿射变换,我们可以明确地写成如下形式: \(w \leftarrow w -\frac{\eta}{|\beta|}\sum_{i\in\beta}\delta_{w}l^{(i)}(w, b) = w - \frac{\eta}{|\beta|}\sum_{i\in\beta}x^{(i)}(w^Tx^{(i)} + b - y{(i)})\)

\[b \leftarrow b -\frac{\eta}{|\beta|}\sum_{i\in\beta}\delta_{b}l^{(i)}(w, b) = b - \frac{\eta}{|\beta|}\sum_{i\in\beta}x^{(i)}(w^Tx^{(i)} + b - y{(i)})\]
公式中的$w$和$x$都是向量。 在这里,更优雅的向量表示法比系数表示法(如$w_1,w_2…w_d$)更具可读性。 $ \beta $表示每个小批量中的样本数,这也称为批量大小(batch size)。$ \eta$表示学习率(learning rate)。 批量大小和学习率的值通常是手动预先指定,而不是通过模型训练得到的。 这些可以调整但不在训练过程中更新的参数称为超参数(hyperparameter)。 调参(hyperparameter tuning)是选择超参数的过程。 超参数通常是我们根据训练迭代结果来调整的, 而训练迭代结果是在独立的验证数据集(validation dataset)上评估得到的。

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

1.5 矢量化加速

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

%matplotlib inline
import math
import time
import numpy as np
import torch
from d2l import torch as d2l

n = 10000
a = torch.ones([n])
b = torch.ones([n])

c = a + b  # 使用+ 来代替for 循环

1.6 正态分布与平方损失

接下来,我们通过对噪声分布的假设来解读平方损失目标函数。正态分布和线性回归之间的关系很密切。 正态分布(normal distribution),也称为高斯分布(Gaussian distribution),简单的说,若随机变量$x$具有均值$\mu$和方差$\sigma^2$(标准差),其正态分布概率密度函数如下: \(p(x) = \frac{1}{\sqrt{2\pi\sigma^2}}exp(-\frac{1}{2\sigma^2}(x-\mu)^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$轴的偏移,增加方差将会分散分布、降低其峰值。

均方误差损失函数(简称均方损失)可以用于线性回归的一个原因是: 我们假设了观测中包含噪声,其中噪声服从正态分布。 噪声正态分布如下式: \(y = w^Tx + b + \epsilon\) 其中,$\epsilon \sim \aleph(0, \sigma^2)$

现在,根据极大似然估计法,参数和的最优值是使整个数据集的似然最大的值: \(P(y|X) = \prod_{i=1}^np(y^{(i)}|x^{(i)})\) 根据极大似然估计法选择的估计量称为极大似然估计量。 虽然使许多指数函数的乘积最大化看起来很困难, 但是我们可以在不改变目标的前提下,通过最大化似然对数来简化。 由于历史原因,优化通常是说最小化而不是最大化。 我们可以改为最小化负对数似然$-logP(y|x)$。 由此可以得到的数学公式是: \(-logP(y|x) = \sum_{i=1}^n\frac{1}{2}log(2\pi\sigma^2) + \frac{1}{2\pi\sigma^2}(y^{(i)} - w^Tx^{(i)} - b)^2\) 现在我们只需要假设$\sigma$是某个固定常数就可以忽略第一项, 因为第一项不依赖于w和b。 现在第二项除了$\frac{1}{2\sigma^2}$常数外,其余部分和前面介绍的均方误差是一样的。 幸运的是,上面式子的解并不依赖于$\sigma$。 因此,在高斯噪声的假设下,最小化均方误差等价于对线性模型的极大似然估计。

1.7 线性回归的python实现

%matplotlib inline
import random
import torch
from d2l import torch as d2l

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)

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]


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

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

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

def sgd(params, lr, batch_size):  #@save
    """小批量随机梯度下降"""
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()

            
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}')

1.8 线性回归的简洁实现

import numpy as np
import torch
from torch.utils import data
from d2l import torch as d2l

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):  #@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)

# 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)

loss = nn.MSELoss()  # 平方l2范数
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}')