0%

一些常用的pytorch技巧

pytorch zero grad 改变tensor形状 矩阵乘法 loss function pytorch nn 操作张量

本文只记录框架层面的使用。关于深度学习层面的研究记录在 神经网络训练技巧(tricks)

view、reshape和resize_

结论写在前面:张量连续,使用 view()(此为多数情况);张量不连续,则使用 reshape()resize_() 最好不要使用。注意,在张量不连续的情况下,非要使用 view() 也可以,但是要先调用 contiguous() 方法,使得张量连续,但是此方法会返回一个新的张量,浪费时间空间。 共同点:这些方法都可以改变 pytorch 中 tensor 的形状,但是它们略有不同。

  1. view():只能改变连续的(contiguous)张量,且返回的张量与原张量的数据是共享的。在张量不连续的情况下,硬要使用此方法,需要在使用之前使用 tensor.contiguous()
  2. reshape():对张量是否连续无要求,且可能返回的原张量的拷贝(这个“可能”是字面意思,开发者指出,你永远无法预先知道返回的张量是否为拷贝版本。参考 stackoverflow 回答)。
  3. resize_():与上述二者有较大差别,此方法可以无视原张量的形状,进行随意变化。如果元素的数量大于原张量的元素数量,那么底层的存储大小会进行调整,即会进行扩容。小于,则存储空间不做变化。参考《pytorch 文档》。

    这是一个底层的方法,大多数情况可以使用 view() 或者 reshape()。想要使用自定义步长改变张量内置的大小,请使用 set_()。总而言之,就是最好不要用这个方法。

不连续的张量的说明

如果对 tensor 使用 transposepermute 等操作会使该 tensor 在内存中变得不再连续。例如,以下代码在使用 view() 方法后会报出 RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead. 的错误。

至于为什么 pytorch 有这种“连续的”设定,这就要从 pytorch 的底层讲起了,又是要几条博客才能解释(ps:关键是我也不懂),不过可以参考《PyTorch 中的 contiguous》。

1
2
3
4
5
6
7
8
9
tensor = torch.Tensor([[1, 2, 3], [8, 9, 0]])
# 正常输出
print(tensor.transpose(1, 0).reshape(2, 3))
print(tensor.permute(1, 0).reshape(2, 3))
print(tensor.T.reshape(2, 3))
# 报错
print(tensor.transpose(1, 0).view(2, 3))
print(tensor.permute(1, 0).view(2, 3))
print(tensor.T.view(2, 3))

contiguous()的解释

此外,运行以下代码可以得出非连续张量在使用 contiguous() 后会返回一个新的张量,它会重新开辟一块内存,并按照行优先一维展开顺序重新存储原张量。取自 PyTorch 中的 contiguous#为什么需要 contiguous ?

1
2
3
4
5
6
7
8
9
10
tensor1 = torch.Tensor([[1, 2, 3], [8, 9, 0]])
tensor2 = tensor1.transpose(1, 0)
# 转置前后,张量并无区别
print(tensor1.data_ptr() == tensor2.data_ptr())# True
# 对连续张量(tensor1)使用 contiguous() 是无意义的,无论是否转置都是同一个张量
tensor3 = tensor1.contiguous()
print(tensor3.data_ptr() == tensor2.data_ptr(), tensor3.data_ptr() == tensor1.data_ptr())# True True
# 对非连续张量(tensor2)使用会返回一个新的张量,无论是否转置都不是同一个张量
tensor4 = tensor2.contiguous()
print(tensor4.data_ptr() == tensor2.data_ptr(), tensor4.data_ptr() == tensor1.data_ptr())# False False

参考资料

  1. pytorch 学习笔记五:pytorch 中 reshape、view 以及 resize 之间的区别
  2. PyTorch 中的 contiguous
  3. What's the difference between reshape and view in pytorch?

矩阵乘法

pytorch 拥有多种矩阵乘法计算的函数,包括但不限于 torch.mul()torch.dot()torch.mm()torch.bmm()torch.matmul()

矩阵相乘

torch.dot() 属于向量点积。

torch.mm()torch.bmm()torch.matmul() 都属于矩阵相乘。

  1. torch.mm():二维矩阵相乘,它只能处理二维矩阵,对于其它维度需要使用 torch.matmul()。注意:如果你需要计算一个矩阵和一个向量的乘法,那么这个向量在框架角度来讲要是二维的。也就是说向量的 shape=(x, 1),而不能是 shape=(x, )。
  2. torch.bmm():与 torch.mm() 一样,只能处理二维矩阵,但是它被用于计算具有批次这个维度的矩阵,也就是说相乘的两个矩阵实际上是三维的。例如矩阵 \(A_{32 \times 3 \times 4}\)\(B_{32 \times 4 \times 3}\),其中 32 是它们的批次大小,32 之后的维度还是要按照矩阵相乘的规则。它在神经网络计算中被频繁使用,因为数据通常都是被一批一批得输入进神经网络。注意,torch.bmm() 只是一个特例(带有批次属性的二维矩阵),如果需要计算高维矩阵的相乘,必须使用 torch.matmul()

    注意,torch.bmm() 的输入只能是 3 维,它不能进行广播。广播矩阵相乘请使用 torch.matmul()

  3. torch.matmul():执行的乘法操作取决于输入张量的维度。
    1. 如果都是 1 维,则点乘;
    2. 如果都是 2 维,则计算二维矩阵相乘;
    3. 如果第一个张量是 1 维,第二个是 2 维,则广播第一个张量,进行二维矩阵相乘计算;
    4. 如果第一个张量是 2 维,第二个是 1 维,则计算矩阵 * 向量;
    5. 如果二者至少都是 1 维,且至少一个参数是 N 维(N > 2)。非矩阵的维度将被广播,矩阵维度一般指每个张量的最后一至两位。如 shape=(1, 2, 3, 4) 和 shape=(1, 4, 3),最后两位是矩阵维度;shape=(1, 2, 3, 4) 和 shape=(4),第一个张量的最后两位和第二个张量的最后一位就是矩阵维度。

综上,其实没有高维矩阵相乘的概念,一直都是在做矩阵-矩阵/向量乘法,张量中其他的维度只被用于广播。

矩阵对应元素相乘

torch.mul() 是对应元素的乘法,例如 \(\begin{pmatrix}1\end{pmatrix}\)。它主要分为两种情况:

  1. 张量乘标量:这个很好理解,就是一个张量乘上一个数字。
  2. 张量乘张量:对应元素相乘,如果两个张量的形状不同,则需要满足能够广播的前提。如果不满足,则报错。张量广播的机制具体看下面的章节。

广播机制

设现有张量 A,B。广播机制分为两种情况:1)张量的维度不同;2)张量的维度相同。

本来想写的,但是博客《pytorch 中的广播机制》中已经写明白了。此外这些都是经验之说,我在实践过程中发现 A.ndim < B.ndim 也可以进行计算。以后看比较权威的书之后,再来更新吧。

但是有一点比较清楚,广播机制只有 torch.mat.mul() 支持。

参考资料

  1. pytorch 官方文档
  2. pytorch 中的广播机制

pytorch loss function

pytorch 的多元分类 loss 有 CrossEntropyLoss 和 NLLLoss。NLLLoss 全称 Negative Log Likelihood Loss,说白了就是求对数概率并取负,我们从函数图像就可以理解。模型输出的概率分布在 0-1 之间,log 函数的 0-1 区间正好全是负数,所以要加上一个负号,让 loss 值为正数。显而易见,概率越接近 1,loss 值越小。接下来描述一下这两个函数。 1. CrossEntropyLoss = LogSoftmax + NLLLoss; 2. CrossEntropyLoss 中已经附带了 log_softmax 操作,所以如果你想省事,那么直接将输出向量输入 CrossEntropyLoss 即可; 3. 如果使用 NLLLoss,那么在使用 NLLLoss 之前,还需要经过一层 LogSoftmax。

需要注意一点,我感觉网上很多人也没有理解什么是 CrossEntropyLoss,导致很多人都被误导了。首先 nll 的公式如下:

\[ nll\_loss = -\log(pred) \]

nll loss 可以有多种表达方式,我把在网上看到的公式都罗列一下。

以下的公式与 crossentropy 一样。(实际上公式就是一样的,只不过在概念上有点不同,由于输出值 y 是 one hot 形式,为 0 时,就相当于没有加,最后的结果就是上面的公式) \[ nll\_loss = -\sum^n_{i=1} y_i log(pred_i) = -log(pred) \] 这里的 class 就是指第几个标签,它不是 one hot 表示形式。大家会发现这里少了一个 log 函数,实际上 pred 是使用 log_softmax 函数计算之后的结果。 \[ nll\_loss = -pred[class] \]

CrossEntropyLoss 公式如下: \[ crossentropy\_loss = -\sum^n_{i=1} y_i log(pred_i) \] 无法理解的原因之一是,在学机器学习的时候,大家都知道啥是 crossentropy,后来在学多元分类时,开始分 binary_crossentropy 和 crossentropy。这点大家都能理解,但是到看到 NLLLoss 时,就开始懵逼了。

由于 CrossEntropyLoss = LogSoftmax + NLLLoss,在 crossentropy 的公式中貌似没有出现 softmax(更没有 log_softmax),所以开始懵了,无法理解其中的 LogSoftmax 是干啥的。

首先我要解释一点 CrossEntropyLoss 是 LogSoftmax 和 NLLLoss 两个步骤之和,之前说的“+”号,并非是数学意义上的加号。也就是说,CrossEntropyLoss 就比 NLLLoss 多做了一步 LogSoftmax(博主注个人认为实际上只是多做了一步 softmax,说多做了一步 log_softmax,是因为站在 pytorch 框架的角度)。

