FCN对图像进行像素级的分类,从而解决了语义级别的图像分割(semantic segmentation)问题。与经典的CNN在卷积层之后使用全连接层得到固定长度的特征向量进行分类(全联接层+softmax输出)不同,FCN可以接受任意尺寸的输入图像,采用反卷积层对最后一个卷积层的feature map进行上采样, 使它恢复到输入图像相同的尺寸,从而可以对每个像素都产生了一个预测, 同时保留了原始输入图像中的空间信息, 最后在上采样的特征图上进行逐像素分类。
全卷积网络利用三种特殊技术
全卷积网络在结构上通常有编码器(Encoder)和解码器(Decoder)组成。编码器是一系列卷积层,如VGG和ResNet。编码器的目标是从图像中提取特征,解码器放大编码器的输出,使它和原来的图像大小相同。
卷积运算的输出是通过用滑动窗口来扫描输入的卷积核,以及执行元素相乘和求和来实现的。
一个1x1的卷积本质上是与一组维数的滤波器进行卷积:
1x1卷积有助于降低该层的维数。相同大小的全连接层会产生相同数量的特征。然而,用卷积层替换全连接层有一个额外的好处,在(测试模型期间,可以将任何大小的图像输入到训练好的网络中。
import numpy as np
import tensorflow as tf
# 自定义初始化,默认设置种子为0
def custom_init(shape, dtype=tf.float32, partition_info=None, seed=0):
return tf.random_normal(shape, dtype=dtype, seed=seed)
# TODO:使用“tf.layers。conv2d '来重现' tf.layers. density '的结果。
# 设置 `kernel_size` 和 `stride`.
def conv_1x1(x, num_outputs):
kernel_size = 1
stride = 1
return tf.layers.conv2d(x, num_outputs, kernel_size, stride, weights_initializer=custom_init)
num_outputs = 2
x = tf.constant(np.random.randn(1, 2, 2, 1), dtype=tf.float32)
# 如果>的秩是2,则输入张量被‘tf. layer’压扁,然后再把它重塑回原来的秩作为输出
dense_out = tf.layers.dense(x, num_outputs, weights_initializer=custom_init)
conv_out = conv_1x1(x, num_outputs)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
a = sess.run(dense_out)
b = sess.run(conv_out)
print("Dense Output =", a)
print("Conv 1x1 Output =", b)
print("Same output? =", np.allclose(a, b, atol=1.e-5))
但是我发现并不能运行,程序报如下错误:
TypeError: conv2d() got an unexpected keyword argument 'weights_initializer'
经查询之后,tf.layers.dense
和tf.layers.conv2d
中并没有相关参数,于是将weights_initializer=custom_init
更改为kernel_initializer=custom_init
,程序正确输出如下:
Dense Output = [[[[-0.07399321 0.3901071 ]
[-0.21965009 1.1580395 ]]
[[ 0.29445606 -1.5524316 ]
[-0.7433553 3.9191186 ]]]]
Conv 1x1 Output = [[[[-0.07399321 0.3901071 ]
[-0.21965009 1.1580395 ]]
[[ 0.29445606 -1.5524316 ]
[-0.7433553 3.9191186 ]]]]
Same output? = True
1x1卷积函数注释:
tf.layers.conv2d(x, num_outputs, 1, 1, kernel_initializer=custom_init)
。
1
。1
。kernel_initializer=custom_init
:我们使用自定义初始化程序,因此dense层和卷积层中的权重相同。这就是保留空间信息的矩阵乘法运算。
我们可以使用转置卷积来创建全卷积网络的解码器。转置卷积本质上是一个反向卷积 ,其中前向和反向传播被调换。因此,我们称它为转置卷积。
有些人可能称它为反卷积,因为它撤销了前一个卷积,由于我们所做的只是调换前向传播和反向传播的顺序,这里的数学计算实际上和之前做的完全一样。因此,其可微性质保留了下来。而训练与之前的神经网络完全相同。
转置卷积有助于将上一层上采样到所需的分辨率或尺寸。假设有一个3x3的输入,并且希望将其上采样到所需的6x6尺寸。该过程涉及将输入的每个像素与内核或过滤器相乘。如果此过滤器的大小为5x5,则此操作的输出将为大小为5x5的加权内核。然后,该加权内核定义您的输出层。
但是,该过程的上采样部分由步幅和填充定义。在TensorFlow中,使用tf.layers.conv2d_transpose
,跨度为2和“ SAME”填充将导致输出尺寸为6x6。
如果我们有2x2输入和3x3内核;使用“ SAME”填充,并且跨度为2,我们可以预期输出尺寸为4x4。下图给出了该过程的想法。3x3加权内核(输入像素与3x3内核的乘积)由红色和蓝色正方形表示,它们之间的步幅为2。虚线正方形表示输出周围的填充。随着加权核的移动,步幅决定了输出的最终尺寸。这些参数的不同值将导致上采样输出的尺寸不同。
在TensorFlow中,API tf.layers.conv2d_transpose
用于创建转置的卷积层,程序如下:
import tensorflow as tf
import numpy as np
def upsample(x):
"""
对x应用两倍upsample并返回结果
"""
output = tf.layers.conv2d_transpose(x, 3, [2, 2], (2, 2), padding='SAME')
return output
x = tf.constant(np.random.randn(1, 4, 4, 3), dtype=tf.float32)
conv = upsample(x)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
result = sess.run(conv)
print('Input Shape: {}'.format(x.get_shape()))
print('Output Shape: {}'.format(result.shape))
输出如下:
Input Shape: (1, 4, 4, 3)
Output Shape: (1, 8, 8, 3)
程序注释
使用tf.layers.conv2d_transpose(x, 3, (2, 2), (2, 2))
上采样。
全卷积网络使用的第三种特殊技术,是跳跃连接。一般来说 卷积或编码的一个缺点是,当你仔细观察某张图片,会导致范围缩小,从而失去了更大的图片。因此 即使我们要将编码器的输出解码回原始图像尺寸,一些信息却已经丢失。
跳跃连接是轻松保留信息的一种方式。跳跃连接工作的方式是使一层的输出与一个非相邻层连接。这里使用按元素添加操作 ,将来自编码器的池化层的输出与当前层的输出相结合,最终的结果连接到下一层。这些跳跃连接使网络可以使用来自多分辨率的信息,因此网络能够做出更精确的分割决策。