????????熟悉 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.Conv2D
和 torch.nn.Conv2d
來做對比。