其次,对于真实输出值 y 来说,无非就是 0 和 1(注意多元分类也只有 0 和 1),并且根据上述 crossentropy 的公式。实际上公式可以化简为以下所示,其中的 m 代表真实值为 1 的索引。 \[ crossentropy\_loss = -\sum^n_{i=1} y_i log(pred_i) = -log(pred_m) \] 请注意这里的 \(pred_m\)。我们都知道在进行分类问题时,我们需要将输出结果置于 0-1 之间,对于二元分类我们使用 sigmoid 函数,对于多元分类我们使用 softmax(到这开始有内味了)。由于分类问题都是要这么做的,所以将 softmax 这个函数放到公式 \(crossentropy\_loss = -log(pred_m)\) 中,我们惊奇的发现 crossentropy 函数变成了 log_softmax(最前面的负号暂时不看)。即 crossentropy + softmax = -log_softmax。

请始终留意,pred 是一个向量通过 softmax/log_softmax 计算之后的值。

最后你会发现这样还是不对。\(nll\_loss = -log(pred)\),之前说 CrossEntropyLoss = LogSoftmax + NLLLoss,我把 log_softmax 放到 nll 里,变成了 \(LogSoftmax + NLLLoss = -log(log(pred))\),怎么多了一个 log?实际上 nll 的公式应该以 \(nll\_loss = -pred[class]\) 为准,你会发现这个公式中没有 log 函数。这样将 logsoftmax 放入 nll loss 中,就正好是 crossentropy 了。

那么你就会问 nll 明明是 Negative Log Liklihood,log 不见了,这不就是名存实亡了?

这可能是因为 pytorch 想要简化操作,才这么设置的,别的框架可能并不是这样。简而言之,pytorch 框架中,nll loss 的公式是 -pred。crossentropy 的公式是 logsoftmax + nll loss,即 nll(log_softmax(output))

也就是说,如果神经网络的最后一层输出是 logsoftmax,那么就使用 nll loss(上一段 nll loss 那个 pred 就是通过 log_softmax 的输出值)。如果最后一层只是输出,偷懒不想写 logsoftmax,那么就使用 crossentropy loss(上一段 crossentropy 中的 output 就是一个普通的神经网络输出)。

顺便一提KLDivLoss

【原】浅谈KL散度(相对熵)在用户画像中的应用 暂时还没用过这个 loss,简单来说,是用来比较两个概率分布之间的信息熵差异,如 AB 两组群体,有对某一商品的总消费分布 P 和群体人数的分布 Q,可以计算 PQ 之间的信息熵差异,从而获得 AB 两组群体对该商品的偏爱程度。

参考资料

  1. CrossEntropyLoss和NLLLoss的理解
  2. Pytorch之CrossEntropyLoss() 与 NLLLoss() 的区别
  3. CrossEntropyLoss与NLLLoss的总结
  4. Pytorch里的CrossEntropyLoss详解

pytorch中的神经网络

LSTM

关于 nn.LSTM 的用法:如果不想手动初始化隐藏状态,而是想让 pytorch 帮你初始化,那么就只需要填入输入值即可。举个例子,下面代码框中的代码所输入的自然语句是 ['how are you', 'i'm fine']。 其中输入值 x 的形状为 (seq_len, batch_size, input_size),对应上面的例子就是 (3, 2, 300),代表序列长度为 3(因为上面的两句话的最大长度为 3),批量大小为 2,词向量维度为 300。需要注意的是:pytorch 会根据你输入的序列长度(输入的张量必须是一样长的,参差不齐的张量无法输入,当然了,你也无法创建出这样的张量)自动地在时间步上做计算,不需要你写一个循环,然后依次输入每一个单词的词向量。

1
2
cell = nn.LSTM(input_size, hidden_size, num_layers)
output, (a, m) = cell(x)

张量操作

expand()

expand() 函数按照你想要的大小扩充 tensor,并且返回一个新的 tensor。注意它是 tensor 的成员方法,并且你所想要新 tensor 的维度必须与原 tensor 的维度相同。例如,原 tensor.shape=(3, 1),那么你可以扩大为 (3, 4),但是你不能扩大为 (3, 1, 1),当然更不能为 (3, 1, 4)。举个例子:

1
2
3
4
5
6
7
8
x = torch.Tensor([[1], [2], [3]])
# (3, 1)
print(x.shape)
y = x.expand(3, 4)
# (3, 4)
print(y.shape)
# error
x.expand(3, 1, 1)

optimizer.zero_grad()

在 pytorch 中,为什么要在每个循环之初调用这个方法?因为 pytorch 把计算的每个梯度都累加起来,并不会每迭代一次就将梯度清零。这样做看起来令人费解,并反常理。但是实际上这样做可以做更多神奇的操作,比如

还有,试想本来你想运行 batch_size=1024,但是由于电脑太差,只能运行 batch_size=256 的批次数据。那么只需要每循环两次调用一次 zero_grad() 即可。 参考:

  1. pytorch中为什么要用 zero_grad() 将梯度清零