3.6 使用 PyDataset 实例进行训练和评估
keras.utils.PyDataset 是一个工具类,可以通过继承它来得到一个具有两个重要属性的 Python 生成器:
- 与多进程工作协同工作优秀。
- 可以被打乱(例如,在 fit() 方法中传递 shuffle=True)。
PyDataset 必须实现两个方法: - getitem
- len
getitem 方法的返回值是一个完整的批次。如果你想在训练轮次之间修改数据集,可以使用on_epoch_end方法实现。
以下是一个简单的示例:
class ExamplePyDataset(keras.utils.PyDataset):
def __init__(self, x, y, batch_size, **kwargs):
super().__init__(**kwargs)
self.x = x
self.y = y
self.batch_size = batch_size
def __len__(self):
return int(np.ceil(len(self.x) / float(self.batch_size)))
def __getitem__(self, idx):
batch_x = self.x[idx * self.batch_size : (idx + 1) * self.batch_size]
batch_y = self.y[idx * self.batch_size : (idx + 1) * self.batch_size]
return batch_x, batch_y
train_py_dataset = ExamplePyDataset(x_train, y_train, batch_size=32)
val_py_dataset = ExamplePyDataset(x_val, y_val, batch_size=32)
为了训练模型,将数据集作为 x
参数传递(由于数据集已经包含了目标值,因此不需要 y
参数),并将验证数据集作为 validation_data
参数传递。由于数据集已经进行了批次处理,因此不需要 batch_size
参数!
model = get_compiled_model()
model.fit(train_py_dataset, batch_size=64, validation_data=val_py_dataset, epochs=1)
1563/1563 ━━━━━━━━━━━━━━━━━━━━ 1s 443us/step - loss: 0.5217 - sparse_categorical_accuracy: 0.8473 - val_loss: 0.1576 - val_sparse_categorical_accuracy: 0.9525
<keras.src.callbacks.history.History at 0x2f9c8d120>
评估模型也同样简单:
model.evaluate(val_py_dataset)
313/313 ━━━━━━━━━━━━━━━━━━━━ 0s 157us/step - loss: 0.1821 - sparse_categorical_accuracy: 0.9450
[0.15764616429805756, 0.9524999856948853]
重要的是,PyDataset 对象支持三个常见的构造函数参数,这些参数处理并行处理配置:
workers:在多线程或多进程中要使用的工作线程数。通常,你会将其设置为你的 CPU 上的核心数。
use_multiprocessing:是否使用 Python 的多进程来进行并行处理。将此参数设置为 True 意味着你的数据集将在多个派生进程中复制。这是为了从并行处理中获得计算级别(而不是 I/O 级别)的益处所必需的。然而,只有当你的数据集可以安全地 pickle 时,才能将此参数设置为 True。
max_queue_size:在多线程或多进程设置中迭代数据集时,队列中保持的最大批次数。你可以减少这个值以减少数据集的 CPU 内存消耗。它默认为 10。
默认情况下,多进程是禁用的(use_multiprocessing=False),并且只使用一个线程。你应该确保只在你的代码运行在 Python 的 if __name__ == "__main__":
块内时才启用 use_multiprocessing,以避免出现问题。
下面是一个 4 线程、非多进程的示例:
train_py_dataset = ExamplePyDataset(x_train, y_train, batch_size=32, workers=4)
val_py_dataset = ExamplePyDataset(x_val, y_val, batch_size=32, workers=4)
model = get_compiled_model()
model.fit(train_py_dataset, batch_size=64, validation_data=val_py_dataset, epochs=1)
1563/1563 ━━━━━━━━━━━━━━━━━━━━ 1s 561us/step - loss: 0.5146 - sparse_categorical_accuracy: 0.8516 - val_loss: 0.1623 - val_sparse_categorical_accuracy: 0.9514
<keras.src.callbacks.history.History at 0x2e7fd5ea0>
3.7 使用 PyTorch DataLoader 对象进行训练和评估
所有内置的训练和评估 API 都与 torch.utils.data.Dataset
和 torch.utils.data.DataLoader
对象兼容——无论你是使用 PyTorch 后端,还是 JAX 或 TensorFlow 后端。让我们看一个简单的例子。
与以批次为中心的 PyDataset 不同,PyTorch Dataset 对象是以样本为中心的:__len__
方法返回样本的数量,而 __getitem__
方法返回特定的样本。
class ExampleTorchDataset(torch.utils.data.Dataset):
def __init__(self, x, y):
self.x = x
self.y = y
def __len__(self):
return len(self.x)
def __getitem__(self, idx):
return self.x[idx], self.y[idx]
train_torch_dataset = ExampleTorchDataset(x_train, y_train)
val_torch_dataset = ExampleTorchDataset(x_val, y_val)
要使用 PyTorch Dataset,需要将其包装到一个 DataLoader 中,DataLoader 负责批处理和打乱数据。
train_dataloader = torch.utils.data.DataLoader(
train_torch_dataset, batch_size=32, shuffle=True
)
val_dataloader = torch.utils.data.DataLoader(
val_torch_dataset, batch_size=32, shuffle=True
)
现在可以像使用其他任何迭代器一样在 Keras API 中使用它们:
model = get_compiled_model()
model.fit(train_dataloader, batch_size=64, validation_data=val_dataloader, epochs=1)
model.evaluate(val_dataloader)
1563/1563 ━━━━━━━━━━━━━━━━━━━━ 1s 575us/step - loss: 0.5051 - sparse_categorical_accuracy: 0.8568 - val_loss: 0.1613 - val_sparse_categorical_accuracy: 0.9528
313/313 ━━━━━━━━━━━━━━━━━━━━ 0s 278us/step - loss: 0.1551 - sparse_categorical_accuracy: 0.9541
[0.16209803521633148, 0.9527999758720398]
3.8 使用样本权重和类别权重
在默认设置下,样本的权重由其在数据集中的频率决定。有两种方法可以根据样本频率之外的因素来加权数据:
- 类别权重
- 样本权重
类别权重
这通过设置传递给 Model.fit()
方法的 class_weight
参数的一个字典来实现。这个字典将类索引映射到应用于属于这个类的样本的权重。
这可以用于在不重新采样的情况下平衡类别,或者训练一个模型,使其对某个特定类别给予更多重视。
例如,如果你的数据中类别“0”的出现频率是类别“1”的一半,你可以使用 Model.fit(..., class_weight={0: 1., 1: 0.5})
。
以下是一个 NumPy 示例,其中我们使用类别权重或样本权重来增加对正确分类第5类(在 MNIST 数据集中是数字“5”)的重视。
class_weight = {
0: 1.0,
1: 1.0,
2: 1.0,
3: 1.0,
4: 1.0,
# Set weight "2" for class "5",
# making this class 2x more important
5: 2.0,
6: 1.0,
7: 1.0,
8: 1.0,
9: 1.0,
}
print("Fit with class weight")
model = get_compiled_model()
model.fit(x_train, y_train, class_weight=class_weight, batch_size=64, epochs=1)
Fit with class weight
782/782 ━━━━━━━━━━━━━━━━━━━━ 1s 534us/step - loss: 0.6205 - sparse_categorical_accuracy: 0.8375
<keras.src.callbacks.history.History at 0x298d44eb0>
样本权重
为了更精细的控制,或者如果你不是在构建分类器,你可以使用“样本权重”。
当从 NumPy 数据进行训练时:将 sample_weight
参数传递给 Model.fit()
。
当从 tf.data
或其他任何类型的迭代器进行训练时:生成 (input_batch, label_batch, sample_weight_batch)
元组。
“样本权重”数组是一个数字数组,指定在计算总损失时每个批次中每个样本应该具有的权重。它通常在类别不平衡的分类问题中使用(想法是给很少见的类别更多的权重)。
当使用的权重是 1 和 0 时,这个数组可以用作损失函数的掩码(完全忽略某些样本对总损失的贡献)。
sample_weight = np.ones(shape=(len(y_train),))
sample_weight[y_train == 5] = 2.0
print("Fit with sample weight")
model = get_compiled_model()
model.fit(x_train, y_train, sample_weight=sample_weight, batch_size=64, epochs=1)
Fit with sample weight
782/782 ━━━━━━━━━━━━━━━━━━━━ 1s 546us/step - loss: 0.6397 - sparse_categorical_accuracy: 0.8388
<keras.src.callbacks.history.History at 0x298e066e0>
以下是一个匹配的数据集示例:
sample_weight = np.ones(shape=(len(y_train),))
sample_weight[y_train == 5] = 2.0
# Create a Dataset that includes sample weights
# (3rd element in the return tuple).
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train, sample_weight))
# Shuffle and slice the dataset.
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)
model = get_compiled_model()
model.fit(train_dataset, epochs=1)
782/782 ━━━━━━━━━━━━━━━━━━━━ 1s 651us/step - loss: 0.5971 - sparse_categorical_accuracy: 0.8445
<keras.src.callbacks.history.History at 0x312854100>
3.9 向多输入、多输出模型传递数据
在前面的示例中,我们考虑的是一个具有单个输入(形状为 (764,) 的张量)和单个输出(形状为 (10,) 的预测张量)的模型。但是,对于具有多个输入或输出的模型又该如何处理呢?
考虑以下模型,它有一个形状为 (32, 32, 3) 的图像输入(即 (高度, 宽度, 通道))和一个形状为 (None, 10) 的时间序列输入(即 (时间步长, 特征))。我们的模型将根据这些输入的组合计算两个输出:一个“分数”(形状为 (1,))和一个五个类别的概率分布(形状为 (5,))。
image_input = keras.Input(shape=(32, 32, 3), name="img_input")
timeseries_input = keras.Input(shape=(None, 10), name="ts_input")
x1 = layers.Conv2D(3, 3)(image_input)
x1 = layers.GlobalMaxPooling2D()(x1)
x2 = layers.Conv1D(3, 3)(timeseries_input)
x2 = layers.GlobalMaxPooling1D()(x2)
x = layers.concatenate([x1, x2])
score_output = layers.Dense(1, name="score_output")(x)
class_output = layers.Dense(5, name="class_output")(x)
model = keras.Model(
inputs=[image_input, timeseries_input], outputs=[score_output, class_output]
)
我们来画出这个模型,这样就能清楚地看到我们在这里做什么(请注意,图中显示的形状是批处理形状,而不是每个样本的形状)。
keras.utils.plot_model(model, "multi_input_and_output_model.png", show_shapes=True)
在编译时,我们可以通过将损失函数作为列表传递来指定不同输出对应的不同损失。
model.compile(
optimizer=keras.optimizers.RMSprop(1e-3),
loss=[
keras.losses.MeanSquaredError(),
keras.losses.CategoricalCrossentropy(),
],
)
如果我们只向模型传递一个损失函数,那么该损失函数将应用于每个输出(这在这种情况下并不适用)。
同样地,对于评估指标也是如此:
model.compile(
optimizer=keras.optimizers.RMSprop(1e-3),
loss=[
keras.losses.MeanSquaredError(),
keras.losses.CategoricalCrossentropy(),
],
metrics=[
[
keras.metrics.MeanAbsolutePercentageError(),
keras.metrics.MeanAbsoluteError(),
],
[keras.metrics.CategoricalAccuracy()],
],
)
由于我们为输出层指定了名称,因此也可以通过字典来指定每个输出的损失和指标:
model.compile(
optimizer=keras.optimizers.RMSprop(1e-3),
loss={
"score_output": keras.losses.MeanSquaredError(),
"class_output": keras.losses.CategoricalCrossentropy(),
},
metrics={
"score_output": [
keras.metrics.MeanAbsolutePercentageError(),
keras.metrics.MeanAbsoluteError(),
],
"class_output": [keras.metrics.CategoricalAccuracy()],
},
)
如果有两个以上的输出,我们建议使用明确的名称和字典。
可以使用 loss_weights
参数给不同特定输出的损失分配不同的权重(例如,在我们的例子中,可能希望“分数”损失的权重是类别损失的两倍,从而给予“分数”损失更多的重视)。
model.compile(
optimizer=keras.optimizers.RMSprop(1e-3),
loss={
"score_output": keras.losses.MeanSquaredError(),
"class_output": keras.losses.CategoricalCrossentropy(),
},
metrics={
"score_output": [
keras.metrics.MeanAbsolutePercentageError(),
keras.metrics.MeanAbsoluteError(),
],
"class_output": [keras.metrics.CategoricalAccuracy()],
},
loss_weights={"score_output": 2.0, "class_output": 1.0},
)
也可以选择不为某些输出计算损失,如果这些输出仅用于预测而不是用于训练的话。
# List loss version
model.compile(
optimizer=keras.optimizers.RMSprop(1e-3),
loss=[None, keras.losses.CategoricalCrossentropy()],
)
# Or dict loss version
model.compile(
optimizer=keras.optimizers.RMSprop(1e-3),
loss={"class_output": keras.losses.CategoricalCrossentropy()},
)
在 fit()
中向多输入或多输出模型传递数据的方式与在 compile
中指定损失函数类似:你可以传递 NumPy 数组的列表(与接收损失函数的输出有一一对应关系),或者传递将输出名称映射到 NumPy 数组的字典。
model.compile(
optimizer=keras.optimizers.RMSprop(1e-3),
loss=[
keras.losses.MeanSquaredError(),
keras.losses.CategoricalCrossentropy(),
],
)
# Generate dummy NumPy data
img_data = np.random.random_sample(size=(100, 32, 32, 3))
ts_data = np.random.random_sample(size=(100, 20, 10))
score_targets = np.random.random_sample(size=(100, 1))
class_targets = np.random.random_sample(size=(100, 5))
# Fit on lists
model.fit([img_data, ts_data], [score_targets, class_targets], batch_size=32, epochs=1)
# Alternatively, fit on dicts
model.fit(
{"img_input": img_data, "ts_input": ts_data},
{"score_output": score_targets, "class_output": class_targets},
batch_size=32,
epochs=1,
)
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 62ms/step - loss: 18.0146
4/4 ━━━━━━━━━━━━━━━━━━━━ 0s 56ms/step - loss: 17.6494
<keras.src.callbacks.history.History at 0x31a6c5810>
以下是数据集的使用案例:类似于我们为 NumPy 数组所做的那样,数据集应该返回一个字典的元组。
train_dataset = tf.data.Dataset.from_tensor_slices(
(
{"img_input": img_data, "ts_input": ts_data},
{"score_output": score_targets, "class_output": class_targets},
)
)
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)
model.fit(train_dataset, epochs=1)
2/2 ━━━━━━━━━━━━━━━━━━━━ 0s 197ms/step - loss: 17.8578
<keras.src.callbacks.history.History at 0x17c7e5690>
3.10 使用回调
在 Keras 中,回调是在训练过程中的不同时间点(如一个纪元的开始、一批次的结束、一个纪元的结束等)被调用的对象。它们可以用于实现某些行为,例如:
- 在训练过程中的不同点进行验证(除了内置的每纪元验证)
- 在固定间隔或模型超过某个准确率阈值时保存模型检查点
- 当训练似乎陷入停滞时改变模型的学习率
- 当训练似乎陷入停滞时对顶层进行微调
- 当训练结束或超过某个性能阈值时发送电子邮件或即时消息通知
- 等等
可以将回调作为列表传递给 fit()
方法的调用:
model = get_compiled_model()
callbacks = [
keras.callbacks.EarlyStopping(
# Stop training when `val_loss` is no longer improving
monitor="val_loss",
# "no longer improving" being defined as "no better than 1e-2 less"
min_delta=1e-2,
# "no longer improving" being further defined as "for at least 2 epochs"
patience=2,
verbose=1,
)
]
model.fit(
x_train,
y_train,
epochs=20,
batch_size=64,
callbacks=callbacks,
validation_split=0.2,
)
Epoch 1/20
625/625 ━━━━━━━━━━━━━━━━━━━━ 1s 622us/step - loss: 0.6245 - sparse_categorical_accuracy: 0.8275 - val_loss: 0.2231 - val_sparse_categorical_accuracy: 0.9330
Epoch 2/20
625/625 ━━━━━━━━━━━━━━━━━━━━ 0s 404us/step - loss: 0.1809 - sparse_categorical_accuracy: 0.9460 - val_loss: 0.1727 - val_sparse_categorical_accuracy: 0.9476
Epoch 3/20
625/625 ━━━━━━━━━━━━━━━━━━━━ 0s 398us/step - loss: 0.1336 - sparse_categorical_accuracy: 0.9598 - val_loss: 0.1564 - val_sparse_categorical_accuracy: 0.9545
Epoch 4/20
625/625 ━━━━━━━━━━━━━━━━━━━━ 0s 400us/step - loss: 0.1012 - sparse_categorical_accuracy: 0.9699 - val_loss: 0.1502 - val_sparse_categorical_accuracy: 0.9570
Epoch 5/20
625/625 ━━━━━━━━━━━━━━━━━━━━ 0s 403us/step - loss: 0.0835 - sparse_categorical_accuracy: 0.9748 - val_loss: 0.1436 - val_sparse_categorical_accuracy: 0.9589
Epoch 6/20
625/625 ━━━━━━━━━━━━━━━━━━━━ 0s 396us/step - loss: 0.0699 - sparse_categorical_accuracy: 0.9783 - val_loss: 0.1484 - val_sparse_categorical_accuracy: 0.9577
Epoch 7/20
625/625 ━━━━━━━━━━━━━━━━━━━━ 0s 402us/step - loss: 0.0603 - sparse_categorical_accuracy: 0.9814 - val_loss: 0.1406 - val_sparse_categorical_accuracy: 0.9629
Epoch 7: early stopping
<keras.src.callbacks.history.History at 0x31ae37c10>
Keras 已经提供了许多内置回调,例如:
- ModelCheckpoint:定期保存模型。
- EarlyStopping:当训练不再提高验证指标时停止训练。
- TensorBoard:定期写入模型日志,这些日志可以在 TensorBoard 中进行可视化(更多详情在“可视化”部分)。
- CSVLogger:将损失和指标数据流式传输到 CSV 文件。
- 等等
请查看回调的文档以获取完整列表。
编写你自己的回调
你可以通过扩展基础类 keras.callbacks.Callback
来创建自定义回调。一个回调可以通过类属性 self.model
访问其关联的模型。
请确保阅读完整的编写自定义回调的指南。
下面是一个简单的示例,用于在训练期间保存每批次的损失值列表:
class LossHistory(keras.callbacks.Callback):
def on_train_begin(self, logs):
self.per_batch_losses = []
def on_batch_end(self, batch, logs):
self.per_batch_losses.append(logs.get("loss"))
模型检查点
当你在相对较大的数据集上训练模型时,定期保存模型的检查点至关重要。
实现这一点的最简单方法是使用 ModelCheckpoint 回调:
model = get_compiled_model()
callbacks = [
keras.callbacks.ModelCheckpoint(
# Path where to save the model
# The two parameters below mean that we will overwrite
# the current checkpoint if and only if
# the `val_loss` score has improved.
# The saved model name will include the current epoch.
filepath="mymodel_{epoch}.keras",
save_best_only=True, # Only save a model if `val_loss` has improved.
monitor="val_loss",
verbose=1,
)
]
model.fit(
x_train,
y_train,
epochs=2,
batch_size=64,
callbacks=callbacks,
validation_split=0.2,
)
Epoch 1/2
559/625 ━━━━━━━━━━━━━━━━━[37m━━━ 0s 360us/step - loss: 0.6490 - sparse_categorical_accuracy: 0.8209
Epoch 1: val_loss improved from inf to 0.22393, saving model to mymodel_1.keras
625/625 ━━━━━━━━━━━━━━━━━━━━ 1s 577us/step - loss: 0.6194 - sparse_categorical_accuracy: 0.8289 - val_loss: 0.2239 - val_sparse_categorical_accuracy: 0.9340
Epoch 2/2
565/625 ━━━━━━━━━━━━━━━━━━[37m━━ 0s 355us/step - loss: 0.1816 - sparse_categorical_accuracy: 0.9476
Epoch 2: val_loss improved from 0.22393 to 0.16868, saving model to mymodel_2.keras
625/625 ━━━━━━━━━━━━━━━━━━━━ 0s 411us/step - loss: 0.1806 - sparse_categorical_accuracy: 0.9479 - val_loss: 0.1687 - val_sparse_categorical_accuracy: 0.9494
<keras.src.callbacks.history.History at 0x2e5cb7250>
ModelCheckpoint 回调可用于实现容错性:在训练随机中断的情况下,能够从模型最后保存的状态重新开始训练。以下是一个基本示例:
# Prepare a directory to store all the checkpoints.
checkpoint_dir = "./ckpt"
if not os.path.exists(checkpoint_dir):
os.makedirs(checkpoint_dir)
def make_or_restore_model():
# Either restore the latest model, or create a fresh one
# if there is no checkpoint available.
checkpoints = [checkpoint_dir + "/" + name for name in os.listdir(checkpoint_dir)]
if checkpoints:
latest_checkpoint = max(checkpoints, key=os.path.getctime)
print("Restoring from", latest_checkpoint)
return keras.models.load_model(latest_checkpoint)
print("Creating a new model")
return get_compiled_model()
model = make_or_restore_model()
callbacks = [
# This callback saves the model every 100 batches.
# We include the training loss in the saved model name.
keras.callbacks.ModelCheckpoint(
filepath=checkpoint_dir + "/model-loss={loss:.2f}.keras", save_freq=100
)
]
model.fit(x_train, y_train, epochs=1, callbacks=callbacks)
Creating a new model
1563/1563 ━━━━━━━━━━━━━━━━━━━━ 1s 390us/step - loss: 0.4910 - sparse_categorical_accuracy: 0.8623
<keras.src.callbacks.history.History at 0x2e5c454e0>
3.11 使用学习率调度
在训练深度学习模型时,一个常见的模式是随着训练的进行逐渐降低学习率。这通常被称为“学习率衰减”。
学习率衰减调度可以是静态的(作为当前纪元或当前批次索引的函数预先固定),也可以是动态的(响应模型的当前行为,特别是验证损失)。
向优化器传递一个调度
你可以通过将调度对象作为学习率参数传递给优化器来轻松使用静态学习率衰减调度:
initial_learning_rate = 0.1
lr_schedule = keras.optimizers.schedules.ExponentialDecay(
initial_learning_rate, decay_steps=100000, decay_rate=0.96, staircase=True
)
optimizer = keras.optimizers.RMSprop(learning_rate=lr_schedule)
提供了几种内置的调度器:ExponentialDecay(指数衰减)、PiecewiseConstantDecay(分段常数衰减)、PolynomialDecay(多项式衰减)和InverseTimeDecay(逆时间衰减)。
使用回调来实现动态学习率调度
动态学习率调度(例如,当验证损失不再改善时降低学习率)无法通过这些调度器对象实现,因为优化器无法访问验证指标。
然而,回调可以访问所有指标,包括验证指标!因此,可以通过使用一个修改优化器当前学习率的回调来实现这种模式。实际上,这甚至已经内置为 ReduceLROnPlateau 回调。