写在前面的话:这是跟着ApacheCN团队学习pytorch的学习笔记,主要资源来自pytorch官网和ApacheCN社区
一、PyTorch 是什么?
它是一个基于 Python 的科学计算包, 其主要是为了解决两类场景:
- NumPy 的替代品, 以使用 GPU 的强大加速功能
- 一个深度学习研究平台, 提供最大的灵活性和速度
个人感觉,其实无论是pytorch还是tensorflow其实都是帮忙解决了在GPU上的自动求导问题,这是对我们这些深度学习使用者来说最关键的。也就是说,通过这些框架,我们不用去过多地操心反向求导的过程,而是可以更多地专注于神经网络(或者说深度学习)的结构等问题。当然,它们也提供了相应的封装来满足一些基本需求。
二、新手入门
对于深度学习来说,一个比较重要的概念就是张量。数学上的定义是张量(Tensor)是一个定义在的一些向量空间和一些对偶空间的笛卡儿积上的多重线性映射。而简单来说其实就是矢量的推广。在同构的意义下,第零阶张量为标量,第一阶张量为向量 (Vector), 第二阶张量则成为矩阵 (Matrix)。对我们来说,常用的其实也就3阶和4阶的张量(这里没有把矩阵它们当成张量),更高阶张量其实也很难遇到。例如一张图片就是3阶张量,包括长、宽和通道(通常是RGB3通道)。在使用时也会变成4阶张量,因为会有batch的值。(也就是有很多3阶张量堆在一起)。
1.初识张量
Tensors 与 NumPy 的 ndarrays 非常相似, 除此之外还可以在 GPU 上使用张量来加速计算。
from __future__ import print_function
import torch
# 构建一个 5x3 的矩阵
# 张量只是创建了,但是未初始化
# 可以看出,其实这里就是生成了一个5行3列的矩阵
x = torch.Tensor(5, 3)
print(x)
>>tensor([[ 0.0000, -2.0000, 0.0000],
[-2.0000, 0.0000, 0.0000],
[ 0.0000, 0.0000, 0.0000],
[ 0.0000, 0.0000, 0.0000],
[ 0.0000, 0.0000, 0.0000]])
# 获取 size,注:torch.Size 实际上是一个 tuple(元组),所以它支持所有 tuple 的操作。
print(x.size())
>>torch.Size([5, 3])
#PS:torch.Size 实际上是一个 tuple(元组), 所以它支持所有 tuple(元组)的操作.
2.基本操作
加操作:
#第一种写法
y = torch.rand(5, 3)
print(x + y)
>>tensor([[ 0.8042, -1.5267, 0.5508],
[-1.0805, 0.2719, 0.9532],
[ 0.8435, 0.5595, 0.8556],
[ 0.6867, 0.8612, 0.7824],
[ 0.9080, 0.1819, 0.2504]])
#这里的y就是tensor([[ 0.8042, 0.4733, 0.5508],
# [ 0.9195, 0.2719, 0.9532],
# [ 0.8435, 0.5595, 0.8556],
# [ 0.6867, 0.8612, 0.7824],
# [ 0.9080, 0.1819, 0.2504]])
#
# x是第一部分代码中的x
#第二种写法
print(torch.add(x, y))
>>tensor([[ 0.8042, -1.5267, 0.5508],
[-1.0805, 0.2719, 0.9532],
[ 0.8435, 0.5595, 0.8556],
[ 0.6867, 0.8612, 0.7824],
[ 0.9080, 0.1819, 0.2504]])
#第三种写法,提供一个输出 tensor 作为参数
result = torch.Tensor(5, 3)
torch.add(x, y, out = result)
print(result)
>>>>tensor([[ 0.8042, -1.5267, 0.5508],
[-1.0805, 0.2719, 0.9532],
[ 0.8435, 0.5595, 0.8556],
[ 0.6867, 0.8612, 0.7824],
[ 0.9080, 0.1819, 0.2504]])
#第四种写法,in-place(就地操作)
# adds x to y
y.add_(x)
print(y)
>>>>>>tensor([[ 0.8042, -1.5267, 0.5508],
[-1.0805, 0.2719, 0.9532],
[ 0.8435, 0.5595, 0.8556],
[ 0.6867, 0.8612, 0.7824],
[ 0.9080, 0.1819, 0.2504]])
索引(类似Numpy的索引):
print(x[:, 1])
>>tensor([-2.0000, 0.0000, 0.0000, 0.0000, 0.0000])
改变大小:
x = torch.randn(4, 4)
>>tensor([[ 0.2755, -0.1519, 0.0257, -0.7659],
[ 0.7431, -1.0414, 0.5645, -1.0806],
[ 0.7274, -0.5298, -1.5444, -0.2279],
[-0.9928, -1.0443, 0.4778, -0.2496]])
y = x.view(16)
>>tensor([ 0.2755, -0.1519, 0.0257, -0.7659, 0.7431, -1.0414, 0.5645,
-1.0806, 0.7274, -0.5298, -1.5444, -0.2279, -0.9928, -1.0443,
0.4778, -0.2496])
z = x.view(-1, 8) # -1就是根据情况,由计算机自己推断这个维数
>>tensor([[ 0.2755, -0.1519, 0.0257, -0.7659, 0.7431, -1.0414, 0.5645,
-1.0806],
[ 0.7274, -0.5298, -1.5444, -0.2279, -0.9928, -1.0443, 0.4778,
-0.2496]])
3.NumPy Bridge
将一个 Torch Tensor 转换为 NumPy 数组, 反之亦然。
Torch Tensor 和 NumPy 数组将会共享它们的实际的内存位置, 改变一个另一个也会跟着改变。
#转换一个 Torch Tensor 为 NumPy 数组
a = torch.ones(5)
print(a)
>>tensor([ 1., 1., 1., 1., 1.])
b = a.numpy()
print(b)
>>array([1., 1., 1., 1., 1.], dtype=float32)
#尽管转换了,但是两者依然共享内存
a.add_(1)
print(a)
print(b)
>>tensor([ 2., 2., 2., 2., 2.])
>>[2. 2. 2. 2. 2.]
#转换 NumPy 数组为 Torch Tensor
import numpy as np
a = np.ones(5)
>>array([1., 1., 1., 1., 1.])
b = torch.from_numpy(a)
>>tensor([ 1., 1., 1., 1., 1.], dtype=torch.float64)
#同样两者共享内存
np.add(a, 1, out = a)
print(a)
print(b)
>>[2. 2. 2. 2. 2.]
>>tensor([ 2., 2., 2., 2., 2.], dtype=torch.float64)
Note:
除了 CharTensor 之外, CPU 上的所有 Tensor 都支持与Numpy进行互相转换
4.CUDA Tensors
可以使用 .cuda 方法将 Tensors 在GPU上运行.
# 只要在 CUDA 是可用的情况下, 我们可以运行这段代码
if torch.cuda.is_available():
x = x.cuda()
y = y.cuda()
x + y
三、自动求导
PyTorch 中所有神经网络的核心是 autograd自动求导包. 我们先来简单介绍一下, 然后我们会去训练我们的第一个神经网络。
autograd 自动求导包针对张量上的所有操作都提供了自动微分操作. 这是一个逐个运行的框架, 这意味着您的反向传播是由您的代码如何运行来定义的, 每个单一的迭代都可以不一样。
1.Variable(变量)
autograd.Variable 是包的核心类. 它包装了张量, 并且支持几乎所有的操作. 一旦你完成了你的计算, 你就可以调用 .backward()方法, 然后所有的梯度计算会自动进行。
pytorch允许通过 .data 属性来访问原始的张量, 而关于该 variable(变量)的梯度会被累计到 .grad 上去。

