开发者指南 / 函数式 API

函数式 API

作者: fchollet
创建日期: 2019/03/01
最后修改: 2023/06/25
描述: 函数式 API 的完整指南。

在 Colab 中查看 GitHub 源代码


设置

import numpy as np
import keras
from keras import layers
from keras import ops

介绍

Keras 的 函数式 API 是一种创建模型的方式,它比 keras.Sequential API 更加灵活。函数式 API 可以处理具有非线性拓扑、共享层,甚至多个输入或输出的模型。

主要思想是深度学习模型通常是一个层的有向无环图 (DAG)。因此,函数式 API 是构建 层图 的一种方式。

考虑以下模型:

(输入:784维向量)
       ↧
[稠密层 (64个单元,relu激活)]
       ↧
[稠密层 (64个单元,relu激活)]
       ↧
[稠密层 (10个单元,softmax激活)]
       ↧
(输出:10类的概率分布的对数)

这是一个包含三层的基本图。要使用函数式 API 构建此模型,请首先创建一个输入节点:

inputs = keras.Input(shape=(784,))

数据的形状设置为784维向量。批大小总是省略,因为只指定了每个样本的形状。

例如,如果您有一个形状为 (32, 32, 3) 的图像输入,您应该使用:

# 仅用于演示目的。
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()
模型: "mnist_model"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ 层 (类型)                        输出形状                   参数 # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ input_layer (输入层)        │ (None, 784)            │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense (稠密层)                   │ (None, 64)             │        50,240 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_1 (稠密层)                 │ (None, 64)             │         4,160 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_2 (Dense)                 │ (None, 10)             │           650 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 总参数: 55,050 (215.04 KB)
 可训练参数: 55,050 (215.04 KB)
 不可训练参数: 0 (0.00 B)

您也可以将模型绘制为图形:

keras.utils.plot_model(model, "my_first_model.png")

png

并且,可选地,在绘制的图形中显示每层的输入和输出形状:

keras.utils.plot_model(model, "my_first_model_with_shape_info.png", show_shapes=True)

png

该图形和代码几乎相同。在代码版本中,连接箭头被调用操作所替代。

“层的图”是深度学习模型的直观心理图像,功能 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_scores[0])
print("测试准确率:", test_scores[1])
Epoch 1/2
 750/750 ━━━━━━━━━━━━━━━━━━━━ 1s 863us/step - 准确率: 0.8425 - 损失: 0.5733 - val_accuracy: 0.9496 - val_loss: 0.1711
Epoch 2/2
 750/750 ━━━━━━━━━━━━━━━━━━━━ 1s 859us/step - 准确率: 0.9509 - 损失: 0.1641 - val_accuracy: 0.9578 - val_loss: 0.1396
313/313 - 0s - 341us/step - 准确率: 0.9613 - 损失: 0.1288
测试损失: 0.12876172363758087
测试准确率: 0.9613000154495239

有关更多阅读,请参见 训练和评估 指南。


保存和序列化

保存模型和序列化的工作方式与使用功能 API 构建的模型相同,和 Sequential 模型相同。保存功能模型的标准方法是调用 model.save() 将整个模型保存为单个文件。您可以稍后从该文件重新创建相同的模型,即使构建模型的代码不再可用。

这个保存的文件包括: - 模型架构 - 模型权重值(在训练过程中学习到的) - 模型训练配置(如果有的话,如传递给 compile() 的) - 优化器及其状态(如果有的话,以便在停止的地方重新开始训练)

model.save("my_model.keras")
del model
# 仅从文件重新创建完全相同的模型:
model = keras.models.load_model("my_model.keras")

For details, read the model serialization & saving guide.


使用相同的层图定义多个模型

在函数式API中,模型是通过在层图中指定其输入和输出来创建的。这意味着单个层图可以用于生成多个模型。

