作者: Martin Görner
创建日期: 2023-12-13
最后修改日期: 2023-12-13
描述: 在共享深度学习模型时,使用功能子类化模式进行打包。
Keras是分享最前沿深度学习模型的理想框架,构建一个预训练(或未训练)模型的库。数百万机器学习工程师流利地使用熟悉的Keras API,使您的模型能够接触到全球社区,无论他们首选的后端是什么(Jax、PyTorch或TensorFlow)。
Keras API的一个优点是它允许用户以编程方式检查或编辑模型,这是在基于预训练模型创建新架构或工作流程时所必需的功能。
在分发模型时,Keras团队建议使用功能子类化模式进行打包。以这种方式实现的模型结合了两个优点:
model = model_collection_xyz.AmazingModel()
本指南解释了如何使用功能子类化模式,并展示了其在程序模型内部检查和模型修改中的好处。它还展示了可共享Keras模型的另外两种最佳实践:配置模型以支持尽可能多的输入范围,例如各种大小的图像,以及使用字典输入以便于在更复杂模型中的清晰表示。
import keras
import tensorflow as tf # 仅用于 tf.data
print("Keras版本", keras.version())
print("Keras运行在", keras.config.backend())
Keras版本 3.0.1
Keras运行在tensorflow
让我们加载一个MNIST数据集,以便我们有一些内容进行训练。
# tf.data是一个用于组合数据流的优秀API。
# 无论您使用TensorFlow、PyTorch还是Jax后端,它都能工作,
# 只要您在数据流中使用它,而不是在模型内部。
BATCH_SIZE = 256
(x_train, train_labels), (x_test, test_labels) = keras.datasets.mnist.load_data()
train_data = tf.data.Dataset.from_tensor_slices((x_train, train_labels))
train_data = train_data.map(
lambda x, y: (tf.expand_dims(x, axis=-1), y)
) # 1通道单色
train_data = train_data.batch(BATCH_SIZE)
train_data = train_data.cache()
train_data = train_data.shuffle(5000, reshuffle_each_iteration=True)
train_data = train_data.repeat()
test_data = tf.data.Dataset.from_tensor_slices((x_test, test_labels))
test_data = test_data.map(
lambda x, y: (tf.expand_dims(x, axis=-1), y)
) # 1通道单色
test_data = test_data.batch(10000)
test_data = test_data.cache()
STEPS_PER_EPOCH = len(train_labels) // BATCH_SIZE
EPOCHS = 5
该模型被封装在一个类中,以便最终用户可以通过调用构造函数MnistModel()
而不是调用工厂函数来正常实例化它。
class MnistModel(keras.Model):
def __init__(self, **kwargs):
# Keras函数模型的定义。这也可以使用Sequential,
# Sequential只是简单函数模型的语法糖。
# 1通道单色输入
inputs = keras.layers.Input(shape=(None, None, 1), dtype="uint8")
# 像素格式从uint8转换为float32
y = keras.layers.Rescaling(1 / 255.0)(inputs)
# 3个卷积层
y = keras.layers.Conv2D(
filters=16, kernel_size=3, padding="same", activation="relu"
)(y)
y = keras.layers.Conv2D(
filters=32, kernel_size=6, padding="same", activation="relu", strides=2
)(y)
y = keras.layers.Conv2D(
filters=48, kernel_size=6, padding="same", activation="relu", strides=2
)(y)
# 2个全连接层
y = keras.layers.GlobalAveragePooling2D()(y)
y = keras.layers.Dense(48, activation="relu")(y)
y = keras.layers.Dropout(0.4)(y)
outputs = keras.layers.Dense(
10, activation="softmax", name="classification_head" # 10个类别
)(y)
# 通过调用keras.Model(inputs, outputs)创建Keras函数模型
super().__init__(inputs=inputs, outputs=outputs, **kwargs)
让我们实例化并训练这个模型。
model = MnistModel()
model.compile(
optimizer="adam",
loss="sparse_categorical_crossentropy",
metrics=["sparse_categorical_accuracy"],
)
history = model.fit(
train_data,
steps_per_epoch=STEPS_PER_EPOCH,
epochs=EPOCHS,
validation_data=test_data,
)
第 1 轮/共 5 轮
234/234 ━━━━━━━━━━━━━━━━━━━━ 9s 33ms/步 - loss: 1.8916 - sparse_categorical_accuracy: 0.2933 - val_loss: 0.4278 - val_sparse_categorical_accuracy: 0.8864
第 2 轮/共 5 轮
234/234 ━━━━━━━━━━━━━━━━━━━━ 7s 31ms/步 - loss: 0.5723 - sparse_categorical_accuracy: 0.8201 - val_loss: 0.2703 - val_sparse_categorical_accuracy: 0.9248
第 3 轮/共 5 轮
234/234 ━━━━━━━━━━━━━━━━━━━━ 7s 31ms/步 - loss: 0.4063 - sparse_categorical_accuracy: 0.8772 - val_loss: 0.2010 - val_sparse_categorical_accuracy: 0.9400
第 4 轮/共 5 轮
234/234 ━━━━━━━━━━━━━━━━━━━━ 7s 31ms/步 - loss: 0.3391 - sparse_categorical_accuracy: 0.8996 - val_loss: 0.1869 - val_sparse_categorical_accuracy: 0.9427
第 5 轮/共 5 轮
234/234 ━━━━━━━━━━━━━━━━━━━━ 7s 31ms/步 - loss: 0.2989 - sparse_categorical_accuracy: 0.9120 - val_loss: 0.1513 - val_sparse_categorical_accuracy: 0.9557
请注意,在上述模型定义中,输入的维度是未定义的:Input(shape=(None, None, 1)
这允许模型接受任何大小的图像作为输入。然而,这仅在宽松定义的形状可以在所有层中传播并仍然确定所有权重的大小时才有效。
model = MnistModel()
model = ModelXYZ(input_size=...)
Keras 为每个模型维护一个可编程访问的层图。它可以用于自省,并通过 model.layers
或 layer.layers
属性访问。实用函数 model.summary()
也在内部使用此机制。
model = MnistModel()
# 模型摘要有效
model.summary()
# 递归遍历层图也有效
def walk_layers(layer):
if hasattr(layer, "layers"):
for layer in layer.layers:
walk_layers(layer)
else:
print(layer.name)
print("\n遍历模型层:\n")
walk_layers(model)
模型: "mnist_model_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓ ┃ 层 (类型) ┃ 输出形状 ┃ 参数 # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩ │ input_layer_1 (输入层) │ (未定义, 未定义, 未定义, 1) │ 0 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ rescaling_1 (重缩放) │ (未定义, 未定义, 未定义, 1) │ 0 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ conv2d_3 (卷积层) │ (未定义, 未定义, 未定义, 16) │ 160 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ conv2d_4 (Conv2D) │ (None, None, None, 32) │ 18,464 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ conv2d_5 (Conv2D) │ (None, None, None, 48) │ 55,344 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ global_average_pooling2d_1 │ (None, 48) │ 0 │ │ (GlobalAveragePooling2D) │ │ │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ dense_1 (Dense) │ (None, 48) │ 2,352 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ dropout_1 (Dropout) │ (None, 48) │ 0 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ classification_head (Dense) │ (None, 10) │ 490 │ └─────────────────────────────────┴───────────────────────────┴────────────┘
总参数: 76,810 (300.04 KB)
可训练参数: 76,810 (300.04 KB)
非可训练参数: 0 (0.00 B)
遍历模型层:
input_layer_1
rescaling_1
conv2d_3
conv2d_4
conv2d_5
global_average_pooling2d_1
dense_1
dropout_1
classification_head
最终用户可能希望从你的库中实例化模型,但在使用前进行修改。 功能模型具有可程序访问的层图。可以通过切片和拼接图形来进行编辑,并创建一个新的功能模型。
另一种选择是分叉模型代码并进行修改,但这迫使用户无限期地维护他们的分叉。
示例:实例化模型,但将分类头更改为进行二元分类,“0”或“不是0”,而不是最初的10类数字分类。
model = MnistModel()
input = model.input
# 在分类头之前截取
y = model.get_layer("classification_head").input
# 添加新的分类头
output = keras.layers.Dense(
1, # 二分类的单一类别
activation="sigmoid",
name="binary_classification_head",
)(y)
# 创建一个新的函数模型
binary_model = keras.Model(input, output)
binary_model.summary()
模型: "functional_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓ ┃ 层 (类型) ┃ 输出形状 ┃ 参数 # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩ │ input_layer_2 (输入层) │ (无, 无, 无, 1) │ 0 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ rescaling_2 (重缩放) │ (无, 无, 无, 1) │ 0 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ conv2d_6 (卷积2D) │ (无, 无, 无, 16) │ 160 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ conv2d_7 (卷积2D) │ (无, 无, 无, 32) │ 18,464 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ conv2d_8 (卷积2D) │ (无, 无, 无, 48) │ 55,344 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ global_average_pooling2d_2 │ (无, 48) │ 0 │ │ (全局平均池化2D) │ │ │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ dense_2 (全连接层) │ (无, 48) │ 2,352 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ dropout_2 (丢弃层) │ (无, 48) │ 0 │ ├─────────────────────────────────┼───────────────────────────┼────────────┤ │ binary_classification_head │ (无, 1) │ 49 │ │ (Dense) │ │ │ └─────────────────────────────────┴───────────────────────────┴────────────┘
总参数: 76,369 (298.32 KB)
可训练参数: 76,369 (298.32 KB)
不可训练参数: 0 (0.00 B)
我们现在可以将新模型训练为二分类器。
# 新数据集,标签为0 / 1(1 = 数字 '0',0 = 所有其他数字)
bin_train_data = train_data.map(
lambda x, y: (x, tf.cast(tf.math.equal(y, tf.zeros_like(y)), dtype=tf.uint8))
)
bin_test_data = test_data.map(
lambda x, y: (x, tf.cast(tf.math.equal(y, tf.zeros_like(y)), dtype=tf.uint8))
)
# 对于二分类的合适损失和评估指标
binary_model.compile(
optimizer="adam", loss="binary_crossentropy", metrics=["binary_accuracy"]
)
history = binary_model.fit(
bin_train_data,
steps_per_epoch=STEPS_PER_EPOCH,
epochs=EPOCHS,
validation_data=bin_test_data,
)
第 1 轮/5
234/234 ━━━━━━━━━━━━━━━━━━━━ 9s 33ms/步 - binary_accuracy: 0.8926 - loss: 0.3635 - val_binary_accuracy: 0.9235 - val_loss: 0.1777
第 2 轮/5
234/234 ━━━━━━━━━━━━━━━━━━━━ 7s 31ms/步 - binary_accuracy: 0.9411 - loss: 0.1620 - val_binary_accuracy: 0.9766 - val_loss: 0.0748
第 3 轮/5
234/234 ━━━━━━━━━━━━━━━━━━━━ 7s 31ms/步 - binary_accuracy: 0.9751 - loss: 0.0794 - val_binary_accuracy: 0.9884 - val_loss: 0.0414
第 4 轮/5
234/234 ━━━━━━━━━━━━━━━━━━━━ 7s 31ms/步 - binary_accuracy: 0.9848 - loss: 0.0480 - val_binary_accuracy: 0.9915 - val_loss: 0.0292
第 5 轮/5
234/234 ━━━━━━━━━━━━━━━━━━━━ 7s 31ms/步 - binary_accuracy: 0.9910 - loss: 0.0326 - val_binary_accuracy: 0.9917 - val_loss: 0.0286
在具有多个输入的更复杂模型中,将输入结构化为字典可以提高可读性和可用性。使用功能模型可以轻松实现这一点:
class MnistDictModel(keras.Model):
def __init__(self, **kwargs):
#
# 输入为字典
#
inputs = {
"image": keras.layers.Input(
shape=(None, None, 1), # 1通道单色
dtype="uint8",
name="image",
)
}
# 像素格式从uint8转换为float32
y = keras.layers.Rescaling(1 / 255.0)(inputs["image"])
# 3个卷积层
y = keras.layers.Conv2D(
filters=16, kernel_size=3, padding="same", activation="relu"
)(y)
y = keras.layers.Conv2D(
filters=32, kernel_size=6, padding="same", activation="relu", strides=2
)(y)
y = keras.layers.Conv2D(
filters=48, kernel_size=6, padding="same", activation="relu", strides=2
)(y)
# 2个全连接层
y = keras.layers.GlobalAveragePooling2D()(y)
y = keras.layers.Dense(48, activation="relu")(y)
y = keras.layers.Dropout(0.4)(y)
outputs = keras.layers.Dense(
10, activation="softmax", name="classification_head" # 10个类别
)(y)
# 通过调用 keras.Model(inputs, outputs) 创建 Keras 功能模型
super().__init__(inputs=inputs, outputs=outputs, **kwargs)
我们现在可以在结构化为字典的输入上训练模型。
model = MnistDictModel()
# 将数据集重新格式化为字典
dict_train_data = train_data.map(lambda x, y: ({"image": x}, y))
dict_test_data = test_data.map(lambda x, y: ({"image": x}, y))
model.compile(
optimizer="adam",
loss="sparse_categorical_crossentropy",
metrics=["sparse_categorical_accuracy"],
)
history = model.fit(
dict_train_data,
steps_per_epoch=STEPS_PER_EPOCH,
epochs=EPOCHS,
validation_data=dict_test_data,
)
Epoch 1/5
234/234 ━━━━━━━━━━━━━━━━━━━━ 9s 34ms/step - loss: 1.8702 - sparse_categorical_accuracy: 0.3175 - val_loss: 0.4505 - val_sparse_categorical_accuracy: 0.8779
Epoch 2/5
234/234 ━━━━━━━━━━━━━━━━━━━━ 8s 32ms/step - loss: 0.5991 - sparse_categorical_accuracy: 0.8131 - val_loss: 0.2582 - val_sparse_categorical_accuracy: 0.9245
Epoch 3/5
234/234 ━━━━━━━━━━━━━━━━━━━━ 7s 32ms/step - loss: 0.3916 - sparse_categorical_accuracy: 0.8846 - val_loss: 0.1938 - val_sparse_categorical_accuracy: 0.9422
Epoch 4/5
234/234 ━━━━━━━━━━━━━━━━━━━━ 8s 33ms/step - loss: 0.3109 - sparse_categorical_accuracy: 0.9089 - val_loss: 0.1450 - val_sparse_categorical_accuracy: 0.9566
Epoch 5/5
234/234 ━━━━━━━━━━━━━━━━━━━━ 8s 32ms/step - loss: 0.2775 - sparse_categorical_accuracy: 0.9197 - val_loss: 0.1316 - val_sparse_categorical_accuracy: 0.9608