L'API funzionale

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza sorgente su GitHub Scarica taccuino

Impostare

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

introduzione

L' API funzionale di Keras è un modo per creare modelli più flessibili dell'API tf.keras.Sequential . L'API funzionale può gestire modelli con topologia non lineare, livelli condivisi e persino più input o output.

L'idea principale è che un modello di apprendimento profondo è solitamente un grafo aciclico diretto (DAG) di strati. Quindi l'API funzionale è un modo per costruire grafici di livelli .

Considera il seguente modello:

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

Questo è un grafico di base con tre livelli. Per costruire questo modello utilizzando l'API funzionale, inizia creando un nodo di input:

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

La forma dei dati è impostata come un vettore a 784 dimensioni. La dimensione del lotto viene sempre omessa poiché viene specificata solo la forma di ciascun campione.

Se, ad esempio, hai un input di immagine con una forma di (32, 32, 3) , dovresti usare:

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

Gli inputs restituiti contengono informazioni sulla forma e il dtype dei dati di input dtype al modello. Ecco la forma:

inputs.shape
TensorShape([None, 784])

Ecco il dtype:

inputs.dtype
tf.float32

Si crea un nuovo nodo nel grafico dei livelli chiamando un livello su questo oggetto di inputs :

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

L'azione "richiamo del livello" è come disegnare una freccia dagli "input" a questo livello che hai creato. Stai "passando" gli input al livello dense e ottieni x come output.

Aggiungiamo alcuni altri livelli al grafico dei livelli:

x = layers.Dense(64, activation="relu")(x)
outputs = layers.Dense(10)(x)

A questo punto, puoi creare un Model specificandone gli input e gli output nel grafico dei livelli:

model = keras.Model(inputs=inputs, outputs=outputs, name="mnist_model")

Vediamo come appare il riepilogo del modello:

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
_________________________________________________________________

Puoi anche tracciare il modello come grafico:

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

png

E, facoltativamente, visualizza le forme di input e output di ogni livello nel grafico tracciato:

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

png

Questa cifra e il codice sono quasi identici. Nella versione in codice, le frecce di connessione sono sostituite dall'operazione di chiamata.

Un "grafico di livelli" è un'immagine mentale intuitiva per un modello di apprendimento profondo e l'API funzionale è un modo per creare modelli che lo rispecchiano da vicino.

Formazione, valutazione e inferenza

L'addestramento, la valutazione e l'inferenza funzionano esattamente allo stesso modo per i modelli costruiti utilizzando l'API funzionale come per i modelli Sequential .

La classe Model offre un ciclo di addestramento fit() metodo fit() ) e un ciclo di valutazione integrato (il metodo evaluate() ). Si noti che è possibile personalizzare facilmente questi cicli per implementare routine di formazione oltre l'apprendimento supervisionato (ad es. GAN ).

Qui, caricare i dati dell'immagine MNIST, rimodellarli in vettori, adattare il modello ai dati (monitorando le prestazioni su una divisione di convalida), quindi valutare il modello sui dati del 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

Per ulteriori letture, vedere la guida alla formazione e alla valutazione .

Salva e serializza

Il salvataggio del modello e la serializzazione funzionano allo stesso modo per i modelli creati utilizzando l'API funzionale come per i modelli Sequential . Il modo standard per salvare un modello funzionale è chiamare model.save() per salvare l'intero modello come un singolo file. Successivamente è possibile ricreare lo stesso modello da questo file, anche se il codice che ha creato il modello non è più disponibile.

