返回介绍

3.1 Tensor

发布于 2024-01-28 10:27:20 字数 36462 浏览 0 评论 0 收藏 0

Tensor,又名张量,读者可能对这个名词似曾相识,因它不仅在 PyTorch 中出现过,它也是 Theano、TensorFlow、 Torch 和 MxNet 中重要的数据结构。关于张量的本质不乏深度的剖析,但从工程角度来讲,可简单地认为它就是一个数组,且支持高效的科学计算。它可以是一个数(标量)、一维数组(向量)、二维数组(矩阵)和更高维的数组(高阶数据)。Tensor 和 Numpy 的 ndarrays 类似,但 PyTorch 的 tensor 支持 GPU 加速。

本节将系统讲解 tensor 的使用,力求面面俱到,但不会涉及每个函数。对于更多函数及其用法,读者可通过在 IPython/Notebook 中使用函数名加 ? 查看帮助文档,或查阅 PyTorch 官方文档 ^1

# Let's begin
from __future__ import print_function
import torch  as t
t.__version__
'0.3.0.post4'

3.1.1 基础操作

学习过 Numpy 的读者会对本节内容感到非常熟悉,因 tensor 的接口有意设计成与 Numpy 类似,以方便用户使用。但不熟悉 Numpy 也没关系,本节内容并不要求先掌握 Numpy。

从接口的角度来讲,对 tensor 的操作可分为两类:

  1. torch.function ,如 torch.save 等。
  2. 另一类是 tensor.function ,如 tensor.view 等。

为方便使用,对 tensor 的大部分操作同时支持这两类接口,在本书中不做具体区分,如 torch.sum (torch.sum(a, b))tensor.sum (a.sum(b)) 功能等价。

而从存储的角度来讲,对 tensor 的操作又可分为两类:

  1. 不会修改自身的数据,如 a.add(b) , 加法的结果会返回一个新的 tensor。
  2. 会修改自身的数据,如 a.add_(b) , 加法的结果仍存储在 a 中,a 被修改了。

函数名以 _ 结尾的都是 inplace 方式, 即会修改调用者自己的数据,在实际应用中需加以区分。

创建 Tensor

在 PyTorch 中新建 tensor 的方法有很多,具体如表 3-1 所示。

表 3-1: 常见新建 tensor 的方法

函数功能
Tensor(*sizes)基础构造函数
ones(*sizes)全 1Tensor
zeros(*sizes)全 0Tensor
eye(*sizes)对角线为 1,其他为 0
arange(s,e,step从 s 到 e,步长为 step
linspace(s,e,steps)从 s 到 e,均匀切分成 steps 份
rand/randn(*sizes)均匀/标准分布
normal(mean,std)/uniform(from,to)正态分布/均匀分布
randperm(m)随机排列

其中使用 Tensor 函数新建 tensor 是最复杂多变的方式,它既可以接收一个 list,并根据 list 的数据新建 tensor,也能根据指定的形状新建 tensor,还能传入其他的 tensor,下面举几个例子。

# 指定 tensor 的形状
a = t.Tensor(2, 3)
a # 数值取决于内存空间的状态
 1.0739e+26  4.5632e-41  1.7047e-37
 0.0000e+00  4.4842e-44  0.0000e+00
[torch.FloatTensor of size 2x3]
# 用 list 的数据创建 tensor
b = t.Tensor([[1,2,3],[4,5,6]])
b
 1  2  3
 4  5  6
[torch.FloatTensor of size 2x3]
b.tolist() # 把 tensor 转为 list
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]

tensor.size() 返回 torch.Size 对象,它是 tuple 的子类,但其使用方式与 tuple 略有区别

b_size = b.size()
b_size
torch.Size([2, 3])
b.numel() # b 中元素总个数,2*3,等价于 b.nelement()
6
# 创建一个和 b 形状一样的 tensor
c = t.Tensor(b_size)
# 创建一个元素为 2 和 3 的 tensor
d = t.Tensor((2, 3))
c, d
(
  1.0739e+26  4.5632e-41  2.1006e-37
  0.0000e+00  4.4842e-44  0.0000e+00
 [torch.FloatTensor of size 2x3], 
  2
  3
 [torch.FloatTensor of size 2])

除了 tensor.size() ,还可以利用 tensor.shape 直接查看 tensor 的形状, tensor.shape 等价于 tensor.size()

c.shape
torch.Size([2, 3])
c.shape??

需要注意的是, t.Tensor(*sizes) 创建 tensor 时,系统不会马上分配空间,只是会计算剩余的内存是否足够使用,使用到 tensor 时才会分配,而其它操作都是在创建完 tensor 之后马上进行空间分配。其它常用的创建 tensor 的方法举例如下。

