Press "Enter" to skip to content

深度学习图像分类问题涨分总结

本站内容均来自兴趣收集,如不慎侵害的您的相关权益,请留言告知,我们将尽快删除.谢谢.

近来刚参加完公司内部比赛,现在整理下各种训练技巧,提升图像分类问题的得分。所有资源整理于网络,不再一一列举引用出处。

 

目录

 

Label smooth 计算公式

 

Test Time Augmentation

 

空间注意力模型(spatial attention)

 

空间和通道注意力机制的融合

 

GPU显存容量不够用怎幺办

 

经典网络模型

网络名称相关链接
RegNethttps://arxiv.org/abs/2003.13678
EfficientNethttps://arxiv.org/pdf/1905.11946.pdf
VovNethttps://arxiv.org/abs/1904.09730
Resnethttps://arxiv.org/pdf/1512.03385.pdf
HRNethttps://arxiv.org/abs/1902.09212

 

Label smooth

 

背景介绍

 

label smoothing是一种在分类问题中,防止过拟合的方法。多分类任务中,神经网络会输出一个当前数据对应于各个类别的置信度分数,将这些分数通过softmax进行归一化处理,最终会得到当前数据属于每个类别的概率。

 

 

然后计算交叉熵损失函数如下,其中i表示多类别中的某一类。

 

 

 

训练神经网络时,最小化预测概率和标签真实概率之间的交叉熵,从而得到最优的预测概率分布。最优的预测概率分布:

 

 

神经网络会促使自身往正确标签和错误标签差值最大的方向学习,在训练数据较少,不足以表征所有的样本特征的情况下,会导致网络过拟合。

 

Label smooth 计算公式

 

将标签强制one-hot的方式使网络过于自信会导致过拟合,因此软化这种编码方式。通过soft one-hot来加入噪声,减少了真实样本标签的类别在计算损失函数时的权重,最终起到抑制过拟合的效果。注意:K表示多分类的类别总数, 是一个较小的超参数。

 

 

下式中等号左侧是一种新的预测分布,等号右侧前半部分是对原分布乘上一个权重, 是一个超参数,需要自己设定,取值范围在0到1之间,后半部分u是一个均匀分布,k表示模型的类别数。

 

 

由以上公式可以看出,这种方式使label有 概率来自于均匀分布,(1- ) 概率来自于原分布。这就相当于在原label上增加噪声,让模型的预测值不要过度集中于概率较高的类别,把一些概率放在概率较低的类别。交叉熵损失函数的改变如下:

 

 

因此交叉熵可以替换为:

 

 

最优预测概率分布如下:

 

 

这里的α是任意实数,最终模型通过抑制正负样本输出差值,使得网络有更强的泛化能力。可以理解为:对“预测的分布与真实分布”及“预测分布与先验分布(均匀分布)”的惩罚。

 

代码实现

 

# 修改y的取值,做平滑
def label_smoothing(inputs, epsilon=0.1):
    K = inputs.get_shape().as_list()[-1]    # number of channels
    return ((1-epsilon) * inputs) + (epsilon / K)

 

Mixup

 

mixup是一种运用在计算机视觉中的对图像进行混类增强的算法,它可以将不同类之间的图像进行混合,从而扩充训练数据集。mixup可以改进当前最先进的神经网络架构的泛化能力。我们还发现,mixup能够减少对错误标签的记忆,增加对抗样本的鲁棒性,并能够稳定对生成对抗网络的训练过程。其以线性插值的方式来构建新的训练样本和标签,具体公式:

 

 

 

 

来自原始数据集中的训练样本(图片+标签)。其中 是一个服从B分布的参数, .Beta分布的概率密度函数如下图所示,其中 。无论如何设置α和β的值,期望 始终近似为0.5。在整个训练过程中有N个batch,权重在N个batch中期望近似为0.5,mixup作者认为α=β=0.5时,效果相对较好。

 

 

随着超参数a的增大,网络的训练误差就会增加,而其泛化能力会随之增强。而当a趋近无穷大时,模型就会退化成最原始的训练策略。Mixup就是一种抑制过拟合的策略,增加了一些扰动,从而提升了模型的泛化能力。

 

代码实现:

 

