近来刚参加完公司内部比赛,现在整理下各种训练技巧,提升图像分类问题的得分。所有资源整理于网络,不再一一列举引用出处。
目录
经典网络模型
网络名称 | 相关链接 |
RegNet | https://arxiv.org/abs/2003.13678 |
EfficientNet | https://arxiv.org/pdf/1905.11946.pdf |
VovNet | https://arxiv.org/abs/1904.09730 |
Resnet | https://arxiv.org/pdf/1512.03385.pdf |
HRNet | https://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个batch的数据通过flips, rotation, scale, etc.等操作生成batches
- 将各个batch分别输入网络
- 每个batch的masks/labels反向转换 [仅对mask有效,labels无反变换]
- 通过mean, max, gmean, etc.合并各个batch预测的结果
- 最后输出最终的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
对于网络训练,有没有办法可以既快又好呢?有兴趣的同学,可以参考如下论文。
- "Super-convergence: Very fast training of neural networks using large learning rates.“
- “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()
]))