原作者源码地址[32s, caffe]:https://github.com/shelhamer/fcn.berkeleyvision.org/blob/master/voc-fcn32s/net.py
(
基本忠于原作的翻译地址[32s, pytorch]:https://github.com/wkentaro/pytorch-fcn/blob/master/torchfcn/models/fcn32s.py
)
这里说原作的几个点。
def fcn(split):
n = caffe.NetSpec()
pydata_params = dict(split=split, mean=(104.00699, 116.66877, 122.67892),
seed=1337)
if split == 'train':
pydata_params['sbdd_dir'] = '../data/sbdd/dataset'
pylayer = 'SBDDSegDataLayer'
else:
pydata_params['voc_dir'] = '../data/pascal/VOC2011'
pylayer = 'VOCSegDataLayer'
n.data, n.label = L.Python(module='voc_layers', layer=pylayer,
ntop=2, param_str=str(pydata_params))# the base net
n.conv1_1, n.relu1_1 = conv_relu(n.data, 64, pad=100)
n.conv1_2, n.relu1_2 = conv_relu(n.relu1_1, 64)
n.pool1 = max_pool(n.relu1_2)n.conv2_1, n.relu2_1 = conv_relu(n.pool1, 128)
n.conv2_2, n.relu2_2 = conv_relu(n.relu2_1, 128)
n.pool2 = max_pool(n.relu2_2)n.conv3_1, n.relu3_1 = conv_relu(n.pool2, 256)
n.conv3_2, n.relu3_2 = conv_relu(n.relu3_1, 256)
n.conv3_3, n.relu3_3 = conv_relu(n.relu3_2, 256)
n.pool3 = max_pool(n.relu3_3)n.conv4_1, n.relu4_1 = conv_relu(n.pool3, 512)
n.conv4_2, n.relu4_2 = conv_relu(n.relu4_1, 512)
n.conv4_3, n.relu4_3 = conv_relu(n.relu4_2, 512)
n.pool4 = max_pool(n.relu4_3)n.conv5_1, n.relu5_1 = conv_relu(n.pool4, 512) # out shape=(13, 13)
n.conv5_2, n.relu5_2 = conv_relu(n.relu5_1, 512)
n.conv5_3, n.relu5_3 = conv_relu(n.relu5_2, 512)
n.pool5 = max_pool(n.relu5_3)# fully conv
n.fc6, n.relu6 = conv_relu(n.pool5, 4096, ks=7, pad=0) # out shape=(7, 7)
n.drop6 = L.Dropout(n.relu6, dropout_ratio=0.5, in_place=True)n.fc7, n.relu7 = conv_relu(n.drop6, 4096, ks=1, pad=0) # out shape=(7, 7)
n.drop7 = L.Dropout(n.relu7, dropout_ratio=0.5, in_place=True)n.score_fr = L.Convolution(n.drop7, num_output=21, kernel_size=1, pad=0,
param=[dict(lr_mult=1, decay_mult=1), dict(lr_mult=2, decay_mult=0)])
n.upscore = L.Deconvolution(n.score_fr,
convolution_param=dict(num_output=21, kernel_size=64, stride=32,
bias_term=False),
param=[dict(lr_mult=0)])
n.score = crop(n.upscore, n.data) # crop size to n.data
n.loss = L.SoftmaxWithLoss(n.score, n.label,
loss_param=dict(normalize=False, ignore_label=255))return n.to_proto()
一些trick在后来都不再使用了。
是像很多复现的论文那样,从conv5后开始上采样32倍得到原图224吗?
不是的。原作是从fc7开始上采样。
那么fc7难道不是feature map=1*1了吗?怎么上采样到224?
不是的。由于前面有padding=100,因此conv5的输出是13×13。fc6后是7×7,这个7×7里面,只有4×4的感受野对应原来的224图像,剩下的3是padding带来的。fc7只增加非线性,输出保持了7×7,但是这个7×7里面,应该学习到了原图224的特征。然后在此基础上作插值32倍。
为了应对小图片的情况。
在不做pad的情况下,当输入224的图片时,经过5个conv后输出是7×7,和fc6的kernel 7×7卷积,仍然有1个像素的输出。但是如果输入图片小于224时,conv之后输出小于7×7,都没有fc6的7×7的kernel大,因此fc6卷积后,就一个像素都没有了。因此,原作者使用了简单粗暴的方法,直接在conv前pad 100,保证小图片经过网络后也有输出。
回答:vgg16运行到pool5缩小了32倍,h0为输入图像大小,h6为输入到fc6的feature map大小,经过7x7卷积大小为:h6-7+1=h6-6=(h0/32)-6=(h0-6*32)/32=(h-192)/32,如果h0<192则报错(因为此时h6的输出将小于卷积核7*7的大小),因此FCN采用了一个简单的方法,直接在第一层卷积增加了一个100的padding,即增加100*2=200来解决这个问题。
FCN设计之初,这个kernel=7是为了替代全连接层的,即对于输入是7×7,kernel也是个7×7,conv就可以代替全连接了。同时当输入更大时,conv也可以适应。
但是等到后来为了解决小图片输入的问题增加了padding=100后,输入到fc6的feature map已经不是7×7了,而是13×13了,这里的ks=7也就没有了原来全连接的作用。
那为什么conv5使用3个kernel=3的卷积代替kernel=7的卷积,而fc6却不把这个kernel=7也换成3个kernel=3呢?应该是作者并没有进一步把注意力重新放在这里。当用kernel=7代替fully connection后,已经是重大突破了。并没有来第二轮重新检查这个地方是不是再可以改进。
为什么不用kernel=N,stride=N实现准确的上采样N倍。这个涉及到FCN的训练和权重初始化等更复杂的问题。
FCN的网络由分类网络和上采样网络两部分组成,其中分类网络权重采用预训练权重,上采样网络权重使用双线性插值初始化,kernel=2N是和双线性插值初始化相关的。64的kernel,双线性插值后就是32了。
因为前面padding了100,所以此处要截掉多余的部分,使输出和原输入的size大小一致。