Введение в метод обратного распространения ошибки

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

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

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

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

Найдём производную от J по w:

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

Основная идея заключается в том, что мы будем делать то же самое и с нейронными сетями, но поскольку нейронные сети являются нелинейными, то и искать мы будем локальный минимум, а не глобальный. К тому же поскольку обновления весовых коэффициентов зависят от ошибок в нескольких выходах, нам понадобится нахождение общей производной. Так, если у нас есть функция f(x, y), причём её аргументы также являются функциями x(t) и y(t), то, используя цепное правило, получим:

В случае вектора xk(t), имеющего k компонент и зависящего от t, мы, как можно догадаться, делаем то же самое, но с использованием суммирования:

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

где t принимает значение 0 или 1 в зависимости от того, выпал ли кубик нужной гранью или нет.

В нейронной сети всё совершенно так же, только вместо 6 граней у нас есть k классов:

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

Найдём логарифм функции правдоподобия:

Итак, у нас есть целевая функция. Что же нам с ней делать? Суть та же, что и в случае логистической регрессии – нам надо найти производную.

В случае нейронной сети с одним скрытым слоем у нас есть два набора весовых коэффициентов. Обозначим их через W и V. Предположим также, что размерности слоёв равны D, M и K. Нам необходимо найти производные и . Поскольку у нас метод именно обратного распространения, то сначала мы найдём , потому что она «справа», а затем обратно распространим ошибку и найдём . Сделать всё это мы можем при помощи цепного правила:

Обратите внимание, что в правой части уравнения стоит k, поскольку это переменная суммирования и вовсе не то же самое, что и k, стоящее в левой части.

Итак, мы применили цепное правило. Вопрос теперь в том, как нам найти Другой насущный вопрос – как найти производную функции софтмакс? Давайте сначала выпишем уравнение софтмакс:

где функция активации равна скалярному произведению входных переменных на весовые коэффициенты:

Её можно переписать и в скалярной форме с помощью суммы:

Теперь найдём производную от софтмакс, а о скалярном произведении побеспокоимся чуть позже. Получим:

Мы можем всё это совместить с помощью символа Кронекера. Напомним его определение:

Тогда можем записать:

Из определения скалярного произведения мы знаем, что производная функции активации равна

Объединяя всё это, можем записать:

Заметьте, что я пропустил некоторые этапы и написал сразу окончательный ответ. Мне бы хотелось, чтобы вы попытались в качестве упражнения самостоятельно разобрали пропущенные этапы.

Метод обратного распространения ошибки. Рекурсивность

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

На этом занятии мы рассмотрим более глубокие сети, чем сети с одним скрытым слоем, которые мы рассматривали ранее, и покажем рекурсивность метода обратного распространения. Нарисуем более глубокую сеть и пусть K будет у нас исходящим слоем, S, R, Q – скрытыми слоями, D – входным слоем.

Введение в метод обратного распространения ошибки

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

Введение в метод обратного распространения ошибки

Фактически одинаковая картина будет повторяться по мере обратного распространения. В каждом слое будет всё больше и больше повторений – сначала затем и так далее.

Алгоритм обратного распространения в коде

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

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

Большую часть кода мы просто скопируем из файла forwardprop.py, поскольку он идентичен – так, идентичны коэффициент классификации, алгоритм прямого распространения и импортируемые библиотеки.

import numpy as np

import matplotlib.pyplot as plt

def forward(X, W1, b1, W2, b2):

Z = 1 / (1 + np.exp(-X.dot(W1) – b1))

A = Z.dot(W2) + b2

expA = np.exp(A)

Y = expA / expA.sum(axis=1, keepdims=True)

return Y, Z

def classification_rate(Y, P):

n_correct = 0

n_total = 0

for i in xrange(len(Y)):

n_total += 1

if Y[i] == P[i]:

n_correct += 1

return float(n_correct) / n_total

Теперь определим основную функцию. Её задача будет состоять в создании данных. Это очень похоже на то, что мы уже делали, но с некоторыми дополнениями. Так, мы введём переменную N, представляющую длину Y, а также преобразуем целевые переменные в показательные, поскольку они равны либо 0, либо 1, тогда как Y представляет собой классы от 0 до K-1. Кроме того, на случай, если вы уже подзабыли, как выглядят наши данные, мы вновь отобразим их графически.

def main():

Nclass = 500

D = 2

M = 3

K = 3

X1 = np.random.randn(Nclass, D) + np.array([0, -2])

X2 = np.random.randn(Nclass, D) + np.array([2, 2])

X3 = np.random.randn(Nclass, D) + np.array([-2, 2])

X = np.vstack([X1, X2, X3])

Y = np.array([0]*Nclass + [1]*Nclass + [2]*Nclass)

N = len(Y)

T = np.zeros((N, K))

for i in xrange(N):

T[i, Y[i]] = 1

plt.scatter(X[:,0], X[:, 1], c=Y, s=100, alpha=0.5)

plt.show()

if __name__ == ‘__main__’:

main()

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

W1 = np.random.randn(D, M)

b1 = np.random.randn(M)

W2 = np.random.randn(M, K)

b2 = np.random.randn(K)

Коэффициент обучения установим равным 10-7, а количество циклов – равным 100 000. Обратите внимание, что теперь функция forward возвращает не только исходящую переменную, но и значения в скрытом слое. После каждых 100 циклов мы будем рассчитывать значение функции затрат – мы ещё не написали код для этой функции, но напишем, – а также будем выводить прогноз. Всё это будет использовано для расчёта коэффициента классификации.

learning_rate = 10e-7

costs = []

