本文分为两个部分,第一部分详解FCN原理,第二部分给出简单的代码实现。

第一部分:算法理解

(理解FCN需要有CNN基础)

0.前言:基于CNN的分割方法与FCN的比较

传统的基于CNN的分割方法:为了对一个像素分类,使用该像素周围的一个图像块作为CNN的输入用于训练和预测。这种方法有几个缺点:

一是存储开销很大。例如对每个像素使用的图像块的大小为15x15,然后不断滑动窗口,每次滑动的窗口给CNN进行判别分类,因此则所需的存储空间根据滑动窗口的次数和大小急剧上升。

二是计算效率低下。相邻的像素块基本上是重复的,针对每个像素块逐个计算卷积,这种计算也有很大程度上的重复。

三是像素块大小的限制了感知区域的大小。通常像素块的大小比整幅图像的大小小很多,只能提取一些局部的特征,从而导致分类的性能受到限制。

而全卷积网络(FCN)则是从抽象的特征中恢复出每个像素所属的类别。即从图像级别的分类进一步延伸到像素级别的分类。

概括

FCN将传统卷积网络后面的全连接层换成了卷积层,这样网络输出不再是类别而是 heatmap;同时为了解决因为卷积和池化对图像尺寸的影响,提出使用上采样的方式恢复。

Keypoint

  • 1.不含全连接层(fc)的全卷积(fully conv)网络。可适应任意尺寸输入。
  • 2.增大数据尺寸的反卷积(deconv)层。能够输出精细的结果。
  • 3.结合不同深度层结果的跳级(skip)结构。同时确保鲁棒性和精确性。

网络结构详解图:

输入可为任意尺寸图像彩色图像;输出与输入尺寸相同,深度为:20类目标+背景=21。

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_计算机视觉


下面详细理解这三个核心思想

1.全连接层→全卷积

通常CNN网络在卷积层之后会接上若干个全连接层, 将卷积层产生的特征图(feature map)映射成一个固定长度的特征向量。与经典的CNN不同,FCN可以接受任意尺寸的输入图像,采用反卷积层对最后一个卷积层的feature map进行上采样, 使它恢复到输入图像相同的尺寸,从而可以对每个像素都产生了一个预测, 同时保留了原始输入图像中的空间信息, 最后在上采样的特征图上进行逐像素分类。最后逐个像素计算softmax分类的损失, 相当于每一个像素对应一个训练样本。

FCN将CNN中的全连接层用卷积层替换,全连接层和卷积层之间唯一的不同就是卷积层中的神经元只与输入数据中的一个局部区域连接,并且不同卷积列中的神经元共享参数。然而在两类层中,神经元都是计算点积,所以它们的函数形式是一样的。因此,将此两者相互转化是可能的:

对于任一个卷积层,都存在一个能实现和它一样的前向传播函数的全连接层:权重矩阵是一个巨大的矩阵,除了某些特定块,其余部分都是零;且在其中大部分块中,元素都是相等的。

同样,任何全连接层都可以被转化为卷积层。比如,一个 K=4096 的全连接层,输入数据体的尺寸是 7∗7∗512,这个全连接层可以被等效地看做一个 F=7,P=0,S=1,K=4096 的卷积层。换句话说,就是将滤波器的尺寸设置为和输入数据体的尺寸一致了。因为只有一个单独的深度列覆盖并滑过输入数据体,所以输出将变成 1∗1∗4096,这个结果就和使用初始的那个全连接层一样了。

2.上采样-反卷积(deconvolution)

经过多次卷积和pooling以后,得到的图像越来越小,分辨率越来越低。其中图像小的一层时(下图的H/32 x W/32),所产生图叫做heatmap(热图),热图就是我们最重要的高维特征图,得到高维特征的heatmap之后就是最重要的一步也是最后的一步对原图像进行upsampling,把图像进行放大、放大、放大,到原图像的大小。最后的输出是n(类别数)个通道heatmap经过upsampling变为原图大小的图片,为了对每个像素进行分类预测label成最后已经进行语义分割的图像,通过逐个像素地求其在n个通道上该像素位置的最大数值描述(概率)作为该像素的分类,由此产生了一张已经分类好的图片。

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_卷积_02


