kaggle:iMet Collection 2019 - FGVC6

这是我的第二场kaggle竞赛(20th top5% 银牌),其实我觉得还能取得更好的名次的,由于实验室机子有限,还有一些想法都没有实验。不过这次比赛比上次比赛学到了更多东西,下面我将把在这次比赛中的感受和心得分享给大家。
kaggle:iMet Collection 2019 - FGVC6_第1张图片

一. 比赛背景及任务介绍

  1. 背景介绍
    参考官方overview。
  2. 任务介绍
    识别每张艺术品中所包含的culture和tag,大部分图片中含有多个标签,因此该比赛是一个multi-label classification的任务。
  3. 评价指标
    f2 score: ( 1 + β 2 ) p r β 2 p + r  where  p = t p t p + f p , r = t p t p + f n , β = 2 \frac{\left(1+\beta^{2}\right) p r}{\beta^{2} p+r} \text { where } p=\frac{t p}{t p+f p}, \quad r=\frac{t p}{t p+f n}, \beta=2 β2p+r(1+β2)pr where p=tp+fptp,r=tp+fntp,β=2
  4. 数据集介绍
    训练集有109237张,测试集分为两个阶段:第一阶段有7443张,训练好模型所提交的LB分数就是在该测试集上测试的;由于该比赛是一个kernel-only的比赛,即提交的submission必须是经过kaggle上的kernel运行提交的。待比赛提交日期结束后,官方会更新第二阶段的unseen测试集(大小为5.2倍于test1 ),由官方对参赛人员所选的kernel进行测试,得到最终private score.
  5. 数据分析(EDA)
    这一步特别重要!!! 为了表达的更清楚点,我想分为以下几点来说明:
    1). 类别总数:1103类,其中culture有398类,tag有705类;下图代表了两类中出现频次较高的标签。

    2). 每张图像中所含类别个数:1~11大多数图像含有2到5个标签,但是有一张图像含有11个标签。。。如下图所示:
    kaggle:iMet Collection 2019 - FGVC6_第2张图片
    11个标签的奇葩图像:
    kaggle:iMet Collection 2019 - FGVC6_第3张图片
    kaggle:iMet Collection 2019 - FGVC6_第4张图片
    3). 出现频率较高的一些标签:由下图可以看到,前20th的label中culturetag分别占了整个数据集的0.72%1.83%。这就说明大多数label是所出现的次数都是非常少的
    kaggle:iMet Collection 2019 - FGVC6_第5张图片
    4). 图像尺寸:数据集中图像尺寸分布特别不均衡,由KDE plot可知,width中最大的到5000,height最大的到7000;
    kaggle:iMet Collection 2019 - FGVC6_第6张图片
    下面列一些具体的数据:
    kaggle:iMet Collection 2019 - FGVC6_第7张图片 kaggle:iMet Collection 2019 - FGVC6_第8张图片
    下面看一下这些图像长什么样子:
    kaggle:iMet Collection 2019 - FGVC6_第9张图片
    kaggle:iMet Collection 2019 - FGVC6_第10张图片 总结:由以上几点可知,该数据集由于在图像尺寸图像所含的标签个数以及每种标签所出现的次数差距均较大,因此该数据集也是极度不均衡的,而且由于共有1103类,从而进一步增大了分类的难度。

二. 数据预处理以及数据增强

由第一部分可知,该比赛数据集严重不均衡,所以我们做了一下几方面尝试,以及验证了该方案是否对结果有提升。

  1. resize_padding_resize: 我们设置了一个阈值aspect_ratio,用来处理那些尺寸极度不均衡的图像。具体的做法是:先判断图像的宽高比,如果大于阈值,则先把短边resize成原来的2倍,然后在把resize后的短边padding到长边的大小,最后在把padding后的图像resize成300*300。具体效果如下:
    kaggle:iMet Collection 2019 - FGVC6_第11张图片
    可以看出,如果直接对原图resize 成300*300,那么出来的图像就会损失太多信息,而且由人看的话,也会把之前的毛笔误认做梳子,所以从理论上来说,这一步应该对结果有提升,但是LB却没有提升,到现在也不知道为啥。。。
  2. MultScaleCrop:为了增加图像的多尺度性,采用不同的scale因子(1, 0.875, 0.75, 0.66),在原图上随机crop,然后将crop后的图像resize成300*300 。效果:LB与直接在原图上RandomCrop差不多,所以后面训练模型是对训练集的处理采用的是RandomCrop。
  3. 对测试集进行FiveCrop:在想出这个方案之前,一直用的是CenterCrop对测试集进行预处理,然后在TTA;后面仔细分析了一下,如果该任务是multi-label,如果有的label在图像的边缘,进行CenterCrop的话就有可能会丢失该label的信息,因此,我们做了FiveCrop,如下图所示:
    kaggle:iMet Collection 2019 - FGVC6_第12张图片
    效果:比CenterCrop的LB提升了0.005左右
  4. data augment:
    训练集:包含了RandomErasing和mix up的增强手段,具体代码如下:
	image_transform = Compose([
		RandomCrop(dsize),
		RandomHorizontalFlip(),
		ToTensor(),
		Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
		RandomErasing(probability=0, sh=0.4, r1=0.3)
	])

