第三章 线性回归

  • 解决线性回归问题的两种方法:使用公式(显式解)和使用梯度下降
  • 评估线性回归模型的指标:均方根误差(MSE_LOSS)
  • 使用Pytorch实现梯度下降法的线性回归模型

1、梯度下降

  • 梯度下降是一种非常通用的优化算法,能够为大范围的问题找到最优解。梯度下降的整体思路是通过多次迭代,每次迭代都试图对损失函数进行优化,从而找到最小损失函数值对应的参数值。
  • 梯度下降的一般流程:
    1. 随机初始化模型参数,通常是一个接近0的随机数,即初始化权重 w_0 和偏差 b_0
    2. 重复下面步骤直到停止条件达成
      • 计算损失函数关于模型参数的梯度,即计算 w_t=w_{t-1}-\eta \frac{\partial l}{\partial w_{t-1}}
      • 使用学习率乘以梯度更新模型参数,沿着梯度下降的方向增加损失函数值
  • 超参数定义:人为定义的参数,不会通过训练得到,需要手动设置,如学习率、迭代次数、批量大小等
  • 本章使用的超参数:学习率、迭代次数、批量大小
  • 梯度下降的学习率:学习率过大,会导致损失函数值不断增大,甚至发散;学习率过小,会导致损失函数值收敛缓慢
  • 梯度下降的批量大小:批量大小过大,会导致内存溢出;批量大小过小,会导致损失函数值收敛缓慢
  • 梯度下降的迭代次数:迭代次数过多,会导致过拟合;迭代次数过少,会导致欠拟合

2、小批量随机梯度下降

  • 梯度下降的一个重要变体是小批量随机梯度下降,它每次迭代都随机采样一个由固定数量训练样本所组成的小批量,然后求小批量中数据样本的平均损失有关模型参数的导数,最后使用此结果来更新模型参数。
  • 随机采样 b 个样本 i_1,i_2,...,i_b 计算损失:l(w,b)=\frac{1}{b}\sum_{j=1}^b l(w,i_j)
  • b 是小批量样本的大小,通常取2的幂次方,如32、64、128等,即为批量大小(batch_size)

3、线性回归的从零开始实现

1、导入所需的包或模块

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

2、根据带有噪声的线性模型生成数据集,使用线性模型真实权重 w=[2,-3.4]^T 和偏差 b=4.2,噪声项 \epsilon 服从均值为0、标准差为0.01的正态分布生成数据集及其标签:y=Xw+b+\epsilon

def synthetic_data(w, b, num_examples): 
    X = torch.normal(0, 1, (num_examples, len(w)))           # 均值为0,标准差为1,形状为num_examples*len(w)
    y = torch.matmul(X, w) + b                               # Xw+b
    y += torch.normal(0, 0.01, y.shape)                      # 均值为0,标准差为0.01,形状为y.shape,增加噪声
    return X, y.reshape((-1, 1))                             # 把X和y做成列向量返回

true_w = torch.tensor([2, -3.4])                             # w的真实值
true_b = 4.2                                                 # b的真实值
features, labels = synthetic_data(true_w, true_b, 1000)

features中的每一行对应一个数据样本,features中的每一列对应一个特征;labels亦是如此

print('features:', features[0],'\nlabel:', labels[0])
features: tensor([0.7274, 0.2557]) 
label: tensor([4.7834])
d2l.set_figsize()
d2l.plt.scatter(features[:, (1)].detach().numpy(), labels.detach().numpy(), 1);  # 画出features[:, (1)]和labels的散点图

svg

3、定义data_iter函数,该函数接收批量大小、特征矩阵和标签向量作为输入,生成大小为batch_size的小批量样本,每个小批量包含一组特征和标签

  • yield关键字:在for循环中,每次循环都会执行yield语句,将yield语句的参数作为返回值返回,同时暂停执行,直到下一次调用或迭代终止
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_size个样本
        batch_indices = torch.tensor(indices[i:min(i + batch_size, num_examples)])  # 取出batch_size个样本的索引
        yield features[batch_indices], labels[batch_indices]                          # 返回batch_size个样本

batch_size = 10
# 生成并且回显小批量样本的特征和标签
for X, y in data_iter(batch_size, features, labels):
    print(X, '\n', y)
    break
tensor([[-1.4941, -0.7959],
        [-0.8461, -1.6286],
        [-0.1836, -0.1776],
        [-0.3241,  0.1180],
        [ 1.8991,  0.7575],
        [-0.3994,  0.1566],
        [-0.1014, -0.7125],
        [ 0.4910, -0.7328],
        [-0.2212,  0.2266],
        [ 0.3280,  2.2837]]) 
 tensor([[ 3.9066],
        [ 8.0464],
        [ 4.4511],
        [ 3.1502],
        [ 5.4203],
        [ 2.8712],
        [ 6.4279],
        [ 7.6542],
        [ 2.9926],
        [-2.9197]])

4、线性模型定义

  • 初始化模型参数,将权重初始化为均值为0、标准差为0.01的正态分布随机数,偏差初始化为0
w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True)   # w的初始值
b = torch.zeros(1, requires_grad=True)                       # b的初始值
  • 定义模型,输入为特征矩阵X,输出为预测值y_hat
def linreg(X, w, b):                              
    return torch.matmul(X, w) + b                            # y_hat = Xw+b
  • 定义损失函数:均方损失
def squared_loss(y_hat, y):                   
    return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2            # (y_hat-y)^2
  • 定义优化算法,使用小批量随机梯度下降算法
def sgd(params, lr, batch_size):                          
    with torch.no_grad():                                   # 不计算梯度
        for param in params:                                # 遍历参数
            param -= lr * param.grad / batch_size           # 更新参数
            param.grad.zero_()                              # 梯度清零

5、模型训练:下方两个超参数学习率lr和迭代数num_epochs可调

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):   # 遍历数据集
        # loss得到的是一个batch_size个样本的损失,sum得到的是样本的损失和,以此计算关于w和b的梯度
        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.033516
epoch 2, loss 0.000116
epoch 3, loss 0.000051

6、比较真实的参数和通过训练学到的参数,以此评估训练得到的模型的效果

print(f'w的估计误差: {true_w - w.reshape(true_w.shape)}')
print(f'b的估计误差: {true_b - b}')
w的估计误差: tensor([ 0.0007, -0.0006], grad_fn=<SubBackward0>)
b的估计误差: tensor([-0.0002], grad_fn=<RsubBackward1>)

4、线性回归的简洁实现

1、导入所需的包或模块,并且给出真实的权重和偏差,生成带有噪声的数据集

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

true_w = torch.tensor([2, -3.4])                             # w的真实值
true_b = 4.2                                                 # b的真实值
features, labels = synthetic_data(true_w, true_b, 1000)      # 生成数据集

2、利用框架中的API函数来读取数据集

def load_array(data_arrays, batch_size, is_train=True):      # 读取数据集
    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)       # 读取数据集
next(iter(data_iter))                                        # 读取第一个小批量样本
[tensor([[ 1.1189, -0.2509],
         [ 1.5438, -0.2172],
         [-1.3757, -1.0243],
         [-1.2592,  1.7337],
         [-1.8912, -1.2692],
         [-0.3273, -0.4090],
         [-0.7802,  0.4697],
         [ 0.9599,  1.4042],
         [-0.1536, -0.8057],
         [ 0.9245,  0.4708]]),
 tensor([[ 7.2854],
         [ 8.0311],
         [ 4.9229],
         [-4.2319],
         [ 4.7270],
         [ 4.9278],
         [ 1.0459],
         [ 1.3580],
         [ 6.6371],
         [ 4.4355]])]

3、定义模型

  • 使用框架中的nn.Linear类定义线性回归模型,该类已包含参数和偏差
# nn是神经网络(Nueral Network)的缩写
from torch import nn

net = nn.Sequential(nn.Linear(2, 1))                         # 线性回归模型
  • 初始化模型参数,将权重初始化为均值为0、标准差为0.01的正态分布随机数,偏差初始化为0
net[0].weight.data.normal_(0, 0.01)                           # 初始化权重
net[0].bias.data.fill_(0)                                    # 初始化偏差
tensor([0.])
  • 计算均方损失,使用框架中的nn.MSELoss
loss = nn.MSELoss()                                          # 损失函数
  • 实例化优化器,使用框架中的optim.SGD
trainer = torch.optim.SGD(net.parameters(), lr=0.03)         # 优化器,net.parameters()返回需要更新的参数w和b

4、训练模型

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}')                  # 打印训练损失
epoch 1, loss 0.000234
epoch 2, loss 0.000106
epoch 3, loss 0.000106

5、小结

  • 线性回归模型是一个较小的神经网络,但是麻雀虽小,五脏俱全,它是一个完整的神经网络
  • 线性回归模型包括了数据的读取、模型的定义、模型参数的初始化、损失函数、模型的训练和模型的预测
  • 线性回归以及其他模型都是比较模板化的,线性回归模型是其中基本且简单的模型
  • 比较4、5可以得出,使用框架的实现比从零开始的实现更加简洁,而且运行效率更高