作者: fchollet
创建日期 2019/03/01
最后修改日期 2023/06/25
描述:函数式 API 的完整指南。
import numpy as np
import keras
from keras import layers
from keras import ops
Keras 的函数式 API 是一种创建比 keras.Sequential API 更灵活的模型的方式。函数式 API 可以处理具有非线性拓扑、共享层甚至多输入或多输出的模型。
核心思想是,深度学习模型通常是层的有向无环图 (DAG)。因此,函数式 API 是一种构建层图的方式。
考虑以下模型
(input: 784-dimensional vectors)
↧
[Dense (64 units, relu activation)]
↧
[Dense (64 units, relu activation)]
↧
[Dense (10 units, softmax activation)]
↧
(output: logits of a probability distribution over 10 classes)
这是一个包含三个基本层的图。要使用函数式 API 构建此模型,请先创建一个输入节点
inputs = keras.Input(shape=(784,))
输入数据的形状设置为一个 784 维的向量。批次大小始终被省略,因为只指定了每个样本的形状。
例如,如果您有一个形状为 (32, 32, 3) 的图像输入,您将使用
# Just for demonstration purposes.
img_inputs = keras.Input(shape=(32, 32, 3))
返回的 inputs 包含您馈送到模型的输入数据的形状和 dtype 信息。这是形状
inputs.shape
(None, 784)
这是 dtype
inputs.dtype
'float32'
通过在此 inputs 对象上调用一个层来创建一个新的层图节点
dense = layers.Dense(64, activation="relu")
x = dense(inputs)
“层调用”操作就像是从“inputs”绘制一条箭头到您创建的这个层。您将输入“传递”给 dense 层,然后得到 x 作为输出。
让我们在层图中添加几个层
x = layers.Dense(64, activation="relu")(x)
outputs = layers.Dense(10)(x)
此时,您可以通过在层图中指定其输入和输出来创建 Model
model = keras.Model(inputs=inputs, outputs=outputs, name="mnist_model")
让我们看看模型摘要的样子
model.summary()
Model: "mnist_model"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ input_layer (InputLayer) │ (None, 784) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense (Dense) │ (None, 64) │ 50,240 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_1 (Dense) │ (None, 64) │ 4,160 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ dense_2 (Dense) │ (None, 10) │ 650 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 55,050 (215.04 KB)
Trainable params: 55,050 (215.04 KB)
Non-trainable params: 0 (0.00 B)
您也可以将模型绘制成图
keras.utils.plot_model(model, "my_first_model.png")

并可以选择性地显示图中每个层的输入和输出形状
keras.utils.plot_model(model, "my_first_model_with_shape_info.png", show_shapes=True)

