Pytorch 与 TensorFlow 二维卷积(Conv2d)填充(padding)上的差异

        熟悉 TensorFlow 的读者知道,在调用其卷积 conv2d 的时候,TensorFlow 有两种填充方式,分别是 padding = 'SAME'padding = 'VALID',其中前者是默认值。如果卷积的步幅(stride)取值为 1,那么 padding = 'SAME' 就是指特征映射的分辨率在卷积前后保持不变,而 padding = 'VALID' 则是要下降 k - 1 个像素(即不填充,k 是卷积核大小)。比如,对于长度为 5 的特征映射,如果卷积核大小为 3,那么两种填充方式对应的结果是:

两种卷积填充方式

padding = 'SAME' 为了保持特征映射分辨率不变,需要在原特征映射四周填充不定大小的 0,然后再计算卷积,而 padding = 'VALID' 则是自然计算,不做任何填充。对于步幅 stride = 1

slim.conv2d(kernel_size=k, padding='SAME', ...)

torch.nn.Conv2d(kernel_size=k, padding=k // 2, ...)

结果是一致的,如果权重是一样的话。但如果步幅 stride = 2,则两者的结果会有差异,比如对于 224x224 分辨率的特征映射,指定 k = 5,虽然两者的结果都得到 112x112 分辨率的特征映射,但结果却是不同的。比如,在输入和权重都一样的情况下,我们得到结果(运行后面给出的的代码:compare_conv.py,将第 22 行简化为:p = k // 2,将第 66/67 行注释掉):

Shape: (1, 112, 112, 16)
Shape: (1, 112, 112, 16)
y_tf:
[[ 0.15286588 -0.13643302 -0.09014875 0.25550553 0.05999924 -0.01149828
-0.30093179 -0.13394017 -0.16866598 0.17772882 0.08939055 -0.15882357
0.02846589 0.18959665 0.09113002 0.13065471]]
y_pth:
[[ 0.01814898 -0.26733394 0.16750193 0.25537257 0.21831602 0.31476249
0.01923549 -0.0464759 -0.02368551 0.05874638 -0.26061299 -0.33947413
-0.20543707 -0.05527851 0.00162258 0.10928829]]

你也可以尝试将 torch.nn.Conv2d() 中的 padding 改成其它值,但得到的特征映射要么分辨率不对,要么值不对。

        这种差异是由 TensorFlow 和 Pytorch 在卷积运算时使用的填充方式不同导致的。Pytorch 在填充的时候,上、下、左、右各方向填充的大小是一样的,但 TensorFlow 却允许不一样。我们以一个实际例子来说明这个问题。假设输入的特征映射的分辨率(resolution)为 ,卷积核(kernel size)大小为 ,步幅(stride)为 (),空洞率(dilation)为 ,那么输出的特征映射的大小将变为 ,其中

为了算出总填充的大小 ,考虑到目标特征映射的宽、高是:

就得到

即:

因此,最终的总填充大小为:

但因为 Pytorch 总是上、下、左、右 4 个方向的填充量都一样大,因此

这样就会出现

的情况。比如,当 时,,而 ,就算人为的设成 ,也避免不了矛盾。另一方面,我们来看 TensorFlow 的填充方式:padding = 'SAME'。因为 TensorFlow 允许不同方向填充不同的大小,而且遵循上小下大、左小右大的原则,因此对于总填充大小 来说,上、下、左、右的填充量分别是:

对于我们举的特殊例子来说,,因此填充量分别是 ,相比于 Pytorch 的 或者 ,自然结果就不一致。

        知道了以上内容,为了消除 Pytorch 与 TensorFlow 填充方面的差别,采取一个简单而有效的策略:

  • 先对输入的特征映射按填充量:

    进行 0 填充;
  • 然后接不做任何填充的卷积:torch.nn.Conv2d(padding=0, ...)

以下为这个策略的验证代码(命名为 compare_conv.py):

# -*- coding: utf-8 -*-
"""
Created on Sat Dec 14 16:44:31 2019

@author: shirhe-lyh
"""

import numpy as np
import tensorflow as tf
import torch

tf.enable_eager_execution()

np.random.seed(123)
tf.set_random_seed(123)
torch.manual_seed(123)

h = 224
w = 224
k = 5
s = 2
p = k // 2 if s == 1 else 0


x_np = np.random.random((1, h, w, 3))
x_tf = tf.constant(x_np)
x_pth = torch.from_numpy(x_np.transpose(0, 3, 1, 2))


def pad(x, kernel_size=3, dilation=1):
    """For stride = 2 or stride = 3"""
    pad_total = dilation * (kernel_size - 1) - 1
    pad_beg = pad_total // 2
    pad_end = pad_total - pad_beg
    x_padded = torch.nn.functional.pad(
        x, pad=(pad_beg, pad_end, pad_beg, pad_end))
    return x_padded


conv_tf = tf.layers.Conv2D(filters=16, 
                           padding='SAME',
                           kernel_size=k,
                           strides=(s, s))

# Tensorflow prediction
with tf.GradientTape(persistent=True) as t:
    t.watch(x_tf)
    y_tf = conv_tf(x_tf).numpy()
    print('Shape: ', y_tf.shape)
    
    
conv_pth = torch.nn.Conv2d(in_channels=3,
                           out_channels=16,
                           kernel_size=k,
                           stride=s,
                           padding=p)

# Reset parameters
weights_tf, biases_tf = conv_tf.get_weights()
conv_pth.weight.data = torch.tensor(weights_tf.transpose(3, 2, 0, 1))
conv_pth.bias.data = torch.tensor(biases_tf)


# Pytorch prediction
conv_pth.eval()
with torch.no_grad():
    if s > 1:
        x_pth = pad(x_pth, kernel_size=k)
    y_pth = conv_pth(x_pth)
    y_pth = y_pth.numpy().transpose(0, 2, 3, 1)
    print('Shape: ', y_pth.shape)
    
    
# Compare results
print('y_tf: ')
print(y_tf[:, h//s-1, 0, :])
print('y_pth: ')
print(y_pth[:, h//s-1, 0, :])  

运行该代码,Pytorch 和 TensorFlow 的输出结果是一致的:

Shape: (1, 112, 112, 16)
Shape: (1, 112, 112, 16)
y_tf:
[[ 0.15286588 -0.13643302 -0.09014875 0.25550553 0.05999924 -0.01149828
-0.30093179 -0.13394017 -0.16866598 0.17772882 0.08939055 -0.15882357
0.02846589 0.18959665 0.09113002 0.13065471]]
y_pth:
[[ 0.15286588 -0.13643302 -0.09014875 0.25550553 0.05999924 -0.01149828
-0.30093179 -0.13394017 -0.16866598 0.17772882 0.08939055 -0.15882357
0.02846589 0.18959665 0.09113002 0.13065471]]

:因为 slim.conv2d 等二维卷积函数都是调用的底层类 tf.layers.Conv2D,因此拿 tf.layers.Conv2Dtorch.nn.Conv2d 来做对比。

你可能感兴趣的:(Pytorch 与 TensorFlow 二维卷积(Conv2d)填充(padding)上的差异)