t.ones(2, 3)
 1  1  1
 1  1  1
[torch.FloatTensor of size 2x3]
t.zeros(2, 3)
 0  0  0
 0  0  0
[torch.FloatTensor of size 2x3]
t.arange(1, 6, 2)
 1
 3
 5
[torch.FloatTensor of size 3]
t.linspace(1, 10, 3)
  1.0000
  5.5000
 10.0000
[torch.FloatTensor of size 3]
t.randn(2, 3)
 0.0015 -0.0256 -2.2059
-1.0305 -0.2663  0.6902
[torch.FloatTensor of size 2x3]
t.randperm(5) # 长度为 5 的随机排列
 4
 3
 0
 1
 2
[torch.LongTensor of size 5]
t.eye(2, 3) # 对角线为 1, 不要求行列数一致
 1  0  0
 0  1  0
[torch.FloatTensor of size 2x3]

常用 Tensor 操作

通过 tensor.view 方法可以调整 tensor 的形状,但必须保证调整前后元素总数一致。 view 不会修改自身的数据,返回的新 tensor 与源 tensor 共享内存,也即更改其中的一个,另外一个也会跟着改变。在实际应用中可能经常需要添加或减少某一维度,这时候 squeezeunsqueeze 两个函数就派上用场了。

a = t.arange(0, 6)
a.view(2, 3)
 0  1  2
 3  4  5
[torch.FloatTensor of size 2x3]
b = a.view(-1, 3) # 当某一维为-1 的时候,会自动计算它的大小
b
 0  1  2
 3  4  5
[torch.FloatTensor of size 2x3]
b.unsqueeze(1) # 注意形状,在第 1 维(下标从 0 开始)上增加“1”
(0 ,.,.) = 
  0  1  2

(1 ,.,.) = 
  3  4  5
[torch.FloatTensor of size 2x1x3]
b.unsqueeze(-2) # -2 表示倒数第二个维度
(0 ,.,.) = 
  0  1  2

(1 ,.,.) = 
  3  4  5
[torch.FloatTensor of size 2x1x3]
c = b.view(1, 1, 1, 2, 3)
c.squeeze(0) # 压缩第 0 维的“1”
(0 ,0 ,.,.) = 
  0  1  2
  3  4  5
[torch.FloatTensor of size 1x1x2x3]
c.squeeze() # 把所有维度为“1”的压缩
 0  1  2
 3  4  5
[torch.FloatTensor of size 2x3]
a[1] = 100
b # a 修改,b 作为 view 之后的,也会跟着修改
   0  100    2
   3    4    5
[torch.FloatTensor of size 2x3]

resize 是另一种可用来调整 size 的方法,但与 view 不同,它可以修改 tensor 的大小。如果新大小超过了原大小,会自动分配新的内存空间,而如果新大小小于原大小,则之前的数据依旧会被保存,看一个例子。

b.resize_(1, 3)
b
   0  100    2
[torch.FloatTensor of size 1x3]
b.resize_(3, 3) # 旧的数据依旧保存着,多出的大小会分配新空间
b
 0.0000e+00  1.0000e+02  2.0000e+00
 3.0000e+00  4.0000e+00  5.0000e+00
 1.5301e-38  0.0000e+00  1.3768e+26
[torch.FloatTensor of size 3x3]

索引操作

Tensor 支持与 numpy.ndarray 类似的索引操作,语法上也类似,下面通过一些例子,讲解常用的索引操作。如无特殊说明,索引出来的结果与原 tensor 共享内存,也即修改一个,另一个会跟着修改。

a = t.randn(3, 4)
a
-2.1098 -1.4390 -1.4180  0.1874
 0.3988  0.4784 -0.9994  1.0953
-0.3281 -0.8193  0.9801 -1.1096
[torch.FloatTensor of size 3x4]
a[0] # 第 0 行(下标从 0 开始)
-2.1098
-1.4390
-1.4180
 0.1874
[torch.FloatTensor of size 4]
a[:, 0] # 第 0 列
-2.1098
 0.3988
-0.3281
[torch.FloatTensor of size 3]
a[0][2] # 第 0 行第 2 个元素,等价于 a[0, 2]
-1.4179892539978027
a[0, -1] # 第 0 行最后一个元素
0.18744279444217682
a[:2] # 前两行
-2.1098 -1.4390 -1.4180  0.1874
 0.3988  0.4784 -0.9994  1.0953
[torch.FloatTensor of size 2x4]
a[:2, 0:2] # 前两行,第 0,1 列
-2.1098 -1.4390
 0.3988  0.4784
