Reserve a data! O Google I / O retorna de 18 a 20 de maio Registre-se agora
Esta página foi traduzida pela API Cloud Translation.
Switch to English

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 do Keras é uma maneira de criar modelos mais flexíveis do que a API tf.keras.Sequential . 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 forma 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ê tiver 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 retornadas contêm informações sobre a forma e o dtype dos dados de entrada que você alimenta em 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 neste objeto de inputs :

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 camada dense e obtém 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

Treinamento, avaliação e inferência funcionam exatamente da mesma maneira para modelos construídos usando a API funcional e para modelos Sequential .

A classe Model oferece um loop de treinamento integrado (o método fit() ) e um loop de avaliação integrado (o método evaluate() ). Observe que você pode personalizar facilmente esses loops para implementar rotinas de treinamento além do aprendizado supervisionado (por exemplo, GANs ).

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

Para mais informações, consulte o guia de treinamento e avaliação .

Salvar e serializar

Salvar o modelo e a serialização funcionam da mesma maneira para modelos construídos usando a API funcional e para modelos Sequential . A maneira padrão de 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 deste 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)
  • configuração de treinamento do modelo, se houver (conforme passado para 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")
INFO:tensorflow:Assets written to: path_to_my_model/assets

Para obter detalhes, leia o guia de serialização e salvamento do modelo.

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ê usa a mesma pilha de camadas para instanciar dois modelos: um modelo de encoder que transforma entradas de imagem em vetores de 16 dimensões e um modelo de autoencoder ponta a autoencoder 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 arquitetura de decodificação é estritamente simétrica à arquitetura de codificação, então a forma de saída é a mesma que a forma de entrada (28, 28, 1) .

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

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 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 aninhamento de modelo é o agrupamento . 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 a API Sequential .

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 tratar 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 a perda desta forma:

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

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 [==============================] - 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>

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

Para obter uma explicação mais detalhada, consulte o guia de treinamento e avaliação .

Um modelo de brinquedo ResNet

Além de modelos com várias entradas e saídas, a API funcional facilita a manipulação de topologias de conectividade não linear - são modelos com camadas que não são conectadas sequencialmente, que a API Sequential 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 [==============================] - 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>

Camadas compartilhadas

Outro bom uso para a API funcional são os 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á uma camada de Embedding 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 [==============================] - 7s 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)

Isso é útil para tarefas como transferência de estilo neural , entre outras coisas.

Estenda a API usando camadas personalizadas

tf.keras inclui uma ampla variedade de camadas integradas, 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 subclassificam a classe Layer e implementam:

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

Para saber mais sobre como criar camadas do zero, leia o guia de camadas e modelos personalizados .

A seguir está 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 de serialização em sua camada personalizada, defina um método get_config que retorna os argumentos do construtor da instância da 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, implemente o método de classe from_config(cls, config) que é usado ao recriar uma instância de camada com seu dicionário de configuração. A implementação padrão de from_config é:

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

Quando usar a API funcional

Você deve usar a API funcional do Keras para criar um novo modelo ou apenas criar uma subclasse da classe Model 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 poderia implementar um Tree-RNN com a API funcional e teria que criar uma subclasse de Model diretamente.

Para uma análise aprofundada das diferenças entre a API funcional e a subclasse de modelo, leia O que são APIs simbólicas e imperativas no 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__(...) , nenhuma 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 tipo de d) é criada antecipadamente (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. Consulte o guia de serialização e salvamento .

Para serializar um modelo com subclasse, é necessário que o implementador especifique um get_config() e from_config() 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 na API tf.keras podem interagir uns com os outros, sejam eles modelos Sequential , modelos funcionais ou modelos de subclasse que são escritos do zero.

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

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 ou modelo de subclasse na API funcional, desde que implemente um método de call que siga um dos seguintes padrões:

  • call(self, inputs, **kwargs) - Onde as inputs são um tensor ou uma estrutura aninhada de 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 o training é um booleano que indica se a camada deve se comportar no modo de treinamento e no modo de inferência.
  • call(self, inputs, mask=None, **kwargs) - Onde mask é um tensor de máscara booleana (útil para RNNs, por exemplo).
  • call(self, inputs, training=None, mask=None, **kwargs) - Claro, você pode ter o mascaramento e o comportamento específico do treinamento ao mesmo tempo.

Além disso, se você implementar o método get_config em sua camada ou modelo personalizado, os modelos funcionais que você criar ainda serão serializáveis ​​e clonáveis.

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