1-线性回归

1-线性回归

线性回归概述

回归是能为一个或多个自变量与因变量之间关系建模的一类方法。经常用来表示输入和输出之间的关系。

在机器学习领域中的大多数人物都涉及到预测,当想预测一个数值时,就会涉及到回归问题。

基本元素

线性回归基于几个简单的假设。

  • 自变量x和因变量y之间的关系是线性的,即y可以表示为x元素的加权和,通常允许包含观测值中一些噪声;
  • 假设任何噪声都比较正常,如噪声遵循正态分布;

举一个具体的例子:根据房屋面积和房龄来估算房屋价格,需要有一个真实的数据集,数据集中包含了房屋销售价格、面积和房龄。在机器学习术语中,每行数据称为样本(sample),也可以称为数据点(data point)或数据样本(data instance)。把预测的目标称为标签(label)或目标(target)。预测所依据的自变量称为特征(feature)或协变量(convariate)。

通常用n表示样本数量,对索引为ii的样本,输入表示为xi=[x1(i),x2(i)]Tx^{i} = [x_1^{(i)}, x_2^{(i)}]^T,表示包含两个自变量或特征,对应的标签为y(i)y^{(i)}

线性模型

线性假设是指目标可以表示为特征的加权和。

例如:

price=wareaarea+wageage+bprice = w_{area}*area + w_{age} * age +b

其中wareaw_{area}wagew_{age}称为权重(weight),决定了每个特征对预测值有多大的影响,bb称为偏置(bias)。偏执是指当所有特征都取值为0时,预测值应该为多少。

在机器学习领域,通常使用的是高维数据集,建模时使用线性代数表示会更方便。当输入包含d个特征时,预测结果y^\widehat{y}表示为:

y^=w1x1++wdxd+b\widehat{y} = w_1x_1 + \cdots + w_dx_d + b

将所有特征放到向量x\boldsymbol{x}中,所有权重放到向量w\boldsymbol{w}中,那么可以用点积来简洁表达模型:

y^=wTx+b\widehat{y} = \boldsymbol{w^Tx} +b

线性回归的目标是找到一组权重向量和偏置,当给定从X的同分布中取样的新样本特征时,这组权重向量和偏置能够使得新样本预测标签的误差尽可能小。

在开始寻找模型参数之前,还需要:

  1. 评价模型质量的方法,也就是如何评价这组参数是好是坏;
  2. 一种能够更新模型以提高模型预测质量的方法;

损失函数

损失函数能够量化目标的实际值与预测值之间的差距,通常会选择非负数作为损失,且数值越小表示损失越小,完美预测时损失为0。回归问题中最常用的损失函数是平方误差函数,当样本i的预测值为y^(i)\widehat{y}^{ (i) },对应真实标签为y(i)y^{(i)}时,平方误差可以定义为:

l(i)(w,b)=12(y^(i)y(i))2l^{(i)}(w,b) = \frac{1}{2} (\widehat{y}^{(i) } - y^{(i)} )^2

常数12\frac{1}{2}不会带来本质差别,只是方便求导后约去,形式简单一些。

这是单个样本的损失计算。

为了度量模型在整个数据集上的质量,需要计算在n个样本上的损失均值,等价于求和。

L(w,b)=1ni=1nl(i)(w,b)=1ni=1n12(wTx(i)+by(i))2L(w, b) = \frac{1}{n}\sum_{i=1}^n l^{ (i) } (w, b) = \frac{1}{n} \sum^{n}_{i=1} \frac{1}{2} (\boldsymbol{w^Tx^{ (i) } } + b - y^{(i)} )^2

在训练模型时,希望寻找一组参数(w,b)(\boldsymbol{w^\ast }, b^\ast),这组参数能最小化在所有训练样本上的总损失,即:

w,b=argminw,bL(w,b)\boldsymbol{w^*}, b^* =\underset{\boldsymbol{w} , b } { \arg\min } L( \boldsymbol{w}, b )

随机梯度下降

梯度下降(Gradient Descent)通过不断在损失函数递减的方向上更新参数来降低误差。

梯度下降最简单的用法是计算损失函数关于模型参数的导数(也可以称为梯度),但在实际的执行中速度会非常慢,因为在每次更新参数前,都必须遍历数据集。因此,在实际中,会在每次更新的时候,随机抽取一小批样本,这种变体称为小批量随机梯度下降。

在每次迭代中,首先随机抽样一个小批量B\mathcal{B},它是由固定数量的训练样本组成的,然后计算小批量的平均损失关于模型参数的导数,最后将导数值乘上学习率η\eta,并从当前参数中减掉。

用数学公式表示(\partial表示偏导数):