RandomErasing:

class RandomErasing(object):
	'''
	Class that performs Random Erasing in Random Erasing Data Augmentation by Zhong et al.
	-------------------------------------------------------------------------------------
	probability: The probability that the operation will be performed.
	sl: min erasing area
	sh: max erasing area
	r1: min aspect ratio
	mean: erasing value

	usage (only for train data):     transform_train = transforms.Compose([
		transforms.RandomCrop(32, padding=4),
		transforms.RandomHorizontalFlip(),
		transforms.ToTensor(),
		transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
		transforms.RandomErasing(probability = args.p (0), sh = args.sh (0.4), r1 = args.r1 (0.3), ),
	])
	-------------------------------------------------------------------------------------
	'''
	
	def __init__(self, probability=0.5, sl=0.02, sh=0.4, r1=0.3, mean=None):
		if mean is None:
			mean = [0.485, 0.456, 0.406]
		self.probability = probability
		self.mean = mean
		self.sl = sl
		self.sh = sh
		self.r1 = r1
	
	def __call__(self, img):
		
		if random.uniform(0, 1) > self.probability:
			return img
		
		for attempt in range(100):
			area = img.size()[1] * img.size()[2]
			
			target_area = random.uniform(self.sl, self.sh) * area
			aspect_ratio = random.uniform(self.r1, 1 / self.r1)
			
			h = int(round(math.sqrt(target_area * aspect_ratio)))
			w = int(round(math.sqrt(target_area / aspect_ratio)))
			
			if w < img.size()[2] and h < img.size()[1]:
				x1 = random.randint(0, img.size()[1] - h)
				y1 = random.randint(0, img.size()[2] - w)
				if img.size()[0] == 3:
					img[0, x1:x1 + h, y1:y1 + w] = self.mean[0]
					img[1, x1:x1 + h, y1:y1 + w] = self.mean[1]
					img[2, x1:x1 + h, y1:y1 + w] = self.mean[2]
				else:
					img[0, x1:x1 + h, y1:y1 + w] = self.mean[0]
				return img
		
		return img

mix up:

l = np.random.beta(mixup_alpha, mixup_alpha)

index = torch.randperm(inputs.size(0))
inputs_a, inputs_b = inputs, inputs[index]
targets_a, targets_b = targets, targets[index]

mixed_images = l * inputs_a + (1 - l) * inputs_b
outputs = self.model(mixed_images)
loss = reduce_loss(l * criterion(outputs, targets_a) + (1 - l) * criterion(outputs, targets_b))

测试集:5倍的TTA:采用FiveCrop的预处理手段。

def load_transform_image(item, root, dsize, aspect_ratio, tta_index):
    image = load_image(item, root, aspect_ratio)
    w, h = image.size
    if tta_index==0:
        image = F.center_crop(image, dsize)
    elif tta_index==1:
        i = 0
        j = w//2 - dsize//2
        image = F.crop(image, i, j, dsize, dsize)
    elif tta_index==2:
        i = h - dsize
        j = w//2 - dsize//2
        image = F.crop(image, i, j, dsize, dsize)
    elif tta_index==3:
        i = h//2 - dsize//2
        j = 0
        image = F.crop(image, i, j, dsize, dsize)
    elif tta_index==4:
        i = h//2 - dsize//2
        j = w - dsize
        image = F.crop(image, i, j, dsize, dsize)
    image_transform = Compose([
        RandomHorizontalFlip(),
        ToTensor(),
        Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    ])
    image = image_transform(image)
    return image
  1. 列出获奖队的data augment方法:
    top1:采用CropIfNedeed + Resize预处理,并对处理后的图像进一步增强。
RandomCropIfNeeded(SIZE * 2, SIZE * 2),
Resize(SIZE, SIZE)
HorizontalFlip(p=0.5),
OneOf([
    RandomBrightness(0.1, p=1),
    RandomContrast(0.1, p=1),
], p=0.3),
ShiftScaleRotate(shift_limit=0.1, scale_limit=0.0, rotate_limit=15, p=0.3),
IAAAdditiveGaussianNoise(p=0.3),
class RandomCropIfNeeded(RandomCrop):
    def __init__(self, height, width, always_apply=False, p=1.0):
        super(RandomCrop, self).__init__(always_apply, p)
        self.height = height
        self.width = width

    def apply(self, img, h_start=0, w_start=0, **params):
        h, w, _ = img.shape
        return F.random_crop(img, min(self.height, h), min(self.width, w), h_start, w_start)

top9:采用了RandomResizedCropV2的预处理。note: 与torchvision提供的RandomResizedCrop接口稍微有点区别,官方的采用的CenterCrop+Resize实现,而作者采用的是RandomCrop+Resize。代码如下:

