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

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

H
Hang Zhang 已提交
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
M
muli 已提交
13
from mxnet import gluon, init, nd, image
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

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

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

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

87
```{.python .input  n=9}
M
add fcn  
muli 已提交
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
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 已提交
104
接下来我们构造一个步幅为2的转置卷积层,将其权重初始化为双线性插值核。
M
muli 已提交
105

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

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

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

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

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

A
Aston Zhang 已提交
132 133 134 135
## 读取数据

我们使用较大的输入图片尺寸,其值选成了32的倍数。数据的读取方法已在上一节描述。

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

A
Aston Zhang 已提交
144 145 146 147 148 149 150
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,
    last_batch='discard', num_workers=num_workers) 
A
Aston Zhang 已提交
151 152 153 154
```

## 训练

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

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

M
add fcn  
muli 已提交
166 167
## 预测

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

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

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

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

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

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

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

A
Aston Zhang 已提交
200
gb.show_images(imgs[::3] + imgs[1::3] + imgs[2::3], 3, n);
A
Aston Zhang 已提交
201 202
```

A
Aston Zhang 已提交
203
## 小结
M
add fcn  
muli 已提交
204

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

## 练习

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

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

A
Aston Zhang 已提交
216
![](../img/qr_fcn.svg)
M
muli 已提交
217 218 219 220


## 参考文献

A
Aston Zhang 已提交
221
[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).