L'API funzionale

Mantieni tutto organizzato con le raccolte Salva e classifica i contenuti in base alle tue preferenze.

Visualizza su TensorFlow.org Esegui in Google Colab Visualizza l'origine su GitHub Scarica quaderno

Impostare

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

introduzione

L'API funzionale Keras è un modo per creare modelli che sono più flessibili rispetto al tf.keras.Sequential API. 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 deep learning sia solitamente un grafo aciclico diretto (DAG) di strati. Così l'API funzionale è un modo per costruire grafici di strati.

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 creare 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, per esempio, si ha un input un'immagine con una forma di (32, 32, 3) , si usa:

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

Gli inputs che viene restituito contiene informazioni sulla forma e dtype dei dati di ingresso che si posiziona al modello. Ecco la forma:

inputs.shape
TensorShape([None, 784])

Ecco il tipo d:

inputs.dtype
tf.float32

Si crea un nuovo nodo nel grafico di strati chiamando uno strato su questa inputs oggetto:

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

L'azione "chiamata livello" è come disegnare una freccia da "input" a questo livello che hai creato. Si è "passare" gli ingressi al dense strato, e si ottiene x come l'uscita.

Aggiungiamo altri livelli al grafico dei livelli:

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

A questo punto, è possibile creare un Model specificando gli ingressi e le uscite nel grafico di strati:

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

Diamo un'occhiata a 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 un grafico:

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

png

E, facoltativamente, visualizzare le forme di input e output di ciascun 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 a codice, le frecce di connessione sono sostituite dall'operazione di chiamata.

Un "grafico dei 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

Formazione, valutazione, e il lavoro di inferenza esattamente nello stesso modo per i modelli costruiti utilizzando l'API funzionale per Sequential modelli.

Le Model offerte di classe dotato di un ciclo di formazione (il fit() metodo) e un built-in loop di valutazione (la evaluate() metodo). Nota che si può facilmente personalizzare questi cicli di implementare le routine di allenamento al di là di apprendimento supervisionato (ad es Gans ).

Qui, carica i dati dell'immagine MNIST, rimodellali in vettori, adatta il modello ai dati (monitorando le prestazioni su una divisione di convalida), quindi valuta 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.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

Per approfondimenti, consultare la formazione e valutazione di guida.

Salva e serializza

