fcn.md 7.9 KB
Newer Older
M
muli 已提交
1
# 全卷积网络:FCN
M
add fcn  
muli 已提交
2

M
muli 已提交
3
在图片分类里,我们通过卷积层和池化层逐渐减少图片高宽最终得到跟预测类别数长的向量。例如用于ImageNet分类的ResNet 18里,我们将高宽为224的输入图片首先减少到高宽7,然后使用全局池化层得到512维输出,最后使用全连接层输出长为1000的预测向量。
M
add fcn  
muli 已提交
4

M
muli 已提交
5
但在语义分割里,我们需要对每个像素预测类别,也就是需要输出形状需要是$1000\times 224\times 224$。如果仍然使用全链接层作为输出,那么这一层权重将多达数百GB。本小节我们将介绍利用卷积神经网络解决语义分割的一个开创性工作之一:全卷积网络(fully convolutional network,简称FCN)[1]。FCN里将最后的全连接层修改称转置卷积层(transposed convolution)来得到所需大小的输出。
M
add fcn  
muli 已提交
6

M
muli 已提交
7
```{.python .input  n=2}
M
muli 已提交
8 9 10 11
%matplotlib inline
import sys
sys.path.append('..')
import gluonbook as gb
M
muli 已提交
12
from mxnet import gluon, init, nd, image
M
muli 已提交
13 14 15
from mxnet.gluon import nn
import numpy as np
```
M
add fcn  
muli 已提交
16

M
muli 已提交
17
## 转置卷积层
M
add fcn  
muli 已提交
18

M
muli 已提交
19
假设$f$是一个卷积层,给定输入$x$,我们可以计算前向输出$y=f(x)$。在反向求导$z=\frac{\partial\, f(y)}{\partial\,x}$时,我们知道$z$会得到跟$x$一样形状的输出。因为卷积运算的导数的导数是自己本身,我们可以合法定义转置卷积层,记为$g$,为交互了前向和反向求导函数的卷积层。也就是$z=g(y)$。
M
add fcn  
muli 已提交
20

M
muli 已提交
21
下面我们构造一个卷积层并打印其输出形状。
M
add fcn  
muli 已提交
22

M
muli 已提交
23
```{.python .input  n=3}
M
muli 已提交
24 25
conv = nn.Conv2D(10, kernel_size=4, padding=1, strides=2)
conv.initialize()
M
add fcn  
muli 已提交
26

M
muli 已提交
27 28 29
x = nd.random.uniform(shape=(1,3,64,64))
y = conv(x)
y.shape
M
add fcn  
muli 已提交
30 31
```

M
muli 已提交
32
使用用样的卷积窗、填充和步幅的转置卷积层,我们可以得到跟`x`一样的输出。
M
add fcn  
muli 已提交
33

M
muli 已提交
34
```{.python .input  n=4}
M
add fcn  
muli 已提交
35 36
conv_trans = nn.Conv2DTranspose(3, kernel_size=4, padding=1, strides=2)
conv_trans.initialize()
M
muli 已提交
37
conv_trans(y).shape
M
add fcn  
muli 已提交
38 39
```

M
muli 已提交
40
简单来说,卷积层通常使得输入高宽变小,而转置卷积层则一般用来将高宽增大。
M
add fcn  
muli 已提交
41

M
muli 已提交
42
## FCN模型
M
add fcn  
muli 已提交
43

M
muli 已提交
44
FCN的核心思想是将一个卷积网络的最后全连接输出层替换成转置卷积层来获取对每个输入像素的预测。具体来说,它去掉了过于损失空间信息的全局池化层,并将最后的全链接层替换成输出通道是原全连接层输出大小的$1\times 1$卷积层,最后接上卷积转置层来得到需要形状的输出。
M
add fcn  
muli 已提交
45

M
muli 已提交
46
![FCN模型。](../img/fcn.svg)
M
add fcn  
muli 已提交
47

M
muli 已提交
48
下面我们基于ResNet 18来创建FCN。首先我们下载一个预先训练好的模型,并打印其最后的数个神经层。
M
add fcn  
muli 已提交
49

M
muli 已提交
50
```{.python .input  n=3}
M
muli 已提交
51
pretrained_net = gluon.model_zoo.vision.resnet18_v2(pretrained=True)
M
add fcn  
muli 已提交
52 53 54 55

(pretrained_net.features[-4:], pretrained_net.output)
```

