A API Funcional

Ver no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno

Configurar

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

Introdução

A API funcional Keras é uma maneira de criar modelos que são mais flexíveis do que o tf.keras.Sequential API. A API funcional pode manipular modelos com topologia não linear, camadas compartilhadas e até mesmo várias entradas ou saídas.

A ideia principal é que um modelo de aprendizado profundo geralmente é um gráfico acíclico direcionado (DAG) de camadas. Portanto, a API funcional é uma maneira de construir gráficos de camadas.

Considere o seguinte modelo:

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

Este é um gráfico básico com três camadas. Para construir este modelo usando a API funcional, comece criando um nó de entrada:

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

A forma dos dados é definida como um vetor de 784 dimensões. O tamanho do lote é sempre omitido, pois apenas a forma de cada amostra é especificada.

Se, por exemplo, você tem uma entrada de imagem com uma forma de (32, 32, 3) , você usaria:

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

As inputs que é retornado contém informações sobre a forma e dtype dos dados de entrada que você alimenta a seu modelo. Esta é a forma:

inputs.shape
TensorShape([None, 784])

Aqui está o dtype:

inputs.dtype
tf.float32

Você cria um novo nó no gráfico de camadas chamando uma camada sobre este inputs objeto:

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

A ação de "chamada de camada" é como desenhar uma seta de "entradas" para essa camada que você criou. Você está "passando" as entradas para a dense camada, e você terá x como a saída.

Vamos adicionar mais algumas camadas ao gráfico de camadas:

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

Neste ponto, você pode criar um Model especificando suas entradas e saídas no gráfico de camadas:

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

Vamos verificar a aparência do resumo do modelo:

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
_________________________________________________________________

Você também pode plotar o modelo como um gráfico:

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

png

E, opcionalmente, exiba as formas de entrada e saída de cada camada no gráfico traçado:

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

png

Esta figura e o código são quase idênticos. Na versão do código, as setas de conexão são substituídas pela operação de chamada.

Um "gráfico de camadas" é uma imagem mental intuitiva para um modelo de aprendizado profundo, e a API funcional é uma maneira de criar modelos que espelham isso de perto.

Treinamento, avaliação e inferência

Formação, avaliação e trabalho inferência exatamente da mesma forma para os modelos construídos utilizando a API funcional como para Sequential modelos.

Os Model ofertas de classe um built-in circuito de treinamento (o fit() método) e um built-in circuito de avaliação (a evaluate() método). Nota que você pode facilmente personalizar estes laços para implementar rotinas de treinamento além do aprendizado supervisionado (por exemplo GAN ).

Aqui, carregue os dados da imagem MNIST, remodele-os em vetores, ajuste o modelo nos dados (enquanto monitora o desempenho em uma divisão de validação) e avalie o modelo nos dados de teste:

(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

Para ler mais, veja a formação e avaliação guia.

Salvar e serializar

Salvar o modelo e serialização de trabalho da mesma forma para os modelos construídos utilizando a API funcional como eles fazem para Sequential modelos. A forma padrão para salvar um modelo funcional é chamar model.save() para salvar o modelo inteiro como um único arquivo. Posteriormente, você pode recriar o mesmo modelo a partir desse arquivo, mesmo se o código que construiu o modelo não estiver mais disponível.

Este arquivo salvo inclui:

  • arquitetura modelo
  • valores de peso do modelo (que foram aprendidos durante o treinamento)
  • modelo de configuração de treinamento, se for o caso (como passou a compile )
  • otimizador e seu estado, se houver (para reiniciar o treinamento de onde você parou)
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

Para mais detalhes, leia o modelo de serialização e salvando guia.

Use o mesmo gráfico de camadas para definir vários modelos

Na API funcional, os modelos são criados especificando suas entradas e saídas em um gráfico de camadas. Isso significa que um único gráfico de camadas pode ser usado para gerar vários modelos.

No exemplo abaixo, você usar a mesma pilha de camadas para instanciar dois modelos: um encoder modelo que entradas imagem se transforma em vetores 16-dimensionais, e um fim-de-final autoencoder modelo para treinamento.

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
_________________________________________________________________

Aqui, a arquitectura de descodificação é estritamente simétrica para a arquitectura de codificação, de modo que a forma de saída é a mesma que a forma de entrada (28, 28, 1) .

O inverso de um Conv2D camada é um Conv2DTranspose camada, e o inverso de uma MaxPooling2D camada é uma UpSampling2D camada.

Todos os modelos podem ser chamados, assim como as camadas

Você pode tratar qualquer modelo como se fosse uma camada invocando-o em uma Input ou na saída de uma outra camada. Ao chamar um modelo, você não está apenas reutilizando a arquitetura do modelo, mas também reutilizando seus pesos.

Para ver isso em ação, aqui está uma visão diferente do exemplo do autoencoder que cria um modelo de codificador, um modelo de decodificador e os encadeia em duas chamadas para obter o modelo do 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
_________________________________________________________________

Como você pode ver, o modelo pode ser aninhado: um modelo pode conter submodelos (já que um modelo é como uma camada). Um caso de uso comum para o modelo de assentamento é ensembling. Por exemplo, veja como agrupar um conjunto de modelos em um único modelo que calcula a média de suas previsões:

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)

Manipular topologias complexas de grafos

Modelos com múltiplas entradas e saídas

A API funcional facilita a manipulação de várias entradas e saídas. Isso não pode ser tratado com o Sequential API.

Por exemplo, se você estiver construindo um sistema para classificar tíquetes de problemas do cliente por prioridade e encaminhá-los para o departamento correto, o modelo terá três entradas:

  • o título do tíquete (entrada de texto),
  • o corpo do texto do tíquete (entrada de texto), e
  • quaisquer tags adicionadas pelo usuário (entrada categórica)

Este modelo terá duas saídas:

  • a pontuação de prioridade entre 0 e 1 (saída sigmóide escalar), e
  • o departamento que deve lidar com o ticket (saída do softmax sobre o conjunto de departamentos).

Você pode construir este modelo em algumas linhas com a API funcional:

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

Agora plote o modelo:

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

png

Ao compilar este modelo, você pode atribuir diferentes perdas a cada saída. Você pode até atribuir pesos diferentes para cada perda - para modular sua contribuição para a perda total de treinamento.

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

Como as camadas de saída têm nomes diferentes, você também pode especificar as perdas e pesos de perda com os nomes das camadas correspondentes:

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

Treine o modelo passando listas de matrizes NumPy de entradas e alvos:

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

Ao chamar ajuste com um Dataset objeto, que deve produzir qualquer um tuplo de listas como ([title_data, body_data, tags_data], [priority_targets, dept_targets]) ou um tuplo de dicionários como ({'title': title_data, 'body': body_data, 'tags': tags_data}, {'priority': priority_targets, 'department': dept_targets}) .

Para uma explicação mais detalhada, consulte a formação e avaliação guia.

Um modelo de brinquedo ResNet

Além dos modelos com várias entradas e saídas, a API funcional faz com que seja fácil de manipular topologias de conectividade não lineares - estes são modelos com camadas que não são ligados sequencialmente, o que Sequential API não pode manipular.

Um caso de uso comum para isso são as conexões residuais. Vamos construir um modelo de ResNet de brinquedo para CIFAR10 para demonstrar isso:

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
__________________________________________________________________________________________________

Trace o modelo:

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

png

Agora treine o modelo:

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

Camadas compartilhadas

Outro bom uso para a API funcional são modelos que usam camadas compartilhadas. Camadas compartilhadas são instâncias de camada que são reutilizadas várias vezes no mesmo modelo - elas aprendem recursos que correspondem a vários caminhos no gráfico de camadas.

As camadas compartilhadas costumam ser usadas para codificar entradas de espaços semelhantes (digamos, duas partes diferentes de texto que apresentam vocabulário semelhante). Eles permitem o compartilhamento de informações entre essas diferentes entradas e tornam possível treinar esse modelo com menos dados. Se uma determinada palavra for vista em uma das entradas, isso beneficiará o processamento de todas as entradas que passam pela camada compartilhada.

Para compartilhar uma camada na API funcional, chame a mesma instância de camada várias vezes. Por exemplo, aqui está um Embedding camada compartilhada entre duas entradas de texto diferentes:

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

Extraia e reutilize nós no gráfico de camadas

Como o gráfico de camadas que você está manipulando é uma estrutura de dados estática, ele pode ser acessado e inspecionado. E é assim que você consegue plotar modelos funcionais como imagens.

Isso também significa que você pode acessar as ativações de camadas intermediárias ("nós" no gráfico) e reutilizá-las em outro lugar - o que é muito útil para algo como extração de recursos.

Vejamos um exemplo. Este é um modelo VGG19 com pesos pré-treinados no 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 essas são as ativações intermediárias do modelo, obtidas através da consulta à estrutura de dados do gráfico:

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

Use esses recursos para criar um novo modelo de extração de recursos que retorna os valores das ativações da camada intermediária:

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)

Isto vem a calhar para tarefas como transferência de estilo neural , entre outras coisas.

Estenda a API usando camadas personalizadas

tf.keras inclui uma vasta gama de incorporada em camadas, por exemplo:

  • Camadas convolucionais: Conv1D , Conv2D , Conv3D , Conv2DTranspose
  • Camadas agrupamento: MaxPooling1D , MaxPooling2D , MaxPooling3D , AveragePooling1D
  • Camadas RNN: GRU , LSTM , ConvLSTM2D
  • BatchNormalization , Dropout , Embedding , etc.

Mas se você não encontrar o que precisa, é fácil estender a API criando suas próprias camadas. Todas as camadas subclasse o Layer classe e implementar:

  • call método, que especifica o cálculo feito pela camada.
  • build método, que cria os pesos da camada (isto é apenas uma convenção estilo desde que você pode criar pesos em __init__ , também).

Para saber mais sobre a criação de camadas a partir do zero, leia camadas personalizados e modelos de guia.

O seguinte é uma implementação básica 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)

Para suporte a serialização em sua camada de costume, definir um get_config método que retorna os argumentos do construtor da instância camada:

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

Opcionalmente, aplicar o método de classe from_config(cls, config) que é usado quando recriando uma camada exemplo, dada a sua configuração dicionário. A implementação padrão de from_config é:

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

Quando usar a API funcional

Se você usar a API funcional Keras para criar um novo modelo, ou apenas subclasse o Model classe diretamente? Em geral, a API funcional é de nível superior, mais fácil e segura, e tem uma série de recursos que os modelos de subclasse não suportam.

No entanto, a subclasse de modelo fornece maior flexibilidade ao construir modelos que não são facilmente expressos como gráficos acíclicos direcionados de camadas. Por exemplo, você não pode implementar uma árvore-RNN com a API funcional e teria a subclasse Model diretamente.

Para um olhar em profundidade as diferenças entre a API funcional e modelo de subclasses, leia Quais são APIs simbólicos e imperativa em TensorFlow 2.0? .

Pontos fortes da API funcional:

As propriedades a seguir também são verdadeiras para modelos sequenciais (que também são estruturas de dados), mas não são verdadeiras para modelos de subclasse (que são bytecode Python, não estruturas de dados).

Menos prolixo

Não há super(MyClass, self).__init__(...) , não def call(self, ...): , etc.

Comparar:

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

Com a versão com subclasse:

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

Validação do modelo ao definir seu gráfico de conectividade

Na API funcional, a especificação de entrada (forma e dtipo) é criado previamente (usando Input ). Cada vez que você chama uma camada, a camada verifica se a especificação passada a ela corresponde às suas suposições e, caso contrário, gerará uma mensagem de erro útil.

Isso garante que qualquer modelo que você possa construir com a API funcional será executado. Toda depuração - exceto depuração relacionada à convergência - acontece estaticamente durante a construção do modelo e não no tempo de execução. Isso é semelhante à verificação de tipo em um compilador.

Um modelo funcional é plotável e inspecionável

Você pode plotar o modelo como um gráfico e pode acessar facilmente os nós intermediários neste gráfico. Por exemplo, para extrair e reutilizar as ativações de camadas intermediárias (como visto em um exemplo anterior):

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

Um modelo funcional pode ser serializado ou clonado

Como um modelo funcional é uma estrutura de dados em vez de um pedaço de código, ele é serializável com segurança e pode ser salvo como um único arquivo que permite recriar exatamente o mesmo modelo sem ter acesso a nenhum código original. Veja a serialização e salvando guia .

Para serializar um modelo subclasse, é necessário que o implementador para especificar um get_config() e from_config() método no nível do modelo.

Fraqueza da API funcional:

Não suporta arquiteturas dinâmicas

A API funcional trata os modelos como DAGs de camadas. Isso é verdadeiro para a maioria das arquiteturas de aprendizado profundo, mas não para todas - por exemplo, redes recursivas ou RNNs de árvore não seguem essa suposição e não podem ser implementadas na API funcional.

Estilos de API combinados

A escolha entre API funcional ou subclasse de modelo não é uma decisão binária que o restringe a uma categoria de modelos. Todos os modelos da tf.keras API podem interagir uns com os outros, se eles são Sequential modelos, modelos funcionais, ou modelos subclasse que são escritos a partir do zero.

Você sempre pode usar um modelo funcional ou Sequential modelo como parte de um modelo subclassed ou camada:

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)

Você pode usar qualquer camada subclasse ou modelo na API funcional, desde que implementa uma call método que segue um dos seguintes padrões:

  • call(self, inputs, **kwargs) - Onde inputs é um tensor ou uma estrutura aninhada dos tensores (por exemplo, uma lista de tensores), e onde **kwargs são argumentos não-tensores (não-entradas).
  • call(self, inputs, training=None, **kwargs) - Onde training é um booleano que indica se a camada deve comportar-se de modo de treinamento e modo de inferência.
  • call(self, inputs, mask=None, **kwargs) - Onde mask é um tensor máscara booleano (útil para RNNs, por exemplo).
  • call(self, inputs, training=None, mask=None, **kwargs) - Claro, você pode ter os dois mascaramento e comportamento específico de formação, ao mesmo tempo.

Além disso, se você implementar o get_config método em sua camada de costume ou modelo, os modelos funcionais que criar ainda será serializado e cloneable.

Aqui está um exemplo rápido de um RNN personalizado, escrito do zero, sendo usado em um modelo funcional:

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