关于Tensorflow中卷积的padding其实在之前的课程中我有讲过,不过本节课会在详细讲解下,并和Pytorch
中卷积的padding进行简单的对比。关于Pytorch
中卷积的padding可以参考我之前写的一篇文章。
在Tensorflow中卷积的padding一般需要指定为same
或者valid
,并不用去详细计算padding的多少。关于padding后的输出特征矩阵的大小,还是参见如下公式(在Pytorch
中就是直接按照这个公式计算的):
o u t p u t _ s i z e = i n p u t _ s i z e − f i l t e r _ s i z e + 2 P s t r i d e + 1 output\_size = \frac{input\_size-filter\_size + 2P}{stride} + 1 output_size=strideinput_size−filter_size+2P+1
借用tf中keras模块关于valid的情况介绍:"valid" means no padding.
简单来说,就是在计算过程中不会为输入特征矩阵添加padding。
valid
模式具体计算公式如下,后面有实验证明:
o u t p u t _ s i z e = c e i l [ i n p u t _ s i z e − f i l t e r _ s i z e + 1 s t r i d e ] output\_size = ceil\lbrack\frac{input\_size-filter\_size+1}{stride}\rbrack output_size=ceil[strideinput_size−filter_size+1]
例如input_size=6
, filter_size=3
, stride=2
根据计算参考实验1
o u t p u t _ s i z e = c e i l [ 6 − 3 + 1 2 ] = c e i l [ 2 ] = 2 output\_size = ceil\lbrack\frac{6-3+1}{2}\rbrack=ceil[2]=2 output_size=ceil[26−3+1]=ceil[2]=2
如下图所示,由于valid
模式不会进行padding,所以为了防止出现越界的情况,实际处理的数据只有有图中红色区域,剩下其他区域的数据都被舍弃掉了。
假如把input_size=5
其他参数不变, 根据计算(参考实验2)
o u t p u t _ s i z e = c e i l [ 5 − 3 + 1 2 ] = c e i l [ 1.5 ] = 2 output\_size = ceil\lbrack\frac{5-3+1}{2}\rbrack=ceil[1.5]=2 output_size=ceil[25−3+1]=ceil[1.5]=2
此时刚好所有数据都能被利用(依旧没有padding)。
借用tf中keras模块关于same的情况介绍:"same" results in padding evenly to the left/right or up/down of the input such that output has the same height/width dimension as the input.
大致的意思是说会在输入特征矩阵的周围进行padding填充,一般是为了将所有的数据都覆盖到。但是保证输出h/w维度与输入h/w维度保持不变
这句话是有误导性的。因为一般使用same
时,stride
都是设置为1的,所以输出特征矩阵的高和宽与输入特征矩阵的高和宽是一样的,但是如果stride
大于1,那后面这句话肯定就是有问题的了,一般height和width都会变。
same
模式具体计算公式如下,后面有实验证明:
o u t p u t _ s i z e = c e i l [ i n p u t _ s i z e s t r i d e ] output\_size = ceil\lbrack\frac{input\_size}{stride}\rbrack output_size=ceil[strideinput_size]
例如input_size=5
, filter_size=3
, stride=1
根据计算(参考实验3)
o u t p u t _ s i z e = c e i l [ 5 1 ] = c e i l [ 5 ] = 5 output\_size = ceil\lbrack\frac{5}{1}\rbrack=ceil[5]=5 output_size=ceil[15]=ceil[5]=5
如下图所示,为了防止出现越界的情况,same
模式会自动进行padding,所以在输入特征矩阵上下左右,都使用0元素进行了padding填充。实际处理的数据为图中红色区域(保证所有信息被利用到的同时,保证高和宽不变)。
再看一个比较特殊的情况,假设input_size=4
, filter_size=3
, stride=2
根据计算(参考实验4) 此时输出特征矩阵的高度和宽度就缩减为原来的一半了。
o u t p u t _ s i z e = c e i l [ 4 2 ] = c e i l [ 2 ] = 2 output\_size = ceil\lbrack\frac{4}{2}\rbrack=ceil[2]=2 output_size=ceil[24]=ceil[2]=2
通过分析能够发现,此时只需要在左右中的一侧以及上下中的一侧进行1个像素的pandding填充即可。那么问题来了到底填充在哪边呢?通过实验发现,tensorflow底层是在右侧以及下侧进行填充的,可见参考实验4。通过padding后,所有的数据都能被覆盖到。
接着谈谈我个人的看法,在Pytorch中的padding一般都是人为计算指定的(个人感觉更灵活),而在Tensorflow中一般只用指定是same
或者valid
即可(个人感觉更方便)。
但有时为了对比tensorflow和pytorch的计算结果(希望拥有相同的padding行为),例如在实验4和实验5中当input_size=4
, filter_size=3
, stride=2
时,为了覆盖所有数据一般padding的数值是等于1的。但在Tensorflow中(padding="same"
)是在输入特征矩阵的右侧和下侧进行填充的,而在Pytorch中是在左侧和上侧进行填充的。
假设这里将Tensorflow的padding方式改成和Pytorch中的一样,此时就不能使用same
方式了。此时,比较常见的方法是先手动padding,然后在用valid
的方式,参考实验6。
Tensorflow使用的版本是2.4
Pytorch使用的版本是1.6
为了方便计算,在初始化卷积核参数时,权重全部置为1.
在Tensorflow
中当input_size=6
, filter_size=3
, stride=2
,padding="valid"
时:
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers
np.random.seed(0)
image = np.random.randint(low=0, high=5, size=[6, 6]).astype(np.float32)
# [B, H, W, C] for tensorflow
print(image)
tf_image = np.expand_dims(image, axis=[0, 3])
conv = layers.Conv2D(filters=1,
kernel_size=3,
strides=2,
padding="valid",
use_bias=False,
kernel_initializer=keras.initializers.constant(1.))
conv.build([1, 6, 6, 1])
result = np.squeeze(conv(tf_image).numpy())
print(result)
在Tensorflow
中当input_size=5
, filter_size=3
, stride=2
,padding="valid"
时:
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers
np.random.seed(0)
image = np.random.randint(low=0, high=5, size=[5, 5]).astype(np.float32)
# [B, H, W, C] for tensorflow
print(image)
tf_image = np.expand_dims(image, axis=[0, 3])
conv = layers.Conv2D(filters=1,
kernel_size=3,
strides=2,
padding="valid",
use_bias=False,
kernel_initializer=keras.initializers.constant(1.))
conv.build([1, 5, 5, 1])
result = np.squeeze(conv(tf_image).numpy())
print(result)
在Tensorflow
中当input_size=5
, filter_size=3
, stride=1
,padding="same"
时:
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers
image = np.ones(shape=[5, 5], dtype=np.float32)
# [B, H, W, C] for tensorflow
print(image)
tf_image = np.expand_dims(image, axis=[0, 3])
conv = layers.Conv2D(filters=1,
kernel_size=3,
strides=1,
padding="same",
use_bias=False,
kernel_initializer=keras.initializers.constant(1.))
conv.build([1, 5, 5, 1])
result = np.squeeze(conv(tf_image).numpy())
print(result)
在Tensorflow
中当input_size=4
, filter_size=3
, stride=2
,padding="same"
时:
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers
image = np.ones(shape=[4, 4], dtype=np.float32)
# [B, H, W, C] for tensorflow
print(image)
tf_image = np.expand_dims(image, axis=[0, 3])
conv = layers.Conv2D(filters=1,
kernel_size=3,
strides=2,
padding="same",
use_bias=False,
kernel_initializer=keras.initializers.constant(1.))
conv.build([1, 4, 4, 1])
result = np.squeeze(conv(tf_image).numpy())
print(result)
在Pytorch中,当input_size=4
, filter_size=3
, stride=2
, padding=1
时:
import numpy as np
import torch
image = np.ones(shape=[4, 4], dtype=np.float32)
# [B, C, H, W] for Pytorch
print(image)
torch_image = torch.as_tensor(np.expand_dims(image, axis=[0, 1]))
conv = torch.nn.Conv2d(in_channels=1,
out_channels=1,
kernel_size=3,
stride=2,
padding=1,
bias=False)
torch.nn.init.ones_(conv.weight)
result = np.squeeze(conv(torch_image).detach().numpy())
print(result)
在Tensorflow
中当input_size=4
, filter_size=3
, stride=2
,padding="valid"
时(事先手动pandding):
import numpy as np
from tensorflow import keras
from tensorflow.keras import layers
image = np.ones(shape=[4, 4], dtype=np.float32)
# [B, H, W, C] for tensorflow
print(image)
tf_image = np.expand_dims(image, axis=[0, 3])
# ((top_pad, bottom_pad), (left_pad, right_pad))
padding = layers.ZeroPadding2D(padding=((1, 0), (1, 0)))
conv = layers.Conv2D(filters=1,
kernel_size=3,
strides=2,
padding="valid",
use_bias=False,
kernel_initializer=keras.initializers.constant(1.))
conv.build([1, 4, 4, 1])
result = np.squeeze(conv(padding(tf_image)).numpy())
print(result)