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)为 R\times R,卷积核(kernel size)大小为 k,步幅(stride)为 ss\le 3),空洞率(dilation)为 d,那么输出的特征映射的大小将变为 R^\prime\times R^\prime,其中
R^\prime = \lfloor \frac{R + p - d(k - 1) - 1}{s} \rfloor + 1. \\
为了算出总填充的大小 p,考虑到目标特征映射的宽、高是:
\lfloor\frac{R - 1}{s}\rfloor + 1, \\
就得到
\lfloor \frac{R + p - d(k - 1) - 1}{s} \rfloor + 1 = \lfloor\frac{R - 1}{s}\rfloor + 1. \\
即:
\lfloor \frac{R + p - d(k - 1) - 1}{s} \rfloor = \lfloor\frac{R - 1}{s}\rfloor. \\
因此,最终的总填充大小为:
p = d(k - 1) -1,\ s=2, 3. \\
但因为 Pytorch 总是上、下、左、右 4 个方向的填充量都一样大,因此
\mathrm{padding} = \lfloor \frac{p}{2} \rfloor = \lfloor \frac{d(k - 1) - 1}{2}\rfloor, \\
这样就会出现
p \ne 2\ \mathrm{padding} \\
的情况。比如,当 k=5, s=2, d=1 时,p=k - 2 = 3,而 \mathrm{padding} = 1,就算人为的设成 \mathrm{padding} = 2,也避免不了矛盾。另一方面,我们来看 TensorFlow 的填充方式:padding = 'SAME'。因为 TensorFlow 允许不同方向填充不同的大小,而且遵循上小下大、左小右大的原则,因此对于总填充大小 p 来说,上、下、左、右的填充量分别是:
\lfloor \frac{p }{2} \rfloor, \ p - \lfloor \frac{p}{2} \rfloor, \ \lfloor \frac{p}{2} \rfloor, \ p - \lfloor \frac{p}{2} \rfloor. \\
对于我们举的特殊例子来说,p = 3,因此填充量分别是 (1, 2, 1, 2),相比于 Pytorch 的 (1, 1, 1, 1) 或者 (2, 2, 2, 2),自然结果就不一致。

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

  • 先对输入的特征映射按填充量:
    \lfloor \frac{p }{2} \rfloor, \ p - \lfloor \frac{p}{2} \rfloor, \ \lfloor \frac{p}{2} \rfloor, \ p - \lfloor \frac{p}{2} \rfloor. \\
    进行 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 来做对比。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容