
Keras possui duas APIs para criar rapidamente arquiteturas de rede neural Sequencial e Funcional. Se o primeiro permitir construir apenas arquiteturas seqüenciais de redes neurais, usando a API Funcional, você poderá definir uma rede neural na forma de um gráfico acíclico direcionado arbitrário, o que oferece muito mais oportunidades para a construção de modelos complexos. Este artigo é uma tradução do Functional API Feature Guide do site TensorFlow.
1. Introdução
A API Funcional permite criar modelos de forma mais flexível que a API Sequencial; ela pode processar modelos com topologia não linear, modelos com camadas comuns e modelos com várias entradas ou saídas.
É baseado no fato de que o modelo de aprendizado profundo é geralmente um gráfico acíclico direcionado (DAG) de camadas
API funcional é um conjunto de ferramentas para
plotagem de camadas .
Considere o seguinte modelo:
(entrada: vetor 784-dimensional)
↧
[Camada densa (64 elementos, ativação de relu)]
↧
[Camada densa (64 elementos, ativação de relu)]
↧
[Camada densa (10 elementos, ativação do softmax)]
↧
(saída: distribuição de probabilidade acima de 10 classes)
Este é um gráfico simples de 3 camadas.
Para construir esse modelo usando a API Funcional, você precisa começar criando um nó de entrada:
from tensorflow import keras inputs = keras.Input(shape=(784,))
Aqui, simplesmente indicamos a dimensão dos nossos dados: vetores 784 dimensionais. Observe que a quantidade de dados é sempre omitida; indicamos apenas a dimensão de cada elemento. Para inserir o tamanho pretendido para as imagens `(32, 32, 3)`, usaríamos:
img_inputs = keras.Input(shape=(32, 32, 3))
Quais
inputs
retornam contém informações sobre o tamanho e o tipo de dados que você planeja transferir para o seu modelo:
inputs.shape
TensorShape([None, 784])
inputs.dtype
tf.float32
Você cria um novo nó no gráfico de camadas chamando a camada neste objeto de
inputs
:
from tensorflow.keras import layers dense = layers.Dense(64, activation='relu') x = dense(inputs)
"Chamar uma camada" é semelhante a desenhar uma seta da "entrada" para a camada que criamos. Passamos a entrada para a camada
dense
e obtemos
x
.
Vamos adicionar mais algumas camadas ao nosso gráfico de camadas:
x = layers.Dense(64, activation='relu')(x) outputs = layers.Dense(10, activation='softmax')(x)
Agora podemos criar um
Model
especificando suas entradas e saídas no gráfico de camadas:
model = keras.Model(inputs=inputs, outputs=outputs)
Vejamos novamente o processo completo de definição de modelo:
inputs = keras.Input(shape=(784,), name='img') x = layers.Dense(64, activation='relu')(inputs) x = layers.Dense(64, activation='relu')(x) outputs = layers.Dense(10, activation='softmax')(x) model = keras.Model(inputs=inputs, outputs=outputs, name='mnist_model')
Vamos ver como é o resumo do modelo:
model.summary()
Model: "mnist_model" _________________________________________________________________ Layer (type) Output Shape Param
Também podemos desenhar o modelo como um gráfico:
keras.utils.plot_model(model, 'my_first_model.png')

E, opcionalmente, derivar as dimensões da entrada e saída de cada camada no gráfico construído:
keras.utils.plot_model(model, 'my_first_model_with_shape_info.png', show_shapes=True)

