携手创作,共同成长!这是我参与「掘金日新计划 · 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;
接着是介绍三条基本规则:
- equation 箭头左边,在不同输入之间重复出现的索引表示,把输入张量沿着该维度做乘法操作,比如还是以上面矩阵乘法为例, "ik,kj->ij",k 在输入中重复出现,所以就是把 a 和 b 沿着 k 这个维度作相乘操作;
- 只出现在 equation 箭头左边的索引,表示中间计算结果需要在这个维度上求和,也就是上面提到的求和索引;
- equation 箭头右边的索引顺序可以是任意的,比如上面的 "ik,kj->ij" 如果写成 "ik,kj->ji",那么就是返回输出结果的转置,用户只需要定义好索引的顺序,转置操作会在 einsum 内部完成。
特殊规则
- equation 可以不写包括箭头在内的右边部分,那么在这种情况下,输出张量的维度会根据默认规则推导。就是把输入中只出现一次的索引取出来,然后按字母表顺序排列,比如上面的矩阵乘法 "ik,kj->ij" 也可以简化为 "ik,kj",根据默认规则,输出就是 "ij" 与原来一样;
- equation 中支持 "..." 省略号,用于表示用户并不关心的索引。
总结
本文介绍了一些Pytorch的基本操作,其中很多基础函数大家都已经很熟悉了,但是这里讲得enisum我是第一次听说,看上去该函数使得张量的变换操作更加简洁,维度更加直观。