python - Por que o TensorFlow 2 é muito mais lento que o TensorFlow 1?




keras performance-testing (2)

Ele foi citado por muitos usuários como o motivo da mudança para o Pytorch, mas ainda não encontrei uma justificativa / explicação para sacrificar a qualidade prática mais importante, a velocidade, para uma execução mais ágil.

Abaixo está o desempenho do benchmarking de código, TF1 vs. TF2 - com o TF1 executando de 47% a 276% mais rápido .

Minha pergunta é: o que é, no nível do gráfico ou do hardware, que gera uma desaceleração tão significativa?

Procurando uma resposta detalhada - já estou familiarizado com conceitos amplos. Git relevante

Especificações : CUDA 10.0.130, cuDNN 7.4.2, Python 3.7.4, Windows 10, GTX 1070

Resultados de referência :

ATUALIZAÇÃO : Desabilitar a Execução Ansiosa pelo código abaixo não ajuda. O comportamento, no entanto, é inconsistente: algumas vezes, a execução no modo gráfico ajuda consideravelmente, outras vezes, mais devagar em relação ao Eager.

Como os desenvolvedores do TF não aparecem em nenhum lugar, eu mesmo vou investigar esse assunto - posso acompanhar o progresso na questão do Github vinculada.

ATUALIZAÇÃO 2 : toneladas de resultados experimentais para compartilhar, juntamente com explicações; deve ser feito hoje.

Código de referência :

# use tensorflow.keras... to benchmark tf.keras; used GPU for all above benchmarks
from keras.layers import Input, Dense, LSTM, Bidirectional, Conv1D
from keras.layers import Flatten, Dropout
from keras.models import Model
from keras.optimizers import Adam
import keras.backend as K
import numpy as np
from time import time

batch_shape = (32, 400, 16)
X, y = make_data(batch_shape)

model_small = make_small_model(batch_shape)
model_small.train_on_batch(X, y)  # skip first iteration which builds graph
timeit(model_small.train_on_batch, 200, X, y)

K.clear_session()  # in my testing, kernel was restarted instead

model_medium = make_medium_model(batch_shape)
model_medium.train_on_batch(X, y)  # skip first iteration which builds graph
timeit(model_medium.train_on_batch, 10, X, y)

Funções utilizadas :

def timeit(func, iterations, *args):
    t0 = time()
    for _ in range(iterations):
        func(*args)
    print("Time/iter: %.4f sec" % ((time() - t0) / iterations))

def make_small_model(batch_shape):
    ipt   = Input(batch_shape=batch_shape)
    x     = Conv1D(128, 400, strides=4, padding='same')(ipt)
    x     = Flatten()(x)
    x     = Dropout(0.5)(x)
    x     = Dense(64, activation='relu')(x)
    out   = Dense(1,  activation='sigmoid')(x)
    model = Model(ipt, out)
    model.compile(Adam(lr=1e-4), 'binary_crossentropy')
    return model

def make_medium_model(batch_shape):
    ipt   = Input(batch_shape=batch_shape)
    x     = Bidirectional(LSTM(512, activation='relu', return_sequences=True))(ipt)
    x     = LSTM(512, activation='relu', return_sequences=True)(x)
    x     = Conv1D(128, 400, strides=4, padding='same')(x)
    x     = Flatten()(x)
    x     = Dense(256, activation='relu')(x)
    x     = Dropout(0.5)(x)
    x     = Dense(128, activation='relu')(x)
    x     = Dense(64,  activation='relu')(x)
    out   = Dense(1,   activation='sigmoid')(x)
    model = Model(ipt, out)
    model.compile(Adam(lr=1e-4), 'binary_crossentropy')
    return model

def make_data(batch_shape):
    return np.random.randn(*batch_shape), np.random.randint(0, 2, (batch_shape[0], 1))

ESTA RESPOSTA : tem como objetivo fornecer uma descrição detalhada do problema em nível de gráfico / hardware - incluindo loops de trem TF2 vs. TF1, processadores de dados de entrada e execuções no modo Ansioso vs. Gráfico. Para um resumo do problema e diretrizes de resolução, consulte minha outra resposta.

VERDITO DE DESEMPENHO : às vezes um é mais rápido, às vezes outro, dependendo da configuração. No que diz respeito ao TF2 vs TF1, eles estão em pé de igualdade, em média, mas existem diferenças significativas baseadas na configuração, e o TF1 supera o TF2 com mais frequência do que vice-versa. Veja "BENCHMARKING" abaixo.

