Содержание страницы
Импульс
Здравствуйте и вновь добро пожаловать на занятия по теме «Обработка данных: практическое глубокое обучение в Theano и TensorFlow».
Вся эта лекция посвящена импульсу. Импульс – это следующая методика, которую мы обсудим и которая позволяет ускорить обучение нейронной сети. По сути она похожа на физический импульс – она позволяет более быстро двигаться в уже избранном направлении.
Существует два типа импульса – обычный, использующий интуитивное понимание импульса, и импульс Нестерова, алгебраически более сложный и более трудный для понимания, но я упоминаю и его, чтобы вы почитали о нём, если вам станет интересно.
Итак, в случае обычного импульса мы позволяем быть прежней скорости изменения весовых коэффициентов:
после чего обновляем весовые коэффициенты по следующей формуле:
то есть весовой коэффициент равен импульсу, умноженному на предыдущую скорость, минус коэффициент обучения, умноженному на градиент функции ошибок.
Основная идея импульса Нестерова заключается в том, что мы смотрим вперёд и в случае ошибки делаем поправку.
Проводя аналогию с кинематикой, можно сказать, что мы предсказываем наше следующее местоположение, учитывая текущее местоположение и нашу скорость. Вычислив точку следующего местоположения, мы корректируем направление движения, так что конечное местоположение будет результатом сложения двух векторов.
Если желаете, вы можете произвести перестановку переменных, чтобы корректировка выражалась в значениях старой и новой скоростей:
Именно такую формулировку мы и будем использовать в коде.
Про импульс Нестерова есть много материала со строгим теоретическим доказательством, и я рекомендую ознакомиться с ним, если вы любите математику.
Код для обучения нейронной сети с применением импульса
В этом разделе я покажу вам код для реализации импульса. Кроме того, мы сравним градиентный спуск с применением импульса и без него.
Если вы хотите лишь загрузить код, не набирая его самостоятельно, перейдите по адресу и найдите папку ann_class2. Соответствующие файлы для данной конкретной лекции называются momentum.py и mlp.py.
Mlp.py содержит лишь код для градиентного спуска с использованием многослойного персептрона. У нас тут две версии многослойного персептрона – с использованием сигмоиды и с использованием усечённого линейного преобразователя (функции relu). Код для них мы рассматривали в предыдущем курсе – «Глубокое обучение на языке Python, часть 1».
Итак, прежде всего мы запустим код для импульса в версии с использованием сигмоиды.
import numpy as np
def forward(X, W1, b1, W2, b2):
Z = 1 / (1 + np.exp(-( X.dot(W1) + b1 )))
# rectifier
# Z = X.dot(W1) + b1
# Z[Z < 0] = 0
A = Z.dot(W2) + b2
expA = np.exp(A)
Y = expA / expA.sum(axis=1, keepdims=True)
return Y, Z
def derivative_w2(Z, T, Y):
return Z.T.dot(Y – T)
def derivative_b2(T, Y):
return (Y – T).sum(axis=0)
def derivative_w1(X, Z, T, Y, W2):
return X.T.dot( ( ( Y-T ).dot(W2.T) * ( Z*(1 – Z) ) ) ) # for sigmoid
# return X.T.dot( ( ( Y-T ).dot(W2.T) * (Z > 0) ) ) # for relu
def derivative_b1(Z, T, Y, W2):
return (( Y-T ).dot(W2.T) * ( Z*(1 – Z) )).sum(axis=0) # for sigmoid
# return (( Y-T ).dot(W2.T) * (Z > 0)).sum(axis=0) # for relu
Теперь пройдёмся по тексту кода файла momentum.py. Итак, мы импортируем все обычные библиотеки, а также все необходимые функции из файла util.py – get_normalized_data, error_rate, cost и y2indicator, и из файла mlp.py – функцию forward и все функции для вычисления производных.
import numpy as np
from sklearn.utils import shuffle
import matplotlib.pyplot as plt
from util import get_normalized_data, error_rate, cost, y2indicator
from mlp import forward, derivative_w2, derivative_w1, derivative_b2, derivative_b1
Я установил количество итераций, равное 20, поскольку мы будем использовать пакетный градиентный спуск. Будем выводить результат на экран через каждых 10 этапов.
def main():
max_iter = 20 # make it 30 for sigmoid
print_period = 10
Далее нормализуем наши X и Y, устанавливаем коэффициент обучения равным 0,00004 и регуляризацию, равную 0,01. Данные уже перемешаны, поэтому просто оставляем последние 1 000 примеров в качестве проверочного набора, остальные служат учебным. Далее, как обычно, с помощью функции y2indicator преобразуем целевые переменные в матрицы. Размер пакета установим равным 500, а количество скрытых узлов – равным 300. На предыдущих лекциях я рассказывал, как выбираются именно такие цифры. После этого инициируем наши весовые коэффициенты и свободные члены.
X, Y = get_normalized_data()
lr = 0.00004
reg = 0.01
Xtrain = X[:-1000,]
Ytrain = Y[:-1000]
Xtest = X[-1000:,]
Ytest = Y[-1000:]
Ytrain_ind = y2indicator(Ytrain)
Ytest_ind = y2indicator(Ytest)
N, D = Xtrain.shape
batch_sz = 500
n_batches = N // batch_sz
M = 300
K = 10
W1 = np.random.randn(D, M) / 28
b1 = np.zeros(M)
W2 = np.random.randn(M, K) / np.sqrt(M)
b2 = np.zeros(K)
В первом случае используем обычный пакетный градиентный спуск с вычислением и выводом на экран значения функции затрат и коэффициента ошибок.
# 1. batch
# cost = -16
LL_batch = []
CR_batch = []
for i in xrange(max_iter):
for j in xrange(n_batches):
Xbatch = Xtrain[j*batch_sz:(j*batch_sz + batch_sz),]
Ybatch = Ytrain_ind[j*batch_sz:(j*batch_sz + batch_sz),]
pYbatch, Z = forward(Xbatch, W1, b1, W2, b2)
# print “first batch cost:”, cost(pYbatch, Ybatch)
# updates
W2 -= lr*(derivative_w2(Z, Ybatch, pYbatch) + reg*W2)
b2 -= lr*(derivative_b2(Ybatch, pYbatch) + reg*b2)
W1 -= lr*(derivative_w1(Xbatch, Z, Ybatch, pYbatch, W2) + reg*W1)
b1 -= lr*(derivative_b1(Z, Ybatch, pYbatch, W2) + reg*b1)
if j % print_period == 0:
# calculate just for LL
pY, _ = forward(Xtest, W1, b1, W2, b2)
ll = cost(pY, Ytest_ind)
LL_batch.append(ll)
print(“Cost at iteration i=%d, j=%d: %.6f” % (i, j, ll))
err = error_rate(pY, Ytest)
CR_batch.append(err)
print (“Error rate:”, err)
pY, _ = forward(Xtest, W1, b1, W2, b2)
print (“Final error rate:”, error_rate(pY, Ytest))
Во втором случае применим пакетный градиентный спуск с использованием импульса, поэтому у нас появился новый гиперпараметр – величина импульса. Установим её равной 0,9. Кроме того, поскольку нам необходимо следить за предыдущими значениями весовых коэффициентов, обнулим их. Далее – всё как раньше, разве что несколько изменена формула вычисления весовых коэффициентов, поскольку теперь нам необходимо учитывать импульс.
# 2. batch with momentum
W1 = np.random.randn(D, M) / 28
b1 = np.zeros(M)
W2 = np.random.randn(M, K) / np.sqrt(M)
b2 = np.zeros(K)
LL_momentum = []
CR_momentum = []
mu = 0.9
dW2 = 0
db2 = 0
dW1 = 0
db1 = 0
for i in xrange(max_iter):
for j in xrange(n_batches):
Xbatch = Xtrain[j*batch_sz:(j*batch_sz + batch_sz),]
Ybatch = Ytrain_ind[j*batch_sz:(j*batch_sz + batch_sz),]
pYbatch, Z = forward(Xbatch, W1, b1, W2, b2)
# updates
dW2 = mu*dW2 – lr*(derivative_w2(Z, Ybatch, pYbatch) + reg*W2)
W2 += dW2
db2 = mu*db2 – lr*(derivative_b2(Ybatch, pYbatch) + reg*b2)
b2 += db2
dW1 = mu*dW1 – lr*(derivative_w1(Xbatch, Z, Ybatch, pYbatch, W2) + reg*W1)
W1 += dW1
db1 = mu*db1 – lr*(derivative_b1(Z, Ybatch, pYbatch, W2) + reg*b1)
b1 += db1
if j % print_period == 0:
# calculate just for LL
pY, _ = forward(Xtest, W1, b1, W2, b2)
# print “pY:”, pY
ll = cost(pY, Ytest_ind)
LL_momentum.append(ll)
print(“Cost at iteration i=%d, j=%d: %.6f” % (i, j, ll))
err = error_rate(pY, Ytest)
CR_momentum.append(err)
print(“Error rate:”, err)
pY, _ = forward(Xtest, W1, b1, W2, b2)
print(“Final error rate:”, error_rate(pY, Ytest))
В третьем случае мы имеем пакетный градиентный спуск с импульсом Нестерова. Значение импульса при этом оставим прежним. Прежним остаётся и весь остальной код, за исключением того, как мы вычисляем новые значения весовых коэффициентов.
# 3. batch with Nesterov momentum
W1 = np.random.randn(D, M) / 28
b1 = np.zeros(M)
W2 = np.random.randn(M, K) / np.sqrt(M)
b2 = np.zeros(K)
LL_nest = []
CR_nest = []
mu = 0.9
W2 = 0
b2 = 0
W1 = 0
b1 = 0
for i in xrange(max_iter):
for j in xrange(n_batches):
Xbatch = Xtrain[j*batch_sz:(j*batch_sz + batch_sz),]
Ybatch = Ytrain_ind[j*batch_sz:(j*batch_sz + batch_sz),]
pYbatch, Z = forward(Xbatch, W1, b1, W2, b2)
# updates
dW2 = mu*mu*dW2 – (1 + mu)*lr*(derivative_w2(Z, Ybatch, pYbatch) + reg*W2)
W2 += dW2
db2 = mu*mu*db2 – (1 + mu)*lr*(derivative_b2(Ybatch, pYbatch) + reg*b2)
b2 += db2
dW1 = mu*mu*dW1 – (1 + mu)*lr*(derivative_w1(Xbatch, Z, Ybatch, pYbatch, W2) + reg*W1)
W1 += dW1
db1 = mu*mu*db1 – (1 + mu)*lr*(derivative_b1(Z, Ybatch, pYbatch, W2) + reg*b1)
b1 += db1
if j % print_period == 0:
# calculate just for LL
pY, _ = forward(Xtest, W1, b1, W2, b2)
# print “pY:”, pY
ll = cost(pY, Ytest_ind)
LL_nest.append(ll)
print (“Cost at iteration i=%d, j=%d: %.6f” % (i, j, ll))
err = error_rate(pY, Ytest)
CR_nest.append(err)
print(“Error rate:”, err)
pY, _ = forward(Xtest, W1, b1, W2, b2)
print(“Final error rate:”, error_rate(pY, Ytest))
И, наконец, выведем на экран результаты работы во всех трёх случаях, чтобы можно было их сравнить.
plt.plot(LL_batch, label=”batch”)
plt.plot(LL_momentum, label=”momentum”)
plt.plot(LL_nest, label=”nesterov”)
plt.legend()
plt.show()
if __name__ == ‘__main__’:
main()
Итак, запустим наш файл.
Как вы можете видеть, при использовании импульса результат совпадает с эталонным тестом с использованием логистической регрессии, причём с импульсом программа работает куда быстрее, чем в случае обычного пакетного градиентного спуска.
Если мы внимательнее сравним оба применённых типа импульса, то увидим, что результаты весьма схожи.
Вернёмся теперь к файлу mlp.py и изменим его таким образом, чтобы в скрытом слое вместо сигмоиды использовалась функция relu.
import numpy as np
def forward(X, W1, b1, W2, b2):
# Z = 1 / (1 + np.exp(-( X.dot(W1) + b1 )))
# rectifier
Z = X.dot(W1) + b1
Z[Z < 0] = 0
A = Z.dot(W2) + b2
expA = np.exp(A)
Y = expA / expA.sum(axis=1, keepdims=True)
return Y, Z
def derivative_w2(Z, T, Y):
return Z.T.dot(Y – T)
def derivative_b2(T, Y):
return (Y – T).sum(axis=0)
def derivative_w1(X, Z, T, Y, W2):
# return X.T.dot( ( ( Y-T ).dot(W2.T) * ( Z*(1 – Z) ) ) ) # for sigmoid
return X.T.dot( ( ( Y-T ).dot(W2.T) * (Z > 0) ) ) # for relu
def derivative_b1(Z, T, Y, W2):
# return (( Y-T ).dot(W2.T) * ( Z*(1 – Z) )).sum(axis=0) # for sigmoid
return (( Y-T ).dot(W2.T) * (Z > 0)).sum(axis=0) # for relu
Запустим теперь программу и сравним результаты. Отметим, что с функцией relu нейронная сеть работает лучше, чем с сигмоидой, да ещё и быстрее. Увеличив масштаб, мы можем видеть, что оба импульса очень похожи и что использование обоих типов импульса даёт лучший результат, чем вовсе без импульса.