[torch.FloatTensor of size 2x2]
print(a[0:1, :2]) # 第 0 行,前两列 
print(a[0, :2]) # 注意两者的区别:形状不同
-2.1098 -1.4390
[torch.FloatTensor of size 1x2]


-2.1098
-1.4390
[torch.FloatTensor of size 2]
a > 1 # 返回一个 ByteTensor
 0  0  0  0
 0  0  0  1
 0  0  0  0
[torch.ByteTensor of size 3x4]
a[a>1] # 等价于 a.masked_select(a>1)
# 选择结果与原 tensor 不共享内存空间
 1.0953
[torch.FloatTensor of size 1]
a[t.LongTensor([0,1])] # 第 0 行和第 1 行
-2.1098 -1.4390 -1.4180  0.1874
 0.3988  0.4784 -0.9994  1.0953
[torch.FloatTensor of size 2x4]

其它常用的选择函数如表 3-2 所示。

表 3-2 常用的选择函数

函数功能
index_select(input, dim, index)在指定维度 dim 上选取,比如选取某些行、某些列
masked_select(input, mask)例子如上,a[a>0],使用 ByteTensor 进行选取
non_zero(input)非 0 元素的下标
gather(input, dim, index)根据 index,在 dim 维度上选取数据,输出的 size 与 index 一样

gather 是一个比较复杂的操作,对一个 2 维 tensor,输出的每个元素如下:

out[i][j] = input[index[i][j]][j]  # dim=0
out[i][j] = input[i][index[i][j]]  # dim=1

三维 tensor 的 gather 操作同理,下面举几个例子。

a = t.arange(0, 16).view(4, 4)
a
  0   1   2   3
  4   5   6   7
  8   9  10  11
 12  13  14  15
[torch.FloatTensor of size 4x4]
# 选取对角线的元素
index = t.LongTensor([[0,1,2,3]])
a.gather(0, index)
  0   5  10  15
[torch.FloatTensor of size 1x4]
# 选取反对角线上的元素
index = t.LongTensor([[3,2,1,0]]).t()
a.gather(1, index)
  3
  6
  9
 12
[torch.FloatTensor of size 4x1]
# 选取反对角线上的元素,注意与上面的不同
index = t.LongTensor([[3,2,1,0]])
a.gather(0, index)
 12   9   6   3
[torch.FloatTensor of size 1x4]
# 选取两个对角线上的元素
index = t.LongTensor([[0,1,2,3],[3,2,1,0]]).t()
b = a.gather(1, index)
b
  0   3
  5   6
 10   9
 15  12
[torch.FloatTensor of size 4x2]

gather 相对应的逆操作是 scatter_gather 把数据从 input 中按 index 取出,而 scatter_ 是把取出的数据再放回去。注意 scatter_ 函数是 inplace 操作。

out = input.gather(dim, index)
-->近似逆操作
out = Tensor()
out.scatter_(dim, index)
# 把两个对角线元素放回去到指定位置
c = t.zeros(4,4)
c.scatter_(1, index, b)
  0   0   0   3
  0   5   6   0
  0   9  10   0
 12   0   0  15
[torch.FloatTensor of size 4x4]

高级索引

PyTorch 在 0.2 版本中完善了索引操作,目前已经支持绝大多数 numpy 的高级索引[^10]。高级索引可以看成是普通索引操作的扩展,但是高级索引操作的结果一般不和原始的 Tensor 贡献内出。

x = t.arange(0,27).view(3,3,3)
x
(0 ,.,.) = 
   0   1   2
   3   4   5
   6   7   8

(1 ,.,.) = 
   9  10  11
  12  13  14
  15  16  17

(2 ,.,.) = 
  18  19  20
  21  22  23
  24  25  26
[torch.FloatTensor of size 3x3x3]
x[[1, 2], [1, 2], [2, 0]] # x[1,1,2]和 x[2,2,0]
 14
 24
[torch.FloatTensor of size 2]
x[[2, 1, 0], [0], [1]] # x[2,0,1],x[1,0,1],x[0,0,1]
 19
 10
  1
[torch.FloatTensor of size 3]
x[[0, 2], ...] # x[0] 和 x[2]
(0 ,.,.) = 
   0   1   2
   3   4   5
   6   7   8

(1 ,.,.) = 
  18  19  20
  21  22  23
  24  25  26
[torch.FloatTensor of size 2x3x3]

Tensor 类型

