Por que o modelo keras prevê mais lento após a compilação?

23

keras velocidade de previsão

Em teoria, a previsão deve ser constante, pois os pesos têm um tamanho fixo. Como recupero minha velocidade após a compilação (sem a necessidade de remover o otimizador)?

Consulte a experiência associada: https://nbviewer.jupyter.org/github/off99555/TensorFlowExperiments/blob/master/test-prediction-speed-after-compile.ipynb?flush_cache=true

off99555
fonte
Eu acho que você precisa ajustar o modelo após a compilação e, em seguida, usar o modelo treinado para prever. Consulte aqui
ingênuo
@naive Fitting é irrelevante para o problema. Se você souber como a rede realmente funciona, ficará curioso para saber por que a previsão é mais lenta. Ao prever, apenas os pesos são usados ​​para multiplicação de matrizes e os pesos devem ser fixados antes e depois da compilação, para que o tempo de previsão permaneça constante.
off99555
Eu sei que isso é irrelevante para o problema . E não é necessário saber como a rede funciona para apontar que as tarefas que você elaborou e comparou a precisão são realmente sem sentido. Sem ajustar o modelo a alguns dados que você está prevendo e, na verdade, está comparando o tempo gasto. Esta não é a casos habituais ou direito de uso de uma rede neural
ingênuo
3
@naive O problema diz respeito à compreensão do desempenho do modelo compilado versus descompilado, não tendo nada a ver com precisão ou design do modelo. É uma questão legítima que pode custar aos usuários de TF - por exemplo, eu não fazia ideia disso até me deparar com essa pergunta.
OverLordGoldDragon
11
@naive Você não pode fitsem compile; o otimizador nem existe para atualizar pesos. predict pode ser usado sem fitou compilecomo descrito na minha resposta, mas a diferença de desempenho não deve ser tão dramática - daí o problema.
OverLordGoldDragon

Respostas:

22

ATUALIZAÇÃO - 15/1/2020 : a melhor prática atual para lotes pequenos deve ser a entrada direta do modelo - isto é preds = model(x), e se as camadas se comportarem de maneira diferente no trem / inferência model(x, training=False),. Pela confirmação mais recente, isso agora está documentado .

Eu não os comparei, mas de acordo com a discussão do Git , também vale a pena tentar predict_on_batch()- especialmente com as melhorias no TF 2.1.


ULTIMATE CULPADO : self._experimental_run_tf_function = True. É experimental . Mas não é realmente ruim.

Para qualquer desenvolvedor de TensorFlow que esteja lendo: limpe seu código . É uma bagunça. E viola importantes práticas de codificação, como uma função faz uma coisa ; _process_inputsfaz muito mais do que "entradas de processo", o mesmo para _standardize_user_data. "Eu não sou pago o suficiente" - mas você fazer pagamento, no tempo extra gasto compreender o seu próprio material, e em usuários de encher sua página Problemas com erros resolvidos mais fácil com um código mais claro.


RESUMO : é apenas um pouco mais lento com compile().

compile()define um sinalizador interno ao qual atribui uma função de previsão diferente predict. Essa função constrói um novo gráfico a cada chamada, diminuindo a velocidade em relação a não compilado. No entanto, a diferença é pronunciada apenas quando o tempo de trem é muito menor que o tempo de processamento de dados . Se aumentarmos o tamanho do modelo para pelo menos o tamanho médio, os dois se tornarão iguais. Veja o código na parte inferior.

Esse ligeiro aumento no tempo de processamento de dados é mais do que compensado pela capacidade de gráficos amplificados. Como é mais eficiente manter apenas um gráfico de modelo, o pré-compilado é descartado. No entanto : se o seu modelo for pequeno em relação aos dados, você estará melhor sem compile()a inferência do modelo. Veja minha outra resposta para uma solução alternativa.


O QUE DEVO FAZER?

Compare o desempenho do modelo compilado versus descompilado como eu tenho no código na parte inferior.

  • Compilado é mais rápido : execute predictem um modelo compilado.
  • Compilado é mais lento : execute predictem um modelo não compilado.

Sim, ambos são possíveis e dependerão do (1) tamanho dos dados; (2) tamanho do modelo; (3) hardware. O código na parte inferior mostra o modelo compilado sendo mais rápido, mas 10 iterações são uma amostra pequena. Veja "soluções alternativas" na minha outra resposta para o "como fazer".


