Introdução aos módulos, camadas e modelos

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

Para fazer o aprendizado de máquina no TensorFlow, provavelmente você precisará definir, salvar e restaurar um modelo.

Um modelo é, abstratamente:

  • Uma função que calcula algo em tensores (um passe para frente)
  • Algumas variáveis ​​que podem ser atualizadas em resposta ao treinamento

Neste guia, você irá abaixo da superfície de Keras para ver como os modelos do TensorFlow são definidos. Isso examina como o TensorFlow coleta variáveis ​​e modelos, bem como como eles são salvos e restaurados.

Configurar

import tensorflow as tf
from datetime import datetime

%load_ext tensorboard

Definição de modelos e camadas no TensorFlow

A maioria dos modelos são feitos de camadas. Camadas são funções com uma estrutura matemática conhecida que podem ser reutilizadas e têm variáveis ​​treináveis. Em TensorFlow, a maioria das implementações de alto nível de camadas e modelos, tais como Keras ou Sonnet , são construídos na mesma classe fundamental: tf.Module .

Aqui está um exemplo de uma forma muito simples tf.Module que opera em um tensor escalar:

class SimpleModule(tf.Module):
  def __init__(self, name=None):
    super().__init__(name=name)
    self.a_variable = tf.Variable(5.0, name="train_me")
    self.non_trainable_variable = tf.Variable(5.0, trainable=False, name="do_not_train_me")
  def __call__(self, x):
    return self.a_variable * x + self.non_trainable_variable

simple_module = SimpleModule(name="simple")

simple_module(tf.constant(5.0))
<tf.Tensor: shape=(), dtype=float32, numpy=30.0>

Módulos e, por extensão, camadas são terminologia de aprendizado profundo para "objetos": eles têm estado interno e métodos que usam esse estado.

Não há nada de especial sobre __call__ exceto para agir como um Python exigível ; você pode invocar seus modelos com quaisquer funções que desejar.

Você pode definir a treinabilidade das variáveis ​​ativada e desativada por qualquer motivo, incluindo camadas de congelamento e variáveis ​​durante o ajuste fino.

Por subclasse tf.Module , nenhum tf.Variable ou tf.Module casos atribuídos a propriedades deste objeto são coletadas automaticamente. Isso permite que você salvar e variáveis de carga, e também criar coleções de tf.Module s.

# All trainable variables
print("trainable variables:", simple_module.trainable_variables)
# Every variable
print("all variables:", simple_module.variables)
trainable variables: (<tf.Variable 'train_me:0' shape=() dtype=float32, numpy=5.0>,)
all variables: (<tf.Variable 'train_me:0' shape=() dtype=float32, numpy=5.0>, <tf.Variable 'do_not_train_me:0' shape=() dtype=float32, numpy=5.0>)
2021-09-22 20:41:24.398693: W tensorflow/python/util/util.cc:348] Sets are not currently considered sequences, but this may change in the future, so consider avoiding using them.

Este é um exemplo de um modelo de camada linear de duas camadas feito de módulos.

Primeiro, uma camada densa (linear):

class Dense(tf.Module):
  def __init__(self, in_features, out_features, name=None):
    super().__init__(name=name)
    self.w = tf.Variable(
      tf.random.normal([in_features, out_features]), name='w')
    self.b = tf.Variable(tf.zeros([out_features]), name='b')
  def __call__(self, x):
    y = tf.matmul(x, self.w) + self.b
    return tf.nn.relu(y)

E então o modelo completo, que cria duas instâncias de camada e as aplica:

class SequentialModule(tf.Module):
  def __init__(self, name=None):
    super().__init__(name=name)

    self.dense_1 = Dense(in_features=3, out_features=3)
    self.dense_2 = Dense(in_features=3, out_features=2)

  def __call__(self, x):
    x = self.dense_1(x)
    return self.dense_2(x)

# You have made a model!
my_model = SequentialModule(name="the_model")

# Call it, with random results
print("Model results:", my_model(tf.constant([[2.0, 2.0, 2.0]])))
Model results: tf.Tensor([[0.         0.05766037]], shape=(1, 2), dtype=float32)

