深度学习框架 Pytorch 深入学习(7):向量化 广播机制 基本索引与切片
Pytorch 搭建神经网络(7)向量化与广播机制与基本索引与切片
笔记和代码存储在我的 GitHub 库中 github.com/isKage/pytorch-notes
1 向量化
向量化计算是一种特殊的并行计算方式。
- 向量化计算:对不同的数据执行同样的一个或一批指令,或者把指令应用到一个数组或向量上,从而将多次循环操作变成一次计算。
例如:当计算乘积加和时,用 替代 可以极大地提高计算效率。
在 Python 中,for
循环是一种及其低效的计算方法,我们往往希望能通过向量化运算并行计算。
- 例如:利用
for
循环和直接向量相加,耗时差距大约数百倍
1 | import torch |
综上可知,在进行计算时尽可能使用向量化计算,避免使用 for
循环。
2 广播机制
广播法则(broadcast):快速执行向量化计算的同时不会占用额外的内存 / 显存。
2.1 NumPy 中的广播法则
-
- 广播后形状一致:所有输入数组都与形状 (shape) 最大的数组看齐,形状不足的部分在前面加 1 补齐
-
- 存在相同大小的维度:两个数组要么在某一个维度的尺寸一致,要么其中一个数组在该维度的尺寸为 1 ,否则不符合广播法则的要求
-
- 通过复制补齐:如果输入数组的某个维度的尺寸为 1 ,那么计算时沿此维度复制扩充成目标的形状大小
2.2 Pytorch 实现广播
PyTorch 支持自动广播法则,也可以手动设置:
-
unsqueeze
view
tensor[None]
: 为数据某一维度补 1 ,实现广播法则 1 -
expand
expand_as
重复数组:实现广播法则 3 (该操作不会复制整个数组,因此不会占用额外的空间)
【注意】
repeat
可以实现与expand
类似的功能:expand
是在已经存在的 Tensor 上创建一个新维度复制数据。故repeat
会占用额外的空间
2.3 具体使用
2.3.1 自动广播法则
以 a = torch.ones(3, 2); b = torch.zeros(2, 3, 1)
为例计算 a + b
的自动广播过程:
- 第一步(法则 1)
a 是 2 维的,b 是 3 维的。所以先在较小的 a 前面补 1 个维度 a.unsqueeze(0) -> a.shape = (1, 3, 2)
- 第二步(法则 2)
此时 a.shape = (1, 3, 2)
而 b.shape = (2, 3, 1)
二者在第二个维度形状一样,都为 3 。满足法则 2,存在相同形状的维度,可以进行广播
- 第三步(法则 3)
a 和 b 在第一和第三个维度的形状不一样, 利用广播法则扩展,按照最大的形状复杂。最终变为 a.shape = b.shape = (2, 3, 2)
即,a 在第一个维度复制,b 在第三个维度复制
1 | a = torch.ones(3, 2) |
2.3.2 详细计算
1 | a = torch.tensor([[1, 2], [3, 4], [5, 6]]) |
加和使用自动广播机制得到结果:
1 | print(a + b) |
具体 a
的广播结果:
1 | a = |
具体 b
的广播结果:
1 | b = |
所以计算过程:
1 | a + b = |
2.3.3 repeat 复制占用额外空间
1 | # 比较 expand 和 repeat 的内存占用情况 |
2.3.4 手动广播
可以先扩展成相同维度数,再扩展成相同形状。
1 | # 手动广播 |
3 基本索引与切片
Pytorch 的基本索引与切片与 Numpy 类似,例如:
元组序列
:在索引中直接使用一个元组序列对 Tensor 中数据的具体位置进行定位,也可以直接使用数字,即去除元组括号切片对象
:切片形如[start:end:step]
,对一个维度进行全选时可以直接使用:
...
:在索引中常用省略号来代表一个或多个维度的切片None
:表示增加一个维度
3.1 元组序列
使用 tensor[x, x, x]
或 tensor[(x, x, x)]
索引
1 | a = torch.Tensor([i for i in range(24)]).view(2, 3, 4) |
1 | # 提取位置 [0, 1, 2] 的元素 |
1 | # 第三个维度全取 |
3.2 ...
和 :
运算符
:
:使用格式[start:end:step]
。单独使用:
代表这个维度全取,start
和end
为空分别表示从头开始和一直到结束,step
的默认值是 1...
:用于省略任意多个维度,可以用在切片的中间,也可以用在首尾
3.2.1 :
运算符代表全取
常见格式 tensor[:, :, start:end:step]
1 | a = torch.rand(64, 3, 224, 224) |
3.2.2 ...
代替多个维度
若 a.shape = (64, 3, 224, 224)
使用 a[..., ::4, :]
代表第一、二维度均取,相当于用 a[:, :, ::4, :]
1 | # 使用 ... 代替一个或多个维度,建议一个索引中只使用一次 |
【建议】
...
在一个索引里只出现一次,否则会出现难以匹配
1 | a[..., ::4, ...].shape # 如果将最后一个维度也改为 ... 那么在匹配维度时将混乱出错 |
3.3 None 索引
None
索引可以简单地理解为维度的扩展,在广播法则用于补充维度。即相当于 unsqueeze()
函数。不过与 unsqueeze()
相比,使用 None
可以更为直观,因为它直接在索引中表示了扩展的维度。
简单而言,【推荐】使用
None
来扩展维度
- 增加一个维度
在增加一个维度时,None
的优势并不明显
1 | print(x.unsqueeze(0).shape) # 使用 unsqueeze 在第 0 位置补充维度 |
- 增加多个维度
如果要增加多个维度,则需要多次 unsqueeze()
并且每次还要计算索引位置,十分复杂
1 | x = torch.randn(3, 3, 3) |
若使用 None
直接指定维度位置,则方便很多
1 | x = torch.randn(3, 3, 3) |
3.4 综合使用
【案例】计算批量 batch_size = 16
的样本的矩阵乘法: [256, 1] @ [1, 256]
最后应该得到 shape = [16, 256, 256]
(其中 @
代表矩阵乘法)
1 | a = torch.arange(16 * 256).view(16, 256) |
3.4.1 手动扩展并计算
1 | b = a.unsqueeze(1) # b.shape = [16, 1, 256] |
这很符合我们平常的矩阵计算原则,但不是我们希望看到的,因为相乘的顺序直接导致了结果形状的不同。
3.4.2 自动广播技术
1 | b = a[:, None, :] # b.shape = [16, 1, 256] |
通过广播机制:形如 [16, 1, 256]
与 [16, 256, 1]
相乘时,首先因为通过 None
扩展了,故维度相同(满足法则一);其次存在有形状的维度,第一个维度均为 16
(满足法则二);最后相乘时,对形状 1
进行复制,复制到 256
再与同维度的数相乘(使用法则三)
所以,通过广播机制,得到的结果是相同的,无需考虑顺序问题【推荐】
Pytorch 的张量 Tensor 在进行乘法时:
- 维度、形状不同:检查是否能进行广播,从而计算
- 维度、形状均相同:直接进行逐元素计算,例如:
1 | # 补充:逐元素计算 |