def get_batch(x, y, step, batch_size, alpha=0.2):
    """
    get batch data
    :param x: training data
    :param y: one-hot label
    :param step: step
    :param batch_size: batch size
    :param alpha: hyper-parameter α, default as 0.2
    :return:
    """
    candidates_data, candidates_label = x, y
    offset = (step * batch_size) % (candidates_data.shape[0] - batch_size)
    # get batch data
    train_features_batch = candidates_data[offset:(offset + batch_size)]
    train_labels_batch = candidates_label[offset:(offset + batch_size)]
    # 最原始的训练方式
    if alpha == 0:
        return train_features_batch, train_labels_batch
    # mixup增强后的训练方式
    if alpha > 0:
        weight = np.random.beta(alpha, alpha, batch_size)
        x_weight = weight.reshape(batch_size, 1, 1, 1)
        y_weight = weight.reshape(batch_size, 1)
        index = np.random.permutation(batch_size)
        x1, x2 = train_features_batch, train_features_batch[index]
        x = x1 * x_weight + x2 * (1 - x_weight)
        y1, y2 = train_labels_batch, train_labels_batch[index]
        y = y1 * y_weight + y2 * (1 - y_weight)
        return x, y

 

import matplotlib.pyplot as plt
import matplotlib.image as Image
import numpy as np
im1 = Image.imread(r"C:\Users\Daisy\Desktop\1\xyjy.png")
im2 = Image.imread(r"C:\Users\Daisy\Desktop\1\xyjy2.png")
for i in range(1,10):
    lam= i*0.1
    im_mixup = (im1*lam+im2*(1-lam))
    plt.subplot(3,3,i)
    plt.imshow(im_mixup)
plt.show()

 

Test Time Augmentation

 

TTA(Test-Time Augmentation) ,即测试时的数据增强。它会为原始图像造出多个不同版本,包括不同区域裁剪和更改缩放程度等,并将它们输入到模型中;然后对多个版本进行计算得到 平均输出 ,作为图像的最终输出分数。

 

实现步骤如下: [此处只考虑分类,忽略语义分割]

 

 

    1. 将1个batch的数据通过flips, rotation, scale, etc.等操作生成batches

 

    1. 将各个batch分别输入网络

 

    1. 每个batch的masks/labels反向转换 [仅对mask有效,labels无反变换]

 

    1. 通过mean, max, gmean, etc.合并各个batch预测的结果

 

    1. 最后输出最终的masks/labels

 

 

 

代码实现:这里推荐github上一个库https://github.com/qubvel/ttach,可以直接调用tta,非常方便

 

tta_model = tta.ClassificationTTAWrapper(model, tta.aliases.five_crop_transform(),  merge_mode='mean')

第一个参数为model,即为输入的模型
第二个参数为transform类型,调用作者已经设定好的tta类型tta.aliases

注意力机制

 

所谓Attention机制,便是聚焦于局部信息的机制,比如图像中的某一个图像区域。随着任务的变化,注意力区域往往会发生变化。注意力机制的本质就是定位到感兴趣的信息,抑制无用信息,结果通常都是以概率图或者概率特征向量的形式展示,从原理上来说,主要分为空间注意力模型,通道注意力模型,空间和通道混合注意力模型三种。

 

空间注意力模型(spatial attention)

 

不是图像中所有的区域对任务的贡献都是同样重要的,只有任务相关的区域才是需要关心的,比如分类任务的主体,空间注意力模型就是寻找网络中最重要的部位进行处理。我们在这里给大家介绍两个具有代表性的模型,第一个就是Google DeepMind提出的STN网络(Spatial Transformer Network[1])。它通过学习输入的形变,从而完成适合任务的预处理操作,是一种基于空间的Attention模型,网络结构如下:

 

 

这里的Localization Net用于生成仿射变换系数,输入是C×H×W维的图像,输出是一个空间变换系数,它的大小根据要学习的变换类型而定,如果是仿射变换,则是一个6维向量。这样的一个网络要完成的效果如下图:

 

\

 

由于在大部分情况下我们感兴趣的区域只是图像中的一小部分,因此空间注意力的本质就是定位目标并进行一些变换或者获取权重。

 

通道注意力机制

 

对于输入2维图像的CNN来说,一个维度是图像的尺度空间,即长宽,另一个维度就是通道,因此基于通道的Attention也是很常用的机制。SENet(Sequeeze and Excitation Net)[3]是2017届ImageNet分类比赛的冠军网络,本质上是一个基于通道的Attention模型,它通过建模各个特征通道的重要程度,然后针对不同的任务增强或者抑制不同的通道,原理图如下

 

 

在正常的卷积操作后分出了一个旁路分支,首先进行Squeeze操作(即图中Fsq(·)),它将空间维度进行特征压缩,即每个二维的特征图变成一个实数,相当于具有全局感受野的池化操作,特征通道数不变。然后是Excitation操作(即图中的Fex(·)),它通过参数w为每个特征通道生成权重,w被学习用来显式地建模特征通道间的相关性。在文章中,使用了一个2层bottleneck结构(先降维再升维)的全连接层+Sigmoid函数来实现。得到了每一个特征通道的权重之后,就将该权重应用于原来的每个特征通道,基于特定的任务,就可以学习到不同通道的重要性。将其机制应用于若干基准模型,在增加少量计算量的情况下,获得了更明显的性能提升。作为一种通用的设计思想,它可以被用于任何现有网络,具有较强的实践意义。

 

空间和通道注意力机制的融合

 

前述的Dynamic Capacity Network是从空间维度进行Attention,SENet是从通道维度进行Attention,自然也可以同时使用空间Attention和通道Attention机制。卷积块注意模块(CBAM),这是一个简单的用于前馈卷积神经网络的有效注意模块。给出一个中间特征映射,我们的模块依次沿着两个独立的维度,通道和空间推断注意图,然后将注意图倍增到用于自适应特征细化的输入特征映射。因为CBAM是一个轻量级的通用模块,它可以无缝地集成到任何CNN架构中,开销可以忽略不计,并且可以与基本CNN一起进行端到端的跟踪。CBAM(Convolutional Block Attention Module)[5]是其中的代表性网络,结构如下:

 

 

通道方向的Attention建模的是特征的重要性,结构如下:

 

 

同时使用平均池化和最大池化操作来聚合特征映射的空间信息,送到一个共享网络, 压缩输入特征图的空间维数,逐元素求和合并,以产生我们的通道注意力图 Mc

 

 

空间方向的Attention建模的是空间位置的重要性,结构如下:

 

 

还是使用average pooling和max pooling对输入feature map进行压缩操作,只不过这里的压缩变成了通道层面上的压缩,连接起来,用7*7卷积生成空间注意力图谱.

 

CBAM集成网络很好的学习目标对象区域中的信息并从中聚合特征。通过实验发现串联两个attention模块的效果要优于并联。通道attention放在前面要优于空间attention模块放在前面。除此之外,还有很多的注意力机制相关的研究,比如残差注意力机制,多尺度注意力机制,递归注意力机制等。

 

pytorch版本实现:

 

class CBAMBlock(tf.keras.layers.Layer):
    def __init__(self, channel, ratio=8):
        super(CBAMBlock, self).__init__()
        self.avgpool = tf.keras.layers.GlobalAveragePooling2D()
        self.maxpool = tf.keras.layers.GlobalMaxPool2D()
        self.shared_layer_one = tf.keras.layers.Dense(channel//ratio, activation='relu',
                                                        kernel_initializer='he_normal',
                                                        use_bias=True,
                                                        bias_initializer='zeros')
        self.shared_layer_two = tf.keras.layers.Dense(channel, kernel_initializer='he_normal',
                                        use_bias=True, bias_initializer='zeros')
        self.reshape = tf.keras.layers.Reshape((1,1,channel))
        self.multiply = tf.keras.layers.Multiply()
        self.add = tf.keras.layers.Add()
        self.activation = tf.keras.layers.Activation('sigmoid')
        self.concat = tf.keras.layers.Concatenate(axis=3)
        self.conv = tf.keras.layers.Conv2D(filters = 1, kernel_size=(7,7), strides=1,
                                            padding='same', activation='sigmoid', kernel_initializer='he_normal', 
                                            use_bias=False)
        self.channel = channel
    def call(self, inputs, **kwargs):
        #channel_attention
        avg_pool = self.avgpool(inputs)
        avg_pool = self.reshape(avg_pool)
        #assert avg_pool.shape[1:] == (1,1,self.channel)
        avg_pool = self.shared_layer_one(avg_pool)
        #assert avg_pool.shape[1:] == (1,1,self.channel//8)
        avg_pool = self.shared_layer_two(avg_pool)
        #assert avg_pool.shape[1:] == (1,1,self.channel)
        
        max_pool = self.maxpool(inputs)
        max_pool = self.reshape(max_pool)
        #assert max_pool.shape[1:] == (1,1,self.channel)
        max_pool = self.shared_layer_one(max_pool)
        #assert max_pool.shape[1:] == (1,1,self.channel//8)
        max_pool = self.shared_layer_two(max_pool)
        #assert max_pool.shape[1:] == (1,1,self.channel)
        
        channel_feature = self.add([avg_pool,max_pool])
        channel_feature = self.activation(channel_feature)
        channel_feature = self.multiply([inputs, channel_feature])
        
        #spatial_attention
        avg_pool = tf.reduce_mean(channel_feature, axis=[3], keepdims=True)
        #assert avg_pool.shape[-1] == 1
        max_pool = tf.reduce_max(channel_feature, axis=[3], keepdims=True)
        #assert max_pool.shape[-1] == 1
        concat = self.concat([avg_pool, max_pool])
        #assert concat.shape[-1] == 2
        spatial_feature = self.conv(concat)
        cbam_feature = self.multiply([channel_feature, spatial_feature])
        
        return cbam_feature

 

分类问题的模型融合

 

投票法

 

假设对于一个二分类问题,有3个基础模型,那幺就采取投票制的方法,投票多者确定为最终的分类。

 

 

即各个分类器输出其预测的类别,取最高票对应的类别作为结果。若有多个类别都是最高票,那幺随机选取一个。

 

加权投票法

 

和上面的简单投票法类似,不过多了权重αi,这样可以区分分类器的重要程度,通常

 

 

 

此外,个体学习器可能产生不同的 的值,比如类标记和类概率。

类标记取值0或1,即hi将样本x预测为类别cj,则取值为1,否则为0。使用类别标记的投票称为“硬投票”
类概率的取值为0~1,即输出类别为cj的概率,使用类别概率的投票称为软投票,对应sklearn中的VotingClassifier,voting参数设置为soft

Bagging

 

使用训练数据的不同随机子集来训练每个 Base Model,最后进行每个 Base Model 权重相同的 Vote。也即 Random Forest 的原理。大概分为这样两步:

 

1.重复K次

 

有放回地重复抽样建模,训练子模型

 

2.模型融合

 

分类问题:voting

 

Boosting

 

每一次训练的时候都更加关心分类错误的样例,给这些分类错误的样例增加更大的权重,下一次迭代的目标就是能够更容易辨别出上一轮分类错误的样例。最终将这些弱分类器进行加权相加。

 

 

 

Stacking

 

这边是横向stack实现,即预训练了好几种模型,然后通过全连接层连接他们。固定之前的参数,只训练新增的全连接层参数。个人认为就是soft voting的参数学习过程。

 

pytorch实现代码:

 

class JSTNET(nn.Module):
    def __init__(self, model_b3, model_12GF, model_32GF, nb_class=4):
    #def __init__(self, model_b3, model_12GF, model_32GF, model_vovnet, nb_class=4):
        super(JSTNET, self).__init__()
        self.model_b3 = model_b3
        self.model_12GF = model_12GF
        self.model_32GF = model_32GF
        #self.model_vovnet = model_vovnet
        
        self.model_b3.fc = nn.Identity()
        self.model_12GF.fc = nn.Identity()
        self.model_32GF.fc = nn.Identity()
        #self.model_vovnet.fc = nn.Identity()
        
        #self.dropout = nn.Dropout(0.5)
        #self.relu = nn.ReLU(inplace=True)
        #self.classifier = nn.Linear(8512, nb_class)
        self.classifier = nn.Linear(7488, nb_class)
        #self.classifier_2 = nn.Linear(200, nb_class)
        
    def forward(self, inputs):
        output_b3 = self.model_b3(inputs.clone())
        output_b3 = output_b3.view(output_b3.size(0), -1)
        output_12GF = self.model_12GF(inputs.clone())
        output_12GF = output_12GF.view(output_12GF.size(0), -1)
        #output_vovnet = self.model_vovnet(inputs.clone())
        #output_vovnet = output_vovnet.view(output_vovnet.size(0), -1)
        output_32GF = self.model_32GF(inputs.clone())
        output_32GF = output_32GF.view(output_32GF.size(0), -1)
        
        #new_input = torch.cat([output_b3,output_12GF,output_vovnet,output_32GF],dim=1)
        new_input = torch.cat([output_b3,output_12GF,output_32GF],dim=1)
        x = self.classifier(new_input)
        #x = self.relu(x)
        #x = self.dropout(x)
        #x = self.classifier_2(x)
        '''
        print('new_input:',new_input.shape)
        test_input = new_input.view(new_input.shape[0],-1)
        print('test_inpu:',test_input.shape)
        (b,in_f) = test_input.shape
        print(b, in_f)
        
        self.fc1 = nn.Linear(in_f, 100) 
        x = self.fc1(new_input)
        x = self.relu(x)
        x = self.dropout(x)
        test_input = x.view(x.size[0],-1)
        (b,in_f) = test_input.shape
        self.fc2 = nn.Linear(in_f,4)
        x = self.fc2(x)
        x = F.softmax(x, dim=1)
        '''
        return x

 

