如何应对深度学习中的数据分布不平衡问题

对数据不平衡的应对

在比赛中经常会遇到数据不平衡的问题,各个类别之间的数据量不平衡容易导致模型对数据量少的类别的检测性能较低。数据不平衡问题可以分为以下两种情况:

  1. 大数据分布不均衡。这种情况下整体数据规模大,只是其中的少样本类的占比较少。但是从每个特征的分布来看,小样本也覆盖了大部分或全部的特征。例如拥有1000万条记录的数据集中,其中占比50万条的少数分类样本便于属于这种情况。
  2. 小数据分布不均衡。这种情况下整体数据规模小,并且占据少量样本比例的分类数量也少,这会导致特征分布的严重不平衡。例如拥有1000条数据样本的数据集中,其中占有10条样本的分类,其特征无论如何拟合也无法实现完整特征值的覆盖,此时属于严重的数据样本分布不均衡。

样本分布不均衡将导致样本量少的分类所包含的特征过少,并很难从中提取规律;即使得到分类模型,也容易产生过度依赖于有限的数据样本而导致过拟合的问题,当模型应用到新的数据上时,模型的准确性和鲁棒性将很差。

1 数据扩充

我们的训练模型是为了拟合原样本的分布,但如果训练集的样本数和多样性不能很好地代表实际分布,那就容易发生过拟合训练集的现象。数据增强使用人类先验,尽量在原样本分布中增加新的样本点,是缓解过拟合的一个重要方法。

常用的数据数据增强手段有以下几点:

  • 水平、垂直翻转
  • $90^。,180^。,270^。$翻转
  • 翻转+旋转
  • 亮度、饱和度、对比度的随机变换
  • 随机裁剪
  • 随机缩放
  • 加模糊(blurring)
  • 加高斯噪声(Gaussian Noise)

除了前面三种之外,后面几种会改变数据的特征,需要谨慎使用。以下内容借鉴自:Kaggle经验

需要小心的是,数据增强的样本点最好不要将原分布的变化范围扩大,比如训练集以及测试集的光照分布十分均匀,就不要做光照变化的数据增强,因为这样只会增加拟合新训练集的难度,对测试集的泛化性能提升却比较小。另外,新增加的样本点最好和原样本点有较大不同,不能随便换掉几个像素就说是一个新的样本,这种变化对大部分模型来说基本是可以忽略的。

对于这个卫星图像识别的任务来说,最好的数据增强方法是什么呢?显然是旋转和翻转。具体来说,我们对这个数据集一张图片先进行水平翻转得到两种表示,再配合0度,90度,180度,270度的旋转,可以获得一张图的八种表示。以人类的先验来看,新的图片与原来的图片是属于同一个分布的,标签也不应该发生任何变化,而对于一个卷积神经网络来说,它又是8张不同的图片。比如下图就是某张图片的八个方向,光看这些我们都没办法判断哪张图是原图,但显然它们拥有相同的标签。

img

其他的数据增强方法就没那么好用了,我们挑几个分析:

  • 亮度,饱和度,对比度随机变化:在这个比赛的数据集中,官方已经对图片进行了比较好的预处理,亮度、饱和度、对比度的波动都比较小,所以在这些属性上进行数据增强没有什么好处。
  • 随机缩放:还记得我们在Overview和Data部分看到的信息吗?这些图片中的一个像素宽大概对应3.7米,也不应该有太大的波动,所以随机缩放不会有立竿见影的增强效果。
  • 随机裁剪:我们观察到有些图片因为边上出现了一小片云朵,被标注了partly cloudy,如果随机裁剪有可能把这块云朵裁掉,但是label却仍然有partly cloudy,这显然是在引入错误的标注样本,有百害而无一利。同样的例子也出现在别的类别上,说明随机裁剪的方法并不适合这个任务。

一旦做了这些操作,新的图片会扩大原样本的分布,所以这些数据增强也就没有翻转、旋转那么优先。在最后的方案中,我们只用了旋转和翻转。并不是说其他数据增强完全没效果,只是相比旋转和翻转,它们带来的好处没那么直接。

所以,在进行数据增强之前,需要仔细观察原始数据,观察其亮度、对比度等性质是否有较大的变化。依据结论进一步选择合适的数据增强方法。

2 采样

当类别之间的差距过大时,有效的数据增强方式无法弥补这种严重的不平衡,因而需要在模型训练过程中对采样过程进行处理。

  1. 过采样:通过增加分类中少数类样本的数量来实现样本均衡,最直接的方法是简单复制少数类样本形成多条记录,这种方法的缺点是如果样本特征少而可能导致过拟合的问题;经过改进的过抽样方法通过在少数类中加入随机噪声、干扰数据或通过一定规则产生新的合成样本。
  2. 欠采样:通过减少分类中多数类样本的样本数量来实现样本均衡,最直接的方法是随机地去掉一些多数类样本来减小多数类的规模,缺点是会丢失多数类样本中的一些重要信息。

总体上,过采样和欠采样更适合大数据分布不均衡的情况,尤其是第一种(过抽样)方法应用更加广泛。

pytoch权重采样

PyTorch中还单独提供了一个sampler模块,用来对数据进行采样。

