Адаптивные коэффициенты обучения

Переменные и адаптивные коэффициенты обучения

Здравствуйте и вновь добро пожаловать на занятия по теме «Обработка данных: практическое глубокое обучение в Theano и TensorFlow».

При обучении нейронной сети мы сталкиваемся с одной проблемой – каким установить коэффициент обучения? Для выбора оптимального коэффициента обучения для наших данных и нашей модели может понадобиться много времени. Нет ли способа автоматически определить коэффициент обучения? В этой лекции я покажу, что да, в некотором роде можно. В лекции мы рассмотрим различные способы определения коэффициента обучения при каждой итерации градиентного спуска и даже один метод, при котором коэффициент обучения вообще не понадобится. Общим во всех этих методах является то, что коэффициент обучения уменьшается с течением времени. Другими словами, мы продвигаемся всё меньшими и меньшими шагами по мере приближения к решению.

Итак, приступим.

Пошаговое затухание (step decay) просто уменьшает коэффициент обучения через каждые 5, 10 или 20 шагов. Одним из способов его применения является, например, деление коэффициента обучения на два по достижении каждых 10 итераций:

if iter % 10 == 0

learning_rate /=2

Экспоненциальное затухание (exponential decay) экспоненциально уменьшает коэффициент обучения. Рассчитать коэффициент обучения в этом случае можно по формуле:

learning_rate = Ae^{\lambda*t}.

Затухание 1/t уменьшает коэффициент обучения с помощью функции вида 1/t. В таком случае коэффициент обучения рассчитывается по формуле:

learning_rate = \frac {A}{1+ kt}.

Что общего у всех этих методов? Хотя мы автоматически вычисляем коэффициент обучения, нам приходится вводить новые гиперпараметры. Это значит, что нам необходимо принимать даже больше решений об их значениях, чем ранее.

Следующий метод, который мы обсудим, вообще не нуждается в коэффициенте обучения. Он называется методом второго порядка, тогда как градиентный спуск называется методом первого порядка, поскольку при нём для обновления значений весовых коэффициентов нужна лишь производная первого порядка. Этот способ основан на методе Ньютона и включает нахождение обратной матрицы Гессе, состоящей из частных производных второго порядка рассматриваемой функции. Как вы можете понять, по мере увеличения количества размерностей и наблюдений такие вычисления становятся нереальными, и именно поэтому вы нечасто увидите упоминание о нём в литературе по глубокому обучению.

Далее мы рассмотрим адаптивные методы.

При методе адаптивного градиента (AdaGrad) в кэше сохраняются все изменения весовых коэффициентов. Мы поэлементно умножаем градиент на самого себя, добавляем это значение в кэш и прибавляем некоторое малое число ε, чтобы не приходилось делить на нуль:

cashe += gradient2

w -= learning_rate*gradient / (sqrt(cashe) + epsilon)

Итак, что при этом происходит?

Прежде всего отметим, что каждая размерность весовых коэффициентов изменяется независимо от других. Кэш аккумулирует, или суммирует, все изменения квадрата градиента. Таким образом, чем более изменялся весовой коэффициент по данной размерности в прошлом, тем медленнее он будет изменяться в прошлом. Но на практике специалисты по глубокому обучению выяснили, что при этом ошибка градиента уменьшает коэффициент обучения чересчур агрессивно.

Вариант, предложенный Джеффри Хинтоном, называется методом адаптивного скользящего среднего градиента (RMSprop) и уменьшает сам кэш, то есть можно сказать, что кэш при этом «протекает». Величина кэша при этом равна скорости затухания, умноженной на старый кэш, плюс единица минус скорость затухания, умноженные на квадрат градиента. Изменение весовых коэффициентов остаётся тем же:

cache = decay_rate*cache + (1 – decay_rate)*gradient2

w -= learning_rate*gradient / (sqrt(cashe) + epsilon)

 

Постоянный коэффициент обучения vs. RMSprop в коде

