函数式 API

作者: fchollet

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看源代码 在 keras.io 上查看

设置

import numpy as np
import tensorflow as tf
from tensorflow import keras
from keras import layers

简介

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

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

考虑以下模型

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

这是一个包含三个层的简单图。要使用函数式 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
TensorShape([None, 784])

以下是 dtype

inputs.dtype
tf.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_1 (InputLayer)        [(None, 784)]             0         
                                                                 
 dense (Dense)               (None, 64)                50240     
                                                                 
 dense_1 (Dense)             (None, 64)                4160      
                                                                 
 dense_2 (Dense)             (None, 10)                650       
                                                                 
=================================================================
Total params: 55050 (215.04 KB)
Trainable params: 55050 (215.04 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________

您也可以将模型绘制成图表

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 模型的工作方式完全相同。

The Model 类提供了一个内置的训练循环(fit() 方法)和一个内置的评估循环(evaluate() 方法)。请注意,您可以轻松地 自定义这些循环 以实现超越监督学习的训练例程(例如 GAN)。

在这里,加载 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=[keras.metrics.SparseCategoricalAccuracy()],
)

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 [==============================] - 4s 3ms/step - loss: 0.3556 - sparse_categorical_accuracy: 0.8971 - val_loss: 0.1962 - val_sparse_categorical_accuracy: 0.9422
Epoch 2/2
750/750 [==============================] - 2s 2ms/step - loss: 0.1612 - sparse_categorical_accuracy: 0.9527 - val_loss: 0.1461 - val_sparse_categorical_accuracy: 0.9592
313/313 - 0s - loss: 0.1492 - sparse_categorical_accuracy: 0.9556 - 463ms/epoch - 1ms/step
Test loss: 0.14915992319583893
Test accuracy: 0.9556000232696533

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

保存和序列化

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

