FCN(Fully Convolutional Networks)简单实现

FCN的概念是在15年CVPR的Fully Convolutional Networks for Semantic Segmentation中提出,这篇论文第一次将端到端的卷积网络推广到了语义分割的任务当中,本篇博客主要会介绍该论文的Pytorch版本的简单实现。

介绍

FCN的主要特点:

  • 做像素级别的分类,使用ground-truth作为监督信息
  • 将全连接层改为卷积层
  • 使用反卷积deconv做上采样upsampling,从而得到跟原图大小一致的特征图
  • 采用Skip-connection,结合不同深度层的特征图,从而衍生出FCN-32s, FCN-16s, FCN-8s.过程如图1所示。


    图1 Skip Connection

Pytorch实现

实现部分大概可分为数据集制作、网络结构实现和训练三部分。

数据集制作

本次实验选用了一个比较小的数据集,是一些随机背景上的一些包的图片,数据集可以点击这里获取。

首先我们先定义一个函数来获取每张图片的地址并划分为训练集,测试集

def read_image(val_ratio=0.3, train=True):
   data_dir = os.path.join(cur_path, "bag_data")
   file_name = os.listdir(data_dir)
   img_path = [os.path.join(cur_path, "bag_data", file_name[i]) for i in range(len(file_name))]
   mask_path = [os.path.join(cur_path, "bag_data_msk", file_name[i]) for i in range(len(file_name))]
   # val_ratio设为0.3,代表70%数据用于训练,30%用于验证(这里并没有加上测试集)
   count = int(len(file_name)*val_ratio)
   if train:
       return img_path[count:], mask_path[count:]
   else:
       return img_path[:count], mask_path[:count]

接下来我们则定义自己的dataset类用于后续的训练。

class Bag_dataset(Dataset):
    def __init__(self, train=True, transforms=None, val_ratio=0.3):
        self.transforms = transforms
        self.data_path, self.mask_path = read_image(val_ratio, train)
        print("read " + str(len(self.data_path)) + " images")
    
    def __getitem__(self, item):
        img_path = self.data_path[item]
        mask_path = self.mask_path[item]
        img = Image.open(img_path).convert('RGB')
        mask = Image.open(mask_path).convert('L')
        img, mask = self.transforms(img, mask)
        return img, mask

    def __len__(self):
        return len(self.data_path)

再定义我们的transfroms函数 用于对读入的图片数据进行处理,这里有几个点要注意:

  • 本身来说全卷积网络去掉了全连接层,可以支持任意大小的图片输入,但是由于训练时同一个batch的数据大小必须一致,所以我这里对图片统一做了resize操作,不过这样会影响到网络精度。如果为了提高精度所以可以不做resize,只需将batch_size设为1即可.
  • ToTensor() 函数是将PIL或者ndarray格式的数据(HxWxC)转为FloatTensor格式的 CxHxW.
  • 对mask的处理,mask中目标区域为0,背景区域为255,我们首先将背景区域全部转为-1,再整体加一,这样便得到了0,1标签的二维矩阵,最后将其转为Tensor格式。
def train_transform(img, mask):
    #定义数据处理模块
    im_aug = tfs.Compose([
        
        tfs.Resize((320, 320)),
        # toTensor
        tfs.ToTensor(),
        tfs.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])

    img = im_aug(img)
    mask = tfs.Resize((320, 320))(mask)
    mask = np.array(mask, dtype=np.int64)
    mask[mask > 0] = -1
    mask += 1
    mask = torch.from_numpy(mask)
    return img, mask

数据集模块代码基本完成,最后做一个接口给外部调用, arg是我程序中的配置类。

def get_dataset(arg, train=True):

   dataset = Bag_dataset(train, train_transform, arg.val_ratio)
   data_loader = DataLoader(dataset, arg.train_batch_size if train else arg.test_batch_size, shuffle=True,
                            num_workers=arg.num_worker)

   return data_loader

