Deep-Learning-with-PyTorch-3.4 命
3.4 命名张量
张量的尺寸(或轴)通常索引诸如像素位置或颜色通道之类的东西。 这意味着当我们想索引张量时,我们需要记住维度的顺序并相应地编写索引。 随着数据通过多个张量转换,跟踪哪个维度包含哪些数据可能容易出错。
为了使事情具体,假设我们在2.1.4节中的img_t这样的3D张量(为简化起见,我们将使用虚拟数据),并将其转换为灰度。 我们查找了颜色的典型权重以得出单个亮度值:
# In[2]:
img_t = torch.randn(3, 5, 5) # shape [channels, rows, columns]
weights = torch.tensor([0.2126, 0.7152, 0.0722])
我们还经常希望代码泛化,例如,从表示为具有高度和宽度尺寸的2D张量的灰度图像到添加第三个通道尺寸的彩色图像(如RGB),或者从单个图像到一批图像。 在2.1.4节中,我们在batch_t中引入了另一个批处理维度; 在这里,我们假装有两个批次:
# In[3]:
batch_t = torch.randn(2, 3, 5, 5) # shape [batch, channels, rows, columns]
因此,有时RGB通道的尺寸为0,有时它们的尺寸为1。但是我们可以通过从末尾开始计数来进行归纳:它们始终在3维中,是末尾的第三个。 简单的,没有权重的均值可以这样写:
# In[4]:
img_gray_naive = img_t.mean(-3)
batch_gray_naive = batch_t.mean(-3)
img_gray_naive.shape, batch_gray_naive.shape
# Out[4]:
(torch.Size([5, 5]), torch.Size([2, 5, 5]))
但是现在我们也有分量了。 PyTorch将允许我们乘以相同形状的东西,以及在给定维度上一个操作数的大小为1的形状。 它还会自动附加尺寸为1的前导尺寸。 这是称为广播的功能。 形状(2,3,5,5)的batch_t乘以形状(3,1,1)的未压缩的权重,得出一个形状为(2、3、5、5)的张量,然后我们可以根据该张量求和来自末端的第三个维度(三个通道):
# In[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
# Out[5]:
(torch.Size([2, 3, 5, 5]), torch.Size([2, 3, 5, 5]), torch.Size([3, 1, 1]))
因为这很快就会变得混乱(并且为了提高效率),所以PyTorch函数einsum(改编自NumPy)指定了索引迷你语言,为此类产品的总和提供了索引名称。 就像在Python中一样,广播是一种总结未命名事物的形式,它使用三个点“ ...”来完成; 但不必太担心einsum,因为我们将在以下内容中不使用它:
# In[6]:
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
# Out[6]:
torch.Size([2, 5, 5])
如我们所见,其中涉及大量簿记。 这很容易出错,尤其是在我们的代码中创建和使用张量的位置相距甚远的情况下,这引起了从业人员的注意,因此建议改用维度名称。
PyTorch 1.3添加了命名张量作为实验功能(请参阅https://pytorch.org/tutorials/intermediate/named_tensor_tutorial.html和https://pytorch.org/docs/stable/named_tensor.html)。 张量工厂函数(例如tensor和rank)带有一个名称参数。 名称应为字符串序列:
# In[7]:
weights_named = torch.tensor([0.2126, 0.7152, 0.0722], names=['channels'])
weights_named
# Out[7]:tensor([0.2126, 0.7152, 0.0722], names=('channels',))
当我们已经有一个张量并且想要添加名称(但不更改现有名称)时,可以在其上调用方法fine_names。 与索引类似,省略号(...)允许您省略任意数量的尺寸。 使用重命名同级方法,您还可以覆盖或删除(通过传入None)现有名称:
# In[8]:
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)
# Out[8]:
img named: torch.Size([3, 5, 5]) ('channels', 'rows', 'columns')
batch named: torch.Size([2, 3, 5, 5]) (None, 'channels', 'rows', 'columns')
对于具有两个输入的操作,除了通常的尺寸检查(大小是否相同,或者一个是否为1并可以广播给另一个)之外,PyTorch现在将为我们检查名称。 到目前为止,它不会自动对齐尺寸,因此我们需要明确地进行此操作。 方法align_as返回一个张量,其中添加了缺失的维,而现有的维以正确的顺序排列:
# In[9]:
weights_aligned = weights_named.align_as(img_named)
weights_aligned.shape, weights_aligned.names
# Out[9]:
(torch.Size([3, 1, 1]), ('channels', 'rows', 'columns'))
接受维度参数(例如sum)的函数也采用命名维度:
# In[10]:
gray_named = (img_named * weights_aligned).sum('channels')
gray_named.shape, gray_named.names
# Out[10]:
(torch.Size([5, 5]), ('rows', 'columns'))
如果尝试将尺寸与不同的名称组合在一起,则会出现错误:
gray_named = (img_named[..., :3] * weights_named).sum('channels')
RuntimeError: 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.
如果要在对命名张量进行运算的函数之外使用张量,则需要通过将名称重命名为None来删除名称。 以下内容使我们回到了未命名尺寸的世界:
# In[12]:
gray_plain = gray_named.rename(None)
gray_plain.shape, gray_plain.names
# Out[12]:
(torch.Size([5, 5]), (None, None))
鉴于在撰写本文时此功能具有实验性质,并且为了避免因索引和对齐方式而搞混,在本书的其余部分中,我们将坚持未命名。 指定的张量可能消除许多潜在的对齐的误差源(会令人头疼的错误),如果PyTorch论坛有任何迹象。 看到它们将被广泛采用将会很有趣。