¿Por qué el modelo de Keras predice más lento después de la compilación?

23

predicción de velocidad keras

En teoría, la predicción debe ser constante ya que los pesos tienen un tamaño fijo. ¿Cómo recupero mi velocidad después de la compilación (sin la necesidad de eliminar el optimizador)?

Ver experimento asociado: https://nbviewer.jupyter.org/github/off99555/TensorFlowExperiments/blob/master/test-prediction-speed-after-compile.ipynb?flush_cache=true

off99555
fuente
Creo que necesita ajustar el modelo después de la compilación y luego usar el modelo entrenado para predecir. Consulte aquí
ingenuo
@naive Fitting es irrelevante para el problema. Si sabe cómo funciona realmente la red, le interesará saber por qué la predicción es más lenta. Al predecir, solo se utilizan los pesos para la multiplicación de matrices, y los pesos deben fijarse antes y después de la compilación, por lo que el tiempo de predicción debe permanecer constante.
off99555
Sé que eso es irrelevante para el problema . Y, uno no necesita saber cómo funciona la red para señalar que las tareas que se le ocurrieron y para comparar la precisión en realidad no tienen sentido. Sin ajustar el modelo sobre algunos datos que está prediciendo y en realidad está comparando el tiempo que lleva. Este no es el caso de uso habitual o correcto para una red neuronal
ingenuo el
3
@naive El problema se refiere a la comprensión del rendimiento del modelo compilado vs no compilado, que no tiene nada que ver con la precisión o el diseño del modelo. Es un problema legítimo que puede costar a los usuarios de TF: por mi parte, no tenía ni idea hasta que tropecé con esta pregunta.
OverLordGoldDragon
1
@naive No puedes fitsin compile; El optimizador ni siquiera existe para actualizar ningún peso. predict puede usarse sin fito compilecomo se describe en mi respuesta, pero la diferencia de rendimiento no debería ser tan dramática, de ahí el problema.
OverLordGoldDragon

Respuestas:

22

ACTUALIZACIÓN - 01/15/2020 : la mejor práctica actual para los pequeños tamaños de lote debe ser para alimentar las entradas al modelo directamente - es decir preds = model(x), y si las capas se comportan de manera diferente en tren / inferencia, model(x, training=False). Según la última confirmación, esto ahora está documentado .

No los he comparado, pero según la discusión de Git , también vale la pena intentarlo predict_on_batch(), especialmente con las mejoras en TF 2.1.


ÚLTIMA CULPABLE : self._experimental_run_tf_function = True. Es experimental . Pero en realidad no es malo.

Para cualquier desarrollador de TensorFlow que lea: limpie su código . Es un desastre. Y viola las prácticas de codificación importantes, como una función hace una cosa ; _process_inputshace mucho más que "entradas de proceso", lo mismo para _standardize_user_data. "No se me paga lo suficiente", pero paga, en el tiempo extra dedicado a comprender sus propias cosas, y en los usuarios que llenan su página de Problemas con errores más fácilmente resueltos con un código más claro.


RESUMEN : es solo un poco más lento con compile().

compile()establece una bandera interna que asigna una función de predicción diferente a predict. Esta función construye un nuevo gráfico en cada llamada, ralentizándolo en relación a no compilado. Sin embargo, la diferencia solo se pronuncia cuando el tiempo del tren es mucho más corto que el tiempo de procesamiento de datos . Si aumentamos el tamaño del modelo al menos a un tamaño mediano, los dos se vuelven iguales. Ver código en la parte inferior.

Este ligero aumento en el tiempo de procesamiento de datos está más que compensado por la capacidad de gráficos amplificados. Como es más eficiente mantener solo un gráfico de modelo, se descarta el precompilado. No obstante : si su modelo es pequeño en relación con los datos, es mejor sin la compile()inferencia del modelo. Vea mi otra respuesta para una solución alternativa.


¿QUÉ TENGO QUE HACER?

Compare el rendimiento del modelo compilado con el no compilado como lo he hecho en el código en la parte inferior.

  • Compilado es más rápido : ejecutar predicten un modelo compilado.
  • Compilado es más lento : ejecutar predicten un modelo sin compilar.