tf.Module casos será automaticamente coleta, de forma recursiva, nenhum tf.Variable ou tf.Module casos atribuído a ele. Isso permite que você gerencie coleções de tf.Module s com uma única instância do modelo, e salvar e carregar modelos inteiros.

print("Submodules:", my_model.submodules)
Submodules: (<__main__.Dense object at 0x7f68843f0610>, <__main__.Dense object at 0x7f693c112990>)
for var in my_model.variables:
  print(var, "\n")
<tf.Variable 'b:0' shape=(3,) dtype=float32, numpy=array([0., 0., 0.], dtype=float32)> 

<tf.Variable 'w:0' shape=(3, 3) dtype=float32, numpy=
array([[ 1.3168278 , -0.04209378,  1.4812242 ],
       [-1.8976173 , -1.1280936 , -2.57399   ],
       [ 0.43279243,  1.2495825 ,  0.31452706]], dtype=float32)> 

<tf.Variable 'b:0' shape=(2,) dtype=float32, numpy=array([0., 0.], dtype=float32)> 

<tf.Variable 'w:0' shape=(3, 2) dtype=float32, numpy=
array([[ 0.70494235,  0.34362802],
       [-1.318382  ,  0.3631226 ],
       [ 0.870611  ,  0.6044129 ]], dtype=float32)>

Esperando para criar variáveis

Você deve ter notado aqui que é necessário definir os tamanhos de entrada e saída para a camada. Isto é assim o w variável tem uma forma conhecida e pode ser alocado.

Ao adiar a criação da variável para a primeira vez que o módulo é chamado com uma forma de entrada específica, você não precisa especificar o tamanho da entrada antecipadamente.

class FlexibleDenseModule(tf.Module):
  # Note: No need for `in_features`
  def __init__(self, out_features, name=None):
    super().__init__(name=name)
    self.is_built = False
    self.out_features = out_features

  def __call__(self, x):
    # Create variables on first call.
    if not self.is_built:
      self.w = tf.Variable(
        tf.random.normal([x.shape[-1], self.out_features]), name='w')
      self.b = tf.Variable(tf.zeros([self.out_features]), name='b')
      self.is_built = True

    y = tf.matmul(x, self.w) + self.b
    return tf.nn.relu(y)
# Used in a module
class MySequentialModule(tf.Module):
  def __init__(self, name=None):
    super().__init__(name=name)

    self.dense_1 = FlexibleDenseModule(out_features=3)
    self.dense_2 = FlexibleDenseModule(out_features=2)

  def __call__(self, x):
    x = self.dense_1(x)
    return self.dense_2(x)

my_model = MySequentialModule(name="the_model")
print("Model results:", my_model(tf.constant([[2.0, 2.0, 2.0]])))
Model results: tf.Tensor([[0.       4.937734]], shape=(1, 2), dtype=float32)

Esta flexibilidade é por camadas TensorFlow muitas vezes só precisa especificar a forma de suas saídas, como em tf.keras.layers.Dense , em vez de tanto o tamanho de entrada e saída.

Economizando peso

Você pode salvar uma tf.Module tanto como um ponto de verificação e uma SavedModel .

Os pontos de verificação são apenas os pesos (ou seja, os valores do conjunto de variáveis ​​dentro do módulo e seus submódulos):

chkp_path = "my_checkpoint"
checkpoint = tf.train.Checkpoint(model=my_model)
checkpoint.write(chkp_path)
'my_checkpoint'

Os pontos de verificação consistem em dois tipos de arquivos: os próprios dados e um arquivo de índice para metadados. O arquivo de índice rastreia o que é realmente salvo e a numeração dos pontos de verificação, enquanto os dados dos pontos de verificação contêm os valores das variáveis ​​e seus caminhos de pesquisa de atributos.

ls my_checkpoint*
my_checkpoint.data-00000-of-00001  my_checkpoint.index

Você pode olhar dentro de um ponto de verificação para ter certeza de que toda a coleção de variáveis ​​foi salva, classificada pelo objeto Python que as contém.

