原标题:实战 | 手把手教你用PyTorch实现图像描述(附完整代码)
作者 | 李理
环信人工智能研发中心 VP,十多年自然语言处理和人工智能研发经验。主持研发过多款智能硬件的问答和对话系统,负责环信中文语义分析开放平台和环信智能机器人的设计与研发。
想要详细了解该系列文章,营长建议你先阅读上篇:一文详解循环神经网络的基本概念(代码版)
Tensor
和TensorFlow 类似,PyTorch 的核心对象也是Tensor。下面是创建Tensor 的代码:
x = torch.Tensor(5, 3)
print(x)
对应的下标是5,那么在这个下标的值为1,而其余的值为0,因此一个词只有一个位置不为0,所以叫作one-hot 的表示方法。这种表示方法的缺点是它是一种“稀疏”的表示方法,两个词,不论语义是相似还是不同,都无法通过这个向量表示出来。比如我们计算两个向量的内积,相同的词内积为1(表示相似度很高);而不同的词为0(表示完全不同)。但实际我们希望“猫”和“狗”的相似度要高于“猫”和“石头”,使用one-hot 就无法表示出来。
Word Embedding 的思想是把高维的稀疏向量映射到一个低维的稠密向量,要求是两个相似的词会映射到低维空间里距离比较近的两个点;而不相似的距离较远。我们可以这样来“理解”这个低维的向量——假设语义可以用n 个基本的“正交”的“原子”语义表示的话,那么向量的不同的维代表这个词在这个原子语义上的“多少”。
当然这只是一种假设,但实际这个语义空间是否存在,或者即使存在也可能和人类理解的不同,但是只要能达到前面的要求——相似的词的距离近而不相似的远,也就可以了。
举例来说,假设向量的第一维表示动物,那么猫和狗应该在这个维度上有较大的值,而石头应该较小。
Embedding 一般有两种方式得到,一种是通过与任务无直接关系的无监督任务中学习,比如早期的RNN 语言模型,它的一个副产品就是Word Embedding,包括后来的专门Embedding 方法如Word to Vector 或者GloVe 等,本书后面的章节会详细介绍。另外一种方式就是在当前任务中让它自己学习出最合适的Word Embedding来。前一种方法的好处是可以利用海量的无监督数据,但是由于领域有差别以及它不是针对具体任务的最优化表示,它的效果可能不会很好;而后一种方法它针对当前任务学习出最优的表示(和模型的参数配合),但是它需要海量的训练数据,这对很多任务来说是无法满足的条件。在实践中,如果领域的数据非常少,我们可能直接用在其它任务中Pretraining 的Embedding 并且fix 住它;而如果领域数据较多的时候我们会用Pretraining 的Embedding 作为初始值,然后用领域数据驱动它进行微调。
PyTorch 基础知识
▌Tensor
和TensorFlow 类似,PyTorch 的核心对象也是Tensor。下面是创建Tensor 的代码:
x = torch.Tensor(5, 3)
print(x)
输出:
0.24550.15160.5319
0.98660.99180.0626
0.01720.64710.1756
0.89640.73120.9922
0.62640.01900.0041
[torch.FloatTensor of size 5x3]
我们可以得到Tensor 的大小:
print(x.size())
输出:
torch.Size([5, 3])
▌Operation
和TensorFlow 一样,有了Tensor 之后就可以用Operation 进行计算了。但是和TensorFlow 不同,TensorFlow 只是定义计算图但不会立即“执行”,而Pytorch 的Operation 是马上“执行”的。所以PyTorch 使用起来更加简单,当然PyTorch 也有计算图的执行引擎,但是它不对用户可见,它是“动态”编译的。
首先是加分操作:
y = torch.rand(5, 3)
print(x + y)
上面的加法会产生一个新的Tensor,但是我们可以提前申请一个Tensor 来存储Operation 的结果:
result = torch.Tensor(5, 3)
torch.add(x, y, out=result)
print(result)
也可以in-place 的修改:
# adds x to y
y.add_(x)
print(y)
一般来说,如果一个方法已_ 结尾,那么这个方法一般来说就是in-place 的函数。
PyTorch 支持numpy 的索引操作,比如取第一列:
print(x[:, 1])
我们也可以用view 来修改Tensor 的shape,注意view 要求新的Tensor 的元素个数和原来是一样的。
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())
输出:torch.Size([4, 4]) torch.Size([16]) torch.Size([2, 8])
▌numpy ndarray 的转换
我们可以很方便的把Tensor 转换成numpy 的ndarray 或者转换回来,注意它们是共享内存的,修改Tensor 会影响numpy 的ndarray,反之亦然。
Tensor 转numpy
a = torch.ones(5)
b = a.numpy()
a.add_(1) # 修改a会影响b
numpy 转Tensor
import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
np.add(a, 1, out=a) # 修改a会影响b
▌CUDA Tensor
Tensor 可以移到GPU 上用GPU 来加速计算:
# let us run this cell only if CUDA is available
if torch.cuda.is_available():
x = x.cuda()
y = y.cuda()
x + y 在GPU上计算
图5.18: PyTorch 的变量
▌Autograd
autograd 是PyTorch 核心的包,用于实现前面我们提到的自动梯度算法。首先我们介绍其中的变量。
▌Variable
autograd.Variable 是Tensor 的封装,我们定义(也就计算)好了最终的变量(一般是Loss) 后,我们可以调用它的backward() 方法,PyTorch 就会自动的计算好梯度。如图5.18所示,PyTorch 的变量值会存储到data 里,而梯度值会存放到grad 里,此外还有一个grad_fn,它是用来计算梯度的函数。除了用户创建的Tensor 之外,通过Operatioon 创建的变量会记住它依赖的变量,从而形成一个有向无环图。计算这个变量的梯度的时候会自动计算它依赖的变量的梯度。
我们可以这样定义变量,参数requires_grad 说明这个变量是否参与计算梯度:
x= Variable(torch.ones(2, 2), requires_grad=True)
▌Gradient
我们可以用backward() 来计算梯度,它等价于variable.backward(torch.Tensor([1.0])),梯度会往后往前传递,最后的变量一般传递的就是1,然后往前计算梯度的时候会把之前的值累积起来,PyTorch 会自动处理这些东西,我们不需要考虑。
x = Variable(torch.ones(2, 2), requires_grad=True)
y=x+2
z = y * y * 3
out = z.mean()
out.backward() # 计算所有的dout/dz,dout/dy,dout/dx
print(x.grad) # x.grad就是dout/dx
输出为:
4.5000 4.5000
4.5000 4.5000
[torch.FloatTensor of size 2x2]
我们手动来验证一下:
注意每次调用backward() 都会计算梯度然后累加到原来的值之上,所以如果每次计算梯度之前要调用变量的zero_grad() 函数。
▌变量的requires_grad 和volatile
每个变量有两个flag:requires_grad 和volatile,它们可以细粒度的控制一个计算图的某个子图不需要计算梯度,从而可以提高计算速度。一个Operation 如果所有的输入都不需要计算梯度(requires_grad==False),那么这个Operation 的requires_grad就是False,而只要有一个输入,那么这个Operation 就需要计算梯度。比如下面的代码片段:
>>> x = Variable(torch.randn(5, 5))
>>> y = Variable(torch.randn(5, 5))
>>> z = Variable(torch.randn(5, 5), requires_grad=True)
>>> a = x + y
>>> a.requires_grad
False
>>> b = a + z
>>> b.requires_grad
True
如果你想固定住模型的某些参数,或者你知道某些参数的梯度不会被用到,那么就可以把它们的requires_grad 设置成False。比如我们想细调(fine-tuing) 预先训练好的一个CNN,我们会固定所有最后全连接层之前的卷积池化层参数,我们可以这样:
model = torchvision.models.resnet18(pretrained=True)
for param in model.parameters():
param.requires_grad = False
# 把最后一个全连接层替换成新构造的全连接层
# 默认的情况下,新构造的模块的requires_grad=True
model.fc = nn.Linear(512, 100)
# 优化器只调整新构造的全连接层的参数。
optimizer = optim.SGD(model.fc.parameters(), lr=1e-2, momentum=0.9)
volatile 在0.4.0 之后的版本以及deprecated 了,不过我们后面的代码会用到它之前的版本,因此还是需要了解一下。它适用于预测的场景,在这里完全不需要调用backward() 函数。它比requires_grad 更加高效,而且如果volatile 是True,那么它会强制requires_grad 也是True。它和requires_grad 的区别在于:如果一个Operation的所有输入的requires_grad 都是False 的时候,这个Operation 的requires_grad 才是False,这时这个Operation 就不参与梯度的计算;而如果一个Operation 的一个输入是volatile 是True,那么这个Operation 的volatile 就是True 了,那么这个Operation 就不参与梯度的计算了。因此它很适合的预测场景时:不修改模型的任何定义,只是把输入变量(的一个)设置成volatile,那么计算forward 的时候就不会保留任何用于backward 的中间结果,这样就会极大的提高预测的速度。下面是示例代码:
>>>regular_input = Variable(torch.randn(1, 3, 227, 227))
>>>volatile_inpu