Keras自定义数据生成器和数据增强教程

本文翻译:A detailed example of how to use data generators with Keras 。主要是想分享其通用框架和自定义数据增强。

By Afshine Amidi and Shervine Amidi

数据增强的动机


在小规模数据集上,我们往往一次性把数据读入内存。但是随着数据越来越多,数据集加载本身就已经占用很多内存,如果再进行数据增强,很可能导致内存无法一次性读取这么多数据。我们需要一个灵活的数据读取和增强方式。

所以这篇博客将要展示如实时生成数据集和数据增强(支持多线程)并且立即传递给深度学习模型。一个简单且易于理解的思想就是生成每个batch时,我们才通过路径,从磁盘读取数据,而不是一开始就将数据全部载入。

本教程中使用的框架Keras,Keras是TensorFlow、heano等的高级API,初学者可以快速搭建深度网络。

教程


以前的做法

在阅读本文之前,你的基于Keras代码可能是这样的:

import numpy as np
from keras.models import Sequential

# Load entire dataset
X, y = np.load('some_training_set_with_labels.npy')

# Design model
model = Sequential()
[...] # Your architecture
model.compile()

# Train model on your dataset
model.fit(x=X, y=y)

上面代码是一次加载整个数据集。实际上,这个任务可能会导致问题,因为所有的训练样本可能无法同时载入内存。

为了解决上述问题,让我们一步一步地研究如何构建适合这种情况的数据生成器(DataGenerator)。顺便说一下,下面的代码是一个很好的框架,可以用于您自己的项目。你可以复制/粘贴以下代码片段,并相应填补空白。

注意

在开始之前,让我们先了解一些在处理大型数据集时特别有用的组织技巧。
对数据集的每个样本设置ID。样本和所属标签label关联采用以下框架:

  1. 创建字典partition,用于划分训练和验证数据集:
    训练集的IDspartition['train']
    验证集的IDspartition['validation']
  2. 创建字典labels,通过labels[ID]和样本进行关联

TIP:也许可以简单点,样本的文件名就包含标签,所以样本文件名可以类似:ID_label.jpg,你可以这么理解,ID变为了ID_label。所以在后面初始化DataGenerator时,可以只传partition。

例如,假设我们的训练集包含id-1id-2id-3,分别带有标签012,验证集包含id-4和标签1。在这种情况下,Python变量的划分和标签看起来是这样的:

>>> partition
{'train': ['id-1', 'id-2', 'id-3'], 'validation': ['id-4']}

以及

>>> labels
{'id-1': 0, 'id-2': 1, 'id-3': 2, 'id-4': 1}

另外,出于模块化的考虑,我们将在单独的文件中编写Keras代码和定制类,这样您的文件夹看起来就像这样:

folder/
├── my_classes.py
├── keras_script.py
└── data/

假设data/是数据集目录。

最后,值得注意的是,本教程中的代码旨在通用性和最小化,以便您可以轻松地针对自己的数据集修改它。

TIP:原作者良心,文件怎么放都给了样例

数据生成器(DataGenerator)

现在,来具体了解怎么设置DataGenerator类,它将用于向模型实时提供数据。

首先,DataGenerator类继承自keras.utils.Sequence类,所以可以方便的给Keras模型提供数据,并且可以直接继承父类提供的诸如多线程处理功能。
下面给出初始化函数:

def __init__(self, list_IDs, labels, batch_size=32, dim=(32,32,32), n_channels=1,
             n_classes=10, shuffle=True):
    'Initialization'
    self.dim = dim
    self.batch_size = batch_size
    self.labels = labels
    self.list_IDs = list_IDs
    self.n_channels = n_channels
    self.n_classes = n_classes
    self.shuffle = shuffle
    self.on_epoch_end()

将有关数据的信息作为参数,例如:尺寸大小(如32通道大小为32x32的维度dim=(32,32,32))、通道数量、类数量、批处理大小,或者决定是否在生成时对数据进行打乱。还存传递了重要的信息:labelID列表。

在这里,on_epoch_end方法在每个epoch结束时会触发一次。如果shuffle参数被设置为True,会打乱索引顺序得到一个新的索引indexes,给本次epoch使用。在上面的__init__方法最后,我们看到调用了on_epoch_end方法,其目的是为了保证最开始的epoch和之前也不一样。

def on_epoch_end(self):
  'Updates indexes after each epoch'
  self.indexes = np.arange(len(self.list_IDs))
  if self.shuffle == True:
      np.random.shuffle(self.indexes)

这样就可以使不同时期的批次看起来不相似。这样做将最终使我们的模型更加鲁棒

生成过程的另一个核心方法是实现最关键任务的方法: 生成batche数据。负责此任务的私有方法(继承父类的方法)称为__data_generation,并将本次batch的ID列表作为参数。

def __data_generation(self, list_IDs_temp):
  'Generates data containing batch_size samples' # X : (n_samples, *dim, n_channels)
  # Initialization
  X = np.empty((self.batch_size, *self.dim, self.n_channels))
  y = np.empty((self.batch_size), dtype=int)

  # Generate data
  for i, ID in enumerate(list_IDs_temp):
      # Store sample
      X[i,] = np.load('data/' + ID + '.npy')

      # Store class
      y[i] = self.labels[ID]

  return X, keras.utils.to_categorical(y, num_classes=self.n_classes)

