Zejście gradientowe przy użyciu TensorFlow jest znacznie wolniejsze niż podstawowa implementacja Pythona, dlaczego?

Dec 29 2020

Idę na kurs uczenia maszynowego. Mam prosty problem z regresją liniową (LR), który pomoże mi przyzwyczaić się do TensorFlow. Problemem LR jest znalezienie parametrów ai btakich, które Y = a*X + bprzybliżają (x, y)chmurę punktów (którą sam wygenerowałem dla uproszczenia).

Rozwiązuję ten problem LR za pomocą „stałego spadku wielkości kroku (FSSGD)”. Zaimplementowałem to za pomocą TensorFlow i działa, ale zauważyłem, że działa bardzo wolno zarówno na GPU, jak i CPU. Ponieważ byłem ciekawy, sam zaimplementowałem FSSGD w Pythonie / NumPy i zgodnie z oczekiwaniami działa to znacznie szybciej, około:

  • 10x szybszy niż TF @ CPU
  • 20x szybszy niż TF @ GPU

Jeśli TensorFlow działa tak wolno, nie wyobrażam sobie, że tak wiele osób korzysta z tego frameworka. Więc chyba robię coś złego. Czy ktoś może mi pomóc, abym mógł przyspieszyć wdrażanie TensorFlow.

NIE interesuje mnie różnica między wydajnością procesora i karty graficznej. Oba wskaźniki wydajności podano jedynie w celu zapewnienia kompletności i ilustracji. Interesuje mnie, dlaczego moja implementacja TensorFlow jest o wiele wolniejsza niż surowa implementacja języka Python / NumPy.

Jako odniesienie dodaję poniżej mój kod.

  • Ograniczony do minimalnego (ale w pełni działającego) przykładu.
  • Korzystanie Python v3.7.9 x64.
  • Używany tensorflow-gpu==1.15na razie (ponieważ kurs korzysta z TensorFlow v1)
  • Przetestowano pod kątem działania zarówno w Spyder, jak i PyCharm.

Moja implementacja FSSGD przy użyciu TensorFlow (czas wykonania około 40 sekund przy procesorze do 80 sekund przy 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('')

Moja własna implementacja FSSGD w Pythonie (czas wykonania około 4 sekundy):

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

Odpowiedzi

1 amin Dec 29 2020 at 21:12

Myślę, że to wynik dużej liczby iteracji. Zmieniłem numer iteracji z 1e5na, 1e3a także zmieniłem x z x_data_input = np.arange(100, step=0.1)na x_data_input = np.arange(100, step=0.0001). W ten sposób zmniejszyłem liczbę iteracji, ale zwiększyłem obliczenia o 10x. W przypadku np. Zajmuje to 22 sekundy, aw przypadku tensorflow - 25 sekund .

Moje przypuszczenie: tensorflow ma dużo narzutów w każdej iteracji (aby dać nam strukturę, która może wiele zdziałać), ale prędkość przejścia do przodu i do tyłu jest w porządku.

Stefan Dec 31 2020 at 17:35

Rzeczywista odpowiedź na moje pytanie jest ukryta w różnych komentarzach. Dla przyszłych czytelników podsumuję te ustalenia w tej odpowiedzi.

O różnicy prędkości między TensorFlow a surową implementacją Python / NumPy

Ta część odpowiedzi jest właściwie całkiem logiczna.

Każda iteracja (= każde wywołanie Session.run()) TensorFlow wykonuje obliczenia. TensorFlow ma duże obciążenie związane z rozpoczęciem każdego obliczenia. W przypadku GPU to obciążenie jest jeszcze gorsze niż w przypadku procesora. Jednak TensorFlow wykonuje rzeczywiste obliczenia bardzo wydajnie i wydajniej niż powyższa surowa implementacja Python / NumPy.

Tak więc, gdy liczba punktów danych wzrośnie, a tym samym liczba obliczeń na iterację, zobaczysz, że względne wydajności między TensorFlow i Python / NumPy zmieniają się na korzyść TensorFlow. Jest też odwrotnie.

Problem opisany w pytaniu jest bardzo mały, co oznacza, że ​​liczba obliczeń jest bardzo mała, podczas gdy liczba iteracji jest bardzo duża. Dlatego TensorFlow działa tak źle. Ten typ drobnych problemów nie jest typowym przypadkiem użycia, dla którego zaprojektowano TensorFlow.

Aby skrócić czas wykonywania

Mimo to czas wykonywania skryptu TensorFlow można znacznie skrócić! Aby skrócić czas wykonywania, należy zmniejszyć liczbę iteracji (bez względu na rozmiar problemu, jest to i tak dobry cel).

Jak zauważył @amin, osiąga się to poprzez skalowanie danych wejściowych. Bardzo krótkie wyjaśnienie, dlaczego to działa: rozmiar gradientu i aktualizacje zmiennych są bardziej zrównoważone w porównaniu z wartościami bezwzględnymi, dla których mają zostać znalezione wartości. Dlatego potrzeba mniej kroków (= iteracji).

Postępując zgodnie z radą @amina, w końcu skończyłem skalując moje dane x w następujący sposób (część kodu jest powtarzana, aby nowy kod był jasny):

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

Skalowanie przyspieszyć konwergencję przez współczynnik 1000. Zamiast 1e5 iterations, 1e2 iterationssą potrzebne. Dzieje się tak częściowo dlatego, step size of 1e-1że zamiast a step size of 1e-4.

Zwróć uwagę, że znaleziona waga i odchylenie są różne i od tej pory musisz podawać skalowane dane.

Opcjonalnie możesz usunąć skalowanie znalezionej wagi i odchylenia, aby móc podawać nieskalowane dane. Odskalowanie odbywa się za pomocą tego kodu (umieszczonego gdzieś na końcu kodu):

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