(w,b)(w,b)ηBiB(w,b)l(i)(w,b)(\boldsymbol{w}, b) \leftarrow (\boldsymbol{w}, b) - \frac{\eta}{| \mathcal{B} |} \sum_{i \in \mathcal{B} } \partial_{ (\boldsymbol{w}, b) } l^{ (i) } (\boldsymbol{w}, b)

总结一下,算法步骤为:

  1. 初始化模型参数值,如随机初始化
  2. 从数据集中随机抽取小批量样本并在负梯度方向上更新参数,并不断迭代这一步骤

批量大小batch-size和学习率η\eta称为超参数,不在训练过程中更新,而是手动预先指定,根据训练迭代结果来调整。

线性回归从零实现

首先导入所需要的包,其中将matplotlib设置为嵌入显示。

1
2
3
4
5
%matplotlib inline
import torch
from matplotlib import pyplot as plt
import numpy as np
import random

生成数据集

构造一个简单的人工训练数据集,可以使我们直观比较学到的参数和真实的模型参数的区别。

设训练数据集样本数为1000,特征数为2。给定随机生成的批量样本特征X\bf X,使用线性回归模型真实权重w=[2,3.4]T\boldsymbol{w} = [2, -3.4]^T和偏差b=4.2b = 4.2,以及一个随机噪声项来生成标签。

y=Xw+b+ϵ\boldsymbol{y} = \mathbf{X}\boldsymbol{w} +b + \epsilon

其中噪声项ϵ\epsilon服从均值为0,标准差为0.01的正态分布,代表了数据集中无意义的干扰。

生成数据集代码为:

1
2
3
4
5
6
7
8
9
10
11
num_inputs = 2
num_examples = 1000
true_w = [2, -3,4]
true_b = 4.2

# 生成数据样本,维度(1000, 2)
features = torch.randn(num_examples, num_inputs, dtype=torch.float32)
# 使用真实w和b构造标签
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
# 添加噪声项
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float32)

其中,features每一行是一个长度为2的向量,labels的每一行是一个长度为1的向量(标量)。

通过第二个特征features[:, 1]和标签labels的散点图,可以更直观观察两者间线性关系。

1
2
3
4
5
6
7
8
9
10
11
import matplotlib_inline.backend_inline
def use_svg_display():
matplotlib_inline.backend_inline.set_matplotlib_formats('svg')

def set_figsize(figsize=(9, 6)):
use_svg_display()
plt.rcParams['figure.figsize'] = figsize

set_figsize()
# 散点图,前两个参数是x和y,数组类型(n,);s->散点的面积大小,默认20; aplha-> 散点透明度,0完全透明,1不透明
plt.scatter(features[:, 1].numpy(), labels.numpy(), 10, alpha=0.9)

生成图像为:

散点图

读取数据

在训练模型时,需要遍历数据集并不断读取小批量数据样本,因此定义一个函数:每次返回batch-size个随机样本的特征和标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def data_iter(batch_size, features, labels): 
# 获得样本总数
num_examples = len(features)
# 索引列表
indices = list(range(num_examples))
# 将索引打乱,是inplace的,也就是在原列表上打乱
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
#最后一次可能不够一个batch,因此进行最小值比较
j = torch.LongTensor(indices[i: min(i + batch_size, num_examples) ])
# 使用yield返回迭代器,会保存函数执行进度,
# 下次再调用会定位到上次yield位置开始执行
# 使用index_select方法,0表示按行索引,j是要选取的索引列表
yield features.index_select(0, j), labels.index_select(0, j)

可以测试一下, 设定batch_size = 10,读取第一个小批量数据样本。

1
2
3
4
batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, y)
break

初始化模型参数

将权重初始化为均值为0,标准差为0.01的正态随机数,偏差初始化为0。

1
2
3
w = torch.tensor(np.random.normal(0, 0.01, (num_inputs, 1)), dtype=torch.float32)
b = torch.zeros(1, dtype=torch.float32)

之后的模型训练中,需要对这些参数求梯度来迭代参数的值,因此要让参数的requires_grad=True

1
2
3
w.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)

定义模型

需要将输入特征X\mathbf X和权重矩阵w\bf w向量乘法后加上偏置bb,偏置bb是一个标量,python有广播机制,当用向量加标量时,标量会被加到向量的每个分量上。

使用torch.mm函数进行矩阵相乘。

torch.mm函数用于矩阵乘法,但只适用于二维矩阵相乘,如果传入高维矩阵,则会报错。

如果x维度(n,m),y维度(m,p),则torch.mm(x,y)返回一个(n,p)维矩阵。

1
2
def linreg(X, w, b): 
return torch.mm(X, w) + b

定义损失函数

