Aide à protéger la Grande barrière de corail avec tensorflow sur Kaggle Rejoignez Défi

L'API fonctionnelle

Voir sur TensorFlow.org Exécuter dans Google Colab Voir 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 souples que l' tf.keras.Sequential API. 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 construire 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)

Il s'agit d'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 à 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 devez utiliser:

# Just for demonstration purposes.
img_inputs = keras.Input(shape=(32, 32, 3))

Les inputs qui est retourné contient des informations sur la forme et dtype des données d'entrée que vous nourrissez à votre modèle. Voici la forme :

inputs.shape
TensorShape([None, 784])

Voici le type :

inputs.dtype
tf.float32

Vous créez un nouveau nœud dans le graphe des couches en appelant une couche sur cette inputs objet:

dense = layers.Dense(64, activation="relu")
x = dense(inputs)

L'action « appel de couche » revient à tracer une flèche à partir des « entrées » vers cette couche que vous avez créée. Vous êtes « passer » les entrées à la dense couche, et vous obtenez x comme la 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 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, afficher 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 le travail d'inférence exactement de la même manière pour les modèles construits en utilisant l'API fonctionnelle pour Sequential modèles.

Le Model offre de classe une boucle de formation intégrée (l' fit() méthode) et une boucle d'évaluation intégrée (l' evaluate() méthode). Notez que vous pouvez facilement personnaliser ces boucles pour mettre en œuvre des routines de formation au - delà de l' apprentissage supervisé (par exemple GAN ).

Ici, chargez les données d'image MNIST, reformulez-les en vecteurs, adaptez le modèle aux données (tout en surveillant les performances sur une division 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.3430 - accuracy: 0.9035 - val_loss: 0.1851 - val_accuracy: 0.9463
Epoch 2/2
750/750 [==============================] - 2s 3ms/step - loss: 0.1585 - accuracy: 0.9527 - val_loss: 0.1366 - val_accuracy: 0.9597
313/313 - 0s - loss: 0.1341 - accuracy: 0.9592
Test loss: 0.13414572179317474
Test accuracy: 0.9592000246047974

Pour en savoir plus, voir la formation et l' évaluation guide.

Enregistrer et sérialiser

Enregistrement des travaux de modèle et sérialisation de la même façon pour les modèles construits en utilisant l'API fonctionnelle comme ils le font pour Sequential modèles. La méthode standard pour enregistrer un modèle fonctionnel est d'appeler model.save() pour enregistrer l'ensemble du modèle en un seul fichier. Vous pouvez recréer ultérieurement le même modèle à partir de ce fichier, même si le code qui a construit le modèle n'est plus disponible.

Ce fichier enregistré comprend :

  • architecture de modèle
  • valeurs de poids du modèle (qui ont été apprises pendant la formation)
  • config modèle de formation, le cas échéant (comme transmis à la compile )
  • optimiseur et son état, le cas échéant (pour reprendre 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")
2021-08-25 17:50:55.989736: W tensorflow/python/util/util.cc:348] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them.
INFO:tensorflow:Assets written to: path_to_my_model/assets

Pour plus de détails, lisez le modèle sérialisation et sauver guide.

Utiliser 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 encoder modèle tours entrées d'image dans des vecteurs 16 dimensions, et un bout en bout autoencoder modèle de formation.

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 par rapport à 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 Conv2D couche est une Conv2DTranspose couche, et l'inverse d'un MaxPooling2D couche est une UpSampling2D couche.

Tous les modèles sont appelables, tout comme les calques

Vous pouvez traiter tout modèle comme si elle était une couche en invoquant sur une Input ou à 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 poids.

Pour voir cela en action, voici une version différente de l'exemple d'encodeur automatique 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'encodeur automatique :

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 un calque). Un cas d'utilisation commune pour la nidification du modèle est ensembling. 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 manipulé avec l' Sequential API.

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 les pertes et les poids de perte avec les noms de couche correspondants :

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

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 [==============================] - 5s 9ms/step - loss: 1.2899 - priority_loss: 0.7186 - department_loss: 2.8564
Epoch 2/2
40/40 [==============================] - 0s 9ms/step - loss: 1.2668 - priority_loss: 0.6991 - department_loss: 2.8389
<keras.callbacks.History at 0x7fc1a66dc790>

Lors de l' appel en forme avec un Dataset objet, il doit donner soit 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 une explication plus détaillée, reportez - vous à la formation et l' évaluation guide.

Un modèle ResNet jouet

En plus des modèles avec plusieurs entrées et sorties, l'API fonctionnelle, il est facile de manipuler des topologies de connectivité non-linéaires - ce sont des modèles avec des couches qui ne sont pas connectées de manière séquentielle, que le Sequential API ne peut pas gérer.

Un cas d'utilisation courant est celui des connexions résiduelles. Construisons un modèle ResNet jouet 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 [==============================] - 11s 0us/step
170508288/170498071 [==============================] - 11s 0us/step
13/13 [==============================] - 2s 29ms/step - loss: 2.3364 - acc: 0.1063 - val_loss: 2.2986 - val_acc: 0.0850
<keras.callbacks.History at 0x7fc19df22610>

Calques partagés

Une autre bonne utilisation de l'API fonctionnelle sont des modèles qui utilisent des couches partagées. 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 caractéristiques qui correspondent à plusieurs chemins dans le graphe des couches.

Les couches partagées sont souvent utilisées pour coder 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 d'entraîner 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 passent par la couche partagée.

Pour partager une couche dans l'API fonctionnelle, appelez plusieurs fois la même instance de couche. Par exemple, est ici une Embedding couche partagée entre deux entrées de texte différents:

# 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 des nœuds dans le graphe des couches

Étant donné que le graphique des couches que vous manipulez est une structure de données statique, il est possible d'y accéder et de l'inspecter. 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 [==============================] - 15s 0us/step
574726144/574710816 [==============================] - 15s 0us/step

Et voici 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 de fonctionnalité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 est très pratique pour des tâches telles que le transfert de style nerveux , entre autres.

Étendre l'API à l'aide de couches personnalisées

tf.keras comprend un large éventail de haut-couches, par exemple:

  • Couches: convolutifs Conv1D , Conv2D , Conv3D , Conv2DTranspose
  • Couches Pooling: 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 - classe la Layer classe et mettre en œuvre:

  • call méthode, qui spécifie le calcul effectué par la couche.
  • build méthode, qui crée les poids de la couche (ce qui est juste une convention de style puisque vous pouvez créer des poids dans __init__ , ainsi).

Pour en savoir plus sur la création de couches à partir de zéro, lisez des couches personnalisées et des modèles guide.

Ce qui suit est 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 le support sérialisation dans votre couche personnalisée, définissez une get_config méthode 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})