上采样(upsampling),简单来说就是pooling的逆过程,pooling采样后数据数量减少,upsample采样后数据数量增多。FCN作者在论文中讨论了3种upsample方法(双线性插值、反卷积、反池化),最后选用的是反卷积的方法(FCN作者称其为后卷积)使图像实现end to end,可以理解upsample就是使大小比原图像小得多的特征图变大,使其大小为原图像大小。

2.1 反卷积(deconvolution)详解

众所诸知,普通的池化会缩小图片的尺寸,比如VGG16 五次池化后图片被长和宽被缩小了32倍。为了得到和原图等大的分割图,我们需要采用反卷积。
反卷积运算的参数和CNN的卷积运算的参数一样,都是在训练FCN模型的过程中通过BP算法学习得到。且都是相乘相加的运算。只不过卷积是多对一,反卷积是一对多(github上有一个项目用于演示各种卷积的动态过程)。而反卷积的前向和后向传播,只用颠倒卷积的前后向传播即可。所以无论优化还是后向传播算法都是没有问题。

2.1.1通过数学推导来理解反卷积过程

先看卷积过程:

假设输入图像 input 尺寸为 4 x 4 ,元素矩阵为:

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_2d_03


卷积核kernel的尺寸为3 x 3,元素矩阵为:

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_pytorch_04


步长 strides = 1,填充 padding = 0 ,即 i = 4, k=3, s=1, p=0

则按照卷积计算公式 o = (i+2p-k) \ s + 1,输出图像 的尺寸为 2 x 2。

矩阵乘法来描述卷过程
把input和ouput展开为一个列向量:

input = X = [x1, x2, …, x16]T
output = Y = [y1, y2, y3, y4]T

对于输入的元素矩阵X和 输出的元素矩阵 Y,用矩阵运算描述这个过程:

Y = CX

通过推导,我们可以得到C为一个稀疏矩阵,其元素值为重复的kernel的元素值:

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_全连接_05

反卷积的操作就是要对这个矩阵运算过程进行逆运算,即通过 C 和 Y 得到 X ,根据各个矩阵的尺寸大小,我们能很轻易的得到计算的过程,即为反卷积的操作:

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_计算机视觉_06


则通学习kernel(即C的参数)的参数,就可以得到我们想要的X。** 2.1.2 反卷积的输入输出尺寸关系**

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_全连接_07


通过卷积运算的尺寸关系公式反推(把i换成o, o换成i)即可得到,反卷积的尺寸关系:

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_pytorch_08


再考虑

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_2d_09



EfficientNet所要求的PyTorch版本是多少 fcn pytorch_2d_10


注:pytorch中nn.ConvTranspose2d()可以视为反卷积操作,但不是真正的反卷积操作(详参考官方文档),输出尺寸的计算稍有不同:

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_2d_11

3.跳跃结构(skip)

经过前面操作,基本就能实现语义分割了,但是直接将全卷积后的heatMap结果进行反卷积,得到的结果往往比较粗糙。

采用vgg16对原图像进行卷积conv1、pool1后原图像缩小为1/2;之后对图像进行第二次conv2、pool2后图像缩小为1/4;接着继续对图像进行第三次卷积操作conv3、pool3缩小为原图像的1/8,此时保留pool3的featureMap;接着继续对图像进行第四次卷积操作conv4、pool4,缩小为原图像的1/16,保留pool4的featureMap;最后对图像进行第五次卷积操作conv5、pool5,缩小为原图像的1/32,然后把原来CNN操作中的全连接变成卷积操作conv6、conv7,图像的featureMap数量改变但是图像大小依然为原图的1/32,此时图像不再叫featureMap而是叫heatMap

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_全连接_12


