ESTA RESPUESTA : tiene como objetivo proporcionar una descripción detallada del problema a nivel gráfico / hardware, incluidos los bucles de tren TF2 vs. TF1, los procesadores de datos de entrada y las ejecuciones en modo Eager vs. Graph. Para un resumen del problema y pautas de resolución, vea mi otra respuesta.
VEREDICTO DE DESEMPEÑO : a veces uno es más rápido, a veces el otro, dependiendo de la configuración. En cuanto a TF2 vs TF1, están a la par en promedio, pero existen diferencias significativas basadas en la configuración, y TF1 supera a TF2 con más frecuencia que viceversa. Ver "BENCHMARKING" a continuación.
EAGER VS. GRÁFICO : la carne de esta respuesta completa para algunos: el deseo de TF2 es más lento que el de TF1, según mis pruebas. Detalles más abajo.
La diferencia fundamental entre los dos es: Graph configura una red computacional de manera proactiva , y se ejecuta cuando se le 'dice', mientras que Eager ejecuta todo al momento de la creación. Pero la historia solo comienza aquí:
Eager no está desprovisto de Graph , y de hecho puede ser principalmente Graph, al contrario de lo esperado. Lo que es en gran parte es el gráfico ejecutado : esto incluye pesos de modelo y optimizador, que comprenden una gran parte del gráfico.
Eager reconstruye parte del propio gráfico en la ejecución ; consecuencia directa de que Graph no se construyó completamente - ver resultados del generador de perfiles. Esto tiene una sobrecarga computacional.
Ansioso es más lento con entradas Numpy ; según este comentario y código de Git , las entradas de Numpy en Eager incluyen el costo general de copiar tensores de la CPU a la GPU. Al recorrer el código fuente, las diferencias en el manejo de datos son claras; Eager pasa directamente a Numpy, mientras que Graph pasa a los tensores que luego evalúan a Numpy; incierto del proceso exacto, pero este último debe incluir optimizaciones a nivel de GPU
TF2 Eager es más lento que TF1 Eager : esto es ... inesperado. Vea los resultados de la evaluación comparativa a continuación. Las diferencias abarcan desde insignificante a significativo, pero son consistentes. No estoy seguro de por qué es así: si un desarrollador de TF aclara, actualizará la respuesta.
TF2 vs. TF1 : citando porciones relevantes de la respuesta de un desarrollador de TF, Q. Scott Zhu, con un poco de mi énfasis y reformulación:
En impaciente, el tiempo de ejecución necesita ejecutar las operaciones y devolver el valor numérico para cada línea de código de Python. La naturaleza de la ejecución de un solo paso hace que sea lenta .
En TF2, Keras aprovecha la función tf para construir su gráfico para entrenamiento, evaluación y predicción. Los llamamos "función de ejecución" para el modelo. En TF1, la "función de ejecución" era un FuncGraph, que compartía algún componente común como función TF, pero tenía una implementación diferente.
Durante el proceso, de alguna manera dejamos una implementación incorrecta para train_on_batch (), test_on_batch () y predict_on_batch () . Todavía son numéricamente correctos , pero la función de ejecución para x_on_batch es una función de Python pura, en lugar de una función de Python envuelta en tf.function. Esto causará lentitud
En TF2, convertimos todos los datos de entrada en un tf.data.Dataset, mediante el cual podemos unificar nuestra función de ejecución para manejar el tipo único de las entradas. Puede haber una sobrecarga en la conversión del conjunto de datos , y creo que esta es una sobrecarga única, en lugar de un costo por lote
Con la última oración del último párrafo anterior y la última cláusula del párrafo siguiente:
Para superar la lentitud en el modo ansioso, tenemos @ tf.function, que convertirá una función de Python en un gráfico. Cuando se introduce un valor numérico como np array, el cuerpo de la función tf se convierte en un gráfico estático, se optimiza y devuelve el valor final, que es rápido y debe tener un rendimiento similar al modo de gráfico TF1.
No estoy de acuerdo, según mis resultados de perfil, que muestran que el procesamiento de datos de entrada de Eager es sustancialmente más lento que el de Graph. Además, no estoy seguro acerca de esto tf.data.Dataset
en particular, pero Eager llama repetidamente a varios de los mismos métodos de conversión de datos; consulte el generador de perfiles.
Por último, el compromiso vinculado del desarrollador: número significativo de cambios para admitir los bucles Keras v2 .
Train Loops : dependiendo de (1) Eager vs. Graph; (2) formato de datos de entrada, el entrenamiento continuará con un ciclo de entrenamiento distinto: en TF2 _select_training_loop()
, training.py , uno de:
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 uno maneja la asignación de recursos de manera diferente y tiene consecuencias en el rendimiento y la capacidad.
Train Loops: fit
vs train_on_batch
, keras
vstf.keras
.: cada uno de los cuatro usa diferentes bucles de tren, aunque quizás no en todas las combinaciones posibles. keras
' fit
, por ejemplo, usa una forma de fit_loop
, por ejemplo training_arrays.fit_loop()
, y train_on_batch
puede usar K.function()
. tf.keras
tiene una jerarquía más sofisticada descrita en parte en la sección anterior.
Train Loops: documentación - cadena de documentación fuente relevante en algunos de los diferentes métodos de ejecución:
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
function
crea una instancia de un gráfico separado para cada conjunto único de formas de entrada y tipos de datos .
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 )
Procesadores de datos de entrada : similar al anterior, el procesador se selecciona caso por caso, dependiendo de los indicadores internos establecidos de acuerdo con las configuraciones de tiempo de ejecución (modo de ejecución, formato de datos, estrategia de distribución). El caso más simple es con Eager, que funciona directamente con matrices Numpy. Para algunos ejemplos específicos, vea esta respuesta .
TAMAÑO DEL MODELO, TAMAÑO DE LOS DATOS:
- Es decisivo; ninguna configuración única se coronó sobre todos los tamaños de modelo y datos.
- El tamaño de los datos en relación con el tamaño del modelo es importante; para datos y modelos pequeños, la sobrecarga de transferencia de datos (por ejemplo, CPU a GPU) puede dominar. Del mismo modo, los pequeños procesadores generales pueden funcionar más lentamente en grandes datos por tiempo de conversión de datos dominante (ver
convert_to_tensor
en "PERFIL")
- La velocidad difiere según los diferentes medios de procesamiento de recursos de los bucles de tren y los procesadores de datos de entrada.
BENCHMARKS : la carne picada. - Documento de Word - Hoja de cálculo de Excel
Terminología :
- Los números sin% son todos segundos
- % calculado como
(1 - longer_time / shorter_time)*100
; justificación: nos interesa qué factor es uno más rápido que el otro; shorter / longer
en realidad es una relación no lineal, no es útil para la comparación directa
- % de determinación de signos:
- TF2 vs TF1:
+
si TF2 es más rápido
- GvE (Graph vs. Eager):
+
si Graph es más rápido
- TF2 = TensorFlow 2.0.0 + Keras 2.3.1; TF1 = TensorFlow 1.14.0 + Keras 2.2.5
PERFIL :
PERFIL - Explicación : Spyder 3.3.6 IDE profiler.
Algunas funciones se repiten en nidos de otros; por lo tanto, es difícil rastrear la separación exacta entre las funciones de "procesamiento de datos" y "entrenamiento", por lo que habrá cierta superposición, como se manifestó en el último resultado.
% de cifras calculadas en tiempo de ejecución wrt menos tiempo de construcción
- Tiempo de compilación calculado sumando todos los tiempos de ejecución (únicos) que se llamaron 1 o 2 veces
- Tiempo de entrenamiento calculado sumando todos los tiempos de ejecución (únicos) que se llamaron el mismo número de veces que el número de iteraciones, y algunos de los tiempos de ejecución de sus nidos
- Desafortunadamente, las funciones se perfilan de acuerdo con sus nombres originales (es decir,
_func = func
se perfilarán como func
), lo que se mezcla en el tiempo de compilación, de ahí la necesidad de excluirlo
ENTORNO DE PRUEBA :
- Código ejecutado en la parte inferior con tareas mínimas en segundo plano en ejecución
- La GPU se "calentó" con algunas iteraciones antes de las iteraciones de temporización, como se sugiere en esta publicación
- CUDA 10.0.130, cuDNN 7.6.0, TensorFlow 1.14.0 y TensorFlow 2.0.0 creados a partir de la fuente, más Anaconda
- Python 3.7.4, Spyder 3.3.6 IDE
- GTX 1070, Windows 10, 24GB DDR4 RAM de 2.4 MHz, CPU i7-7700HQ 2.8-GHz
METODOLOGÍA :
- Benchmark 'pequeño', 'mediano' y 'grande' de modelos y tamaños de datos
- Se corrigió el número de parámetros para cada tamaño de modelo, independientemente del tamaño de los datos de entrada
- El modelo "más grande" tiene más parámetros y capas
- Los datos "más grandes" tienen una secuencia más larga, pero igual
batch_size
ynum_channels
- Sólo utilizan modelos
Conv1D
, Dense
capas '' se pueden aprender; RNNs evitados por implem versión TF. diferencias
- Siempre ejecutó un ajuste de tren fuera del ciclo de evaluación comparativa, para omitir la construcción del modelo y del optimizador gráfico
- No utilizar datos dispersos (p
layers.Embedding()
. Ej. ) U objetivos dispersos (p. Ej.SparseCategoricalCrossEntropy()
LIMITACIONES : una respuesta "completa" explicaría todos los posibles bucles e iteradores de trenes, pero eso seguramente está más allá de mi capacidad de tiempo, cheque de pago inexistente o necesidad general. Los resultados son tan buenos como la metodología: interprete con una mente abierta.
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)