1-线性回归
线性回归概述
回归是能为一个或多个自变量与因变量之间关系建模的一类方法。经常用来表示输入和输出之间的关系。
在机器学习领域中的大多数人物都涉及到预测,当想预测一个数值时,就会涉及到回归问题。
基本元素
线性回归基于几个简单的假设。
- 自变量x和因变量y之间的关系是线性的,即y可以表示为x元素的加权和,通常允许包含观测值中一些噪声;
- 假设任何噪声都比较正常,如噪声遵循正态分布;
举一个具体的例子:根据房屋面积和房龄来估算房屋价格,需要有一个真实的数据集,数据集中包含了房屋销售价格、面积和房龄。在机器学习术语中,每行数据称为样本(sample),也可以称为数据点(data point)或数据样本(data instance)。把预测的目标称为标签(label)或目标(target)。预测所依据的自变量称为特征(feature)或协变量(convariate)。
通常用n表示样本数量,对索引为i的样本,输入表示为xi=[x1(i),x2(i)]T,表示包含两个自变量或特征,对应的标签为y(i)。
线性模型
线性假设是指目标可以表示为特征的加权和。
例如:
price=warea∗area+wage∗age+b
其中warea和wage称为权重(weight),决定了每个特征对预测值有多大的影响,b称为偏置(bias)。偏执是指当所有特征都取值为0时,预测值应该为多少。
在机器学习领域,通常使用的是高维数据集,建模时使用线性代数表示会更方便。当输入包含d个特征时,预测结果y表示为:
y=w1x1+⋯+wdxd+b
将所有特征放到向量x中,所有权重放到向量w中,那么可以用点积来简洁表达模型:
y=wTx+b
线性回归的目标是找到一组权重向量和偏置,当给定从X的同分布中取样的新样本特征时,这组权重向量和偏置能够使得新样本预测标签的误差尽可能小。
在开始寻找模型参数之前,还需要:
- 评价模型质量的方法,也就是如何评价这组参数是好是坏;
- 一种能够更新模型以提高模型预测质量的方法;
损失函数
损失函数能够量化目标的实际值与预测值之间的差距,通常会选择非负数作为损失,且数值越小表示损失越小,完美预测时损失为0。回归问题中最常用的损失函数是平方误差函数,当样本i的预测值为y(i),对应真实标签为y(i)时,平方误差可以定义为:
l(i)(w,b)=21(y(i)−y(i))2
常数21不会带来本质差别,只是方便求导后约去,形式简单一些。
这是单个样本的损失计算。
为了度量模型在整个数据集上的质量,需要计算在n个样本上的损失均值,等价于求和。
L(w,b)=n1i=1∑nl(i)(w,b)=n1i=1∑n21(wTx(i)+b−y(i))2
在训练模型时,希望寻找一组参数(w∗,b∗),这组参数能最小化在所有训练样本上的总损失,即:
w∗,b∗=w,bargminL(w,b)
随机梯度下降
梯度下降(Gradient Descent)通过不断在损失函数递减的方向上更新参数来降低误差。
梯度下降最简单的用法是计算损失函数关于模型参数的导数(也可以称为梯度),但在实际的执行中速度会非常慢,因为在每次更新参数前,都必须遍历数据集。因此,在实际中,会在每次更新的时候,随机抽取一小批样本,这种变体称为小批量随机梯度下降。
在每次迭代中,首先随机抽样一个小批量B,它是由固定数量的训练样本组成的,然后计算小批量的平均损失关于模型参数的导数,最后将导数值乘上学习率η,并从当前参数中减掉。
用数学公式表示(∂表示偏导数):
(w,b)←(w,b)−∣B∣ηi∈B∑∂(w,b)l(i)(w,b)
总结一下,算法步骤为:
- 初始化模型参数值,如随机初始化
- 从数据集中随机抽取小批量样本并在负梯度方向上更新参数,并不断迭代这一步骤
批量大小batch-size和学习率η称为超参数,不在训练过程中更新,而是手动预先指定,根据训练迭代结果来调整。
线性回归从零实现
首先导入所需要的包,其中将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,使用线性回归模型真实权重w=[2,−3.4]T和偏差b=4.2,以及一个随机噪声项来生成标签。
y=Xw+b+ϵ
其中噪声项ϵ服从均值为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
features = torch.randn(num_examples, num_inputs, dtype=torch.float32)
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()
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)) random.shuffle(indices) for i in range(0, num_examples, batch_size): j = torch.LongTensor(indices[i: min(i + batch_size, num_examples) ]) 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和权重矩阵w向量乘法后加上偏置b,偏置b是一个标量,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
|
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: 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)
|
可以看到,训练后的模型参数十分接近真实的参数。
线性回归简洁实现
在这节中,使用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
常用作变量名,因此将导入的data
用Data
代替,和上节一样,将随机读取包含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
模块,nn
是neural 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) )
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)
|
注意: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].weight
和net[0].bias
获取模型参数。
定义损失函数
PyTorch在nn
模块中提供了各种损失函数,可以看做一种特殊的层,PyTorch也将这些损失函数实现为nn.Module
的子类。现在使用PyTorch提供的均方误差损失作为模型的损失函数。
计算均方误差使用的是MSELoss类,也称为平方L2范数,默认情况下,返回所有样本损失的平均值。
定义优化算法
PyTorch的torch.optim
模块提供了很多常用的优化算法如SGD
、Adam
和RMSProp
等。下面创建一个用于优化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)
|
还可以为不同子网络设置不同的学习率,在fine-tune
时经常用到。
1 2 3 4 5
| optimizer = optim.SGD([ {'params': net.subnet1.parameters()}, {'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
|
训练
回顾一下,在每个迭代中,将完整遍历一次数据集,不停获取一个小批量输入和相应标签,对于每一个小批量,将进行以下步骤:
- 通过调用
net(X)
生成预测并计算损失l(前向传播) - 进行反向传播计算参数梯度
- 调用优化器更新模型参数
为了更好衡量训练效果,计算每个迭代周期后的损失,并打印它来监控整个训练过程。
调用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) l = loss(output, y.view(-1, 1)) optimizer.zero_grad() l.backward() optimizer.step()
print('epoch %d, loss: %f' % (epoch, l.item()))
|
将实际参数和训练参数进行比较。
1 2 3 4 5 6 7 8 9
| linear = net.linear print(true_w, linear.weight) print(true_b, linear.bias)
|