В этой лекции мы рассмотрим переменные и адаптивные коэффициенты обучения в коде. Если вы не хотите набирать весь код самостоятельно, вы можете найти его на Github по адресу в папке ann_class2. Соответствующие файлы называются mlp.py, поскольку мы будем использовать ту же нейронную сеть, что и в прошлый раз, и rmsprop.py. Мы сравним метод адаптивного скользящего среднего градиента (RMSprop) и постоянный коэффициент обучения.

Итак, файл 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

    print ‘’Z:’’, Z

    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

Теперь пройдёмся по тексту файла rmsprop.py.

Весь код вначале остаётся тем же, что и использовавшийся в лекции про импульс. Количество итераций, периодичность вывода результата на экран, разбивка на учебный и проверочный наборы, значения коэффициента обучения, значение регуляризации, размер пакета и количество скрытых узлов также остаются прежними. Точно так же мы и инициируем весовые коэффициенты.

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

def main():

    max_iter = 20 # make it 30 for sigmoid

    print_period = 10

    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. const

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

                # print “pY:”, pY

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

RMSprop же несколько отличается, а в качестве упражнения я рекомендую попробовать также экспоненциальное затухание и сравнить результаты.

Теперь нам необходимо инициировать первоначальный коэффициент обучения, а также кэш для каждой переменной. Нам также понадобится коэффициент затухания и ипсилон.

# 2. RMSprop

    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_rms = []

    CR_rms = []

    lr0 = 0.001 # if you set this too high you’ll get NaN!

    cache_W2 = 0

    cache_b2 = 0

    cache_W1 = 0

    cache_b1 = 0

    decay_rate = 0.999

    eps = 0.000001

Далее пакетный градиент рассчитывается как и прежде.

    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)

А вот дальше в игру вступают отличия. Мы находим градиент, являющийся производной функции затрат и добавляем член регуляризации. Затем мы обновляем кэш, равный коэффициенту затухания, умноженному на кэш плюс единица минус коэффициент затухания, умноженные на квадрат градиента (умножение происходит поэлементно). Всё это проделывается для каждого из параметров W1, b1, W2 и b2.

            # updates

            gW2 = derivative_w2(Z, Ybatch, pYbatch) + reg*W2

            cache_W2 = decay_rate*cache_W2 + (1 – decay_rate)*gW2*gW2

            W2 -= lr0 * gW2 / (np.sqrt(cache_W2) + eps)

            gb2 = derivative_b2(Ybatch, pYbatch) + reg*b2

            cache_b2 = decay_rate*cache_b2 + (1 – decay_rate)*gb2*gb2

            b2 -= lr0 * gb2 / (np.sqrt(cache_b2) + eps)

            gW1 = derivative_w1(Xbatch, Z, Ybatch, pYbatch, W2) + reg*W1

            cache_W1 = decay_rate*cache_W1 + (1 – decay_rate)*gW1*gW1

            W1 -= lr0 * gW1 / (np.sqrt(cache_W1) + eps)

            gb1 = derivative_b1(Z, Ybatch, pYbatch, W2) + reg*b1

            cache_b1 = decay_rate*cache_b1 + (1 – decay_rate)*gb1*gb1

            b1 -= lr0 * gb1 / (np.sqrt(cache_b1) + eps)

Затем мы выводим на экран значения функции затрат для обоих случаев, чтобы можно было их сравнить.

            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_rms.append(ll)

                print(“Cost at iteration i=%d, j=%d: %.6f” % (i, j, ll))

                err = error_rate(pY, Ytest)

                CR_rms.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=’const’)

    plt.plot(LL_rms, label=’rms’)

    plt.legend()

    plt.show()

if __name__ == ‘__main__’:

main()

Итак, запустим программу и посмотрим, что получится.

Адаптивные коэффициенты обучения

Как вы можете видеть, при RMSprop мы могли начать с большим коэффициентом обучения, при этом сходился он намного быстрее. Кроме того, он даёт лучший коэффициент классификации. Обратите внимание, что при RMSprop кривая куда более ломанная.