Esta imagem e o código que escrevemos são idênticos. Na versão do código, as setas de ligação são simplesmente substituídas por operações de chamada.
O "gráfico de camadas" é uma imagem mental muito intuitiva para o modelo de aprendizado profundo, e a API Funcional é uma maneira de criar modelos que refletem de perto essa imagem mental.
Treinamento, avaliação e conclusão
Aprender, avaliar e derivar trabalhos para modelos criados usando a API Funcional, como nos modelos Sequenciais.
Considere uma demonstração rápida.
Aqui, carregamos o conjunto de dados de imagem MNIST, convertemos em vetores, treinamos o modelo nos dados (enquanto monitoramos a qualidade do trabalho na amostra de teste) e, finalmente, avaliamos nosso 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='sparse_categorical_crossentropy', optimizer=keras.optimizers.RMSprop(), metrics=['accuracy']) history = model.fit(x_train, y_train, batch_size=64, epochs=5, 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])
Salvando e serializando
Salvar e serializar modelos criados com a API Funcional funciona exatamente da mesma forma que nos modelos Sequenciais.
A maneira padrão de salvar um modelo Funcional é chamar
model.save(
), que permite salvar o modelo inteiro em um arquivo.
Posteriormente, você pode restaurar o mesmo modelo nesse arquivo, mesmo que não tenha mais acesso ao código que criou o modelo.
Este arquivo inclui:
- Arquitetura de modelo
- Pesos do modelo (que foram obtidos durante o treinamento)
- Configuração de treinamento do modelo (o que você passou na
compile
) - O otimizador e sua condição, se fosse (isso permite que você retome o treinamento de onde parou)
model.save('path_to_my_model.h5') del model
Usando o mesmo gráfico de camada para definir vários modelos
Na API Funcional, os modelos são criados especificando dados de entrada e saída em um gráfico de camadas. Isso significa que um gráfico de camada única pode ser usado para gerar vários modelos.
No exemplo abaixo, usamos a mesma pilha de camadas para criar dois modelos:
um modelo de
(encoder)
que converte imagens de entrada 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()
Observe que tornamos a arquitetura de decodificação estritamente simétrica à arquitetura de codificação, para que a dimensão dos dados de saída seja igual à dos dados de entrada
(28, 28, 1)
. A camada
Conv2D
é
Conv2D
para a camada
Conv2D
e a camada
MaxPooling2D
será a volta para a camada
MaxPooling2D
.
Os modelos podem ser chamados como camadas
Você pode usar qualquer modelo como se fosse uma camada, chamando-o de
Input
ou de saída de outra camada.
Observe que, ao invocar um modelo, você não apenas reutiliza sua arquitetura, mas também seus pesos. Vamos vê-lo em ação. Aqui está outra olhada em um exemplo de codificador automático quando um modelo de codificador, um modelo de decodificador é criado e eles são conectados em duas chamadas para obter um modelo de codificador automático:
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()
Como você vê, um modelo pode ser aninhado: um modelo pode conter um submodelo (já que o modelo pode ser considerado como uma camada).
Um caso de uso comum para aninhar modelos é o
conjunto .
Como exemplo, veja como combinar um conjunto de modelos em um modelo que calcula a média de suas previsões:
def get_model(): inputs = keras.Input(shape=(128,)) outputs = layers.Dense(1, activation='sigmoid')(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)
Manipulando topologias complexas de gráficos
Modelos com várias entradas e saídas
A API funcional simplifica a manipulação de várias entradas e saídas. Isso não pode ser feito com a API sequencial.
Aqui está um exemplo simples.
Suponha que você esteja criando um sistema para classificar os aplicativos dos clientes por prioridade e enviá-los para o departamento certo.
Seu modelo terá 3 entradas:
- Cabeçalho do aplicativo (entrada de texto)
- Conteúdo de texto do aplicativo (entrada de texto)
- Quaisquer tags adicionadas pelo usuário (entrada categórica)
O modelo terá 2 saídas:
- Escore de prioridade entre 0 e 1 (saída sigmóide escalar)
- O departamento que deve processar o aplicativo (saída softmax referente a muitos departamentos)
Vamos construir um modelo em várias linhas usando a API Funcional.
num_tags = 12
Vamos desenhar um gráfico de modelo:
keras.utils.plot_model(model, 'multi_input_and_output_model.png', show_shapes=True)

Ao compilar esse modelo, podemos atribuir diferentes funções de perda para cada saída.
Você pode até atribuir pesos diferentes a cada função de perda para variar sua contribuição para a função geral de perda de aprendizado.
model.compile(optimizer=keras.optimizers.RMSprop(1e-3), loss=['binary_crossentropy', 'categorical_crossentropy'], loss_weights=[1., 0.2])
Como demos nomes às nossas camadas de saída, também podemos especificar funções de perda:
model.compile(optimizer=keras.optimizers.RMSprop(1e-3), loss={'priority': 'binary_crossentropy', 'department': 'categorical_crossentropy'}, loss_weights=[1., 0.2])
Podemos treinar o modelo passando listas de matrizes Numpy de dados e rótulos de entrada:
import numpy as np
Ao chamar o ajuste com um objeto
Dataset
, uma tupla de listas como
([title_data, body_data, tags_data], [priority_targets, dept_targets])
ou uma tupla de dicionários
({'title': title_data, 'body': body_data, 'tags': tags_data}, {'priority': priority_targets, 'department': dept_targets})
devem ser retornadas
({'title': title_data, 'body': body_data, 'tags': tags_data}, {'priority': priority_targets, 'department': dept_targets})
.
Modelo de resnet de treinamento
Além de modelos com várias entradas e saídas, a API Funcional simplifica a manipulação de topologias com conectividade não linear, ou seja, modelos nos quais as camadas não estão conectadas em série. Esses modelos também não podem ser implementados usando a API sequencial (como o nome indica).
Um caso de uso comum para isso são as conexões residuais.
Vamos criar um modelo de treinamento ResNet 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, activation='softmax')(x) model = keras.Model(inputs, outputs, name='toy_resnet') model.summary()
Vamos desenhar um gráfico de modelo:
keras.utils.plot_model(model, 'mini_resnet.png', show_shapes=True)

