多标签分类与视觉属性预测

0. 问题概述

对于标签分类问题,表示事物本身可以分为多个类别,但是对于每一个样本存在至少一个类别,例如分类猫、狗、植物、动物。一个样本是猫的同时,又属于动物。因此就不能再像以往的模型一样,输出用softmax激活函数激活,因为我们最终的输出标签可能同时有很多的类,例如鸟类有羽毛和啄。

1. 搭建模型

通常对多标签任务可以采取两种网络模型,一种是直接输出一个全连接层分支,最后一层输出的神经元数应与标签数量相同,使用Sigmoid函数激活,将数值映射在0~1之间。不像Softmax函数,最后一层输出层数值之和并不等于1,而是全部都介于0与1之间,这样我们便可以设置一个阈值,当某类输出层数值大于这个阈值,则判断该标签为Positive,反之为Negative。模型如下图所示:

cnn多标签分类算法_数据

假设我们的图像属性类别为cnn多标签分类算法_数据_02cnn多标签分类算法_损失函数_03个标签类。最后一层的输出为cnn多标签分类算法_cnn多标签分类算法_04,经过Sigmoid函数激活输出的值在0-1之间,则判断属性时只要设置阈值cnn多标签分类算法_损失函数_05,大于阈值表示该标签存在,否则不存在:
cnn多标签分类算法_数据_06

2. 损失函数与标签数据平衡

由于最后的激活函数是Sigmoid函数,因此我们需要用二分类的损失函数:Binary Cross Entropy。假设预测为cnn多标签分类算法_cnn多标签分类算法_07,标签为cnn多标签分类算法_cnn多标签分类算法_08cnn多标签分类算法_cnn多标签分类算法_07为经过激活函数Sigmoid的结果。则我们可以构建损失函数:

cnn多标签分类算法_激活函数_10

我们会考虑到,不同的标签会存在正样本与负样本失衡的情况,例如300张图像中,红色翅膀属性有100,蓝色翅膀属性有200,正负样本不均匀,因此需要我们调节正负样本比例,但是数据不好扩增,因此我们可以修改损失函数:

cnn多标签分类算法_激活函数_11

其中cnn多标签分类算法_cnn多标签分类算法_12为调节不同属性的重要性,例如我希望模型更倾向于红色翅膀属性预测的正确性,可以在该类下调大比例。cnn多标签分类算法_激活函数_13为调节正负样本均衡的参数,例如在红色翅膀属性下,有100个正例,400个负例,则在该属性下的cnn多标签分类算法_激活函数_13我们可以设置为:cnn多标签分类算法_数据_15。在Pytorch框架中函数torch.nn.BCEWithLogitsLoss实现了该功能,其中cnn多标签分类算法_cnn多标签分类算法_12对应超参cnn多标签分类算法_激活函数_17cnn多标签分类算法_激活函数_13对应超参pos_weight。更多细节见BCEWithLogitsLoss

3. 视觉属性预测

基于深度学习的视觉属性通常预测方法包括多标签分类多任务学习,在TPAMI 2018一篇文章中提到:

Attribute learning problem can be formulated in the multi-task learning framework, where each task corresponds to learning one semantic attribute.

属性学习问题可以在多任务学习框架中形成,每个任务对应学习一个语义属性。

cnn多标签分类算法_损失函数_19


即可以在最后一层连接多个softmax二分类层或者用Sigmoid激活向量做多属性预测,通常其也会加入原物体类别信息,这样是防止偏见性。例如海豹在海水中,蓝色并不能作为海豹的属性,加入人为先验会更好一些,当然也可以直接进行属性预测。

cnn多标签分类算法_数据_20

cnn多标签分类算法_数据_21


