如何混合不平衡的数据集以达到每个标签所需的分布?

How to mix unbalanced Datasets to reach a desired distribution per label?

我是 运行 我的神经网络 ubuntu 16.04,有 1 个 GPU (GTX 1070) 和 4 个 CPU。

我的数据集包含大约 35,000 张图像,但数据集不平衡:class 0 占 90%,class 1、2、3、4 共享其他 10%。因此,我通过使用 dataset.repeat(class_weight) [我还使用一个函数来应用随机增强] 对 class 1-4 进行过采样,然后 concatenate 它们。

重采样策略为:

1) 一开始的时候,class_weight[n]会被设置成一个很大的数字,这样每个class就会有和class0一样多的图片。

2) 随着训练的进行,epoch数增加,权重会根据epoch数下降,从而使分布更接近实际分布。

因为我的 class_weight 会因时代而异,所以我无法在一开始就打乱整个数据集。相反,我必须通过 class 接收数据 class,并在连接来自每个 class 的过采样数据后洗牌整个数据集。而且,为了实现平衡的批次,我必须按元素对整个数据集进行洗牌。

以下是我的部分代码。

def my_estimator_func():
    d0 = tf.data.TextLineDataset(train_csv_0).map(_parse_csv_train)
    d1 = tf.data.TextLineDataset(train_csv_1).map(_parse_csv_train)
    d2 = tf.data.TextLineDataset(train_csv_2).map(_parse_csv_train)
    d3 = tf.data.TextLineDataset(train_csv_3).map(_parse_csv_train)
    d4 = tf.data.TextLineDataset(train_csv_4).map(_parse_csv_train)
    d1 = d1.repeat(class_weight[1])
    d2 = d2.repeat(class_weight[2])
    d3 = d3.repeat(class_weight[3])
    d4 = d4.repeat(class_weight[4])
    dataset = d0.concatenate(d1).concatenate(d2).concatenate(d3).concatenate(d4)    
    dataset = dataset.shuffle(180000) # <- This is where the issue comes from
    dataset = dataset.batch(100)
    iterator = dataset.make_one_shot_iterator()  
    feature, label = iterator.get_next()
    return feature, label

def _parse_csv_train(line):
    parsed_line= tf.decode_csv(line, [[""], []])
    filename = parsed_line[0]
    label = parsed_line[1]
    image_string = tf.read_file(filename)
    image_decoded = tf.image.decode_jpeg(image_string, channels=3)
    # my_random_augmentation_func will apply random augmentation on the image. 
    image_aug = my_random_augmentation_func(image_decoded)
    image_resized = tf.image.resize_images(image_aug, image_resize)
    return image_resized, label

为了说清楚,让我一步步描述为什么我会遇到这个问题:

  1. 因为我数据集中的classes不平衡,我想对那些少数classes.

  2. 进行过采样
  3. 因为 1.,我想对那些 class 应用随机增强并将大多数 class (class 0) 与它们连接起来。

  4. 经过研究,我发现repeat()如果有随机函数会产生不同的结果,所以我使用repeat()和my_random_augmentation_func来实现2 .

  5. 现在,已经实现了2.,我想合并所有的数据集,所以我使用concatenate()

  6. 4后,我现在面临一个问题:总共有大约40,000 - 180,000张图片(因为class_weight逐个epoch改变,一开始会有180,000张图片总数,最后大约有 40,000 个),它们由 class 连接 class,数据集看起来像 [0000-1111-2222-3333-4444],因此批大小为 100,没有任何洗牌,每批几乎总是只有一个class,这意味着每批中的分布将是不平衡的。

  7. 为了解决5.中的"imbalanced batch"问题,我想到了应该对整个数据集进行shuffle,所以我使用了shuffle(180000).

  8. 最后,砰的一声,我的电脑在随机播放数据集中的 180000 个项目时死机了。

那么,有没有更好的方法可以让我获得平衡的批次,但仍然保持我想要的特征(例如,逐个改变分配时期)?

--- 编辑:问题已解决 ---

原来我不应该应用地图功能 在开头。我应该只接受文件名而不是真实文件,只是打乱文件名,然后将它们映射到真实文件。

更详细的,删除d0 = tf.data.TextLineDataset(train_csv_0)之后的map(_parse_csv_train)部分和其他4行,并在dataset = dataset.map(_parse_csv_train)之后添加新行after shuffle( 180000).

我还要感谢@P-Gn,他"shuffling"部分的博客link真的很有帮助。它回答了我心中的一个问题,但我没有问:"Can I get similar randomness by using many small shuffles v.s. one large shuffle?"(我不会在这里给出答案,请查看该博客!)该博客中的方法也可能是解决此问题的潜在方法问题,但我还没有尝试过。

我建议依赖 tf.contrib.data.choose_from_datasets, with labels picked by a tf.multinomial 分布。与基于样本拒绝的其他函数相比,这样做的优点是您不会丢失 I/O 读取未使用样本的带宽。

这是一个与您的案例类似的工作示例,带有一个虚拟数据集:

import tensorflow as tf

# create dummy datasets
class_num_samples = [900, 25, 25, 25, 25]
class_start = [0, 1000, 2000, 3000, 4000]
ds = [
  tf.data.Dataset.range(class_start[0], class_start[0] + class_num_samples[0]),
  tf.data.Dataset.range(class_start[1], class_start[1] + class_num_samples[1]),
  tf.data.Dataset.range(class_start[2], class_start[2] + class_num_samples[2]),
  tf.data.Dataset.range(class_start[3], class_start[3] + class_num_samples[3]),
  tf.data.Dataset.range(class_start[4], class_start[4] + class_num_samples[4])
]

# pick from dataset according to a parameterizable distribution
class_relprob_ph = tf.placeholder(tf.float32, shape=len(class_num_samples))
pick = tf.data.Dataset.from_tensor_slices(
  tf.multinomial(tf.log(class_relprob_ph)[None], max(class_num_samples))[0])
ds = tf.contrib.data.choose_from_datasets(ds, pick).repeat().batch(20)

iterator = ds.make_initializable_iterator()
batch = iterator.get_next()

with tf.Session() as sess:
  # choose uniform distribution
  sess.run(iterator.initializer, feed_dict={class_relprob_ph: [1, 1, 1, 1, 1]})
  print(batch.eval())
# [   0 1000 1001    1 3000 4000 3001 4001    2    3 1002 1003 2000    4    5 2001 3002 1004    6 2002]

  # now follow input distribution
  sess.run(iterator.initializer, feed_dict={class_relprob_ph: class_num_samples})
  print(batch.eval())
# [   0    1 4000    2    3    4    5 3000    6    7    8    9 2000   10   11   12   13 4001   14   15]

请注意,"epoch" 的长度现在由多项式采样的长度定义。我在这里有点武断地将它设置为 max(class_num_samples)——当你开始混合不同长度的数据集时,确实没有很好的选择来定义一个纪元。

然而,有一个具体的理由让它至少与最大数据集一样大:如您所见,调用 iterator.initializer 从头开始​​重新启动 Dataset。因此,既然您的洗牌缓冲区比您的数据小得多(通常是这种情况),重要的是不要过早重新启动以确保训练看到所有数据。

关于洗牌

这个答案解决了使用自定义权重交错数据集的问题,而不是数据集改组的问题,这是一个不相关的问题。对大型数据集进行混洗需要做出妥协——如果不以某种方式牺牲内存和性能,就无法进行有效的动态混洗。例如,关于该主题的 this excellent blog post 以图形方式说明了缓冲区大小对洗牌质量的影响。