Tensor 有不同的数据类型,如表 3-3 所示,每种类型分别对应有 CPU 和 GPU 版本(HalfTensor 除外)。默认的 tensor 是 FloatTensor,可通过 t.set_default_tensor_type 来修改默认 tensor 类型(如果默认类型为 GPU tensor,则所有操作都将在 GPU 上进行)。Tensor 的类型对分析内存占用很有帮助。例如对于一个 size 为(1000, 1000, 1000) 的 FloatTensor,它有 1000*1000*1000=10^9 个元素,每个元素占 32bit/8 = 4Byte 内存,所以共占大约 4GB 内存/显存。HalfTensor 是专门为 GPU 版本设计的,同样的元素个数,显存占用只有 FloatTensor 的一半,所以可以极大缓解 GPU 显存不足的问题,但由于 HalfTensor 所能表示的数值大小和精度有限 ^2 ,所以可能出现溢出等问题。

表 3-3: tensor 数据类型

数据类型CPU tensorGPU tensor
32-bit 浮点torch.FloatTensortorch.cuda.FloatTensor
64-bit 浮点torch.DoubleTensortorch.cuda.DoubleTensor
16-bit 半精度浮点N/Atorch.cuda.HalfTensor
8-bit 无符号整形(0~255)torch.ByteTensortorch.cuda.ByteTensor
8-bit 有符号整形(-128~127)torch.CharTensortorch.cuda.CharTensor
16-bit 有符号整形torch.ShortTensortorch.cuda.ShortTensor
32-bit 有符号整形torch.IntTensortorch.cuda.IntTensor
64-bit 有符号整形torch.LongTensortorch.cuda.LongTensor

各数据类型之间可以互相转换, type(new_type) 是通用的做法,同时还有 floatlonghalf 等快捷方法。CPU tensor 与 GPU tensor 之间的互相转换通过 tensor.cudatensor.cpu 方法实现。Tensor 还有一个 new 方法,用法与 t.Tensor 一样,会调用该 tensor 对应类型的构造函数,生成与当前 tensor 类型一致的 tensor。

# 设置默认 tensor,注意参数是字符串
t.set_default_tensor_type('torch.IntTensor')
a = t.Tensor(2,3)
a # 现在 a 是 IntTensor
 1.7900e+09  3.2564e+04  4.3056e+07
 0.0000e+00  3.2000e+01  0.0000e+00
[torch.IntTensor of size 2x3]
# 把 a 转成 FloatTensor,等价于 b=a.type(t.FloatTensor)
b = a.float() 
b
 1.7900e+09  3.2564e+04  4.3056e+07
 0.0000e+00  3.2000e+01  0.0000e+00
[torch.FloatTensor of size 2x3]
c = a.type_as(b)
c
 1.7900e+09  3.2564e+04  4.3056e+07
 0.0000e+00  3.2000e+01  0.0000e+00
[torch.FloatTensor of size 2x3]
d = a.new(2,3) # 等价于 torch.IntTensor(2,3)
d
 1.7900e+09  3.2564e+04  4.3020e+07
 0.0000e+00  2.1139e+09  3.2563e+04
[torch.IntTensor of size 2x3]
# 查看函数 new 的源码
a.new??
# 恢复之前的默认设置
t.set_default_tensor_type('torch.FloatTensor')

逐元素操作

这部分操作会对 tensor 的每一个元素(point-wise,又名 element-wise) 进行操作,此类操作的输入与输出形状一致。常用的操作如表 3-4 所示。

表 3-4: 常见的逐元素操作

函数功能
abs/sqrt/div/exp/fmod/log/pow..绝对值/平方根/除法/指数/求余/求幂..
cos/sin/asin/atan2/cosh..相关三角函数
ceil/round/floor/trunc上取整/四舍五入/下取整/只保留整数部分
clamp(input, min, max)超过 min 和 max 部分截断
sigmod/tanh..激活函数

对于很多操作,例如 div、mul、pow、fmod 等,PyTorch 都实现了运算符重载,所以可以直接使用运算符。如 a ** 2 等价于 torch.pow(a,2) , a * 2 等价于 torch.mul(a,2)

其中 clamp(x, min, max) 的输出满足以下公式: $$ yi = \begin{cases} min, & \text{if } xi \lt min \ xi, & \text{if } min \le xi \le max \ max, & \text{if } x_i \gt max\ \end{cases} $$ clamp 常用在某些需要比较大小的地方,如取一个 tensor 的每个元素与另一个数的较大值。

a = t.arange(0, 6).view(2, 3)
t.cos(a)
 1.0000  0.5403 -0.4161
-0.9900 -0.6536  0.2837
[torch.FloatTensor of size 2x3]
a % 3 # 等价于 t.fmod(a, 3)
 0  1  2
 0  1  2
[torch.FloatTensor of size 2x3]
a ** 2 # 等价于 t.pow(a, 2)
  0   1   4
  9  16  25