EAGER VS. GRÁFICO : a carne de toda essa resposta para alguns: o desejo do TF2 é mais lento que o do TF1, de acordo com meus testes. Detalhes mais abaixo.

A diferença fundamental entre os dois é: o Graph configura uma rede computacional de forma proativa e é executado quando 'solicitado' - enquanto o Eager executa tudo na criação. Mas a história só começa aqui:

  • Ansioso NÃO é desprovido de Graph , e pode de fato ser principalmente Graph, contrário à expectativa. O que é em grande parte, é executado Graph - isso inclui pesos de modelo e otimizador, compreendendo uma grande parte do gráfico.

  • Ansioso reconstrói parte do próprio gráfico na execução ; conseqüência direta do Graph não estar totalmente construído - veja os resultados do criador de perfil. Isso tem uma sobrecarga computacional.

  • Ansioso é mais lento com entradas Numpy ; de acordo com esse comentário e código do Git , as entradas do Numpy no Eager incluem o custo adicional de copiar tensores da CPU para a GPU. Percorrendo o código fonte, as diferenças de manipulação de dados são claras; Ansioso passa diretamente o Numpy, enquanto o Graph passa os tensores que são avaliados para o Numpy; incerto do processo exato, mas este último deve envolver otimizações no nível da GPU

  • TF2 Eager é mais lento que TF1 Eager - isso é ... inesperado. Veja os resultados do benchmarking abaixo. As diferenças variam de insignificante a significante, mas são consistentes. Não sei por que é esse o caso - se um desenvolvedor do TF esclarecer, atualizará a resposta.

TF2 vs. TF1 : citando partes relevantes da confirmed de um desenvolvedor de TF, Q. Scott Zhu, com um pouco da minha ênfase e reformulação:

No aguardo, o tempo de execução precisa executar as operações e retornar o valor numérico para cada linha de código python. A natureza da execução de uma única etapa faz com que seja lenta .

No TF2, o Keras utiliza o tf.function para criar seu gráfico para treinamento, avaliação e previsão. Nós os chamamos de "função de execução" para o modelo. No TF1, a "função de execução" era um FuncGraph, que compartilhava algum componente comum como função TF, mas tem uma implementação diferente.

Durante o processo, deixamos de alguma forma uma implementação incorreta para train_on_batch (), test_on_batch () e predict_on_batch () . Eles ainda estão numericamente corretos , mas a função de execução para x_on_batch é uma função python pura, em vez de uma função python empacotada tf.function. Isso causará lentidão

No TF2, convertemos todos os dados de entrada em um tf.data.Dataset, pelo qual podemos unificar nossa função de execução para manipular o tipo único de entradas. Pode haver alguma sobrecarga na conversão do conjunto de dados , e acho que essa é uma sobrecarga única, em vez de um custo por lote

Com a última frase do último parágrafo acima e a última cláusula do parágrafo abaixo:

Para superar a lentidão no modo ansioso, temos @ tf.function, que transformará uma função python em um gráfico. Ao alimentar um valor numérico como a matriz np, o corpo da função tf.f. é convertido em gráfico estático, sendo otimizado e retorna o valor final, que é rápido e deve ter desempenho semelhante ao modo gráfico TF1.

Discordo - de acordo com meus resultados de criação de perfil, que mostram que o processamento de dados de entrada do Eager é substancialmente mais lento que o do Graph. Além disso, não tf.data.Dataset certeza sobre o tf.data.Dataset em particular, mas o Eager chama repetidamente vários dos mesmos métodos de conversão de dados - consulte o profiler.

Por fim, o commit vinculado do desenvolvedor: número significativo de alterações para dar suporte aos loops do Keras v2 .

Loops de trem : dependendo de (1) Ansioso vs. Gráfico; (2) formato de dados de entrada, o treinamento continuará com um loop de trem distinto - em TF2, _select_training_loop() , training.py , um dos seguintes:

training_v2.Loop()
training_distributed.DistributionMultiWorkerTrainingLoop(
              training_v2.Loop()) # multi-worker mode
# Case 1: distribution strategy
training_distributed.DistributionMultiWorkerTrainingLoop(
            training_distributed.DistributionSingleWorkerTrainingLoop())