现在我们有1/32尺寸的heatMap,1/16尺寸的featureMap和1/8尺寸的featureMap,1/32尺寸的heatMap进行upsampling操作之后,因为这样的操作还原的图片仅仅是conv5中的卷积核中的特征,限于精度问题不能够很好地还原图像当中的特征。因此在这里向前迭代,把conv4中的卷积核对上一次upsampling之后的图进行反卷积补充细节,最后把conv3中的卷积核对刚才upsampling之后的图像进行再次反卷积补充细节,最后就完成了整个图像的还原。

具体来说,就是将不同池化层的结果进行上采样,然后结合这些结果来优化输出,分为FCN-32s,FCN-16s,FCN-8s三种,第一行对应FCN-32s,第二行对应FCN-16s,第三行对应FCN-8s。 具体结构如下:

如图FCN32s、FCN16s和FCN8s 的前向计算过程已经很明了:

(1) FCN32s
结构图中第一行,原图在经过vgg16的block1-5后,尺寸变为原来的1/32,在经过conv6、conv7后得到heatMap仍为原来的1/32,这里通过一个反卷积层,将输入来的heatMap的尺寸变为32倍,于是得到了一个与原图尺寸一样特征图,再通过一个卷积层(不改变图像尺寸)将图像通道数改变为分类的类别数量,最后通过softmax,则每个像素点的位置处是一个深度为类别数量的向量,每个值分别代表属于对应类别的概率,最后再通过np.argmax()就可以得到一张二维数据图,每个像素值就代表对应的类别。

(2) FCN16s
结构图中第二行,即先将conv7通过一个反卷积层,尺寸放大两倍(此时尺寸与pool4的featureMap尺寸相同),再与pool4得到的特征图结合(结合方式有两种说法,一个是数据直接相加,一个是在通道方向上拼接,个人觉得二者都可,有兴趣可以自己实验那种效果较好)。然后再通过一个反卷积层,将尺寸放大16倍,得到原图的尺寸。之后的操作和FCN32s一样了

(3)FCN8s
结构图中第三行,以类似的方式将pool3、pool4的featureMap和conv7d的heatMap结合起来。

实验表明FCN-8s优于FCN-16s,FCN-32s。
我们可以发现,如果继续仿照FCN作者的步骤,我们可以对pool2,pool1实现同样的方法,可以有FCN-4s,FCN-2s,最后得到end to end的输出。这里作者给出了明确的结论,超过FCN-8s之后,结果并不能继续优化。

训练思路

训练过程分为四个阶段,也体现了作者的设计思路,值得学习研究。

第1阶段

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_2d_13


先训练好经典的分类网络,再弃去最后两级是全连接(红色)不用。一般我们直接使用训练好的vgg16。第二阶段

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_全连接_14


直接upsample得到预测图,即训练FCN32s第三阶段

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_pytorch_15


升采样分为两次完成(橙色×2)。

在第二次升采样前,把第4个pooling层(绿色)的预测结果(蓝色)融合进来。使用跳级结构提升精确性。 第二次反卷积放大16倍,这个网络称为FCN-16s。第四阶段

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_2d_16


升采样分为三次完成(橙色×3)。

进一步融合了第3个pooling层的预测结果。

第三次反卷积步长为8,记为FCN-8s。较浅层的预测结果包含了更多细节信息。比较2,3,4阶段可以看出,跳级结构利用浅层信息辅助逐步升采样,有更精细的结果。

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_2d_17

总结

FCN的核心贡献在于提出使用卷积层通过学习让图片实现end to end分类。
事实上,FCN有一些短处,例如使用了较浅层的特征,因为fuse操作会加上较上层的pool特征值,导致高维特征不能很好得以使用,同时也因为使用较上层的pool特征值,导致FCN对图像大小变化有所要求,如果测试集的图像远大于或小于训练集的图像,FCN的效果就会变差。