[torch.FloatTensor of size 2x3]
# 取 a 中的每一个元素与 3 相比较大的一个 (小于 3 的截断成 3)
print(a)
t.clamp(a, min=3)
 0  1  2
 3  4  5
[torch.FloatTensor of size 2x3]
 3  3  3
 3  4  5
[torch.FloatTensor of size 2x3]

归并操作

此类操作会使输出形状小于输入形状,并可以沿着某一维度进行指定操作。如加法 sum ,既可以计算整个 tensor 的和,也可以计算 tensor 中每一行或每一列的和。常用的归并操作如表 3-5 所示。

表 3-5: 常用归并操作

函数功能
mean/sum/median/mode均值/和/中位数/众数
norm/dist范数/距离
std/var标准差/方差
cumsum/cumprod累加/累乘

以上大多数函数都有一个参数dim,用来指定这些操作是在哪个维度上执行的。关于 dim(对应于 Numpy 中的 axis) 的解释众说纷纭,这里提供一个简单的记忆方式:

假设输入的形状是(m, n, k)

  • 如果指定 dim=0,输出的形状就是(1, n, k) 或者(n, k)
  • 如果指定 dim=1,输出的形状就是(m, 1, k) 或者(m, k)
  • 如果指定 dim=2,输出的形状就是(m, n, 1) 或者(m, n)

size 中是否有"1",取决于参数 keepdimkeepdim=True 会保留维度 1 。注意,以上只是经验总结,并非所有函数都符合这种形状变化方式,如 cumsum

b = t.ones(2, 3)
b.sum(dim = 0, keepdim=True)
 2  2  2
[torch.FloatTensor of size 1x3]
# keepdim=False,不保留维度"1",注意形状
b.sum(dim=0, keepdim=False)
 2
 2
 2
[torch.FloatTensor of size 3]
b.sum(dim=1)
 3
 3
[torch.FloatTensor of size 2]
a = t.arange(0, 6).view(2, 3)
print(a)
a.cumsum(dim=1) # 沿着行累加
 0  1  2
 3  4  5
[torch.FloatTensor of size 2x3]
  0   1   3
  3   7  12
[torch.FloatTensor of size 2x3]

比较

比较函数中有一些是逐元素比较,操作类似于逐元素操作,还有一些则类似于归并操作。常用比较函数如表 3-6 所示。

表 3-6: 常用比较函数

函数功能
gt/lt/ge/le/eq/ne大于/小于/大于等于/小于等于/等于/不等
topk最大的 k 个数
sort排序
max/min比较两个 tensor 最大最小值

表中第一行的比较操作已经实现了运算符重载,因此可以使用 a>=ba>ba!=ba==b ,其返回结果是一个 ByteTensor ,可用来选取元素。max/min 这两个操作比较特殊,以 max 来说,它有以下三种使用情况:

  • t.max(tensor):返回 tensor 中最大的一个数
  • t.max(tensor,dim):指定维上最大的数,返回 tensor 和下标
  • t.max(tensor1, tensor2): 比较两个 tensor 相比较大的元素

至于比较一个 tensor 和一个数,可以使用 clamp 函数。下面举例说明。

a = t.linspace(0, 15, 6).view(2, 3)
a
  0   3   6
  9  12  15
[torch.FloatTensor of size 2x3]
b = t.linspace(15, 0, 6).view(2, 3)
b
 15  12   9
  6   3   0
[torch.FloatTensor of size 2x3]
a>b
 0  0  0
 1  1  1
[torch.ByteTensor of size 2x3]
a[a>b] # a 中大于 b 的元素
  9
 12
 15
[torch.FloatTensor of size 3]
t.max(a)
15.0
t.max(b, dim=1) 
# 第一个返回值的 15 和 6 分别表示第 0 行和第 1 行最大的元素
# 第二个返回值的 0 和 0 表示上述最大的数是该行第 0 个元素
(
  15
   6
 [torch.FloatTensor of size 2], 
  0
  0
 [torch.LongTensor of size 2])
t.max(a,b)
 15  12   9
  9  12  15
[torch.FloatTensor of size 2x3]
# 比较 a 和 10 较大的元素
t.clamp(a, min=10)
 10  10  10
 10  12  15
[torch.FloatTensor of size 2x3]

线性代数

PyTorch 的线性函数主要封装了 Blas 和 Lapack,其用法和接口都与之类似。常用的线性代数函数如表 3-7 所示。

表 3-7: 常用的线性代数函数

函数功能
trace对角线元素之和(矩阵的迹)
diag对角线元素
triu/tril矩阵的上三角/下三角,可指定偏移量
mm/bmm矩阵乘法,batch 的矩阵乘法
addmm/addbmm/addmv/addr/badbmm..矩阵运算
t转置
dot/cross内积/外积
inverse求逆矩阵
svd奇异值分解