# Case 2: generator-like. Input is Python generator, or Sequence object,
# or a non-distributed Dataset or iterator in eager execution.
training_generator.GeneratorOrSequenceTrainingLoop()
training_generator.EagerDatasetOrIteratorTrainingLoop()
# Case 3: Symbolic tensors or Numpy array-like. This includes Datasets and iterators 
# in graph mode (since they generate symbolic tensors).
training_generator.GeneratorLikeTrainingLoop() # Eager
training_arrays.ArrayLikeTrainingLoop() # Graph

Cada um deles lida com a alocação de recursos de maneira diferente e tem consequências no desempenho e na capacidade.

Loops de trem: fit vs train_on_batch , keras vs. tf.keras : cada um dos quatro usa loops de trem diferentes, embora talvez não em todas as combinações possíveis. o fit keras , por exemplo, usa uma forma de fit_loop , por exemplo training_arrays.fit_loop() , e seu train_on_batch pode usar K.function() . tf.keras possui uma hierarquia mais sofisticada descrita em parte na seção anterior.

Loops de treinamento: documentação - documentação relevante da fonte sobre alguns dos diferentes métodos de execução:

Diferentemente de outras operações do TensorFlow, não convertemos entradas numéricas python em tensores. Além disso, um novo gráfico é gerado para cada valor numérico python distinto

function instancia um gráfico separado para cada conjunto exclusivo de formas e tipos de dados de entrada .

Um único objeto tf.function pode precisar mapear para vários gráficos de computação sob o capô. Isso deve ser visível apenas como desempenho (os gráficos de rastreamento têm um custo computacional e de memória diferente de zero )

Processadores de dados de entrada : semelhante ao acima, o processador é selecionado caso a caso, dependendo dos sinalizadores internos definidos de acordo com as configurações de tempo de execução (modo de execução, formato de dados, estratégia de distribuição). O caso mais simples é o Eager, que funciona diretamente com matrizes Numpy. Para alguns exemplos específicos, consulte esta resposta .

TAMANHO DO MODELO, TAMANHO DOS DADOS:

  • É decisivo; nenhuma configuração única se destacava no topo de todos os tamanhos de modelo e de dados.
  • O tamanho dos dados em relação ao tamanho do modelo é importante; para dados e modelos pequenos, a transferência de dados (por exemplo, CPU para GPU) pode dominar. Da mesma forma, pequenos processadores aéreos podem rodar mais lentamente em grandes dados por tempo de conversão de dados dominante (consulte convert_to_tensor em "PROFILER")
  • A velocidade varia de acordo com os diferentes meios de processamento dos recursos dos loops de trem e dos processadores de dados de entrada.

REFERÊNCIAS : a carne moída. - Documento do Word - Planilha do Excel

Terminologia :

  • % sem números são todos os segundos
  • % calculado como (1 - longer_time / shorter_time)*100 ; lógica: estamos interessados em qual fator um é mais rápido que o outro; shorter / longer é na verdade uma relação não linear, não é útil para comparação direta
  • Determinação do sinal de%:
    • TF2 vs TF1: + se TF2 for mais rápido
    • GvE (Gráfico vs. Ansioso): + se o Gráfico for mais rápido
  • TF2 = TensorFlow 2.0.0 + Keras 2.3.1; TF1 = TensorFlow 1.14.0 + Keras 2.2.5

PROFILER :

PROFILER - Explicação : Spyder 3.3.6 IDE profiler.

  • Algumas funções são repetidas em ninhos de outras; portanto, é difícil rastrear a separação exata entre as funções "processamento de dados" e "treinamento", para que haja alguma sobreposição - conforme pronunciado no último resultado.

  • % de números calculados em tempo de execução wrt menos tempo de compilação

  • Tempo de construção calculado somando todos os tempos de execução (únicos) que foram chamados 1 ou 2 vezes
  • Tempo de trem calculado somando todos os tempos de execução (únicos) que foram chamados o mesmo número de vezes que o número de iterações e alguns dos tempos de execução de seus ninhos
  • Infelizmente, as funções são criadas de acordo com seus nomes originais (por exemplo, _func = func terá o perfil de func ), que se mistura no tempo de construção - daí a necessidade de excluí-lo

