Esta página foi traduzida pela API Cloud Translation.
Switch to English

A API Funcional

Ver em TensorFlow.org Executar no Google Colab Ver fonte no GitHub Download do caderno

Configuração

 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 mais flexíveis que a API tf.keras.Sequential . A API funcional pode manipular modelos com topologia não linear, modelos com camadas compartilhadas e modelos com várias entradas ou saídas.

A idéia principal de que um modelo de aprendizado profundo é geralmente um gráfico acíclico direcionado (DAG) de camadas. Portanto, a API funcional é uma maneira de criar 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 esse modelo usando a API funcional, comece criando um nó de entrada:

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

A forma dos dados é definida como um vetor 784-dimensional. O tamanho do lote é sempre omitido, pois somente a forma de cada amostra é especificada.

Se, por exemplo, você tiver uma entrada de imagem com um formato de (32, 32, 3) , use:

 # 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 para o seu modelo. Aqui está 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 "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 .

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

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

Nesse 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 como é o 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 plotado:

 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 refletem isso de perto.

Treinamento, avaliação e inferência

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

Aqui, carregue os dados da imagem MNIST, reformule-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 [==============================] - 2s 3ms/step - loss: 0.3458 - accuracy: 0.9013 - val_loss: 0.1860 - val_accuracy: 0.9463
Epoch 2/2
750/750 [==============================] - 2s 2ms/step - loss: 0.1588 - accuracy: 0.9524 - val_loss: 0.1242 - val_accuracy: 0.9645
313/313 - 1s - loss: 0.1297 - accuracy: 0.9604
Test loss: 0.12967276573181152
Test accuracy: 0.9603999853134155

Para leitura adicional, consulte o guia de treinamento e avaliação .

Salvar e serializar

O salvamento do modelo e da serialização funciona da mesma maneira para os modelos criados usando a API funcional, assim como para os 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 nesse arquivo, mesmo que o código que criou o modelo não esteja mais disponível.

Este arquivo salvo inclui:

  • arquitetura modelo
  • modelar valores de peso (que foram aprendidos durante o treinamento)
  • configuração de treinamento do modelo, se houver (como 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")
 
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/ops/resource_variable_ops.py:1817: calling BaseResourceVariable.__init__ (from tensorflow.python.ops.resource_variable_ops) with constraint is deprecated and will be removed in a future version.
Instructions for updating:
If using Keras pass *_constraint arguments to layers.
INFO:tensorflow:Assets written to: path_to_my_model/assets

Para detalhes, leia o guia de serialização e economia de 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 16-dimensionais 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; portanto, o formato de saída é o mesmo que o formato 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 são 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 encadeia-os em duas chamadas para obter o modelo de 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 (Model)              (None, 16)                18672     
_________________________________________________________________
decoder (Model)              (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 é exatamente como uma camada). Um caso de uso comum para aninhamento de modelo é conjunto . 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 gráficos

Modelos com várias 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 tickets de emissão personalizados por prioridade e encaminhá-los para o departamento correto, o modelo terá três entradas:

  • o título do ticket (entrada de texto),
  • o corpo do texto do ticket (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 softmax sobre o conjunto de departamentos).

Você pode criar esse 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 esse modelo, você pode atribuir perdas diferentes para 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 assim:

 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 destinos:

 # 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 [==============================] - 0s 12ms/step - loss: 1.3372 - priority_loss: 0.7055 - department_loss: 3.1586
Epoch 2/2
40/40 [==============================] - 0s 11ms/step - loss: 1.3285 - priority_loss: 0.6998 - department_loss: 3.1438

<tensorflow.python.keras.callbacks.History at 0x7f6ad8133160>

Ao chamar fit com um objeto Dataset , ele deve gerar 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 uma explicação mais detalhada, consulte o guia de treinamento e avaliação .

Um modelo ResNet de brinquedo

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 - esses são modelos com camadas que não estão conectadas sequencialmente. Algo que a API Sequential não pode lidar.

Um caso de uso comum para isso são as conexões residuais. Vamos construir um modelo ResNet de brinquedo para o 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
__________________________________________________________________________________________________

Traçar 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 [==============================] - 0s 25ms/step - loss: 2.3091 - acc: 0.1037 - val_loss: 2.2991 - val_acc: 0.1300

<tensorflow.python.keras.callbacks.History at 0x7f6ac00f31d0>

Camadas compartilhadas

Outro bom uso da 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 (por exemplo, duas partes diferentes de texto que apresentam vocabulário semelhante). Eles permitem o compartilhamento de informações entre essas diferentes entradas e possibilitam treinar esse modelo em 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)
 

Extrair e reutilizar 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ê é capaz de 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 local - 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 [==============================] - 5s 0us/step

E estas são as ativações intermediárias do modelo, obtidas consultando a estrutura de dados do gráfico:

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

Use estes recursos para criar um novo modelo de extração de recursos que retorne 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 embutidas, 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 (essa é apenas uma convenção de estilo, pois você também pode criar pesos em __init__ ).

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

A seguir está uma implementação básica do 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 à serialização em sua camada personalizada, defina um método get_config que retorne 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 from_config(cls, config) que é usado ao recriar uma instância de camada, considerando seu dicionário de configuração. A implementação padrão do from_config é:

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

Quando usar a API funcional

Quando você deve usar a API funcional Keras para criar um novo modelo ou apenas subclassificar a classe Model diretamente? Em geral, a API funcional é de nível superior, mais fácil e segura e possui vários recursos aos quais os modelos de subclasse não oferecem suporte.

No entanto, a subclasse de modelo fornece maior flexibilidade ao criar modelos que não são facilmente expressáveis ​​como gráficos acíclicos direcionados de camadas. Por exemplo, você não pode implementar um Tree-RNN com a API funcional e precisaria subclassificar 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 seguintes propriedades também são verdadeiras para modelos seqüenciais (que também são estruturas de dados), mas não são verdadeiras para modelos de subclasse (que são bytecode do Python, não estruturas de dados).

Menos detalhado

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 subclassificada:

 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 de modelo ao definir seu gráfico de conectividade

Na API funcional, a especificação de entrada (shape e dtype) é criada previamente (usando Input ). Toda vez que você chama uma camada, a camada verifica se a especificação passada corresponde a suas suposições e, caso contrário, gera uma mensagem de erro útil.

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

Um modelo funcional é plotável e inspecionável

Você pode plotar o modelo como um gráfico e pode acessar facilmente 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 e não um trecho de código, é 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 economia .

Para serializar um modelo subclasse, é necessário que o implementador especifique os 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 é verdade para a maioria das arquiteturas de aprendizado profundo, mas não todas - por exemplo, redes recursivas ou RNNs da Árvore não seguem essa suposição e não podem ser implementadas na API funcional.

Estilos de API de combinação e correspondência

Escolher entre a API funcional ou a subclasse de Modelo não é uma decisão binária que restringe você a uma categoria de modelos. Todos os modelos na API tf.keras podem interagir entre si, sejam modelos Sequential , funcionais ou subclassificados, criados a partir do zero.

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

 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 inputs é 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 inputs).
  • call(self, inputs, training=None, **kwargs) - Onde o training é um booleano indicando 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 booleano (útil para RNNs, por exemplo).
  • call(self, inputs, training=None, mask=None, **kwargs) - Obviamente, você pode ter ao mesmo tempo um comportamento específico de mascaramento e de treinamento.

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 uma RNN personalizada, escrita do zero, sendo usada 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)))