M
muli 已提交
56
可以看到`feature`模块最后两层是`GlobalAvgPool2D``Flatten`,在FCN里均不需要,`output`模块里的全链接层也需要舍去。下面我们定义一个新的网络,它复制除了`feature`里除去最后两层的所有神经层以及权重。
M
add fcn  
muli 已提交
57

M
muli 已提交
58
```{.python .input  n=4}
M
add fcn  
muli 已提交
59 60 61
net = nn.HybridSequential()
for layer in pretrained_net.features[:-2]:
    net.add(layer)
M
muli 已提交
62
```
M
muli 已提交
63

M
muli 已提交
64 65 66 67 68
给定高宽为224的输入,`net`的输出将输入高宽减少了32倍。

```{.python .input}
x = nd.random.uniform(shape=(1,3,224,224))
net(x).shape
M
add fcn  
muli 已提交
69 70
```

M
muli 已提交
71
为了是的输出跟输入有同样的高宽,我们构建一个步幅为32的转置卷积层,卷积核的窗口高宽设置成步幅的2倍,并补充适当的填充。在转置卷积层之前,我们加上$1\times 1$卷积层来将通道数从512降到标注类别数,对Pascal VOC数据集来说是21。
M
add fcn  
muli 已提交
72

M
muli 已提交
73 74
```{.python .input  n=5}
num_classes = 21
M
add fcn  
muli 已提交
75

M
muli 已提交
76 77 78 79
net.add(
    nn.Conv2D(num_classes, kernel_size=1),
    nn.Conv2DTranspose(num_classes, kernel_size=64, padding=16,strides=32)
)
M
add fcn  
muli 已提交
80 81
```

M
muli 已提交
82
## 模型初始化
M
add fcn  
muli 已提交
83

M
muli 已提交
84
`net`中的最后两层需要对权重进行初始化,通常我们会使用随机初始化。但新加入的转置卷积层的功能有些类似于将输入调整到更大的尺寸。在图片处理里面,我们可以通过有适当卷积核的卷积运算符来完成这个操作。常用的包括双线性差值核,下面函数构造核权重。
M
add fcn  
muli 已提交
85

M
muli 已提交
86
```{.python .input  n=6}
M
add fcn  
muli 已提交
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
def bilinear_kernel(in_channels, out_channels, kernel_size):
    factor = (kernel_size + 1) // 2
    if kernel_size % 2 == 1:
        center = factor - 1
    else:
        center = factor - 0.5
    og = np.ogrid[:kernel_size, :kernel_size]
    filt = (1 - abs(og[0] - center) / factor) * \
           (1 - abs(og[1] - center) / factor)
    weight = np.zeros(
        (in_channels, out_channels, kernel_size, kernel_size),
        dtype='float32')
    weight[range(in_channels), range(out_channels), :, :] = filt
    return nd.array(weight)
```

M
muli 已提交
103
接下来我们构造一个步幅为2的转置卷积层,将其权重初始化成双线性差值核。
M
muli 已提交
104

M
muli 已提交
105
```{.python .input  n=7}
106
conv_trans = nn.Conv2DTranspose(3, kernel_size=4, padding=1, strides=2)
M
muli 已提交
107 108
conv_trans.initialize(init.Constant(bilinear_kernel(3, 3, 4)))
```
M
add fcn  
muli 已提交
109

M
muli 已提交
110
可以看到这个转置卷积层的前向函数的效果是将输入图片高宽扩大2倍。
M
add fcn  
muli 已提交
111

M
muli 已提交
112 113 114 115
```{.python .input}
img = image.imread('../img/catdog.jpg')
print('Input', img.shape)
x = img.astype('float32').transpose((2,0,1)).expand_dims(axis=0)/255
M
add fcn  
muli 已提交
116
y = conv_trans(x)
M
muli 已提交
117
y = y[0].clip(0,1).transpose((1,2,0))
M
add fcn  
muli 已提交
118
print('Output', y.shape)
M
muli 已提交
119
gb.plt.imshow(y.asnumpy());
M
add fcn  
muli 已提交
120 121
```

M
muli 已提交
122
下面对`net`的最后两层进行初始化。其中$1\times 1$卷积层使用Xavier,转置卷积层则使用双线性差值核。
M
add fcn  
muli 已提交
123

M
muli 已提交
124 125 126
```{.python .input  n=8}
trans_conv_weights = bilinear_kernel(num_classes, num_classes, 64)
net[-1].initialize(init.Constant(trans_conv_weights))
M
muli 已提交
127
net[-2].initialize(init=init.Xavier())
M
add fcn  
muli 已提交
128 129
```