AMBIENTE DE TESTE :

  • Código executado na parte inferior com o mínimo de tarefas em segundo plano em execução
  • A GPU foi "aquecida" com algumas iterações antes de cronometrar as iterações, conforme sugerido nesta postagem
  • CUDA 10.0.130, cuDNN 7.6.0, TensorFlow 1.14.0 e TensorFlow 2.0.0 criados a partir da fonte, além do Anaconda
  • Python 3.7.4, Spyder 3.3.6 IDE
  • GTX 1070, Windows 10, 24 GB DDR4 2,4 MHz RAM, i7-7700HQ CPU de 2,8 GHz

METODOLOGIA :

  • Modelo de referência 'pequeno', 'médio' e 'grande' e tamanhos de dados
  • Corrija o número de parâmetros para cada tamanho de modelo, independentemente do tamanho dos dados de entrada
  • O modelo "maior" possui mais parâmetros e camadas
  • Dados "maiores" têm uma sequência mais longa, mas os mesmos batch_size e num_channels
  • Os modelos usam apenas Conv1D , camadas Dense ' Conv1D '; RNNs evitados por implemento da versão TF. diferenças
  • Sempre executou um ajuste de trem fora do loop de benchmarking, para omitir a construção de gráficos do modelo e do otimizador
  • Não usar dados esparsos (por exemplo, layers.Embedding() ) ou destinos esparsos (por exemplo, SparseCategoricalCrossEntropy()

LIMITAÇÕES : uma resposta "completa" explicaria todos os circuitos e iteradores de trem possíveis, mas isso certamente está além da minha capacidade de tempo, salário inexistente ou necessidade geral. Os resultados são tão bons quanto a metodologia - interpretem com a mente aberta.

CÓDIGO :

import numpy as np
import tensorflow as tf
import random
from termcolor import cprint
from time import time

from tensorflow.keras.layers import Input, Dense, Conv1D
from tensorflow.keras.layers import Dropout, GlobalAveragePooling1D
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
import tensorflow.keras.backend as K
#from keras.layers import Input, Dense, Conv1D
#from keras.layers import Dropout, GlobalAveragePooling1D
#from keras.models import Model 
#from keras.optimizers import Adam
#import keras.backend as K

#tf.compat.v1.disable_eager_execution()
#tf.enable_eager_execution()

def reset_seeds(reset_graph_with_backend=None, verbose=1):
    if reset_graph_with_backend is not None:
        K = reset_graph_with_backend
        K.clear_session()
        tf.compat.v1.reset_default_graph()
        if verbose:
            print("KERAS AND TENSORFLOW GRAPHS RESET")

    np.random.seed(1)
    random.seed(2)
    if tf.__version__[0] == '2':
        tf.random.set_seed(3)
    else:
        tf.set_random_seed(3)
    if verbose:
        print("RANDOM SEEDS RESET")

print("TF version: {}".format(tf.__version__))
reset_seeds()

def timeit(func, iterations, *args, _verbose=0, **kwargs):
    t0 = time()
    for _ in range(iterations):
        func(*args, **kwargs)
        print(end='.'*int(_verbose))
    print("Time/iter: %.4f sec" % ((time() - t0) / iterations))

def make_model_small(batch_shape):
    ipt   = Input(batch_shape=batch_shape)
    x     = Conv1D(128, 40, strides=4, padding='same')(ipt)
    x     = GlobalAveragePooling1D()(x)
    x     = Dropout(0.5)(x)
    x     = Dense(64, activation='relu')(x)
    out   = Dense(1,  activation='sigmoid')(x)
    model = Model(ipt, out)
    model.compile(Adam(lr=1e-4), 'binary_crossentropy')
    return model

def make_model_medium(batch_shape):
    ipt = Input(batch_shape=batch_shape)
    x = ipt
    for filters in [64, 128, 256, 256, 128, 64]:
        x  = Conv1D(filters, 20, strides=1, padding='valid')(x)
    x     = GlobalAveragePooling1D()(x)
    x     = Dense(256, activation='relu')(x)
    x     = Dropout(0.5)(x)
    x     = Dense(128, activation='relu')(x)
    x     = Dense(64,  activation='relu')(x)
    out   = Dense(1,   activation='sigmoid')(x)
    model = Model(ipt, out)
    model.compile(Adam(lr=1e-4), 'binary_crossentropy')
    return model

def make_model_large(batch_shape):
    ipt   = Input(batch_shape=batch_shape)
    x     = Conv1D(64,  400, strides=4, padding='valid')(ipt)
    x     = Conv1D(128, 200, strides=1, padding='valid')(x)
    for _ in range(40):
        x = Conv1D(256,  12, strides=1, padding='same')(x)
    x     = Conv1D(512,  20, strides=2, padding='valid')(x)
    x     = Conv1D(1028, 10, strides=2, padding='valid')(x)
    x     = Conv1D(256,   1, strides=1, padding='valid')(x)
    x     = GlobalAveragePooling1D()(x)
    x     = Dense(256, activation='relu')(x)
    x     = Dropout(0.5)(x)
    x     = Dense(128, activation='relu')(x)
    x     = Dense(64,  activation='relu')(x)    
    out   = Dense(1,   activation='sigmoid')(x)
    model = Model(ipt, out)
    model.compile(Adam(lr=1e-4), 'binary_crossentropy')
    return model

def make_data(batch_shape):
    return np.random.randn(*batch_shape), \
           np.random.randint(0, 2, (batch_shape[0], 1))

def make_data_tf(batch_shape, n_batches, iters):
    data = np.random.randn(n_batches, *batch_shape),
    trgt = np.random.randint(0, 2, (n_batches, batch_shape[0], 1))
    return tf.data.Dataset.from_tensor_slices((data, trgt))#.repeat(iters)

batch_shape_small  = (32, 140,   30)
batch_shape_medium = (32, 1400,  30)
batch_shape_large  = (32, 14000, 30)

batch_shapes = batch_shape_small, batch_shape_medium, batch_shape_large
make_model_fns = make_model_small, make_model_medium, make_model_large
iterations = [200, 100, 50]
shape_names = ["Small data",  "Medium data",  "Large data"]
model_names = ["Small model", "Medium model", "Large model"]

def test_all(fit=False, tf_dataset=False):
    for model_fn, model_name, iters in zip(make_model_fns, model_names, iterations):
        for batch_shape, shape_name in zip(batch_shapes, shape_names):
            if (model_fn is make_model_large) and (batch_shape is batch_shape_small):
                continue
            reset_seeds(reset_graph_with_backend=K)
            if tf_dataset:
                data = make_data_tf(batch_shape, iters, iters)
            else:
                data = make_data(batch_shape)
            model = model_fn(batch_shape)

            if fit:
                if tf_dataset:
                    model.train_on_batch(data.take(1))
                    t0 = time()
                    model.fit(data, steps_per_epoch=iters)
                    print("Time/iter: %.4f sec" % ((time() - t0) / iters))
                else:
                    model.train_on_batch(*data)
                    timeit(model.fit, iters, *data, _verbose=1, verbose=0)
            else:
                model.train_on_batch(*data)
                timeit(model.train_on_batch, iters, *data, _verbose=1)
            cprint(">> {}, {} done <<\n".format(model_name, shape_name), 'blue')
            del model

test_all(fit=True, tf_dataset=False)

VERDITO : não é , se você sabe o que está fazendo. Mas, se não o fizer , pode custar muito - com algumas atualizações de GPU, em média, e com o pior caso de várias GPUs.

ESTA RESPOSTA : tem como objetivo fornecer uma descrição de alto nível do problema, bem como diretrizes sobre como decidir sobre a configuração de treinamento específica para suas necessidades. Para uma descrição detalhada e de baixo nível, que inclui todos os resultados de benchmarking + código usado, veja minha outra resposta.

Estarei atualizando minhas respostas com mais informações, se descobrir alguma - pode marcar / "marcar com estrela" esta pergunta para referência.

RESUMO DO PROBLEMA : conforme confirmed por um desenvolvedor do TensorFlow, Q. Scott Zhu, o TF2 focou o desenvolvimento na execução ansiosa e forte integração com o Keras, que envolveu mudanças radicais na fonte do TF - inclusive no nível do gráfico. Benefícios: recursos de processamento, distribuição, depuração e implantação amplamente expandidos. O custo de alguns deles, no entanto, é a velocidade.

O assunto, no entanto, é bastante mais complexo. Não se trata apenas de TF1 vs. TF2 - os fatores que geram diferenças significativas na velocidade do trem incluem:

  1. TF2 vs. TF1
  2. Modo Ansioso vs. Gráfico
  3. keras vs. tf.keras
  4. numpy vs. tf.data.Dataset vs.
  5. train_on_batch() vs. fit()
  6. GPU vs. CPU

Infelizmente, quase nenhuma das opções acima é independente da outra, e cada uma pode pelo menos dobrar o tempo de execução em relação à outra. Felizmente, você pode determinar o que funcionará melhor sistematicamente e com alguns atalhos - como mostrarei.

O QUE DEVO FAZER? Atualmente, a única maneira é experimentar o seu modelo, dados e hardware específicos. Nenhuma configuração única sempre funcionará melhor - mas há o que fazer e o que não fazer para simplificar sua pesquisa:

>> FAÇA:

  • train_on_batch() + numpy + tf.keras + TF1 + Ansioso / Gráfico
  • train_on_batch() + numpy + tf.keras + TF2 + gráfico
  • fit() + numpy + tf.keras + TF1 / TF2 + Gráfico + modelo grande e dados

>> NÃO:

  • fit() + numpy + keras para modelos e dados pequenos e médios
  • fit() + numpy + tf.keras + TF1 / TF2 + Ansioso
  • train_on_batch() + numpy + keras + TF1 + Ansioso

  • [Major] tf.python.keras ; ele pode ser executado 10-100x mais lento e com muitos bugs; mais informações

    • Isso inclui layers , models , optimizers e importações relacionadas ao uso "pronto para uso"; ops, utils e importações 'privadas' relacionadas são boas - mas, para ter certeza, verifique as alterações e se elas são usadas no tf.keras

Consulte o código na parte inferior da minha outra resposta para um exemplo de configuração de benchmarking. A lista acima é baseada principalmente nas tabelas "BENCHMARKS" na outra resposta.

LIMITAÇÕES dos DOs e NÃOs acima:

  • Esta pergunta é intitulada "Por que o TF2 é muito mais lento que o TF1?" E, embora seu corpo esteja relacionado ao treinamento explicitamente, o assunto não está limitado a ele; a inferência também está sujeita a grandes diferenças de velocidade, mesmo dentro da mesma versão TF, importação, formato de dados etc. - veja esta resposta .
  • É provável que as RNNs alterem notavelmente a grade de dados na outra resposta, pois foram aprimoradas no TF2
  • Modelos usados ​​principalmente Conv1D e Dense - sem RNNs, dados / destinos esparsos, entradas 4 / 5D e outras configurações
  • Dados de entrada limitados a numpy e tf.data.Dataset , enquanto existem muitos outros formatos; veja outra resposta
  • GPU foi usado; os resultados serão diferentes em uma CPU. De fato, quando fiz a pergunta, meu CUDA não estava configurado corretamente e alguns dos resultados foram baseados na CPU.

Por que o TF2 sacrificou a qualidade mais prática, a velocidade, para uma execução mais ágil? Claramente não está - o gráfico ainda está disponível. Mas se a pergunta é "por que ficar ansiosa":

  • Depuração superior : você provavelmente já encontrou várias perguntas: "como obtenho saídas de camada intermediária" ou "como inspeciono pesos"; com ansioso, é (quase) tão simples quanto .__dict__ . O gráfico, ao contrário, requer familiaridade com funções especiais de back-end - complicando bastante todo o processo de depuração e introspecção.
  • Prototipagem mais rápida : por idéias semelhantes às anteriores; entendimento mais rápido = mais tempo restante para o DL real.

COMO ATIVAR / DESATIVAR O EAGER?

tf.enable_eager_execution()  # TF1; must be done before any model/tensor creation
tf.compat.v1.disable_eager_execution() # TF2; above holds

INFORMAÇÕES ADICIONAIS :

  • Cuidado com os métodos _on_batch() no TF2; de acordo com o desenvolvedor do TF, eles ainda usam uma implementação mais lenta, mas não intencionalmente - isto é, deve ser corrigido. Veja outra resposta para detalhes.

PEDIDOS PARA DEVS DE FLUXO DE TENSOR :

  1. Corrija o train_on_batch() e o aspecto de desempenho de chamar fit() iterativamente; loops de trem personalizados são importantes para muitos, especialmente para mim.
  2. Adicione documentação / menção de documentação dessas diferenças de desempenho para o conhecimento dos usuários.
  3. Melhore a velocidade geral de execução para impedir que os espreitadelas pulem para o Pytorch.

AGRADECIMENTOS : Graças a

  • Q. Scott Zhu, desenvolvedor do TensorFlow, por seus confirmed sobre o assunto.
  • P. Andrey por compartilhar testes e discussões úteis .






tensorflow2.0