class RandomResizedCropV2(T.RandomResizedCrop):

    @staticmethod
    def get_params(img, scale, ratio):

        # ...

        # fallback
        w = min(img.size[0], img.size[1])
        i = random.randint(0, img.size[1] - w)
        j = random.randint(0, img.size[0] - w)

        return i, j, w, w
def train_transform(size):
    return T.Compose([
        RandomResizedCropV2(size, scale=(0.7, 1.0), ratio=(4/5, 5/4)),
        T.RandomHorizontalFlip(),
        T.ToTensor(),
        T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
        RandomErasing(probability=0.3, sh=0.3),
    ])

def test_transform(size):
    return T.Compose([
        RandomResizedCropV2(size, scale=(0.7, 1.0), ratio=(4/5, 5/4)),
        T.RandomHorizontalFlip(),
        T.ToTensor(),
        T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

三. 模型选择及设计

首先将训练集分成6折,然后使用第一折的数据进行单模型的训练和验证,从而确定不同种类模型的性能。由于kernel-only的规则,kaggle官方kernel运行时间不能超过九小时,所以就选择的是复杂度适中的模型。但有人实验证明,网络越深,其效果越好。

  1. backbone
    resnet50, cbam_resnet50,seresnext50,airnext50, resnet101, densenet121,inceptionv3
    对这些模型做过测试后,基于运行时间网络性能选择了最终的三个backbone分别是带有attention机制cbam_resnet50,seresnext50,airnext50

  2. backbone的改进-引入multiScale机制
    受SSD的启发,我们引入了multiScale机制,即将网络中间层的feature map经过global average pooling后concat到最后的全连接层,这样做能使得feature map得到更好的复用。这一操作使得cv和LB均提高了0.005左右。

  3. label correlation-引入图卷积网络GCN
    由于该比赛是一个multi-label classification,所以不同类别之间具有一定的相关性,具体的来说,有的类别一旦出现,另一个类别有很大概率也会出现。所以为了让网络能学到这种相关性,我们参考了ML-GCN,然后设计了基于该任务的GCN网络。但是得到的效果却没有提升。我们分析了原因可能是类别基数太大(1103类),而ML-GCN所采用的数据集coco和voc分别是80类和20类,这样生成的adjacent matrix太过于稀疏,我们分别统计了一下三种数据集adjacent matrix的稀疏程度,计算方式是用矩阵中非零值的元素个数(阈值τ设置的0.2)除以矩阵的大小。结果如下:
    voc:21 / (20x20) = 5.25%
    coco: 311 / (80x80) = 4.86%
    imet: 649 / (1103x1103) = 0.05%
    因此GCN网络对这种太过于稀疏的adjacent matrix,并不能学到类别间的相关性。

  4. Culture and tags separately
    由于所有的类别都基于这两大类,所以一个最直观的想法是在用CNN提取完特征后,设立两路的fc层,一路用来识别culture的398类,另一路用来识别tag的705类。然后就能得到两种loss:culture loss和tag loss,将这两种loss加权后就能得到最终的loss。但是效果与单路fc层效果差不多,这也是我没太理解的地方。

四. 训练

由第三部分可知,我们一共选择了三个模型:cbam_resnet50,airnext50和seresnext50进行fine tuning,引入了multiScale机制,并进行6折的交叉验证。在三个模型中,
相同的训练策略有:

  • crop的image size:288(试过320的没效果)
  • loss function:bce loss(试过focal loss与bce loss性能相当)
  • optimizer:Adam
  • init_lr:0.0001
  • 学习率衰减策略:当验证集的f2 score连续4次都不在提高时,就把学习率衰减为原来的0.2
  • fine tuning机制:第一个epoch只训练fc层,之后在将前面的卷积层unfreeze
  • 框架:pytorch

不同的训练的策略有:

  • batch size: cbam_resnet50(48),airnext50(36),seresnext50(42)

其它的训练策略:

  • 采用multi image size的训练技巧,即将image size设置了三种大小:160,228,288。训练细节可以参考我之前的这篇博客,但是效果与直接训练288一样。
  • adjust the threshold for each image:有这种想法但是没有实现,能力有限,这里我贴出top6的解决方法。

五. 测试

  1. 训练完全部的单模型
    每种模型训练了6折,一共有3种模型,因此总共有18个模型
  2. ensemble机制
    先分别将每种模型的6折结果进行融合(即对结果取平均),这样就有三种融合后的结果,然后再将这三种结果进行融合(即对结果取平均)便得到了最终的结果。

六. 心得

  1. 选择合适的baseline模型!!! 一个好的baseline可以进入前top20%;
  2. 多了解一些训练技巧。可以参考我之前的博客。
  3. 多了解一些简单实用的package。可以参考我之前的博客。
  4. 善用模型融合!!!
  5. 相信自己本地的CV验证集。每天在kaggle的提交次数是有限的,因此要设置好离线验证集,不断探索好的参数,不要过分相信kaggle的线上得分。
  6. 了解了multi-task任务与multi-label的区别,可参考这里。

七. 其它队好的方案

top1:link。
top4:link。
top6:link。
top9:link。
another a good solution:link。

你可能感兴趣的:(深度学习与计算机视觉,kaggle)