第二部分、pytorch实现

注:本文简单实现FCN8s(为方便,直接训练FCN8s,而不分为四个阶段训练), 特征采用pytorch提供的在ImageNet上训练好的vgg16网络,采用比较简单的数据集(附在文末参考资料部分)。

先导入如需要的包。(实际项目中最好讲不同的功能块写在不同的脚本中,方便管理和调试,这里为了展示方便,把所有的代码写在一个脚本里了)

import os

import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset, random_split
from torchvision import transforms
from torchvision import models
from torchvision.models.vgg import VGG

import cv2
import numpy as np

1.数据集预处理

这部分参考自附录gitbub源码中的onehot.py 和 BagData.py中,下面是代码和注释。

# 将标记图(每个像素值代该位置像素点的类别)转换为onehot编码
def onehot(data, n):
    buf = np.zeros(data.shape + (n, ))
    nmsk = np.arange(data.size)*n + data.ravel()
    buf.ravel()[nmsk-1] = 1
    return buf

# 利用torchvision提供的transform,定义原始图片的预处理步骤(转换为tensor和标准化处理) 
transform = transforms.Compose([
    transforms.ToTensor(), 
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])

# 利用torch提供的Dataset类,定义我们自己的数据集
class BagDataset(Dataset):

    def __init__(self, transform=None):
        self.transform = transform
        
    def __len__(self):
        return len(os.listdir('./bag_data'))

    def __getitem__(self, idx):
        img_name = os.listdir('./bag_data')[idx]
        imgA = cv2.imread('./bag_data/'+img_name)
        imgA = cv2.resize(imgA, (160, 160))
        imgB = cv2.imread('./bag_data_msk/'+img_name, 0)
        imgB = cv2.resize(imgB, (160, 160))
        imgB = imgB/255
        imgB = imgB.astype('uint8')
        imgB = onehot(imgB, 2)
        imgB = imgB.transpose(2,0,1)
        imgB = torch.FloatTensor(imgB)
        #print(imgB.shape)
        if self.transform:
            imgA = self.transform(imgA)    

        return imgA, imgB

# 实例化数据集
bag = BagDataset(transform)

train_size = int(0.9 * len(bag))
test_size = len(bag) - train_size
train_dataset, test_dataset = random_split(bag, [train_size, test_size])

# 利用DataLoader生成一个分batch获取数据的可迭代对象
train_dataloader = DataLoader(train_dataset, batch_size=4, shuffle=True, num_workers=4)
test_dataloader = DataLoader(test_dataset, batch_size=4, shuffle=True, num_workers=4)

这里是利用torch的Dataset和DataLoader加载训练数据,下面是演示:

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_pytorch_18


得到的输出为:

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_计算机视觉_19

2.网络模型搭建

这部分参考自附录github源码的FCN.py,发现他的FCN8s(事实上是上他定义的所有FCN) 结构缺少了conv6和conv7,下面是带注释的代码:

# <-------------------------------------------------------->#
# 下面开始定义网络模型
# 先定义VGG结构

# ranges 是用于方便获取和记录每个池化层得到的特征图
# 例如vgg16,需要(0, 5)的原因是为方便记录第一个pooling层得到的输出(详见下午、稳VGG定义)
ranges = {
    'vgg11': ((0, 3), (3, 6),  (6, 11),  (11, 16), (16, 21)),
    'vgg13': ((0, 5), (5, 10), (10, 15), (15, 20), (20, 25)),
    'vgg16': ((0, 5), (5, 10), (10, 17), (17, 24), (24, 31)),
    'vgg19': ((0, 5), (5, 10), (10, 19), (19, 28), (28, 37))
}