Éventuellement, mettre en oeuvre la méthode de classe from_config(cls, config) qui est utilisé lors de la recréation d' une instance de couche donné 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

Si vous utilisez l'API fonctionnelle Keras pour créer un nouveau modèle, ou tout simplement sous - classe du Model classe directement? En général, l'API fonctionnelle est de niveau supérieur, 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, le sous-classement de modèles offre une plus grande flexibilité lors de la création de modèles qui ne sont pas facilement exprimables sous forme de graphes acycliques dirigés de couches. Par exemple, on ne pouvait pas mettre en œuvre un arbre RNN avec l'API fonctionnelle et devrait sous - classe Model directement.

Pour un regard en profondeur les différences entre l'API fonctionnelle et subclassing modèle, lisez Quelles sont les API symboliques et IMPERIEUSE tensorflow 2.0? .

Points forts de l'API fonctionnelle :

Les propriétés suivantes sont également vraies 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, pas des structures de données).

Moins bavard

Il n'y a pas de super(MyClass, self).__init__(...) , pas d' def call(self, ...): , 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 (forme et DTYPE) est créée à l' avance ( à l' aide d' Input ). Chaque fois que vous appelez une couche, la couche vérifie que la spécification qui lui est transmise correspond à ses hypothèses, et sinon, elle générera un message d'erreur utile.

Cela garantit que tout modèle que vous pouvez créer avec l'API fonctionnelle s'exécutera. 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 est traçable et inspectable

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 de 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é

Parce 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 au code d'origine. Voir le guide de sérialisation et d' économie .

Pour sérialiser un modèle sous - classé, il est nécessaire de préciser l'implémenteur un get_config() et from_config() méthode 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 Tree RNN ne suivent pas cette hypothèse et ne peuvent pas être implémentés dans l'API fonctionnelle.

Mélanger et assortir les styles d'API

Choisir entre l'API fonctionnelle ou la sous-classe de modèle n'est pas une décision binaire qui vous limite à une catégorie de modèles. Tous les modèles de la tf.keras API peuvent interagir les uns avec les autres, si elles sont Sequential des modèles, des modèles fonctionnels ou modèles qui sont écrits sous - classées à partir de zéro.

Vous pouvez toujours utiliser un modèle fonctionnel ou Sequential modèle dans le cadre d'un modèle ou une couche sous - classé:

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 une couche ou modèle sous - classé l'API fonctionnelle tant qu'elle met en oeuvre un call méthode qui suit l' un des motifs suivants:

  • call(self, inputs, **kwargs) - Lorsque des inputs est un tenseur ou une structure imbriquée de tenseurs (par exemple , une liste des tenseurs), et où **kwargs sont des arguments non-tenseur (non-entrées).
  • call(self, inputs, training=None, **kwargs) - Lorsque la training est un booléen indiquant si la couche doit se comporter en mode de formation et le mode d'inférence.
  • call(self, inputs, mask=None, **kwargs) - Si le mask est un tenseur de masque booléen (utile pour RNNs, par exemple).
  • call(self, inputs, training=None, mask=None, **kwargs) - Bien sûr, vous pouvez avoir à la fois le masquage et le comportement spécifique à la formation en même temps.

De plus, si vous implémentez la get_config méthode sur votre couche personnalisée ou modèle, les modèles fonctionnels que vous créez toujours sérialisable et cloneable.

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