目前的深度学习框架实在太多了,初学者难免不知道选哪个才好,是追求曝光率最高的 TensorFlow,还是最 Geek 的 MXNet,又或是最易用的 Keras,又或者是最贴近 Python 编程思维的 Pytorch?

在这诸多框架中,我也曾徘徊不定,从 TensorFlow 入手,实在太难写了,然后换到 Keras, 发现想实现很多细节的东西还是比较麻烦,而且 debug 都很难受,最后选了 Pytorch,在单机上,目前可以说是无可匹敌的。不过,Pytorch 也是有它的坑的。下面来详细说说我目前遇到的那些坑。

Dropout & BatchNorm

如果模型里面包含了这两个 Layer ,就需要特别注意了,这时候模型需要在 model.train() 和 model.eval() 两个状态之间切换,来激活或固定这两个 Layer 。

这一点非常重要,而且不易发现,官方的文档中对应的 API 部分并没有写。而且即便没有使用,模型依旧可以正常在一定数量的 batch 上进行训练,最后你会在 predict 的时候发现问题。以 BatchNorm 为例,因为 predict 的时候通常是对单个 query 进行的,结果会出现很大的偏差,对应到图像上,轻则图片的颜色变得很不正常,如果是批量的则可以通过 norm 来消除一定的影响,重则生成的图片简直像妖怪;对应到文本分类上,就可能出现任意输入的输出都是常数了。

感受过 Keras 的简易之后,一开始很容易忽略掉这个。不得不说 Keras 设计的真好,太容易上手了,都有点娇生惯养的意思了。

Conv1d & Pool1d

对一维的数据进行操作,对象通常是文本。一般处理文本数据的时候,都是将一个句子先用向量表示,如 Word2Vec 或者 GloVe 之类的,之后进行卷积操作时,是对 “字” 或者 “词” 进行卷积的,输入是 $size_{batch} \times length_{sentence} \times dimension_{embedding}$ ,输出是 $size_{batch} \times channel_{out} \times number_{filters}$ ,其中 $channel_{out}$ 由输入的 $length_{sentence}$ 和卷积所用的 $size_{kernel}, stride, padding, dilation$ 参数计算得到。不过在 Pytorch 中,卷积操作是对数据的最后一个维度进行卷积的,也就是对 Embedding 进行卷积,这时候就需要用户手动转置了。Pooling 操作也是一样的,只要一开始对输入进行了转置,后面就正常了。

是不是觉得不可思议,为什么会有这么反直觉的设计?我猜是因为 Pytorch 的卷积和池化接口,都是从图片的接口改过来的,因此保留的图片的那种顺序,而且 Pytorch 处理图片的时候是 channel-first 的,就这样,图片的长和宽对应于句子词向量的维度,图片的 channel 对应到句子的长度,应该算是设计的时候没有考虑清楚。

但其实回过头去看看,就发现从 Embedding 层开始顺序就不一样了……所以我算是强行往 Keras 的顺序上套,纯属自己折腾自己。

保存模型

通常来讲,保存模型的 best practice 当然是保留各层的参数,而不是把整个模型直接存成二进制,这样可以节省很多空间。

不过也有其他需求的时候,如果你的模型中包含了 pre-trained embedding layer ,那么模型本身就会变得非常大了,无论哪种方式都不优雅。既然如此,我们肯定倾向于直接存个二进制模型算了,这样加载的时候也很方便。不过 Pytorch 坑的地方就在于,你存的二进制文件的时候,当前目录下必须有模型的定义文件 model.py ,否则无法加载模型 ) : 。换句话说,并没有什么 best practice ,而是 only one practice 。

其他

算不上坑的地方,不过有待改进。

  • 使用 GPU 需要明确指定,并且对模型和数据执行 .cuda() 操作。记得 evaluate 或者 predict 时候对数据 Variable(data, volatile=False) ,可以加快速度。
  • Pytorch 本身的 array 并不完全等同于 numpy.array ,有时候会踩到一些意想不到的坑,而且加载数据,保存数据的时候换来换去,真的挺麻烦的。
  • custom loss function 需要仔细考虑梯度传播的问题,这个有时间再写一篇介绍下,不算麻烦