具体使用说明请参见官方文档[^3],需要注意的是,矩阵的转置会导致存储空间不连续,需调用它的`.contiguous`方法将其转为连续。

b = a.t()
b.is_contiguous()
False
b.contiguous()
  0   9
  3  12
  6  15
[torch.FloatTensor of size 3x2]

3.1.2 Tensor 和 Numpy

Tensor 和 Numpy 数组之间具有很高的相似性,彼此之间的互操作也非常简单高效。需要注意的是,Numpy 和 Tensor 共享内存。由于 Numpy 历史悠久,支持丰富的操作,所以当遇到 Tensor 不支持的操作时,可先转成 Numpy 数组,处理后再转回 tensor,其转换开销很小。

import numpy as np
a = np.ones([2, 3],dtype=np.float32)
a
array([[1., 1., 1.],
       [1., 1., 1.]], dtype=float32)
b = t.from_numpy(a)
b
 1  1  1
 1  1  1
[torch.FloatTensor of size 2x3]
b = t.Tensor(a) # 也可以直接将 numpy 对象传入 Tensor
b
 1  1  1
 1  1  1
[torch.FloatTensor of size 2x3]
a[0, 1]=100
b
   1  100    1
   1    1    1
[torch.FloatTensor of size 2x3]
c = b.numpy() # a, b, c 三个对象共享内存
c
array([[  1., 100.,   1.],
       [  1.,   1.,   1.]], dtype=float32)

注意: 当 numpy 的数据类型和 Tensor 的类型不一样的时候,数据会被复制,不会共享内存。

a = np.ones([2, 3])
a # 注意和上面的 a 的区别(dtype 不是 float32)
array([[1., 1., 1.],
       [1., 1., 1.]])
b = t.Tensor(a) # FloatTensor(double64 或者 float64)
b
 1  1  1
 1  1  1
[torch.FloatTensor of size 2x3]
c = t.from_numpy(a) # 注意 c 的类型(DoubleTensor)
c
 1  1  1
 1  1  1
[torch.DoubleTensor of size 2x3]
a[0, 1] = 100
b # b 与 a 不通向内存,所以即使 a 改变了,b 也不变
 1  1  1
 1  1  1
[torch.FloatTensor of size 2x3]
c # c 与 a 共享内存
   1  100    1
   1    1    1
[torch.DoubleTensor of size 2x3]

广播法则(broadcast) 是科学运算中经常使用的一个技巧,它在快速执行向量化的同时不会占用额外的内存/显存。 Numpy 的广播法则定义如下:

  • 让所有输入数组都向其中 shape 最长的数组看齐,shape 中不足的部分通过在前面加 1 补齐
  • 两个数组要么在某一个维度的长度一致,要么其中一个为 1,否则不能计算
  • 当输入数组的某个维度的长度为 1 时,计算时沿此维度复制扩充成一样的形状

PyTorch 当前已经支持了自动广播法则,但是笔者还是建议读者通过以下两个函数的组合手动实现广播法则,这样更直观,更不易出错:

  • unsqueeze 或者 view :为数据某一维的形状补 1,实现法则 1
  • expand 或者 expand_as ,重复数组,实现法则 3;该操作不会复制数组,所以不会占用额外的空间。

注意,repeat 实现与 expand 相类似的功能,但是 repeat 会把相同数据复制多份,因此会占用额外的空间。

a = t.ones(3, 2)
b = t.zeros(2, 3,1)
# 自动广播法则
# 第一步:a 是 2 维,b 是 3 维,所以先在较小的 a 前面补 1 ,
#               即:a.unsqueeze(0),a 的形状变成(1,3,2),b 的形状是(2,3,1),
# 第二步:   a 和 b 在第一维和第三维形状不一样,其中一个为 1 ,
#               可以利用广播法则扩展,两个形状都变成了(2,3,2)
a+b
(0 ,.,.) = 
  1  1
  1  1
  1  1

(1 ,.,.) = 
  1  1
  1  1
  1  1
[torch.FloatTensor of size 2x3x2]
# 手动广播法则
# 或者 a.view(1,3,2).expand(2,3,2)+b.expand(2,3,2)
a.unsqueeze(0).expand(2, 3, 2) + b.expand(2,3,2)
(0 ,.,.) = 
  1  1
  1  1
  1  1

(1 ,.,.) = 
  1  1
  1  1
  1  1
[torch.FloatTensor of size 2x3x2]
# expand 不会占用额外空间,只会在需要的时候才扩充,可极大节省内存
e = a.unsqueeze(0).expand(10000000000000, 3,2)

3.1.3 内部结构

tensor 的数据结构如图 3-1 所示。tensor 分为头信息区(Tensor) 和存储区(Storage),信息区主要保存着 tensor 的形状(size)、步长(stride)、数据类型(type)等信息,而真正的数据则保存成连续数组。由于数据动辄成千上万,因此信息区元素占用内存较少,主要内存占用则取决于 tensor 中元素的数目,也即存储区的大小。

一般来说一个 tensor 有着与之相对应的 storage, storage 是在 data 之上封装的接口,便于使用,而不同 tensor 的头信息一般不同,但却可能使用相同的数据。下面看两个例子。

图 3-1: Tensor 的数据结构

a = t.arange(0, 6)
a.storage()
 0.0
 1.0
 2.0
 3.0
 4.0
 5.0
[torch.FloatStorage of size 6]
b = a.view(2, 3)
b.storage()
 0.0
 1.0
 2.0
 3.0
 4.0
 5.0
[torch.FloatStorage of size 6]
# 一个对象的 id 值可以看作它在内存中的地址
# storage 的内存地址一样,即是同一个 storage
id(b.storage()) == id(a.storage())
True
# a 改变,b 也随之改变,因为他们共享 storage
a[1] = 100
b
   0  100    2
   3    4    5
[torch.FloatTensor of size 2x3]
c = a[2:] 
c.storage()
 0.0
 100.0
 2.0
 3.0
 4.0
 5.0
[torch.FloatStorage of size 6]
c.data_ptr(), a.data_ptr() # data_ptr 返回 tensor 首元素的内存地址
# 可以看出相差 8,这是因为 2*4=8--相差两个元素,每个元素占 4 个字节(float)
(29054536, 29054528)
c[0] = -100 # c[0]的内存地址对应 a[2]的内存地址
a
   0
 100
-100
   3
   4
   5
[torch.FloatTensor of size 6]
d = t.Tensor(c.storage())
d[0] = 6666
b
 6666   100  -100
    3     4     5
[torch.FloatTensor of size 2x3]
# 下面4个 tensor 共享 storage
id(a.storage()) == id(b.storage()) == id(c.storage()) == id(d.storage())
True
a.storage_offset(), c.storage_offset(), d.storage_offset()
(0, 2, 0)
e = b[::2, ::2] # 隔 2 行/列取一个元素
id(e.storage()) == id(a.storage())
True
b.stride(), e.stride()
((3, 1), (6, 2))
e.is_contiguous()
False

可见绝大多数操作并不修改 tensor 的数据,而只是修改了 tensor 的头信息。这种做法更节省内存,同时提升了处理速度。在使用中需要注意。 此外有些操作会导致 tensor 不连续,这时需调用 tensor.contiguous 方法将它们变成连续的数据,该方法会使数据复制一份,不再与原来的数据共享 storage。 另外读者可以思考一下,之前说过的高级索引一般不共享 stroage,而普通索引共享 storage,这是为什么?(提示:普通索引可以通过只修改 tensor 的 offset,stride 和 size,而不修改 storage 来实现)。

3.1.4 其它有关 Tensor 的话题

这部分的内容不好专门划分一小节,但是笔者认为仍值得读者注意,故而将其放在这一小节。

持久化

Tensor 的保存和加载十分的简单,使用 t.save 和 t.load 即可完成相应的功能。在 save/load 时可指定使用的 pickle 模块,在 load 时还可将 GPU tensor 映射到 CPU 或其它 GPU 上。

if t.cuda.is_available():
    a = a.cuda(1) # 把 a 转为 GPU1 上的 tensor,
    t.save(a,'a.pth')

    # 加载为 b, 存储于 GPU1 上(因为保存时 tensor 就在 GPU1 上)
    b = t.load('a.pth')
    # 加载为 c, 存储于 CPU
    c = t.load('a.pth', map_location=lambda storage, loc: storage)
    # 加载为 d, 存储于 GPU0 上
    d = t.load('a.pth', map_location={'cuda:1':'cuda:0'})

向量化

向量化计算是一种特殊的并行计算方式,相对于一般程序在同一时间只执行一个操作的方式,它可在同一时间执行多个操作,通常是对不同的数据执行同样的一个或一批指令,或者说把指令应用于一个数组/向量上。向量化可极大提高科学运算的效率,Python 本身是一门高级语言,使用很方便,但这也意味着很多操作很低效,尤其是 for 循环。在科学计算程序中应当极力避免使用 Python 原生的 for 循环

def for_loop_add(x, y):
    result = []
    for i,j in zip(x, y):
        result.append(i + j)
    return t.Tensor(result)
