Градиентный спуск с использованием TensorFlow намного медленнее, чем базовая реализация Python, почему?
Я слежу за курсом машинного обучения. У меня есть простая проблема линейной регрессии (LR), которая помогает мне привыкнуть к TensorFlow. Проблема LR состоит в том, чтобы найти параметры a
и b
такие, которые Y = a*X + b
аппроксимируют (x, y)
облако точек (которое я создал сам для простоты).
Я решаю эту проблему LR, используя «градиентный спуск с фиксированным размером шага (FSSGD)». Я реализовал его с помощью TensorFlow, и он работает, но я заметил, что он очень медленный как на GPU, так и на CPU. Поскольку мне было любопытно, я сам реализовал FSSGD на Python / NumPy, и, как и ожидалось, он работает намного быстрее, примерно:
- В 10 раз быстрее, чем TF @ CPU
- В 20 раз быстрее, чем TF @ GPU
Если TensorFlow настолько медленный, я не могу представить, что так много людей используют этот фреймворк. Так что я, должно быть, делаю что-то не так. Может ли кто-нибудь помочь мне, чтобы я мог ускорить реализацию TensorFlow.
Меня НЕ интересует разница между производительностью CPU и GPU. Оба показателя эффективности представлены только для полноты и иллюстрации. Меня интересует, почему моя реализация TensorFlow намного медленнее, чем необработанная реализация Python / NumPy.
В качестве справки я добавляю свой код ниже.
- Разрезан до минимального (но полностью рабочего) примера.
- Использование
Python v3.7.9 x64
. - Используется
tensorflow-gpu==1.15
на данный момент (поскольку в курсе используется TensorFlow v1) - Протестировано для работы как в Spyder, так и в PyCharm.
Моя реализация FSSGD с использованием TensorFlow (время выполнения от 40 секунд @CPU до 80 секунд @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('')
Моя собственная реализация FSSGD на Python (время выполнения около 4 секунд):
#%% 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('')
Ответы
Думаю, это результат большого количества итераций. Я изменил номер итерации с 1e5
на, 1e3
а также изменил x с x_data_input = np.arange(100, step=0.1)
на x_data_input = np.arange(100, step=0.0001)
. Таким образом я уменьшил количество итераций, но увеличил вычисление в 10 раз. С np это делается за 22 секунды, а в тензорном потоке - за 25 секунд .
Мое предположение: у tenorflow много накладных расходов на каждой итерации (чтобы дать нам фреймворк, который может многое), но скорость прямого и обратного прохода в порядке.
Фактический ответ на мой вопрос скрыт в различных комментариях. Для будущих читателей я резюмирую эти выводы в этом ответе.
О разнице в скорости между TensorFlow и необработанной реализацией Python / NumPy
Эта часть ответа на самом деле вполне логична.
Каждая итерация (= каждый вызов Session.run()
) TensorFlow выполняет вычисления. У TensorFlow большие накладные расходы на запуск каждого вычисления. На GPU эти накладные расходы даже хуже, чем на CPU. Однако TensorFlow выполняет фактические вычисления очень эффективно и более эффективно, чем это делает приведенная выше необработанная реализация Python / NumPy.
Итак, когда количество точек данных увеличится и, следовательно, количество вычислений на итерацию, вы увидите, что относительная производительность между TensorFlow и Python / NumPy сдвигается в пользу TensorFlow. Обратное тоже верно.
Проблема, описанная в вопросе, очень мала, что означает, что количество вычислений очень мало, а количество итераций очень велико. Вот почему TensorFlow так плохо работает. Этот тип небольших проблем не является типичным вариантом использования, для которого был разработан TensorFlow.
Чтобы сократить время выполнения
Тем не менее, время выполнения сценария TensorFlow можно значительно сократить! Чтобы сократить время выполнения, необходимо уменьшить количество итераций (независимо от размера проблемы, в любом случае это хорошая цель).
Как отметил @amin, это достигается путем масштабирования входных данных. Очень краткое объяснение, почему это работает: размер обновлений градиента и переменных более сбалансирован по сравнению с абсолютными значениями, для которых значения должны быть найдены. Следовательно, требуется меньше шагов (= итераций).
Следуя совету @amin, я, наконец, закончил масштабированием своих x-данных следующим образом (некоторый код повторяется, чтобы сделать положение нового кода ясным):
# 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")
Масштабирование ускоряет сходимость в 1000 раз. Вместо 1e5 iterations
, 1e2 iterations
необходимы. Частично это связано с тем, step size of 1e-1
что вместо файла step size of 1e-4
.
Обратите внимание, что найденный вес и смещение отличаются, и с этого момента вы должны вводить масштабированные данные.
При желании вы можете отменить масштабирование найденного веса и смещения, чтобы вы могли передавать немасштабированные данные. Масштабирование выполняется с помощью этого кода (поместите где-нибудь в конце кода):
#%% Unscaling
W_val_unscaled = W_val[0,0]/x_std
b_val_unscaled = b_val[0]-x_mean*W_val[0,0]/x_std