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
sí
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)
-
Model
crea instancias por defecto conFLAG=False
.compile()
establece enTrue
. -
predict()
implica adquirir la función de predicción,func = self._select_training_loop(x)
-
Sin pasar ningún kwargs especial para
predict
ycompile
, todos los demás indicadores son tales que:-
(A)
FLAG==True
->func = training_v2.Loop()
-
(B)
FLAG==False
->func = training_arrays.ArrayLikeTrainingLoop()
-
(A)
- 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 diferenteUn 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