Bug and Usage of Torch

Bugs

PyG的Planetoid的下载问题

PyG(PyTorch Geometric)是图神经网络(GNN)常用的库函数的简称。它里面包含了训练图神经网络时常用的数据集以及网络架构等。PyG是基于PyTorch的,因此它和PyTorch完全兼容。

Planetoid是PyG中包含的图数据集(torch_geometric.datasets.Planetoid)。它里面有CoraCiteSeerPubMed这三个常用的引文网络。但是,原数据集是寄存在github上面的,因此在国内下载可能会遇到问题。解决方法为:

  1. 找到PyG库中的planetoid.py文件;
  2. 将:
    1
    url = 'https://github.com/kimiyoung/planetoid/raw/master/data'
    修改为:
    1
    url='https://gitee.com/jiajiewu/planetoid/raw/master/data'
    即,在gitee上下载而不用github。

若无图形界面,如简易的Ubuntu,可用sudo find / -name planetoid.py快速找到文件的位置。

完成上述操作后PubMed数据集的下载仍会出问题,具体报错为_pickle.UnpicklingError: invalid load key, '<'.。原因未知,但可以确定是从服务器上下载文件时出现的问题。解决方法:

  • Planetoid中把与PubMed有关的个文件下载下来,手动放到raw文件夹里即可。

需要注意的是,在PyG的Planetoid数据集中,num_classes字段已经被删除,所以要想获得图中顶点的类别数,应该用dataset.y.max().item() + 1(其中dataset是对应的图数据集,如PubMedCora等)。

Usage of Torch

这一节包含了一些有用的PyTorch函数用法或语法。本节中,假设a为一个已经定义好的张量torch.tensor

torch.max

torch.max(input, dim, keepdim=False, *, out=None)或者a.(dim, keepdim=False, *, out=None)

该函数将返回一个元组(values, indices),其中valuesinput的给定dim的同一行的元素的最大值张量,而indices是该最大值在dim的坐标张量,如:

1
2
3
4
5
6
7
8
>>> a = torch.randn(4, 4)
>>> a
tensor([[-1.2360, -0.2942, -0.1222, 0.8475],
[ 1.1949, -1.1127, -2.2379, -0.6702],
[ 1.5717, -0.9207, 0.1297, -1.8768],
[-0.6172, 1.0036, -0.6060, -0.2432]])
>>> torch.max(a, 1)
torch.return_types.max(values=tensor([0.8475, 1.1949, 1.5717, 1.0036]), indices=tensor([3, 0, 0, 1]))

得到的是行最大值。该函数可以很容易地得到softmax的最终结果,也可用于分析分类的准确性。

此类操作属于PyTorch中的张量降维操作,更多类似的操作见reduction-ops

torch.eq

torch.eq(input, other, *, out=None)或者a.eq(other, *, out=None)

该函数将两个张量(inputother)进行逐元素比较,若相同位置的两个元素相同,则返回True;否则,返回False。最终结果是个bool张量:

1
2
3
>>> torch.eq(torch.tensor([[1, 2], [3, 4]]), torch.tensor([[1, 1], [4, 4]]))
tensor([[ True, False],
[False, True]])

上述操作结合torch.max可以用于分析softmax预测正确的个数,如:

1
correctnesss = y_hat.max(dim=1)[1].eq(y).float().sum()

以上y_hat为softmax结果,而y为实际的标签。上面的代码首先求出softmax的预测结果(假设标签为0,1,……,n-1,那么dim的下标就是预测值),然后再比较预测值与实际值是否相同,将正确的个数相加就得到了最终预测正确的个数。

此类操作属于PyTorch中的数值比较操作,更多类似的操作见comparison-ops

torch.dtype

torch.dtype指的是张量元素的数据类型。PyTorch支持张量元素类型的随意转换:

1
2
3
a = torch.normal(0, 1, size=(1,3))  # PyTorch默认使用float32
x = a.type(torch.int) # float32 -> int
y = a.type(torch.bool) # float32 -> bool

上述操作等价于:

1
2
x = a.int()  # float32 -> int
y = a.bool() # float32 -> bool

更多Tensor数据类型见torch-dtype

torch.nn.NLLLoss & torch.nn.CrossEntropyLoss

两者本质上都是交叉熵损失函数,但是覆盖的范围不同:

  1. CrossEntropyLoss会一次性完成softmax、取对数log和交叉熵操作,即:
    $$-\sum\limits _{n=1} ^N Y _{nm}\left\{\log[\text{softmax}(a)]\right\} _{nm}$$
    式中,$Y$是标签的one-hot编码,其下标$nm$表示第$n$个样本的标签为$m$。当使用CrossEntropyLoss为损失函数时,神经网络的最后一层无需再做softmax和log操作,但这也导致实际预测时还要对神经网络的输出做一次softmax(因为此时只在乎值之间的相对关系,故不用再取log)。
  2. NLLLoss只会完成最后一步的交叉熵操作,故在神经网络的最后一层要添加softmax和log操作,不过最后预测时就不同再做额外的softmax了。
  3. 即:
    $$\text{CrossEntropyLoss}=\text{softmax}+\text{log}+\text{NLLLoss}$$

torch.nn.parameter.Parameter

torch.nn.parameter.Parameter(Tensor: data=None, requires_grad=True)

它属于Tensor的子类,但是,不同于Tensor的是,Parameter默认有梯度,且当其与nn.Module类一起使用时,会被自动添加进参数列表,并出现在parameters()迭代器中。当我们自定义的网络需要额外的可训练参数时,可以使用Parameter,但是要记得单独对其进行初始化且data应该以torch.empty(shape)的形式传入(troch.empty将生成未被初始化的张量)。

更多信息详见PARAMETER

