一、绪论
在Keras中,使用内置方法进行模型的训练和评估是非常常见的做法。Keras是一个流行的深度学习库,它为用户提供了简洁易用的API来构建和训练深度学习模型。
本问探讨了使用内置API进行模型训练、评估和预测(推断)的过程,这些API包括Model.fit()
、Model.evaluate()
和Model.predict()
。
如果你希望在调用fit()
时指定自己的训练步骤函数,可以按照以下内容的要求,了解如何自定义fit()
中的行为:
- 使用TensorFlow编写自定义训练步骤
- 使用JAX编写自定义训练步骤
- 使用PyTorch编写自定义训练步骤
如果你对从头开始编写自己的训练和评估循环,那么你可以按照以下内容的要求进行脚本编写: - 使用TensorFlow编写训练循环
- 使用JAX编写训练循环
- 使用PyTorch编写训练循环
总的来说,无论你使用内置循环还是自行编写,模型训练和评估在Keras的各种模型类型中(如Sequential模型、使用Functional API构建的模型以及通过模型子类化从头编写的模型)的工作方式都是完全相同的。
二、API概览:一个端到端的初步示例
当将数据传递给模型的内置训练循环时,应该使用以下方式之一:
- NumPy数组(如果您的数据较小且能够装入内存)
- keras.utils.Sequence的子类
- tf.data.Dataset对象
- PyTorch DataLoader实例
在接下来的几段中,我们将使用MNIST数据集作为NumPy数组,以演示如何使用优化器、损失函数和评估指标。之后,我们将详细探讨其他选项。
现在让我们考虑以下模型(这里我们使用函数式API构建,但也可以是Sequential模型或子类模型):
inputs = keras.Input(shape=(784,), name="digits")
x = layers.Dense(64, activation="relu", name="dense_1")(inputs)
x = layers.Dense(64, activation="relu", name="dense_2")(x)
outputs = layers.Dense(10, activation="softmax", name="predictions")(x)
model = keras.Model(inputs=inputs, outputs=outputs)
以下是典型的端到端工作流程,包括:
- 训练
- 在从原始训练数据中分割出来的保留集上进行验证
- 在测试数据上进行评估
我们将使用MNIST数据集作为这个示例。
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
# Preprocess the data (these are NumPy arrays)
x_train = x_train.reshape(60000, 784).astype("float32") / 255
x_test = x_test.reshape(10000, 784).astype("float32") / 255
y_train = y_train.astype("float32")
y_test = y_test.astype("float32")
# Reserve 10,000 samples for validation
x_val = x_train[-10000:]
y_val = y_train[-10000:]
x_train = x_train[:-10000]
y_train = y_train[:-10000]
指定训练配置(优化器、损失函数、评估指标):
model.compile(
optimizer=keras.optimizers.RMSprop(), # Optimizer
# Loss function to minimize
loss=keras.losses.SparseCategoricalCrossentropy(),
# List of metrics to monitor
metrics=[keras.metrics.SparseCategoricalAccuracy()],
)
调用fit()
方法,该方法将模型训练数据分割成大小为batch_size
的“批次”,并在给定数量的epochs
内反复迭代整个数据集。
print("Fit model on training data")
history = model.fit(
x_train,
y_train,
batch_size=64,
epochs=2,
# We pass some validation for
# monitoring validation loss and metrics
# at the end of each epoch
validation_data=(x_val, y_val),
)
Fit model on training data
Epoch 1/2
782/782 ━━━━━━━━━━━━━━━━━━━━ 1s 955us/step - loss: 0.5740 - sparse_categorical_accuracy: 0.8368 - val_loss: 0.2040 - val_sparse_categorical_accuracy: 0.9420
Epoch 2/2
782/782 ━━━━━━━━━━━━━━━━━━━━ 0s 390us/step - loss: 0.1745 - sparse_categorical_accuracy: 0.9492 - val_loss: 0.1415 - val_sparse_categorical_accuracy: 0.9581
通过evaluate()
方法来在测试数据上评估模型的性能:
# Evaluate the model on the test data using `evaluate`
print("Evaluate on test data")
results = model.evaluate(x_test, y_test, batch_size=128)
print("test loss, test acc:", results)
# Generate predictions (probabilities -- the output of the last layer)
# on new data using `predict`
print("Generate predictions for 3 samples")
predictions = model.predict(x_test[:3])
print("predictions shape:", predictions.shape)
Evaluate on test data
79/79 ━━━━━━━━━━━━━━━━━━━━ 0s 271us/step - loss: 0.1670 - sparse_categorical_accuracy: 0.9489
test loss, test acc: [0.1484374850988388, 0.9550999999046326]
Generate predictions for 3 samples
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 33ms/step
predictions shape: (3, 10)
三、训练和评估方法详解
3.1 compile()
方法:指定损失函数、评估指标和优化器
要使用fit()
方法来训练模型,需要指定一个损失函数、一个优化器,以及可选的一些要监控的评估指标。
通过将这些参数作为compile()
方法的参数传递给模型来实现这一点:
model.compile(
optimizer=keras.optimizers.RMSprop(learning_rate=1e-3),
loss=keras.losses.SparseCategoricalCrossentropy(),
metrics=[keras.metrics.SparseCategoricalAccuracy()],
)
metrics
参数应该是一个列表——模型可以有任意数量的评估指标。
如果模型有多个输出,可以为每个输出指定不同的损失函数和评估指标,并且可以调整每个输出对模型总损失的贡献。详情请参考“将数据传递给多输入、多输出模型”部分。
请注意,如果对默认设置满意,在许多情况下,可以通过字符串标识符作为快捷方式指定优化器、损失函数和评估指标。
model.compile(
optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["sparse_categorical_accuracy"],
)
为了后续的重用,让我们将模型的定义和编译步骤放在函数中;在本文的不同示例中,我们将多次调用这些函数。
def get_uncompiled_model():
inputs = keras.Input(shape=(784,), name="digits")
x = layers.Dense(64, activation="relu", name="dense_1")(inputs)
x = layers.Dense(64, activation="relu", name="dense_2")(x)
outputs = layers.Dense(10, activation="softmax", name="predictions")(x)
model = keras.Model(inputs=inputs, outputs=outputs)
return model
def get_compiled_model():
model = get_uncompiled_model()
model.compile(
optimizer="rmsprop",
loss="sparse_categorical_crossentropy",
metrics=["sparse_categorical_accuracy"],
)
return model
3.2 内置的优化器、损失函数和评估指标
通常,创建模型时不需要从头开始创建自己的损失函数、评估指标或优化器,因为这些需要已经是Keras API的一部分:
优化器:
-
SGD()(带或不带动量)
-
RMSprop()
-
Adam()
-
等等
损失函数: -
MeanSquaredError()
-
KLDivergence()
-
CosineSimilarity()
-
等等
评估指标: -
AUC()
-
Precision()
-
Recall()
-
等等
自定义损失
如果您需要创建一个自定义损失函数,Keras提供了三种方法来实现。
第一种方法涉及创建一个函数,该函数接受输入y_true
和y_pred
。以下示例显示了一个损失函数,它计算真实数据和预测之间的均方误差:
def custom_mean_squared_error(y_true, y_pred):
return ops.mean(ops.square(y_true - y_pred), axis=-1)
model = get_uncompiled_model()
model.compile(optimizer=keras.optimizers.Adam(), loss=custom_mean_squared_error)
# We need to one-hot encode the labels to use MSE
y_train_one_hot = ops.one_hot(y_train, num_classes=10)
model.fit(x_train, y_train_one_hot, batch_size=64, epochs=1)
782/782 ━━━━━━━━━━━━━━━━━━━━ 1s 525us/step - loss: 0.0277
<keras.src.callbacks.history.History at 0x2e5dde350>
如果需要一个除了y_true
和y_pred
之外还接受参数的损失函数,可以通过继承keras.losses.Loss
类并实现以下两个方法来实现:
__init__(self)
: 接受在调用损失函数时需要传递的参数
call(self, y_true, y_pred)
: 使用目标值(y_true
)和模型预测值(y_pred
)来计算模型的损失
假设想使用均方误差,但增加了一个项来减少远离0.5的预测值(我们假设类别目标是独热编码的,取值在0和1之间)。这会激励模型不要太过于自信,可能有助于减少过拟合(在我们尝试之前,我们不知道这是否有效!)。
以下是如何实现方法:
class CustomMSE(keras.losses.Loss):
def __init__(self, regularization_factor=0.1, name="custom_mse"):
super().__init__(name=name)
self.regularization_factor = regularization_factor
def call(self, y_true, y_pred):
mse = ops.mean(ops.square(y_true - y_pred), axis=-1)
reg = ops.mean(ops.square(0.5 - y_pred), axis=-1)
return mse + reg * self.regularization_factor
model = get_uncompiled_model()
model.compile(optimizer=keras.optimizers.Adam(), loss=CustomMSE())
y_train_one_hot = ops.one_hot(y_train, num_classes=10)
model.fit(x_train, y_train_one_hot, batch_size=64, epochs=1)
782/782 ━━━━━━━━━━━━━━━━━━━━ 1s 532us/step - loss: 0.0492
<keras.src.callbacks.history.History at 0x2e5d0d360>
3.3 自定义评估指标
如果需要API中没有提供的评估指标,可以通过继承keras.metrics.Metric
类来轻松地创建自定义评估指标。可以通过以下四个方法来实现:
__init__(self)
,在其中将为评估指标创建状态变量。
update_state(self, y_true, y_pred, sample_weight=None)
,该方法使用目标值y_true
和模型预测值y_pred
来更新状态变量。
result(self)
,该方法使用状态变量来计算最终结果。
reset_state(self)
,该方法重置评估指标的状态。
将状态更新和结果计算分开(分别在update_state()
和result()
方法中)是因为在某些情况下,结果计算可能非常昂贵,并且只会定期执行。
下面是一个简单的示例,展示了如何实现一个CategoricalTruePositives
评估指标,该指标计算被正确分类为给定类别的样本数:
class CategoricalTruePositives(keras.metrics.Metric):
def __init__(self, name="categorical_true_positives", **kwargs):
super().__init__(name=name, **kwargs)
self.true_positives = self.add_variable(
shape=(), name="ctp", initializer="zeros"
)
def update_state(self, y_true, y_pred, sample_weight=None):
y_pred = ops.reshape(ops.argmax(y_pred, axis=1), (-1, 1))
values = ops.cast(y_true, "int32") == ops.cast(y_pred, "int32")
values = ops.cast(values, "float32")
if sample_weight is not None:
sample_weight = ops.cast(sample_weight, "float32")
values = ops.multiply(values, sample_weight)
self.true_positives.assign_add(ops.sum(values))
def result(self):
return self.true_positives.value
def reset_state(self):
# The state of the metric will be reset at the start of each epoch.
self.true_positives.assign(0.0)
model = get_uncompiled_model()
model.compile(
optimizer=keras.optimizers.RMSprop(learning_rate=1e-3),
loss=keras.losses.SparseCategoricalCrossentropy(),
metrics=[CategoricalTruePositives()],
)
model.fit(x_train, y_train, batch_size=64, epochs=3)
Epoch 1/3
782/782 ━━━━━━━━━━━━━━━━━━━━ 1s 568us/step - categorical_true_positives: 180967.9219 - loss: 0.5876
Epoch 2/3
782/782 ━━━━━━━━━━━━━━━━━━━━ 0s 377us/step - categorical_true_positives: 182141.9375 - loss: 0.1733
Epoch 3/3
782/782 ━━━━━━━━━━━━━━━━━━━━ 0s 377us/step - categorical_true_positives: 182303.5312 - loss: 0.1180
<keras.src.callbacks.history.History at 0x2e5f02d10>
3.4 处理不符合标准签名的损失和指标
绝大多数的损失和指标都可以从y_true
和y_pred
中计算出来,其中y_pred
是模型的一个输出——但并非所有情况都如此。例如,正则化损失可能只需要某一层的激活值(在这种情况下没有目标值),而这个激活值可能并不是模型的输出。
在这种情况下,可以在自定义层的call
方法内部调用self.add_loss(loss_value)
。以这种方式添加的损失会在训练过程中被添加到“主要”损失中(即传递给compile()
方法的损失)。下面是一个简单的例子,展示了如何添加活动正则化(请注意,活动正则化在所有的Keras层中都是内置的——这个层只是为了提供一个具体的例子):
class ActivityRegularizationLayer(layers.Layer):
def call(self, inputs):
self.add_loss(ops.sum(inputs) * 0.1)
return inputs # Pass-through layer.
inputs = keras.Input(shape=(784,), name="digits")
x = layers.Dense(64, activation="relu", name="dense_1")(inputs)
# Insert activity regularization as a layer
x = ActivityRegularizationLayer()(x)
x = layers.Dense(64, activation="relu", name="dense_2")(x)
outputs = layers.Dense(10, name="predictions")(x)
model = keras.Model(inputs=inputs, outputs=outputs)
model.compile(
optimizer=keras.optimizers.RMSprop(learning_rate=1e-3),
loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
)
# The displayed loss will be much higher than before
# due to the regularization component.
model.fit(x_train, y_train, batch_size=64, epochs=1)
782/782 ━━━━━━━━━━━━━━━━━━━━ 1s 505us/step - loss: 3.4083
<keras.src.callbacks.history.History at 0x2e60226b0>
当通过add_loss()
方法添加损失时,您可以在调用compile()
方法时不指定损失函数,因为模型已经有了需要最小化的损失。
以下是一个LogisticEndpoint
层的例子:它接收目标和逻辑值(logits)作为输入,并通过add_loss()
方法跟踪交叉熵损失。
class LogisticEndpoint(keras.layers.Layer):
def __init__(self, name=None):
super().__init__(name=name)
self.loss_fn = keras.losses.BinaryCrossentropy(from_logits=True)
def call(self, targets, logits, sample_weights=None):
# Compute the training-time loss value and add it
# to the layer using `self.add_loss()`.
loss = self.loss_fn(targets, logits, sample_weights)
self.add_loss(loss)
# Return the inference-time prediction tensor (for `.predict()`).
return ops.softmax(logits)
可以在一个具有两个输入(输入数据和目标)的模型中使用它,并在编译时省略损失参数,如下所示:
inputs = keras.Input(shape=(3,), name="inputs")
targets = keras.Input(shape=(10,), name="targets")
logits = keras.layers.Dense(10)(inputs)
predictions = LogisticEndpoint(name="predictions")(targets, logits)
model = keras.Model(inputs=[inputs, targets], outputs=predictions)
model.compile(optimizer="adam") # No loss argument!
data = {
"inputs": np.random.random((3, 3)),
"targets": np.random.random((3, 10)),
}
model.fit(data)
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 89ms/step - loss: 0.6982
<keras.src.callbacks.history.History at 0x2e5cc91e0>
自动设置验证保留集
在第一个端到端的示例中,已经看到我们使用了validation_data
参数来向模型传递NumPy数组(x_val
, y_val
)的元组,以便在每个epoch结束时评估验证损失和验证指标。
这里还有另一种选择:validation_split
参数允许您自动将部分训练数据保留为验证集。该参数值表示要保留用于验证的数据的比例,因此应设置为大于0且小于1的数字。例如,validation_split=0.2
表示“使用20%的数据进行验证”,而validation_split=0.6
表示“使用60%的数据进行验证”。
验证的计算方式是在fit()
调用接收到的数组中的最后x%个样本中进行的,且这些样本在任何打乱之前就已经确定。
请注意,您只能在使用NumPy数据进行训练时使用validation_split
。
model = get_compiled_model()
model.fit(x_train, y_train, batch_size=64, validation_split=0.2, epochs=1)
625/625 ━━━━━━━━━━━━━━━━━━━━ 1s 563us/step - loss: 0.6161 - sparse_categorical_accuracy: 0.8259 - val_loss: 0.2379 - val_sparse_categorical_accuracy: 0.9302
<keras.src.callbacks.history.History at 0x2e6007610>
3.5 使用tf.data
数据集进行训练和评估
在前面的几个段落中,已经了解了如何处理损失、指标和优化器,并且看到了当数据作为NumPy数组传递时,如何在fit()
方法中使用validation_data
和validation_split
参数。
另一种选择是使用迭代器,如tf.data.Dataset
、PyTorch的DataLoader
或Keras的PyDataset
。让我们来看看tf.data.Dataset
。
tf.data
API是TensorFlow 2.0中一组用于以快速且可扩展的方式加载和预处理数据的实用工具。
无论使用的是哪个后端(JAX、PyTorch还是TensorFlow),可以使用tf.data
来训练您的Keras模型,可以直接将Dataset
实例传递给fit()
、evaluate()
和predict()
方法:
model = get_compiled_model()
# First, let's create a training Dataset instance.
# For the sake of our example, we'll use the same MNIST data as before.
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
# Shuffle and slice the dataset.
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)
# Now we get a test dataset.
test_dataset = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_dataset = test_dataset.batch(64)
# Since the dataset already takes care of batching,
# we don't pass a `batch_size` argument.
model.fit(train_dataset, epochs=3)
# You can also evaluate or predict on a dataset.
print("Evaluate")
result = model.evaluate(test_dataset)
dict(zip(model.metrics_names, result))
Epoch 1/3
782/782 ━━━━━━━━━━━━━━━━━━━━ 1s 688us/step - loss: 0.5631 - sparse_categorical_accuracy: 0.8458
Epoch 2/3
782/782 ━━━━━━━━━━━━━━━━━━━━ 0s 512us/step - loss: 0.1703 - sparse_categorical_accuracy: 0.9484
Epoch 3/3
782/782 ━━━━━━━━━━━━━━━━━━━━ 0s 506us/step - loss: 0.1187 - sparse_categorical_accuracy: 0.9640
Evaluate
157/157 ━━━━━━━━━━━━━━━━━━━━ 0s 622us/step - loss: 0.1380 - sparse_categorical_accuracy: 0.9582
{'loss': 0.11913617700338364, 'compile_metrics': 0.965399980545044}
请注意,在每个epoch结束时,Dataset会被重置,因此可以在下一个epoch中重复使用。
如果只想从这个Dataset中运行特定数量的批次进行训练,可以传递steps_per_epoch
参数,该参数指定模型在使用此Dataset进行多少个训练步骤后,再进入下一个epoch。
model = get_compiled_model()
# Prepare the training dataset
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)
# Only use the 100 batches per epoch (that's 64 * 100 samples)
model.fit(train_dataset, epochs=3, steps_per_epoch=100)
Epoch 1/3
100/100 ━━━━━━━━━━━━━━━━━━━━ 0s 508us/step - loss: 1.2000 - sparse_categorical_accuracy: 0.6822
Epoch 2/3
100/100 ━━━━━━━━━━━━━━━━━━━━ 0s 481us/step - loss: 0.4004 - sparse_categorical_accuracy: 0.8827
Epoch 3/3
100/100 ━━━━━━━━━━━━━━━━━━━━ 0s 471us/step - loss: 0.3546 - sparse_categorical_accuracy: 0.8968
<keras.src.callbacks.history.History at 0x2e64df400>
还可以将Dataset实例作为validation_data
参数传递给fit()
方法:
model = get_compiled_model()
# Prepare the training dataset
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)
# Prepare the validation dataset
val_dataset = tf.data.Dataset.from_tensor_slices((x_val, y_val))
val_dataset = val_dataset.batch(64)
model.fit(train_dataset, epochs=1, validation_data=val_dataset)
782/782 ━━━━━━━━━━━━━━━━━━━━ 1s 837us/step - loss: 0.5569 - sparse_categorical_accuracy: 0.8508 - val_loss: 0.1711 - val_sparse_categorical_accuracy: 0.9527
<keras.src.callbacks.history.History at 0x2e641e920>
在每个epoch结束时,模型将遍历验证数据集并计算验证损失和验证指标。
如果您只想从这个数据集中运行特定数量的批次进行验证,您可以传递validation_steps
参数,该参数指定模型应该使用验证数据集运行多少个验证步骤,然后再中断验证并进入下一个epoch。
model = get_compiled_model()
# Prepare the training dataset
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(64)
# Prepare the validation dataset
val_dataset = tf.data.Dataset.from_tensor_slices((x_val, y_val))
val_dataset = val_dataset.batch(64)
model.fit(
train_dataset,
epochs=1,
# Only run validation using the first 10 batches of the dataset
# using the `validation_steps` argument
validation_data=val_dataset,
validation_steps=10,
)
782/782 ━━━━━━━━━━━━━━━━━━━━ 1s 771us/step - loss: 0.5562 - sparse_categorical_accuracy: 0.8436 - val_loss: 0.3345 - val_sparse_categorical_accuracy: 0.9062
<keras.src.callbacks.history.History at 0x2f9542e00>
请注意,验证数据集在每次使用后都会被重置(。
当从Dataset对象进行训练时,不支持使用validation_split
参数(从训练数据中生成保留集),因为此功能需要能够索引数据集的样本,这在一般情况下使用Dataset API是不可行的。
未完待续…