常用的有随机采样器:RandomSampler,当dataloadershuffle参数为True时,系统会自动调用这个采样器,实现打乱数据。默认的是采用SequentialSampler,它会按顺序一个一个进行采样。

这里介绍另外一个很有用的采样方法: WeightedRandomSampler,它会根据每个样本的权重选取数据,在样本比例不均衡的问题中,可用它来进行重采样。

torch.utils.data.WeightedRandomSampler(weights, num_samples, replacement=True)

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class WeightedRandomSampler(Sampler):
r"""Samples elements from [0,..,len(weights)-1] with given probabilities (weights).
Arguments:
weights (sequence) : a sequence of weights, not necessary summing up to one
num_samples (int): number of samples to draw
replacement (bool): if ``True``, samples are drawn with replacement.
If not, they are drawn without replacement, which means that when a
sample index is drawn for a row, it cannot be drawn again for that row.
"""

def __init__(self, weights, num_samples, replacement=True):
if not isinstance(num_samples, _int_classes) or isinstance(num_samples, bool) or \
num_samples <= 0:
raise ValueError("num_samples should be a positive integeral "
"value, but got num_samples={}".format(num_samples))
if not isinstance(replacement, bool):
raise ValueError("replacement should be a boolean value, but got "
"replacement={}".format(replacement))
self.weights = torch.tensor(weights, dtype=torch.double)
self.num_samples = num_samples
self.replacement = replacement

def __iter__(self):
return iter(torch.multinomial(self.weights, self.num_samples, self.replacement).tolist())

def __len__(self):
return self.num_samples

构建WeightedRandomSampler时需提供两个参数:

每个样本的权重weights、共选取的样本总数num_samples,以及一个可选参数replacement。权重越大的样本被选中的概率越大,待选取的样本数目一般小于全部的样本数目。replacement用于指定是否可以重复选取某一个样本,默认为True,即允许在一个epoch中重复采样某一个数据。如果设为False,则当某一类的样本被全部选取完,但其样本数目仍未达到num_samples时,sampler将不会再从该类中选择数据,此时可能导致weights参数失效。下面举例说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from dataSet import *
dataset = DogCat('data/dogcat/', transform=transform)

from torch.utils.data import DataLoader
# 狗的图片被取出的概率是猫的概率的两倍
# 两类图片被取出的概率与weights的绝对大小无关,只和比值有关
weights = [2 if label == 1 else 1 for data, label in dataset]

print(weights)

from torch.utils.data.sampler import WeightedRandomSampler
sampler = WeightedRandomSampler(weights,\
num_samples=9,\
replacement=True)
dataloader = DataLoader(dataset,
batch_size=3,
sampler=sampler)
for datas, labels in dataloader:
print(labels.tolist())

3 更改损失函数-通过正负样本的惩罚权重解决样本不均衡

通过正负样本的惩罚权重解决样本不均衡的问题的思想是在算法实现过程中,对于分类中不同样本数量的类别分别赋予不同的权重(一般思路分类中的小样本量类别权重高,大样本量类别权重低),然后进行计算和建模。

使用Focal Loss

在机器学习任务中,除了会遇到严重的类别样本数不均衡问题之外,经常也会遇到容易识别的样本数目和难识别的样本数目不均衡的问题。为了解决这一问题,何凯明大神提出了Focal loss。

Focal loss尝试降低easy example对损失的贡献,这样网络会集中注意力在难样本上。

FL定义如下:

上述公式为二分类问题的Focal loss,可以看出对于每一个样本,使用$(1-\hat p)^\gamma$作为其识别难易程度的指标,预测值$\hat p$越大代表对其进行预测越容易,因而其在总体损失中的占比应该越小。

对于多分类问题,其形式为:

对于每一个样本,$p_t$为模型预测出其属于其真实类别的概率,$\alpha_t$可用于调节不同类别之间的权重。将$\lambda$设置为0便可以得到BCE。

使用加权损失

当样本分布不均衡时,我们可以依据先验知识给不同的类别赋予不同的损失权重,例如,可以使用加权的二维交叉熵损失,在pytorch实现的BCE损失函数中,提供了positive_weight参数用于指定各个类别对应的权重。假设训练集中,正类和负类的样本的比例为3:1,那么,可以将正类的比例设为0.75,负类的比例设为0.25。

4 使用样例挖掘

OHEM

OHEM(online hard example mining),即在线难例挖掘,指在训练过程中,只使用样本中损失较大的一部分样本进行网络的训练。

5. 通过组合、集成方法解决样本不均衡

组合/集成方法指的是在每次生成训练集时使用所有分类中的小样本量,同时从分类中的大样本量中随机抽取数据来与小样本量合并构成训练集,这样反复多次会得到很多训练集和训练模型。最后在应用时,使用组合方法(例如投票、加权投票等)产生分类预测结果。

例如,在数据集中的正、负例的样本分别为100和10000条,比例为1:100。此时可以将负例样本(类别中的大量样本集)随机分为100份(当然也可以分更多),每份100条数据;然后每次形成训练集时使用所有的正样本(100条)和随机抽取的负样本(100条)形成新的数据集。如此反复可以得到100个训练集和对应的训练模型。

参考