广播机制
在某些情况下,即使形状不同,我们仍然可以通过调用 广播机制(broadcasting mechanism)来执行按元素操作。这种机制的工作方式如下:
- 通过适当复制元素来扩展一个或两个数组,以便在转换之后,两个张量具有相同的形状;
- 对生成的数组执行按元素操作。
你尝试对两个形状不匹配的张量进行元素级(element-wise)操作时,如加法、减法、乘法等,PyTorch会尝试进行广播(broadcasting)以使得这两个张量在形状上兼容。广播是一种强大的机制,它允许NumPy和PyTorch等库对形状不同的数组进行数值计算。
a = torch.arange(3).reshape((3, 1))
b = torch.arange(4).reshape((1, 4))
print(a+b)
tensor([[0, 1, 2, 3],
[1, 2, 3, 4],
[2, 3, 4, 5]])
# 注释总结:
# a和b的形状分别是(3, 1)和(1, 4),但由于PyTorch的广播规则,我们可以将它们相加。
# 结果是一个(3, 4)的张量,其中每个元素都是a和b中对应元素的和。
索引和切片
在任何其他Python数组中一样,张量中的元素可以通过索引访问。与任何Python数组一样:第一个元素的索引是0,最后一个元素索引是‐1;可以指定范围以包含第一个元素和最后一个之前的元素。
我们可以用[-1]选择最后一个元素,可以用[1:3]选择第二个和第三个元素:
import torch
# 创建一个从0到15(包括15)的一维张量,并将其重新整形为4x4的二维张量
a = torch.arange(16).reshape((4, 4))
# 打印a的最后一行,即索引为3的行
print(a[-1]) # 输出:tensor([12, 13, 14, 15])
# 打印a的前三行,即索引为0, 1, 2的行
print(a[0:3]) # 输出:
# tensor([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])
# 打印a的第三行,即索引为2的行
print(a[2]) # 输出:tensor([ 8, 9, 10, 11])
# 打印a中索引为(2, 3)的元素,即第三行第四列的元素
print(a[2,3]) # 输出:tensor(11)
# 打印a中从第三行到最后一行的所有行(即索引为2, 3的行)
print(a[2:,:]) # 输出:
# tensor([[ 8, 9, 10, 11],
# [12, 13, 14, 15]])
# 打印a中从第一行到第二行的所有行(即索引为0, 1的行),以及这些行的所有列
print(a[0:2,:]) # 输出:
# tensor([[0, 1, 2, 3],
# [4, 5, 6, 7]])
节省内存
运行一些操作可能会导致为新结果分配内存。例如,如果我们用Y = X + Y,我们将取消引用Y指向的张量,而是指向新分配的内存处的张量。
import torch
import numpy as np # 注意:虽然导入了numpy,但在这段代码中并没有使用numpy
a = torch.arange(16).reshape((4, 4))
b = torch.arange(16).reshape((4, 4))
# 记录a的原始id(内存地址)
before = id(a)
c = a + b
# 打印c的内存地址是否与a的原始内存地址相同
# 因为+运算符在PyTorch中返回一个新的张量,所以id(c)与before是不同的
print(id(c) == before) # 输出应为False
# 使用加法赋值运算符+=修改a的值,使其为a和b的和
a += b
# 打印a的内存地址是否与其原始内存地址相同
# 在PyTorch中,+=运算符可能会原地修改a(取决于具体的实现和上下文),但即使原地修改,
# Python的id函数也可能不会改变(因为对象本身还在同一个内存位置),但内部数据可能已经改变
print(id(a) == before) # 输出可能为True(但这不保证在所有PyTorch版本和所有环境中都如此)
# 使用切片操作[:]来“原地”修改a的值,但实际上这仍然是一个赋值操作
# 它首先计算a+b的结果,然后将这个结果赋值给a的切片(即整个a)
# 这与+=的效果类似,但它更明确地表示了我们想要替换整个a的内容
a[:] = a + b
# 再次打印a的内存地址是否与其原始内存地址相同
# 同样,id(a)可能保持不变,但a的内容已经改变
print(id(a) == before) # 输出可能为True(但这不保证在所有PyTorch版本和所有环境中都如此)
PyTorch张量(tensor)和NumPy数组(array)之间的转换
import torch # 导入PyTorch库
a = torch.arange(16).reshape((4, 4))
# 使用a的.numpy()方法将其从PyTorch张量转换为NumPy数组
c = a.numpy()
print(type(c)) # 打印c的类型,输出应为<class 'numpy.ndarray'>,表示c是一个NumPy数组
# 使用torch.from_numpy(c)将NumPy数组c转换回PyTorch张量
c = torch.from_numpy(c)
print(type(c)) # 打印c的类型,输出应为<class 'torch.Tensor'>,表示c现在是一个PyTorch张量
# 注释总结:
# 这段代码展示了如何在PyTorch张量和NumPy数组之间进行转换。
# 首先,使用.numpy()方法将PyTorch张量转换为NumPy数组。
# 然后,使用torch.from_numpy()函数将NumPy数组转换回PyTorch张量。
# 转换过程中,数据的值保持不变,但数据类型(即Python中的类)会发生变化。
思考
当使用三维张量(或其他更高维度的张量)替换广播机制中按元素操作的两个张量时,结果是否与预期相同?
import torch
# 创建一个形状为 (2, 1, 3) 的三维张量 a
a = torch.arange(6).reshape((2, 1, 3))
print(a)
# tensor([[[0, 1, 2]],
# [[3, 4, 5]]])
# 创建一个形状为 (1, 4, 1) 的三维张量 b
b = torch.arange(4).reshape((1, 4, 1)) * 10
print(b)
# tensor([[[ 0],
# [10],
# [20],
# [30]]])
# 尝试将 a 和 b 相加
c = a + b
# 广播规则将允许这样的加法,因为:
# 1. 在第一个维度上(从0开始),a 有 2,b 有 1,1 可以广播到 2
# 2. 在第二个维度上,a 有 1,b 有 4,1 可以广播到 4
# 3. 在第三个维度上,a 有 3,b 有 1,1 可以广播到 3
print(c)
# tensor([[[ 0, 10, 20],
# [ 1, 11, 21],
# [ 2, 12, 22],
# [ 3, 13, 23]],
#
# [[ 3, 13, 23],
# [ 4, 14, 24],
# [ 5, 15, 25],
# [ 6, 16, 26]]])
归纳起来,确保两个张量大小匹配或满足广播规则的方法有:
- 确保两个张量的形状完全相同。
- 如果两个张量的形状在某些维度上不同,检查是否满足广播规则,即这些维度的大小是否相等或其中一个大小为1。
- 如果不满足上述条件,你可能需要调整张量的形状(如使用reshape、view、squeeze、unsqueeze等方法)或重新考虑你的操作,以确保它们能够兼容。
自律每一天,fighting