torch.bmm

torch.bmm(input, mat2, *, out=None)

其中input的形状为$b\times n\times m$,mat2的形状为$b\times m\times k$,最后输出的形状为$b\times n\times k$。换句话说,torch.bmm是对每个batch单独做了矩阵乘法:

$$
\text{output}[i] = \text{input}[i] \space @\space \text{mat2}[i]
$$

此类操作属于PyTorch中的与线性代数有关的运算,更多信息见BLAS and LAPACK Operations

torch.permute

torch.permute(input, dim)或者a.permute(dim)

torch.permute,如其字面意思,表维度大小的交换。dim是一个表示新维度次序的列表,如对形状为(2, 3, 4)的张量a,应用a.permute(2, 1, 0)后,将返回一个形状为(4, 3, 2)的张量。对于二维张量或者只交换张量的最后两维的次序,permute相当于对二维张量进行了一次转置,其他情况则可视为一次广义转置:原来维度$i$位置的值变成了新的维度上$i$位置的值(比如转置就是让原来第$i$列的元素变成第$i$行的元素)。

需要特别注意permutereshapeview的区别。reshapeview的运行机理是将原张量从低维到高维拉成一个向量,然后再以新维度分布从高维到低维切割、堆叠,所以reshapeview前后,张量拉成的向量都是一样的,但是permute前后则不是。

torch.nonzero

torch.nonzero(input, *, out=None, as_tuple=False)

其中,input是任意维度的张量。当as_tuple=False时,上述操作会以二维张量的形式返回input中所有值不为0的元素的下标,在该二维张量中,每一行是一个非0元素的完整下标,如:

1
2
3
4
5
6
7
8
>>> torch.nonzero(torch.tensor([[0.6, 0.0, 0.0, 0.0],
... [0.0, 0.0, 0.4, 0.0],
... [0.0, 0.0, 1.2, 0.0],
... [0.0, 0.0, 0.0,-0.4]]))
tensor([[ 0, 0],
[ 1, 2],
[ 2, 2],
[ 3, 3]])

as_tuple=True时,上述操作会返回一个长度为len(input.shape)的元组,元组的第i个元素表示所有非0原则在第i维的下标,如:

1
2
3
4
5
>>> torch.nonzero(torch.tensor([[0.6, 0.0, 0.0, 0.0],
... [0.0, 0.0, 0.4, 0.0],
... [0.0, 0.0, 1.2, 0.0],
... [0.0, 0.0, 0.0,-0.4]]), as_tuple=True)
(tensor([0, 1, 2, 3]), tensor([0, 2, 2, 3]))

torch.nonzero操作在GNN中特别有用。通过使用torch.nonzero,我们可以快速地从邻接矩阵A中获得所有边的顶点。

torch.nonzero是PyTorch中丰富的切片操作中的一种,更多详细的切片操作见Indexing, Slicing, Joining, Mutating Ops

torch.randperm

torch.randperm(n, *, generator=None, out=None, dtype=torch.int64, layout=torch.strided, device=None, requires_grad=False, pin_memory=False) → Tensor

一般来说,有用的参数只有n,该参数表明对0n-1的所有下标随机排序。整个函数会返回随机排序后的下标张量。由于random库中的random.shuffle无法用于Tensor,我们只能使用torch.randperm来生成随机排序的下标,再通过切片来达到对原张量随机排序的目的。

1
2
>>> torch.randperm(4)
tensor([2, 1, 0, 3])

torch.randperm是PyTorch中丰富的随机采样操作中的一种,更多的随机操作见Random sampling

torch.triu_indices & torch.tril_indices

torch.triu_indices(row, col, offset=0, *, dtype=torch.long, device='cpu', layout=torch.strided) → Tensor

此处只列出torch.triu_indices的用法,因为两个函数用法是一致的,只不过前者取上三角下标而后者取下三角下标。

  • 参数rowcol表示矩阵的行数和列数;
  • offset是待取三角相对于主对角线的偏移,上为+,下为-,当取0时表示得到的下标包括主对角线。对于上三角,若想去掉主对角线,则需令offset=1;对于下三角,则需令offset=-1
1
2
3
4
5
6
7
8
9
10
11
12
>>> a = torch.triu_indices(3, 3)
>>> a
tensor([[0, 0, 0, 1, 1, 2],
[0, 1, 2, 1, 2, 2]])
>>> a = torch.triu_indices(3, 3, 1)
>>> a
tensor([[0, 0, 1],
[1, 2, 2]])
>>> a = torch.tril_indices(3, 3)
>>> a
tensor([[0, 1, 1, 2, 2, 2],
[0, 0, 1, 0, 1, 2]])

需要注意的是,上三角下标的排列顺序是:

1
2
3
|--->
| -->
V ->

而下三角下标是:

1
2
3
4
5
-->
|
||
|||
VVV

torch.clone() & torch.Tensor.detach()

torch.clone(),用法为:

1
2
3
torch.clone(input, *, memory_format=torch.preserve_format)`
或者
a.clone()

torch.Tensor.detach(),用法为:a.detach()

两者看似很像,但其实是作用完全不同的两个函数:

  • torch.clone()是对张量的深拷贝,它将产生一个全新的张量,这个张量拥有原张量的所有属性,但是存储空间不重叠,新、旧张量互不影响;
  • torch.Tensor.detach()的作用是产生一个不属于原张量计算图、grad_fn=Nonerequires_grad=False但是与原张量共享内存的张量。也就是说,原张量的变化会影响detach后的张量;
  • 可以这样理解,张量就是一个结构体,它有requires_gradgrad_fn以及数据等属性。torch.clone()创建了一个全新的结构体,torch.Tensor.detach()也创建了一个结构体,但是数据是指向原张量的指针。