在数据生成过程中,上面代码从每个样本相应的文件 ID.npy中读取每个示例的NumPy数组。由于继承keras.utils.Sequence类,支持多线程,所以可以做更复杂的操作(例如通过源文件计算,也就是后面提到的数据增强)而不用担心数据生成成为训练过程中的瓶颈。

另外,请注意我们使用了Keras的Keras.util.to_categorical函数,将存储在y中的数字标签转换为适合分类的二进制形式(例如,在6分类类问题中,第三个标签对应于[0 0 1 0 0 0])。

每个batch请求数据,需要定义其长度__len__方法

def __len__(self):
  'Denotes the number of batches per epoch'
  return int(np.floor(len(self.list_IDs) / self.batch_size))

通常设置为这个值:
⌊ #  samples batch size ⌋ \biggl\lfloor\frac{\#\textrm{ samples}}{\textrm{batch size}}\biggr\rfloor batch size# samples

所以模型每个epoch最多只能看到1次训练样本。

TIP:比如有100条数据,batch_size=5__len__ = 100 / 5 = 20,那么第一个batch里的每20条数据和下一个batch里的20条数据是完全不一样的。所以在1个epoch最多只能看到1次训练样本。

现在,当在每个batch开始时,会重新初始化DataGenerator,即调用__init__on_epoch_end方法,我们得到新的索引index作为参数,生成器执行__getitem__方法给batch传递数据。

def __getitem__(self, index):
  'Generate one batch of data'
  # Generate indexes of the batch
  indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]

  # Find list of IDs
  list_IDs_temp = [self.list_IDs[k] for k in indexes]

  # Generate data
  X, y = self.__data_generation(list_IDs_temp)

  return X, y

TIP:其实数据增强就可以写在这里了,我们可以在__data_generation方法中实现数据增强,具体可以参考原作者github代码,或者看我下一篇分享。

与我们在本节中描述的步骤对应的完整代码如下所示:

import numpy as np
import keras

class DataGenerator(keras.utils.Sequence):
    'Generates data for Keras'
    def __init__(self, list_IDs, labels, batch_size=32, dim=(32,32,32), n_channels=1,
                 n_classes=10, shuffle=True):
        'Initialization'
        self.dim = dim
        self.batch_size = batch_size
        self.labels = labels
        self.list_IDs = list_IDs
        self.n_channels = n_channels
        self.n_classes = n_classes
        self.shuffle = shuffle
        self.on_epoch_end()

    def __len__(self):
        'Denotes the number of batches per epoch'
        return int(np.floor(len(self.list_IDs) / self.batch_size))

    def __getitem__(self, index):
        'Generate one batch of data'
        # Generate indexes of the batch
        indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]

        # Find list of IDs
        list_IDs_temp = [self.list_IDs[k] for k in indexes]

        # Generate data
        X, y = self.__data_generation(list_IDs_temp)

        return X, y

    def on_epoch_end(self):
        'Updates indexes after each epoch'
        self.indexes = np.arange(len(self.list_IDs))
        if self.shuffle == True:
            np.random.shuffle(self.indexes)

    def __data_generation(self, list_IDs_temp):
        'Generates data containing batch_size samples' # X : (n_samples, *dim, n_channels)
        # Initialization
        X = np.empty((self.batch_size, *self.dim, self.n_channels))
        y = np.empty((self.batch_size), dtype=int)

        # Generate data
        for i, ID in enumerate(list_IDs_temp):
            # Store sample
            X[i,] = np.load('data/' + ID + '.npy')

            # Store class
            y[i] = self.labels[ID]

        return X, keras.utils.to_categorical(y, num_classes=self.n_classes)

主程序代码

现在,我们必须相应地修改主程序代码,以便它接受我们刚刚创建的生成器(DataGenerator)。

import numpy as np

from keras.models import Sequential
from my_classes import DataGenerator

# Parameters
params = {'dim': (32,32,32),
          'batch_size': 64,
          'n_classes': 6,
          'n_channels': 1,
          'shuffle': True}

# Datasets
partition = # IDs
labels = # Labels

# Generators
training_generator = DataGenerator(partition['train'], labels, **params)
validation_generator = DataGenerator(partition['validation'], labels, **params)

# Design model
model = Sequential()
[...] # Architecture
model.compile()

# Train model on dataset
model.fit_generator(generator=training_generator,
                    validation_data=validation_generator,
                    use_multiprocessing=True,
                    workers=6)

可以看到,我们使用fit_generator替换fit方法,这里我们只需要把训练生成器作为参数。剩下的就交给Keras处理了。

到这里,你是否担心在每次生成batch时,要进行文件读取,更或者进行数据增强,会影响训练速度?别担心,我们可以使用多线程,提前处理好后面的batch。

注意,上面实现支持使用fit_generator的多处理参数,其中worker中指定的线程数是那些并行生成批的线程。足够多的worker可以保证CPU计算得到有效管理,也就是说,瓶颈实际上是神经网络在GPU上的前向和反向传播(而不是数据生成)

总结

This is it! 在命令行运行

python3 keras_script.py

你会看到,在训练阶段,数据是由CPU并行生成,然后直接反馈给GPU。
你可以在GitHub上的一个特定示例中找到这个方案的完整示例,其中提供了数据生成代码和Keras脚本


在下一篇分享中,我将介绍如何使用Keras进行中断继续训练,以及绘制绘制实时训练Loss和Acc图。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值