1.什么是张量
百科知识:“张量”一词最初由威廉·罗恩·哈密顿在1846年引入,但他把这个词用于指代现在称为模的对象。该词的现代意义是沃尔德马尔·福格特在1899年开始使用的。
这个概念由格雷戈里奥·里奇-库尔巴斯特罗在1890年在《绝对微分几何》的标题下发展出来,随着1900年列维-奇维塔的经典文章《绝对微分》(意大利文,随后出版了其他译本)的出版而为许多数学家所知。随着1915年左右爱因斯坦的广义相对论的引入,张量微积分获得了更广泛的承认。广义相对论完全由张量语言表述,爱因斯坦从列维-奇维塔本人那里学了很多张量语言(其实是Marcel
Grossman,他是爱因斯坦在苏黎世联邦理工学院的同学,一个几何学家,也是爱因斯坦在张量语言方面的良师益友 - 参看Abraham
Pais所著《上帝是微妙的(Subtle is the Lord)》),并学得很艰苦。但张量也用于其它领域,例如连续力学,譬如应变张量(参看线性弹性)。
注意“张量”一词经常用作张量场的简写,而张量场是对流形的每一点给定一个张量值。要更好的理解张量场,必须首先理解张量的基本思想。
看起来,张量是一个物理学概念,不过在这里,我们不用想的那么复杂,简单来理解,张量就是一个多维数组
,当然如果它的维度是0那就是一个数,如果维度是1那就是一个矢量,或者称作一维数组。在PyTorch中都是使用张量的概念和数据结构来进行运算的。
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))
关于命名这个操作看起来挺美好,主要是适配人阅读习惯,但是对齐它也是很困难的事情,所以这个特性或许并不怎么好用。
今天的学习就到这里了。