x = t.zeros(100)
y = t.ones(100)
%timeit -n 10 for_loop_add(x, y)
%timeit -n 10 x + y
192 µs ± 8.38 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
The slowest run took 14.64 times longer than the fastest. This could mean that an intermediate result is being cached.
9.97 µs ± 13.8 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

可见二者有超过 40 倍的速度差距,因此在实际使用中应尽量调用内建函数(buildin-function),这些函数底层由 C/C++实现,能通过执行底层优化实现高效计算。因此在平时写代码时,就应养成向量化的思维习惯。

此外还有以下几点需要注意:

  • 大多数 t.function 都有一个参数 out ,这时候产生的结果将保存在 out 指定 tensor 之中。
  • t.set_num_threads 可以设置 PyTorch 进行 CPU 多线程并行计算时候所占用的线程数,这个可以用来限制 PyTorch 所占用的 CPU 数目。
  • t.set_printoptions 可以用来设置打印 tensor 时的数值精度和格式。 下面举例说明。
a = t.arange(0, 20000000)
print(a[-1], a[-2]) # 32bit 的 IntTensor 精度有限导致溢出
b = t.LongTensor()
t.arange(0, 200000, out=b) # 64bit 的 LongTensor 不会溢出
b[-1],b[-2]
16777216.0 16777216.0
(199999, 199998)
a = t.randn(2,3)
a
 0.0785 -0.2514 -1.0843
 0.7733  0.0812 -0.4563
[torch.FloatTensor of size 2x3]
t.set_printoptions(precision=10)
a
0.0785463676 -0.2514404655 -1.0843452215
0.7733024955 0.0811786801 -0.4562841356
[torch.FloatTensor of size 2x3]

3.1.5 小试牛刀:线性回归

线性回归是机器学习入门知识,应用十分广泛。线性回归利用数理统计中回归分析,来确定两种或两种以上变量间相互依赖的定量关系的,其表达形式为$y = wx+b+e$,$e$为误差服从均值为 0 的正态分布。首先让我们来确认线性回归的损失函数: $$ loss = \sumi^N \frac 1 2 ({yi-(wx_i+b)})^2 $$ 然后利用随机梯度下降法更新参数$\textbf{w}$和$\textbf{b}$来最小化损失函数,最终学得$\textbf{w}$和$\textbf{b}$的数值。

import torch as t
%matplotlib inline
from matplotlib import pyplot as plt
from IPython import display
# 设置随机数种子,保证在不同电脑上运行时下面的输出一致
t.manual_seed(1000) 

def get_fake_data(batch_size=8):
    ''' 产生随机数据:y=x*2+3,加上了一些噪声'''
    x = t.rand(batch_size, 1) * 20
    y = x * 2 + (1 + t.randn(batch_size, 1))*3
    return x, y
# 来看看产生的 x-y 分布
x, y = get_fake_data()
plt.scatter(x.squeeze().numpy(), y.squeeze().numpy())
<matplotlib.collections.PathCollection at 0x7f32f3150908>

# 随机初始化参数
w = t.rand(1, 1) 
b = t.zeros(1, 1)

lr =0.001 # 学习率

for ii in range(20000):
    x, y = get_fake_data()

    # forward:计算 loss
    y_pred = x.mm(w) + b.expand_as(y) # x@W 等价于 x.mm(w);for python3 only
    loss = 0.5 * (y_pred - y) ** 2 # 均方误差
    loss = loss.sum()

    # backward:手动计算梯度
    dloss = 1
    dy_pred = dloss * (y_pred - y)

    dw = x.t().mm(dy_pred)
    db = dy_pred.sum()

    # 更新参数
    w.sub_(lr * dw)
    b.sub_(lr * db)

    if ii%1000 ==0:

        # 画图
        display.clear_output(wait=True)
        x = t.arange(0, 20).view(-1, 1)
        y = x.mm(w) + b.expand_as(x)
        plt.plot(x.numpy(), y.numpy()) # predicted

        x2, y2 = get_fake_data(batch_size=20) 
        plt.scatter(x2.numpy(), y2.numpy()) # true data

        plt.xlim(0, 20)
        plt.ylim(0, 41)
        plt.show()
        plt.pause(0.5)

print(w.squeeze()[0], b.squeeze()[0])

1.9918574094772339 2.9549660682678223

可见程序已经基本学出 w=2、b=3,并且图中直线和数据已经实现较好的拟合。

虽然上面提到了许多操作,但是只要掌握了这个例子基本上就可以了,其他的知识,读者日后遇到的时候,可以再看看这部份的内容或者查找对应文档。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文