El descenso de gradientes con TensorFlow es mucho más lento que una implementación básica de Python, ¿por qué?
Estoy siguiendo un curso de aprendizaje automático. Tengo un problema de regresión lineal simple (LR) para ayudarme a acostumbrarme a TensorFlow. El problema es encontrar LR parámetros a
y b
de tal forma que Y = a*X + b
se aproxima a un (x, y)
nube de puntos (que me genera a mí mismo en aras de la simplicidad).
Estoy resolviendo este problema de LR usando un 'descenso de gradiente de tamaño de paso fijo (FSSGD)'. Lo implementé usando TensorFlow y funciona, pero noté que es muy lento tanto en la GPU como en la CPU. Como tenía curiosidad, implementé el FSSGD yo mismo en Python / NumPy y, como era de esperar, esto se ejecuta mucho más rápido, aproximadamente:
- 10 veces más rápido que TF @ CPU
- 20 veces más rápido que TF @ GPU
Si TensorFlow es tan lento, no puedo imaginar que tanta gente esté usando este marco. Entonces debo estar haciendo algo mal. ¿Alguien puede ayudarme para que pueda acelerar mi implementación de TensorFlow?
NO me interesa la diferencia entre el rendimiento de la CPU y la GPU. Ambos indicadores de desempeño se proporcionan simplemente para completar e ilustrar. Me interesa saber por qué mi implementación de TensorFlow es mucho más lenta que una implementación de Python / NumPy sin procesar.
Como referencia, agrego mi código a continuación.
- Despojado de un ejemplo mínimo (pero completamente funcional).
- Utilizando
Python v3.7.9 x64
. - Usado
tensorflow-gpu==1.15
por ahora (porque el curso usa TensorFlow v1) - Probado para ejecutarse tanto en Spyder como en PyCharm.
Mi implementación de FSSGD usando TensorFlow (tiempo de ejecución de aproximadamente 40 segundos @CPU a 80 segundos @GPU):
#%% General imports
import numpy as np
import timeit
import tensorflow.compat.v1 as tf
#%% Get input data
# Generate simulated input data
x_data_input = np.arange(100, step=0.1)
y_data_input = x_data_input + 20 * np.sin(x_data_input/10) + 15
#%% Define tensorflow model
# Define data size
n_samples = x_data_input.shape[0]
# Tensorflow is finicky about shapes, so resize
x_data = np.reshape(x_data_input, (n_samples, 1))
y_data = np.reshape(y_data_input, (n_samples, 1))
# Define placeholders for input
X = tf.placeholder(tf.float32, shape=(n_samples, 1), name="tf_x_data")
Y = tf.placeholder(tf.float32, shape=(n_samples, 1), name="tf_y_data")
# Define variables to be learned
with tf.variable_scope("linear-regression", reuse=tf.AUTO_REUSE): #reuse= True | False | tf.AUTO_REUSE
W = tf.get_variable("weights", (1, 1), initializer=tf.constant_initializer(0.0))
b = tf.get_variable("bias", (1,), initializer=tf.constant_initializer(0.0))
# Define loss function
Y_pred = tf.matmul(X, W) + b
loss = tf.reduce_sum((Y - Y_pred) ** 2 / n_samples) # Quadratic loss function
# %% Solve tensorflow model
#Define algorithm parameters
total_iterations = 1e5 # Defines total training iterations
#Construct TensorFlow optimizer
with tf.variable_scope("linear-regression", reuse=tf.AUTO_REUSE): #reuse= True | False | tf.AUTO_REUSE
opt = tf.train.GradientDescentOptimizer(learning_rate = 1e-4)
opt_operation = opt.minimize(loss, name="GDO")
#To measure execution time
time_start = timeit.default_timer()
with tf.Session() as sess:
#Initialize variables
sess.run(tf.global_variables_initializer())
#Train variables
for index in range(int(total_iterations)):
_, loss_val_tmp = sess.run([opt_operation, loss], feed_dict={X: x_data, Y: y_data})
#Get final values of variables
W_val, b_val, loss_val = sess.run([W, b, loss], feed_dict={X: x_data, Y: y_data})
#Print execution time
time_end = timeit.default_timer()
print('')
print("Time to execute code: {0:0.9f} sec.".format(time_end - time_start))
print('')
# %% Print results
print('')
print('Iteration = {0:0.3f}'.format(total_iterations))
print('W_val = {0:0.3f}'.format(W_val[0,0]))
print('b_val = {0:0.3f}'.format(b_val[0]))
print('')
Mi propia implementación de Python FSSGD (tiempo de ejecución de aproximadamente 4 segundos):
#%% General imports
import numpy as np
import timeit
#%% Get input data
# Define input data
x_data_input = np.arange(100, step=0.1)
y_data_input = x_data_input + 20 * np.sin(x_data_input/10) + 15
#%% Define Gradient Descent (GD) model
# Define data size
n_samples = x_data_input.shape[0]
#Initialize data
W = 0.0 # Initial condition
b = 0.0 # Initial condition
# Compute initial loss
y_gd_approx = W*x_data_input+b
loss = np.sum((y_data_input - y_gd_approx)**2)/n_samples # Quadratic loss function
#%% Execute Gradient Descent algorithm
#Define algorithm parameters
total_iterations = 1e5 # Defines total training iterations
GD_stepsize = 1e-4 # Gradient Descent fixed step size
#To measure execution time
time_start = timeit.default_timer()
for index in range(int(total_iterations)):
#Compute gradient (derived manually for the quadratic cost function)
loss_gradient_W = 2.0/n_samples*np.sum(-x_data_input*(y_data_input - y_gd_approx))
loss_gradient_b = 2.0/n_samples*np.sum(-1*(y_data_input - y_gd_approx))
#Update trainable variables using fixed step size gradient descent
W = W - GD_stepsize * loss_gradient_W
b = b - GD_stepsize * loss_gradient_b
#Compute loss
y_gd_approx = W*x_data_input+b
loss = np.sum((y_data_input - y_gd_approx)**2)/x_data_input.shape[0]
#Print execution time
time_end = timeit.default_timer()
print('')
print("Time to execute code: {0:0.9f} sec.".format(time_end - time_start))
print('')
# %% Print results
print('')
print('Iteration = {0:0.3f}'.format(total_iterations))
print('W_val = {0:0.3f}'.format(W))
print('b_val = {0:0.3f}'.format(b))
print('')
Respuestas
Creo que es el resultado de un gran número de iteraciones. Cambié el número de iteración de 1e5
a 1e3
y también cambié x de x_data_input = np.arange(100, step=0.1)
a x_data_input = np.arange(100, step=0.0001)
. De esta manera, reduje el número de iteraciones pero aumenté el cálculo en 10x. Con np se hace en 22 segundos y en tensorflow se hace en 25 segundos .
Mi conjetura: tensorflow tiene mucha sobrecarga en cada iteración (para darnos un marco que puede hacer mucho) pero la velocidad de pase hacia adelante y hacia atrás está bien.
La respuesta real a mi pregunta está oculta en los diversos comentarios. Para futuros lectores, resumiré estos hallazgos en esta respuesta.
Acerca de la diferencia de velocidad entre TensorFlow y una implementación de Python / NumPy sin procesar
Esta parte de la respuesta es bastante lógica.
Cada iteración (= cada llamada de Session.run()
) TensorFlow realiza cálculos. TensorFlow tiene una gran sobrecarga para iniciar cada cálculo. En la GPU, esta sobrecarga es incluso peor que en la CPU. Sin embargo, TensorFlow ejecuta los cálculos reales de manera muy eficiente y más eficiente que la implementación de Python / NumPy sin procesar anterior.
Entonces, cuando la cantidad de puntos de datos aumenta y, por lo tanto, la cantidad de cálculos por iteración, verá que el rendimiento relativo entre TensorFlow y Python / NumPy cambia en la ventaja de TensorFlow. Lo opuesto también es cierto.
El problema descrito en la pregunta es muy pequeño, lo que significa que el número de cálculos es muy bajo mientras que el número de iteraciones es muy grande. Es por eso que TensorFlow funciona tan mal. Este tipo de pequeños problemas no es el caso de uso típico para el que se diseñó TensorFlow.
Para reducir el tiempo de ejecución
¡Aún así, el tiempo de ejecución del script de TensorFlow se puede reducir mucho! Para reducir el tiempo de ejecución, se debe reducir el número de iteraciones (no importa el tamaño del problema, este es un buen objetivo de todos modos).
Como señaló @ amin, esto se logra escalando los datos de entrada. Una explicación muy breve de por qué funciona esto: el tamaño del gradiente y las actualizaciones de las variables están más equilibrados en comparación con los valores absolutos para los que se encuentran los valores. Por lo tanto, se requieren menos pasos (= iteraciones).
Siguiendo el consejo de @ amin, finalmente terminé escalando mis datos x de la siguiente manera (se repite parte del código para aclarar la posición del nuevo código):
# Tensorflow is finicky about shapes, so resize
x_data = np.reshape(x_data_input, (n_samples, 1))
y_data = np.reshape(y_data_input, (n_samples, 1))
### START NEW CODE ###
# Scale x_data
x_mean = np.mean(x_data)
x_std = np.std(x_data)
x_data = (x_data - x_mean) / x_std
### END NEW CODE ###
# Define placeholders for input
X = tf.placeholder(tf.float32, shape=(n_samples, 1), name="tf_x_data")
Y = tf.placeholder(tf.float32, shape=(n_samples, 1), name="tf_y_data")
El escalado acelera la convergencia en un factor de 1000. En lugar de 1e5 iterations
, 1e2 iterations
se necesitan. Esto se debe en parte a que step size of 1e-1
se puede utilizar un máximo en lugar de un step size of 1e-4
.
Tenga en cuenta que el peso y el sesgo encontrados son diferentes y que debe alimentar datos escalados a partir de ahora.
Opcionalmente, puede optar por eliminar la escala del peso y el sesgo encontrados para poder alimentar datos sin escala. El desescalado se realiza usando este código (poner en algún lugar al final del código):
#%% Unscaling
W_val_unscaled = W_val[0,0]/x_std
b_val_unscaled = b_val[0]-x_mean*W_val[0,0]/x_std