for epoch in xrange(100000):

output, hidden = forward(X, W1, b1, W2, b2)

if epoch % 100 == 0:

c = cost(T, output)

P = np.argmax(output, axis=1)

r = classification_rate(Y, P)

print ‘’cost:’’, c, ‘’classification_rate:’’, r

costs.append(c)

Теперь мы напишем код для градиентного восхождения – это лишь действие, противоположное градиентному спуску – плюс вместо минуса и ty вместо yt. Сугубо для того, чтобы вы знали, как это происходит в обоих направлениях.

W2 += learning_rate * derivative_w2(hidden, T, output)

b2 += learning_rate * derivative_b2(T, output)

W1 += learning_rate * derivative_w1(X, hidden, T, output, W2)

b1+= learning_rate * derivative_b2(T, output, W2, hidden)

И наконец, когда всё это уже сделано, мы можем вывести на экран значение функции затрат.

plt.plit(costs)

plt.show()

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

def cost(T, Y):

tot = T * np.log(Y)

return tot.sum

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

def derivative_w2(Z, T, Y):

N, K = T.shape

M = Z.shape[1]

ret1 = np.zeros((M, K))

for n in xrange(N):

for m in xrange(M):

for k in xrange(K):

ret1[m,k] += (T[n,k] – Y[n,k])*Z[n,m]

return ret1

Далее напишем функцию для нахождения производных относительно b2 и W1.

def derivative_b2(T, Y):

return (T – Y).sum(axis=0)

def derivative_w1(X, Z, T, Y, W2):

N, D = X.shape

M, K = W2.shape

ret1 = np.zeros((D, M))

for n in xrange(N):

for k in xrange(K):

for m in xrange(M):

for d in xrange(D):

ret1[d,m] += (T[n,k] – Y[n,k])*W2[m,k]*Z[n,m]*(1 – Z[n.m])*X[n,d]

return ret1

И последнее – напишем функцию для производной относительно b1, и на этот раз мы сделаем это быстрым способом. Я рекомендую разобрать эту часть кода самостоятельно в качестве домашнего задания, чтобы научиться пользоваться векторными операциями.

def derivative_b1(T, Y, W2, Z):

return ((T – Y).dot(W2.T) * Z *(1 – Z)).sum(axis=0)

Запустим программу.

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

def derivative_w2(Z, T, Y):

N, K = T.shape

M = Z.shape[1]

ret1 = np.zeros((M, K))

for n in xrange(N):

for m in xrange(M):

for k in xrange(K):

ret1[m,k] += (T[n,k] – Y[n,k])*Z[n,m]

ret2 = np.zeros((M, K))

for n in xrange(N):

for k in xrange(K):

ret2[:,k] += (T[n,k] – Y[n,k])*Z[n,:]

assert(np.abs(ret1 – ret2).sum() < 10e-10)

return ret1

Запустим программу ещё раз. Получается медленно, поэтому уберём всю часть с циклами.

def derivative_w2(Z, T, Y):

N, K = T.shape

M = Z.shape[1]

ret2 = np.zeros((M, K))

for n in xrange(N):

for k in xrange(K):

ret2[:,k] += (T[n,k] – Y[n,k])*Z[n,:]

ret3 = np.zeros((M, K))

for n in xrange(N):

ret3 += np.outer(Z[n], T[n] – Y[n])

assert(np.abs(ret2 – ret3).sum() < 10e-10)

return ret3

Попробуем снова. Всё работает. Но мы можем ещё более упростить код.

def derivative_w2(Z, T, Y):

N, K = T.shape

M = Z.shape[1]

ret3 = np.zeros((M, K))

for n in xrange(N):

ret3 += np.outer(Z[n], T[n] – Y[n])

ret4 = Z.T.dot(T – Y)

assert(np.abs(ret4 – ret3).sum() < 10e-10)

return ret3

Попробуем ещё раз. Всё работает, и даже, кажется быстрее. Но мы можем всё переписать ещё проще.

def derivative_w2(Z, T, Y):

N, K = T.shape

M = Z.shape[1]

return Z.T.dot(T – Y)

В качестве упражнения попытайтесь самостоятельно переписать таким же образом функцию производной для w1.

def derivative_w1(X, Z, T, Y, W2):

dZ = (T – Y).dot(W2.T) * Z * (1 – Z)).sum(axis=0)

return X.T.dot(dZ)

Запустим программу ещё раз. Заметьте, насколько увеличилась скорость её выполнения.

Далее на графике мы видим логарифм функции правдоподобия. В конечном же результате у нас получилась точность 97%.

Неправильный способ изучения метода обратного распространения ошибок

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

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

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

Давайте рассмотрим типичный подход к методу обратного распространения ошибок. Мы начали с члена

который мы рассматриваем как ошибку в узле. В этом заключается первый вопрос – почему именно он является ошибкой? Почему не какое-то другое выражение? Объяснение недостаточно чёткое.

Далее мы говорим, что обновление весовых коэффициентов может быть выражено через эту ошибку:

Это логично, что обновление должно зависеть от ошибки в узле, который связан с соответствующим весовым коэффициентом. Но, вновь-таки, откуда берётся это выражение? Мы не можем обойти эти вопросы, поскольку они становятся понятными, только будучи объяснёнными словами.

Следующее – собственно метод обратного распространения ошибок. Мы видим, что ошибка распространяется обратно, поскольку δ2(j) предыдущего слоя зависит от δ3(k) предыдущего слоя:

Но, опять же, почему именно так? У вас не получится разобраться, просто приняв всё на веру.

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

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

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

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

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

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