从头学pytorch(三) 线性回归
关于什么是线性回归,不多做介绍了.可以参考我以前的博客https://www.cnblogs.com/sdu20112013/p/10186516.html
实现线性回归
分为以下几个部分:
- 生成数据集
- 读取数据
- 初始化模型参数
- 定义模型
- 定义损失函数
- 定义优化算法
- 训练模型
生成数据集
我们构造一个简单的人工训练数据集,它可以使我们能够直观比较学到的参数和真实的模型参数的区别。设训练数据集样本数为1000,输入个数(特征数)为2。给定随机生成的批量样本特征 \(\boldsymbol{X} \in \mathbb{R}^{1000 \times 2}\),我们使用线性回归模型真实权重 \(\boldsymbol{w} = [2, -3.4]^\top\) 和偏差 \(b = 4.2\),以及一个随机噪声项 \(\epsilon\) 来生成标签
其中噪声项 \(\epsilon\) 服从均值为0、标准差为0.01的正态分布。噪声代表了数据集中无意义的干扰。
%matplotlib inline
import torch
from IPython import display
from matplotlib import pyplot as plt
import numpy as np
import random
num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
features = torch.from_numpy(np.random.normal(0, 1, (num_examples, num_inputs)))
print(type(features),features.shape)
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
print(type(labels),labels.shape)
labels += torch.from_numpy(np.random.normal(0, 0.01, size=labels.size()))
def use_svg_display():
# 用矢量图显示
display.set_matplotlib_formats('svg')
def set_figsize(figsize=(3.5, 2.5)):
use_svg_display()
# 设置图的尺寸
plt.rcParams['figure.figsize'] = figsize
set_figsize()
plt.scatter(features[:, 1].numpy(), labels.numpy(), 1);
读取数据
每次读取batch_size个样本.注意乱序读取.以使得每个batch的样本多样性足够丰富.
def data_iter(batch_size, features, labels):
num_examples = len(features)
#print(num_examples)
indices = list(range(num_examples))
random.shuffle(indices) # 样本的读取顺序是随机的
#print(indices)
for i in range(0, num_examples, batch_size):
j = torch.LongTensor(indices[i: min(i + batch_size, num_examples)]) # 最后一次可能不足一个batch
#print(j)
yield features.index_select(0, j), labels.index_select(0, j)
batch_size = 10
for X, y in data_iter(batch_size, features, labels):
#print(X, y)
#break
pass
关于yiled用法参考:https://www.cnblogs.com/sdu20112013/p/11216584.html中yield部分.
关于torch的index_select用法参考:https://pytorch-cn.readthedocs.io/zh/latest/package_references/torch/#torchindex_select
features是[1000,2]的Tensor。所以features.index_select(0, j)即在第0维度上对索引为j的输入进行切片.也即选取第j(j为一个长度为batch_size的tensor)个样本.
初始化模型参数
权重值有2个.所以我们初始化一个shape为[2,1]的Tensor.我们将其随机初始化为符合均值0,标准差0.01的正态分布随机数,bias初始化为0.
w=torch.from_numpy(np.random.normal(0,0.01,(num_inputs,1)))
b = torch.zeros(1, dtype=torch.float64)
print(w.dtype,b.dtype)
ndarray的类型是float64,所以w的类型是float64,在生成b的时候我们指定dtype=float64.
之后的模型训练中,需要对这些参数求梯度来迭代参数的值,因此我们要让它们的requires_grad=True
。
w.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)
定义模型
下面是线性回归的矢量计算表达式的实现。我们使用mm
函数做矩阵乘法。
在我们的例子中,X是[1000,2]的矩阵,w是[2,1]的矩阵,相乘得到[1000,1]的矩阵.
def linreg(X, w, b): # 本函数已保存在d2lzh_pytorch包中方便以后使用
return torch.mm(X, w) + b
定义损失函数
我们使用平方损失来定义线性回归的损失函数。在实现中,我们需要把真实值y
变形成预测值y_hat
的形状。以下函数返回的结果也将和y_hat
的形状相同。
def squared_loss(y_hat, y):
# 注意这里返回的是向量, 另外, pytorch里的MSELoss并没有除以 2
return (y_hat - y.view(y_hat.size())) ** 2 / 2
定义优化算法
以下的sgd
函数实现了上一节中介绍的小批量随机梯度下降算法。它通过不断迭代模型参数来优化损失函数。这里自动求梯度模块计算得来的梯度是一个批量样本的梯度和。我们将它除以批量大小来得到平均值。均值反映了平均而言,对单个样本,朝着哪个梯度方向去更新参数可以使得loss最小
def sgd(params, lr, batch_size):
for param in params:
param.data -= lr * param.grad / batch_size # 注意这里更改param时用的param.data
这里的params传入的即w,b
训练模型
我们创建一个循环,每次传入batch_size个样本,计算损失.反向传播,计算w,b的梯度,然后更新w,b.循环往复.注意每次方向传播后清空梯度. 以及l是一个向量. 调用.sum()将其转换为标量,再计算梯度.
一个epoch即所有样本均计算一次损失.
代码如下:
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss
batch_size=10
for epoch in range(num_epochs):
for X,y in data_iter(batch_size,features,labels):
l = loss(linreg(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()))
print(true_w,'\n',w)
print(true_b,'\n',b)
输出如下:
epoch 1, loss 0.051109
epoch 2, loss 0.000217
epoch 3, loss 0.000049
[2, -3.4]
tensor([[ 1.9996],
[-3.3993]], dtype=torch.float64, requires_grad=True)
4.2
tensor([4.1995], dtype=torch.float64, requires_grad=True)
可以看到得到的w和b都已经非常接近true_w,true_b了.
之前我们是手写代码构建模型,创建损失函数,定义随机梯度下降等等.用pytorch里提供的类和函数,可以更方便地实现线性回归.
线性回归的简洁实现
生成数据集
与前面没有区别.
num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
features = torch.from_numpy(np.random.normal(0, 1, (num_examples, num_inputs)))
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
labels += torch.from_numpy(np.random.normal(0, 0.01, size=labels.size()))
数据读取
用torch.utils.data模块,主要使用TensorDataset类和DataLoader类
import torch.utils.data as Data
batch_size=10
dataset = Data.TensorDataset(features,labels)
data_iter = Data.DataLoader(dataset,batch_size=batch_size,shuffle=True)
for X,y in data_iter:
print(X,y)
break
创建网络结构
在上一节从零开始的实现中,我们需要定义模型参数,并使用它们一步步描述模型是怎样计算的。当模型结构变得更复杂时,这些步骤将变得更繁琐。其实,PyTorch提供了大量预定义的层,这使我们只需关注使用哪些层来构造模型。下面将介绍如何使用PyTorch更简洁地定义线性回归。
首先,导入torch.nn
模块。实际上,“nn”是neural networks(神经网络)的缩写。顾名思义,该模块定义了大量神经网络的层。之前我们已经用过了autograd
,而nn
就是利用autograd
来定义模型。nn
的核心数据结构是Module
,它是一个抽象概念,既可以表示神经网络中的某个层(layer),也可以表示一个包含很多层的神经网络。在实际使用中,最常见的做法是继承nn.Module
,撰写自己的网络/层。一个nn.Module
实例应该包含一些层以及返回输出的前向传播(forward)方法。下面先来看看如何用nn.Module
实现一个线性回归模型。
class LinearNet(nn.Module):
def __init__(self, n_feature):
super(LinearNet, self).__init__()
self.linear = nn.Linear(n_feature, 1)
# forward 定义前向传播
def forward(self, x):
y = self.linear(x)
return y
net = LinearNet(num_inputs)
print(net) # 使用print可以打印出网络的结构
输出:
LinearNet(
(linear): Linear(in_features=2, out_features=1, bias=True)
)
事实上我们还可以用nn.Sequential
来更加方便地搭建网络,Sequential
是一个有序的容器,网络层将按照在传入Sequential
的顺序依次被添加到计算图中。
# 写法一
net = nn.Sequential(
nn.Linear(num_inputs, 1)
# 此处还可以传入其他层
)
# 写法二
net = nn.Sequential()
net.add_module('linear', nn.Linear(num_inputs, 1))
# net.add_module ......
# 写法三
from collections import OrderedDict
net = nn.Sequential(OrderedDict([
('linear', nn.Linear(num_inputs, 1))
# ......
]))
print(net)
print(net[0])
输出:
Sequential(
(linear): Linear(in_features=2, out_features=1, bias=True)
)
Linear(in_features=2, out_features=1, bias=True)
可以通过net.parameters()
来查看模型所有的可学习参数,此函数将返回一个生成器。
for param in net.parameters():
print(param)
输出:
Parameter containing:
tensor([[-0.2956, -0.2817]], requires_grad=True)
Parameter containing:
tensor([-0.1443], requires_grad=True)
作为一个单层神经网络,线性回归输出层中的神经元和输入层中各个输入完全连接。因此,线性回归的输出层又叫全连接层。
注意:
torch.nn
仅支持输入一个batch的样本不支持单个样本输入,如果只有单个样本,可使用input.unsqueeze(0)
来添加一维。
初始化模型参数
在使用net
前,我们需要初始化模型参数,如线性回归模型中的权重和偏差。PyTorch在init
模块中提供了多种参数初始化方法。这里的init
是initializer
的缩写形式。我们通过init.normal_
将权重参数每个元素初始化为随机采样于均值为0、标准差为0.01的正态分布。偏差会初始化为零。
from torch.nn import init
init.normal_(net[0].weight, mean=0, std=0.01)
init.constant_(net[0].bias, val=0) # 也可以直接修改bias的data: net[0].bias.data.fill_(0)
定义优化算法
同样,我们也无须自己实现小批量随机梯度下降算法。torch.optim
模块提供了很多常用的优化算法比如SGD、Adam和RMSProp等。下面我们创建一个用于优化net
所有参数的优化器实例,并指定学习率为0.03的小批量随机梯度下降(SGD)为优化算法。
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
)
我们还可以为不同子网络设置不同的学习率,这在finetune时经常用到。例:
optimizer =optim.SGD([
# 如果对某个参数不指定学习率,就使用最外层的默认学习率
{'params': net.subnet1.parameters()}, # lr=0.03
{'params': net.subnet2.parameters(), 'lr': 0.01}
], lr=0.03)
有时候我们不想让学习率固定成一个常数,那如何调整学习率呢?主要有两种做法。
- 一种是修改
optimizer.param_groups
中对应的学习率
# 调整学习率
for param_group in optimizer.param_groups:
param_group['lr'] *= 0.1 # 学习率为之前的0.1倍
- 另一种是更简单也是较为推荐的做法——新建优化器,由于optimizer十分轻量级,构建开销很小,故而可以构建新的optimizer。但是后者对于使用动量的优化器(如Adam),会丢失动量等状态信息,可能会造成损失函数的收敛出现震荡等情况。
训练
所有的optimizer都实现了step()方法,这个方法会更新所有的参数。它能按两种方式来使用:
- optimizer.step()
这是大多数optimizer所支持的简化版本。一旦梯度被如backward()之类的函数计算好后,我们就可以调用这个函数。
for input, target in dataset:
optimizer.zero_grad()
output = model(input)
loss = loss_fn(output, target)
loss.backward()
optimizer.step()
- optimizer.step(closure)
一些优化算法例如Conjugate Gradient和LBFGS需要重复多次计算函数,因此你需要传入一个闭包去允许它们重新计算你的模型。这个闭包应当清空梯度, 计算损失,然后返回。
for input, target in dataset:
def closure():
optimizer.zero_grad()
output = model(input)
loss = loss_fn(output, target)
loss.backward()
return loss
optimizer.step(closure)
具体参考https://pytorch-cn.readthedocs.io/zh/latest/package_references/torch-optim/
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() # 梯度清零,等价于net.zero_grad()
l.backward()
optimizer.step()
print('epoch %d, loss: %f' % (epoch, l.item()))
dense = net[0]
print(true_w, dense.weight)
print(true_b, dense.bias)
输出:
epoch 1, loss: 0.000227
epoch 2, loss: 0.000160
epoch 3, loss: 0.000136
[2, -3.4] Parameter containing:
tensor([[ 2.0007, -3.4010]], requires_grad=True)
4.2 Parameter containing:
tensor([4.1998], requires_grad=True)
总结:
- 使用PyTorch可以更简洁地实现模型。
torch.utils.data
模块提供了有关数据处理的工具,torch.nn
模块定义了大量神经网络的层,torch.nn.init
模块定义了各种初始化方法,torch.optim
模块提供了模型参数优化的各种方法。