DETALHES :

Demorou um pouco para depurar, mas foi divertido. Abaixo, descrevo os principais culpados que descobri, cito alguma documentação relevante e mostro os resultados do criador de perfil que levaram ao gargalo final.

( FLAG == self.experimental_run_tf_function, por questões de concisão)

  1. Modelpor padrão instancia com FLAG=False. compile()define para True.
  2. predict() envolve a aquisição da função de previsão, func = self._select_training_loop(x)
  3. Sem nenhum kwargs especial passado para predicte compile, todos os outros sinalizadores são tais que:
    • (A) FLAG==True ->func = training_v2.Loop()
    • (B) FLAG==False ->func = training_arrays.ArrayLikeTrainingLoop()
  4. A partir da documentação do código-fonte , (A) é fortemente dependente de gráficos, usa mais estratégia de distribuição e as operações são propensas a criar e destruir elementos gráficos, o que "pode" afetar o desempenho.

Verdadeiro culpado : _process_inputs(), respondendo por 81% do tempo de execução . Seu principal componente? _create_graph_function(), 72% do tempo de execução . Este método nem existe para (B) . Usar um modelo de tamanho médio, no entanto, _process_inputscompreende menos de 1% do tempo de execução . Código na parte inferior e resultados de criação de perfil a seguir.


PROCESSADORES DE DADOS :

(A) :, <class 'tensorflow.python.keras.engine.data_adapter.TensorLikeDataAdapter'>usado em _process_inputs(). Código fonte relevante

(B) : numpy.ndarray, devolvido por convert_eager_tensors_to_numpy. Código fonte relevante e aqui


FUNÇÃO DE EXECUÇÃO DE MODELO (por exemplo, prever)

(A) : função de distribuição , e aqui

(B) : função de distribuição (diferente) , e aqui


PROFILER : resultados para o código na minha outra resposta, "modelo pequeno", e nesta resposta, "modelo médio":

Modelo minúsculo : 1000 iterações,compile()

Modelo minúsculo : 1000 iterações, não compile()

Modelo médio : 10 iterações


DOCUMENTAÇÃO (indiretamente) sobre os efeitos de compile(): fonte

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 , por exemplo, chamando g(2)e g(3)gerará dois novos gráficos

function instancia um gráfico separado para cada conjunto exclusivo de formas e tipos de dados de entrada . Por exemplo, o seguinte trecho de código resultará em três gráficos distintos sendo rastreados, pois cada entrada tem uma forma diferente

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 ), mas não devem afetar a correção do programa


COUNTEREXAMPLE :

from tensorflow.keras.layers import Input, Dense, LSTM, Bidirectional, Conv1D
from tensorflow.keras.layers import Flatten, Dropout
from tensorflow.keras.models import Model
import numpy as np
from time import time

def timeit(func, arg, iterations):
    t0 = time()
    for _ in range(iterations):
        func(arg)
    print("%.4f sec" % (time() - t0))

batch_size = 32
batch_shape = (batch_size, 400, 16)
ipt   = Input(batch_shape=batch_shape)
x     = Bidirectional(LSTM(512, activation='relu', return_sequences=True))(ipt)
x     = LSTM(512, activation='relu', return_sequences=True)(ipt)
x     = Conv1D(128, 400, 1, 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)

X = np.random.randn(*batch_shape)
timeit(model.predict, X, 10)
model.compile('adam', loss='binary_crossentropy')
timeit(model.predict, X, 10)

Saídas :

34.8542 sec
34.7435 sec
OverLordGoldDragon
fonte
11
Qual é a conclusão sobre o que devemos fazer para obter a velocidade de previsão mais rápida para qualquer tamanho de modelo? É apenas para não fazer compile()?
off99555
3
@ off99555 "para qualquer tamanho de modelo" - não existe. Leia a resposta inteira - se eu levasse horas para depurá-la, alguns minutos do solicitante não deveriam ser irracionais.
OverLordGoldDragon
Eu li a coisa toda, mas é difícil de entender porque não sou eu quem depurou o código. Portanto, você precisa concluir que não envolve as variáveis ​​intermediárias encontradas durante a fase de depuração. Por exemplo: "Se o seu modelo é pequeno, então não use compilação Se o seu modelo é de tamanho médio, você pode usar compilação` Algo assim...
off99555
11
@ off99555 É justo; Atualizada. A nova seção é bastante sensata, mas posso ver que ela não está sendo realizada imediatamente.
OverLordGoldDragon
11
@ off99555 Não que eu testei, mas modelos muito grandes (ResNet, etc.) podem ser compilados notavelmente mais rápido, esp. se distribuído em muitos dispositivos - como (A) é mais pesado em gráficos e distribuição. O teste mais seguro é, bem, um teste - como na resposta. Familiarizado com TF lite, mas isso é uma questão separada
OverLordGoldDragon
15

