写在前面
https://stanford.edu/~shervine/blog/keras-how-to-generate-data-on-the-fly
Keras作为一个高层的深度学习API,诞生之初就受到了大量使用者的欢迎,如今已被集成到Tensorflow内,更加速了它的发展。我们可以很方便的使用内置的函数加载数据、搭建网络、训练模型。我们都知道,在加载数据时,有两种方式:
- 一下子全部加载进来,然后使用.fit()方法,如下代码所示:
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)
简洁明了,但是我们往往并没有足够的内存去加载全部的数据。
- 使用数据生成器的方法,本质就是Python中的生成器,这样的话我们就可以一批(batch)一批的将数据送入网络中迭代训练。Keras中就有专门的函数:ImageDataGenerator,用法可参照:这里 这个函数的原理也很简单,就是通过并行读取指定文件夹里的图像数据,加以应用各种变换,然后送入到训练的网络中。但是这个函数有时无法满足我们的特定功能,我们也不能过于依赖他,下面将逐步探讨在keras中设计数据生成器的方法和步骤。
符号表示
开始前,我们先浏览一些组织技巧,这将对我们处理大的数据集带来很大帮助。
让ID表示数据集里的一个样本,以字符串形式表示。我们使用下面的框架可以很好地追溯样本和对应的标签:
- 创建一个名为partition的字典,其中partition[‘train’]和partition[‘validation’]分别表示训练集合验证集的ID.
- 创建一个名为labels的字典,数据集中的每个ID,其对应的label为labels[ID].
举例来说,如果我们训练集包含id-1,id-2和id-3的样本并且对应的标签分别为0,1和2.验证集中id-4的标签为1。这种情况下,Python变量partition和labels的内容分别为:
>>> partition
{'train': ['id-1', 'id-2', 'id-3'], 'validation': ['id-4']}
>>> labels
{'id-1': 0, 'id-2': 1, 'id-3': 2, 'id-4': 1}
为了代码的模块化,我们将单独编写代码和自定义的类放在两个不同的文件中,文件的组织结构类似于:
folder/
├── my_classes.py
├── keras_script.py
└── data/
其中data/假定为你存储数据集的文件夹。最后,需要注意的是,本教程中的代码是较为通用和极简,因此你可以轻松地将其调整为自己的数据集。
数据生成器
现在,将详细介绍如何设置Python类DataGenerator,该类将实时把数据投喂到Keras模型中。
首先来编写该类的初始化函数,该类将继承keras.utils.Sequence的属性,以便我们可以利用例如多线程等功能。
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()
初始化函数中,我们将与数据相关的信息导入进来,比如数据的尺寸,batch_size,同时也将存储重要的信息例如labels和IDs的列表.我们也可以在on_epoch_end方法中指定在每个epoch后是否将数据打乱(shuffle):
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)
在这个代码片中,我们根据self.shuffle的属性决定是否每个epoch之后将之后数据迭代的次序(self.indexes)打乱。在每个epoch后打乱数据的次序会使我们训练出来的模型表现更加稳定。
核心的数据生成方法定义为私有方法__data_generation,
def __data_generation(self, list_IDs_temp):
'Generates data containing batch_size samples' # X : (n_samples, *dim, n_channels)
# 初始化,给迭代的每一批数据预先分配空间
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):
# 根据数据的ID加载数据,这里的数据是.npy格式
X[i,] = np.load('data/' + ID + '.npy')
# Store class
y[i] = self.labels[ID]
#返回的label是经过one hot编码之后的数据
return X, keras.utils.to_categorical(y, num_classes=self.n_classes)
我们还需要在建立每一个batch的索引,该索引的长度在0到数据总数/batch_size之间,定义为私有方法:
def __len__(self):
'Denotes the number of batches per epoch'
return int(np.floor(len(self.list_IDs) / self.batch_size))
举例来说,我们数据总的个数为1002,batch_size为10,我们在一个epoch中最多迭代100次,多了就会有重复迭代的数据。因此我们使用了np.floor向下取整函数。
当一个batch的索引确定之后,将使用__getitem __方法产生一个batch的数据:
def __getitem__(self, index):
'Generate one batch of data'
# 一个Batch的索引
indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]
# 根据索引找文件名
list_IDs_temp = [self.list_IDs[k] for k in indexes]
# 找到了文件名后调用__data_generation方法读取数据
X, y = self.__data_generation(list_IDs_temp)
#这里可以加入一些扩增的方法,例如旋转平移等
return X, y
全部的代码如下:
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)
我们接着创建一个脚本使用我们刚刚写好的生成器:
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)
现在我们就可以使用创建好的数据生成器训练模型了。