网络结构实现

本篇论文的网络结构主要由卷积和反卷积模块构成,前面使用卷积模块进行特征提取,这部分可以使用一些现成的网络结构如VGG,ResNet等,再使用反卷积层将图像恢复到原始大小,最后再使用一个1x1的卷积核进行像素级别的分类。我这里只介绍FCN-8s的实现。


class FCN8s(nn.Module):
   def __init__(self, pretrained_net, num_class=2, verbose=False):
       super().__init__()
       self.num_class = num_class
       self.verbose = verbose
       # 我使用的预训练网络为ResNet-34
       self.stage1 = nn.Sequential(*list(pretrained_net.children()))[:-4]
       self.stage2 = nn.Sequential(*list(pretrained_net.children()))[-4]
       self.stage3 = nn.Sequential(*list(pretrained_net.children()))[-3]

       self.relu = nn.ReLU(True)
       # 定义反卷积层,反卷积层的具体计算可查看pytorch官方手册
       self.deconv1 = nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
       self.bn1 = nn.BatchNorm2d(256)
       self.deconv2 = nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
       self.bn2 = nn.BatchNorm2d(128)
       self.deconv3 = nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
       self.bn3 = nn.BatchNorm2d(64)
       self.deconv4 = nn.ConvTranspose2d(64, 32, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
       self.bn4 = nn.BatchNorm2d(32)
       self.deconv5 = nn.ConvTranspose2d(32, 16, kernel_size=3, stride=2, padding=1, dilation=1, output_padding=1)
       self.bn5 = nn.BatchNorm2d(16)
       # 定义1x1的卷积核用于像素分类
       self.classifier = nn.Conv2d(16, num_class, kernel_size=1)

   def forward(self, x):
       
       x3 = self.stage1(x)
       x4 = self.stage2(x3)
       x5 = self.stage3(x4)
       # 使用反卷积做图像像素恢复 将x5的size恢复至于x4一致,后续即可融合x5和x4(Skip Connection)
       score = self.relu(self.deconv1(x5))
       # 使用上采样的目的是当输入图片大小不一致时,可能使用反卷积恢复的x5大小会跟x4相差1个像素,所以采样上采样来补偿(如果有别的办法也可以推荐给我)
       score = F.interpolate(score, x4.size()[2:])
       score = self.bn1(score+x4)
       score = self.relu(self.deconv2(score))
       score = F.interpolate(score, x3.size()[2:])
       score = self.bn2(score+x3)
       score = self.bn3(self.relu(self.deconv3(score)))
       score = self.bn4(self.relu(self.deconv4(score)))
       score = self.bn5(self.relu(self.deconv5(score)))
       score = self.classifier(score)

       return score

训练

首先我们先定义损失函数。因为是分类任务所以我们采用了交叉熵。不过因为输入的mask标签为二维数据,所以我们预先做了一些处理。

def cross_entropy2d(input, target, weight=None, size_average=True):
    # input (n, c, h, w) , target(n, h, w)
    n, c, h, w = input.size()
    log_p = F.log_softmax(input, dim=1)
    log_p = log_p.transpose(1, 2).transpose(2, 3).contiguous()
    log_p = log_p[target.view(n, h, w, 1).repeat(1, 1, 1, c) >= 0]
    log_p = log_p.view(-1, c)
    mask = target >= 0
    target = target[mask]
    loss = F.nll_loss(log_p, target, weight=weight, reduction='sum')
    if size_average:
        loss /= mask.data.sum()
    return loss

为了衡量网络训练的结构,我们再定义一个测量的准确度函数

def get_accuracy(output, target):
   v, i = torch.max(output, 1)
   return torch.sum(target.long() == i).float()/target.numel()

有了这两个函数我们就可以开始进行网络的训练了,具体的训练代码这里就不贴出来了,可以去看我的GitHub源码

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容