# Vgg网络结构配置(数字代表经过卷积后的channel数,‘M’代表卷积层)
cfg = {
    'vgg11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'vgg13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'vgg16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
    'vgg19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}

# 由cfg构建vgg-Net的卷积层和池化层(block1-block5)
def make_layers(cfg, batch_norm=False):
    layers = []
    in_channels = 3
    for v in cfg:
        if v == 'M':
            layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
        else:
            conv2d = nn.Conv2d(in_channels, v, kernel_size=3, padding=1)
            if batch_norm:
                layers += [conv2d, nn.BatchNorm2d(v), nn.ReLU(inplace=True)]
            else:
                layers += [conv2d, nn.ReLU(inplace=True)]
            in_channels = v
    return nn.Sequential(*layers)

# 下面开始构建VGGnet
class VGGNet(VGG):
    def __init__(self, pretrained = True, model='vgg16', requires_grad=True, remove_fc=True, show_params=False):
        super().__init__(make_layers(cfg[model]))
        self.ranges = ranges[model]
        
        # 获取VGG模型训练好的参数,并加载(第一次执行需要下载一段时间)
        if pretrained:
            exec("self.load_state_dict(models.%s(pretrained=True).state_dict())" % model)

        if not requires_grad:
            for param in super().parameters():
                param.requires_grad = False

        # 去掉vgg最后的全连接层(classifier)
        if remove_fc:  
            del self.classifier

        if show_params:
            for name, param in self.named_parameters():
                print(name, param.size())

    def forward(self, x):
        output = {}
        # 利用之前定义的ranges获取每个maxpooling层输出的特征图
        for idx, (begin, end) in enumerate(self.ranges):
        #self.ranges = ((0, 5), (5, 10), (10, 17), (17, 24), (24, 31)) (vgg16 examples)
            for layer in range(begin, end):
                x = self.features[layer](x)
            output["x%d"%(idx+1)] = x
        # output 为一个字典键x1d对应第一个maxpooling输出的特征图,x2...x5类推
        return output

# 下面由VGG构建FCN8s
class FCN8s(nn.Module):

    def __init__(self, pretrained_net, n_class):
        super().__init__()
        self.n_class = n_class
        self.pretrained_net = pretrained_net
        self.conv6 = nn.Conv2d(512, 512, kernel_size=1, stride=1, padding=0, dilation=1)
        self.conv7 = nn.Conv2d(512, 512, kernel_size=1, stride=1, padding=0, dilation=1)
        self.relu    = nn.ReLU(inplace=True)
        self.deconv1 = nn.ConvTranspose2d(512, 512, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn1     = nn.BatchNorm2d(512)
        self.deconv2 = nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn2     = nn.BatchNorm2d(256)
        self.deconv3 = nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn3     = nn.BatchNorm2d(128)
        self.deconv4 = nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn4     = nn.BatchNorm2d(64)
        self.deconv5 = nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
        self.bn5     = nn.BatchNorm2d(32)
        self.classifier = nn.Conv2d(32, n_class, kernel_size=1)

    def forward(self, x):
        output = self.pretrained_net(x)
        x5 = output['x5']    # maxpooling5的feature map (1/32)
        x4 = output['x4']    # maxpooling4的feature map (1/16)
        x3 = output['x3']    # maxpooling3的feature map (1/8)
    
        score = self.relu(self.conv6(x5))    # conv6  size不变 (1/32)
        score = self.relu(self.conv7(score)) # conv7  size不变 (1/32)
        score = self.relu(self.deconv1(x5))   # out_size = 2*in_size (1/16)       
        score = self.bn1(score + x4)                      
        score = self.relu(self.deconv2(score)) # out_size = 2*in_size (1/8)           
        score = self.bn2(score + x3)                      
        score = self.bn3(self.relu(self.deconv3(score)))  # out_size = 2*in_size (1/4)
        score = self.bn4(self.relu(self.deconv4(score)))  # out_size = 2*in_size (1/2)
        score = self.bn5(self.relu(self.deconv5(score)))  # out_size = 2*in_size (1)
        score = self.classifier(score)                    # size不变,使输出的channel等于类别数

        return score

3.模型训练

这部分参考自附录github源码的train.py,他的代码中使用了visdom可视化,我这里考虑到有些朋友不会visdom(好吧我承认,这些代码我是准备在云服务器colab上跑,而colab上跑visdom有点麻烦),我使用matplotlib来简单绘制训练过程的loos、acc和mIou的变化,若想边训练边监控训练过程,参考github源码中的visdom可视化。另外,他的源码中没有计算acc和mIou。下面是带注释的代码:

# <---------------------------------------------->
# 下面开始训练网络

# 在训练网络前定义函数用于计算Acc 和 mIou
# 计算混淆矩阵
def _fast_hist(label_true, label_pred, n_class):
    mask = (label_true >= 0) & (label_true < n_class)
    hist = np.bincount(
        n_class * label_true[mask].astype(int) +
        label_pred[mask], minlength=n_class ** 2).reshape(n_class, n_class)
    return hist

 # 根据混淆矩阵计算Acc和mIou
def label_accuracy_score(label_trues, label_preds, n_class):
    """Returns accuracy score evaluation result.
      - overall accuracy
      - mean accuracy
      - mean IU
    """
    hist = np.zeros((n_class, n_class))
    for lt, lp in zip(label_trues, label_preds):
        hist += _fast_hist(lt.flatten(), lp.flatten(), n_class)
    acc = np.diag(hist).sum() / hist.sum()
    with np.errstate(divide='ignore', invalid='ignore'):
        acc_cls = np.diag(hist) / hist.sum(axis=1)
    acc_cls = np.nanmean(acc_cls)
    with np.errstate(divide='ignore', invalid='ignore'):
        iu = np.diag(hist) / (
            hist.sum(axis=1) + hist.sum(axis=0) - np.diag(hist)
        )
    mean_iu = np.nanmean(iu)
    freq = hist.sum(axis=1) / hist.sum()
    return acc, acc_cls, mean_iu
    

from datetime import datetime

import torch.optim as optim
import matplotlib.pyplot as plt

def train(epo_num=50, show_vgg_params=False):


    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

    vgg_model = VGGNet(requires_grad=True, show_params=show_vgg_params)
    fcn_model = FCN8s(pretrained_net=vgg_model, n_class=2)
    fcn_model = fcn_model.to(device)
    # 这里只有两类,采用二分类常用的损失函数BCE
    criterion = nn.BCELoss().to(device)     
    # 随机梯度下降优化,学习率0.001,惯性分数0.7
    optimizer = optim.SGD(fcn_model.parameters(), lr=1e-3, momentum=0.7)
    
    # 记录训练过程相关指标
    all_train_iter_loss = []
    all_test_iter_loss = []
    test_Acc = []
    test_mIou = []
    # start timing
    prev_time = datetime.now()

    for epo in range(epo_num):
        
        # 训练
        train_loss = 0
        fcn_model.train()
        for index, (bag, bag_msk) in enumerate(train_dataloader):
            # bag.shape is torch.Size([4, 3, 160, 160])
            # bag_msk.shape is torch.Size([4, 2, 160, 160])

            bag = bag.to(device)
            bag_msk = bag_msk.to(device)

            optimizer.zero_grad()
            output = fcn_model(bag)
            output = torch.sigmoid(output) # output.shape is torch.Size([4, 2, 160, 160])
            loss = criterion(output, bag_msk)
            loss.backward()     # 需要计算导数,则调用backward
            iter_loss = loss.item()    # .item()返回一个具体的值,一般用于loss和acc
            all_train_iter_loss.append(iter_loss)
            train_loss += iter_loss
            optimizer.step()

            output_np = output.cpu().detach().numpy().copy() 
            output_np = np.argmin(output_np, axis=1)
            bag_msk_np = bag_msk.cpu().detach().numpy().copy() 
            bag_msk_np = np.argmin(bag_msk_np, axis=1)
            
            # 每15个bacth,输出一次训练过程的数据
            if np.mod(index, 15) == 0:
                print('epoch {}, {}/{},train loss is {}'.format(epo, index, len(train_dataloader), iter_loss))

        # 验证
        test_loss = 0
        fcn_model.eval()
        with torch.no_grad():
            for index, (bag, bag_msk) in enumerate(test_dataloader):

                bag = bag.to(device)
                bag_msk = bag_msk.to(device)

                optimizer.zero_grad()
                output = fcn_model(bag)
                output = torch.sigmoid(output) # output.shape is torch.Size([4, 2, 160, 160])
                loss = criterion(output, bag_msk)
                iter_loss = loss.item()
                all_test_iter_loss.append(iter_loss)
                test_loss += iter_loss

                output_np = output.cpu().detach().numpy().copy() 
                output_np = np.argmin(output_np, axis=1)
                bag_msk_np = bag_msk.cpu().detach().numpy().copy() 
                bag_msk_np = np.argmin(bag_msk_np, axis=1)
                


        cur_time = datetime.now()
        h, remainder = divmod((cur_time - prev_time).seconds, 3600)
        m, s = divmod(remainder, 60)
        time_str = "Time %02d:%02d:%02d" % (h, m, s)
        prev_time = cur_time
        
        print('<---------------------------------------------------->')
        print('epoch: %f'%epo)
        print('epoch train loss = %f, epoch test loss = %f, %s'
                %( train_loss/len(train_dataloader), test_loss/len(test_dataloader), time_str))
        
        acc, acc_cls, mean_iu = label_accuracy_score(bag_msk_np, output_np, 2)
        test_Acc.append(acc)
        test_mIou.append(mIou)

        print('Acc = %f, mIou = %f'%(acc, me))
        # 每5个epoch存储一次模型
        if np.mod(epo, 5) == 0:
            # 只存储模型参数
            torch.save(fcn_model.state_dict(), 'checkpoints/fcn_model_{}.pth'.format(epo))
            print('saveing checkpoints/fcn_model_{}.pth'.format(epo))
    # 绘制训练过程数据
    plt.figure()
    plt.subplot(221)
    plt.title('train_loss')
    plt.plot(all_train_iter_loss)
    plt.xlabel('batch')
    plt.subplot(222)
    plt.title('test_loss')
    plt.plot( all_test_iter_loss)
    plt.xlabel('batch')
    plt.subplot(223)
    plt.title('test_Acc')
    plot.plot(test_Acc)
    plt.xlabel('epoch')
    plt.subplot(224)
    plt.title('test_mIou')
    plt.plot(test_mIou)
    plt.xlabel('epoch')
    plt.show()


if __name__ == "__main__":

    train(epo_num=100, show_vgg_params=True)

4.运行

将上面所有的代码块的代码写在一个脚本中,命名为FCN.py,放在下面的工程目录中,bag_data和bag_data_msk分别是图片和标注,checkpoints用于存放训练模型。如下图:

然后在配置好的torch环境环境中执行:python FCN.py

即可开始训练

EfficientNet所要求的PyTorch版本是多少 fcn pytorch_2d_20

总结

FCN的优点和不足

优势:
  • 可以接受任意大小的输入图像(没有全连接层)
  • 更加高效,避免了使用邻域带来的重复计算和空间浪费的问题。
不足:
  • 得到的结果还不够精细 。进行8倍上采样虽然比32倍的效果好了很多,但是上采样的结果还是比较模糊和平滑,对图像中的细节不敏感。
  • 对各个像素进行分类,没有充分考虑像素与像素之间的关系。忽略了在通常的基于像素分类的分割方法中使用的空间规整(spatial regularization)步骤,缺乏空间一致性。
附:参考

论文原文网络博客(重点参考)
网络博客(反卷积详解)github源码和样例数据集(本文代码与其源码稍有区别)