NLP实战高手课学习笔记(12):Pytorch的基本操作

368 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第29天,点击查看活动详情

说明

本系列博客将记录自己学习的课程:NLP实战高手课,链接为:time.geekbang.org/course/intr… 本篇为24节的课程笔记,主要介绍Pytorch中的一些基本操作。

Pytorch基础操作

Pytorch中的基本数据类型是Tensor,即张量,可以将其理解为一个多维数组的形式。它的操作类似于numpy中的一些操作,举例如下:

Tensor的创建和类型变换

直接创建

import torch

my_tensor = torch.tensor([[0.0, 1.0],[0.1, 0.2]])
print(my_tensor)

new_tensor= my_tensor.int()
print(new_tensor)

得到输出如下:

tensor([[0.0000, 1.0000],
        [0.1000, 0.2000]])
tensor([[0, 1],
        [0, 0]], dtype=torch.int32)

可以看到,Pytorch中默认的数据类型是float类型,我们可以通过.int()方法直接将其转换为整数类型。

从numpy数据创建

同时,我们也可以从一个numpy类型的数据转换得到,示例如下:

import numpy as np

np_tensor = np.array([[0.1,1.0],[1.0,0.2]])
tensor_from_np = torch.tensor(np_tensor)

print(tensor_from_np)

输出为:

tensor([[0.1000, 1.0000],
        [1.0000, 0.2000]], dtype=torch.float64)

Tensor的梯度

我们可以在创建张量时显式声明需要记录梯度,例如:

tensor_with_gradient = torch.tensor([[0.1,1.0], [1.0, 2.0]], requires_grad=True)
result =tensor_with_gradient.pow(2).sum()
result.backward()
tensor_with_gradient.grad

输出为:

tensor([[0.2000, 2.0000],
        [2.0000, 4.0000]])

这里,对该张量的所有计算操作都被记录了下来,并且使用.grad方法就可以查看梯度。

基本运算与索引

一下为几个基本运算示例:

x = torch.tensor([[0.1, 1.0],[2.0, 1.0]])
print("x + 1: ", x+1)
print("x * 2: ", x*2)

y = torch.tensor([[0.1, 2.0], [2.0, 3.0]])
print("x + y: ", x+y) 

打印结果为:

x + 1:  tensor([[1.1000, 2.0000],
        [3.0000, 2.0000]])
x * 2:  tensor([[0.2000, 2.0000],
        [4.0000, 2.0000]])
x + y:  tensor([[0.2000, 3.0000],
        [4.0000, 4.0000]])

类似于numpy中非常灵活的索引方式,Pytorch支持高效的数据索引。

print(x[:,1])

输出为:

tensor([1., 1.])

Squeeze与Unsqueeze

在某些场景下,为了保持维度的对齐,我们需要添加一个长度为1的维度或者删除中间处理过程中长度为1的维度。这时,就需要使用Pytorch中的Squeeze和Unsqueeze方法,下面来看一个例子:

print("=="*24)

x = torch.tensor([[0.1, 1.0],[2.0, 1.0]])
print("x: ", x)
print("x.shape: ", x.shape)

print("=="*24)

x = x.unsqueeze(0)
print("x: ", x)
print("x.shape: ", x.shape)

print("=="*24)

x = x.squeeze(0)
print("x: ", x)
print("x.shape: ", x.shape)

打印输出为:

================================================
x:  tensor([[0.1000, 1.0000],
        [2.0000, 1.0000]])
x.shape:  torch.Size([2, 2])
================================================
x:  tensor([[[0.1000, 1.0000],
         [2.0000, 1.0000]]])
x.shape:  torch.Size([1, 2, 2])
================================================
x:  tensor([[0.1000, 1.0000],
        [2.0000, 1.0000]])
x.shape:  torch.Size([2, 2])

我们首先定义了一个2维向量,其中每个维度的长度分为为2,2。

接着,我们使用unsqueeze方法给其在第0个维度上增添了一个长度为1的维度,反映到具体形式上就是原来的多维数组外面又“套上”了一对中括号。

最后,我们使用squeeze方法将长度为1的维度剔除掉,又变回了原来的2维数组。

einsum(爱因斯坦求和约定)

爱因斯坦求和约定是一套既简洁优雅的规则,可以很方便的实现复杂的张量操作,且过程比较直观。以下将举两个例子:

求矩阵的迹

矩阵的迹指的是矩阵对角线元素之和,其使用Einsum的方式表示如下:

# trace
result = torch.einsum('ii', torch.randn(4, 4))

求矩阵的对角线元素

可以使用如下的方式:

# diagonal
result = torch.einsum('ii->i', torch.randn(4, 4))

求向量的外积

求解向量外积时,设输入向量的维度分别为m,n,那么输出将会是一个m*n的矩阵。

# outer product
x = torch.randn(5)
y = torch.randn(4)
result = torch.einsum('i,j->ij', x, y)
print(result)

得到输出如下:

tensor([[-0.7711, -0.1712, -0.1501,  0.2474],
        [-1.3566, -0.3012, -0.2640,  0.4353],
        [-0.5856, -0.1300, -0.1140,  0.1879],
        [ 1.1502,  0.2554,  0.2239, -0.3690],
        [-1.5559, -0.3455, -0.3028,  0.4992]])

可以看到,输入分别是5,4维的向量,得到的输出为一个5*4的二维矩阵。其中第i,j个位置上的值是由向量x第i个位置的数值与向量y第j个位置的数值相乘得到的。

一些enisum的规则

以下内容参考自一文学会 Pytorch 中的 einsum .

以"ik,jk->ij"为例,在enisum表达式中存在两种索引:自由索引(Free indices)和求和索引(Summation indices):

  • 自由索引,出现在箭头右边的索引,如 i 和 j;
  • 求和索引,只出现在箭头左边的索引,表示中间计算结果需要这个维度上求和之后才能得到输出,如k;

接着是介绍三条基本规则

  1. equation 箭头左边,在不同输入之间重复出现的索引表示,把输入张量沿着该维度做乘法操作,比如还是以上面矩阵乘法为例, "ik,kj->ij",k 在输入中重复出现,所以就是把 a 和 b 沿着 k 这个维度作相乘操作;
  2. 只出现在 equation 箭头左边的索引,表示中间计算结果需要在这个维度上求和,也就是上面提到的求和索引;
  3. equation 箭头右边的索引顺序可以是任意的,比如上面的 "ik,kj->ij" 如果写成 "ik,kj->ji",那么就是返回输出结果的转置,用户只需要定义好索引的顺序,转置操作会在 einsum 内部完成。

特殊规则

  1. equation 可以不写包括箭头在内的右边部分,那么在这种情况下,输出张量的维度会根据默认规则推导。就是把输入中只出现一次的索引取出来,然后按字母表顺序排列,比如上面的矩阵乘法 "ik,kj->ij" 也可以简化为 "ik,kj",根据默认规则,输出就是 "ij" 与原来一样;
  2. equation 中支持 "..." 省略号,用于表示用户并不关心的索引。

总结

本文介绍了一些Pytorch的基本操作,其中很多基础函数大家都已经很熟悉了,但是这里讲得enisum我是第一次听说,看上去该函数使得张量的变换操作更加简洁,维度更加直观。