本文翻译: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
关联采用以下框架:
- 创建字典
partition
,用于划分训练和验证数据集:
训练集的IDs
:partition['train']
验证集的IDs
:partition['validation']
- 创建字典
labels
,通过labels[ID]
和样本进行关联
TIP:也许可以简单点,样本的文件名就包含标签,所以样本文件名可以类似:ID_label.jpg,你可以这么理解,ID变为了ID_label。所以在后面初始化DataGenerator时,可以只传partition。
例如,假设我们的训练集包含id-1
、id-2
和id-3
,分别带有标签0
、1
和2
,验证集包含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)
)、通道数量、类数量、批处理大小,或者决定是否在生成时对数据进行打乱。还存传递了重要的信息:label
和ID
列表。
在这里,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图。