深度学习框架很多,TensorFlow 算是比较流行的了,但静态图有时候确实写的很难受,以及一些比较复杂的 model 实现起来比较蛋疼;最近在看的文章的实现没有 TensorFlow 版本,起初我想改写一个 TensorFlow 版本出来,结果实现不了,一方面可能我确实比较菜,另一方面,也只能说 TensorFlow 有些东西在设计上让人为难。所以,那就试试 PyTorch 咯。这篇文章主要是记录我学习 PyTorch 官方 60 分钟教程的过程。
Tensor
逃不开的还是 Tensor 这个对象,PyTorch 创建、操作 Tensor 的方法和 numpy 很类似,据说 Torch 一开始就是 Numpy 的 GPU 加速版,简单列些一些 API:
1 | x = torch.empty(5, 3) # 创建一个未初始化的 5x3 tensor |
比较有意思的是,创建 Tensor 的时候 size 参数不是一个 tuple
e.g. (5, 3)
,而是用 *args
的方式传入。
常用的算术操作可以通过 x+y
或者是 torch.add(x, y)
来实现,同时 PyTorch 也提供了就地 (in place) 的操作:
1 | y.add_(x) # equals y = y + x |
所有的操作名 + 下划线都是一个就地操作,x.copy_(y)
会用 y
的值替代x
。
另外,对于单元素的 Tensor,可以通过使用 .item()
来获取它;TensorFlow 中的 reshape()
对应的是 view()
,并且,和 numpy 一样,Tensor 支持切片操作:
1 | x = torch.randn(4, 4) # x -> 4x4 |
既然之前说 Torch 是 Numpy 的一个 GPU 版本,也就支持 ndarray 和 Tensor 的相互转化(这就比 TensorFlow 用起来舒服很多),可以用 Tensor.numpy()
获取到对应的 ndarray,逆操作则是 torch.from_numpy()
:
1 | # example 1 tensor -> numpy |
上面这个例子很重要,说明了ndarray 和 Tensor 对象之间的转换是一种引用关系,而非创建新的对象。也就是说,任何一个相关对象的变化都会影响到另一方,就像例子 1 中,我们对原始的 a
进行 add_(1)
,在此之前得到的 b
对象的值也改变了,例子二中同样出现了这样的情况,这一点需要特别注意。
CUDA Tensors
我们可以把 Tensor 放到 GPU 上来加速计算,而 PyTorch 为此提供了很方便的方式:
1 | if torch.cuda.is_available(): # 如果 CUDA 可用 |
如上面的例子所示,我们可以在创建 Tensor 对象时指定 device
参数来指明 Tensor 创建的位置,也可以再创建了之后使用 .to()
方法来进行双向的迁移:既可以从 CPU 到 GPU(例子中的 x.to(device)
,也可以从 GPU 到 CPU(z.to("cpu", torch.double)
)。对比 TensorFlow :
1 | with tf.device('/cpu:0'): |
就我所知,TensorFlow 只能在创建的时候指明位置,而无法像 PyTorch 这般灵活的迁移。
Autograd
自动求导已经是深度学习框架的必备了,首先,我们可以通过设置 Tensor 的 .requires_grad
参数来指明 Tensor 是否参与梯度运算,这和 TensorFlow 中的 trainable
是类似的;接着我们通过对某个对象(比如 loss )函数进行 .backward()
操作,来进行反向梯度的计算,并且能够通过计算链上对象的 .grad
属性来获取到对应的梯度。拿官方的例子做一个简单的说明:
1 | x = torch.ones(2, 2, requires_grad=True) # 声明 x,指明需要参加梯度的计算 x: 2x2 全 1 |
梯度计算的过程如下:
o=14∑izi
zi=3(xi+2)2
zi|xi=1=27
∂o∂xi=32(xi+2)
∂o∂xi|xi=1=92=4.5
因此,x.grad
的值在我们对 out
进行 out.backward()
操作之后,结果为:
tensor([[ 4.5000, 4.5000],
[ 4.5000, 4.5000]])
backward()
函数的原型如下:
torch.autograd.backward(tensors, grad_tensors=None, retain_graph=None, create_graph=False, grad_variables=None)
其中第一个参数用来声明梯度的权重。如果目标函数是一个标量,则可以不传这个参数,比如上面的 out.backward()
,则默认权重为 1
,如果我们改成 out.backward(torch.tensor(2,dtype=torch.float))
则各个 xi
Build A Neural Network
学框架不搭一个 NN 来玩 MNIST 或者 CIFAR 怎么能叫入门呢?来来来,走一波~
1 | class Net(nn.Module): |
诶,有没有觉得有点像 Keras,不过高级的 API 用起来就是爽,不用写那么多代码2333,接下来就是定义 loss 和进行 backprop 来更新参数:
1 | import torch.optim as optim |
和 TensorFlow 的区别就在于,TensorFlow 的话只要我们用 session.run(optimizer)
就可以完成梯度的更新操作,而 PyTorch 则需要:
- Optimizer 清零梯度
- 对 loss 进行手动的
backward()
操作 - Optimizer 更新
.step()
三步走,记住了没有!
还有一点比较方便的是,和 Tensor 类似,我们可以使用 Net.to()
来把模型部署到 GPU 上:
1 | net.to("cuda:0") |
但是!记得要把你的输入也放到 GPU 上,不然就会报错:
1 | inputs, labels = inputs.to(device), labels.to(device) # 把 input 和 label 同样迁移到 GPU 上 |
Summary
初体验如果要给个评价的话,我觉得是比 TensorFlow 好很多(毕竟 TensorFlow 一上来的 Session、静态图会让人有点摸不着头脑)。但用什么框架其实都无所谓,就和语言一样,虽然争来争去,但每种语言都有自己的用武之地,以前我还会和室友争辩 Java 和 Python 谁才是最好的语言,现在就不会了(因为我也觉得 Python 好写一点,逃)。框架更不用说,TensorFlow 有他应用的工业场景(希望能早日接触到),PyTorch 现在看来也许更适合需要快速实现原型的科研人员。而我们能做的,就是多接触,横向比较着来看而不要因为自己擅长而蒙蔽了双眼,多学一技压身,总是没错的。