RSVP pour votre événement TensorFlow Everywhere local dès aujourd'hui!
Cette page a été traduite par l'API Cloud Translation.
Switch to English

L'API fonctionnelle

Voir sur TensorFlow.org Exécuter dans Google Colab Afficher la source sur GitHub Télécharger le cahier

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

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)

png

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)

png

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)

png

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