tensorflow与pytorch卷积填充方式的差异

2019-11-19  本文已影响0人  欠我的都给我吐出来

keras是基于tensorflow backend的,因此tensorlfow的卷积和keras的卷积操作应该是一致的(实验证明也是一致的),因为工作需要,使用pytorch训练densenet模型,然后转为keras模型。因为基于tensorflow backend的keras模型可以转为tf-serving,方便部署,但是在实际训练的时候,发现pytorch的模型训练速度要比keras的训练快两个小时左右。

但是在实际的测试中,发现pytroch转为keras之后,两者的文本ocr的模型的结果会有微小的差异。经过排查,一个原因如下是keras代码和pytorch代码在卷积填充中的实现细节不一样。在keras中,如果只填充一列(一行)就足够了,那么默认在最后一行之后和最后一列之后填充。在pytorch中,使用padding=1规定了需要在上下左右填充一行(一列)0值,而使用的时候默认从左到右使用,当只需要使用一行(一列)就足够的情况下,默认使用上面和前面的填充。

具体的实现细节如下:

keras的实现

keras代码中有一个conv2d的操作。代码如下

x = Conv2D(_nb_filter, (3, 3), strides=(2, 2), kernel_initializer='he_normal', padding='same',use_bias=False, kernel_regularizer=l2(_weight_decay), name='conv1')(input)

在keras中,使用same的填充方式,最终的输出大小为math.ceil(Hin/stride),在填充的时候,优先填充的是右边和下边。

通过实验验证,下面是一个实验代码:

import keras
from keras.layers.convolutional import Conv2D


data1=np.array([i for i in range(1,37)]).reshape((6,6))
data1=data1[np.newaxis,np.newaxis,:]
x = tf.Variable(data1,dtype=tf.float32)
y = Conv2D(1, (3, 3), strides=(2, 2), data_format='channels_first',kernel_initializer=keras.initializers.Ones(), padding='same',
               use_bias=False, name='conv1')(x)
with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    _,output = sess.run([x,y])
    print(output)

其中data1是这个样子的


Out[31]:
array([[[[ 1,  2,  3,  4,  5,  6],
         [ 7,  8,  9, 10, 11, 12],
         [13, 14, 15, 16, 17, 18],
         [19, 20, 21, 22, 23, 24],
         [25, 26, 27, 28, 29, 30],
         [31, 32, 33, 34, 35, 36]]]])

最终的output是这个样子的,最后一行和最后一列是右边和下面填充0之后的结果。

[[[[ 72.  90.  69.]
   [180. 198. 141.]
   [174. 186. 130.]]]]

pytorch的实现

在pytorch中densenet,同样的卷积操作的代码是这个样子的:

self.conv1 = nn.Conv2d(1, in_planes, kernel_size=3, stride=(2,2),padding=1, bias=False)

这里的填充是通过padding指定的,当padding为整数n时,默认在上下左右填充n行n列;当padding为二元数组(m,n)时,默认在height的上下填充m行0值,在width维度上左右填充n列0值。会优先使用上面和左边的填充行列。


2.png 1.png

以一个6 * 6的input,stride=(2,2),kernel size=3 * 3的例子来说,使用pytorch的卷积代码如下

import numpy as np
import tensorflow as tf
import torch
import torch.nn as nn
import torch.nn.functional as F

data1=np.array([i for i in range(1,37)]).reshape((6,6))
data1=data1[np.newaxis,np.newaxis,:]
tensor1 = torch.tensor(data1[:,:,:3,:3],dtype=torch.float32)
kernel = [[1, 1, 1],[1, 1, 1],[1, 1, 1]]
kernel = torch.FloatTensor(kernel).expand(1,1,3,3)
weight = nn.Parameter(data=kernel, requires_grad=False)
made_conv2 = lambda x: F.conv2d(x,weight,bias=None,stride=2,padding=1)
print(made_conv2(tensor1))

其中tensor1是一个四维数组

array([[[[ 1,  2,  3,  4,  5,  6],
         [ 7,  8,  9, 10, 11, 12],
         [13, 14, 15, 16, 17, 18],
         [19, 20, 21, 22, 23, 24],
         [25, 26, 27, 28, 29, 30],
         [31, 32, 33, 34, 35, 36]]]])

而得到的结果是这样的

tensor([[[[ 18.,  36.,  48.],
          [ 81., 135., 153.],
          [153., 243., 261.]]]])

验证了在pytorch中,这个结果是在tensor1的上下左右都填充了0之后,然后从左到右匹配,如果列数不够就放弃。比如,在这个例子中,实际计算的时候,并没有用到最后一列填充的0值。

或许会好奇,如果不需要填充的时候正好可以被步长完整遍历的时候,会怎么样呢?比如我们的data是一个33的矩阵,而卷积也是33的,padding=1,stride=2.按照公式可以得到结果为(2,2)。也就是会充分的使用填充的上下左右的0值。此时右边和下面的0值也使用了。这个时候就和使用keras的samepadding一致了。

解决的方法

在网上找到一份使用pytorch实现tensorflow中的samepadding的代码,只要把这一层添加在pytorch的conv2d之前即可。

class SamePad2d(nn.Module):
    """Mimics tensorflow's 'SAME' padding.
    """

    def __init__(self, kernel_size, stride):
        super(SamePad2d, self).__init__()
        self.kernel_size = torch.nn.modules.utils._pair(kernel_size)
        self.stride = torch.nn.modules.utils._pair(stride)

    def forward(self, input):
        in_width = input.size()[2]
        in_height = input.size()[3]
        out_width = math.ceil(float(in_width) / float(self.stride[0]))
        out_height = math.ceil(float(in_height) / float(self.stride[1]))
        pad_along_width = ((out_width - 1) * self.stride[0] +
                           self.kernel_size[0] - in_width)
        pad_along_height = ((out_height - 1) * self.stride[1] +
                            self.kernel_size[1] - in_height)
        pad_left = math.floor(pad_along_width / 2)
        pad_top = math.floor(pad_along_height / 2)
        pad_right = pad_along_width - pad_left
        pad_bottom = pad_along_height - pad_top
        return F.pad(input, (pad_left, pad_right, pad_top, pad_bottom), 'constant', 0)

    def __repr__(self):
        return self.__class__.__name__

原文链接:用pytorch实现tensorflow卷积中的SAME

结果

在经过一天短期的训练之后,使用pytorch模型进行训练,然后通过代码将其转换为keras的模型,然后测试了4张图片,发现两个模型的实现效果确实改进了一点。之前每张图都会有些微的区别,如下图所示:


image.png

在使用samepadding改进之后,发现只有最后一张图还有微小的差异,后续还会继续寻找自己代码中pytorch和keras模型转换存在的细微差距问题。


image.png
上一篇下一篇

猜你喜欢

热点阅读