E ensine a ela:
(x_train, y_train), (x_test, y_test) = keras.datasets.cifar10.load_data() x_train = x_train.astype('float32') / 255. x_test = x_test.astype('float32') / 255. 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='categorical_crossentropy', metrics=['acc']) model.fit(x_train, y_train, batch_size=64, epochs=1, validation_split=0.2)
Compartilhamento de camada
Outro bom uso da API funcional são os modelos que usam camadas comuns. Camadas comuns são instâncias de camadas que são reutilizadas no mesmo modelo: elas estudam recursos relacionados a vários caminhos em um gráfico de camadas.
Camadas comuns são frequentemente usadas para codificar dados de entrada provenientes dos mesmos espaços (digamos, de duas partes diferentes de texto que possuem o mesmo dicionário), pois fornecem a troca de informações entre esses dados diferentes, o que permite que esses modelos sejam treinados com menos dados. Se uma determinada palavra aparecer em uma das entradas, isso facilitará seu processamento em todas as entradas que passam pelo nível geral.
Para compartilhar uma camada na API Funcional, basta chamar a mesma instância da camada várias vezes. Por exemplo, aqui a camada
Embedding
é compartilhada em duas entradas de texto:
Recuperando e Reutilizando Nós em um Gráfico de Camadas
Como o gráfico de camada que você manipula na API Funcional é uma estrutura de dados estática, é possível acessá-lo e verificá-lo. É assim que construímos modelos funcionais, por exemplo, na forma de imagens.
Isso também significa que podemos acessar as ativações das camadas intermediárias (“nós” no gráfico) e usá-las em outros lugares. Isso é extremamente útil para extrair características, por exemplo!
Vamos ver um exemplo. Este é um modelo VGG19 com escalas pré-treinadas no ImageNet:
from tensorflow.keras.applications import VGG19 vgg19 = VGG19()
E estas são ativações de modelo intermediárias obtidas consultando a estrutura de dados do gráfico:
features_list = [layer.output for layer in vgg19.layers]
Podemos usar esses recursos para criar um novo modelo de extração de recursos que retorna valores de ativação de nível intermediário - e podemos fazer tudo isso em 3 linhas
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 é conveniente ao implementar a transferência de estilo neural, como em outros casos.
Estendendo a API escrevendo camadas personalizadas
tf.keras
possui uma ampla variedade de camadas embutidas. Aqui estão alguns exemplos:
Camadas convolucionais:
Conv1D
,
Conv2D
,
Conv3D
,
Conv2DTranspose
, etc.
Camadas de
MaxPooling1D
:
MaxPooling1D
,
MaxPooling2D
,
MaxPooling3D
,
MaxPooling3D
, etc.
Camadas RNN:
GRU
,
LSTM
,
ConvLSTM2D
, etc.
BatchNormalization
,
BatchNormalization
,
Embedding
, etc.
Se você não encontrou o que precisa, é fácil estender a API criando sua própria camada.
Todas as camadas subclassificam a classe
Layer
e implementam:
O método de
call
que define os cálculos executados pela camada.
O método de
build
que cria os pesos da camada (observe que esta é apenas uma convenção de estilo; você também pode criar pesos em
__init__
).
Aqui está uma implementação simples da camada
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)
Se você deseja que sua camada customizada suporte a serialização, também deve definir o 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, você também pode implementar o método da classe
from_config (cls, config)
, responsável por recriar a instância da camada, conforme seu dicionário de configuração. A
from_config
padrão
from_config
é assim:
def from_config(cls, config): return cls(**config)
Quando usar a API Funcional
Como determinar quando é melhor usar a API Funcional para criar um novo modelo ou simplesmente subclassificar
Model
diretamente?
Em geral, a API Funcional é de alto nível e fácil de usar, possui várias funções que não são suportadas por modelos de subclasse.
No entanto, a subclassificação do modelo oferece grande flexibilidade ao criar modelos que não são facilmente descritos como um gráfico acíclico direcionado de camadas (por exemplo, você não pode implementar o Tree-RNN com a API funcional, é necessário subclassificar o
Model
diretamente).
Pontos fortes da API funcional:
As propriedades listadas abaixo são verdadeiras para modelos seqüenciais (que também são estruturas de dados), mas são verdadeiras para modelos de subclasse (que são código Python, não estruturas de dados).
A API funcional produz código mais curto.
Sem
super(MyClass, self).__init__(...)
, sem
def call(self, ...):
etc.
Compare:
inputs = keras.Input(shape=(32,)) x = layers.Dense(64, activation='relu')(inputs) outputs = layers.Dense(10)(x) mlp = keras.Model(inputs, outputs)
Com 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)
Seu modelo é validado conforme está escrito.
Na API Funcional, as especificações de entrada (shape e dtype) são criadas antecipadamente (via `Input`) e, cada vez que você chama a camada, a camada verifica se as especificações passadas correspondem às suas suposições; se esse não for o caso, você receberá uma mensagem de erro útil .
Isso garante que qualquer modelo que você construa com a API Funcional seja iniciado. Toda a depuração (não relacionada à depuração de convergência) ocorrerá estaticamente durante a construção do modelo, e não no tempo de execução. Isso é semelhante à verificação de tipo no compilador.
Seu modelo funcional pode ser representado graficamente e também é testável.
Você pode desenhar o modelo na forma de um gráfico e pode acessar facilmente os nós intermediários do gráfico, por exemplo, para extrair e reutilizar a ativação das camadas intermediárias, como vimos no exemplo anterior:
features_list = [layer.output for layer in vgg19.layers] feat_extraction_model = keras.Model(inputs=vgg19.input, outputs=features_list)
Como o modelo Funcional é mais uma estrutura de dados do que um pedaço de código, ele pode ser serializado com segurança e pode ser salvo como um único arquivo que permite recriar exatamente o mesmo modelo sem acesso ao código-fonte.
Deficiências funcionais da API
Ele não suporta arquiteturas dinâmicas.
A API Funcional processa modelos como camadas DAG. Isso é verdade para a maioria das arquiteturas de aprendizado profundo, mas não para todos: por exemplo, redes recursivas ou RNNs da Árvore não atendem a essa suposição e não podem ser implementadas na API Funcional.
Às vezes, você só precisa escrever tudo do zero.
Ao escrever arquiteturas avançadas, convém fazer algo que vá além da “definição de camadas do DAG”: por exemplo, você pode usar vários métodos personalizados de treinamento e saída em uma instância do seu modelo. Isso requer subclassificação.
Combinando e combinando vários estilos de API
É importante observar que escolher entre a API Funcional ou subclassificar o Modelo não é uma solução binária que limita você a uma categoria de modelos.
Todos os modelos na API tf.keras podem interagir entre si, sejam modelos sequenciais, modelos funcionais ou modelos / camadas subclassificados, escritos do zero.Você sempre pode usar o modelo Funcional ou o modelo Sequencial como parte do modelo / camada subclassificado: units = 32 timesteps = 10 input_dim = 5
Por outro lado, você pode usar qualquer Camada ou Modelo subclassificado na API Funcional se implementar um método call
que corresponda a um dos seguintes padrões:call(self, inputs, **kwargs)
onde inputs
está a estrutura do tensor ou do tensor aninhado (por exemplo, lista de tensores) e onde **kwargs
estão os argumentos sem tensor (não entrada) .call(self, inputs, training=None, **kwargs)
onde training
é um valor booleano indicando em qual modo a camada, aprendizado ou saída deve se comportar.call(self, inputs, mask=None, **kwargs)
onde mask
é o tensor da máscara booleana (útil para RNN, por exemplo).call(self, inputs, training=None, mask=None, **kwargs)
- é claro que você pode ter os dois parâmetros que definem o comportamento da camada ao mesmo tempo.Além disso, se você implementar o método `get_config` em sua camada ou modelo personalizado, os modelos funcionais criados com ele serão serializados e clonados.Abaixo está um pequeno exemplo em que usamos RNN personalizados escritos do zero Modelos funcionais: 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, activation='sigmoid') 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)
Isso conclui nosso Guia de API funcional!Agora você tem em mãos um poderoso conjunto de ferramentas para a construção de modelos de aprendizado profundo.Após a verificação, a tradução também aparecerá no Tensorflow.org. Se você deseja participar da tradução da documentação do site Tensorflow.org para o russo, entre em contato com um comentário ou pessoal. Quaisquer correções ou comentários são apreciados. Como ilustração, usamos a imagem do modelo GoogLeNet, que também é um gráfico acíclico direcionado.