Pytorch是Torch在Python上的衍生。因为Torch是一个使用Lua语言的神经网络库,Torch很好用,但是Lua不是特别流行,所有开发团队将Lua的Torch移植到了更流行的语言Python上。
几乎所有的框架都是基于计算图的,而计算图又可以分为静态计算图和动态计算图,静态计算图先定义再运行,一次定义多次运行,而动态计算图是在运行过程中被定义的,在运行的时候构建,可以多次构建多次运行。Pytorch使用的是动态图,TensorFlow使用的是静态图。在Pytorch中每一次前向传播(每一次运行代码)都会创建一幅新的计算图。
静态图一旦创建就不能修改,而且静态图定义的时候,使用了特殊的语法,就相当于新学一门语言,这意味着无法使用一些常用的Python语句,而是专门设计语法,同时在构建图的时候必须把所有可能出现的情况都包含进去,这就导致了静态图过于庞大,可能占用过高的显存。
动态图就没有这个问题,它可以使用Python的if,while,for-loop等基础语句,最终创建的计算图取决你执行的条件分支。所以,使用动态图的Pytorch的实现方式完全和Python的语法一致,简洁直观;而TensorFlow的实现不仅代码冗长,而且十分不直观。
动态图的思想简洁明了,更符合 人的思考过程。动态图的方式使得我们可以任意修改前向传播,还可以随时查看变量的值。如果说静态图框架好比C++,每次运行都要编译才行,那么动态图框架就是Python,动态执行,可以交互式查看修改。
动态图带来的另外一个优势是调试更容易,在Pytorch中,代码报错的地方,往往就是你代码写错的地方,而静态图需要先根据你的代码生成Graph对象,然后在session.run()时报错,这种报错几乎很难找到对应的代码中真正错误的地方。
Pytorch是当前难得的简洁优雅且高效快速的框架。当前开源的框架中,没有哪一个能够在灵活性、易用性、速度这三个方面有两个能同时超过Pytorch。
(1)简洁:Pytorch的设计追求最少的封装,尽量避免重复造轮子。Pytorch的设计遵循
tensor–>variable(autograd)–>nn.Moudle三个由低到高的抽象层次,分别代表高维数组(张量)、自动求导(变量)和神经网络(层/模块),而且这三个抽象之间联系紧密,可以同时进行修改和操作。
(2)速度:Pytorch的灵活性不以速度为代价,运行速度比其他的框架比较快。
(3)易用:Pytorch是所有的框架中面向对象设计的最优雅的一个。接口设计来源于Torch,而Torch的接口设计以灵活性易用性而著称。Pytorch的设计最符合人们的思维,它让用户尽可能地专注于实现自己的想法,即所思即所得,不需要考虑太多关于框架本身的束缚。
(4)活跃的社区:Pytorch提供了完整的文档,循序渐进的指南。
Torch自称为神经网络界的Numpy,因为它能将torch产生的tensor放在GPU中加速运算,就像Numpy会把array放在CPU中加速运算。Torch和Numpy有着很好的兼容性,可以自由的转换numpy的array和torch的tensor。
(1)格式转换
import torch
import numpy as np
np_data = np.arange(6).reshape((2,3))
torch_data = torch.from_numpy(np_data)
tensor2array = torch_data.numpy() #tensor转为numpy的array
array2tensor = torch.from_numpy(np_data) # array转为pytorch的tensor
print('\nnumpy',np_data,
'\ntorch',torch_data,
'\ntensor2array',tensor2array,
'\narray2tensor', array2tensor
)
运行结果:
import torch as t
import numpy as np
data = [[1,2],[3,4]]
tensor_data = t.FloatTensor(data) # 将data转为float32类型的tensor
print('\nmatmul',
'\nnumpy:',np.matmul(data,data),
'\ntorch:',t.mm(tensor_data,tensor_data)
)
运算结果:
还有求绝对值,加法,减法,sin等,详情可见:http://pytorch.org/docs/torch.html
一般是安装两个部分:pytorch和torchvision。前者是Pytorch的主模块,后者是一些库,包括一些网络的预先训练好的model和各种图片等。
pip3 install torch===1.3.0 torchvision===0.4.1 -f https://download.pytorch.org/whl/torch_stable.html
conda install pytorch torchvision cudatoolkit=10.1 -c pytorch
首先安装可选依赖
conda install numpy pyyaml mkl setuptools cmake gcc cffi
然后下载Pytorch源码
git clone https://github.com/pytorch/pytorch
最后编译安装
cd pytorch
python setup.py install
Tensor是Pytorch中重要的数据结构,可认为是一个高维数组。他可以是一个数(标量)、一维数组(向量)、二维数组(矩阵)或更高维的数组。Tensor和numpy的array类似,但Tensor可以GPU加速。Tensor的使用和numpy的接口类似。
import numpy as np
x1 = t.Tensor(2,3) #构建了2*3的矩阵,并未初始化
x2 = t.rand(3,4) #使用[0,1]均匀分布随机初始化二维数组
print(x1,'\n',x2)
print(x1.size(),x2.size()) # 输出矩阵的形状
import torch as t
import numpy as np
x = t.rand(3,4)
y = t.rand(3,4)
# 三种加法
result1 = x + y
result2 = t.add(x,y)
result3 = t.Tensor(3,4)
t.add(x,y,out=result3) #指定输出加法结果到result3
print(result1,'\n',result2,'\n',result3)
.cuda方法:
Tensor可以通过.cuda方法转为GPU的Tensor,从而加速运算。
import torch as t
import numpy as np
x = t.rand(3,4)
y = t.rand(3,4)
if t.cuda.is_available():
x = x.cuda()
y = y.cuda()
result = x + y
print(result)
Tensor还支持很多操作,包括数学运算、线性代数、选择、切片(与numpy类似)。
此外,Tensor和numpy的数组间互操作非常容易且快速。Tensor不支持的操作,可以先转为numpy数组处理,之后再转回Tensor。Tensor和numpy对象共享内存,所以他们之间的转换很快,几乎不会消耗资源。这意味着其中一个变了,另一个也会随之改变。
深度学习的算法实质上是通过反向传播求导数,Pytorch的Autograd模块实现了此功能。在Tensor上的所有操作,Autograd都能为他们自动提供微分,避免手动计算导数的复杂过程。
autograd.Variable类是Autograd中的核心类,它简单封装了Tensor,并支持几乎所有Tensor的操作。Tensor在被封装为Variable后,可以调用它的.backward实现反向传播,自动计算所有梯度。
Variable主要包含三个属性:
(1)data:保存Variable所包含的eTensor。
(2)grad:保存data对应的梯度,grad也是个Variable,而不是Tensor,他和data的形状一样。
(3)grad_fn:指向一个Function对象,这个Function用来反向传播计算输入的梯度。
grad在反向传播过程中是累加的,这意味着每次运行反向传播,梯度都会累加之前的梯度,所以反向传播之前需要把梯度清零。
Variable和Tensor具有几乎一致的接口,在实际使用中可以无缝切换。
Autograd实现了反向传播功能,但是直接用来写深度学习代码在很多情况下还是有点复杂,torch.nn是专门为神经网络设计的模块化接口。nn构建于Autograd之上,可以用来定义和运行神经网络。nn.Module是nn中最重要的类,可以把它看作一个网络的封装,包含网络各层定义及forward方法,调用forward(input)方法,可返回前向传播的结果。
定义网络时,需要继承nn.Module,并实现它的forward方法,把网络中具有可学习参数的层放在构造函数__init__中。如果某一层(如ReLU)不具有可学习的参数,则既可以放在构造函数中,也可以不放。
一个简单的神经网络:
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self):
# nn.Module子类的函数必须在构造函数中执行父类的构造函数
# 下式等价于nn.Module.__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)
# 全连接层
self.fc1 = nn.Linear(16*5*5,120)
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))
x = F.max_pool2d(F.relu(self.conv2(x)),2)
# reshape,’-1‘表示自适应
x = x.view(x.size()[0],-1)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
net = Net()
print(net)
输出结果:
只要在nn.Module的子类中定义了forward函数,backward函数就会被自动实现(利用Autograd)。在forward函数中可使用任何Variable支持的函数,还可以使用if,for循环,print,log等Python语法,写法和标准的Python写法一致。
网络的可学习参数通过net.parameters()返回,net.named_parameters可同时返回可学习的参数及名称。
...........
params = list(net.parameters())
print(len(params))
for name,parameters in net.named_parameters():
print(name,':',parameters.size())
forward函数的输入和输出都是Variable,只有Variable才具有自动求导功能,Tensor是没有的,所以在输入时,需要把Tensor封装成Variable。不过0.4版本以后,这两个已经整合到了一起,不再需要这步了。
需要注意的是,torch.nn只支持mini-batches,不支持一次只输入一个样本,即一次必须是一个batch。如果只想输入一个样本,则用input.unsqueeze(0)将batch-size设为1.
nn实现了神经网络种大多数的损失函数,例如nn.MSELoss用来计算均方误差,nn.CrossEntropyLoss用来计算交叉熵损失。
如果对loss进行反向传播溯源(使用grad_fn属性),可看到它的计算图如下:
当调用loss.backward()函数时,该图会动态生成并自动微分,也会自动计算图中参数的导数。
...........
input = t.randn(1,1,32,32)
output = net(input)
target = t.arange(0,10).reshape(1,10)
target = target.float()
criterion = nn.MSELoss()
loss = criterion(output,target)
net.zero_grad()
print('反向传播之前的conv1.bias的梯度')
print(net.conv1.bias.grad)
loss.backward()
print('反向传播之后的conv1.bias的梯度')
print(net.conv1.bias.grad)
在反向传播计算完所有参数的梯度后,还需要使用优化方法更新网络的权重和参数。torch.optim中实现了深度学习中绝大多数的优化方法,例如RMSProp、Adam、SGD等,便于使用。
optimizer = optim.SGD(net.parameters(),lr=0.01)
# 在训练过程中,先把梯度清零
optimizer.zero_grad()
# 计算损失
input = t.randn(1,1,32,32)
output = net(input)
target = t.arange(0,10).reshape(1,10)
target = target.float()
criterion = nn.MSELoss()
loss = criterion(output,target)
# 反向传播
loss.backward()
# 更新参数
optimizer.step()
主要步骤如下:
(1)使用torchvision加载并预处理CIFAR-10数据集
(2)定义网络
(3)定义损失函数和优化器
(4)训练网络并更新网络参数
(5)测试网络
使用的数据集是CIFAR-10,这是一个常用的彩色图片数据集,他有10个类别:airplane、automobile、bird、cat、deer、dog、frog、horse、ship和truck。每张图片都是3×32×32,即3通道,分辨率为32×32.
代码如下:
import torchvision as tv
import torchvision.transforms as transforms
from torchvision.transforms import ToPILImage
import torch.nn as nn
import torch as t
import torch.nn.functional as F
import torch.optim as optim
# 定义对数据的预处理
transform = transforms.Compose([
transforms.ToTensor(), # 转为Tensor
transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)) # 归一化
])
# 训练集
trainset = tv.datasets.CIFAR10(
root='I:/pycharm-project/DL_code-pytorch/data/',
train=True,
download=False,
transform=transform)
trainloader = t.utils.data.DataLoader(
trainset,
batch_size=4,
shuffle=True,
num_workers=0)
# 测试集
testset = tv.datasets.CIFAR10(
'I:/pycharm-project/DL_code-pytorch/data/',
train=False,
download=False,
transform=transform)
testloader = t.utils.data.DataLoader(
testset,
batch_size=4,
shuffle=False,
num_workers=0)
classes = ('plane','car','bird','cat','deer','dog',
'frog','horse','ship','truck')
# 定义网络
class Net(nn.Module):
def __init__(self):
# nn.Module子类的函数必须在构造函数中执行父类的构造函数
# 下式等价于nn.Module.__init__(self)
super(Net,self).__init__()
# 卷积层
# 卷积层’1‘表示输入图片为单通道,’6‘表示输出通道数,’5‘表示卷积核为5*5
self.conv1 = nn.Conv2d(3,6,5)
self.conv2 = nn.Conv2d(6,16,5)
# 全连接层
self.fc1 = nn.Linear(16*5*5,120)
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))
x = F.max_pool2d(F.relu(self.conv2(x)),2)
# reshape,’-1‘表示自适应
x = x.view(x.size()[0],-1)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
net = Net()
# 损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(),lr=0.001,momentum=0.9)
# 训练网络
for epoch in range(2):
running_loss = 0.0
for i,data in enumerate(trainloader,0):
# 输入数据
inputs,labels = data
# 梯度清零
optimizer.zero_grad()
# 前向传播和反向传播
outputs = net(inputs)
loss = criterion(outputs,labels)
loss.backward()
# 更新参数
optimizer.step()
# 打印log信息
running_loss += loss.item()
if i%2000 ==1999:
print('[%d,%5d] loss: %.3f'%(epoch+1,i+1,running_loss/2000))
running_loss = 0.0
print('Finished Training')
# 预测
correct = 0
total = 0
for data in testloader:
images,labels = data
outputs = net(images)
_,predicted = t.max(outputs.data,1)
total += labels.size(0)
correct += (predicted == labels).sum()
print('10000张测试集中的准确率为:%d %%' % (100*correct/total))
运算结果:
最终的准确率为55%。
程序中:Dataset对象是一个数据集,可以按下标访问,返回形如(data,label)的数据。
Dataloader是一个可迭代的对象,它将dataset返回的每一条数据样本拼接成一个batch,并提供多线程加速优化和数据打乱等操作。当程序对dataset的所有数据遍历完一遍之后,对Dataloader也完成了一次迭代。