使用上面描述的平方损失来定义线性回归的损失函数。

1
2
3
4
# y_hat是预测值,y是真实值
# 使用view函数确保y和y_hat维度相同,注意在pytorch的MSELoss中没有除2
def squared_loss(y_hat, y):
return (y_hat - y.view(y_hat.size()))**2 / 2

定义优化函数

实现上面介绍的小批量随机梯度下降算法,这里的自动求梯度模块计算得到的梯度是一个批量样本的梯度和,除以批量大小来得到平均值。

1
2
3
4
def sgd(params, lr, batch_size): 
for param in params:
# 使用data属性来改变参数值
param.data -= lr * param.grad / batch_size

训练模型

在训练中,将不断迭代模型参数。在每次迭代中,根据当前读取的小批量数据样本,通过调用反向传播函数backward计算小批量随机梯度,并调用优化算法sgd迭代模型参数。

由于之前设批量大小batch_size为10,每个小批量损失l的维度为(10, 1)。由于变量l不是一个标量,因此可以调用sum()函数求和得到一个标量,再运行backward()得到该变量有关模型参数的梯度。

注意更新完参数后,需要将参数梯度清零。

在一个迭代周期(epoch)中,将完整执行一遍data_iter函数。这里迭代周期num_epochs和学习率lr都是超参数,分别设为3和0.03。在实践中,大多数参数需要反复尝试来调节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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).sum()
# 反向传播,计算每个参数的梯度
l.backward()
# 优化,更新参数
sgd([w,b], lr, batch_size)
# 梯度清零
w.grad.data.zero_()
b.grad.data.zero_()

# 每次迭代完成后,计算每个样本的损失值
train_l = loss(net(features, w, b), labels)
# 打印迭代次数和平均损失值
print('epoch %d, loss %f' % (epoch + 1, train_l.mean().item()))

输出为:

1
2
3
epoch 1, loss 0.032384
epoch 2, loss 0.000117
epoch 3, loss 0.000051

可以查看训练完成后,设定参数和训练参数是否接近。

1
2
3
4
5
6
7
print(true_w, '\n', w)
print(true_b, '\n', b)
# 输出
# [2, -3.4]
# tensor([[ 1.9996],[-3.3998]], requires_grad=True)
# 4.2
# tensor([4.1993], requires_grad=True)

可以看到,训练后的模型参数十分接近真实的参数。

线性回归简洁实现

在这节中,使用PyTorch框架更简洁地实现线性回归。

生成数据集

与上一节中的数据集相同,features是训练数据特征,labels是标签。

1
2
3
4
5
6
7
8
9
num_inputs = 2
num_examples = 1000

true_w = [2, -3.4]
true_b = 4.2

features = torch.tensor(np.random.normal(0, 1, (num_examples, num_inputs)), dtype=torch.float)
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float)

读取数据

PyTorch提供了data包来读取数据,由于data常用作变量名,因此将导入的dataData代替,和上节一样,将随机读取包含10个数据样本的小批量。

1
2
3
4
5
6
7
import torch.utils.data as Data

batch_size = 10
# 将训练数据的特征和标签组合
dataset = Data.TensorDataset(features, labels)
# 随机读取小批量
data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)

这里的data_iter和上一节用法一样,DataLoader包含数据集和取样器,用来分小批量地迭代数据样本。

定义模型

PyTorch提供了大量预定义的层。

首先,导入torch.nn模块,nnneural network的缩写,该模块定义了大量神经网络的层。nn的核心数据结构是Module,它是一个抽象概念,既可以表示神经网络中的某个层,也可以表示一个包含很多层的神经网络。

在实际使用中,最常见的做法是继承nn.Module,撰写自己的网络层,一个nn.Module实例应该包含一些层以及返回输出的前向传播(forward)方法。

Linear类中需要两个参数,第一个指定输入特征数量,即2,第二个指定输出特征数量,一个标量,为1。

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch.nn as nn
class LinearNet(nn.Module):
def __init__(self, n_feature):
super(LinearNet, self).__init__()
# 线性回归层
self.linear = nn.Linear(n_feature, 1)

def forward(self, x):
y = self.linear(x)
return y

net = LinearNet(num_inputs)
print(net)

print(net)可以输出网络结构。

还可以使用nn.Sequential来更方便的搭建网络,Sequential是一个有序的容器,网络层将按照传入Sequential的顺序依次添加到计算图中。

1
2
3
4
5
6
7
8
9
10
11
12
# 写法一
net = nn.Sequential(
nn.Linear(num_inputs, 1)
)
# 写法二, linear是该层的名字
net = nn.Sequential()
net.add_module('linear', nn.Linear(num_inputs, 1))
# 写法三
from collections import OrderedDict
net = nn.Sequential(OrderedDict([
('linear', nn.Linear(num_inputs, 1))
]))

