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

6
我正在ubuntu 16.04上运行神经网络,使用1个GPU(GTX 1070)和4个CPU。我的数据集包含约35,000张图片,但数据集不平衡:类别0占90%,而类别1、2、3、4共占另外10%。因此,我通过使用dataset.repeat(class_weight)进行过采样,以及使用随机增强函数,然后将它们合并来过采样类别1-4。
重新采样策略如下:
1)在最开始时,class_weight[n]将设置为一个较大的数字,以使每个类别拥有与类别0相同数量的图像。
2)随着训练的进行,epoch数量递增,权重会根据epoch数量递减,使分布更接近实际分布。
由于我的class_weight会随着epoch而变化,因此无法在一开始对整个数据集进行洗牌。相反,我必须逐个类别地取出数据,并在连接每个类别的过采样数据之后对整个数据集进行洗牌。为了实现平衡的批处理,我必须对整个数据集进行逐元素的洗牌。
以下是我的部分代码。
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. 因为我的数据集中的类别不平衡,我想对那些少数类进行过采样。 2. 因为有了上述原因,我想对那些类别应用随机增强技术,并将多数类(类别 0)与它们连接起来。 3. 经过一些调研,我发现如果其中包含一个随机函数,则 repeat() 生成的结果会有所不同,因此我使用 repeat() 以及 my_random_augmentation_func 来实现上述目标。 4. 现在,完成了第 2 步,我想将所有数据集合并,因此我使用 concatenate()。 5. 在完成第 4 步之后,我遇到了一个问题:总共大约有 40,000 至 180,000 张图片(因为 class_weight 每个 epoch 都会变化,因此开始时总共有 180,000 张图像,最终大约有 40,000 张),它们按类别串联起来,数据集的形式将会是 [0000-1111-2222-3333-4444],因此每批次的大小为 100,没有任何混洗,几乎每个批次只包含一个类别,这意味着每个批次内的分布是不平衡的。 6. 为了解决第 5 步中的“不平衡批次”问题,我想到一个思路:对整个数据集进行混洗,因此我使用 shuffle(180000)。 7. 最后,我的电脑在对这个数据集的 180,000 项进行混洗时卡住了。
那么,有更好的方法可以获得平衡的批次,但仍然保持我想要的特性(例如按 epoch 更改分布)吗?
--- 编辑:问题已解决 ---
事实证明,我不应该一开始就应用 map 函数。我应该只取文件名而不是真实文件,并仅对文件名进行混洗,然后将其映射到真实文件中。
更详细地说,在 d0 = tf.data.TextLineDataset(train_csv_0) 和其他 4 行之后删除 map(_parse_csv_train) 部分,再添加一行 dataset = dataset.map(_parse_csv_train) 在 shuffle(180000) 之后。
我还要感谢 @P-Gn,他在“混洗”部分中提供的博客链接非常有帮助。它回答了我心中的一个问题,但我没有问出来:“我可以通过多次小混洗或一次大混洗来获得类似的随机性吗?”(我不会在这里给出答案,请查看那篇博客!)该博客中的方法也可能是解决此问题的潜在解决方案,但我还没有尝试过。
1个回答

2
我建议使用tf.contrib.data.choose_from_datasets,并且用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),这是一个相当任意的设置——当您开始混合不同长度的数据集时,确实没有好的选择来定义一个“epoch”。
然而,将其设置为至少与最大数据集一样大是有一个明确的原因的:正如你所注意到的,调用iterator.initializer会从头开始重启 Dataset。因此,现在您的混洗缓冲区比您的数据小得多(通常情况下是这样),重启得太早会导致训练无法看到所有数据。
关于混洗
本答案解决了使用自定义加权对数据集进行交错的问题,而不是数据集混洗问题,这是一个无关的问题。对大型数据集进行混洗需要做出妥协——您不能有一个有效的动态混洗同时又不牺牲内存和性能。例如,这里有一篇关于这个主题的博客文章,以图形方式说明了缓冲区大小对混洗质量的影响。

感谢您的回答!但是,iterator.initializer会重置所有内容,并且批次将始终从开头开始,因此对于大多数类(在本例中为类0),它将始终是要抽样的前几个元素。如果您在for循环中运行sess.run(iterator.initializer, feed_dict={class_relprob_ph: [1, 1, 1, 1, 1]}) print(batch.eval()),则输出中的类0元素将始终为0-5,这意味着类0中的元素6-900永远不会被使用。 - user10253771
那么如何解决这个问题呢?感觉我得回到shuffle()函数。不过这一次,我只需要按类别对数据集进行洗牌(最多只有35,000个,大约是180,000的1/5),但仍然相当庞大,需要耗费时间。 - user10253771
@user10253771,我在我的答案中添加了有关洗牌的注释。 - P-Gn
谢谢!可视化洗牌的博客帮了很多忙。 - user10253771

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接