在Keras中导入测试数据的方法
在对Keras框架的学习中,一个很大的难点就是数据的导入,尤其是当数据不能一次放入内存的时候,应该如何导入的问题。在Keras的官网,没有章节特意讲这个内容,而专门去找资料,也很难找到相关的内容。绝大多数的教程都是直接使用的Keras自带的数据集。为了处理大量数据的情况,我还特意研究了Python的多线程。后来我还知道了导入数据的时候的随机性的重要性等各种问题。这篇文章算是一个总结。
如果看过我前面的文章Keras入门与LeNet的实现,应该知道Keras里面有很多经典的数据集。当我们研究自己的模型的时候,只需要拿出其中与我们要研究的问题类似的数据集进行试验就行了。比如之前提到的手写数字识别的经典数据集mnist。就只需要一行代码:
(X_train, y_train), (X_test, y_test) = mnist.load_data()
就可以成功导入了。在Keras官网上面有各个常用数据库的导入方法,这使得使用这些经典的数据库特别简单。但是我们使用Keras是不完全是为了研究自己的model,还可能是为了解决实际问题。这个时候,我们就要创造自己的数据集,并且把数据集运用到自己的模型之中。
创造数据集是一件比较难的事情。尤其是要创造大量的、靠谱的数据集。通常来说,很多数据集只能通过大公司去收集,而不能自己创造。但是凡事总有例外。有的时候我们还是能够自己创造数据集的。例如我们的OCR。我们只需要把字符进行变形就可以生成我们的数据集了。(当然,如果要考虑手写的顺序等问题,这就不够了。)关于生成字符识别数据集,请参考我之前的文章。
接着,我来说明我查到的第一种运用自己的数据集的方法。这种方法需要一次性地把数据放入内存,因此数据量不能过大。
原理也很简单。考虑到Keras的输入数据是numpy、float类型,因此我们只需要把图片读入,然后转成numpy就行了。
首先,我们的目录结构是这样的:
./words
./0
0.png
1.png
2.png
...
/1
0.png
1.png
2.png
...
/2
...
先写一个读图片的函数,我们用这个函数把Image类型转成numpy类型。
def read_image(imageName):
im = Image.open(imageName).convert('L')
data = np.array(im)
return data
我们创造一个images的列表和labels的列表,用来存图片和对应的结果。接着,我们把图片和它对应的结果读入:
# 读取在words里面有几个文件夹
text = os.listdir('./words')
# 把文件夹里面的图片和其对应的文件夹的名字也就是对应的字
for textPath in text:
for fn in os.listdir(os.path.join('words', textPath)):
if fn.endswith('.png'):
fd = os.path.join('./words', textPath, fn)
images.append(read_image(fd))
labels.append(textPath)
接着我们把刚刚得到的images和labels也变成numpy类型。当然,labels首先要变成int类型。
X = np.array(images)
y = np.array(list(map(int, labels)))
最后,我们按三七分把这些分为训练集和测试集。
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, random_state=30)
数据的导入工作就完成了。但是显然,这样做的话我们必须一次性把所有数据读入内存。当我们的数据量特别大的时候,这肯定是行不通的。就算数据量不大,这样也会浪费很多时间在IO上面。我们的希望的是,在训练的时候拿数据,一份一份地训练。
这是一个很难解决的问题,在网上很难找到对应的教程专门提到这一点。大神们都默认这是一个很简单的问题。但是对于新手来说,这却是一个很难的问题。我甚至考虑过用多线程去写。但是甚至读入的数据的顺序,都会影响最后的结果。如果我们一批一批地训练,就容易使得最后的结果偏向我们最后导入的数据,从而过拟合。最后我在查阅了Keras的官方文档,并且查看了很多相关的内容之后,找到了解决方法。
原来,Keras的训练不仅仅有fit,还有fit_generator,也就是一个一个训练。fit_generator的API如下:
fit_generator(self, generator, steps_per_epoch, epochs=1, verbose=1, callbacks=None, validation_data=None, validation_steps=None, class_weight=None, max_q_size=10, workers=1, pickle_safe=False, initial_epoch=0)
文档是这样写的:
函数的参数是:
- generator:生成器函数,生成器的输出应该为:
- 一个形如(inputs,targets)的tuple
- 一个形如(inputs, targets,sample_weight)的tuple。所有的返回值都应该包含相同数目的样本。生成器将无限在数据集上循环。每个epoch以经过模型的样本数达到samples_per_epoch时,记一个epoch结束
- steps_per_epoch:整数,当生成器返回steps_per_epoch次数据时计一个epoch结束,执行下一个epoch
- epochs:整数,数据迭代的轮数
- verbose:日志显示,0为不在标准输出流输出日志信息,1为输出进度条记录,2为每个epoch输出一行记录
- validation_data:具有以下三种形式之一
- 生成验证集的生成器
- 一个形如(inputs,targets)的tuple
- 一个形如(inputs,targets,sample_weights)的tuple
- validation_steps: 当validation_data为生成器时,本参数指定验证集的生成器返回次数
- class_weight:规定类别权重的字典,将类别映射为权重,常用于处理样本不均衡问题。
- sample_weight:权值的numpy array,用于在训练时调整损失函数(仅用于训练)。可以传递一个1D的与样本等长的向量用于对样本进行1对1的加权,或者在面对时序数据时,传递一个的形式为(samples,sequence_length)的矩阵来为每个时间步上的样本赋不同的权。这种情况下请确定在编译模型时添加了sample_weight_mode='temporal'。
- workers:最大进程数
- max_q_size:生成器队列的最大容量
- pickle_safe: 若为真,则使用基于进程的线程。由于该实现依赖多进程,不能传递non picklable(无法被pickle序列化)的参数到生成器中,因为无法轻易将它们传入子进程中。
- initial_epoch: 从该参数指定的epoch开始训练,在继续之前的训练时有用。
这个里面,只有前面六个是比较重要的,其他的默认就行了。甚至我们只需要前面三个就行了。steps_per_epoch和epochs都很好理解。这个generator,也就是生成器函数,才是问题的关键。
接下来就非常简单了。在keras系列︱利用fit_generator最小化显存占用比率/数据Batch化这篇博客里面已经讲得很清楚了。
里面有个demo一般的生成器函数:
def generate_batch_data_random(x, y, batch_size):
"""逐步提取batch数据到显存,降低对显存的占用"""
ylen = len(y)
loopcount = ylen // batch_size
while (True):
i = randint(0,loopcount)
yield x[i * batch_size:(i + 1) * batch_size], y[i * batch_size:(i + 1) * batch_size]
这里应该注意的有两点,第一点就是数据必须是要打乱的,没有规律的。第二点就是最后用的是yield。这也是Python的一个高级特性了。简单地说,这就是一个return。但是你每调用一次,它就返回一次,而不像其他函数一样,return了就出去了。这样就成为了一个生成器。具体可以看廖雪峰的Python教程
剩下的就很简单了。我们甚至可以直接在这个生成器函数里面写图片生成的算法。当然,考虑到IO操作肯定比直接生成要快,直接生成肯定是不可取的。
当然,这不是最好的导入数据的方法。Keras还有更快的方法。在介绍这个方法之前,我需要先介绍Keras的图像预处理的方法。
为了防止图像的过拟合,Keras里面自带了图片生成器用来对图像进行一些简单的操作,例如平移,旋转,缩放等等。这样我们就可以在有限的数据集上面生成无限的训练样本。这样可以扩大训练集的大小,防止图像的过拟合。具体的内容可以查看图片生成器的文章。
关键问题不在于这个图片生成,而是这个图片生成器的方法里面提供了一个函数——flow_from_directory(directory)
这个函数的参数如下:
flow_from_directory(directory): 以文件夹路径为参数,生成经过数据提升/归一化后的数据,在一个无限循环中无限产生batch数据
- directory: 目标文件夹路径,对于每一个类,该文件夹都要包含一个子文件夹.子文件夹中任何JPG、PNG、BNP、PPM的图片都会被生成器使用.详情请查看此脚本
- target_size: 整数tuple,默认为(256, 256). 图像将被resize成该尺寸
- color_mode: 颜色模式,为"grayscale","rgb"之一,默认为"rgb".代表这些图片是否会被转换为单通道或三通道的图片.
- classes: 可选参数,为子文件夹的列表,如['dogs','cats']默认为None. 若未提供,则该类别列表将从
directory
下的子文件夹名称/结构自动推断。每一个子文件夹都会被认为是一个新的类。(类别的顺序将按照字母表顺序映射到标签值)。通过属性class_indices
可获得文件夹名与类的序号的对应字典。- class_mode: "categorical", "binary", "sparse"或None之一. 默认为"categorical. 该参数决定了返回的标签数组的形式, "categorical"会返回2D的one-hot编码标签,"binary"返回1D的二值标签."sparse"返回1D的整数标签,如果为None则不返回任何标签, 生成器将仅仅生成batch数据, 这种情况在使用
model.predict_generator()
和model.evaluate_generator()
等函数时会用到.- batch_size: batch数据的大小,默认32
- shuffle: 是否打乱数据,默认为True
- seed: 可选参数,打乱数据和进行变换时的随机数种子
- save_to_dir: None或字符串,该参数能让你将提升后的图片保存起来,用以可视化
- save_prefix:字符串,保存提升后图片时使用的前缀, 仅当设置了
save_to_dir
时生效- save_format:"png"或"jpeg"之一,指定保存图片的数据格式,默认"jpeg"
- flollow_links: 是否访问子文件夹中的软链接
这样,我们导入数据就可以直接使用Keras自带的导入数据的方法了,并且附带了图片的处理。
我们的代码可以这样写:
datagen = ImageDataGenerator(...)
train_generator = datagen.flow_from_directory(
'./words',
target_size=(30, 30),
color_mode='grayscale',
batch_size=64)
model.fit_generator(train_generator, steps_per_epoch=500, epochs=50)
这样我们就导入了数据了。自此Keras的简单使用已经不成问题。
最后,我们需要注意,最后的导入数据的时候,会自动搜索里面的文件夹,但是是按字典序排序的。这很自然。例如你的文件夹是分类问题,文件夹都是猫、狗、鼠这样的汉字,它当然得按字典序排序。但是如果是像我们用0、1、2、3…这样的数字,就容易让人崩溃。因此我们需要注意在生成文件夹的时候,前面补0,即000、001、003、…、999。