![]() | ![]() | ![]() | ![]() |
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")
E, opcionalmente, exiba as formas de entrada e saída de cada camada no gráfico plotado:
keras.utils.plot_model(model, "my_first_model_with_shape_info.png", show_shapes=True)
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 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)
- 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 é a combinação . 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 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)
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)
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.
Camadas compartilhadas são frequentemente 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 possui vários 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 bytes de 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) é 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 qualquer parte do 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 asinputs
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 otraining
é 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)
- Ondemask
é 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)))