在下面的示例中,您使用相同的层堆栈来实例化两个模型:一个将图像输入转换为16维向量的encoder模型,以及一个用于训练的端到端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()
模型: "encoder"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ 层 (类型)                      输出形状                  参数 # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ img (输入层)                │ (, 28, 28, 1)      │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d (卷积层)                 │ (, 26, 26, 16)     │           160 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_1 (卷积层)               │ (, 24, 24, 32)     │         4,640 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ max_pooling2d (最大池化层)    │ (, 8, 8, 32)       │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_2 (卷积层)               │ (, 6, 6, 32)       │         9,248 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_3 (卷积层)               │ (, 4, 4, 16)       │         4,624 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ global_max_pooling2d            │ (, 16)             │             0 │
│ (全局最大池化层)            │                        │               │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 总参数: 18,672 (72.94 KB)
 可训练参数: 18,672 (72.94 KB)
 不可训练参数: 0 (0.00 B)
模型: "自动编码器"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ 层 (类型)                     输出形状                  参数 # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ img (输入层)                │ (, 28, 28, 1)      │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d (卷积层)                 │ (, 26, 26, 16)     │           160 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_1 (卷积层)               │ (, 24, 24, 32)     │         4,640 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ max_pooling2d (最大池化层)    │ (, 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              │ (, 28, 28, 1)      │           145 │
│ (Conv2DTranspose)               │                        │               │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 总参数: 28,241 (110.32 KB)
 可训练参数: 28,241 (110.32 KB)
 不可训练参数: 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()
模型: "encoder"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ 层 (类型)                     输出形状                  参数数量 ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ original_img (InputLayer)       │ (, 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)            │                        │               │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 总参数: 18,672 (72.94 KB)
 可训练参数: 18,672 (72.94 KB)
 不可训练参数: 0 (0.00 B)
模型: "decoder"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ 层 (类型)                     输出形状                  参数 # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ encoded_img (输入层)        │ (None, 16)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ reshape_1 (重塑)             │ (None, 4, 4, 1)        │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_transpose_4              │ (None, 6, 6, 16)       │           160 │
│ (反卷积2D)               │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_transpose_5              │ (None, 8, 8, 32)       │         4,640 │
│ (反卷积2D)               │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ up_sampling2d_1 (上采样2D)  │ (None, 24, 24, 32)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_transpose_6              │ (None, 26, 26, 16)     │         4,624 │
│ (反卷积2D)               │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_transpose_7              │ (None, 28, 28, 1)      │           145 │
│ (反卷积2D)               │                        │               │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 总参数: 9,569 (37.38 KB)
 可训练参数: 9,569 (37.38 KB)
 不可训练参数: 0 (0.00 B)
模型: "autoencoder"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ 层 (类型)                       输出形状                      参数 # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ img (输入层)                │ (None, 28, 28, 1)      │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ encoder (功能型)            │ (None, 16)             │        18,672 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ decoder (功能型)            │ (None, 28, 28, 1)      │         9,569 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 总参数: 28,241 (110.32 KB)
 可训练参数: 28,241 (110.32 KB)
 不可训练参数: 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 处理。

例如,如果你正在构建一个系统以按 优先级对客户问题票进行排序并将它们路由到正确的部门, 那么模型将有三个输入:

  • 票据的标题(文本输入),
  • 票据的正文(文本输入),以及
  • 用户添加的任何标签(分类输入)

这个模型将有两个输出:

  • 介于 0 到 1 之间的优先级分数(标量 sigmoid 输出),以及
  • 应该处理票据的部门(在部门集合上的 softmax 输出)。

你可以用几个代码行通过功能 API 构建这个模型:

num_tags = 12  # 唯一问题标签的数量
num_words = 10000  # 在预处理文本数据时获得的词汇表大小
num_departments = 4  # 用于预测的部门数量

title_input = keras.Input(
    shape=(None,), name="title"
)  # 可变长度的整数序列
body_input = keras.Input(shape=(None,), name="body")  # 可变长度的整数序列
tags_input = keras.Input(
    shape=(num_tags,), name="tags"
)  # 大小为 `num_tags` 的二进制向量

# 将标题中的每个单词嵌入到一个 64 维的向量中
title_features = layers.Embedding(num_words, 64)(title_input)
# 将文本中的每个单词嵌入到一个 64 维的向量中
body_features = layers.Embedding(num_words, 64)(body_input)

# 将标题中的嵌入单词序列简化为一个 128 维的向量
title_features = layers.LSTM(128)(title_features)
# 将主体中的嵌入单词序列简化为一个 32 维的向量
body_features = layers.LSTM(32)(body_features)

# 通过连接将所有可用特征合并为一个大的向量
x = layers.concatenate([title_features, body_features, tags_input])