Sí, ambos son posibles y dependerá del (1) tamaño de los datos; (2) tamaño del modelo; (3) hardware. El código en la parte inferior en realidad muestra que el modelo compilado es más rápido, pero 10 iteraciones es una pequeña muestra. Ver "soluciones" en mi otra respuesta para el "cómo hacerlo".


DETALLES :

Esto tomó un tiempo para depurar, pero fue divertido. A continuación, describo a los principales culpables que descubrí, cito documentación relevante y muestro los resultados del generador de perfiles que condujeron al último cuello de botella.

( FLAG == self.experimental_run_tf_functionpor brevedad)

  1. Modelpor defecto crea una instancia con FLAG=False. compile()ajustarlo en el True.
  2. predict() implica adquirir la función de predicción, func = self._select_training_loop(x)
  3. Sin ningún kwargs especial pasado a predicty compile, todas las demás banderas son tales que:
    • (A) FLAG==True ->func = training_v2.Loop()
    • (B) FLAG==False ->func = training_arrays.ArrayLikeTrainingLoop()
  4. Desde la cadena de documentación del código fuente , (A) depende en gran medida de los gráficos, utiliza más estrategias de distribución y las operaciones son propensas a crear y destruir elementos gráficos, lo que "puede" (afectar) al rendimiento.

Verdadero culpable : _process_inputs()representa el 81% del tiempo de ejecución . ¿Su componente principal? _create_graph_function(), 72% del tiempo de ejecución . Este método ni siquiera existe para (B) . Sin embargo, el uso de un modelo de tamaño medio _process_inputscomprende menos del 1% del tiempo de ejecución . Código en la parte inferior, y siguen los resultados del perfil.


Procesadores de datos :

(A) : <class 'tensorflow.python.keras.engine.data_adapter.TensorLikeDataAdapter'>utilizado en _process_inputs(). Código fuente relevante

(B) : numpy.ndarraydevuelto por convert_eager_tensors_to_numpy. Código fuente relevante , y aquí


MODELO DE FUNCIÓN DE EJECUCIÓN (por ejemplo, predecir)

(A) : función de distribución , y aquí

(B) : función de distribución (diferente) , y aquí


PERFIL : resultados para el código en mi otra respuesta, "modelo pequeño", y en esta respuesta, "modelo mediano":

Modelo minúsculo : 1000 iteraciones,compile()

Modelo minúsculo : 1000 iteraciones, no compile()

Modelo medio : 10 iteraciones


DOCUMENTACIÓN (indirectamente) sobre los efectos de compile(): fuente

A diferencia de otras operaciones de TensorFlow, no convertimos entradas numéricas de python en tensores. Además, se genera un nuevo gráfico para cada valor numérico de Python distinto , por ejemplo, llamando g(2)y g(3)generará dos nuevos gráficos

function crea una instancia de un gráfico separado para cada conjunto único de formas de entrada y tipos de datos . Por ejemplo, el siguiente fragmento de código dará como resultado el seguimiento de tres gráficos distintos, ya que cada entrada tiene una forma diferente

Un solo objeto tf.function podría necesitar mapearse a múltiples gráficos de cálculo debajo del capó. Esto debería ser visible solo como rendimiento (los gráficos de seguimiento tienen un costo computacional y de memoria distinto de cero ), pero no deberían afectar la corrección del programa


Contraejemplo :

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)

Salidas :

