最近在学习PyTorch,网上资料有些杂乱,进行了稍许整理,望能有所助益。
PyTorch简介
- Torch是 PyTorch的前身,其底层语言相同,但使用不同的上层包装语言。Torch是一个支持大量机器学习算法的科学计算框架。
- PyTorch是基于Torch的python开源机器学习库,由FaceBook人工智能小组开发。
为什么PyTorch?
- 不仅能够实现强大的GPU加速,同时还支持动态神经网络。
- TensorFlow和Caffe都是命令式的编程语言,是静态的,想要改变网络的结构,就必须从头开始。而PyTorch通过反向求导技术,零延迟的任意改变神经网络行为,而且实现速度快,十分灵活。
- 对比于Tensorflow,更加简洁直观,底层代码也更容易看懂。
缺点
- 全面性处于劣势,不支持快速傅里叶、沿维翻转张量和检查无穷与非数值张量。
- 针对移动端、嵌入式部署以及高性能服务器端的部署其性能表现有待提升。
- 框架较新,社区没有那么强大,在文档方面其C库大多数没有文档。
PyTorch安装
- 下载安装Anaconda,提供了python包管理与环境管理的功能。
- 可以使用pip安装或者conda:
pip3 install http://download.pytorch.org/whl/cpu/torch-0.4.0-cp36-cp36m-win_amd64.whl(链接根据不同python版本而定,py3.6 CPU)
conda install pytorch cuda80 -c pytorch GPU版本
conda install pytorch-cpu -c pytorch CPU版本- 以上安装完成之后还需要执行:
pip3 install torchvision
- 可以通过import torch检测是否安装成功。
做足准备工作以后,就要真正学习pytorch了!
PyTorch运算
PyTorch中tensor的运算和Numpy的array如出一辙。- 不加初始化地构建矩阵:
torch.empty(row, column)
- 构建一个随机初始化矩阵:
x = torch.rand(5, 3)
,输出:
- 构建全0矩阵:
x = torch.zeros(5, 3, dtype=torch.long)
,第三个参数指定数据类型。 - 直接构建一个张量:
x = torch.tensor([5.5, 3.2])
,参数为一个array。 - 获取维度信息:
print(x.size())
,输出为:
- 求和有多种方式:
- 直接求和
y = torch.rand(5, 3) print(x + y)
print(torch.add(x, y))
result = torch.empty(5, 3) torch.add(x, y, out=result)
y.add_(x)
注意 任何使张量会发生变化的操作都有一个前缀 ‘_’。例如x.copy_(y)
- 直接求和
- 可以使用标准的NumPy类似的索引操作,例如
print(x[:, 1])
打印x第一列元素。 - 改变tensor的形状,使用
torch.view
,注意大小不能改变。
结果如图所示:x = torch.randn(4, 4) y = x.view(16) z = x.view(-1, 8) # the size -1 is inferred from other dimensions print(x.size(), y.size(), z.size())
- 获取只有一个元素的tensor值:
x.item()
NumPy与Tensor相互转换
- N2T:
b = a.numpy()
,注意,转换后的tensor与numpy指向同一地址,改变一方的值另一个也会随之改变。 - T2N:
b = torch.from_numpy(a)
。
- N2T:
PyTorch自动微分
PyTorch中所有神经网络的核心是 Autograd 自动求导包。torch.Tensor是包的核心类,Autograd软件包为Tensors上的所有操作提供自动微分。
- 如果设置属性
.requires_grad=True
,则跟踪针对tensor的所有操作。 - 完成计算后,调用
.backward()
来自动计算所有梯度。该tensor的梯度将累积到.grad
属性中。 - 若要停止tensor历史记录的跟踪,调用
y=x.detach()
,则y为独立于计算图之外的tensor。 - 若要停止跟踪历史记录和使用内存,可使用
with torch.no_grad():
包装代码块,在测试阶段使用。
除了显示设置requires_grad属性外,还有一个规则:
如果一个tensor Y由其他tensor X 1 X_1 X1, X 2 X_2 X2…计算得到,则只要 X i X_i Xi任意一个requires_grad为True,则Y的requires_grad即为True。
该属性可以用来“冻结”model的一部分使其参数不变,微调网络中的其他部分。如下代码所示,只调整最后的FC层参数。model = torchvision.models.resnet18(pretrained=True) for param in model.parameters(): param.requires_grad = False model.fc = nn.Linear(512, 100) optimizer = optim.SGD(model.fc.parameters(), lr=1e-2, momentum=0.9)
那么,Autograd如何记录历史信息呢?
概念上来说,它为每条数据记录了一个有向无环图,即计算图,如 y = x ∗ s i n ( x ∗ a + b ) y=x*sin(x*a+b) y=x∗sin(x∗a+b)计算图如下所示,沿着计算图应用链式求导法则就可以求出梯度:
可以看出,PyTorch中的计算图的每个结点都是一个Function对象,这个对象可以使用apply()进行操作。
在前向传播过程中,Autograd一边执行着前向计算,一边搭建一个graph,这个graph的结点是用于计算梯度的函数,这些函数结点保存在对应Tensor的.grad_fn中;而反向传播就利用这个graph计算梯度。举个例子:
x = torch.ones(2, 2, requires_grad=True) # 设置requires_grad为True,默认为False y = x + 2 print(y.requires_grad) # 因为y由x计算得到,故其结果为True z = y * y * 3 # z.requires_grad_(False) # 会报错,告诉你只能改变叶子变量的状态 # print(z.grad_fn) # 打印Function节点操作 out = z.mean() out.backward() # 进行反向传播 print(x.grad)
其结果为:
分析:链式求导 ∂ o u t ∂ x = ∂ o u t ∂ z ⋅ ∂ z ∂ y ⋅ ∂ y ∂ x = 1 4 ⋅ 6 y ⋅ 1 \frac{\partial out}{\partial x}=\frac{\partial out}{\partial z}\cdot \frac{\partial z}{\partial y}\cdot\frac{\partial y}{\partial x}=\frac 14 \cdot6y\cdot1 ∂x∂out=∂z∂out⋅∂y∂z⋅∂x∂y=41⋅6y⋅1在使用Tensor.backward()时,如果Tensor是标量(如上面例子),则无需指定任何参数。但若它有多个元素,则需要指定一个gradient参数来指定张量的形状。
当输出tensor为向量时,需要指定一个grad_output参数,举例如下:x = torch.randn(3, requires_grad=True) y = x * x * x gradients = torch.tensor([0.1, 1.0, 0.01]) y.backward(gradients) print("=========y对x求导 =======") print(x.grad) print("=========数学求导=======") # y=x^3 求导 3x^2,然后乘以权重系数 0.1,1.0,0.01 比较结果 print(3 * x * x)
PyTorch神经网络
神经网络可以通过 torch.nn 包来构建。现在以手写数字识别任务为例,使用LeNet网络结构,我们分步来看PyTorch如何构建及训练该网络。
1.定义神经网络
import torch import torch.nn as nn import torch.nn.functional as F class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = nn.Conv2d(1, 6, 5) # 卷积参数介绍:输入通道数、卷积核数量、卷积核大小 self.conv2 = nn.Conv2d(6, 16, 5) self.fc1 = nn.Linear(16 * 5 * 5, 120) # 全连接层:y = Wx + b self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) def forward(self, x): x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2)) # 最大池化层:2*2 x = F.max_pool2d(F.relu(self.conv2(x)), 2) # 2*2可简写为2 x = x.view(-1, self.num_flat_features(x)) # 调用num_flat_features(x),计算flat维度作为fc层输入 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) )
2.前馈得到输出
在Net类定义时,参数为nn.Module,其包括网络层和一个方法forward(input)它会返回输出(output)。
先来看下模型参数,一个模型可训练的参数可以通过调用net.parameters()返回.params = list(net.parameters()) print(len(params)) # 结果为10,五个网络层,每个网络层包括2个,一个为矩阵参数,一个为bias print(params[0].size()) # 第一层卷积层参数,torch.Size([6, 1, 5, 5])
然后随机生成一个32x32的输入:
input = torch.randn(1, 1, 32, 32) out = net(input) print(out)
调用forward得到输出结果。
3.计算loss
target = torch.randn(10) # 使用随机数模拟标准输出 target = target.view(1, -1) # 改变维度,与输出一致 criterion = nn.MSELoss() loss = criterion(out, target) print(loss)
4. 反向传播梯度
有了loss,就可以进行梯度的反向传播了。首先,看一下传播路径(计算图):
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d -> view -> linear -> relu -> linear -> relu -> linear -> MSELoss -> loss
调用loss.backward(),整个图都会微分,而且所有的在图中的requires_grad=True的张量将会使其grad张量累计梯度。但是注意,首先要清空现存梯度。
net.zero_grad() # 清空现存梯度 print(net.conv1.bias.grad) # 输出反向传播之前的梯度 loss.backward() print(net.conv1.bias.grad) # 输出反向传播之后的梯度
得到输出:
conv1.bias.grad before backward tensor([0., 0., 0., 0., 0., 0.]) conv1.bias.grad after backward tensor([-0.0054, 0.0011, 0.0012, 0.0148, -0.0186, 0.0087])
5. 更新网络参数
- 可以使用python进行随机梯度下降, w e i g h t = w e i g h t − l e a r n i n g r a t e ∗ g r a d i e n t weight = weight - learning_rate * gradient weight=weight−learningrate∗gradient。
learning_rate = 0.01 #设置学习率 params = list(net.parameters()) # 列出参数,查看参数在更新前后的变化 print(params[1]) # 第一个卷积层的bias for f in net.parameters(): f.data.sub_(f.grad.data * learning_rate) # 对所有参数进行更新 print(params[1])
- 可以使用神经网络包使用其他更新规则,先引入
import torch.optim as optim
。
optimizer = optim.SGD(net.parameters(), lr=0.01) # 创建一个optimizer # in your training loop: optimizer.zero_grad() # zero the gradient buffers output = net(input) loss = criterion(out, target) loss.backward() optimizer.step() # Does the update
参考文档:
- 不加初始化地构建矩阵: