2 | PyTorch张量操作:基本操作、索引、命名

1.什么是张量

百科知识:“张量”一词最初由威廉·罗恩·哈密顿在1846年引入,但他把这个词用于指代现在称为模的对象。该词的现代意义是沃尔德马尔·福格特在1899年开始使用的。
这个概念由格雷戈里奥·里奇-库尔巴斯特罗在1890年在《绝对微分几何》的标题下发展出来,随着1900年列维-奇维塔的经典文章《绝对微分》(意大利文,随后出版了其他译本)的出版而为许多数学家所知。随着1915年左右爱因斯坦的广义相对论的引入,张量微积分获得了更广泛的承认。广义相对论完全由张量语言表述,爱因斯坦从列维-奇维塔本人那里学了很多张量语言(其实是Marcel Grossman,他是爱因斯坦在苏黎世联邦理工学院的同学,一个几何学家,也是爱因斯坦在张量语言方面的良师益友 - 参看Abraham Pais所著《上帝是微妙的(Subtle is the Lord)》),并学得很艰苦。但张量也用于其它领域,例如连续力学,譬如应变张量(参看线性弹性)。
注意“张量”一词经常用作张量场的简写,而张量场是对流形的每一点给定一个张量值。要更好的理解张量场,必须首先理解张量的基本思想。

看起来,张量是一个物理学概念,不过在这里,我们不用想的那么复杂,简单来理解,张量就是一个多维数组,当然如果它的维度是0那就是一个数,如果维度是1那就是一个矢量,或者称作一维数组。在PyTorch中都是使用张量的概念和数据结构来进行运算的。

2 | PyTorch张量操作:基本操作、索引、命名_第1张图片

image.png


搞过机器学习的朋友可以知道,并不是只有PyTorch是处理多维数组的唯一库,像常用的科学计算库NumPy,都是以处理多维数组为基础的。而PyTorch可以与NumPy无缝衔接,这使得它可以很方便的与scikit-learn等库进行集成。当然,PyTorch有很多处理多维数组的大杀器,这里先不介绍了,毕竟我也是才刚开始学,到底有什么大杀器我们后面再看。

2.从列表到张量

搞过Python的应该都知道列表这个东西,也可以认为是数组,比如像下面这样定义一个列表

a = [1.0, 2.0, 3.0]
a[0] #按位置索引访问列表元素

这时候就返回其中的值1.0
张量也是很类似的,这里我们来写一下,导入torch包,调用了一个ones方法,这个方法的作用是给生成的张量内部全部赋值为1

import torch
a = torch.ones(3)
a #输出一下张量结果

结果是tensor([1., 1., 1.])
可以看到跟列表基本上没有区别,但是前面有tensor限定,表明这是一个张量元素。当然了,我理解限定张量元素主要是它还有很多各种各样的操作,要比列表丰富的多,后面应该可以学到。

尝试几个简单的操作

a[1] ### 按位置索引访问元素
out: tensor(1.)
float(a[1]) #强行转为浮点数
out: 1.0 #可以看到这个时候输出的就不带tensor限定了
a[2] = 2.0 #改变其中的元素
a #输出看看
out:tensor([1.,1.,2.]) #这里看到了,最后一个变成了2,这些操作跟列表操作基本没啥区别

3.张量的本质

书上的这一小段我没太看明白,就文字描述来说,大意是列表中的元素在实际内存的存储中使用的是随机区块,而PyTorch中的张量使用的往往是连续内存区块,这意味着如果内存中碎片较多的时候,对于比较大的tensor就没办法放进去了,当然连续内容的好处就是读写方便,运算速度快。但是在特殊情况下是否支持非连续内存块的存储呢?现在这书上没有写,后面慢慢观察。

这里还有一个代码示例。我们期望用一个tensor去存储一个三角形的三个点的坐标。
首先尝试用一维张量来存储,那就要把每个坐标拆开,然后要用脑子记住0,1位置标识第一个坐标点,2,3位置标识第二个坐标点,4,5位置标识第三个坐标点。这里用到一个zeros方法,跟ones类似,只不过它是用内部全0来初始化tensor。

points = torch.zeros(6) 
points[0] = 4.0 
points[1] = 1.0
points[2] = 5.0
points[3] = 3.0
points[4] = 2.0
points[5] = 1.0
points
outs:tensor([4., 1., 5., 3., 2., 1.])

或者我们可以用一个二维张量来标识三个点,可以看到二维张量跟列表的列表是一样的表现形式,里面会嵌套一层[],如果要三维张量就再嵌套一层[],不断嵌套,我们可以构建足够多维度的张量

points = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]])
points
outs:tensor([[4., 1.],
        [5., 3.],
        [2., 1.]])

使用shape方法查看张量的形状,这里返回的size表示这是一个三行二列的张量(数组)

points.shape
out:torch.size([3,2])

tips:当我们用索引访问张量中的元素,或者张量中的张量时,返回的是一个张量的引用,而不会分配一个新的内存,这个事情很重要,要记清楚,以后的操作什么时候需要开辟一块新的内存,什么时候不需要,不然有些bug会很难查。

4.范围索引

这个跟Python list的操作是一样的

points = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]])
points
outs:tensor([[4., 1.],
     [5., 3.],
     [2., 1.]])

points[1:] #输出第一行之后的所有行,列不做处理
outs:tensor([[5., 3.],
     [2., 1.]])

points[1:,:] #输出第一行之后的所有行,列选取全部列
outs:tensor([[5., 3.],
     [2., 1.]])

points[1:,0] #输出第一行之后的所有行,列选取第一列
outs:tensor([5., 2.])

points[None] #这个比较有意思,增加大小为1的维度,可以看到输出的时候多了一组[],也就是维度提升了1维,就像unsqueeze()方法一样
outs:tensor([[[4., 1.],
      [5., 3.],
      [2., 1.]]])

5.张量命名

最开始读这一小节的时候有点难度,但是总体而言,张量命名就是指的tensor中有个给维度命名的功能,看起来还是有点实用的,主要就是防止在张量的反复变换中,都已经搞不清哪个维度是哪个维度了。我想随着使用的深入应该能够加强对这个功能的理解。

并且我在使用张量命名的时候出现了一个提示,大意是张量命名还处于试验阶段,请不要在任何重要的代码中使用这个功能以及相关的API,可以等到推出stable版本的时候再使用。可见这个功能还不太完善,这不影响我们看看它到底实现了什么功能。

考虑我们现在有一幅彩色图像,通常都是由RGB三通道构成的,那么我们在随机生成一组数据,假装它是一幅图像的数据。这个tensor数据有三个维度,一个是channels表示rgb通道,另外两个是rows和columns表示图上点信息。另外给出一个weights,这个weights就是把

tips: PyTorch Torch.randn()返回由可变参数大小(定义输出张量的形状的整数序列)定义的张量,其中包含标准正态分布的随机数。

img_t = torch.randn(3, 5, 5) # shape [channels, rows, columns]
weights = torch.tensor([0.2126, 0.7152, 0.0722])

mean函数中的参数dim代表在第几维度求平均数。

这里有一系列的操作,比如求平均值,求加和,升维,广播,张量乘法等等,我觉得不理解倒是没啥关系,这里的核心思想就是我们需要在代码中对tensor做各种各样的变换运算,很快我们就搞不清楚到底哪个维度是哪个维度了。

img_gray_naive = img_t.mean(-3)
batch_gray_naive = batch_t.mean(-3)
img_gray_naive.shape, batch_gray_naive.shape
outs: (torch.Size([5, 5]), torch.Size([2, 5, 5]))
unsqueezed_weights = weights.unsqueeze(-1).unsqueeze_(-1)
img_weights = (img_t * unsqueezed_weights)
batch_weights = (batch_t * unsqueezed_weights)
img_gray_weighted = img_weights.sum(-3)
batch_gray_weighted = batch_weights.sum(-3)
batch_weights.shape, batch_t.shape, unsqueezed_weights.shape
outs: (torch.Size([2, 3, 5, 5]), torch.Size([2, 3, 5, 5]), torch.Size([3, 1, 1]))

爱因斯坦求和约定(einsum)提供了一套既简洁又优雅的规则,可实现包括但不限于:向量内积,向量外积,矩阵乘法,转置和张量收缩(tensor contraction)等张量操作,熟练运用 einsum 可以很方便的实现复杂的张量操作,而且不容易出错。

img_gray_weighted_fancy = torch.einsum('...chw,c->...hw', img_t, weights)
batch_gray_weighted_fancy = torch.einsum('...chw,c->...hw', batch_t, weights)
batch_gray_weighted_fancy.shape
outs: torch.Size([2, 5, 5])

这个时候拿出我们的命名操作,可以看到在给weights_named赋值的时候,后面加了一个names参数

weights_named = torch.tensor([0.2126, 0.7152, 0.0722], names=['channels'])
weights_named
outs: tensor([0.2126, 0.7152, 0.0722], names=('channels',))

接下来是一些命名相关的操作,比如说给三个维度同时命名,前面省略号就是表明省略,这里是为倒数的三个维度命名

img_named =  img_t.refine_names(..., 'channels', 'rows', 'columns')
batch_named = batch_t.refine_names(..., 'channels', 'rows', 'columns')
print("img named:", img_named.shape, img_named.names)
print("batch named:", batch_named.shape, batch_named.names)
outs: img named: torch.Size([3, 5, 5]) ('channels', 'rows', 'columns')
batch named: torch.Size([2, 3, 5, 5]) (None, 'channels', 'rows', 'columns')

需要注意的是,已经带有名称的维度在运算的时候需要使用相同的维度名称,否则会导致错误,比如前面的mean操作,sum操作等等

weights_aligned = weights_named.align_as(img_named) #这两行代码我还没太看明白
weights_aligned.shape, weights_aligned.names
outs: (torch.Size([3, 1, 1]), ('channels', 'rows', 'columns'))

gray_named = (img_named * weights_aligned).sum('channels')
gray_named.shape, gray_named.names
outs: (torch.Size([5, 5]), ('rows', 'columns'))

try:
    gray_named = (img_named[..., :3] * weights_named).sum('channels') #这里尝试对不同维度名称的tensor进行运算,结果得到了一个错误
except Exception as e:
    print(e)
try:
    gray_named = (img_named[..., :3] * weights_named).sum('channels')
except Exception as e:
    print(e)
outs: Error when attempting to broadcast dims ['channels', 'rows', 'columns'] and dims ['channels']: dim 'columns' and dim 'channels' are at the same position from the right but do not match.

如果不想要名字或者换个名字可以用rename操作

gray_plain = gray_named.rename(None)
gray_plain.shape, gray_plain.names
outs: (torch.Size([5, 5]), (None, None))

关于命名这个操作看起来挺美好,主要是适配人阅读习惯,但是对齐它也是很困难的事情,所以这个特性或许并不怎么好用。

今天的学习就到这里了。

 

你可能感兴趣的