python tensorflow keras jupyter-notebook jupyter-lab

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



tensorflow jupyter-notebook (2)

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


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 pérdidas, métricas, gradientes y, en parte, el optimizador y sus pesos, lo que garantiza una notable desaceleración.

Lo inesperado es el alcance de la desaceleración: 10 veces en mi propio experimento y para predict() , que no 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 para predict el 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; here )

Solución alternativa : entrene un modelo, guarde sus pesos , reconstruya el modelo sin compilar, cargue los pesos. No guarde todo el modelo (por ejemplo, model.save() ), ya que se cargará compilado; en su lugar, use model.save_weights() y model.load_weights() .

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

ACTUALIZACIÓN : para aclarar, el optimizador no se instancia completamente con la compile , incluidos sus weights y los tensores de updates ; esto se hace cuando se realiza la primera llamada a una función de fit ( 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


ULTIMATE CULPRIT : 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_inputs hace mucho más que "procesar entradas", lo mismo para _standardize_user_data . "No me pagan 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 para 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 compile() para la 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 : ejecute predict en un modelo compilado.
  • Compilado es más lento : ejecute predict en 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_function , por brevedad)

  1. Model crea instancias por defecto con FLAG=False . compile() establece en True .
  2. predict() implica adquirir la función de predicción, func = self._select_training_loop(x)
  3. Sin pasar ningún kwargs especial para predict y compile , todos los demás indicadores 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() , que 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, utilizando un modelo de tamaño medio, _process_inputs comprende 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.ndarray , devuelto por convert_eager_tensors_to_numpy . Código fuente relevante , y here

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

(A) : función de distribución , y here

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

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, sin 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, llamar a g(2) 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