本文的内容整理自:
《动手学深度学习(Pytorch版)》跟李沐学AI的个人空间-跟李沐学AI个人主页-哔哩哔哩视频 (bilibili.com)
《Pytorch框架基础》01-05-mp4-autograd与逻辑回归_哔哩哔哩_bilibili
《Python深度学习-基于Pytorch》
1. 求导
在神经网络的训练中,想让 loss 下降,就需要使用 w 相对于 loss 的梯度来更新 w,因此计算梯度或者说导数就是神经网络框架的必备功能之一,在PyTorch中也提供了相应的功能。当设置torch.Tensor
对象的属性.requires_grad=True
(或者调用.requires_grad_(True)
也可以)后,PyTorch就会开始追踪在该变量上的所有操作。在完成计算后,可以调用 .backward()
函数,自动计算所有被追踪对象的梯度,得到的梯度结果保存在属性 .grad
中。
需要注意的是我们一般只能在一个标量上进行求导操作,如果对象是一个张量类型,可以在对其进行.sum()
之后再进行.backward()
import torch
# note:此处必须使用浮点型数据,否则会报错
# Only Tensors of floating point and complex dtype can require gradients
x = torch.arange(4.0, requires_grad=True)
print(x)
tensor([0., 1., 2., 3.], requires_grad=True)
通过打印x
,可以看到变量x
中的属性requires_grad
的值为True,此时执行
y = x + 2
print(y)
tensor([2., 3., 4., 5.], grad_fn=<AddBackward0>)
由于y
是一个基于x
的操作(函数),所以它带有grad_fn
属性
z = y * y * 3
out = z.mean()
print('z=', z)
print('out=', out)
z= tensor([12., 27., 48., 75.], grad_fn=<MulBackward0>)
out= tensor(40.5000, grad_fn=<MeanBackward0>)
out.backward()
# 输出梯度 d(out)/dx
print(x.grad)
tensor([3.0000, 4.5000, 6.0000, 7.5000])
之所以得到上述结果是因为
则根据链式法则可知:
默认情况下,PyTorch会累计梯度,因此如果再次计算的时候需要调用.zero_()
函数,以清除之前的值。
x.grad.zero_()
out1 = (x * x).sum()
out1.backward()
print(x.grad)
tensor([0., 2., 4., 6.])
对于上面的代码而言,如果没有使用x.grad.zero_()
,则PyTorch会将此次计算得到的[0,2,4,6]
与之前计算得到的[3,4.5,6,7.5]
进行相加得到结果[3,6.5,10,13.5]
当在Torch
对象上调用.detach()
方法时,可以将该对象进行剥离,防止未来的计算会被追踪。例如在下面的代码中,我们将u
设置为y.detach()
,此时可以将u
视为一个与x
无关的常量。
x.grad.zero_()
y = x * x
u = y.detach()
z = u * x
z.sum().backward()
print(x.grad)
tensor([0., 1., 4., 9.])
1.梯度不自动清零
2.依赖于叶子节点的节点,require_grad默认为True
3.叶子节点不可以执行in-place操作(原地操作)
2. 线性回归
一般而言,深度学习模型的训练步骤主要包括五个模块:
- 数据
- 模型
- 损失函数 得到损失函数后,需要在该函数上调用backward()来得到梯度
- 优化器 用于更新模型参数,因此在构造优化器的时候,需要向其中传入模型参数
- 迭代训练
接下来我们用一个线性回归的例子来将之前学到的知识利用起来,首先来构造一个数据集。在此使用线性模型的参数为
、
2.1 手工版
首先可以利用基础的PyTorch内容,来构造一个线性回归的模型,第一步就是生成数据
import random
import torch
def synthetic_data(w, b, num_examples):
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)
print('features:', features[0], '\nlabel:', labels[0])
features: tensor([0.1043, 0.0730])
label: tensor([4.1636])
此处为了简化显示,使用了李沐老师开发的d2l工具包
pip install d2l
# pytorch 的某些版本中,要想把Tensor对象转换为numpy对象,需要先调用detach()函数
from d2l import torch as d2l
d2l.set_figsize()
d2l.plt.scatter(features[:, 1].detach().numpy(),
labels.detach().numpy(), 1)
# 数据迭代器
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]
batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, '\n', y)
break
tensor([[ 0.5895, 2.6396],
[ 1.3606, -0.6769],
[ 0.2840, 3.0876],
[-0.5803, -0.1469],
[-0.6710, 1.4318],
[-0.5053, -0.3726],
[ 1.5884, 0.2266],
[ 0.3222, -0.7637],
[ 0.0375, -1.9838],
[-0.1626, 0.5017]])
tensor([[-3.5732],
[ 9.2212],
[-5.7238],
[ 3.5467],
[-2.0095],
[ 4.4570],
[ 6.6029],
[ 7.4260],
[11.0141],
[ 2.1572]])
# 初始化模型参数
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
# 定义模型(就是深度学习中的网络结构)
def linreg(X, w, b):
"""线性回归模型"""
return torch.matmul(X, w) + b
# 损失函数
# 均方误差
# 因为输出只有一个维度,所以在这里进行了简化,没有计算均值
def squared_loss(y_hat, y):
return (y_hat - y.reshape(y_hat.shape))**2 / 2
# 优化器——用于更新模型参数
def sgd(params, lr, batch_size):
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
lr = .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)
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.043805
epoch 2, loss 0.000168
epoch 3, loss 0.000049
可以看到损失逐渐减小,此时得到的模型参数为:
print(w, b)
tensor([[ 2.0000],
[-3.3998]], requires_grad=True) tensor([4.1994], requires_grad=True)
2.2 框架版
使用pyTorch框架内置的对象和函数,可以更加容易地实现上面的功能,在此我们主要使用以下函数与对象
-
2.2.1 TensorDataset
TensorDataset
定义为:
class torch.utils.data.TensorDataset(data_tensor, target_tensor)
用来对两个Tensor
对象(即data_tensor
和target_tensor
)进行配对打包,两个对象第一个维度上的大小应该相同。
2.2.2 DataLoader
torch.utils.data.DataLoader
DataLoader
为数据加载器,其作用为组合数据集与采样器,并在数据集上提供一个可迭代的对象。DataLoader
类的定义为:
torch.utils.data.DataLoader(dataset,
batch_size=1,
shuffle=None,
sampler=None,
batch_sampler=None,
num_workers=0,
collate_fn=None,
pin_memory=False,
drop_last=False,
timeout=0,
worker_init_fn=None,
multiprocessing_context=None,
generator=None, *,
prefetch_factor=None,
persistent_workers=False,
pin_memory_device='')
其中:
-
dataset
:数据集 -
batch_size
: 每个batch的大小 -
num_workers
: 是否使用多进程读取数据 -
shuffle
: 每个epoch中是否重新进行乱序操作 -
drop_last
:当样本数不能被batch_size
整除时,是否舍弃最后一批数据
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):
"""构造一个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)
next(iter(data_iter))
[tensor([[-1.3819, 0.3498],
[ 1.2611, 1.3081],
[-1.2086, -1.8001],
[-1.3380, 0.4080],
[-0.9351, 1.1630],
[-0.3644, -0.4445],
[-0.8413, 0.8786],
[-1.3562, -0.9043],
[-1.0410, 1.8153],
[ 0.0348, -0.3503]]),
tensor([[ 0.2431],
[ 2.2674],
[ 7.9073],
[ 0.1253],
[-1.6114],
[ 4.9960],
[-0.4723],
[ 4.5547],
[-4.0486],
[ 5.4708]])]
2.2.3 Sequential和Linear
在PyTorch中Module
是torch.nn
中最核心的数据结构,它可以是神经网络的某个层,也可以是包含多个层的神经网络。在实际使用中,最常见的做法就是继承torch.nn.Module
,而Sequential
就是继承自torch.nn.Module
,表示一种简单的序贯模型。
其主要有三种常见的实现形式:
实现方式 1
model = nn.Sequential(
nn.Conv2d(1,20,5),
nn.ReLU(),
nn.Conv2d(20,64,5),
nn.ReLU())
这种实现方式比较简单,但是也有明显的缺点,即每一层没有自己的名字
实现方式 2
from collections import OrderedDict
model = nn.Sequential(OrderedDict([
('conv1', nn.Conv2d(1,20,5)),
('relu1', nn.ReLU()),
('conv2', nn.Conv2d(20,64,5)),
('relu2', nn.ReLU())
]))
这种实现方式会为每一层添加一个名字,但是在使用时仍然无法通过名称进行索引
实现方式 3
model = nn.Sequential()
model.add_module("conv1",nn.Conv2d(1,20,5))
model.add_module('relu1', nn.ReLU())
model.add_module('conv2', nn.Conv2d(20,64,5))
model.add_module('relu2', nn.ReLU())
torch.nn.Linear
Linear
是一个线性变换,即,也可以理解为一个全连接模型,其定义为:
torch.nn.Linear(in_features, out_features, bias=True, device=None, dtype=None)
-
in_features
输入的大小 -
out_features
输出的大小 -
bias
是否使用了偏置,默认为启用
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()
# SGD
trainer = torch.optim.SGD(net.parameters(), lr=.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}')
epoch 1, loss 0.000206
epoch 2, loss 0.000100
epoch 3, loss 0.000100