Um dos motivos é que a abstração do “Gráfico de Computação” usada pelo TensorFlow é uma correspondência aproximada, mas não exata, para o modelo de ML que esperamos treinar e usar. Como assim?
Normalmente, um modelo será usado de pelo menos três maneiras:
- Treinamento - localizando os pesos ou parâmetros corretos para o modelo, dados alguns dados de treinamento. Geralmente feito periodicamente à medida que novos dados chegam.
- Avaliação - calculando várias métricas durante o treinamento em um conjunto de dados diferente para avaliar a qualidade do treinamento ou para validação cruzada.
- Exibição - previsão sob demanda para novos dados
Pode haver mais modos. Por exemplo, podemos treinar novamente um modelo existente ou aplicá-lo a uma grande quantidade de dados no modo em lote.
Embora o modelo conceitual seja o mesmo, esses casos de uso podem precisar de gráficos computacionais diferentes.
Por exemplo, se usarmos o TensorFlow Serving, não poderemos carregar modelos com operações da função Python.
Outro exemplo são as métricas de avaliação e as operações de depuração, como tf.Assert
- talvez não desejemos executá-las ao servir por motivos de desempenho.
Acontece que precisamos de 3 a 5 gráficos diferentes para representar o nosso modelo. Existem algumas maneiras de fazer isso, e escolher a correta não é fácil .
Não podemos simplesmente construir um gráfico e atualizá-lo à medida que avançamos?
Os gráficos do TensorFlow no Python são apenas anexados. As operações do TensorFlow implicitamente criam nós de gráfico e não há operações para remover nós. Mesmo se tentarmos substituir
1 2 3 4 5 6 | with tf.Session() as sess: my_sqrt = tf.sqrt(4.0, name='my_sqrt') # override my_sqrt = tf.sqrt(2.0, name='my_sqrt') #print all nodes print sess.graph._nodes_by_name.keys() |
O TensorFlow adicionará apenas um sufixo ao nome da operação:
1 | [u'my_sqrt_1/x', u'my_sqrt_1', u'my_sqrt/x', u'my_sqrt'] |
O que está feito não pode ser desfeito, por assim dizer.
Então o que nós podemos fazer? Podemos tentar criar mais de um gráfico.
Criando vários gráficos com o mesmo código
Esse é o método que geralmente encontramos na documentação. Um exemplo de abandono pode ser assim:
1 2 | if is_training: activations = tf.nn.dropout(activations, 0.7) |
O TensorFlow é fornecido com ferramentas como tf.variable_scope
essa, facilitando a criação de gráficos diferentes.
Essa abordagem tem uma grande desvantagem - o gráfico serializado não pode mais ser usado sem o código que o produziu. Mesmo uma pequena alteração (como alterar o nome de uma variável) interromperá o modelo em produção. Portanto, para reverter para uma versão mais antiga, também precisamos reverter para o código mais antigo. Isso nem sempre é prático com repositórios maiores e, em qualquer caso, requer algum esforço de operações.
Além disso, o treinamento e a avaliação não usam o mesmo gráfico (mesmo que compartilhem pesos) e exigem coordenação desajeitada para se mesclar.
Então, ainda queremos um gráfico, mas queremos usá-lo para treinamento e avaliação. E talvez servindo ... Mas definitivamente treinamento e avaliação.
Usando um gráfico com lógica condicional
O TensorFlow tem uma maneira de codificar comportamentos diferentes em um único gráfico - a tf.cond
operação.
1 2 3 4 5 | is_train = tf.placeholder(tf.bool) dropout = tf.nn.dropout(activations, 0.7) activations = tf.cond(is_train, lambda: dropout, lambda: activations) |
A grande vantagem é que agora temos toda a lógica em um gráfico, por exemplo, podemos vê-la no TensorBoard. Agora, nossos modelos serializados trabalham para treinamento e avaliação.
Existem duas fontes de complexidade que tornam a imagem menos positiva - preguiça e filas.
Preguiça e o operador condicional
Digamos que tenhamos uma operação cara que gostaríamos de executar apenas durante a avaliação. Se o colocarmos atrás de um operador condicional, esperamos que ele seja executado apenas no momento da avaliação. Isso também é verdade se alterarmos algo como resultado de uma condição como o caso da normalização de lote.
Nem sempre é assim que funciona no TF. A tf.cond
operação é como uma caixa de preguiça, mas protege apenas o que está dentro.
Portanto, esse código funciona corretamente:
1 2 3 4 5 | # Good - dropout inside the conditional is_train = tf.placeholder(tf.bool) activations = tf.cond(is_train, lambda: tf.nn.dropout(activations, 0.7), lambda: activations) |
Mas isso irá interromper o abandono, mesmo que is_train == False
.
1 2 3 4 5 6 | # Bad - droupout outside the conditional evaluated every time! is_train = tf.placeholder(tf.bool) do_activations = tf.nn.dropout(activations, 0.7) activations = tf.cond(is_train, lambda: do_activations, lambda: activations) |
Filas e o operador condicional
As filas são a maneira preferida (e com melhor desempenho) de obter dados no TensorFlow. Normalmente, o treinamento e a avaliação serão feitos simultaneamente em diferentes entradas, portanto, podemos tentar a abordagem acima para colocá-los no mesmo gráfico.
1 2 3 | tf.cond(is_eval, lambda: tf.train.shuffle_batch(eval_tensors, 1024,100000,10000), lambda: tf.train.shuffle_batch(train_tensors,1024,100000,10000)) |
Não funciona no entanto. O TensorFlow nos informaria isso operation has been marked as not fetchable
e travaria .
A questão é que o TensorFlow não nos permite enfileirar condicionalmente. No entanto, a shuffle_batch
operação cria a operação de fila e desenfileiramento juntas.
Para evitar isso, precisamos dividir a operação em uma parte condicional que cria a fila e a parte condicional que extrai da fila correta.
Aqui está um exemplo (o código completo é bastante longo, então deixei apenas as partes relevantes)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | def create_queue(tensors, capacity, ...): ... queue = data_flow_ops.RandomShuffleQueue( capacity=capacity, min_after_dequeue=min_after_dequeue, seed=seed, dtypes=types, shapes=shapes, shared_name=shared_name) return queue def create_dequeue(queue, ...): ... dequeued = queue.dequeue_up_to(batch_size, name=name) ... return dequeued def merge_queues(self, is_train_tensor, train_tensors, test_tensors, ...): train_queue = self.create_queue(tensors=train_tensors, capacity=... ) test_queue = self.create_queue(tensors=test_tensors capacity=... ) input_values = tf.cond(is_train_tensor, lambda: self.create_dequeue(train_queue, ...), lambda: self.create_dequeue(test_queue, ...) |
Portanto, podemos obter o que queremos com o operador condicional, mas o código é mais complexo e difícil de entender. Porém, as operações devem ser mais fáceis - temos gráficos e monitoramento serializados simples.
Poderíamos evitar completamente as condições e, de alguma forma, contornar a limitação somente de acréscimo?
Trabalhando com gráficos salvos
A maioria dos pipelines serializa gráficos, mesmo que apenas para veiculação. Uma coisa muito importante que podemos fazer com o modelo é servir representações serializadas. O serviço TensorFlow está fora do escopo desta publicação, mas a idéia geral é que, para obter um servidor completo, precisamos apenas executar:
1 | bazel-bin/tensorflow_serving/model_servers/tensorflow_model_server --model_name=my_model --model_base_path=/my/model/path |
A execução de nosso gráfico de treinamento no TensorFlow Serving não é a melhor idéia, no entanto. O desempenho é prejudicado pela execução de operações desnecessárias e as tf.py_func
operações não podem ser carregadas pelo servidor.
Felizmente, o gráfico serializado não é como o único anexo que tínhamos quando começamos. É apenas um monte de objetos Protobuf para que possamos criar novas versões. Como exemplo, abaixo está uma versão simplificada e anotada da convert_variables_to_constants
função em graph_util_impl.py
que (sem surpresa) converte variáveis em constantes. É útil porque isso pode ser mais rápido ao veicular em alguns casos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | def convert_variables_to_constants(sess, input_graph_def, output_node_names, variable_names_whitelist=None, variable_names_blacklist=None): inference_graph = extract_sub_graph(input_graph_def, output_node_names) ... # Here we find the variable we want to convert for node in inference_graph.node: if node.op in ["Variable", "VariableV2"]: # Compute a list of variables found ... #Here we create a new graph with the variables replaced by constants output_graph_def = graph_pb2.GraphDef() ... for input_node in inference_graph.node: output_node = node_def_pb2.NodeDef() if input_node.name in found_variables: # Make output_node into a new constant with the variables weights else: output_node.CopyFrom(input_node) output_graph_def.node.extend
تبصرے
|