此保存的文件包括

  • 模型架构
  • 模型权重值(在训练期间学习到的)
  • 模型训练配置(如果有)(如传递给 compile
  • 优化器及其状态(如果有)(用于从您停止的地方重新开始训练)
model.save("path_to_my_model.keras")
del model
# Recreate the exact same model purely from the file:
model = keras.models.load_model("path_to_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)        4640      
                                                                 
 max_pooling2d (MaxPooling2  (None, 8, 8, 32)          0         
 D)                                                              
                                                                 
 conv2d_2 (Conv2D)           (None, 6, 6, 32)          9248      
                                                                 
 conv2d_3 (Conv2D)           (None, 4, 4, 16)          4624      
                                                                 
 global_max_pooling2d (Glob  (None, 16)                0         
 alMaxPooling2D)                                                 
                                                                 
=================================================================
Total params: 18672 (72.94 KB)
Trainable params: 18672 (72.94 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________
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)        4640      
                                                                 
 max_pooling2d (MaxPooling2  (None, 8, 8, 32)          0         
 D)                                                              
                                                                 
 conv2d_2 (Conv2D)           (None, 6, 6, 32)          9248      
                                                                 
 conv2d_3 (Conv2D)           (None, 4, 4, 16)          4624      
                                                                 
 global_max_pooling2d (Glob  (None, 16)                0         
 alMaxPooling2D)                                                 
                                                                 
 reshape (Reshape)           (None, 4, 4, 1)           0         
                                                                 
 conv2d_transpose (Conv2DTr  (None, 6, 6, 16)          160       
 anspose)                                                        
                                                                 
 conv2d_transpose_1 (Conv2D  (None, 8, 8, 32)          4640      
 Transpose)                                                      
                                                                 
 up_sampling2d (UpSampling2  (None, 24, 24, 32)        0         
 D)                                                              
                                                                 
 conv2d_transpose_2 (Conv2D  (None, 26, 26, 16)        4624      
 Transpose)                                                      
                                                                 
 conv2d_transpose_3 (Conv2D  (None, 28, 28, 1)         145       
 Transpose)                                                      
                                                                 
=================================================================
Total params: 28241 (110.32 KB)
Trainable params: 28241 (110.32 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________

在这里,解码架构与编码架构严格对称,因此输出形状与输入形状相同 (28, 28, 1)

The reverse of a Conv2D layer is a Conv2DTranspose layer, and the reverse of a MaxPooling2D layer is an UpSampling2D layer.

所有模型都可调用,就像层一样

您可以将任何模型视为层,方法是在 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)        4640      
                                                                 
 max_pooling2d_1 (MaxPoolin  (None, 8, 8, 32)          0         
 g2D)                                                            
                                                                 
 conv2d_6 (Conv2D)           (None, 6, 6, 32)          9248      
                                                                 
 conv2d_7 (Conv2D)           (None, 4, 4, 16)          4624      
                                                                 
 global_max_pooling2d_1 (Gl  (None, 16)                0         
 obalMaxPooling2D)                                               
                                                                 
=================================================================
Total params: 18672 (72.94 KB)
Trainable params: 18672 (72.94 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________
Model: "decoder"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 encoded_img (InputLayer)    [(None, 16)]              0         
                                                                 
 reshape_1 (Reshape)         (None, 4, 4, 1)           0         
                                                                 
 conv2d_transpose_4 (Conv2D  (None, 6, 6, 16)          160       
 Transpose)                                                      
                                                                 
 conv2d_transpose_5 (Conv2D  (None, 8, 8, 32)          4640      
 Transpose)                                                      
                                                                 
 up_sampling2d_1 (UpSamplin  (None, 24, 24, 32)        0         
 g2D)                                                            
                                                                 
 conv2d_transpose_6 (Conv2D  (None, 26, 26, 16)        4624      
 Transpose)                                                      
                                                                 
 conv2d_transpose_7 (Conv2D  (None, 28, 28, 1)         145       
 Transpose)                                                      
                                                                 
=================================================================
Total params: 9569 (37.38 KB)
Trainable params: 9569 (37.38 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________
Model: "autoencoder"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 img (InputLayer)            [(None, 28, 28, 1)]       0         
                                                                 
 encoder (Functional)        (None, 16)                18672     
                                                                 
 decoder (Functional)        (None, 28, 28, 1)         9569      
                                                                 
=================================================================
Total params: 28241 (110.32 KB)
Trainable params: 28241 (110.32 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________

如您所见,模型可以嵌套:模型可以包含子模型(因为模型就像层一样)。模型嵌套的常见用例是集成。例如,以下是如何将一组模型集成到一个模型中,该模型平均其预测

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  # 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_pred, 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 数组列表来训练模型

# Dummy input data
title_data = np.random.randint(num_words, size=(1280, 10))
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 [==============================] - 8s 112ms/step - loss: 1.2982 - priority_loss: 0.6991 - department_loss: 2.9958
Epoch 2/2
40/40 [==============================] - 3s 64ms/step - loss: 1.3110 - priority_loss: 0.6977 - department_loss: 3.0666
<keras.src.callbacks.History at 0x7f08d51fab80>

当使用 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()
Model: "toy_resnet"
__________________________________________________________________________________________________
 Layer (type)                Output Shape                 Param #   Connected to                  
==================================================================================================
 img (InputLayer)            [(None, 32, 32, 3)]          0         []                            
                                                                                                  
 conv2d_8 (Conv2D)           (None, 30, 30, 32)           896       ['img[0][0]']                 
                                                                                                  
 conv2d_9 (Conv2D)           (None, 28, 28, 64)           18496     ['conv2d_8[0][0]']            
                                                                                                  
 max_pooling2d_2 (MaxPoolin  (None, 9, 9, 64)             0         ['conv2d_9[0][0]']            
 g2D)                                                                                             
                                                                                                  
 conv2d_10 (Conv2D)          (None, 9, 9, 64)             36928     ['max_pooling2d_2[0][0]']     
                                                                                                  
 conv2d_11 (Conv2D)          (None, 9, 9, 64)             36928     ['conv2d_10[0][0]']           
                                                                                                  
 add (Add)                   (None, 9, 9, 64)             0         ['conv2d_11[0][0]',           
                                                                     'max_pooling2d_2[0][0]']     
                                                                                                  
 conv2d_12 (Conv2D)          (None, 9, 9, 64)             36928     ['add[0][0]']                 
                                                                                                  
 conv2d_13 (Conv2D)          (None, 9, 9, 64)             36928     ['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)             36928     ['add_1[0][0]']               
                                                                                                  
 global_average_pooling2d (  (None, 64)                   0         ['conv2d_14[0][0]']           
 GlobalAveragePooling2D)                                                                          
                                                                                                  
 dense_6 (Dense)             (None, 256)                  16640     ['global_average_pooling2d[0][
                                                                    0]']                          
                                                                                                  
 dropout (Dropout)           (None, 256)                  0         ['dense_6[0][0]']             
                                                                                                  
 dense_7 (Dense)             (None, 10)                   2570      ['dropout[0][0]']             
                                                                                                  
==================================================================================================
Total params: 223242 (872.04 KB)
Trainable params: 223242 (872.04 KB)
Non-trainable params: 0 (0.00 Byte)
__________________________________________________________________________________________________

绘制模型

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"],
)
# 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 [==============================] - 4s 39ms/step - loss: 2.3086 - acc: 0.0988 - val_loss: 2.3020 - val_acc: 0.0850
<keras.src.callbacks.History at 0x7f078810c880>

共享层

函数式 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()
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/vgg19/vgg19_weights_tf_dim_ordering_tf_kernels.h5
574710816/574710816 [==============================] - 4s 0us/step

这些是模型的中间激活,通过查询图数据结构获得

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 tf.matmul(inputs, self.w) + self.b


inputs = keras.Input((4,))
outputs = CustomDense(10)(inputs)

model = keras.Model(inputs, outputs)

为了在您的自定义层中支持序列化,请定义一个 get_config 方法,该方法返回层实例的构造函数参数

@keras.saving.register_keras_serializable()
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 tf.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)

可选地,实现类方法 from_config(cls, config),该方法在使用其配置字典重新创建层实例时使用。from_config 的默认实现是

def from_config(cls, config):
  return cls(**config)

何时使用函数式 API

您应该使用 Keras 函数式 API 创建一个新模型,还是直接子类化 Model 类?一般来说,函数式 API 更高级、更简单、更安全,并且具有一些子类模型不支持的功能。

但是,模型子类化在构建难以用层的定向无环图表示的模型时提供了更大的灵活性。例如,您无法使用函数式 API 实现 Tree-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)

# 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(tf.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

# Define a Functional model
inputs = keras.Input((None, units))
x = layers.GlobalAveragePooling1D()(inputs)
outputs = layers.Dense(1)(x)
model = keras.Model(inputs, outputs)


@keras.saving.register_keras_serializable()
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 = tf.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 = tf.stack(outputs, axis=1)
        print(features.shape)
        return self.classifier(features)


rnn_model = CustomRNN()
_ = rnn_model(tf.zeros((1, timesteps, input_dim)))
(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


@keras.saving.register_keras_serializable()
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 = tf.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 = tf.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(tf.zeros((1, 10, 5)))