赞
踩
在对Keras框架的学习中,一个很大的难点就是数据的导入,尤其是当数据不能一次放入内存的时候,应该如何导入的问题。在Keras的官网,没有章节特意讲这个内容,而专门去找资料,也很难找到相关的内容。绝大多数的教程都是直接使用的Keras自带的数据集。为了处理大量数据的情况,我还特意研究了Python的多线程。后来我还知道了导入数据的时候的随机性的重要性等各种问题。这篇文章算是一个总结。
详细内容参见链接:https://www.jianshu.com/p/0fbe5b5d0ab8
来源:简书
方便查看搬运至此(含个人解读):
一、Keras里面集成的数据集读取:
如果看过我前面的文章Keras入门与LeNet的实现,应该知道Keras里面有很多经典的数据集。当我们研究自己的模型的时候,只需要拿出其中与我们要研究的问题类似的数据集进行试验就行了。比如之前提到的手写数字识别的经典数据集mnist。就只需要一行代码:
(X_train, y_train), (X_test, y_test) = mnist.load_data()
就可以成功导入了。在Keras官网上面有各个常用数据库的导入方法,这使得使用这些经典的数据库特别简单。
但是我们使用Keras是不完全是为了研究自己的model,还可能是为了解决实际问题。这个时候,我们就要创造自己的数据集,并且把数据集运用到自己的模型之中。
二、自制数据集的读取
创造数据集是一件比较难的事情。尤其是要创造大量的、靠谱的数据集。通常来说,很多数据集只能通过大公司去收集,而不能自己创造。但是凡事总有例外。有的时候我们还是能够自己创造数据集的。例如我们的OCR。我们只需要把字符进行变形就可以生成我们的数据集了。(当然,如果要考虑手写的顺序等问题,这就不够了。)关于生成字符识别数据集,请参考我之前的文章。
1、一次性读入到内存(适合小数据集)
接着,我来说明我查到的第一种运用自己的数据集的方法。这种方法需要一次性地把数据放入内存,因此数据量不能过大。
原理也很简单。考虑到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上面。我们的希望的是,在训练的时候拿数据,一份一份地训练。
2、批量读取数据(个人不推荐)
这是一个很难解决的问题,在网上很难找到对应的教程专门提到这一点。大神们都默认这是一个很简单的问题。但是对于新手来说,这却是一个很难的问题。我甚至考虑过用多线程去写。但是甚至读入的数据的顺序,都会影响最后的结果。如果我们一批一批地训练,就容易使得最后的结果偏向我们最后导入的数据,从而过拟合。最后我在查阅了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操作肯定比直接生成要快,直接生成肯定是不可取的。
3、更快的导入数据方法(推荐)
当然,这不是最好的导入数据的方法。Keras还有更快的方法。在介绍这个方法之前,我需要先介绍Keras的图像预处理的方法。
为了防止图像的过拟合,Keras里面自带了图片生成器ImageDataGenerator用来对图像进行一些简单的操作,例如平移,旋转,缩放等等。这样我们就可以在有限的数据集上面生成无限的训练样本。这样可以扩大训练集的大小,防止图像的过拟合。具体的内容可以查看图片生成器ImageDataGenerator的文章。
keras.preprocessing.image.ImageDataGenerator(featurewise_center=False, samplewise_center=False, featurewise_std_normalization=False, samplewise_std_normalization=False, zca_whitening=False, zca_epsilon=1e-06, rotation_range=0, width_shift_range=0.0, height_shift_range=0.0, brightness_range=None, shear_range=0.0, zoom_range=0.0, channel_shift_range=0.0, fill_mode='nearest', cval=0.0, horizontal_flip=False, vertical_flip=False, rescale=None, preprocessing_function=None, data_format=None, validation_split=0.0, dtype=None)
通过实时数据增强生成张量图像数据批次。数据将不断循环(按批次)。
参数:
featurewise_center: 布尔值。将输入数据的均值设置为 0,逐特征进行。 samplewise_center: 布尔值。将每个样本的均值设置为 0。 featurewise_std_normalization: Boolean. 布尔值。将输入除以数据标准差,逐特征进行。 samplewise_std_normalization: 布尔值。将每个输入除以其标准差。 zca_epsilon: ZCA 白化的 epsilon 值,默认为 1e-6。 zca_whitening: 布尔值。是否应用 ZCA 白化。 rotation_range: 整数。随机旋转的度数范围。 width_shift_range: 浮点数、一维数组或整数 float: 如果 <1,则是除以总宽度的值,或者如果 >=1,则为像素值。 1-D 数组: 数组中的随机元素。 int: 来自间隔 (-width_shift_range, +width_shift_range) 之间的整数个像素。 width_shift_range=2 时,可能值是整数 [-1, 0, +1],与 width_shift_range=[-1, 0, +1] 相同;而 width_shift_range=1.0 时,可能值是 [-1.0, +1.0) 之间的浮点数。 height_shift_range: 浮点数、一维数组或整数 float: 如果 <1,则是除以总宽度的值,或者如果 >=1,则为像素值。 1-D array-like: 数组中的随机元素。 int: 来自间隔 (-height_shift_range, +height_shift_range) 之间的整数个像素。 height_shift_range=2 时,可能值是整数 [-1, 0, +1],与 height_shift_range=[-1, 0, +1] 相同;而 height_shift_range=1.0 时,可能值是 [-1.0, +1.0) 之间的浮点数。 shear_range: 浮点数。剪切强度(以弧度逆时针方向剪切角度)。 zoom_range: 浮点数 或 [lower, upper]。随机缩放范围。如果是浮点数,[lower, upper] = [1-zoom_range, 1+zoom_range]。 channel_shift_range: 浮点数。随机通道转换的范围。 fill_mode: {"constant", "nearest", "reflect" or "wrap"} 之一。默认为 'nearest'。输入边界以外的点根据给定的模式填充: 'constant': kkkkkkkk|abcd|kkkkkkkk (cval=k) 'nearest': aaaaaaaa|abcd|dddddddd 'reflect': abcddcba|abcd|dcbaabcd 'wrap': abcdabcd|abcd|abcdabcd cval: 浮点数或整数。用于边界之外的点的值,当 fill_mode = "constant" 时。 horizontal_flip: 布尔值。随机水平翻转。 vertical_flip: 布尔值。随机垂直翻转。 rescale: 重缩放因子。默认为 None。如果是 None 或 0,不进行缩放,否则将数据乘以所提供的值(在应用任何其他转换之前)。 preprocessing_function: 应用于每个输入的函数。这个函数会在任何其他改变之前运行。这个函数需要一个参数:一张图像(秩为 3 的 Numpy 张量),并且应该输出一个同尺寸的 Numpy 张量。 data_format: 图像数据格式,{"channels_first", "channels_last"} 之一。"channels_last" 模式表示图像输入尺寸应该为 (samples, height, width, channels),"channels_first" 模式表示输入尺寸应该为 (samples, channels, height, width)。默认为 在 Keras 配置文件 ~/.keras/keras.json 中的 image_data_format 值。如果你从未设置它,那它就是 "channels_last"。 validation_split: 浮点数。Float. 保留用于验证的图像的比例(严格在0和1之间)。 dtype: 生成数组使用的数据类型。
关键问题不在于这个图片生成,而是这个图片生成器的方法里面提供了一个函数——flow_from_directory(directory)
这个函数的参数如下:
flow_from_directory(directory): 以文件夹路径为参数,生成经过数据提升/归一化后的数据,在一个无限循环中无限产生batch数据
directory: 目标文件夹路径,对于每一个类,该文件夹都要包含一个子文件夹.子文件夹中任何JPG、PNG、BNP、PPM的图片都会被生成器使用.详情请查看[此脚本](https://link.jianshu.com/?t=https://gist.github.com/fchollet/0830affa1f7f19fd47b06d4cf89ed44d)
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自带的导入数据的方法了,并且附带了图片的处理。
我们的代码可以这样写:
from keras import backend as K from keras.preprocessing.image import ImageDataGenerator ‘’‘数据导入’‘’ datagen = ImageDataGenerator(data_format = K.image_data_format()) train_generator = datagen.flow_from_directory( '/home/ubuntu/MNIST/train', target_size=(120, 120), color_mode='rgb', batch_size=256) test_generator = datagen.flow_from_directory( '/home/ubuntu/MNIST/test', target_size=(120, 120), color_mode='rgb', batch_size=256) ‘’‘训练’‘’ path = os.path.join(MODEL_PATH,'MNIST_googlenet_keras.h5')#训练模型的存储地址 model.fit_generator(train_generator, steps_per_epoch=500, epochs=30,workers=4,verbose=1)#训练 model.save(path)#存储模型 scores = model.evaluate_generator(generator=test_generator,workers=4,use_multiprocessing=True,verbose=0)#验证 print('%s: %.2f' % (model.metrics_names[0], scores[0])) # Loss print('%s: %.2f%%' % (model.metrics_names[1], scores[1] * 100)) # metrics1 print('%s: %.2f%%' % (model.metrics_names[2], scores[2] * 100)) # metrics2
这样我们就导入了数据了。自此Keras的简单使用已经不成问题。
最后,我们需要注意,最后的导入数据的时候,会自动搜索里面的文件夹,但是是按字典序排序的。这很自然。例如你的文件夹是分类问题,文件夹都是猫、狗、鼠这样的汉字,它当然得按字典序排序。但是如果是像我们用0、1、2、3…这样的数字,就容易让人崩溃。因此我们需要注意在生成文件夹的时候,前面补0,即000、001、003、…、999。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。