在论文MAAD-Face: A Massively Annotated Attribute Dataset for Face Images中提供了人脸属性的标注,数据集来源于VGGFace2,在该数据集上训练了一个人脸属性模型,具体便是采用多任务学习的方法。Backbone便采用了Resnet50,最后的特征共享,并行输出多个全连接层。例如性别一栏,人只有男女之分,因此我不需要对男女进行二分类,而是直接分类性别,非男即女。种族也是,只有黄种人,白种人和黑种人,这个分支只输出3类。而人的胡子却不一样,有的人会同时有山羊胡和痄腮胡等,因此需要多个二分网络,最终将多个任务的交叉熵loss直接求和即可。这里值得提及一下pytorch的多输出网络方便的实现方法,以及多任务交叉熵Loss的写法,首先大概展示下网络的架构,与论文Multi-task deep neural network for multi-label learning中一个架构相似,如下图(a)所示:

cnn多标签分类算法_激活函数_22

下方代码主要部分是函数_make_multi_output,这里需要注意的是nn.ModuleList,因为循环中没有顺序,所以不能用nn.Sequential。如果不加nn.ModuleList而只用列表存储,则在model.cuda时不会将列表中的模型参数放入GPU,因为列表不会被识别为pytorch的方法。

class ResNet(nn.Module):
 
    def __init__(self, block, layers, attribute_classes):
        self.inplanes = 64
        super(ResNet, self).__init__()

        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3,
                               bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
        self.avgpool = nn.AvgPool2d(7, stride=1)
        
        self.layers = self._make_multi_output(block, attribute_classes)
 
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 0.001)
                nn.init.constant_(m.bias, 0)
 
    def _make_layer(self, block, planes, blocks, stride=1):
        downsample = None
        if stride != 1 or self.inplanes != planes * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.inplanes, planes * block.expansion,
                          kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(planes * block.expansion),
            )
 
        layers = []
        layers.append(block(self.inplanes, planes, stride, downsample))
        self.inplanes = planes * block.expansion
        for i in range(1, blocks):
            layers.append(block(self.inplanes, planes))
 
        return nn.Sequential(*layers)
    
    def _make_multi_output(self,block,attribute_classes):
        """
        Created by Ruoyu Chen on 07/15/2021
        """
        layers = []
        for i in range(attribute_classes):
            layers.append(nn.Linear(512*block.expansion, 2))
        
        return nn.ModuleList(layers)
 
    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)
 
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
 
        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        
        outs = []
        for layer in self.layers:
            outs.append(layer(x))
 
        return outs

损失函数需要注意的是outs和labels的shape:

class MultiBranchLabelLoss(nn.Module):
    def __init__(self):
        super(MultiBranchLabelLoss, self).__init__()
        self.criterion = nn.CrossEntropyLoss()  # Input (N,C), Target (C)
    def forward(self, outs, labels):
        """
        outs: List[Torch_size(batch,2)]
        labels: Torch_size(batch, attributes)
        """
        loss = 0
        for out,label in zip(outs,labels.t()):
            # out: Torch_size(batch,2)
            # label: Torch_size(batch)
            criterion_loss = self.criterion(out, label)
            loss += criterion_loss

        return loss

4. 物体检测的属性数据集

最早的关于物体检测中的属性分布,原网站https://vision.cs.uiuc.edu/attributes/,基于Pasc VOC2008标注,2009年的一篇文章。

最新根据CVPR2021的一篇文章Learning To Predict Visual Attributes in the Wild的统计,有如下的最新的物体检测相关属性数据集:

cnn多标签分类算法_cnn多标签分类算法_23

5. 多任务学习

Multitask Learning: A Knowledge-Based Source of Inductive Bias

cnn多标签分类算法_激活函数_24


cnn多标签分类算法_cnn多标签分类算法_25

6. 根据多标签学习训练Pasc VOC2008属性

首先,根据pasc voc2008数据集,下载:https://www.kaggle.com/sulaimannadeem/pascal-voc-2008

代码及结果请见我的github:https://github.com/RuoyuChen10/Multi-label-on-VOC2008-attributes