其他高阶玩法

 

 

GPU显存容量不够用怎幺办

 

我们知道网络训练时,使用较大的batch size会比小batch size得到更好的结果。如果GPU资源充裕的,可以考虑GPU分布式训练(模型分布式和数据分布式),这里不扩展。 如果GPU资源不充裕,可以在训练初始阶段,采用“小尺寸图片+大批量”的训练方式。然后再基于上一步的模型,逐渐扩大图片大小,减少batch size,进行迁移学习。 这样确保精调阶段的最终解,不会过分远离大批量时找到初期的较优解。

 

One cycle policy

 

对于网络训练,有没有办法可以既快又好呢?有兴趣的同学,可以参考如下论文。

 

 

    1. “Super-convergence: Very fast training of neural networks using large learning rates.“

 

    1. “Cyclical Learning Rates for Training Neural Networks”

 

 

就是通过寻找一个较好的学习率策略,让网络快速收敛。下面是论文里截图,在cifar10数据集上采用resnet-56训练,one-cycle policy达到了既好又快的目的。

 

当前fastai和pytorch lightning都支持该技术。

 

 

第一步寻找学习率上下界,在训练集上学习率从小到大依次递增,随着学习率增大,当梯度爆炸loss反弹后停止,对应的损失函数如下图所示。

 

 

上图中红点是fastai给出的建议学习率,我们只需挑选loss能显着下降的学习率,1e-3和1e-2都是可以接受的。通过这张图就可以挑选学习率的上界,另外论文推荐,学习率的下界就是上界的1/10或1/20。

 

第二步在整个训练集上采用下图所示的学习率策略训练。注意下图迭代次数是N个epoch内所有迭代次数,迭代次数=图片数量/batch size。下图中每个iteration会采用不同的学习率进行训练。另一张图就是动量了,它和学习率的大小关系正好相反,防止网络陷入到局部极值无法跳出来。

 

 

学习率从小到大,可以保证Loss快速收敛,因为在learning rate finding阶段已经是事实了。学习率从大到小,属于网络精调,收敛到最佳位置。

 

Fix Resolution

 

FixRes是Fix Resolution的简写形式,最初用在对EfficientNet的改进。固定分辨率是指,在训练时间和测试裁剪的分辨率保持固定大小,属于数据增强技术。具体参考论文:Fixing the train-test resolution discrepancy。

 

 

红框表示crop的区域,它会被送入神经网络。对于传统标准训练,假设训练和测试时都用224*224大小裁剪图片,我们可以发现,尽管两张照片的马大小是一样的,但是训练时“马儿”图片大小比测试时大很多,这会影响性能。为了尽可能保证训练和测试时马儿大小一样,减少网络对于尺度不变性的要求。可以在训练时采用更小的图片裁剪尺寸比如128*128,然后测试时采用224*224的裁剪尺寸;也可以训练224*224,测试时384*384。这里需要注意,这个缩放比例采用如下公式,K是crop后的size,HW是原始图片大小。

 

 

pytorch版本代码实现:

 

network = "RegNetY_6_4GF"
    pretrained = False
    num_classes = 4
    seed = 0
    input_image_size = 500
    scale = 600 / 500
    train_dataset = datasets.ImageFolder(
        train_dataset_path,
        transforms.Compose([
            transforms.RandomHorizontalFlip(),
            transforms.RandomResizedCrop(input_image_size),
            transforms.ColorJitter(brightness=0.5,
                                   contrast=0.5,
                                   saturation=0.5,
                                   hue=0.5),
            transforms.RandomRotation(10),
            transforms.ToTensor()
        ]))
    val_dataset = datasets.ImageFolder(
        val_dataset_path,
        transforms.Compose([
            transforms.Resize(int(input_image_size*scale)),
            transforms.CenterCrop(input_image_size),
            transforms.ToTensor()
        ]))

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注