tf.train.list_variables(chkp_path)
[('_CHECKPOINTABLE_OBJECT_GRAPH', []),
 ('model/dense_1/b/.ATTRIBUTES/VARIABLE_VALUE', [3]),
 ('model/dense_1/w/.ATTRIBUTES/VARIABLE_VALUE', [3, 3]),
 ('model/dense_2/b/.ATTRIBUTES/VARIABLE_VALUE', [2]),
 ('model/dense_2/w/.ATTRIBUTES/VARIABLE_VALUE', [3, 2])]

Durante o treinamento distribuído (multi-máquina), eles podem ser fragmentados, e é por isso que são numerados (por exemplo, '00000-de-00001'). Nesse caso, porém, há apenas um fragmento.

Ao carregar os modelos de volta, você sobrescreve os valores em seu objeto Python.

new_model = MySequentialModule()
new_checkpoint = tf.train.Checkpoint(model=new_model)
new_checkpoint.restore("my_checkpoint")

# Should be the same result as above
new_model(tf.constant([[2.0, 2.0, 2.0]]))
<tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[0.      , 4.937734]], dtype=float32)>

Salvar funções

TensorFlow pode executar modelos sem os objetos Python originais, como demonstrado por TensorFlow Servindo e TensorFlow Lite , mesmo quando você baixar um modelo treinado desde TensorFlow Hub .

TensorFlow precisa saber como fazer os cálculos descritos no Python, mas sem o código original. Para fazer isso, você pode fazer um gráfico, que é descrito na Introdução aos gráficos e funções guia .

Este gráfico contém operações ou operações, que implementam a função.

Você pode definir um gráfico no modelo acima, adicionando o @tf.function decorador para indicar que este código deve ser executado como um gráfico.

class MySequentialModule(tf.Module):
  def __init__(self, name=None):
    super().__init__(name=name)

    self.dense_1 = Dense(in_features=3, out_features=3)
    self.dense_2 = Dense(in_features=3, out_features=2)

  @tf.function
  def __call__(self, x):
    x = self.dense_1(x)
    return self.dense_2(x)

# You have made a model with a graph!
my_model = MySequentialModule(name="the_model")

O módulo que você criou funciona exatamente como antes. Cada assinatura exclusiva passada para a função cria um gráfico separado. Verifique a Introdução aos gráficos e funções de guiar para mais detalhes.

print(my_model([[2.0, 2.0, 2.0]]))
print(my_model([[[2.0, 2.0, 2.0], [2.0, 2.0, 2.0]]]))
tf.Tensor([[0.57633626 0.8910464 ]], shape=(1, 2), dtype=float32)
tf.Tensor(
[[[0.57633626 0.8910464 ]
  [0.57633626 0.8910464 ]]], shape=(1, 2, 2), dtype=float32)

Você pode visualizar o gráfico traçando-o em um resumo do TensorBoard.

# Set up logging.
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
logdir = "logs/func/%s" % stamp
writer = tf.summary.create_file_writer(logdir)

# Create a new model to get a fresh trace
# Otherwise the summary will not see the graph.
new_model = MySequentialModule()

# Bracket the function call with
# tf.summary.trace_on() and tf.summary.trace_export().
tf.summary.trace_on(graph=True)
tf.profiler.experimental.start(logdir)
# Call only one tf.function when tracing.
z = print(new_model(tf.constant([[2.0, 2.0, 2.0]])))
with writer.as_default():
  tf.summary.trace_export(
      name="my_func_trace",
      step=0,
      profiler_outdir=logdir)
tf.Tensor([[0. 0.]], shape=(1, 2), dtype=float32)

Inicie o TensorBoard para ver o trace resultante:

%tensorboard --logdir logs/func

Uma captura de tela do gráfico no TensorBoard

Criando um SavedModel

A maneira recomendada de compartilhar modelos completamente treinados é usar SavedModel . SavedModel contém tanto uma colecção de funções e um conjunto de pesos.

Você pode salvar o modelo que acabou de treinar da seguinte maneira:

tf.saved_model.save(my_model, "the_saved_model")
INFO:tensorflow:Assets written to: the_saved_model/assets
# Inspect the SavedModel in the directory
ls -l the_saved_model
total 24
drwxr-sr-x 2 kbuilder kokoro  4096 Sep 22 20:41 assets
-rw-rw-r-- 1 kbuilder kokoro 14702 Sep 22 20:41 saved_model.pb
drwxr-sr-x 2 kbuilder kokoro  4096 Sep 22 20:41 variables
# The variables/ directory contains a checkpoint of the variables
ls -l the_saved_model/variables
total 8
-rw-rw-r-- 1 kbuilder kokoro 408 Sep 22 20:41 variables.data-00000-of-00001
-rw-rw-r-- 1 kbuilder kokoro 356 Sep 22 20:41 variables.index

O saved_model.pb arquivo é um protocolo de tampão descrevendo o funcional tf.Graph .

Modelos e camadas podem ser carregados a partir dessa representação sem realmente fazer uma instância da classe que os criou. Isso é desejado em situações em que você não tem (ou deseja) um interpretador Python, como servir em escala ou em um dispositivo de ponta, ou em situações em que o código Python original não está disponível ou não é prático de usar.

Você pode carregar o modelo como um novo objeto:

new_model = tf.saved_model.load("the_saved_model")

new_model , criado a partir de carregar um modelo salvo, é um objeto de usuário TensorFlow interna sem qualquer do conhecimento classe. Não é do tipo SequentialModule .

isinstance(new_model, SequentialModule)
False

Este novo modelo funciona nas assinaturas de entrada já definidas. Você não pode adicionar mais assinaturas a um modelo restaurado como este.

print(my_model([[2.0, 2.0, 2.0]]))
print(my_model([[[2.0, 2.0, 2.0], [2.0, 2.0, 2.0]]]))
tf.Tensor([[0.57633626 0.8910464 ]], shape=(1, 2), dtype=float32)
tf.Tensor(
[[[0.57633626 0.8910464 ]
  [0.57633626 0.8910464 ]]], shape=(1, 2, 2), dtype=float32)

Assim, usando SavedModel , você é capaz de salvar pesos e gráficos usando TensorFlow tf.Module , e, em seguida, carregá-los novamente.

Modelos e camadas Keras

Observe que até este ponto, não há menção a Keras. Você pode construir o seu próprio alto nível API em cima de tf.Module , e as pessoas têm.

Nesta seção, você vai examinar como Keras usa tf.Module . Um guia de usuário completo aos modelos Keras podem ser encontradas no guia Keras .

Camadas Keras

tf.keras.layers.Layer é a classe de base de todas as camadas Keras, e ele herda tf.Module .

Você pode converter um módulo em uma camada Keras apenas trocando o pai e depois mudando __call__ a call :

class MyDense(tf.keras.layers.Layer):
  # Adding **kwargs to support base Keras layer arguments
  def __init__(self, in_features, out_features, **kwargs):
    super().__init__(**kwargs)

    # This will soon move to the build step; see below
    self.w = tf.Variable(
      tf.random.normal([in_features, out_features]), name='w')
    self.b = tf.Variable(tf.zeros([out_features]), name='b')
  def call(self, x):
    y = tf.matmul(x, self.w) + self.b
    return tf.nn.relu(y)

simple_layer = MyDense(name="simple", in_features=3, out_features=3)

Camadas Keras têm o seu próprio __call__ que faz alguma contabilidade descrito na próxima seção e, em seguida, chama call() . Você não deve notar nenhuma mudança na funcionalidade.

simple_layer([[2.0, 2.0, 2.0]])
<tf.Tensor: shape=(1, 3), dtype=float32, numpy=array([[4.441887  , 0.00987494, 1.2474661 ]], dtype=float32)>

A build passo

Conforme observado, é conveniente em muitos casos esperar para criar variáveis ​​até que você tenha certeza do formato de entrada.

As camadas Keras vêm com uma etapa extra do ciclo de vida que permite mais flexibilidade em como você define suas camadas. Isto é definido na build função.

build é chamado exatamente uma vez, e é chamado com a forma da entrada. Geralmente é usado para criar variáveis ​​(pesos).

