La descente de gradient à l'aide de TensorFlow est beaucoup plus lente qu'une implémentation Python de base, pourquoi?
Je suis un cours d'apprentissage automatique. J'ai un simple problème de régression linéaire (LR) pour m'aider à m'habituer à TensorFlow. Le problème LR est de trouver des paramètres a
et b
tels qui se Y = a*X + b
rapproche d'un (x, y)
nuage de points (que j'ai généré moi-même par souci de simplicité).
Je résous ce problème de LR en utilisant une `` descente de gradient à pas fixe (FSSGD) ''. Je l'ai implémenté en utilisant TensorFlow et cela fonctionne mais j'ai remarqué qu'il est vraiment lent à la fois sur le GPU et le CPU. Parce que j'étais curieux, j'ai implémenté le FSSGD moi-même dans Python / NumPy et, comme prévu, cela fonctionne beaucoup plus rapidement, environ:
- 10x plus rapide que TF @ CPU
- 20x plus rapide que TF @ GPU
Si TensorFlow est aussi lent, je ne peux pas imaginer que tant de gens utilisent ce framework. Je dois donc faire quelque chose de mal. Quelqu'un peut-il m'aider afin que je puisse accélérer ma mise en œuvre TensorFlow.
Je ne suis PAS intéressé par la différence entre les performances du CPU et du GPU. Les deux indicateurs de performance ne sont fournis qu'à titre d'exhaustivité et d'illustration. Je m'intéresse à la raison pour laquelle mon implémentation TensorFlow est tellement plus lente qu'une implémentation brute Python / NumPy.
A titre de référence, j'ajoute mon code ci-dessous.
- Dépouillé à un exemple minimal (mais pleinement fonctionnel).
- Utilisation
Python v3.7.9 x64
. - Utilisé
tensorflow-gpu==1.15
pour l'instant (car le cours utilise TensorFlow v1) - Testé pour fonctionner à la fois dans Spyder et PyCharm.
Ma mise en œuvre FSSGD à l'aide de TensorFlow (temps d'exécution d'environ 40 sec @CPU à 80 sec @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('')
Ma propre implémentation Python FSSGD (temps d'exécution d'environ 4 sec):
#%% 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('')
Réponses
Je pense que c'est le résultat d'un grand nombre d'itérations. J'ai changé le numéro d'itération de 1e5
à 1e3
et également changé x de x_data_input = np.arange(100, step=0.1)
à x_data_input = np.arange(100, step=0.0001)
. De cette façon, j'ai réduit le nombre d'itérations mais augmenté le calcul de 10x. Avec np, cela se fait en 22 secondes et en tensorflow, cela se fait en 25 secondes .
Ma supposition: tensorflow a beaucoup de frais généraux à chaque itération (pour nous donner un cadre qui peut faire beaucoup) mais la vitesse de passe avant et arrière est correcte.
La vraie réponse à ma question est cachée dans les différents commentaires. Pour les futurs lecteurs, je résumerai ces résultats dans cette réponse.
À propos de la différence de vitesse entre TensorFlow et une implémentation brute de Python / NumPy
Cette partie de la réponse est en fait assez logique.
Chaque itération (= chaque appel de Session.run()
) TensorFlow effectue des calculs. TensorFlow a une surcharge importante pour le démarrage de chaque calcul. Sur le GPU, cette surcharge est encore pire que sur le CPU. Cependant, TensorFlow exécute les calculs réels de manière très efficace et plus efficace que l'implémentation brute Python / NumPy ci-dessus.
Ainsi, lorsque le nombre de points de données est augmenté, et donc le nombre de calculs par itération, vous verrez que les performances relatives entre TensorFlow et Python / NumPy se déplacent à l'avantage de TensorFlow. L'inverse est également vrai.
Le problème décrit dans la question est très petit, ce qui signifie que le nombre de calculs est très faible alors que le nombre d'itérations est très grand. C'est pourquoi TensorFlow fonctionne si mal. Ce type de petits problèmes n'est pas le cas d'utilisation typique pour lequel TensorFlow a été conçu.
Pour réduire le temps d'exécution
Pourtant, le temps d'exécution du script TensorFlow peut être considérablement réduit! Pour réduire le temps d'exécution, le nombre d'itérations doit être réduit (quelle que soit la taille du problème, c'est quand même un bon objectif).
Comme @ amin l'a souligné, ceci est réalisé en mettant à l'échelle les données d'entrée. Une explication très brève pourquoi cela fonctionne: la taille du gradient et les mises à jour des variables sont plus équilibrées par rapport aux valeurs absolues pour lesquelles les valeurs doivent être trouvées. Par conséquent, moins d'étapes (= itérations) sont nécessaires.
Suivant les conseils de @ amin, j'ai finalement mis à l'échelle mes données x comme suit (du code est répété pour rendre la position du nouveau code claire):
# 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")
La mise à l'échelle accélère la convergence d'un facteur 1000. Au lieu de 1e5 iterations
, 1e2 iterations
sont nécessaires. Ceci est en partie dû au fait qu'un maximum step size of 1e-1
peut être utilisé au lieu d'un step size of 1e-4
.
Veuillez noter que le poids et le biais trouvés sont différents et que vous devez désormais alimenter des données mises à l'échelle.
Si vous le souhaitez, vous pouvez choisir de décaler le poids et le biais trouvés afin de pouvoir alimenter des données non mises à l'échelle. La non-mise à l'échelle se fait à l'aide de ce code (placé quelque part à la fin du code):
#%% Unscaling
W_val_unscaled = W_val[0,0]/x_std
b_val_unscaled = b_val[0]-x_mean*W_val[0,0]/x_std