Оптимизация гиперпараметров, перекрёстная проверка, решётчатый и случайный поиск

На данный момент нам необходимо оптимизировать множество гиперпараметров, и ситуация будет только ухудшаться по мере того, как наши нейронные сети будут становиться всё более и более сложными. Именно поэтому в данной лекции мы обсудим то, что называется оптимизацией гиперпараметров.

Итак, давайте рассмотрим все уже изученные нами гиперпараметры. Это коэффициент обучения, включая адаптивную его инициацию и коэффициент затухания; импульс; регуляризация весовых коэффициентов; размер скрытого слоя; количество скрытых слоёв.

Вот некоторые подходы к выбору этих гиперпараметров.

Вы часто слышали от меня о перекрёстной проверке. Это когда мы разбиваем наши данные на K частей и проводим K итераций, причём проверка модели идёт на одной части, а остальные K – 1 частей используются для обучения. В коде это выглядит примерно так:

for k in xrange(K):

    training_data = data[:k] + data[k+1:]

    model = train(trainig_data)

    score = test(data[k])

В конце мы находим среднее значение индекса соответствия и выбираем такие значения гиперпараметров, которые дают наилучший результат.

Весьма важно по-прежнему выделять часть данных для проверки. Прежде мы говорили лишь об учебном и проверочном наборах данных. Но если мы используем проверочный набор для выбора значений гиперпараметров, он перестаёт быть действительно проверочным, поэтому необходимо выделить ещё один набор данных. Проверочный набор данных, использующийся для вычисления результатов перекрёстной проверки, называется валидационным набором (validation set). Вы можете использовать команду sklearn.cross_validation.KFold, чтобы не разбивать данные вручную.

Какие ещё есть подходы к оптимизации гиперпараметров? Мы обсудим два подхода – решётчатый поиск и случайный поиск.

Решётчатый поиск является исчерпывающим. Вы можете выбрать ряд значений коэффициента обучения, которые хотите проверить, выбрать ряд значений импульса, который хотите проверить, и ряд значений регуляризации, а затем испытать каждую их комбинацию. Вот пример, каким может быть код для этого:

for lr in learning_rates:

    for mu in momentums:

        for reg in regularizations:

            score = cross_validation(lr, mu, reg, data)

Как вы понимаете, этот процесс крайне медленный, но поскольку каждая модель независима от других, тут открываются огромные возможности для параллельных вычислений. Фреймворки вроде Hadoop или Spark являются идеальными для такого рода задач.

При случайном поиске, вместо того чтобы рассматривать все возможности, движение идёт в случайном направлении, пока результат не станет лучше. Основной алгоритм работы выглядит примерно так:

theta = случайная позиция в пространстве гиперпараметров

score1 = cross_validation(theta, data)

for i in xrange(max_iterations):

    next_theta = пример из гиперсферы около theta

    score2 = cross_validation(next_theta, data)

    if score2 лучше, чем score1:

        theta = next_theta

То есть мы инициируем theta, представляющее собой набор параметров, вычисляем результат при данном значении theta и затем создаём цикл для максимального количества итераций, выбирая следующие данные из области гиперсферы в окрестностях текущего значения theta. Далее вычисляем результат при новом значении theta, и если он лучше, чем предыдущий, перемещаемся к этому значению.

Обратите внимание, что и в этих случаях остаётся множество комбинаций и вариантов обсуждаемых нами вещей, и данная тема всё ещё является объектом активных исследований.

Например, какая комбинация лучше – импульс Нестерова с методом адаптивного скользящего среднего градиента или обычный импульс с экспоненциальным спадом? Именно поэтому глубокое обучение в большой степени является скорее искусством, чем наукой. Перепробовать все возможные варианты нереально, и вам понадобится использовать свой опыт чтобы найти оптимальное решение.

Понравилась статья? Поделить с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: