Zejście gradientowe przy użyciu TensorFlow jest znacznie wolniejsze niż podstawowa implementacja Pythona, dlaczego?
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 a
i b
takich, które Y = a*X + b
przybliż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.15
na 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
Myślę, że to wynik dużej liczby iteracji. Zmieniłem numer iteracji z 1e5
na, 1e3
a 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.
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 iterations
są 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