Pode reescrever MyDense camada acima de ser flexível para o tamanho das suas entradas:

class FlexibleDense(tf.keras.layers.Layer):
  # Note the added `**kwargs`, as Keras supports many arguments
  def __init__(self, out_features, **kwargs):
    super().__init__(**kwargs)
    self.out_features = out_features

  def build(self, input_shape):  # Create the state of the layer (weights)
    self.w = tf.Variable(
      tf.random.normal([input_shape[-1], self.out_features]), name='w')
    self.b = tf.Variable(tf.zeros([self.out_features]), name='b')

  def call(self, inputs):  # Defines the computation from inputs to outputs
    return tf.matmul(inputs, self.w) + self.b

# Create the instance of the layer
flexible_dense = FlexibleDense(out_features=3)

Neste ponto, o modelo não foi construído, então não há variáveis:

flexible_dense.variables
[]

Chamar a função aloca variáveis ​​de tamanho apropriado:

# Call it, with predictably random results
print("Model results:", flexible_dense(tf.constant([[2.0, 2.0, 2.0], [3.0, 3.0, 3.0]])))
Model results: tf.Tensor(
[[ 0.26693314 -4.5170803  -2.1615696 ]
 [ 0.40039957 -6.7756205  -3.2423544 ]], shape=(2, 3), dtype=float32)
flexible_dense.variables
[<tf.Variable 'flexible_dense/w:0' shape=(3, 3) dtype=float32, numpy=
 array([[ 0.69237417, -1.9560792 , -0.8260334 ],
        [-0.5022879 , -1.314342  ,  0.6498058 ],
        [-0.05661968,  1.0118811 , -0.90455717]], dtype=float32)>,
 <tf.Variable 'flexible_dense/b:0' shape=(3,) dtype=float32, numpy=array([0., 0., 0.], dtype=float32)>]

Desde build só é chamado uma vez, as entradas serão rejeitados se a forma de entrada não é compatível com variáveis da camada:

try:
  print("Model results:", flexible_dense(tf.constant([[2.0, 2.0, 2.0, 2.0]])))
except tf.errors.InvalidArgumentError as e:
  print("Failed:", e)
Failed: In[0] mismatch In[1] shape: 4 vs. 3: [1,4] [3,3] 0 0 [Op:MatMul]

As camadas Keras têm muito mais recursos extras, incluindo:

  • Perdas opcionais
  • Suporte para métricas
  • Built-in suporte para um opcional training argumento para diferenciar entre a formação eo uso de inferência
  • get_config e from_config métodos que permitem que você armazene com precisão configurações para permitir modelo de clonagem em Python

Ler sobre eles na guia completo para camadas e modelos personalizados.

Modelos Keras

Você pode definir seu modelo como camadas Keras aninhadas.

No entanto, Keras também fornece uma classe de modelo full-featured chamado tf.keras.Model . Ele herda de tf.keras.layers.Layer , então um modelo Keras pode ser usado, aninhado, e salvo da mesma forma como camadas Keras. Os modelos Keras vêm com funcionalidades extras que os tornam fáceis de treinar, avaliar, carregar, salvar e até mesmo treinar em várias máquinas.

Pode-se definir a SequentialModule a partir de cima com o código quase idêntico, novamente converter __call__ a call() e mudando o pai:

class MySequentialModel(tf.keras.Model):
  def __init__(self, name=None, **kwargs):
    super().__init__(**kwargs)

    self.dense_1 = FlexibleDense(out_features=3)
    self.dense_2 = FlexibleDense(out_features=2)
  def call(self, x):
    x = self.dense_1(x)
    return self.dense_2(x)

# You have made a Keras model!
my_sequential_model = MySequentialModel(name="the_model")

# Call it on a tensor, with random results
print("Model results:", my_sequential_model(tf.constant([[2.0, 2.0, 2.0]])))
Model results: tf.Tensor([[13.535655 -1.076561]], shape=(1, 2), dtype=float32)

Todos os mesmos recursos estão disponíveis, incluindo variáveis ​​de rastreamento e submódulos.

