La discesa del gradiente utilizzando TensorFlow è molto più lenta di un'implementazione di base di Python, perché?

Dec 29 2020

Sto seguendo un corso di machine learning. Ho un semplice problema di regressione lineare (LR) che mi aiuta ad abituarmi a TensorFlow. Il problema di LR è trovare parametri ae btali che Y = a*X + bapprossimino una (x, y)nuvola di punti (che ho generato io stesso per semplicità).

Sto risolvendo questo problema di LR utilizzando una "discesa del gradiente a gradini fissi (FSSGD)". L'ho implementato utilizzando TensorFlow e funziona ma ho notato che è molto lento sia su GPU che su CPU. Poiché ero curioso, ho implementato personalmente FSSGD in Python / NumPy e come previsto funziona molto più velocemente, su:

  • 10 volte più veloce di TF @ CPU
  • 20 volte più veloce di TF @ GPU

Se TensorFlow è così lento, non riesco a immaginare che così tante persone utilizzino questo framework. Quindi devo fare qualcosa di sbagliato. Qualcuno può aiutarmi in modo da poter accelerare la mia implementazione di TensorFlow.

NON mi interessa la differenza tra le prestazioni della CPU e della GPU. Entrambi gli indicatori di performance sono forniti solo per completezza e illustrazione. Sono interessato al motivo per cui la mia implementazione di TensorFlow è molto più lenta di un'implementazione grezza di Python / NumPy.

Come riferimento, aggiungo il mio codice di seguito.

  • Ridotto a un esempio minimo (ma completamente funzionante).
  • Utilizzando Python v3.7.9 x64.
  • Utilizzato tensorflow-gpu==1.15per ora (perché il corso utilizza TensorFlow v1)
  • Testato per funzionare sia in Spyder che in PyCharm.

La mia implementazione FSSGD utilizzando TensorFlow (tempo di esecuzione da circa 40 sec @CPU a 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('')

La mia implementazione FSSGD di python (tempo di esecuzione circa 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('')

Risposte

1 amin Dec 29 2020 at 21:12

Penso che sia il risultato di un grande numero di iterazioni. Ho cambiato il numero di iterazione da 1e5a 1e3e ho anche cambiato x da x_data_input = np.arange(100, step=0.1)a x_data_input = np.arange(100, step=0.0001). In questo modo ho ridotto il numero di iterazioni ma aumentato il calcolo di 10 volte. Con np è fatto in 22 sec e in tensorflow è fatto in 25 sec .

La mia ipotesi: tensorflow ha un sacco di overhead in ogni iterazione (per darci un quadro che può fare molto) ma la velocità di passaggio in avanti e all'indietro sono ok.

Stefan Dec 31 2020 at 17:35

La risposta effettiva alla mia domanda è nascosta nei vari commenti. Per i futuri lettori, riassumerò questi risultati in questa risposta.

Informazioni sulla differenza di velocità tra TensorFlow e un'implementazione Python / NumPy non elaborata

Questa parte della risposta è in realtà abbastanza logica.

Ogni iterazione (= ogni chiamata di Session.run()) TensorFlow esegue i calcoli. TensorFlow ha un grande overhead per l'avvio di ogni calcolo. Sulla GPU, questo sovraccarico è persino peggiore che sulla CPU. Tuttavia, TensorFlow esegue i calcoli effettivi in ​​modo molto efficiente e più efficiente rispetto all'implementazione grezza di Python / NumPy di ​​cui sopra.

Quindi, quando il numero di punti dati aumenta, e quindi il numero di calcoli per iterazione, vedrai che le prestazioni relative tra TensorFlow e Python / NumPy cambiano a vantaggio di TensorFlow. È vero anche il contrario.

Il problema descritto nella domanda è molto piccolo, il che significa che il numero di calcoli è molto basso mentre il numero di iterazioni è molto grande. Ecco perché TensorFlow funziona così male. Questo tipo di piccoli problemi non è il tipico caso d'uso per il quale TensorFlow è stato progettato.

Per ridurre i tempi di esecuzione

Tuttavia, il tempo di esecuzione dello script TensorFlow può essere notevolmente ridotto! Per ridurre il tempo di esecuzione è necessario ridurre il numero di iterazioni (indipendentemente dalla dimensione del problema, questo è comunque un buon obiettivo).

Come sottolineato da @ amin, ciò si ottiene ridimensionando i dati di input. Una spiegazione molto breve del perché funziona: la dimensione del gradiente e gli aggiornamenti delle variabili sono più equilibrati rispetto ai valori assoluti per i quali si devono trovare i valori. Pertanto, sono necessari meno passaggi (= iterazioni).

Seguendo il consiglio di @ amin, ho finalmente finito per ridimensionare i miei dati x come segue (un po 'di codice viene ripetuto per rendere chiara la posizione del nuovo codice):

# 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")

Il ridimensionamento accelera la convergenza di un fattore 1000. Invece di 1e5 iterations, 1e2 iterationssono necessari. Ciò è in parte dovuto al fatto che è step size of 1e-1possibile utilizzare un massimo al posto di un file step size of 1e-4.

Tieni presente che il peso e il bias trovati sono diversi e che d'ora in poi devi fornire dati in scala.

Facoltativamente, puoi scegliere di annullare la scala del peso e del bias trovati in modo da poter fornire dati non graduati. L'annullamento della scala viene eseguito utilizzando questo codice (messo da qualche parte alla fine del codice):

#%% Unscaling
W_val_unscaled = W_val[0,0]/x_std
b_val_unscaled = b_val[0]-x_mean*W_val[0,0]/x_std