还有一个针对自动求导实现来说非常重要的类 - Function。
Variable 和 Function 是相互联系的, 并且它们构建了一个非循环的图, 编码了一个完整的计算历史信息. 每一个 variable(变量)都有一个 .grad_fn 属性, 它引用了一个已经创建了 Variable 的 Function (除了用户创建的 Variable 之外 - 它们的 grad_fn is None )
如果你想计算导数, 你可以在 Variable 上调用 .backward() 方法. 如果 Variable 是标量的形式(例如, 它包含一个元素数据), 你不必指定任何参数给 backward(), 但是, 如果它有更多的元素. 你需要去指定一个 grad_output 参数, 该参数是一个匹配 shape(形状)的张量。
import torch
from torch.autograd import Variable
#创建 variable(变量)
x = Variable(torch.ones(2, 2), requires_grad = True)
print(x)
>>tensor([[ 1., 1.],
[ 1., 1.]])
#y 由操作创建,所以它有 grad_fn 属性.
y = x + 2
print(y)
>>tensor([[ 3., 3.],
[ 3., 3.]])
z = y * y * 3
out = z.mean()
print(z, out)
>>tensor([[ 27., 27.],
[ 27., 27.]])
>>tensor(27.)
2.梯度
pytorch之类的框架对于我们学习者来说最大的帮助莫过于反向求导的简单化。
我们考虑上述例子的反向求导过程,首先,先写出整体的前向过程:
所以在反向求导时:
故而:
如果没有框架,单纯编写这段代码其实比较的繁琐。而在使用了pytorch框架后,只需要调用out.backward(),pytorch就会自动求导其导数,将其存放在.grad中:
out.backward()
print(x.grad)
>>tensor([[ 4.5000, 4.5000],
[ 4.5000, 4.5000]])
同时,梯度的有趣应用:
x = torch.randn(3)
x = Variable(x, requires_grad = True)
y = x * 2
gradients = torch.FloatTensor([0.1, 1.0, 0.0001])
y.backward(gradients)
print(x.grad)
>>tensor([ 0.2000, 2.0000, 0.0002])
四、实战之基本的卷积神经网络
神经网络可以使用 torch.nn 包构建。
autograd 实现了反向传播功能, 但是直接用来写深度学习的代码在很多情况下还是稍显复杂, torch.nn 是专门为神经网络设计的模块化接口. nn 构建于 Autograd 之上, 可用来定义和运行神经网络. nn.Module 是 nn 中最重要的类, 可把它看成是一个网络的封装, 包含网络各层定义以及 forward 方法, 调用 forward(input) 方法, 可返回前向传播的结果。
1.定义一个网络
import torch
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# 卷积层 '1'表示输入图片为单通道, '6'表示输出通道数, '5'表示卷积核为5*5
# 核心
# 初始化的过程中其实没有再定义网络结构,只是定义了一些函数
self.conv1 = nn.Conv2d(1, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
# 仿射层/全连接层: y = Wx + b
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
# 这里的前向过程才定义了整个网络结构
def forward(self, x):
# 在由多个输入平面组成的输入信号上应用2D最大池化.
# (2, 2) 代表的是池化操作的步幅
# 这里正是从输入层,通过一个卷积之后,在经过一个pool
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
# 如果大小是正方形, 则只能指定一个数字
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
# 这边便是将x拉值,以便用于全连接
x = x.view(-1, self.num_flat_features(x))
# 接下来就是普通的两个全连接层
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
# 下面是输出层
x = self.fc3(x)
return x
def num_flat_features(self, x):
size = x.size()[1:] # 除批量维度外的所有维度
num_features = 1
for s in size:
num_features *= s
return num_features
net = Net()
print(net)
>>Net(
(conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=400, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=84, bias=True)
(fc3): Linear(in_features=84, out_features=10, bias=True)
)
只要在 nn.Module 的子类中定义了 forward 函数, backward 函数就会自动被实现(利用 autograd )。 在 forward 函数中可使用任何 Tensor 支持的操作。
并不像tensorflow需要显式地定义参数,pytorch在上述过程中只需要用户输入维度信息,参数的维度便可由计算机自动给出。网络的可学习参数通过 net.parameters() 返回,net.named_parameters 可同时返回学习的参数以及名称。
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1的weight
>>10
>>torch.Size([6, 1, 5, 5])
向前的输入是一个 autograd.Variable, 输出也是如此。注意: 这个网络(LeNet)的预期输入大小是 32x32, 使用这个网上 MNIST 数据集, 请将数据集中的图像调整为 32x32
input = Variable(torch.randn(1, 1, 32, 32))
out = net(input)
print(out)
>>tensor([[-0.0821, 0.1081, 0.0103, 0.1502, 0.0191, 0.0097, -0.0175,
-0.0804, -0.0055, -0.0382]])
Note:
- torch.nn 只支持小批量(mini-batches), 不支持一次输入一个样本, 即一次必须是一个 batch
- nn.Conv2d 的输入必须是 4 维的, 形如 nSamples x nChannels x Height x Width
2.损失函数
损失函数采用 (output, target) 输入对, 并计算预测输出结果与实际目标的距离。
在 nn 包下有几种不同的损失函数。一个简单的损失函数是: nn.MSELoss 计算输出和目标之间的均方误差
output = net(input)
target = Variable(torch.arange(1, 11)) # 一个虚拟的目标
criterion = nn.MSELoss()
loss = criterion(output, target)
print(loss)
现在, 如果你沿着 loss 反向传播的方向使用.grad_fn 属性, 你将会看到一个如下所示的计算图:
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss
所以, 当我们调用loss.backward(), 整个图与损失是有区别的, 图中的所有变量都将用 .grad 梯度累加它们的变量。
3.反向传播
为了反向传播误差, 我们所要做的就是 loss.backward()。你需要清除现有的梯度, 否则梯度会累加之前的梯度。
现在我们使用 loss.backward(), 看看反向传播之前和之后 conv1 的梯度。
net.zero_grad() # 把之前的梯度清零
print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)
>>None
loss.backward()
print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)
>>tensor([ 0.1580, -0.0348, -0.1106, 0.0706, -0.0937, -0.0539])
4.更新权重
实践中使用的最简单的更新规则是随机梯度下降( SGD ):
可以使用简单的 python 代码来实现这个:
learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate)
当然,更新权重的方法pytorch也已经做了封装(torch.optim),方便我们调用:
import torch.optim as optim
# 新建一个优化器, 指定要调整的参数和学习率
optimizer = optim.SGD(net.parameters(), lr = 0.01)
# 在训练过程中:
optimizer.zero_grad() # 首先梯度清零(与 net.zero_grad() 效果一样)
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step() # 更新参数