# 在特征上方添加一个用于优先级预测的逻辑回归
priority_pred = layers.Dense(1, name="priority")(x)
# 在特征上方添加一个部门分类器
department_pred = layers.Dense(num_departments, name="department")(x)

# 实例化一个端到端模型,预测优先级和部门
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)

png

在编译此模型时,您可以为每个输出分配不同的损失。 您甚至可以为每个损失分配不同的权重,以调节 其对总训练损失的贡献。

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 数组的输入和目标列表来训练模型:

# 虚拟输入数据
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")

# 虚拟目标数据
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>

当使用 Dataset 对象调用 fit 时,它应该生成 一个元组列表,如 ([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})

有关更详细的说明,请参阅 训练和评估 指南。

一个玩具 ResNet 模型

除了具有多个输入和输出的模型外, 功能 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()
模型: "toy_resnet"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓
┃ 层 (类型)         输出形状          参数 #  连接到      ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩
│ img (输入层)    │ (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]     │
└─────────────────────┴───────────────────┴────────────┴───────────────────┘
 总参数: 223,242 (872.04 KB)
 可训练参数: 223,242 (872.04 KB)
 非可训练参数: 0 (0.00 B)

绘制模型:

keras.utils.plot_model(model, "mini_resnet.png", show_shapes=True)

png

现在训练模型:

(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"],
)
# 我们将数据限制为前 1000 个样本,以限制在 Colab 上的执行时间。
# 尝试在整个数据集上训练直到收敛!
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 层:

# 映射到128维向量的1000个独特单词的嵌入
shared_embedding = layers.Embedding(1000, 128)

# 可变长度整数序列
text_input_a = keras.Input(shape=(None,), dtype="int32")

# 可变长度整数序列
text_input_b = keras.Input(shape=(None,), dtype="int32")

# 重新使用相同的层来编码两个输入
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)

这对于类似任务很有帮助,例如 神经风格迁移, 以及其他内容。


使用自定义层扩展API

keras 包含多种内置层,例如:

  • 卷积层:Conv1DConv2DConv3DConv2DTranspose
  • 池化层:MaxPooling1DMaxPooling2DMaxPooling3DAveragePooling1D
  • RNN层:GRULSTMConvLSTM2D
  • BatchNormalizationDropoutEmbedding等。

但是,如果您没有找到所需的层,可以通过创建自己的层轻松扩展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)

何时使用函数式API

您应该使用Keras函数式API来创建新模型, 还是直接继承 Model 类?一般而言,函数式API 是更高层次的,更简单和更安全,并且具有一些 子类模型不支持的特性。

然而,模型子类化在构建不易表示为层的有向无环图时提供了更大的灵活性。 例如,您无法使用函数式API实现树型RNN, 而必须直接继承 Model

要深入了解函数式API和模型子类化之间的差异,请阅读 TensorFlow 2.0中的符号API和命令式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)

# 实例化模型。
mlp = MLP()
# 必须创建模型的状态。
# 模型在被调用至少一次之前没有状态。
_ = mlp(ops.zeros((1, 32)))

在定义其连接图时进行模型验证

在函数式API中,输入规范(形状和数据类型)是在前面创建的(使用 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的弱点:

不支持动态架构

函数式API将模型视为层的有向无环图(DAG)。 这对于大多数深度学习架构都是正确的,但并不适用于所有情况——例如, 递归网络或树状RNN不遵循这一假设,无法在函数式API中实现。


混合使用API风格

选择函数式API或模型子类化并不是限制您进入某一模型类别的二元决策。 keras API中的所有模型都可以相互交互,无论它们是 Sequential 模型、函数式模型还是从头编写的子类化模型。

您始终可以将函数式模型或 Sequential 模型 作为子类化模型或层的一部分使用:

units = 32
timesteps = 10
input_dim = 5

# 定义一个函数式模型
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")
        # 我们之前定义的函数式模型
        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)

您可以在函数式API中使用任何子类化层或模型 只要它实现一个 call 方法,并遵循以下模式之一:

  • 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)


# 请注意,您使用 `batch_shape` 参数为输入指定静态批大小,
# 因为 `CustomRNN` 的内部计算需要静态批大小
# (当您创建 `state` 零张量时)。
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)))