Salvare il modello e la serializzazione di lavoro allo stesso modo per i modelli costruiti utilizzando l'API funzionale come fanno per Sequential modelli. Il metodo standard per salvare un modello funzionale è quello di chiamare model.save() per salvare l'intero modello come un singolo file. In seguito puoi 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 (appresi durante l'allenamento)
  • modello di formazione config, se del caso (come passato alla compile )
  • ottimizzatore e il suo stato, se presente (per riprendere l'allenamento da dove eri rimasto)
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

Per ulteriori informazioni, leggere il modello di serializzazione e il salvataggio manuale.

Usa lo stesso grafico dei 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 decodifica è rigorosamente simmetrico all'architettura codifica, così la forma di uscita è uguale alla forma di ingresso (28, 28, 1) .

L'inverso di un Conv2D strato è un Conv2DTranspose strato, e l'inverso di un MaxPooling2D strato è un UpSampling2D strato.

Tutti i modelli sono richiamabili, proprio come i livelli

Si può trattare qualsiasi modello come se fosse uno strato invocando su di un Input o in uscita di un altro livello. Chiamando un modello non stai solo riutilizzando l'architettura del modello, stai anche riutilizzando i suoi pesi.

Per vedere questo 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 codifica automatica:

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 nidificato: un modello può contenere dei sottomodelli (poiché un modello è proprio come un livello). Un caso di uso comune per il modello di nidificazione è classificatori binari. Ad esempio, ecco come riunire un insieme 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)

Manipolare topologie di grafi complesse

Modelli con più ingressi e uscite

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

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 (testo inserito),
  • 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à compreso tra 0 e 1 (uscita sigmoidea scalare) e
  • il dipartimento che dovrebbe gestire il ticket (output softmax sull'insieme dei dipartimenti).

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 perdite diverse a ciascuna uscita. Puoi anche assegnare pesi diversi a ciascuna perdita, per modulare il loro contributo alla perdita totale di 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 le perdite e i pesi delle perdite con i nomi dei livelli corrispondenti:

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

Addestra il modello passando elenchi di matrici 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 [==============================] - 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>

Quando si chiama in forma con un Dataset oggetto, dovrebbe produrre come una tupla di liste 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 formazione e valutazione di guida.

Un modello ResNet giocattolo

Oltre ai modelli con più ingressi e uscite, l'API funzionale rende facile manipolare topologie connettività non lineari - questi sono modelli con strati che non sono collegati in sequenza, che il Sequential API non può gestire.

Un caso d'uso comune per questo sono le 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 allena 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 [==============================] - 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>

Livelli condivisi

Un altro uso buono per l'API funzionale sono modelli che utilizzano livelli condivisi. I livelli condivisi sono istanze di livello che vengono riutilizzate più volte nello stesso modello: apprendono funzionalità che corrispondono a più percorsi nel grafico dei livelli.

I livelli condivisi vengono spesso utilizzati per codificare gli input da spazi simili (ad esempio, due diverse parti di testo che presentano un vocabolario simile). Consentono la condivisione di informazioni tra questi diversi input e consentono di addestrare un tale modello su meno dati. Se una determinata parola viene vista in uno degli input, ciò andrà a vantaggio dell'elaborazione di tutti gli input che passano attraverso il livello condiviso.

Per condividere un livello nell'API funzionale, chiama più volte la stessa istanza del livello. Ad esempio, ecco un Embedding strato condiviso tra due ingressi di testo differenti:

# 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 di dati statica, è possibile accedervi e controllarlo. 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 preaddestrati 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 [==============================] - 15s 0us/step
574726144/574710816 [==============================] - 15s 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 funzionalità che restituisca 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 compiti come il trasferimento stile neurale , tra le altre cose.

Estendi l'API utilizzando livelli personalizzati

tf.keras comprende una vasta gamma di built-in strati, ad esempio:

  • Strati convoluzionali: Conv1D , Conv2D , Conv3D , Conv2DTranspose
  • Strati pooling: MaxPooling1D , MaxPooling2D , MaxPooling3D , AveragePooling1D
  • Strati RNN: GRU , LSTM , ConvLSTM2D
  • BatchNormalization , Dropout , Embedding , etc.

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

  • call metodo, che specifica il calcolo svolto dallo strato.
  • build il metodo, che crea i pesi dello strato (questa è solo una convenzione stile dal momento che è possibile creare i pesi in __init__ , pure).

Per ulteriori informazioni sulla creazione di livelli da zero, leggere livelli personalizzati e modelli di guida.

Quanto 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 di serializzazione nel vostro livello personalizzato, definire un get_config metodo che restituisce gli argomenti del costruttore dell'istanza 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 della classe from_config(cls, config) che viene utilizzato quando ricreare un'istanza strato data la sua configurazione dizionario. L'implementazione predefinita di from_config è:

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

Quando utilizzare l'API funzionale

Si dovrebbe utilizzare l'API funzionale Keras per creare un nuovo modello, o semplicemente sottoclasse il Model direttamente classe? In generale, l'API funzionale è di livello superiore, più semplice e più sicura e presenta una serie di funzionalità che i modelli di sottoclassi non supportano.

Tuttavia, la sottoclasse del modello offre una maggiore flessibilità durante la creazione di modelli che non sono facilmente esprimibili come grafici aciclici diretti di livelli. Ad esempio, non si poteva realizzare un albero-RNN con l'API funzionale e avrebbe dovuto creare una sottoclasse Model direttamente.

Per uno sguardo approfondito le differenze tra le API funzionale e modello di creazione di sottoclassi, leggere Quali sono le API simbolici e imperativo tensorflow 2.0? .

Punti di forza dell'API funzionale:

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

Meno prolisso

Non v'è alcun super(MyClass, self).__init__(...) , non def call(self, ...): , etc.

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

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

Nel API funzionale, specifiche di ingresso (forma e dtype) è creato in anticipo (utilizzando Input ). Ogni volta che si chiama un livello, il livello controlla che la specifica passata ad esso corrisponda ai suoi presupposti e in caso contrario genererà un utile messaggio di errore.

Ciò garantisce l'esecuzione di qualsiasi modello che puoi creare con l'API funzionale. Tutto il debug, diverso dal debug relativo alla convergenza, avviene in modo statico 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 degli 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 di dati piuttosto che un pezzo di codice, è serializzabile in modo sicuro e può essere salvato come un unico file che consente di ricreare lo stesso identico modello senza avere accesso al codice originale. Consultare la guida di serializzazione e il risparmio .

Per serializzare un modello sottoclasse, è necessario che il realizzatore di specificare un get_config() e from_config() il metodo a livello di modello.

Punto debole 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 deep learning, 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.

Combina stili API

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 della tf.keras API possono interagire con l'altro, se sono Sequential modelli, modelli funzionali, o modelli sottoclasse che sono scritte da zero.

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

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 strato sottoclasse o modello nella API funzionale fintanto che implementa una call metodo che segue uno dei seguenti schemi:

  • call(self, inputs, **kwargs) - Dove inputs è un tensore o una struttura annidata dei tensori (ad esempio, un elenco dei tensori), e dove **kwargs sono argomenti non tensoriali (non input).
  • call(self, inputs, training=None, **kwargs) - dove training è un valore booleano che indica se lo strato deve comportarsi in modalità allenamento e la modalità di inferenza.
  • call(self, inputs, mask=None, **kwargs) - Dove mask è una maschera tensore booleana (utile per RNR, per esempio).
  • call(self, inputs, training=None, mask=None, **kwargs) - Naturalmente, si possono avere entrambe mascheratura e comportamenti specifici di formazione allo stesso tempo.

Inoltre, se si implementa il get_config metodo sul vostro livello personalizzato o modello, i modelli funzionali creati saranno ancora serializzabile e clonabile.

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