my_sequential_model.variables
[<tf.Variable 'my_sequential_model/flexible_dense_1/w:0' shape=(3, 3) dtype=float32, numpy=
 array([[ 0.45659065,  0.38672394, -1.4717953 ],
        [-0.60538477, -0.7337349 , -1.9595573 ],
        [ 0.56756437, -0.7816317 , -0.43361524]], dtype=float32)>,
 <tf.Variable 'my_sequential_model/flexible_dense_1/b:0' shape=(3,) dtype=float32, numpy=array([0., 0., 0.], dtype=float32)>,
 <tf.Variable 'my_sequential_model/flexible_dense_2/w:0' shape=(3, 2) dtype=float32, numpy=
 array([[-0.66438836, -0.6865299 ],
        [-0.5305109 , -0.68015355],
        [-1.6681372 ,  0.2635035 ]], dtype=float32)>,
 <tf.Variable 'my_sequential_model/flexible_dense_2/b:0' shape=(2,) dtype=float32, numpy=array([0., 0.], dtype=float32)>]
my_sequential_model.submodules
(<__main__.FlexibleDense at 0x7f691bf92d10>,
 <__main__.FlexibleDense at 0x7f691ad189d0>)

Substituindo tf.keras.Model é uma abordagem muito Pythonic para a construção de modelos TensorFlow. Se você estiver migrando modelos de outras estruturas, isso pode ser muito simples.

Se você está construindo modelos que são conjuntos simples de camadas e as entradas existentes, você pode economizar tempo e espaço usando a API funcional , que vem com recursos adicionais em torno do modelo de reconstrução e arquitetura.

Aqui está o mesmo modelo com a API funcional:

inputs = tf.keras.Input(shape=[3,])

x = FlexibleDense(3)(inputs)
x = FlexibleDense(2)(x)

my_functional_model = tf.keras.Model(inputs=inputs, outputs=x)

my_functional_model.summary()
Model: "model"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         [(None, 3)]               0         
_________________________________________________________________
flexible_dense_3 (FlexibleDe (None, 3)                 12        
_________________________________________________________________
flexible_dense_4 (FlexibleDe (None, 2)                 8         
=================================================================
Total params: 20
Trainable params: 20
Non-trainable params: 0
_________________________________________________________________
my_functional_model(tf.constant([[2.0, 2.0, 2.0]]))
<tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[ 0.5620958, -0.3502033]], dtype=float32)>

A principal diferença aqui é que a forma de entrada é especificada antecipadamente como parte do processo de construção funcional. O input_shape argumento, neste caso, não tem que ser completamente especificado; você pode deixar algumas dimensões como None .

Salvando modelos Keras

Modelos Keras pode ser checkpointed, e que vai olhar o mesmo que tf.Module .

Modelos Keras também podem ser salvos com tf.saved_model.save() , como eles são módulos. No entanto, os modelos Keras têm métodos de conveniência e outras funcionalidades:

my_sequential_model.save("exname_of_file")
INFO:tensorflow:Assets written to: exname_of_file/assets

Com a mesma facilidade, eles podem ser carregados de volta em:

reconstructed_model = tf.keras.models.load_model("exname_of_file")
WARNING:tensorflow:No training configuration found in save file, so the model was *not* compiled. Compile it manually.

Keras SavedModels também poupar estados métrica, perda e otimizador.

Este modelo reconstruído pode ser usado e produzirá o mesmo resultado quando chamado nos mesmos dados:

reconstructed_model(tf.constant([[2.0, 2.0, 2.0]]))
<tf.Tensor: shape=(1, 2), dtype=float32, numpy=array([[13.535655, -1.076561]], dtype=float32)>

Há mais informações sobre como salvar e serializar os modelos Keras, incluindo o fornecimento de métodos de configuração para camadas personalizadas para suporte de recursos. Confira o guia para a poupança e serialização .

Qual é o próximo

Se você quiser saber mais detalhes sobre Keras, você pode seguir as guias Keras existentes aqui .

Outro exemplo de uma API de alto nível construído sobre tf.module é Soneto de DeepMind, que é coberto em seu site .