Pytorch简介 [TOC]
为什么是PyTorch? 人生苦短 几乎所有的深度学习框架都是基于计算图的,而图又可以分为静态计算图和动态计算图,静态计算图是先定义再运行(define and run),一次定义多次运行,静态图一旦创建则不能修改,如果修改了图的结构,则必须重新开始运行,而动态计算图是在运行的过程中被定义,在运行的时候构建(define by run),可以多次构建多次运行。以静态计算图为代表的深度学习框架是TensorFlow,动态计算图为代表的是PyTorch。下图则展示了PyTorch动态构建计算图的过程。
静态图在构建的时候必须把所有可能出现的情况都要包含进去,则也导致了静态图过于庞大,可能占用过高的显存,而且静态图的定义无法使用if、while、for 等常用的python语句,不得不为这些操作专门设计语法。而动态计算图则没有这个问题,它可以使用原生自带的if、while、for等条件语句,最终创建的计算图取决于你执行的条件分支。 我们来看下if语句在TensorFlow和Pytorch中两种实现方式。
PyTorch
{.line-numbers} 1 2 3 4 5 6 7 8 9 10 11 12 import torchfrom torch.autograd import VariableN,D,H =3 ,4 ,5 X = Variable(torch.randn(N,D)) W1 = Variable(torch.randn(N,H)) W1 = Variable(torch.randn(N,H)) z = 10 y = X.mm(W1) if z>0 else X.mm(W2)
TensorFlow
{.line-numbers} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import tensorflow as tfimport numpy as npN,D,H = 3 ,4 ,5 X = tf.placeholder(tf.float32,shape=(N,D)) W1 = tf.placeholder(tf.float32,shape=(D,H)) W2 = tf.placeholder(tf.float32,shape=(D,H)) z = tf.placeholder(tf.float32,shape=None ) def f1 () : return tf.matmul(X,W1) def f2 () : return tf.matmul(X,W2) y = tf.cond(tf.less(z,0 ),f1,f2) with tf.Session() as sess: values = { X:np.random.randn(N,D), z:10 , W1:np.random.randn(D,H), W2:np.random.randn(D,H) } y_val = sess.run(y,feed_dict=values)
动态计算图可以使我们任意修改前向传播,可以随时查看和修改变量的值,十分灵活,这样的特性带来的另外一个优势是调试更方便,Pytorch中报错的地方,往往就是写错代码的地方,而静态图先构建图(Graph对象,构建图的过程不报错,然后在session.run()的时候报错,这种报错很难定位到源代码中真正出错的地方,更像在操作一个黑箱。
2017年在PyTorch发布不久后,OpenAI的科学家,Tesla的AI部门主管Andrej Karpathy发了一篇Twitter调侃道:
I’ve been using PyTorch a few months now. I’ve never felt better,I have more energy. My skin is clearer. My eye sight has improved.
人生苦短,我用PyTorch.
Tensor Tensor 是PyTorch中一个重要的数据结构,可以认为是一高维数组。它可以是标量(一位数)、向量(一维数组)、矩阵(二维数组)以及更高维的数组。Tensor和numpy的ndarray类似,二者可以互相自由转化。但Tensor可以使用GPU进行加速运算。下面通过几个例子来看下Tensor的基本用法。
1 2 3 import torch torch.__version__
1 2 3 4 import numpy as npx0 = torch.Tensor(5 ,3 ) x0
1 2 3 4 5 tensor([[-1.2792e+36, 3.0679e-41, 5.7453e-44], [ 0.0000e+00, nan, 0.0000e+00], [ 1.3733e-14, 6.4076e+07, 2.0706e-19], [ 7.3909e+22, 2.4176e-12, 1.1625e+33], [ 8.9605e-01, 1.1632e+33, 5.6003e-02]])
可以看到torch.Tensor()方法默认生成指定维度的随机浮点数,默认的类型为torch.FloatTensor
1 2 3 4 x0.size() print(x0.size()) print("Tensor的行数:{r},列数:{c}" .format(r=x0.size(0 ),c=x0.size(1 )))
1 2 torch.Size([5, 3]) Tensor的行数:5,列数:3
torch.Size 是tuple对象的子类,因此它支持tuple的所有操作,如x.size()[0]
1 2 3 x1 = torch.rand(5 ,3 ) x1
1 2 3 4 5 tensor([[0.4395, 0.7811, 0.9241], [0.0098, 0.5738, 0.6705], [0.9428, 0.7089, 0.1504], [0.7572, 0.4142, 0.6234], [0.1544, 0.4454, 0.5211]])
1 2 3 4 5 tensor([[-1.2792e+36, 7.8115e-01, 9.2413e-01], [ 9.7851e-03, nan, 6.7051e-01], [ 9.4281e-01, 6.4076e+07, 1.5038e-01], [ 7.3909e+22, 4.1425e-01, 1.1625e+33], [ 1.0504e+00, 1.1632e+33, 5.7714e-01]])
1 2 3 4 5 tensor([[-1.2792e+36, 7.8115e-01, 9.2413e-01], [ 9.7851e-03, nan, 6.7051e-01], [ 9.4281e-01, 6.4076e+07, 1.5038e-01], [ 7.3909e+22, 4.1425e-01, 1.1625e+33], [ 1.0504e+00, 1.1632e+33, 5.7714e-01]])
1 2 3 result= torch.Tensor(5 ,3 ) torch.add(x0,x1,out=result)
1 2 3 4 5 tensor([[-1.2792e+36, 7.8115e-01, 9.2413e-01], [ 9.7851e-03, nan, 6.7051e-01], [ 9.4281e-01, 6.4076e+07, 1.5038e-01], [ 7.3909e+22, 4.1425e-01, 1.1625e+33], [ 1.0504e+00, 1.1632e+33, 5.7714e-01]])
下面我们看下另外一种写法,函数名后面带下划线_
,这种带下划线的函数会修改Tensor本身的内容。 例如x0.add_(x1)会改变x0,而x0.add(x1)则会返回一个新的Tensor,而x0不变。
1 2 3 4 5 tensor([[-1.2792e+36, 3.0679e-41, 5.7453e-44], [ 0.0000e+00, nan, 0.0000e+00], [ 1.3733e-14, 6.4076e+07, 2.0706e-19], [ 7.3909e+22, 2.4176e-12, 1.1625e+33], [ 8.9605e-01, 1.1632e+33, 5.6003e-02]])
1 2 3 4 5 tensor([[-1.2792e+36, 7.8115e-01, 9.2413e-01], [ 9.7851e-03, nan, 6.7051e-01], [ 9.4281e-01, 6.4076e+07, 1.5038e-01], [ 7.3909e+22, 4.1425e-01, 1.1625e+33], [ 1.0504e+00, 1.1632e+33, 5.7714e-01]])
1 2 3 4 5 tensor([[-1.2792e+36, 7.8115e-01, 9.2413e-01], [ 9.7851e-03, nan, 6.7051e-01], [ 9.4281e-01, 6.4076e+07, 1.5038e-01], [ 7.3909e+22, 4.1425e-01, 1.1625e+33], [ 1.0504e+00, 1.1632e+33, 5.7714e-01]])
Tensor和Numpy的数组之间的可以无缝转换,对于Tensor不支持的操作,我们可以先转为Numpy数组,处理后再转回Tensor。
1 tensor([1., 1., 1., 1., 1.])
1 2 3 x2_np = x2.numpy() x2_np
1 array([1., 1., 1., 1., 1.], dtype=float32)
1 tensor([2., 2., 2., 2., 2.])
1 array([2., 2., 2., 2., 2.], dtype=float32)
我们可以看到x2被修改后,x2_np对象也被修改了,这是因为Tensor和numpy对象共享内存,所以他们之间的转换很快,而且几乎不会消耗什么资源。但其中一个改变了。另外一个也会被修改。
1 2 3 4 x3_np = np.array([[1 ,2 ,3 ]]) x3_0 = torch.tensor(x3_np) x3_1 = torch.from_numpy(x3_np)
x3_0和x3_1 有什么区别?我们先对x3_0和x3_1 进行add_
操作,改变对象的值,再看看x3_np 有什么变化。
1 2 x3_0.add_(1 ) print(x3_0,x3_np)
1 tensor([[2, 3, 4]]) [[1 2 3]]
1 2 x3_1.add_(1 ) print(x3_1,x3_np)
1 tensor([[2, 3, 4]]) [[2 3 4]]
我们发现,通过torch.tensor()将Numpy对象转成Tensor对象,二者并不共享内存数据,torch.Tensor()总是先将Numpy数据拷贝一份。而torch.form()则直接引用原先Numpy数据。 Tensor可以通过cuda方法将CPU数据转化为GPU数据,从而可以使用GPU进行加速运算。
1 tensor = tensor.cuda if torch.cuda.is_available() else tensor
autograd: 自动微分 深度学习的本质是通过反向传播求导来减小误差,而Pytorch的autograd模块实现了此功能。Tensor上的所有操作,autograd都能为他们自动提供微分,避免手动求导的复杂过程。 从pytorch 0.4 起,Variable 正式并入 Tensor, 现在还可以使用旧版本的Variable,但Variable(tensor) 其实什么也没做 要想使Tensor对象使用autograd功能,只需要设置renor.requries_grad = True
。
1 2 x = torch.from_numpy(np.array([[1 ,2 ,3 ,4 ]],dtype=np.float)) x.requires_grad=True
1 2 3 y = x.sum() y.backward() x.grad
1 tensor([[1., 1., 1., 1.]], dtype=torch.float64)
$y = {x_1} + {x_2} + {x_3} + {x_4}$,对$x_i$分别求偏导,每个值都是1.
1 tensor(10., dtype=torch.float64, grad_fn=<SumBackward0>)
1 2 3 y = x.sum() y.backward() x.grad
1 tensor([[2., 2., 2., 2.]], dtype=torch.float64)
反向传播的过程是累加的(accumulated)。这意味着每次一次反向传播,梯度都会累加之前的梯度,所有反向传播之前记得把梯度清零 。
1 2 3 x.grad.data.zero_() x.grad
1 tensor([[0., 0., 0., 0.]], dtype=torch.float64)
1 tensor([[1., 1., 1., 1.]], dtype=torch.float64)
下面这个例子可能更深刻。求$y = 4\sqrt {(x_1^4 + x_2^4)} $在x=(1,1)处的偏导数。
1 2 3 4 x = torch.ones(2 ,requires_grad=True ) z=(4 *x*x) y= z.norm() y.data.item()
1 tensor([5.6569, 5.6569])
我们可以看到x.grad也与我们上图计算结果一致。
神经网络(neural network) Autograde 实现了反向传播的功能,但是直接用来实现神经网络还是稍显复杂。torh.nn 是专门为神经网络设计的模块化接口。torch.nn 构建于Autograd之上,可用来定义和运行神经网络。nn.Module是nn种最重要的类,可把它看成是一个网络的封装,包含网络各层定义以及forward方法,调用forward(input)方法,可返回前向传播的结果。下面就以最早的卷积神经网络LeNet为例,来看看如何实现nn.Module实现。LeNet的网络结果如图所示: LeNet-5出自论文Gradient-Based Learning Applied to Document Recognition,是一种用于手写体字符识别的非常高效的卷积神经网络。
定义网络 定义网络的时候,需要继承nn.Module,并实现它的forward方法,把网络中具有可学习参数的层放在构造函数__init__
中。如果某一层不具有可学习参数(如ReLU层),则建议不建议放在构造函数中,而是在forward中使用nn.functional
代替。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import torch.nn as nnimport torch.nn.functional as Fclass LeNet (nn.Module) : def __init__ (self) : super(LeNet,self).__init__() self.conv1 = nn.Conv2d(in_channels=1 ,out_channels=6 ,kernel_size=5 ) self.conv2 = nn.Conv2d(in_channels=6 ,out_channels=16 ,kernel_size=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(input=F.relu(self.conv1(x)),kernel_size=2 ) x = F.max_pool2d(input=F.relu(self.conv2(x)),kernel_size=2 ) 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
1 2 3 4 5 6 7 LeNet( (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支持的函数,还可以使用if、for、log等任何python 支持的语法。 网络的参数可以通过lenet.parameters()返回,也可通过lenet.named_parameters()返回参数名和参数值。
1 2 for name,parameters in lenet.named_parameters(): print(name,':' ,parameters.size())
1 2 3 4 5 6 7 8 9 10 conv1.weight : torch.Size([6, 1, 5, 5]) conv1.bias : torch.Size([6]) conv2.weight : torch.Size([16, 6, 5, 5]) conv2.bias : torch.Size([16]) fc1.weight : torch.Size([120, 400]) fc1.bias : torch.Size([120]) fc2.weight : torch.Size([84, 120]) fc2.bias : torch.Size([84]) fc3.weight : torch.Size([10, 84]) fc3.bias : torch.Size([10])
1 2 3 input = torch.randn(1 ,1 ,32 ,32 ) out = lenet(input) out
1 2 tensor([[-0.0417, -0.0039, -0.1051, 0.1119, 0.0675, 0.0071, 0.0435, 0.0524, -0.0480, -0.0928]], grad_fn=<ThAddmmBackward>)
1 2 3 4 5 6 7 tensor([[[[ 0.8405, -0.9849, 0.2180, ..., 0.1453, -1.0257, 0.1306], [-1.0547, 1.2620, -1.1557, ..., 0.3912, -0.5614, 0.4352], [-1.2492, 0.3228, -1.0925, ..., -0.2140, -0.4023, -1.5622], ..., [ 0.4966, 0.4494, 0.2998, ..., 1.2110, -0.4863, -1.0164], [ 0.8932, -1.0220, 0.0874, ..., 0.8751, 1.7069, -0.6414], [ 1.0658, 0.6862, 0.2639, ..., 1.2042, 0.2181, 0.5975]]]])
需要注意的是,torch.nn只支持mini-batches,不支持一次只输入一个样本,即一次必须是一个batch。但如果只想输入一个样本,则用 input.unsqueeze(0)将batch_size设为1。例如 nn.Conv2d 输入必须是4维的,形如$nSamples \times nChannels \times Height \times Width$。可将nSample设为1,即$1 \times nChannels \times Height \times Width$。
损失函数 nn
实现了神经网络中绝大多数的损失函数,例如nn.MSELoss
用来计算均方误差,nn.CrossEntropyLoss
用来计算交叉熵损失。
1 2 3 4 5 output = lenet(input) target = torch.arange(0.0 ,10.0 ).view(1 ,10 ) criterion = nn.MSELoss() loss = criterion(output,target) loss
1 tensor(28.5375, grad_fn=<MseLossBackward>)
当调用loss.backward()的时候,该图会动态生成并自动微分,也即会自动计算图中参数(Parameter)的导数。
1 2 3 4 5 6 7 lenet.zero_grad() print("误差反向传播前的conv1.bias的梯度" ) print(lenet.conv1.bias.grad) loss.backward() print("误差反向传播后的conv2.bias的梯度" ) print(lenet.conv1.bias.grad)
1 2 3 4 误差反向传播前的conv1.bias的梯度 None 误差反向传播后的conv2.bias的梯度 tensor([ 0.0069, 0.0428, 0.1053, -0.1353, -0.0520, 0.0168])
优化器 在反向传播计算完所有的参数的梯度后,还需要使用优化方法来更新网络的权重和参数,例如随机梯度下降法(SGD)的策略来更新: $$w_i+1 = w_i - lr \times grad$$ 代码实现如下:
1 2 3 learning_rate = 0.01 for f in lenet.parameters(): f.data.sub_(f.grad.data*learning_rate)
在torch.optime模块中实现了深度学习的绝大多数优化方法,例如RMSEProp、Adam、SGD等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import torch.optim as optimoptimizer = optim.SGD(lenet.parameters(),lr=0.01 ) optimizer.zero_grad() output = lenet(input) loss = criterion(output,target) loss.backward() optimizer.step()
常用数据集 在深度学习中,数据加载和预处理是非常繁琐的,但PyTorch提供了一些可以极大简化和加载数据处理流程的工具。同时,对于常用的数据集,PyTorch也提供了封装好的接口供用户快速调用。这些数据集主要保存在torchvison
中。
torchvision
实现了常用的图像数据加载功能,例如imagenet、CIFAR10、MNIST等。以及常用的数据转换操作,这极大的方便了数据加载和预处理,并且代码具有可重用性。
小试牛刀 :CIFAR-10分类
该数据集共有60000张彩色图像,它有10个类别: ‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’。每张图片都是$3\times32\times32$,也即3-通道彩色图片,分辨率为$32\times32$。每类6000张图。这里面有50000张用于训练,构成了5个训练批,每一批10000张图;另外10000用于测试,单独构成一批。测试批的数据里,取自10类中的每一类,每一类随机取1000张。抽剩下的就随机排列组成了训练批。注意一个训练批中的各类图像并不一定数量相同,总的来看训练批,每一类都有5000张图。
![cifar.png]https://images2018.cnblogs.com/blog/1196151/201712/1196151-20171225161744462-2083152737.png )
下面我们来实现对CIFAR-10数据集的分类,步骤如下:
使用torchvision 加载并预处理CIFAR-10数据集
定义网络
定义损失函数和优化器
训练网络并更新网络参数
测试网络
1 2 3 4 import torchvision as tvimport torchvision.transforms as transformsfrom torchvision.transforms import ToPILImageshow = ToPILImage()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5 ,0.5 ,0.5 ),(0.5 ,0.5 ,0.5 )) , ] ) trainset = tv.datasets.CIFAR10( root="/home/fltsettlement/zenwan/jupyternote/data/" , train=True , download=False , transform=transform ) trainloader = torch.utils.data.DataLoader( trainset, batch_size =4 , shuffle = True , num_workers = 2 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 testset = tv.datasets.CIFAR10( root="/home/fltsettlement/zenwan/jupyternote/data/" , train=False , download=False , transform=transform ) testloader = torch.utils.data.DataLoader( testset, batch_size =4 , shuffle = False , num_workers = 2 ) classes = ('plane' , 'car' , 'bird' , 'cat' ,'deer' , 'dog' , 'frog' , 'horse' , 'ship' , 'truck' )
DataSet 是一个数据集,可以按照下标访问,返回形式如(data,label)的数据。
1 2 3 4 (data,label) = trainset[100 ] print(classes[label]) show((data+1 )/2 ).resize((100 ,100 ))
DataLoader是一个可迭代的对象,它将dataset返回的每一条数据拼接成一个batch,并提供多线程加速优化和数据混洗等操作。当程序对DataSet的所有数据遍历完一遍后,相应的DataLoader 也就完成了一遍迭代。
1 2 3 4 dataiter = iter(trainloader) images,labels = dataiter.next() print(" " .join([classes[labels[j]] for j in range(4 )])) show(tv.utils.make_grid((images+1 )/2 )).resize((100 *4 ,100 ))
重新定义网络 拷贝之前定义的LeNet网络结构,修改第一个卷积层的输入通道数为3,因为CIFAR-10图片为彩色三通道图片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import torch.nn as nnimport torch.nn.functional as Fclass LeNet (nn.Module) : def __init__ (self) : super(LeNet, self).__init__() 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 ) 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 lenet = LeNet() print(lenet)
1 2 3 4 5 6 7 LeNet( (conv1): Conv2d(3, 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) )
定义损失函数(loss function)和优化器(optimizer) 损失函数,又叫目标函数,是编译一个神经网络模型必须的两个参数之一,另外一个必不可少的是优化器。 pytorch的优化器都在torch.optim
模块中。 常用的optimizer有SGD、Adam、Adadelta、Adagrad、Adamax等。 我们这里选择SGD(stochastic gradient descent),即随机梯度下降法,参数lr
为学习率,momentum
为动量因子,parameters
为需要优化的对象。
1 2 criterion = nn.CrossEntropyLoss() optimizer = optim.SGD(lenet.parameters(), lr=0.001 , momentum=0.9 )
训练网络 训练网络的步骤都是类似的,不断执行下面流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 from tensorboardX import SummaryWriterwriter = SummaryWriter('./logs/chapter2' ) _inputs, _labels = iter(trainloader).__next__() writer.add_graph(lenet,input_to_model=_inputs) torch.set_num_threads(8 ) niter=0 for epoch in range(5 ): running_loss = 0.0 for i, data in enumerate(trainloader, 0 ): inputs, labels = data if torch.cuda.is_available(): inputs = inputs.cuda() labels = labels.cuda() optimizer.zero_grad() outputs = lenet(inputs) loss = criterion(outputs, labels.long()) loss.backward() optimizer.step() running_loss += loss.item() writer.add_scalar('Train/Loss' , loss.item(), niter) niter+=1 if i % 1000 == 0 : print('[%d, %5d] loss: %.3f' % (epoch , i , running_loss / 2000 )) running_loss = 0.0 correct = 0 total = 0 for testdata in testloader: test_imgs,test_labels = testdata test_outpus = lenet(test_imgs) _, predicted = torch.max(test_outpus,1 ) total += test_labels.size(0 ) correct += (predicted.data.numpy()==test_labels.data.numpy()).sum() writer.add_scalar('Test/Accu' , correct / total, niter) print("acc" ,correct*1.0 / total) print('Finished Training' ) writer.close()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [0, 0] loss: 0.001 acc 0.1 [0, 1000] loss: 1.149 acc 0.1551 [0, 12000] loss: 0.736 acc 0.4635 [1, 12000] loss: 0.659 acc 0.5231 [2, 0] loss: 0.001 acc 0.5343 [2, 12000] loss: 0.608 acc 0.5579 [3, 0] loss: 0.001 acc 0.5717 [3, 12000] loss: 0.571 acc 0.5737 [4, 0] loss: 0.000 acc 0.5701 [4, 12000] loss: 0.527 acc 0.6094 Finished Training
经过简单的5轮训练,准确率达0.6,比随机猜测1/10的概率要大很大,看来我们的网络的确学习到了有用信息。