OrderedDict是一个有序的字典,可以自定义层名字和结构。

可以通过net.parameters()来查看模型所有的可学习参数,该函数会返回一个生成器。

1
2
3
4
5
6
7
8
for param in net.parameters(): 
print(param)

# 输出
# Parameter containing:
# tensor([[ 0.1954, -0.2418]], requires_grad=True)
# Parameter containing:
# tensor([-0.5784], requires_grad=True)

注意:torch.nn仅支持输入一个batch样本,不支持单个样本输入,如果只有单个样本,可以使用input.unsqueeze(0)来添加一维。

在使用net之前,需要初始化模型参数。PyTorch在init模块中提供了多种参数初始化方法,这里使用init.normal_将权重参数每个元素初始化为随机采样于均值为0, 标准差为0.01的正态分布,偏差初始化为0。

1
2
3
from torch.nn import init
init.normal_(net.linear.weight, mean=0, std=0.01)
init.constant_(net.linear.bias, val=0)

这里也可以直接修改data属性,

net.linear.weight.data.normal_(0, 0.01); net.linear.bias.data.fill_(0)

如果是使用Sequential定义模型的,则使用net[0].weightnet[0].bias获取模型参数。

定义损失函数

PyTorch在nn模块中提供了各种损失函数,可以看做一种特殊的层,PyTorch也将这些损失函数实现为nn.Module的子类。现在使用PyTorch提供的均方误差损失作为模型的损失函数。

1
loss = nn.MSELoss()

计算均方误差使用的是MSELoss类,也称为平方L2范数,默认情况下,返回所有样本损失的平均值。

定义优化算法

PyTorch的torch.optim模块提供了很多常用的优化算法如SGDAdamRMSProp等。下面创建一个用于优化net参数的优化器实例,并指定学习率为0.03的小批量随机梯度下降(SGD)为优化算法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import torch.optim as optim

optimizer = optim.SGD(net.parameters(), lr=0.03)
print(optimizer)

# 输出
# SGD (
# Parameter Group 0
# dampening: 0
# lr: 0.03
# momentum: 0
# nesterov: False
# weight_decay: 0
# )

还可以为不同子网络设置不同的学习率,在fine-tune时经常用到。

1
2
3
4
5
optimizer = optim.SGD([
# 如果对某个参数不指定学习率,就使用最外层默认学习率
{'params': net.subnet1.parameters()}, # 使用默认学习率0.03
{'params': net.subnet2.parameters(), 'lr': 0.01}
], lr = 0.03)

有时候不想让学习率固定为常数,调整学习率有两种做法,一种是修改optimizer.param_groups中对应的学习率,另一种是更简单也更推荐的做法:新建优化器,因为optimizer十分轻量级,构建开销很小,因此可以构建新的optimizer,但是对于使用动量的优化器(如Adam),会丢失动量等状态信息。

1
2
3
# 调整学习率
for param_group in optimizer.param_groups:
param_group['lr'] *= 0.1 # 学习率为之前的0.1倍

训练

回顾一下,在每个迭代中,将完整遍历一次数据集,不停获取一个小批量输入和相应标签,对于每一个小批量,将进行以下步骤:

  • 通过调用net(X)生成预测并计算损失ll(前向传播)
  • 进行反向传播计算参数梯度
  • 调用优化器更新模型参数

为了更好衡量训练效果,计算每个迭代周期后的损失,并打印它来监控整个训练过程。

调用optim实例的step函数迭代更新模型参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
num_epochs = 3
for epoch in range(1, num_epochs + 1):
for X,y in data_iter:
output = net(X)
# 调用view,将标签看成n*1的向量
l = loss(output, y.view(-1, 1))
# 梯度清零,相等于net.zero_grad()
optimizer.zero_grad()
l.backward()
optimizer.step()

print('epoch %d, loss: %f' % (epoch, l.item()))

# 输出
# epoch 1, loss: 0.000280
# epoch 2, loss: 0.000090
# epoch 3, loss: 0.000105

将实际参数和训练参数进行比较。

1
2
3
4
5
6
7
8
9
linear = net.linear
print(true_w, linear.weight)
print(true_b, linear.bias)

# 输出
# [2, -3.4] Parameter containing:
# tensor([[ 2.0006, -3.3992]], requires_grad=True)
# 4.2 Parameter containing:
# tensor([4.2004], requires_grad=True)

1-线性回归
https://zhaoquaner.github.io/2022/11/21/DeepLearning/practice/1-线性回归/
更新于
2022年11月23日
许可协议