1、图像分类与语义分割
通常CNN网络在卷积层之后会接上若干个全连接层, 将卷积层产生的特征图(feature map)映射成一个固定长度的特征向量。以AlexNet为代表的经典CNN结构适合于图像级的分类和回归任务,因为它们最后都期望得到整个输入图像的一个数值描述(概率),比如AlexNet的ImageNet模型输出一个1000维的向量表示输入图像属于每一类的概率(softmax归一化)。
FCN对图像进行像素级的分类,从而解决了语义级别的图像分割(semantic segmentation)问题。与经典的CNN在卷积层之后使用全连接层得到固定长度的特征向量进行分类(全联接层+softmax输出)不同,FCN可以接受任意尺寸的输入图像,采用反卷积层对最后一个卷积层的feature map进行上采样, 使它恢复到输入图像相同的尺寸,从而可以对每个像素都产生了一个预测, 同时保留了原始输入图像中的空间信息, 最后在上采样的特征图上进行逐像素分类(如上图)
2、全卷积网络(FCN)
可以看到:
- FCN以传统的卷积网络(VGG,ResNet等)作为基础网络,去掉最后的全连接层。
- 经过卷积层(conv1,conv2...)输入尺寸不变,而经过池化层后feature map尺寸减半。
- 经过pool3后feature map尺寸变为原始图像的1/8,经过pool4后变为1/16,经过pool5后变为1/32。
- 将pool5得到的1/32尺寸feature map(此时应叫heat map)进行上采样(转置卷积或双线性插值)得到1/16与pool4的1/16对应相加,得到新的1/16的heat map再进行上采样变为1/8,再与pool3的1/8进行对应相加操作。
- 最后对合并后的原图的1/8 的heat map进行上采样恢复到原图大小,此时的通道数应为分类的类别数。
3、FCN的pytorch实现
# 定义双线性插值,作为转置卷积的初始化权重参数
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 torch.from_numpy(weight)
class fcn(nn.Module):
def __init__(self, num_classes):
super(fcn, self).__init__()
pretrained_net = resnet34(pretrained=True)
self.stage1 = nn.Sequential(*list(pretrained_net.children())[:-4]) # 第一段
self.stage2 = list(pretrained_net.children())[-4] # 第二段
self.stage3 = list(pretrained_net.children())[-3] # 第三段
# 通道统一
self.scores1 = nn.Conv2d(512, num_classes, 1)
self.scores2 = nn.Conv2d(256, num_classes, 1)
self.scores3 = nn.Conv2d(128, num_classes, 1)
# 8倍上采样
self.upsample_8x = nn.ConvTranspose2d(num_classes, num_classes, 16, 8, 4, bias=False)
self.upsample_8x.weight.data = bilinear_kernel(num_classes, num_classes, 16) # 使用双线性 kernel
# 2倍上采样
self.upsample_4x = nn.ConvTranspose2d(num_classes, num_classes, 4, 2, 1, bias=False)
self.upsample_4x.weight.data = bilinear_kernel(num_classes, num_classes, 4) # 使用双线性 kernel
self.upsample_2x = nn.ConvTranspose2d(num_classes, num_classes, 4, 2, 1, bias=False)
self.upsample_2x.weight.data = bilinear_kernel(num_classes, num_classes, 4) # 使用双线性 kernel
def forward(self, x):
x = self.stage1(x)
s1 = x # 1/8
x = self.stage2(x)
s2 = x # 1/16
x = self.stage3(x)
s3 = x # 1/32
s3 = self.scores1(s3)
s3 = self.upsample_2x(s3) # 1/16
s2 = self.scores2(s2)
s2 = s2 + s3
s1 = self.scores3(s1)
s2 = self.upsample_4x(s2) # 1/8
s = s1 + s2
s = self.upsample_8x(s) # 1/1
return s
我们伪造一个batch的图像,输入网络,看看网络输出是怎样的
x = torch.randn(1,3,64,64) # 伪造图像
num_calsses = 21
net = fcn(num_classes)
y = net(x)
y.shape
输出:
torch.Size([1, 21, 64, 64])
可见,输出和原图一样为64 × 64大小,通道数为标签类别数21(VOC数据集有21个类别,含背景)的张量。
最后,21个通道上的每一个对应位置的像素分别预测一个类别的概率,保留概率最大的那个像素的索引,即可得到一个64×64的索引矩阵,根据索引矩阵找出对应像素属于哪个类别。
网友评论