fcn.md 8.7 KB
Newer Older
A
Aston Zhang 已提交
1
# 全卷积网络(FCN)
M
add fcn  
muli 已提交
2

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

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

7
```{.python .input  n=2}
8 9 10
import sys
sys.path.insert(0, '..')

A
Aston Zhang 已提交
11
%matplotlib inline
M
muli 已提交
12
import gluonbook as gb
A
Aston Zhang 已提交
13
from mxnet import gluon, image, init, nd
A
Aston Zhang 已提交
14
from mxnet.gluon import data as gdata, loss as gloss, model_zoo, nn
M
muli 已提交
15
import numpy as np
16
import sys
M
muli 已提交
17
```
M
add fcn  
muli 已提交
18

M
muli 已提交
19
## 转置卷积层
M
add fcn  
muli 已提交
20

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

A
Aston Zhang 已提交
23
下面我们构造一个卷积层并打印它的输出形状。
M
add fcn  
muli 已提交
24

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

A
Aston Zhang 已提交
29
x = nd.random.uniform(shape=(1, 3, 64, 64))
M
muli 已提交
30 31
y = conv(x)
y.shape
M
add fcn  
muli 已提交
32 33
```

F
Feywell 已提交
34
使用同样的卷积窗、填充和步幅的转置卷积层,我们可以得到和`x`形状一样的输出。
M
add fcn  
muli 已提交
35

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

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

M
muli 已提交
44
## FCN模型
M
add fcn  
muli 已提交
45

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

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

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

52
```{.python .input  n=5}
A
Aston Zhang 已提交
53 54 55
pretrained_net = model_zoo.vision.resnet18_v2(pretrained=True)
pretrained_net.features[-4:], pretrained_net.output
```
M
add fcn  
muli 已提交
56

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

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

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

67
```{.python .input  n=7}
A
Aston Zhang 已提交
68
x = nd.random.uniform(shape=(1, 3, 224, 224))
M
muli 已提交
69
net(x).shape
M
add fcn  
muli 已提交
70 71
```

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

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

A
Aston Zhang 已提交
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

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

86
```{.python .input  n=9}
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)
```

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

105
```{.python .input  n=10}
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

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

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

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

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

A
Aston Zhang 已提交
131 132
## 读取数据

A
Aston Zhang 已提交
133
我们使用较大的输入图像尺寸,其值选成了32的倍数。数据的读取方法已在上一节描述。
A
Aston Zhang 已提交
134

135
```{.python .input  n=13}
A
Aston Zhang 已提交
136
input_shape, batch_size, colormap2label = (320, 480), 32, nd.zeros(256**3)
A
Aston Zhang 已提交
137
for i, cm in enumerate(gb.VOC_COLORMAP):
138
    colormap2label[(cm[0] * 256 + cm[1]) * 256 + cm[2]] = i
A
Aston Zhang 已提交
139
voc_dir = gb.download_voc_pascal(data_dir='../data')
140

A
Aston Zhang 已提交
141 142 143 144 145 146
num_workers = 0 if sys.platform.startswith('win32') else 4
train_iter = gdata.DataLoader(
    gb.VOCSegDataset(True, input_shape, voc_dir, colormap2label), batch_size,
    shuffle=True, last_batch='discard', num_workers=num_workers)
test_iter = gdata.DataLoader(
    gb.VOCSegDataset(False, input_shape, voc_dir, colormap2label), batch_size,
147
    last_batch='discard', num_workers=num_workers)
A
Aston Zhang 已提交
148 149 150 151
```

## 训练

F
Feywell 已提交
152
这时候我们可以开始训练了。因为我们使用转置卷积层的通道来预测像素的类别,所以softmax是作用在通道这个维度(维度1)上的。于是,我们在`SoftmaxCrossEntropyLoss`里加入了额外的`axis=1`选项。
A
Aston Zhang 已提交
153 154

```{.python .input  n=12}
A
Aston Zhang 已提交
155
ctx = gb.try_all_gpus()
A
Aston Zhang 已提交
156
loss = gloss.SoftmaxCrossEntropyLoss(axis=1)
M
add fcn  
muli 已提交
157
net.collect_params().reset_ctx(ctx)
A
Aston Zhang 已提交
158
trainer = gluon.Trainer(net.collect_params(), 'sgd',
159
                        {'learning_rate': 0.1, 'wd': 1e-3})
A
Aston Zhang 已提交
160
gb.train(train_iter, test_iter, net, loss, trainer, ctx, num_epochs=5)
A
Aston Zhang 已提交
161 162
```

M
add fcn  
muli 已提交
163 164
## 预测

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

A
Aston Zhang 已提交
167
```{.python .input  n=13}
M
add fcn  
muli 已提交
168
def predict(im):
A
fix fcn  
Aston Zhang 已提交
169
    data = test_iter._dataset.normalize_image(im)
170
    data = data.transpose((2, 0, 1)).expand_dims(axis=0)
M
add fcn  
muli 已提交
171 172 173
    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 已提交
174 175 176 177
```

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

A
Aston Zhang 已提交
178
```{.python .input  n=14}
M
add fcn  
muli 已提交
179
def label2image(pred):
A
Aston Zhang 已提交
180
    colormap = nd.array(gb.VOC_COLORMAP, ctx=ctx[0], dtype='uint8')
M
muli 已提交
181
    x = pred.astype('int32')
182
    return colormap[x, :]
M
add fcn  
muli 已提交
183 184
```

A
Aston Zhang 已提交
185
现在我们读取前几张测试图像并对其进行预测。
M
add fcn  
muli 已提交
186

A
Aston Zhang 已提交
187
```{.python .input  n=15}
M
muli 已提交
188
test_images, test_labels = gb.read_voc_images(train=False)
M
add fcn  
muli 已提交
189

M
muli 已提交
190
n = 5
M
add fcn  
muli 已提交
191 192 193
imgs = []
for i in range(n):
    x = test_images[i]
A
Aston Zhang 已提交
194 195
    pred = label2image(predict(x))
    imgs += [x, pred, test_labels[i]]
M
muli 已提交
196

A
Aston Zhang 已提交
197
gb.show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n);
A
Aston Zhang 已提交
198 199
```

A
Aston Zhang 已提交
200
## 小结
M
add fcn  
muli 已提交
201

F
Feywell 已提交
202
* FCN通过转置卷积层来为每个像素预测类别。
M
add fcn  
muli 已提交
203 204 205

## 练习

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

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

A
Aston Zhang 已提交
213
![](../img/qr_fcn.svg)
M
muli 已提交
214 215 216 217


## 参考文献

A
Aston Zhang 已提交
218
[1] Long, J., Shelhamer, E., & Darrell, T. (2015). Fully convolutional networks for semantic segmentation. In Proceedings of the IEEE conference on computer vision and pattern recognition (pp. 3431-3440).