![]() | ![]() | ![]() | ![]() |
Installer
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
introduction
L' API fonctionnelle Keras est un moyen de créer des modèles plus flexibles que l'API tf.keras.Sequential
. L'API fonctionnelle peut gérer des modèles avec une topologie non linéaire, des couches partagées et même plusieurs entrées ou sorties.
L'idée principale est qu'un modèle d'apprentissage en profondeur est généralement un graphe acyclique dirigé (DAG) de couches. Ainsi, l'API fonctionnelle est un moyen de créer des graphiques de couches .
Considérez le modèle suivant:
(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)
Ceci est un graphique de base avec trois couches. Pour créer ce modèle à l'aide de l'API fonctionnelle, commencez par créer un nœud d'entrée:
inputs = keras.Input(shape=(784,))
La forme des données est définie comme un vecteur de 784 dimensions. La taille du lot est toujours omise car seule la forme de chaque échantillon est spécifiée.
Si, par exemple, vous avez une entrée d'image avec une forme de (32, 32, 3)
, vous utiliserez:
# Just for demonstration purposes.
img_inputs = keras.Input(shape=(32, 32, 3))
Les inputs
renvoyées contiennent des informations sur la forme et le dtype
des données d'entrée que vous dtype
à votre modèle. Voici la forme:
inputs.shape
TensorShape([None, 784])
Voici le dtype:
inputs.dtype
tf.float32
Vous créez un nouveau nœud dans le graphique des couches en appelant une couche sur cet objet d' inputs
:
dense = layers.Dense(64, activation="relu")
x = dense(inputs)
L'action "appel de couche" est comme dessiner une flèche à partir des "entrées" vers cette couche que vous avez créée. Vous «passez» les entrées à la couche dense
, et vous obtenez x
comme sortie.
Ajoutons quelques couches supplémentaires au graphique des couches:
x = layers.Dense(64, activation="relu")(x)
outputs = layers.Dense(10)(x)
À ce stade, vous pouvez créer un Model
en spécifiant ses entrées et ses sorties dans le graphique des couches:
model = keras.Model(inputs=inputs, outputs=outputs, name="mnist_model")
Voyons à quoi ressemble le résumé du modèle:
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: 55,050 Trainable params: 55,050 Non-trainable params: 0 _________________________________________________________________
Vous pouvez également tracer le modèle sous forme de graphique:
keras.utils.plot_model(model, "my_first_model.png")
Et, éventuellement, affichez les formes d'entrée et de sortie de chaque couche dans le graphique tracé:
keras.utils.plot_model(model, "my_first_model_with_shape_info.png", show_shapes=True)
Ce chiffre et le code sont presque identiques. Dans la version code, les flèches de connexion sont remplacées par l'opération d'appel.
Un «graphique de couches» est une image mentale intuitive pour un modèle d'apprentissage en profondeur, et l'API fonctionnelle est un moyen de créer des modèles qui reflètent étroitement cela.
Formation, évaluation et inférence
La formation, l'évaluation et l'inférence fonctionnent exactement de la même manière pour les modèles créés à l'aide de l'API fonctionnelle et pour les modèles Sequential
.
La classe Model
propose une boucle d'apprentissage intégrée (la méthode fit()
) et une boucle d'évaluation intégrée (la méthode evaluate()
). Notez que vous pouvez facilement personnaliser ces boucles pour implémenter des routines de formation au-delà de l'apprentissage supervisé (par exemple les GAN ).
Ici, chargez les données d'image MNIST, remodelez-les en vecteurs, ajustez le modèle sur les données (tout en surveillant les performances lors d'un fractionnement de validation), puis évaluez le modèle sur les données de test:
(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 [==============================] - 3s 3ms/step - loss: 0.5848 - accuracy: 0.8332 - val_loss: 0.1880 - val_accuracy: 0.9480 Epoch 2/2 750/750 [==============================] - 2s 2ms/step - loss: 0.1699 - accuracy: 0.9503 - val_loss: 0.1490 - val_accuracy: 0.9563 313/313 - 0s - loss: 0.1463 - accuracy: 0.9563 Test loss: 0.14626088738441467 Test accuracy: 0.9563000202178955
Pour plus d'informations, consultez le guide de formation et d'évaluation .
Enregistrer et sérialiser
L'enregistrement du modèle et la sérialisation fonctionnent de la même manière pour les modèles créés à l'aide de l'API fonctionnelle que pour les modèles Sequential
. La méthode standard pour enregistrer un modèle fonctionnel consiste à appeler model.save()
pour enregistrer le modèle entier dans un seul fichier. Vous pouvez ultérieurement recréer le même modèle à partir de ce fichier, même si le code qui a généré le modèle n'est plus disponible.
Ce fichier enregistré comprend:
- architecture modèle
- les valeurs de poids du modèle (qui ont été apprises pendant l'entraînement)
- configuration de la formation du modèle, le cas échéant (telle que transmise à la
compile
) - l'optimiseur et son état, le cas échéant (pour redémarrer l'entraînement là où vous l'avez laissé)
model.save("path_to_my_model")
del model
# Recreate the exact same model purely from the file:
model = keras.models.load_model("path_to_my_model")
INFO:tensorflow:Assets written to: path_to_my_model/assets
Pour plus de détails, lisez le guide de sérialisation et d'enregistrement des modèles.
Utilisez le même graphique de couches pour définir plusieurs modèles
Dans l'API fonctionnelle, les modèles sont créés en spécifiant leurs entrées et sorties dans un graphique de couches. Cela signifie qu'un seul graphique de couches peut être utilisé pour générer plusieurs modèles.
Dans l'exemple ci-dessous, vous utilisez la même pile de couches pour instancier deux modèles: un modèle de encoder
qui transforme les entrées d'image en vecteurs à 16 dimensions et un modèle de autoencoder
bout en autoencoder
pour l'apprentissage.
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 (MaxPooling2D) (None, 8, 8, 32) 0 _________________________________________________________________ conv2d_2 (Conv2D) (None, 6, 6, 32) 9248 _________________________________________________________________ conv2d_3 (Conv2D) (None, 4, 4, 16) 4624 _________________________________________________________________ global_max_pooling2d (Global (None, 16) 0 ================================================================= Total params: 18,672 Trainable params: 18,672 Non-trainable params: 0 _________________________________________________________________ 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 (MaxPooling2D) (None, 8, 8, 32) 0 _________________________________________________________________ conv2d_2 (Conv2D) (None, 6, 6, 32) 9248 _________________________________________________________________ conv2d_3 (Conv2D) (None, 4, 4, 16) 4624 _________________________________________________________________ global_max_pooling2d (Global (None, 16) 0 _________________________________________________________________ reshape (Reshape) (None, 4, 4, 1) 0 _________________________________________________________________ conv2d_transpose (Conv2DTran (None, 6, 6, 16) 160 _________________________________________________________________ conv2d_transpose_1 (Conv2DTr (None, 8, 8, 32) 4640 _________________________________________________________________ up_sampling2d (UpSampling2D) (None, 24, 24, 32) 0 _________________________________________________________________ conv2d_transpose_2 (Conv2DTr (None, 26, 26, 16) 4624 _________________________________________________________________ conv2d_transpose_3 (Conv2DTr (None, 28, 28, 1) 145 ================================================================= Total params: 28,241 Trainable params: 28,241 Non-trainable params: 0 _________________________________________________________________
Ici, l'architecture de décodage est strictement symétrique à l'architecture de codage, de sorte que la forme de sortie est la même que la forme d'entrée (28, 28, 1)
.
L'inverse d'un calque Conv2D
est un calque Conv2DTranspose
et l'inverse d'un calque MaxPooling2D
est un calque UpSampling2D
.
Tous les modèles sont appelables, tout comme les calques
Vous pouvez traiter n'importe quel modèle comme s'il s'agissait d'une couche en l'appelant sur une Input
ou sur la sortie d'une autre couche. En appelant un modèle, vous ne réutilisez pas seulement l'architecture du modèle, vous réutilisez également ses pondérations.
Pour voir cela en action, voici une interprétation différente de l'exemple d'autoencoder qui crée un modèle d'encodeur, un modèle de décodeur et les enchaîne en deux appels pour obtenir le modèle d'autoencoder:
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 (MaxPooling2 (None, 8, 8, 32) 0 _________________________________________________________________ conv2d_6 (Conv2D) (None, 6, 6, 32) 9248 _________________________________________________________________ conv2d_7 (Conv2D) (None, 4, 4, 16) 4624 _________________________________________________________________ global_max_pooling2d_1 (Glob (None, 16) 0 ================================================================= Total params: 18,672 Trainable params: 18,672 Non-trainable params: 0 _________________________________________________________________ Model: "decoder" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= encoded_img (InputLayer) [(None, 16)] 0 _________________________________________________________________ reshape_1 (Reshape) (None, 4, 4, 1) 0 _________________________________________________________________ conv2d_transpose_4 (Conv2DTr (None, 6, 6, 16) 160 _________________________________________________________________ conv2d_transpose_5 (Conv2DTr (None, 8, 8, 32) 4640 _________________________________________________________________ up_sampling2d_1 (UpSampling2 (None, 24, 24, 32) 0 _________________________________________________________________ conv2d_transpose_6 (Conv2DTr (None, 26, 26, 16) 4624 _________________________________________________________________ conv2d_transpose_7 (Conv2DTr (None, 28, 28, 1) 145 ================================================================= Total params: 9,569 Trainable params: 9,569 Non-trainable params: 0 _________________________________________________________________ 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: 28,241 Trainable params: 28,241 Non-trainable params: 0 _________________________________________________________________
Comme vous pouvez le voir, le modèle peut être imbriqué: un modèle peut contenir des sous-modèles (puisqu'un modèle est comme une couche). Un cas d'utilisation courant pour l'imbrication de modèles est l' assemblage . Par exemple, voici comment regrouper un ensemble de modèles en un seul modèle qui fait la moyenne de leurs prédictions:
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)
Manipuler des topologies de graphes complexes
Modèles avec plusieurs entrées et sorties
L'API fonctionnelle facilite la manipulation de plusieurs entrées et sorties. Cela ne peut pas être géré avec l'API Sequential
.
Par exemple, si vous créez un système pour classer les tickets de problème client par priorité et les acheminer vers le service approprié, le modèle aura trois entrées:
- le titre du ticket (saisie de texte),
- le corps du texte du ticket (saisie de texte), et
- toutes les balises ajoutées par l'utilisateur (entrée catégorielle)
Ce modèle aura deux sorties:
- le score de priorité entre 0 et 1 (sortie sigmoïde scalaire), et
- le département qui doit gérer le ticket (sortie softmax sur l'ensemble des départements).
Vous pouvez construire ce modèle en quelques lignes avec l'API fonctionnelle:
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],
)
Tracez maintenant le modèle:
keras.utils.plot_model(model, "multi_input_and_output_model.png", show_shapes=True)
Lors de la compilation de ce modèle, vous pouvez affecter différentes pertes à chaque sortie. Vous pouvez même attribuer différents poids à chaque perte - pour moduler leur contribution à la perte totale d'entraînement.
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],
)
Étant donné que les couches de sortie ont des noms différents, vous pouvez également spécifier la perte comme suit:
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=[1.0, 0.2],
)
Entraînez le modèle en passant des listes de tableaux NumPy d'entrées et de cibles:
# 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 [==============================] - 4s 11ms/step - loss: 1.2978 - priority_loss: 0.7067 - department_loss: 2.9554 Epoch 2/2 40/40 [==============================] - 0s 11ms/step - loss: 1.2947 - priority_loss: 0.7023 - department_loss: 2.9621 <tensorflow.python.keras.callbacks.History at 0x7fe18923e6a0>
Lors de l'appel à fit avec un objet Dataset
, il doit générer un tuple de listes comme ([title_data, body_data, tags_data], [priority_targets, dept_targets])
ou un tuple de dictionnaires comme ({'title': title_data, 'body': body_data, 'tags': tags_data}, {'priority': priority_targets, 'department': dept_targets})
.
Pour des explications plus détaillées, reportez-vous au guide de formation et d'évaluation .
Un modèle jouet ResNet
Outre les modèles avec plusieurs entrées et sorties, l'API fonctionnelle facilite la manipulation des topologies de connectivité non linéaires - ce sont des modèles avec des couches qui ne sont pas connectées séquentiellement, ce que l'API Sequential
ne peut pas gérer.
Un cas d'utilisation courant est celui des connexions résiduelles. Construisons un modèle de jouet ResNet pour CIFAR10 pour démontrer ceci:
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 (MaxPooling2D) (None, 9, 9, 64) 0 conv2d_9[0][0] __________________________________________________________________________________________________ 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 (Globa (None, 64) 0 conv2d_14[0][0] __________________________________________________________________________________________________ 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: 223,242 Trainable params: 223,242 Non-trainable params: 0 __________________________________________________________________________________________________
Tracez le modèle:
keras.utils.plot_model(model, "mini_resnet.png", show_shapes=True)
Maintenant, entraînez le modèle:
(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)
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz 170500096/170498071 [==============================] - 6s 0us/step 13/13 [==============================] - 2s 35ms/step - loss: 2.2992 - acc: 0.1273 - val_loss: 2.2629 - val_acc: 0.1850 <tensorflow.python.keras.callbacks.History at 0x7fe210396ef0>
Couches partagées
Les modèles qui utilisent des couches partagées sont une autre bonne utilisation de l'API fonctionnelle. Les couches partagées sont des instances de couche qui sont réutilisées plusieurs fois dans le même modèle - elles apprennent des fonctionnalités qui correspondent à plusieurs chemins dans le graphe des couches.
Les couches partagées sont souvent utilisées pour encoder les entrées d'espaces similaires (par exemple, deux morceaux de texte différents qui présentent un vocabulaire similaire). Ils permettent le partage d'informations entre ces différentes entrées, et ils permettent de former un tel modèle sur moins de données. Si un mot donné est vu dans l'une des entrées, cela profitera au traitement de toutes les entrées qui traversent la couche partagée.
Pour partager une couche dans l'API fonctionnelle, appelez plusieurs fois la même instance de couche. Par exemple, voici un calque d' Embedding
partagé sur deux entrées de texte différentes:
# 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)
Extraire et réutiliser les nœuds dans le graphique des couches
Étant donné que le graphique des couches que vous manipulez est une structure de données statique, il peut être consulté et inspecté. Et c'est ainsi que vous pouvez tracer des modèles fonctionnels sous forme d'images.
Cela signifie également que vous pouvez accéder aux activations des couches intermédiaires («nœuds» dans le graphique) et les réutiliser ailleurs - ce qui est très utile pour quelque chose comme l'extraction de caractéristiques.
Regardons un exemple. Il s'agit d'un modèle VGG19 avec des poids pré-entraînés sur ImageNet:
vgg19 = tf.keras.applications.VGG19()
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/vgg19/vgg19_weights_tf_dim_ordering_tf_kernels.h5 574717952/574710816 [==============================] - 7s 0us/step
Et ce sont les activations intermédiaires du modèle, obtenues en interrogeant la structure de données du graphe:
features_list = [layer.output for layer in vgg19.layers]
Utilisez ces fonctionnalités pour créer un nouveau modèle d'extraction d'entités qui renvoie les valeurs des activations de couche intermédiaire:
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)
Cela s'avère utile pour des tâches telles que le transfert de style neuronal , entre autres.
Étendez l'API à l'aide de couches personnalisées
tf.keras
comprend une large gamme de couches intégrées, par exemple:
- Couches convolutives:
Conv1D
,Conv2D
,Conv3D
,Conv2DTranspose
-
MaxPooling1D
commun des couches:MaxPooling1D
,MaxPooling2D
,MaxPooling3D
,AveragePooling1D
- Couches RNN:
GRU
,LSTM
,ConvLSTM2D
-
BatchNormalization
,Dropout
,Embedding
, etc.
Mais si vous ne trouvez pas ce dont vous avez besoin, il est facile d'étendre l'API en créant vos propres couches. Toutes les couches sous-classent la classe Layer
et implémentent:
-
call
, qui spécifie le calcul effectué par la couche. -
build
, qui crée les pondérations du calque (il ne s'agit que d'une convention de style puisque vous pouvez également créer des pondérations dans__init__
).
Pour en savoir plus sur la création de calques à partir de zéro, lisez le guide des calques et modèles personnalisés .
Voici une implémentation de base de tf.keras.layers.Dense
:
class CustomDense(layers.Layer):
def __init__(self, units=32):
super(CustomDense, self).__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)
Pour la prise en charge de la sérialisation dans votre couche personnalisée, définissez une méthode get_config
qui renvoie les arguments du constructeur de l'instance de la couche:
class CustomDense(layers.Layer):
def __init__(self, units=32):
super(CustomDense, self).__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, custom_objects={"CustomDense": CustomDense})
Facultativement, implémentez la méthode de classe from_config(cls, config)
qui est utilisée lors de la recréation d'une instance de couche en fonction de son dictionnaire de configuration. L'implémentation par défaut de from_config
est:
def from_config(cls, config):
return cls(**config)
Quand utiliser l'API fonctionnelle
Devez-vous utiliser l'API fonctionnelle Keras pour créer un nouveau modèle ou simplement sous-classer directement la classe Model
? En général, l'API fonctionnelle est de plus haut niveau, plus simple et plus sûre, et possède un certain nombre de fonctionnalités que les modèles sous-classés ne prennent pas en charge.
Cependant, la sous-classification des modèles offre une plus grande flexibilité lors de la création de modèles qui ne sont pas facilement exprimables en tant que graphiques acycliques dirigés de couches. Par exemple, vous ne pouvez pas implémenter un Tree-RNN avec l'API fonctionnelle et devez sous- Model
directement le Model
.
Pour un examen approfondi des différences entre l'API fonctionnelle et la sous-classification de modèle, lisez Que sont les API symboliques et impératives dans TensorFlow 2.0? .
Points forts de l'API fonctionnelle:
Les propriétés suivantes sont également valables pour les modèles séquentiels (qui sont également des structures de données), mais ne le sont pas pour les modèles sous-classés (qui sont du bytecode Python et non des structures de données).
Moins verbeux
Il n'y a pas de super(MyClass, self).__init__(...)
, pas d' def call(self, ...):
super(MyClass, self).__init__(...)
etc.
Comparer:
inputs = keras.Input(shape=(32,))
x = layers.Dense(64, activation='relu')(inputs)
outputs = layers.Dense(10)(x)
mlp = keras.Model(inputs, outputs)
Avec la version sous-classée:
class MLP(keras.Model):
def __init__(self, **kwargs):
super(MLP, self).__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)))
Validation du modèle lors de la définition de son graphe de connectivité
Dans l'API fonctionnelle, la spécification d'entrée (shape et dtype) est créée à l'avance (en utilisant Input
). Chaque fois que vous appelez une couche, la couche vérifie que la spécification qui lui a été transmise correspond à ses hypothèses, et elle lèvera un message d'erreur utile si ce n'est pas le cas.
Cela garantit que tout modèle que vous pouvez créer avec l'API fonctionnelle sera exécuté. Tout le débogage - autre que le débogage lié à la convergence - se produit de manière statique pendant la construction du modèle et non au moment de l'exécution. Ceci est similaire à la vérification de type dans un compilateur.
Un modèle fonctionnel peut être tracé et inspecté
Vous pouvez tracer le modèle sous forme de graphique et accéder facilement aux nœuds intermédiaires de ce graphique. Par exemple, pour extraire et réutiliser les activations des couches intermédiaires (comme vu dans un exemple précédent):
features_list = [layer.output for layer in vgg19.layers]
feat_extraction_model = keras.Model(inputs=vgg19.input, outputs=features_list)
Un modèle fonctionnel peut être sérialisé ou cloné
Étant donné qu'un modèle fonctionnel est une structure de données plutôt qu'un morceau de code, il est sérialisable en toute sécurité et peut être enregistré en tant que fichier unique qui vous permet de recréer exactement le même modèle sans avoir accès à aucun des codes d'origine. Consultez le guide de sérialisation et d'enregistrement .
Pour sérialiser un modèle sous- get_config()
, il est nécessaire que l'implémenteur spécifie une get_config()
et from_config()
au niveau du modèle.
Faiblesse fonctionnelle de l'API:
Il ne prend pas en charge les architectures dynamiques
L'API fonctionnelle traite les modèles comme des DAG de couches. Cela est vrai pour la plupart des architectures d'apprentissage en profondeur, mais pas pour toutes - par exemple, les réseaux récursifs ou les RNN d'arbre ne suivent pas cette hypothèse et ne peuvent pas être implémentés dans l'API fonctionnelle.
Styles d'API mix-and-match
Le choix entre l'API fonctionnelle ou la sous-classification de modèles n'est pas une décision binaire qui vous restreint à une seule catégorie de modèles. Tous les modèles de l'API tf.keras
peuvent interagir les uns avec les autres, qu'il s'agisse de modèles Sequential
modèles fonctionnels ou de modèles sous- tf.keras
écrits à partir de zéro.
Vous pouvez toujours utiliser un modèle fonctionnel ou un modèle Sequential
dans le cadre d'un modèle ou d'une couche sous-classée:
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(CustomRNN, self).__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)
Vous pouvez utiliser n'importe quelle couche ou modèle sous-classé dans l'API fonctionnelle tant qu'il implémente une méthode d' call
qui suit l'un des modèles suivants:
-
call(self, inputs, **kwargs)
- Oùinputs
est un tenseur ou une structure imbriquée de tenseurs (par exemple une liste de tenseurs), et où**kwargs
sont des arguments non tensoriels (non-inputs). -
call(self, inputs, training=None, **kwargs)
- Oùtraining
est un booléen indiquant si la couche doit se comporter en mode apprentissage et en mode inférence. -
call(self, inputs, mask=None, **kwargs)
- Oùmask
est un tenseur de masque booléen (utile pour les RNN, par exemple). -
call(self, inputs, training=None, mask=None, **kwargs)
- Bien sûr, vous pouvez avoir à la fois un comportement de masquage et un comportement spécifique à la formation.
En outre, si vous implémentez la méthode get_config
sur votre couche ou modèle personnalisé, les modèles fonctionnels que vous créez seront toujours sérialisables et clonables.
Voici un exemple rapide d'un RNN personnalisé, écrit à partir de zéro, utilisé dans un modèle fonctionnel:
units = 32
timesteps = 10
input_dim = 5
batch_size = 16
class CustomRNN(layers.Layer):
def __init__(self):
super(CustomRNN, self).__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)))