PyTorch爬坑指南

1,523 阅读5分钟

用PyTorch也有个把年头了,但是偶尔还是会出现各种各样的bug,当bug积累到一定数量就会引起质变——写成一个笔记来长记性……

Variable和Tensor

0.4之后的版本可以直接用Tensor代替Variable了,不需要再繁琐得包裹一层Variable

Tensor的转置

tensor的转置有很多方式,比如.transpose.transpose_是最常用的(带下划线的是在原tensor上做改变),但是这个转置方式有个限制就是仅能对两个维度进行操作,是个“根正苗红”的转置函数。

有时候需要将更多的维度进行交换,这个时候就可以用.permuate函数,该函数接受一个维度列表,然后根据列表中的维度的排列顺序更新tensor的维度排列。

升维和降维

升维.unsqueeze是在指定的维度上插入一维,但是数据实际上没有发生变化。比如下面得示例:

In [1]: a
Out[1]:
tensor([[[-0.7908, -1.4576, -0.3251],
         [-1.2053,  0.3667,  0.9423],
         [ 0.0517,  0.6051, -0.1360],
         [ 0.8666, -1.4679, -0.4511]]])

In [2]: a.shape
Out[2]: torch.Size([1, 4, 3])

In [3]: a.unsqueeze(2).shape
Out[3]: torch.Size([1, 4, 1, 3])

降维.squeeze是升维得逆操作,会消除所有维度为1的维度:

In [1]: a.shape
Out[1]: torch.Size([1, 4, 3])

In [2]: a.squeeze().shape
Out[2]: torch.Size([4, 3])

Tensor的contiguous

一个Tensor执行转置操作后,实际上数据并没有发生任何变化,只是读取数据的顺序变化了。这是一种节约空间和运算的方法。

不过转置之后的数据如果需要进行.view等操作的话,由于数据在逻辑上并不连续,因此需要手动调用contiguous让数据恢复连续存储的模样。

Loss Functions的参数格式到底是啥啊

这个是早期经常会遇到的情况,咋一会要FloatTensor,一会就要LongTensor了?要解决这个问题,还是需要深入了解不同的loss function到底在做啥。

BCELoss

二分类的交叉熵,计算公式如下:

\sum_{i}{-y_ilog\hat{y_i}-(1-y_i)log(1-\hat{y_i})}

这样就不难看出 y_i\hat{y_i} 都需要是FloatTensor了。如果是不同的tensor进行运算会出错:

Expected object of type torch.FloatTensor but found type torch.LongTensor

CrossEntropyLoss

上面的BCELoss是计算二分类情况下的损失函数,那么当遇到多分类问题的时候,也想用交叉熵来计算的话,就需要用到CrossEntropyLoss了。

不论是二分类还是多分类,都可以用最简单的L2损失来计算,但是如果使用Sigmoid作为激活函数的话容易导致梯度消失,导致效果不好

自定义网络一定要初始化权值

权值不初始化会出事的……

有时候会遇到一些结构清奇的网络,各种参数纠缠在一起,因此torch自带的函数可能不够用,那就需要自定义网络结构了。一般会从最底层的矩阵开始构建,就像tensorflow一样。比如定义一个不带偏置项的全连接层可以用nn.Linear(xxx, xxx, bias=False)。但特殊情况下甚至连全连接层内部的细节都要自己定义,这个时候就需要参考torch的源码了,比如全连接层的部分源码:

class Linear(Module):
    def __init__(self, in_features, out_features, bias=True):
        super(Linear, self).__init__()
        self.in_features = in_features
        self.out_features = out_features
        self.weight = Parameter(torch.Tensor(out_features, in_features))
        if bias:
            self.bias = Parameter(torch.Tensor(out_features))
        else:
            self.register_parameter('bias', None)
        self.reset_parameters()

    def reset_parameters(self):
        stdv = 1. / math.sqrt(self.weight.size(1))
        self.weight.data.uniform_(-stdv, stdv)
        if self.bias is not None:
            self.bias.data.uniform_(-stdv, stdv)

    def forward(self, input):
        return F.linear(input, self.weight, self.bias)

在这个例子中已经能学到很多了,这里重点参考Parameters的使用(out_feature在前)和权值初始化函数reset_parameters的内部细节。

之前一次机器学习的作业,相同的模型,PyTorch版始终达不到Tensorflow版的准确率,大概就是权值初始化两者存在差异吧。

CUDA的一些坑

自定义的网络不会自动转cuda??

之前有用过自己先定义了一个简单的底层网络,方便上层网络的构建。这样定义出来的网络在CPU上运行的好好的,但是当我把模型迁移到GPU上时却发现模型出现了类型不符的错误:

Expected object of type torch.FloatTensor but found type torch.cuda.FloatTensor for argument

后来在调试的时候发现对上层网络调用.cuda后,下层网络却还是处于device(type='cpu')!虽然解决这个问题最简单的方法就是把底层网络的定义放到上层网络中,但是这样不利于代码的维护,并且在不同网络中共享模块也会变得很复杂。

PyTorch在CPU和GPU数据上的迁移的确没有tensorflow来的方便……

在GitHub上看到很多类似的问题的解决方法,其中有一个点子很不错:

if torch.cuda.available():
    import torch.cuda as t
else:
    import torch as t

如果一开始写代码的时候就考虑到自动CUDA的话,这的确是个好办法,如果已经写了臃肿的代码再改写还是会很繁琐。

最终我选择的方案还是重写.cuda函数:

def cuda(self, device=None):
    r"""Moves all model parameters and buffers to the GPU..."""
    return self._apply(lambda t: t.cuda(device))

.cuda函数中对子模块也调用.cuda就可以了。

CUDA数据不能直接转numpy数据

如果一个数据已经迁移到GPU上了,那么如果要将运算结果转为符合numpy特性的数据就不是这么直接的了,一般会报错:

TypeError: can't convert CUDA tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.

解决方案报错信息已经给了。当然还可以将之后需要numpy来帮忙的运算改成PyTorch支持的运算,这样速度还更快呢,毕竟条条大路通罗马。