当我们建立和训练Keras深度学习模型时,训练数据可以以几种不同的方式提供。将数据呈现为NumPy数组或TensorFlow张量是一种常见的方式。制作一个Python生成器函数并让训练循环从它那里读取数据是另一种方式。然而,另一种提供数据的方式是使用tf.data 数据集。
在本教程中,我们将看到我们如何为Keras模型使用tf.data 数据集。完成本教程后,你将学会。
- 如何创建和使用
tf.data数据集 - 与生成器函数相比,这样做的好处
让我们开始吧。
概述
本文分为四个部分,分别是:
- 用NumPy阵列和生成器函数训练Keras模型
- 使用NumPy阵列和Generator函数创建数据集
tf.data - 从生成器函数创建数据集
- 使用预取的数据
用NumPy数组和Generator函数训练Keras模型
在我们看到tf.data API如何工作之前,让我们回顾一下我们通常如何训练一个Keras模型。
首先,我们需要一个数据集。一个例子是Keras API附带的时尚MNIST数据集,我们有60,000个训练样本和10,000个28×28像素的灰度测试样本,相应的分类标签用整数0到9编码。
该数据集是一个NumPy数组。然后我们可以建立一个Keras模型进行分类,通过模型的fit() ,我们提供NumPy数组作为数据。
完整的代码如下。
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras.datasets.fashion_mnist import load_data
from tensorflow.keras.layers import Dense, Flatten
from tensorflow.keras.models import Sequential
(train_image, train_label), (test_image, test_label) = load_data()
print(train_image.shape)
print(train_label.shape)
print(test_image.shape)
print(test_label.shape)
model = Sequential([
Flatten(input_shape=(28,28)),
Dense(100, activation="relu"),
Dense(100, activation="relu"),
Dense(10, activation="sigmoid")
])
model.compile(optimizer="adam",
loss="sparse_categorical_crossentropy",
metrics="sparse_categorical_accuracy")
history = model.fit(train_image, train_label,
batch_size=32, epochs=50,
validation_data=(test_image, test_label), verbose=0)
print(model.evaluate(test_image, test_label))
plt.plot(history.history['val_sparse_categorical_accuracy'])
plt.show()
运行这段代码将打印出以下内容。
(60000, 28, 28)
(60000,)
(10000, 28, 28)
(10000,)
313/313 [==============================] - 0s 392us/step - loss: 0.5114 - sparse_categorical_accuracy: 0.8446
[0.5113903284072876, 0.8446000218391418]
并且还可以在我们训练模型的50个epochs中创建以下的验证准确率图。
训练同一网络的另一种方法是通过Python生成器函数而不是NumPy数组来提供数据。生成器函数是一个带有yield 语句的函数,在该函数与数据消费者并行运行时,发出数据。一个时尚的MNIST数据集的生成器可以被创建如下。
def batch_generator(image, label, batchsize):
N = len(image)
i = 0
while True:
yield image[i:i+batchsize], label[i:i+batchsize]
i = i + batchsize
if i + batchsize > N:
i = 0
这个函数的调用语法是:batch_generator(train_image, train_label, 32) 。它将无限期地分批扫描输入数组。一旦它到达数组的末端,它将从头开始。
用生成器训练Keras模型是类似的,使用fit() 函数。
history = model.fit(batch_generator(train_image, train_label, 32),
steps_per_epoch=len(train_image)//32,
epochs=50, validation_data=(test_image, test_label), verbose=0)
我们不需要提供数据和标签,而只需要提供生成器,因为生成器会给出这两个数据。当数据以NumPy数组形式呈现时,我们可以通过查看数组的长度来判断有多少个样本。当整个数据集被使用一次时,Keras可以完成一个epoch。然而,我们的生成器函数将无限期地发射批次,所以我们需要告诉一个纪元何时结束,使用fit() 函数的参数steps_per_epoch 。
虽然在上面的代码中,我们以NumPy数组的形式提供了验证数据,但我们也可以用生成器代替,并指定validation_steps 参数。
下面是使用生成器函数的完整代码,其输出与前面的例子相同。
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras.datasets.fashion_mnist import load_data
from tensorflow.keras.layers import Dense, Flatten
from tensorflow.keras.models import Sequential
(train_image, train_label), (test_image, test_label) = load_data()
print(train_image.shape)
print(train_label.shape)
print(test_image.shape)
print(test_label.shape)
model = Sequential([
Flatten(input_shape=(28,28)),
Dense(100, activation="relu"),
Dense(100, activation="relu"),
Dense(10, activation="sigmoid")
])
def batch_generator(image, label, batchsize):
N = len(image)
i = 0
while True:
yield image[i:i+batchsize], label[i:i+batchsize]
i = i + batchsize
if i + batchsize > N:
i = 0
model.compile(optimizer="adam",
loss="sparse_categorical_crossentropy",
metrics="sparse_categorical_accuracy")
history = model.fit(batch_generator(train_image, train_label, 32),
steps_per_epoch=len(train_image)//32,
epochs=50, validation_data=(test_image, test_label), verbose=0)
print(model.evaluate(test_image, test_label))
plt.plot(history.history['val_sparse_categorical_accuracy'])
plt.show()
使用生成器创建一个数据集tf.data
鉴于我们已经加载了时尚的MNIST数据,我们可以把它转换成一个tf.data 数据集,如下所示。
...
dataset = tf.data.Dataset.from_tensor_slices((train_image, train_label))
print(dataset.element_spec)
这将打印出数据集的规格,如下所示。
(TensorSpec(shape=(28, 28), dtype=tf.uint8, name=None),
TensorSpec(shape=(), dtype=tf.uint8, name=None))
我们可以看到数据是一个元组(因为我们把元组作为参数传给了from_tensor_slices() 函数),而第一个元素的形状是(28,28) ,第二个元素是一个标量。这两个元素都被存储为8位无符号整数。
如果我们在创建数据集的时候没有把数据作为两个NumPy数组的元组来呈现,我们也可以在以后这样做。下面是创建相同的数据集,但首先为图像数据和标签分别创建数据集,然后再将它们合并。
...
train_image_data = tf.data.Dataset.from_tensor_slices(train_image)
train_label_data = tf.data.Dataset.from_tensor_slices(train_label)
dataset = tf.data.Dataset.zip((train_image_data, train_label_data))
print(dataset.element_spec)
这将打印出相同的规格。
(TensorSpec(shape=(28, 28), dtype=tf.uint8, name=None),
TensorSpec(shape=(), dtype=tf.uint8, name=None))
dataset中的zip() 函数与Python中的zip() 函数类似,它将多个数据集的数据逐一匹配成一个元组。
使用tf.data dataset的一个好处是可以灵活处理数据。下面是关于我们如何使用数据集训练Keras模型的完整代码,其中批次大小被设置为数据集。
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras.datasets.fashion_mnist import load_data
from tensorflow.keras.layers import Dense, Flatten
from tensorflow.keras.models import Sequential
(train_image, train_label), (test_image, test_label) = load_data()
dataset = tf.data.Dataset.from_tensor_slices((train_image, train_label))
model = Sequential([
Flatten(input_shape=(28,28)),
Dense(100, activation="relu"),
Dense(100, activation="relu"),
Dense(10, activation="sigmoid")
])
history = model.fit(dataset.batch(32),
epochs=50,
validation_data=(test_image, test_label),
verbose=0)
print(model.evaluate(test_image, test_label))
plt.plot(history.history['val_sparse_categorical_accuracy'])
plt.show()
这是使用数据集的最简单用例。如果我们深入研究,我们可以看到,数据集只是一个迭代器。因此,我们可以用下面的方法打印出数据集中的每个样本。
for image, label in dataset:
print(image) # array of shape (28,28) in tf.Tensor
print(label) # integer label in tf.Tensor
数据集有许多内置的函数。我们之前使用的batch() ,就是其中之一。如果我们从dataset中创建批次并打印出来,我们就会有以下结果。
for image, label in dataset.batch(32):
print(image) # array of shape (32,28,28) in tf.Tensor
print(label) # array of shape (32,) in tf.Tensor
其中我们从一个批次中得到的每一项不是一个样本,而是一批样本。我们还有一些函数,如:map(),filter(), 和reduce() ,用于序列转换,或concatendate() 和interleave() ,用于与另一个数据集结合。还有repeat(),take(),take_while(), 和skip() ,就像我们熟悉的来自Python的itertools 模块的对应函数。这些函数的完整列表可以从API文档中找到。
从生成器函数创建一个数据集
到目前为止,我们看到了在训练Keras模型时如何用数据集来代替NumPy数组。事实上,数据集也可以从生成器函数中创建。但是,我们不是像在上面的一个例子中看到的生成器函数生成一个批次,而是在这里做一个生成器函数,每次生成一个样本。下面是这个函数。
import numpy as np
import tensorflow as tf
def shuffle_generator(image, label, seed):
idx = np.arange(len(image))
np.random.default_rng(seed).shuffle(idx)
for i in idx:
yield image[i], label[i]
dataset = tf.data.Dataset.from_generator(
shuffle_generator,
args=[train_image, train_label, 42],
output_signature=(
tf.TensorSpec(shape=(28,28), dtype=tf.uint8),
tf.TensorSpec(shape=(), dtype=tf.uint8)))
print(dataset.element_spec)
这个函数通过对索引向量进行洗牌来随机化输入数组。然后它每次生成一个样本。与之前的例子不同,这个生成器将在数组中的样本用完后结束。
我们使用from_generator() ,从该函数中创建一个数据集。我们需要提供生成器函数的名称(而不是实例化的生成器),同时提供数据集的输出签名。这是必须的,因为tf.data.Dataset API不能在生成器被消耗之前推断出数据集的规格。
运行上述代码将打印出与之前相同的规格。
(TensorSpec(shape=(28, 28), dtype=tf.uint8, name=None),
TensorSpec(shape=(), dtype=tf.uint8, name=None))
这样的数据集在功能上与我们之前创建的数据集是等同的。因此,我们可以像以前一样使用它进行训练。下面是完整的代码。
import matplotlib.pyplot as plt
import numpy as np
import tensorflow as tf
from tensorflow.keras.datasets.fashion_mnist import load_data
from tensorflow.keras.layers import Dense, Flatten
from tensorflow.keras.models import Sequential
(train_image, train_label), (test_image, test_label) = load_data()
def shuffle_generator(image, label, seed):
idx = np.arange(len(image))
np.random.default_rng(seed).shuffle(idx)
for i in idx:
yield image[i], label[i]
dataset = tf.data.Dataset.from_generator(
shuffle_generator,
args=[train_image, train_label, 42],
output_signature=(
tf.TensorSpec(shape=(28,28), dtype=tf.uint8),
tf.TensorSpec(shape=(), dtype=tf.uint8)))
model = Sequential([
Flatten(input_shape=(28,28)),
Dense(100, activation="relu"),
Dense(100, activation="relu"),
Dense(10, activation="sigmoid")
])
history = model.fit(dataset.batch(32),
epochs=50,
validation_data=(test_image, test_label),
verbose=0)
print(model.evaluate(test_image, test_label))
plt.plot(history.history['val_sparse_categorical_accuracy'])
plt.show()
带预取的数据集
使用数据集的真正好处是使用prefetch() 。
使用NumPy数组进行训练在性能上可能是最好的。然而,这意味着我们需要将所有数据加载到内存中。使用生成器函数进行训练可以让我们一次准备一个批次,比如说,数据可以按需从磁盘加载。然而,使用生成器函数来训练Keras模型意味着训练循环或生成器函数在任何时候都在运行。要让生成器函数和Keras的训练循环并行运行,并不容易。
Dataset是允许生成器和训练循环并行运行的API。如果你有一个计算成本很高的生成器(例如,在实时做图像增强),你可以从这样的生成器函数中创建一个数据集,然后用prefetch() ,如下所示。
...
history = model.fit(dataset.batch(32).prefetch(3),
epochs=50,
validation_data=(test_image, test_label),
verbose=0)
prefetch() 的数字参数是缓冲区的大小。这里我们要求数据集在内存中保留3个批次,供训练循环使用。每当一个批次被消耗,数据集的API将恢复生成器函数来重新填充缓冲区,在后台异步进行。因此,我们可以允许训练循环和生成器函数中的数据准备算法并行运行。
值得一提的是,在上一节中,我们为数据集API创建了一个洗牌发生器。事实上,数据集API也有一个shuffle() 函数来做同样的事情,但是我们可能不想使用它,除非数据集小到足以装入内存。
shuffle() 函数,与prefetch() 相同,需要一个缓冲区大小的参数。shuffle算法将用数据集填充缓冲区,并从中随机抽取一个元素。被消耗的元素将被数据集中的下一个元素取代。因此,我们需要一个和数据集本身一样大的缓冲区来进行真正的随机洗牌。我们可以用下面的片段来证明这个限制。
import tensorflow as tf
import numpy as np
n_dataset = tf.data.Dataset.from_tensor_slices(np.arange(10000))
for n in n_dataset.shuffle(10).take(20):
print(n.numpy())
上面的输出看起来像下面这样。
9
6
2
7
5
1
4
14
11
17
19
18
3
16
15
22
10
23
21
13
我们可以看到数字是在其附近洗牌的,我们从来没有在其输出中看到大的数字。
总结
在这篇文章中,你已经看到了我们如何使用tf.data 数据集,以及如何将其用于训练Keras模型。
具体来说,你学到了:
- 如何使用来自NumPy数组、生成器和数据集的数据来训练一个模型
- 如何使用NumPy数组或生成器函数创建一个数据集
- 如何使用数据集的预取功能来使生成器和训练循环并行运行