34.8542 sec
34.7435 sec
OverLordGoldDragon
fuente
1
¿Cuál es la conclusión sobre lo que debemos hacer para obtener la velocidad de predicción más rápida para cualquier tamaño de modelo? ¿Es simplemente no hacer compile()?
off99555
3
@ off99555 "para cualquier tamaño de modelo": no existe tal cosa. Lea la respuesta completa: si me tomé horas para depurarlo, unos minutos del autor no deberían ser irrazonables.
OverLordGoldDragon
Lo leí todo pero es difícil de entender porque no soy yo quien depuró el código. Por lo tanto, debe dar una conclusión que no implique las variables intermedias que encuentre durante la fase de depuración. Por ejemplo, "Si su modelo es pequeño, no use la compilación. Si su modelo es de tamaño mediano, puede usar la compilación. Algo así.
off99555
1
@ off99555 Bastante justo; actualizado. La nueva sección es de sentido común, pero puedo ver que no se realiza de inmediato.
OverLordGoldDragon
1
@ off99555 No es que lo haya probado, pero los modelos muy grandes (ResNet, etc.) pueden ejecutarse en una compilación notablemente más rápida, especialmente. si se distribuye en muchos dispositivos, ya que (A) es más pesado en gráficos y distribución. La prueba más segura es, bueno, una prueba, como en la respuesta. No estoy familiarizado con TF lite, pero esa es una pregunta separada
OverLordGoldDragon
15

ACTUALIZACIÓN : vea la respuesta real publicada como una respuesta separada; esta publicación contiene información complementaria


.compile() configura la mayoría del gráfico TF / Keras, incluidas las pérdidas, las métricas, los gradientes y, en parte, el optimizador y sus pesos, lo que garantiza una notable desaceleración.

Que es inesperado es el grado de desaceleración - 10 veces en mi propio experimento, y para predict()que no se actualiza ningún peso. Mirando el código fuente de TF2, los elementos del gráfico aparecen estrechamente entrelazados, con recursos que no necesariamente se asignan "de manera justa".

Posible pasar por alto por los desarrolladores sobre predictel rendimiento de un modelo sin compilar, ya que los modelos generalmente se usan compilados, pero en la práctica , esta es una diferencia inaceptable. También es posible que sea un "mal necesario", ya que existe una solución simple (ver más abajo).

Esta no es una respuesta completa, y espero que alguien pueda proporcionarla aquí; de lo contrario, sugeriría abrir un problema de Github en TensorFlow. (OP tiene; aquí )


Solución alternativa : entrene un modelo, guarde sus pesos , reconstruya el modelo sin compilar, cargue los pesos. No , no guardar el modelo completo (por ejemplo model.save()), ya que va a cargar compilado - en lugar de utilizar model.save_weights()e model.load_weights().

Solución 2 : arriba, pero use load_model(path, compile=False); crédito de sugerencia: D. Möller


ACTUALIZACIÓN : aclarar, optimizador está no totalmente instancia utilizando compile, incluidas sus weightsy updatestensores - esto se hace cuando la primera llamada a una función de ajuste se hace ( fit, train_on_batch, etc.), a través de model._make_train_function().

El comportamiento observado es, por lo tanto, aún más extraño. Peor aún, construir el optimizador no provoca más ralentizaciones (ver más abajo), lo que sugiere que el "tamaño del gráfico" no es la explicación principal aquí.


EDITAR : en algunos modelos, una desaceleración de 30x . TensorFlow, ¿qué has hecho? Ejemplo a continuación:

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)

Salidas :

0.9891 sec
29.785 sec
29.521 sec
OverLordGoldDragon
fuente
1
Eso es interesante. Ha pasado un tiempo que quiero probar el entrenamiento con un gráfico estático model.fit()versus un bucle dinámico con ansiosa ejecución para ver si la pérdida de rendimiento es demasiado grande ...
Daniel Möller
1
En el pasado pude notar una diferencia de velocidad significativa entre Keras y PyTorch (siendo PyTorch mucho más rápido).
Daniel Möller
1
He abierto un problema aquí: github.com/tensorflow/tensorflow/issues/33340
off99555
2
Si. Es una mala elección de diseño que coloque el código relacionado con la capacitación dentro de la predicción. Porque los usuarios usarán esta función de predicción secuencialmente varias veces en la producción. Debería funcionar más rápido para causar la menor sorpresa. En comparación con la implementación de numpy, solo necesita multiplicar una matriz, agregar un sesgo, activar y eso es todo para una capa densa. No hay necesidad de preocuparse por ninguna función de pérdida.
off99555
1
Sugerencia, puede usar load_model(name, compile=False), es más simple que guardar / cargar pesas y recrear el modelo.
Daniel Möller