此图和代码几乎相同。在代码版本中,连接箭头被调用操作替换了。
“层图”是深度学习模型的直观的心理图像,而函数式 API 是创建与此非常相似的模型的一种方式。
使用函数式 API 构建的模型,其训练、评估和推理方式与 Sequential 模型完全相同。
Model 类提供了内置的训练循环(fit() 方法)和内置的评估循环(evaluate() 方法)。请注意,您可以轻松地自定义这些循环来实现您自己的训练例程。另请参阅有关自定义 fit() 中行为的指南
在这里,加载 MNIST 图像数据,将其重塑为向量,在数据上拟合模型(同时监控验证集上的性能),然后对测试数据评估模型
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
x_train = x_train.reshape(60000, 784).astype("float32") / 255
x_test = x_test.reshape(10000, 784).astype("float32") / 255
model.compile(
loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True),
optimizer=keras.optimizers.RMSprop(),
metrics=["accuracy"],
)
history = model.fit(x_train, y_train, batch_size=64, epochs=2, validation_split=0.2)
test_scores = model.evaluate(x_test, y_test, verbose=2)
print("Test loss:", test_scores[0])
print("Test accuracy:", test_scores[1])
Epoch 1/2
750/750 ━━━━━━━━━━━━━━━━━━━━ 1s 863us/step - accuracy: 0.8425 - loss: 0.5733 - val_accuracy: 0.9496 - val_loss: 0.1711
Epoch 2/2
750/750 ━━━━━━━━━━━━━━━━━━━━ 1s 859us/step - accuracy: 0.9509 - loss: 0.1641 - val_accuracy: 0.9578 - val_loss: 0.1396
313/313 - 0s - 341us/step - accuracy: 0.9613 - loss: 0.1288
Test loss: 0.12876172363758087
Test accuracy: 0.9613000154495239
欲进一步阅读,请参阅 训练和评估 指南。
使用函数式 API 构建的模型,其保存和序列化方式与 Sequential 模型相同。保存函数式模型的标准方法是调用 model.save() 将整个模型保存为单个文件。之后,您可以从该文件重新创建相同的模型,即使构建该模型的代码不再可用。
此保存的文件包括: - 模型架构 - 模型权重值(在训练期间学习的) - 模型训练配置(如果有)(如传递给 compile()) - 优化器及其状态(如果有)(以便从中断处继续训练)
model.save("my_model.keras")
del model
# Recreate the exact same model purely from the file:
model = keras.models.load_model("my_model.keras")
有关详细信息,请阅读模型 序列化和保存 指南。
在函数式 API 中,模型是通过在层图中指定其输入和输出来创建的。这意味着一个层图可以用来生成多个模型。
在下面的示例中,您使用相同的层堆栈来实例化两个模型:一个 encoder 模型,将图像输入转换为 16 维向量;以及一个端到端的 autoencoder 模型用于训练。
encoder_input = keras.Input(shape=(28, 28, 1), name="img")
x = layers.Conv2D(16, 3, activation="relu")(encoder_input)
x = layers.Conv2D(32, 3, activation="relu")(x)
x = layers.MaxPooling2D(3)(x)
x = layers.Conv2D(32, 3, activation="relu")(x)
x = layers.Conv2D(16, 3, activation="relu")(x)
encoder_output = layers.GlobalMaxPooling2D()(x)
encoder = keras.Model(encoder_input, encoder_output, name="encoder")
encoder.summary()
x = layers.Reshape((4, 4, 1))(encoder_output)
x = layers.Conv2DTranspose(16, 3, activation="relu")(x)
x = layers.Conv2DTranspose(32, 3, activation="relu")(x)
x = layers.UpSampling2D(3)(x)
x = layers.Conv2DTranspose(16, 3, activation="relu")(x)
decoder_output = layers.Conv2DTranspose(1, 3, activation="relu")(x)
autoencoder = keras.Model(encoder_input, decoder_output, name="autoencoder")
autoencoder.summary()
Model: "encoder"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ img (InputLayer) │ (None, 28, 28, 1) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d (Conv2D) │ (None, 26, 26, 16) │ 160 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_1 (Conv2D) │ (None, 24, 24, 32) │ 4,640 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ max_pooling2d (MaxPooling2D) │ (None, 8, 8, 32) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_2 (Conv2D) │ (None, 6, 6, 32) │ 9,248 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_3 (Conv2D) │ (None, 4, 4, 16) │ 4,624 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ global_max_pooling2d │ (None, 16) │ 0 │ │ (GlobalMaxPooling2D) │ │ │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 18,672 (72.94 KB)
Trainable params: 18,672 (72.94 KB)
Non-trainable params: 0 (0.00 B)
Model: "autoencoder"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ img (InputLayer) │ (None, 28, 28, 1) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d (Conv2D) │ (None, 26, 26, 16) │ 160 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_1 (Conv2D) │ (None, 24, 24, 32) │ 4,640 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ max_pooling2d (MaxPooling2D) │ (None, 8, 8, 32) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_2 (Conv2D) │ (None, 6, 6, 32) │ 9,248 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_3 (Conv2D) │ (None, 4, 4, 16) │ 4,624 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ global_max_pooling2d │ (None, 16) │ 0 │ │ (GlobalMaxPooling2D) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ reshape (Reshape) │ (None, 4, 4, 1) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_transpose │ (None, 6, 6, 16) │ 160 │ │ (Conv2DTranspose) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_transpose_1 │ (None, 8, 8, 32) │ 4,640 │ │ (Conv2DTranspose) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ up_sampling2d (UpSampling2D) │ (None, 24, 24, 32) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_transpose_2 │ (None, 26, 26, 16) │ 4,624 │ │ (Conv2DTranspose) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_transpose_3 │ (None, 28, 28, 1) │ 145 │ │ (Conv2DTranspose) │ │ │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 28,241 (110.32 KB)
Trainable params: 28,241 (110.32 KB)
Non-trainable params: 0 (0.00 B)
这里,解码架构与编码架构严格对称,因此输出形状与输入形状 (28, 28, 1) 相同。
Conv2D 层的反向是 Conv2DTranspose 层,MaxPooling2D 层的反向是 UpSampling2D 层。
您可以将任何模型视为一个层,通过在其 Input 或另一个层的输出上调用它来处理。通过调用模型,您不仅重用了模型的架构,还重用了其权重。
为了在实践中看到这一点,这里有一个自动编码器示例的不同版本,它创建了一个编码器模型、一个解码器模型,并将它们链接在两次调用中以获得自动编码器模型
encoder_input = keras.Input(shape=(28, 28, 1), name="original_img")
x = layers.Conv2D(16, 3, activation="relu")(encoder_input)
x = layers.Conv2D(32, 3, activation="relu")(x)
x = layers.MaxPooling2D(3)(x)
x = layers.Conv2D(32, 3, activation="relu")(x)
x = layers.Conv2D(16, 3, activation="relu")(x)
encoder_output = layers.GlobalMaxPooling2D()(x)
encoder = keras.Model(encoder_input, encoder_output, name="encoder")
encoder.summary()
decoder_input = keras.Input(shape=(16,), name="encoded_img")
x = layers.Reshape((4, 4, 1))(decoder_input)
x = layers.Conv2DTranspose(16, 3, activation="relu")(x)
x = layers.Conv2DTranspose(32, 3, activation="relu")(x)
x = layers.UpSampling2D(3)(x)
x = layers.Conv2DTranspose(16, 3, activation="relu")(x)
decoder_output = layers.Conv2DTranspose(1, 3, activation="relu")(x)
decoder = keras.Model(decoder_input, decoder_output, name="decoder")
decoder.summary()
autoencoder_input = keras.Input(shape=(28, 28, 1), name="img")
encoded_img = encoder(autoencoder_input)
decoded_img = decoder(encoded_img)
autoencoder = keras.Model(autoencoder_input, decoded_img, name="autoencoder")
autoencoder.summary()
Model: "encoder"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ original_img (InputLayer) │ (None, 28, 28, 1) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_4 (Conv2D) │ (None, 26, 26, 16) │ 160 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_5 (Conv2D) │ (None, 24, 24, 32) │ 4,640 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ max_pooling2d_1 (MaxPooling2D) │ (None, 8, 8, 32) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_6 (Conv2D) │ (None, 6, 6, 32) │ 9,248 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_7 (Conv2D) │ (None, 4, 4, 16) │ 4,624 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ global_max_pooling2d_1 │ (None, 16) │ 0 │ │ (GlobalMaxPooling2D) │ │ │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 18,672 (72.94 KB)
Trainable params: 18,672 (72.94 KB)
Non-trainable params: 0 (0.00 B)
Model: "decoder"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ encoded_img (InputLayer) │ (None, 16) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ reshape_1 (Reshape) │ (None, 4, 4, 1) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_transpose_4 │ (None, 6, 6, 16) │ 160 │ │ (Conv2DTranspose) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_transpose_5 │ (None, 8, 8, 32) │ 4,640 │ │ (Conv2DTranspose) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ up_sampling2d_1 (UpSampling2D) │ (None, 24, 24, 32) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_transpose_6 │ (None, 26, 26, 16) │ 4,624 │ │ (Conv2DTranspose) │ │ │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ conv2d_transpose_7 │ (None, 28, 28, 1) │ 145 │ │ (Conv2DTranspose) │ │ │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 9,569 (37.38 KB)
Trainable params: 9,569 (37.38 KB)
Non-trainable params: 0 (0.00 B)
Model: "autoencoder"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ img (InputLayer) │ (None, 28, 28, 1) │ 0 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ encoder (Functional) │ (None, 16) │ 18,672 │ ├─────────────────────────────────┼────────────────────────┼───────────────┤ │ decoder (Functional) │ (None, 28, 28, 1) │ 9,569 │ └─────────────────────────────────┴────────────────────────┴───────────────┘
Total params: 28,241 (110.32 KB)
Trainable params: 28,241 (110.32 KB)
Non-trainable params: 0 (0.00 B)
如您所见,模型可以嵌套:一个模型可以包含子模型(因为一个模型就像一个层)。模型嵌套的一个常见用例是集成。例如,如何将一组模型集成到一个平均预测的单个模型中
def get_model():
inputs = keras.Input(shape=(128,))
outputs = layers.Dense(1)(inputs)
return keras.Model(inputs, outputs)
model1 = get_model()
model2 = get_model()
model3 = get_model()
inputs = keras.Input(shape=(128,))
y1 = model1(inputs)
y2 = model2(inputs)
y3 = model3(inputs)
outputs = layers.average([y1, y2, y3])
ensemble_model = keras.Model(inputs=inputs, outputs=outputs)
函数式 API 可以轻松操作多个输入和输出。这无法使用 Sequential API 来处理。
例如,如果您正在构建一个系统,用于按优先级对客户问题票据进行排序并将它们路由到正确的部门,那么模型将有三个输入
该模型将有两个输出
您可以用几行函数式 API 代码构建此模型
num_tags = 12 # Number of unique issue tags
num_words = 10000 # Size of vocabulary obtained when preprocessing text data
num_departments = 4 # Number of departments for predictions
title_input = keras.Input(
shape=(None,), name="title"
) # Variable-length sequence of ints
body_input = keras.Input(shape=(None,), name="body") # Variable-length sequence of ints
tags_input = keras.Input(
shape=(num_tags,), name="tags"
) # Binary vectors of size `num_tags`
# Embed each word in the title into a 64-dimensional vector
title_features = layers.Embedding(num_words, 64)(title_input)
# Embed each word in the text into a 64-dimensional vector
body_features = layers.Embedding(num_words, 64)(body_input)
# Reduce sequence of embedded words in the title into a single 128-dimensional vector
title_features = layers.LSTM(128)(title_features)
# Reduce sequence of embedded words in the body into a single 32-dimensional vector
body_features = layers.LSTM(32)(body_features)
# Merge all available features into a single large vector via concatenation
x = layers.concatenate([title_features, body_features, tags_input])
# Stick a logistic regression for priority prediction on top of the features
priority_pred = layers.Dense(1, name="priority")(x)
# Stick a department classifier on top of the features
department_pred = layers.Dense(num_departments, name="department")(x)
# Instantiate an end-to-end model predicting both priority and department
model = keras.Model(
inputs=[title_input, body_input, tags_input],
outputs={"priority": priority_pred, "department": department_pred},
)
现在绘制模型
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.BinaryCrossentropy(from_logits=True),
keras.losses.CategoricalCrossentropy(from_logits=True),
],
loss_weights=[1.0, 0.2],
)
由于输出层具有不同的名称,您也可以使用相应的层名称指定损失和损失权重
model.compile(
optimizer=keras.optimizers.RMSprop(1e-3),
loss={
"priority": keras.losses.BinaryCrossentropy(from_logits=True),
"department": keras.losses.CategoricalCrossentropy(from_logits=True),
},
loss_weights={"priority": 1.0, "department": 0.2},
)
通过传递 NumPy 数组的输入和目标列表来训练模型
# Dummy input data
title_data = np.random.randint(num_words, size=(1280, 12))
body_data = np.random.randint(num_words, size=(1280, 100))
tags_data = np.random.randint(2, size=(1280, num_tags)).astype("float32")
# Dummy target data
priority_targets = np.random.random(size=(1280, 1))
dept_targets = np.random.randint(2, size=(1280, num_departments))
model.fit(
{"title": title_data, "body": body_data, "tags": tags_data},
{"priority": priority_targets, "department": dept_targets},
epochs=2,
batch_size=32,
)
Epoch 1/2
40/40 ━━━━━━━━━━━━━━━━━━━━ 3s 57ms/step - loss: 1108.3792
Epoch 2/2
40/40 ━━━━━━━━━━━━━━━━━━━━ 2s 54ms/step - loss: 621.3049
<keras.src.callbacks.history.History at 0x34afc3d90>
调用 fit 时,如果使用 Dataset 对象,它应该会生成一个列表元组,如 ([title_data, body_data, tags_data], [priority_targets, dept_targets]),或者一个字典元组,如 ({'title': title_data, 'body': body_data, 'tags': tags_data}, {'priority': priority_targets, 'department': dept_targets})。
有关更详细的解释,请参阅 训练和评估 指南。
除了多输入和多输出的模型之外,函数式 API 还可以轻松操作非线性连接拓扑——这些是层之间不是顺序连接的模型,而 Sequential API 无法处理。
一个常见的用例是残差连接。让我们为 CIFAR10 构建一个玩具 ResNet 模型来演示这一点
inputs = keras.Input(shape=(32, 32, 3), name="img")
x = layers.Conv2D(32, 3, activation="relu")(inputs)
x = layers.Conv2D(64, 3, activation="relu")(x)
block_1_output = layers.MaxPooling2D(3)(x)
x = layers.Conv2D(64, 3, activation="relu", padding="same")(block_1_output)
x = layers.Conv2D(64, 3, activation="relu", padding="same")(x)
block_2_output = layers.add([x, block_1_output])
x = layers.Conv2D(64, 3, activation="relu", padding="same")(block_2_output)
x = layers.Conv2D(64, 3, activation="relu", padding="same")(x)
block_3_output = layers.add([x, block_2_output])
x = layers.Conv2D(64, 3, activation="relu")(block_3_output)
x = layers.GlobalAveragePooling2D()(x)
x = layers.Dense(256, activation="relu")(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(10)(x)
model = keras.Model(inputs, outputs, name="toy_resnet")
model.summary()
Model: "toy_resnet"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓ ┃ Layer (type) ┃ Output Shape ┃ Param # ┃ Connected to ┃ ┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩ │ img (InputLayer) │ (None, 32, 32, 3) │ 0 │ - │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv2d_8 (Conv2D) │ (None, 30, 30, │ 896 │ img[0][0] │ │ │ 32) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv2d_9 (Conv2D) │ (None, 28, 28, │ 18,496 │ conv2d_8[0][0] │ │ │ 64) │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ max_pooling2d_2 │ (None, 9, 9, 64) │ 0 │ conv2d_9[0][0] │ │ (MaxPooling2D) │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv2d_10 (Conv2D) │ (None, 9, 9, 64) │ 36,928 │ max_pooling2d_2[… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv2d_11 (Conv2D) │ (None, 9, 9, 64) │ 36,928 │ conv2d_10[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add (Add) │ (None, 9, 9, 64) │ 0 │ conv2d_11[0][0], │ │ │ │ │ max_pooling2d_2[… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv2d_12 (Conv2D) │ (None, 9, 9, 64) │ 36,928 │ add[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv2d_13 (Conv2D) │ (None, 9, 9, 64) │ 36,928 │ conv2d_12[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ add_1 (Add) │ (None, 9, 9, 64) │ 0 │ conv2d_13[0][0], │ │ │ │ │ add[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ conv2d_14 (Conv2D) │ (None, 7, 7, 64) │ 36,928 │ add_1[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ global_average_poo… │ (None, 64) │ 0 │ conv2d_14[0][0] │ │ (GlobalAveragePool… │ │ │ │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense_6 (Dense) │ (None, 256) │ 16,640 │ global_average_p… │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dropout (Dropout) │ (None, 256) │ 0 │ dense_6[0][0] │ ├─────────────────────┼───────────────────┼────────────┼───────────────────┤ │ dense_7 (Dense) │ (None, 10) │ 2,570 │ dropout[0][0] │ └─────────────────────┴───────────────────┴────────────┴───────────────────┘
Total params: 223,242 (872.04 KB)
Trainable params: 223,242 (872.04 KB)
Non-trainable params: 0 (0.00 B)
绘制模型
keras.utils.plot_model(model, "mini_resnet.png", show_shapes=True)

现在训练模型
(x_train, y_train), (x_test, y_test) = keras.datasets.cifar10.load_data()
x_train = x_train.astype("float32") / 255.0
x_test = x_test.astype("float32") / 255.0
y_train = keras.utils.to_categorical(y_train, 10)
y_test = keras.utils.to_categorical(y_test, 10)
model.compile(
optimizer=keras.optimizers.RMSprop(1e-3),
loss=keras.losses.CategoricalCrossentropy(from_logits=True),
metrics=["acc"],
)
# We restrict the data to the first 1000 samples so as to limit execution time
# on Colab. Try to train on the entire dataset until convergence!
model.fit(
x_train[:1000],
y_train[:1000],
batch_size=64,
epochs=1,
validation_split=0.2,
)
13/13 ━━━━━━━━━━━━━━━━━━━━ 1s 60ms/step - acc: 0.1096 - loss: 2.3053 - val_acc: 0.1150 - val_loss: 2.2973
<keras.src.callbacks.history.History at 0x1758bed40>
函数式 API 的另一个好用途是使用共享层的模型。共享层是在同一模型中被多次重用的层实例——它们学习对应于层图中多个路径的特征。
共享层通常用于对来自相似空间(例如,具有相似词汇的两个不同文本片段)的输入进行编码。它们能够跨这些不同的输入共享信息,并且可以在更少的数据上训练这样的模型。如果在其中一个输入中看到了某个单词,这将有利于通过共享层的所有输入的处理。
要在函数式 API 中共享层,请多次调用相同的层实例。例如,这里有一个跨两个不同文本输入的共享 Embedding 层
# Embedding for 1000 unique words mapped to 128-dimensional vectors
shared_embedding = layers.Embedding(1000, 128)
# Variable-length sequence of integers
text_input_a = keras.Input(shape=(None,), dtype="int32")
# Variable-length sequence of integers
text_input_b = keras.Input(shape=(None,), dtype="int32")
# Reuse the same layer to encode both inputs
encoded_input_a = shared_embedding(text_input_a)
encoded_input_b = shared_embedding(text_input_b)
因为您正在操作的层图是一个静态数据结构,所以它可以被访问和检查。这就是您能够将函数式模型绘制成图像的原因。
这也意味着您可以访问中间层的激活(图中的“节点”)并在其他地方重用它们——这对于特征提取等非常有用。
让我们看一个例子。这是一个在 ImageNet 上预训练了权重的 VGG19 模型
vgg19 = keras.applications.VGG19()
这些是模型的中间激活,通过查询图数据结构获得
features_list = [layer.output for layer in vgg19.layers]
使用这些特征创建一个新的特征提取模型,该模型返回中间层激活的值
feat_extraction_model = keras.Model(inputs=vgg19.input, outputs=features_list)
img = np.random.random((1, 224, 224, 3)).astype("float32")
extracted_features = feat_extraction_model(img)
这对于神经风格迁移等任务非常有用。
keras 包含广泛的内置层,例如
Conv1D, Conv2D, Conv3D, Conv2DTransposeMaxPooling1D, MaxPooling2D, MaxPooling3D, AveragePooling1DGRU, LSTM, ConvLSTM2DBatchNormalization, Dropout, Embedding 等。但是,如果您找不到所需的内容,则可以轻松地创建自己的层来扩展 API。所有层都继承自 Layer 类并实现
call 方法,该方法指定层执行的计算。build 方法,该方法创建层的权重(这只是一个风格约定,因为您可以在 __init__ 中创建权重)。要了解有关从头开始创建层的更多信息,请阅读 自定义层和模型 指南。
以下是 keras.layers.Dense 的基本实现
class CustomDense(layers.Layer):
def __init__(self, units=32):
super().__init__()
self.units = units
def build(self, input_shape):
self.w = self.add_weight(
shape=(input_shape[-1], self.units),
initializer="random_normal",
trainable=True,
)
self.b = self.add_weight(
shape=(self.units,), initializer="random_normal", trainable=True
)
def call(self, inputs):
return ops.matmul(inputs, self.w) + self.b
inputs = keras.Input((4,))
outputs = CustomDense(10)(inputs)
model = keras.Model(inputs, outputs)
为了在您的自定义层中支持序列化,请定义一个 get_config() 方法,该方法返回层实例的构造函数参数
class CustomDense(layers.Layer):
def __init__(self, units=32):
super().__init__()
self.units = units
def build(self, input_shape):
self.w = self.add_weight(
shape=(input_shape[-1], self.units),
initializer="random_normal",
trainable=True,
)
self.b = self.add_weight(
shape=(self.units,), initializer="random_normal", trainable=True
)
def call(self, inputs):
return ops.matmul(inputs, self.w) + self.b
def get_config(self):
return {"units": self.units}
inputs = keras.Input((4,))
outputs = CustomDense(10)(inputs)
model = keras.Model(inputs, outputs)
config = model.get_config()
new_model = keras.Model.from_config(config, custom_objects={"CustomDense": CustomDense})
可选地,实现类方法 from_config(cls, config),该方法在给定配置字典重新创建层实例时使用。from_config 的默认实现是
def from_config(cls, config):
return cls(**config)
您应该使用 Keras 函数式 API 来创建新模型,还是直接子类化 Model 类?总的来说,函数式 API 更高级、更简单、更安全,并且具有子类模型不支持的许多功能。
但是,当构建不易表示为层有向无环图的模型时,模型子类化提供了更大的灵活性。例如,您无法使用函数式 API 实现 Tree-RNN,而必须直接子类化 Model。
有关函数式 API 和模型子类化之间差异的深入了解,请阅读 TensorFlow 2.0 中的符号 API 和命令式 API 是什么?。
以下属性也适用于序贯模型(它们也是数据结构),但不适用于子类模型(它们是 Python 字节码,而不是数据结构)。
没有 super().__init__(...),没有 def call(self, ...): 等。
比较
inputs = keras.Input(shape=(32,))
x = layers.Dense(64, activation='relu')(inputs)
outputs = layers.Dense(10)(x)
mlp = keras.Model(inputs, outputs)
与子类化版本
class MLP(keras.Model):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.dense_1 = layers.Dense(64, activation='relu')
self.dense_2 = layers.Dense(10)
def call(self, inputs):
x = self.dense_1(inputs)
return self.dense_2(x)
# Instantiate the model.
mlp = MLP()
# Necessary to create the model's state.
# The model doesn't have a state until it's called at least once.
_ = mlp(ops.zeros((1, 32)))
在函数式 API 中,输入规范(形状和 dtype)会提前创建(使用 Input)。每次调用层时,该层都会检查传递给它的规范是否符合其假设,如果不符合,它会引发有用的错误消息。
这保证了您可以使用函数式 API 构建的任何模型都将运行。除收敛相关的调试外,所有调试都发生在模型构建过程中,而不是在执行时。这类似于编译器中的类型检查。
您可以将模型绘制成图,并且可以轻松访问此图中的中间节点。例如,要提取和重用中间层的激活(如前例所示)
features_list = [layer.output for layer in vgg19.layers]
feat_extraction_model = keras.Model(inputs=vgg19.input, outputs=features_list)
由于函数式模型是一个数据结构而不是一段代码,因此它可以安全地序列化,并可以保存为单个文件,让您在无法访问任何原始代码的情况下重新创建完全相同的模型。请参阅 序列化和保存指南。
要序列化子类模型,实现者必须在模型级别定义 get_config() 和 from_config() 方法。
函数式 API 将模型视为层的 DAG。这适用于大多数深度学习架构,但并非全部——例如,递归网络或 Tree RNN 不遵循此假设,并且无法在函数式 API 中实现。
在函数式 API 或 Model 子类化之间进行选择不是一个限制您进入一类模型的二元决定。keras API 中的所有模型都可以相互交互,无论是 Sequential 模型、函数式模型,还是从头开始编写的子类模型。
您始终可以将函数式模型或 Sequential 模型用作子类模型或层的一部分
units = 32
timesteps = 10
input_dim = 5
# Define a Functional model
inputs = keras.Input((None, units))
x = layers.GlobalAveragePooling1D()(inputs)
outputs = layers.Dense(1)(x)
model = keras.Model(inputs, outputs)
class CustomRNN(layers.Layer):
def __init__(self):
super().__init__()
self.units = units
self.projection_1 = layers.Dense(units=units, activation="tanh")
self.projection_2 = layers.Dense(units=units, activation="tanh")
# Our previously-defined Functional model
self.classifier = model
def call(self, inputs):
outputs = []
state = ops.zeros(shape=(inputs.shape[0], self.units))
for t in range(inputs.shape[1]):
x = inputs[:, t, :]
h = self.projection_1(x)
y = h + self.projection_2(state)
state = y
outputs.append(y)
features = ops.stack(outputs, axis=1)
print(features.shape)
return self.classifier(features)
rnn_model = CustomRNN()
_ = rnn_model(ops.zeros((1, timesteps, input_dim)))
(1, 10, 32)
(1, 10, 32)
只要自定义层或模型实现了 call 方法并遵循以下模式之一,您就可以在函数式 API 中使用任何子类层或模型
call(self, inputs, **kwargs) – 其中 inputs 是张量或张量的嵌套结构(例如张量列表),而 **kwargs 是非张量参数(非输入)。call(self, inputs, training=None, **kwargs) – 其中 training 是一个布尔值,指示层是否应在训练模式和推理模式下运行。call(self, inputs, mask=None, **kwargs) – 其中 mask 是一个布尔掩码张量(例如,对于 RNN 很常用)。call(self, inputs, training=None, mask=None, **kwargs) – 当然,您可以同时拥有掩码和特定于训练的行为。此外,如果您在自定义层或模型上实现了 get_config 方法,那么您创建的函数式模型仍然是可序列化的和可克隆的。
这是一个关于从头开始编写的自定义 RNN 在函数式模型中使用的快速示例
units = 32
timesteps = 10
input_dim = 5
batch_size = 16
class CustomRNN(layers.Layer):
def __init__(self):
super().__init__()
self.units = units
self.projection_1 = layers.Dense(units=units, activation="tanh")
self.projection_2 = layers.Dense(units=units, activation="tanh")
self.classifier = layers.Dense(1)
def call(self, inputs):
outputs = []
state = ops.zeros(shape=(inputs.shape[0], self.units))
for t in range(inputs.shape[1]):
x = inputs[:, t, :]
h = self.projection_1(x)
y = h + self.projection_2(state)
state = y
outputs.append(y)
features = ops.stack(outputs, axis=1)
return self.classifier(features)
# Note that you specify a static batch size for the inputs with the `batch_shape`
# arg, because the inner computation of `CustomRNN` requires a static batch size
# (when you create the `state` zeros tensor).
inputs = keras.Input(batch_shape=(batch_size, timesteps, input_dim))
x = layers.Conv1D(32, 3)(inputs)
outputs = CustomRNN()(x)
model = keras.Model(inputs, outputs)
rnn_model = CustomRNN()
_ = rnn_model(ops.zeros((1, 10, 5)))