Der Gradientenabstieg mit TensorFlow ist viel langsamer als eine grundlegende Python-Implementierung. Warum?

Dec 29 2020

Ich folge einem maschinellen Lernkurs. Ich habe ein einfaches lineares Regressionsproblem (LR), das mir hilft, mich an TensorFlow zu gewöhnen. Das LR Problem ist , Parameter zu finden aund bsolche , die Y = a*X + beine annähert (x, y)Punktwolke (die ich mich aus Gründen der Einfachheit erzeugt).

Ich löse dieses LR-Problem mit einem FSSGD (Fixed Step Size Gradient Descent). Ich habe es mit TensorFlow implementiert und es funktioniert, aber ich habe festgestellt, dass es sowohl auf der GPU als auch auf der CPU sehr langsam ist. Weil ich neugierig war, habe ich die FSSGD selbst in Python / NumPy implementiert und wie erwartet läuft dies viel schneller, ungefähr:

  • 10x schneller als TF @ CPU
  • 20x schneller als TF @ GPU

Wenn TensorFlow so langsam ist, kann ich mir nicht vorstellen, dass so viele Leute dieses Framework verwenden. Also muss ich etwas falsch machen. Kann mir jemand helfen, damit ich meine TensorFlow-Implementierung beschleunigen kann?

Der Unterschied zwischen der CPU- und der GPU-Leistung interessiert mich NICHT. Beide Leistungsindikatoren dienen lediglich der Vollständigkeit und Veranschaulichung. Ich bin daran interessiert, warum meine TensorFlow-Implementierung so viel langsamer ist als eine rohe Python / NumPy-Implementierung.

Als Referenz füge ich meinen Code unten hinzu.

  • Auf ein minimales (aber voll funktionsfähiges) Beispiel reduziert.
  • Verwenden von Python v3.7.9 x64.
  • Wird tensorflow-gpu==1.15vorerst verwendet (da der Kurs TensorFlow v1 verwendet)
  • Getestet, um sowohl in Spyder als auch in PyCharm zu laufen.

Meine FSSGD-Implementierung mit TensorFlow (Ausführungszeit ca. 40 Sek. @CPU bis 80 Sek. @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('')

Meine eigene Python-FSSGD-Implementierung (Ausführungszeit ca. 4 Sek.):

#%% 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('')

Antworten

1 amin Dec 29 2020 at 21:12

Ich denke, es ist das Ergebnis einer großen Iterationszahl. Ich habe die Iterationsnummer von 1e5bis 1e3geändert und auch x von x_data_input = np.arange(100, step=0.1)bis geändert x_data_input = np.arange(100, step=0.0001). Auf diese Weise habe ich die Iterationszahl reduziert, aber die Berechnung um das 10-fache erhöht. Mit np ist es in 22 Sekunden erledigt und im Tensorflow in 25 Sekunden .

Meine Vermutung: Tensorflow hat in jeder Iteration viel Overhead (um uns ein Framework zu geben, das viel kann), aber die Vorwärts- und Rückwärtsdurchlaufgeschwindigkeit sind in Ordnung.

Stefan Dec 31 2020 at 17:35

Die eigentliche Antwort auf meine Frage ist in den verschiedenen Kommentaren versteckt. Für zukünftige Leser werde ich diese Ergebnisse in dieser Antwort zusammenfassen.

Informationen zum Geschwindigkeitsunterschied zwischen TensorFlow und einer rohen Python / NumPy-Implementierung

Dieser Teil der Antwort ist eigentlich ziemlich logisch.

Jede Iteration (= jeder Aufruf von Session.run()) TensorFlow führt Berechnungen durch. TensorFlow hat einen großen Aufwand für das Starten jeder Berechnung. Auf der GPU ist dieser Overhead noch schlimmer als auf der CPU. TensorFlow führt die eigentlichen Berechnungen jedoch sehr effizient und effizienter aus als die oben beschriebene Python / NumPy-Rohimplementierung.

Wenn also die Anzahl der Datenpunkte und damit die Anzahl der Berechnungen pro Iteration erhöht wird, werden Sie feststellen, dass sich die relativen Leistungen zwischen TensorFlow und Python / NumPy im Vorteil von TensorFlow verschieben. Das Gegenteil ist auch der Fall.

Das in der Frage beschriebene Problem ist sehr klein, was bedeutet, dass die Anzahl der Berechnungen sehr gering ist, während die Anzahl der Iterationen sehr groß ist. Deshalb arbeitet TensorFlow so schlecht. Diese Art von kleinen Problemen ist nicht der typische Anwendungsfall, für den TensorFlow entwickelt wurde.

Um die Ausführungszeit zu reduzieren

Trotzdem kann die Ausführungszeit des TensorFlow-Skripts erheblich reduziert werden! Um die Ausführungszeit zu verkürzen, muss die Anzahl der Iterationen reduziert werden (unabhängig von der Größe des Problems ist dies ohnehin ein gutes Ziel).

Wie @ amin hervorhob, wird dies durch Skalieren der Eingabedaten erreicht. Eine sehr kurze Erklärung, warum dies funktioniert: Die Größe des Gradienten und die Variablenaktualisierungen sind ausgeglichener als die absoluten Werte, für die die Werte zu finden sind. Daher sind weniger Schritte (= Iterationen) erforderlich.

Nach dem Rat von @ amin habe ich schließlich meine x-Daten wie folgt skaliert (ein Teil des Codes wird wiederholt, um die Position des neuen Codes zu verdeutlichen):

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

Skalierung beschleunigt die Konvergenz um einen Faktor 1000 statt 1e5 iterations, 1e2 iterationsbenötigt werden . Dies liegt teilweise daran, step size of 1e-1dass anstelle von a ein Maximum verwendet werden kann step size of 1e-4.

Bitte beachten Sie, dass das gefundene Gewicht und die Abweichung unterschiedlich sind und Sie von nun an skalierte Daten eingeben müssen.

Optional können Sie das gefundene Gewicht und die Abweichung entkalken, um nicht skalierte Daten einzugeben. Das Entkalken erfolgt mit diesem Code (irgendwo am Ende des Codes):

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