Questo file salvato include:

  • architettura del modello
  • valori di peso del modello (che sono stati appresi durante l'allenamento)
  • configurazione dell'addestramento del modello, se presente (come passato alla compile )
  • ottimizzatore e il suo stato, se presente (per ricominciare l'allenamento da dove si era interrotto)
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

Per i dettagli, leggi la guida al salvataggio e alla serializzazione del modello.

Usa lo stesso grafico di livelli per definire più modelli

Nell'API funzionale, i modelli vengono creati specificando i loro input e output in un grafico di livelli. Ciò significa che un singolo grafico di livelli può essere utilizzato per generare più modelli.

Nel seguente esempio, si utilizza la stessa pila di strati istanziare due modelli: un encoder modello che input immagine assume vettori 16-dimensionale, e un end-to-end autoencoder modello per la formazione.

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
_________________________________________________________________

Qui, l'architettura di decodifica è strettamente simmetrica all'architettura di codifica, quindi la forma di output è la stessa della forma di input (28, 28, 1) .

Il rovescio di un livello Conv2D è un livello Conv2DTranspose e il rovescio di un livello MaxPooling2D è un livello UpSampling2D .

Tutti i modelli sono richiamabili, proprio come i livelli

Puoi trattare qualsiasi modello come se fosse un layer richiamandolo su un Input o sull'output di un altro layer. Chiamando un modello non stai solo riutilizzando l'architettura del modello, ma stai anche riutilizzando i suoi pesi.

Per vederlo in azione, ecco una versione diversa dell'esempio di autoencoder che crea un modello di codificatore, un modello di decodificatore e li concatena in due chiamate per ottenere il modello di 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
_________________________________________________________________

Come puoi vedere, il modello può essere annidato: un modello può contenere sottomodelli (poiché un modello è proprio come un livello). Un caso d'uso comune per l'annidamento del modello è l' assemblaggio . Ad esempio, ecco come assemblare una serie di modelli in un unico modello che calcola la media delle loro previsioni:

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)

Manipola topologie di grafi complessi

Modelli con più ingressi e uscite

L'API funzionale semplifica la manipolazione di più input e output. Questo non può essere gestito con l'API Sequential .

Ad esempio, se stai creando un sistema per classificare i ticket di emissione dei clienti in base alla priorità e indirizzarli al reparto corretto, il modello avrà tre input:

  • il titolo del biglietto (input di testo),
  • il corpo del testo del ticket (input di testo) e
  • eventuali tag aggiunti dall'utente (input categoriale)

Questo modello avrà due uscite:

  • il punteggio di priorità tra 0 e 1 (output sigmoideo scalare) e
  • il reparto che dovrebbe gestire il ticket (output softmax sull'insieme di reparti).

Puoi costruire questo modello in poche righe con l'API funzionale:

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

Ora traccia il modello:

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

png

Quando si compila questo modello, è possibile assegnare diverse perdite a ciascun output. Puoi persino assegnare pesi diversi a ciascuna perdita, per modulare il loro contributo alla perdita totale dell'allenamento.

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

Poiché i livelli di output hanno nomi diversi, puoi anche specificare la perdita in questo modo:

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

Addestra il modello passando elenchi di array NumPy di ​​input e target:

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

Quando si chiama fit con un oggetto Dataset , dovrebbe produrre una tupla di elenchi come ([title_data, body_data, tags_data], [priority_targets, dept_targets]) o una tupla di dizionari come ({'title': title_data, 'body': body_data, 'tags': tags_data}, {'priority': priority_targets, 'department': dept_targets}) .

Per una spiegazione più dettagliata, fare riferimento alla guida alla formazione e alla valutazione .

Un modello ResNet giocattolo

Oltre ai modelli con più input e output, l'API funzionale semplifica la manipolazione delle topologie di connettività non lineare: si tratta di modelli con livelli che non sono collegati sequenzialmente, che l'API Sequential non è in grado di gestire.

Un caso d'uso comune per questo è connessioni residue. Costruiamo un modello ResNet giocattolo per CIFAR10 per dimostrarlo:

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
__________________________________________________________________________________________________

Traccia il modello:

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

png

Ora addestra il modello:

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

Livelli condivisi

Un altro buon utilizzo per l'API funzionale sono i modelli che utilizzano livelli condivisi . I livelli condivisi sono istanze di livelli che vengono riutilizzate più volte nello stesso modello: apprendono le caratteristiche che corrispondono a più percorsi nel grafico dei livelli.

I livelli condivisi vengono spesso utilizzati per codificare input da spazi simili (ad esempio, due diverse parti di testo che presentano un vocabolario simile). Consentono la condivisione delle informazioni tra questi diversi input e rendono possibile l'addestramento di un modello di questo tipo su meno dati. Se una determinata parola viene visualizzata in uno degli input, ne trarrà vantaggio l'elaborazione di tutti gli input che passano attraverso il livello condiviso.

Per condividere un livello nell'API funzionale, chiamare più volte la stessa istanza di livello. Ad esempio, ecco un livello di Embedding condiviso tra due diversi input di testo:

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

Estrai e riutilizza i nodi nel grafico dei livelli

Poiché il grafico dei livelli che stai manipolando è una struttura dati statica, è possibile accedervi e ispezionarlo. Ed è così che puoi tracciare modelli funzionali come immagini.

Ciò significa anche che puoi accedere alle attivazioni dei livelli intermedi ("nodi" nel grafico) e riutilizzarli altrove, il che è molto utile per qualcosa come l'estrazione di funzionalità.

Diamo un'occhiata a un esempio. Questo è un modello VGG19 con pesi pre-addestrati su 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

E queste sono le attivazioni intermedie del modello, ottenute interrogando la struttura dati del grafico:

features_list = [layer.output for layer in vgg19.layers]

Utilizzare queste funzionalità per creare un nuovo modello di estrazione delle caratteristiche che restituisce i valori delle attivazioni del livello intermedio:

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)

Questo è utile per attività come il trasferimento in stile neurale , tra le altre cose.

Estendi l'API utilizzando livelli personalizzati

tf.keras include una vasta gamma di livelli incorporati, ad esempio:

  • Livelli convoluzionali: Conv1D , Conv2D , Conv3D , Conv2DTranspose
  • Livelli di MaxPooling1D : MaxPooling1D , MaxPooling2D , MaxPooling3D , AveragePooling1D
  • Livelli RNN: GRU , LSTM , ConvLSTM2D
  • BatchNormalization , Dropout , Embedding , ecc.

Ma se non trovi quello che ti serve, è facile estendere l'API creando i tuoi livelli. Tutti i livelli sottoclassano la classe Layer e implementano:

  • call metodo, che specifica il calcolo eseguito dal layer.
  • build , che crea i pesi del livello (questa è solo una convenzione di stile poiché puoi creare pesi anche in __init__ ).

Per ulteriori informazioni sulla creazione di livelli da zero, leggi la guida ai modelli e ai livelli personalizzati .

Quella che segue è un'implementazione di base di 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)

Per il supporto della serializzazione nel livello personalizzato, definire un metodo get_config che restituisca gli argomenti del costruttore dell'istanza del livello:

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

Facoltativamente, implementare il metodo di classe from_config(cls, config) che viene utilizzato quando si ricrea un'istanza di livello dato il suo dizionario di configurazione. L'implementazione predefinita di from_config è:

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

Quando utilizzare l'API funzionale

Dovresti usare l'API funzionale di Keras per creare un nuovo modello o semplicemente sottoclassare direttamente la classe Model ? In generale, l'API funzionale è di livello superiore, più facile e più sicura e ha una serie di funzionalità che i modelli sottoclasse non supportano.

Tuttavia, la sottoclasse del modello fornisce una maggiore flessibilità quando si costruiscono modelli che non sono facilmente esprimibili come grafici aciclici diretti di strati. Ad esempio, non è possibile implementare un Tree-RNN con l'API funzionale e si dovrebbe creare una sottoclasse diretta del Model .

Per un'analisi approfondita delle differenze tra l'API funzionale e la sottoclasse del modello, leggi Cosa sono le API simboliche e imperative in TensorFlow 2.0? .

Punti di forza dell'API funzionale:

Le seguenti proprietà sono vere anche per i modelli sequenziali (che sono anche strutture di dati), ma non sono vere per i modelli sottoclasse (che sono bytecode Python, non strutture di dati).

Meno prolisso

Non c'è super(MyClass, self).__init__(...) , no def call(self, ...): ecc.

Confrontare:

inputs = keras.Input(shape=(32,))
x = layers.Dense(64, activation='relu')(inputs)
outputs = layers.Dense(10)(x)
mlp = keras.Model(inputs, outputs)

Con la versione sottoclasse:

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

Validazione del modello durante la definizione del suo grafico di connettività

Nell'API funzionale, la specifica di input (forma e dtype) viene creata in anticipo (utilizzando Input ). Ogni volta che chiami un livello, il livello verifica che la specifica passata corrisponda ai suoi presupposti e in caso contrario solleverà un utile messaggio di errore.

Ciò garantisce l'esecuzione di qualsiasi modello che è possibile creare con l'API funzionale. Tutto il debug, diverso da quello relativo alla convergenza, avviene staticamente durante la costruzione del modello e non al momento dell'esecuzione. Questo è simile al controllo del tipo in un compilatore.

Un modello funzionale è tracciabile e ispezionabile

Puoi tracciare il modello come un grafico e puoi accedere facilmente ai nodi intermedi in questo grafico. Ad esempio, per estrarre e riutilizzare le attivazioni di strati intermedi (come visto in un esempio precedente):

features_list = [layer.output for layer in vgg19.layers]
feat_extraction_model = keras.Model(inputs=vgg19.input, outputs=features_list)

Un modello funzionale può essere serializzato o clonato

Poiché un modello funzionale è una struttura dati piuttosto che un pezzo di codice, è serializzabile in modo sicuro e può essere salvato come un singolo file che consente di ricreare lo stesso identico modello senza avere accesso a nessuno dei codici originali. Consulta la guida alla serializzazione e al salvataggio .

Per serializzare un modello sottoclasse, è necessario che l'implementatore specifichi un get_config() e from_config() a livello di modello.

Debolezza dell'API funzionale:

Non supporta architetture dinamiche

L'API funzionale tratta i modelli come DAG di livelli. Questo è vero per la maggior parte delle architetture di apprendimento profondo, ma non per tutte: ad esempio, le reti ricorsive o gli RNN ad albero non seguono questo presupposto e non possono essere implementati nell'API funzionale.

Stili API combinabili

La scelta tra l'API funzionale o la sottoclasse del modello non è una decisione binaria che ti limita a una categoria di modelli. Tutti i modelli nell'API tf.keras possono interagire tra loro, indipendentemente dal fatto che si tratti di modelli Sequential , modelli funzionali o modelli di sottoclassi scritti da zero.

È sempre possibile utilizzare un modello funzionale o un modello Sequential come parte di un modello o livello sottoclasse:

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)

È possibile utilizzare qualsiasi livello o modello sottoclasse nell'API funzionale purché implementi un metodo di call che segue uno dei seguenti modelli:

  • call(self, inputs, **kwargs) - Dove inputs è un tensore o una struttura annidata di tensori (ad esempio un elenco di tensori) e dove **kwargs sono argomenti non tensoriali (non input).
  • call(self, inputs, training=None, **kwargs) - Dove training è un valore booleano che indica se il layer deve comportarsi in modalità training e modalità inferenza.
  • call(self, inputs, mask=None, **kwargs) - Dove mask è un tensore maschera booleano (utile per RNN, ad esempio).
  • call(self, inputs, training=None, mask=None, **kwargs) - Ovviamente, puoi avere sia il mascheramento che il comportamento specifico dell'allenamento allo stesso tempo.

Inoltre, se implementi il ​​metodo get_config sul tuo Layer o modello personalizzato, i modelli funzionali che crei saranno comunque serializzabili e clonabili.

Ecco un rapido esempio di un RNN personalizzato, scritto da zero, utilizzato in un modello funzionale:

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