M
muli 已提交
130
## 训练
M
add fcn  
muli 已提交
131

M
muli 已提交
132
这时候我们可以真正开始训练了。我们使用较大的输入图片尺寸,其值选成了32的倍数。因为我们使用转置卷积层的通道来预测像素的类别,所以在做softmax是作用在通道这个维度(维度1),所以在`SoftmaxCrossEntropyLoss`里加入了额外了`axis=1`选项。
M
add fcn  
muli 已提交
133

M
muli 已提交
134 135 136
```{.python .input  n=9}
input_shape = (320, 480)
batch_size = 32
A
Aston Zhang 已提交
137
ctx = gb.try_all_gpus()
M
muli 已提交
138
loss = gluon.loss.SoftmaxCrossEntropyLoss(axis=1)
M
add fcn  
muli 已提交
139 140 141
net.collect_params().reset_ctx(ctx)
trainer = gluon.Trainer(net.collect_params(),
                        'sgd', {'learning_rate': .1, 'wd':1e-3})
M
muli 已提交
142
train_data, test_data = gb.load_data_pascal_voc(batch_size, input_shape)
M
muli 已提交
143
gb.train(train_data, test_data, net, loss, trainer, ctx, num_epochs=10)
M
add fcn  
muli 已提交
144 145 146 147
```

## 预测

M
muli 已提交
148
预测一张新图片时,我们只需要将其归一化并转成卷积网络需要的4D格式。
M
add fcn  
muli 已提交
149

M
muli 已提交
150
```{.python .input  n=57}
M
add fcn  
muli 已提交
151
def predict(im):
M
muli 已提交
152
    data = test_data._dataset.normalize_image(im)
M
muli 已提交
153
    data = data.transpose((2,0,1)).expand_dims(axis=0)
M
add fcn  
muli 已提交
154 155 156 157
    yhat = net(data.as_in_context(ctx[0]))
    pred = nd.argmax(yhat, axis=1)
    return pred.reshape((pred.shape[1], pred.shape[2]))

M
muli 已提交
158 159 160 161 162
```

同时我们根据每个像素预测的类别找出其RGB颜色以便画图。

```{.python .input}
M
add fcn  
muli 已提交
163
def label2image(pred):
M
muli 已提交
164 165 166 167
    colormap = nd.array(
        test_data._dataset.voc_colormap, ctx=ctx[0],dtype='uint8')
    x = pred.astype('int32')
    return colormap[x,:]
M
add fcn  
muli 已提交
168 169
```

M
muli 已提交
170
现在我们读取前几张测试图片并对其进行预测。
M
add fcn  
muli 已提交
171

M
muli 已提交
172 173
```{.python .input  n=58}
test_images, test_labels = gb.read_voc_images(train=False)
M
add fcn  
muli 已提交
174

M
muli 已提交
175
n = 5
M
add fcn  
muli 已提交
176 177 178
imgs = []
for i in range(n):
    x = test_images[i]
M
muli 已提交
179 180
    pred = label2image(predict(x))
    imgs += [x, pred, test_labels[i]]
M
muli 已提交
181

M
muli 已提交
182
gb.show_images(imgs[::3]+imgs[1::3]+imgs[2::3], 3, n);
M
add fcn  
muli 已提交
183 184
```

A
Aston Zhang 已提交
185
## 小结
M
add fcn  
muli 已提交
186

M
muli 已提交
187
FCN通过使用转置卷积层来为每个像素预测类别。
M
add fcn  
muli 已提交
188 189 190

## 练习

M
muli 已提交
191 192 193 194
* 试着改改最后的转置卷积层的参数设定。
* 看看双线性差值初始化是不是必要的。
* 试着改改训练参数来使得收敛更好些。
* FCN论文[1]中提到了不只是使用主体卷积网络输出,还可以考虑其中间层的输出。试着实现这个想法。
S
Sheng Zha 已提交
195

A
Aston Zhang 已提交
196
## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/3041)
S
Sheng Zha 已提交
197

A
Aston Zhang 已提交
198
![](../img/qr_fcn.svg)
M
muli 已提交
199 200 201 202 203


## 参考文献

[1] Long, Jonathan, Evan Shelhamer, and Trevor Darrell. "Fully convolutional networks for semantic segmentation." CVPR. 2015.