UPDATE : veja a resposta real postada como uma resposta separada; este post contém informações adicionais


.compile() configura a maioria do gráfico TF / Keras, incluindo perdas, métricas, gradientes e parcialmente o otimizador e seus pesos - o que garante uma desaceleração notável.

O que é inesperado é a extensão da desaceleração - 10 vezes em meu próprio experimento e para predict(), que não atualiza nenhum peso. Analisando o código-fonte do TF2, os elementos gráficos aparecem fortemente interligados, com os recursos não sendo necessariamente alocados "de maneira justa".

Possível negligência pelos desenvolvedores sobre predicto desempenho de um modelo não compilado, pois os modelos geralmente são usados ​​compilados - mas, na prática , essa é uma diferença inaceitável. Também é possível que seja um "mal necessário", pois há uma solução simples (veja abaixo).

Esta não é uma resposta completa e espero que alguém possa fornecê-la aqui - caso contrário, sugiro que abra um problema do Github no TensorFlow. (OP tem; aqui )


Solução alternativa : treine um modelo, salve seus pesos , reconstrua o modelo sem compilar, carregue os pesos. Você não salvar o modelo inteiro (por exemplo model.save()), como ele irá carregar compilado - em vez disso usar model.save_weights()e model.load_weights().

Solução 2 : acima, mas use load_model(path, compile=False); crédito da sugestão: D. Möller


ACTUALIZAÇÃO : clarificar, optimizador está não totalmente instanciado com compile, incluindo os seus weightse updatestensores - isto é feito quando a primeira chamada de uma função de encaixe é feito ( fit, train_on_batch, etc.), por meio de model._make_train_function().

O comportamento observado é, portanto, ainda mais estranho. Pior ainda, a construção do otimizador não provoca mais lentidões (veja abaixo) - sugerir "tamanho do gráfico" não é a principal explicação aqui.


EDIT : em alguns modelos, uma desaceleração de 30x . TensorFlow, o que você fez. Exemplo abaixo:

from tensorflow.keras.layers import Input, Dense
from tensorflow.keras.models import Model
import numpy as np
from time import time

def timeit(func, arg, iterations):
    t0 = time()
    for _ in range(iterations):
        func(arg)
    print("%.4f sec" % (time() - t0))

ipt   = Input(shape=(4,))
x     = Dense(2, activation='relu')(ipt)
out   = Dense(1, activation='sigmoid')(x)
model = Model(ipt, out)

X = np.random.randn(32,4)

timeit(model.predict, X, 1000)
model.compile('adam', loss='binary_crossentropy')
timeit(model.predict, X, 1000)
model._make_train_function()  # build optimizer
timeit(model.predict, X, 1000)

Saídas :

0.9891 sec
29.785 sec
29.521 sec
OverLordGoldDragon
fonte
11
Isso é interessante. Tem sido um tempo eu quero formação de teste com um gráfico estático model.fit()contra um laço dinâmico com a execução ansiosos para ver se a perda de desempenho é muito grande ...
Daniel Möller
11
No passado, eu pude notar uma diferença significativa de velocidade entre o Keras e o PyTorch (sendo o PyTorch muito mais rápido).
Daniel Möller
11
Eu abri um problema aqui: github.com/tensorflow/tensorflow/issues/33340
off99555:
2
Sim. É uma má escolha de design que você coloque o código relacionado ao treinamento dentro da previsão. Como os usuários usarão essa função de previsão sequencialmente várias vezes na produção. Deve funcionar mais rápido para causar a menor surpresa. Comparando com a implementação numpy, você só precisa multiplicar uma matriz, adicionar um viés, ativar e é isso para uma camada densa. Não há necessidade de se preocupar com nenhuma função de perda.
off99555
11
Dica, você pode usar load_model(name, compile=False), é mais simples do que salvar / carregar pesos e recriar o modelo.
Daniel Möller