Пошаговое объяснение вычислительных графов и обратного распространения ошибки в рекуррентной нейронной сети.

Введение

На заре машинного обучения, когда не было фреймворков, большая часть времени при построении модели тратилась на ручное кодирование обратного распространения. Сегодня, с развитием фреймворков и автограда, для обратного распространения через глубокую нейронную сеть с десятками слоев нам достаточно вызвать loss.backward — и все. Однако, чтобы получить четкое представление о глубоком обучении, крайне важно понимать основы обратного распространения ошибки и вычислительных графов.

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

В этой статье мы поймем, как происходит обратное распространение в рекуррентной нейронной сети.

Вычислительные графики

В основе обратного распространения лежат операции и функции, которые можно элегантно представить в виде вычислительного графа. Давайте рассмотрим пример: рассмотрим функцию f = z(x+y); Представление вычислительного графа показано ниже:

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

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

Обозначение: представим производную от a относительно b как ∂a/∂b по всей статье.

Сначала мы начинаем с конца и вычисляем ∂f/∂f, что равно 1, затем, двигаясь назад, мы вычисляем ∂f/∂q, что равноz,затем ∂f/∂z, что равноq,и наконец, мы вычисляем ∂f/∂x иf/∂y.

Восходящие, нисходящие и локальные градиенты

Если вы заметили, мы не можем вычислить ∂f/∂x иf/∂y напрямую, поэтому мы используйте цепное правило, чтобы сначала вычислить ∂q/∂x, а затем умножить его наf/∂q, который уже был рассчитан на предыдущем шаге, чтобы получитьf/∂x. Здесь ∂f/∂q называется восходящим градиентом,f/∂xназывается нисходящим градиентом, а ∂q/∂xназывается локальным градиентом.

downstream gradient = local gradient × upstream gradient

Преимущества вычислительных графиков

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

Рассмотрим простой узел добавления, как показано на рисунке ниже:

Учитывая входные данные x и y, выходные данные z = x + y. Градиент вверх по течению составляетL/∂z, гдеL — окончательные потери. Локальный градиент равен z/∂x, но посколькуz = x + y, ∂z/∂ x = 1.Теперь нисходящий градиентL/∂x является произведением восходящего градиента и локальный градиент, но поскольку локальный градиент равен единице, градиент вниз по течению равен градиенту вверх по течению. Другими словами, градиент проходит через узел сложения как есть, поэтому узел сложения называется распределителем градиента.

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

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

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

3. Пользовательские узлы. Мы можем объединить несколько операций в один узел, например сигмовидный узел или узел softmax, как мы увидим далее.

Градиенты векторов и матриц

При работе с нейронными сетями мы обычно имеем дело с многомерными входными и выходными данными, которые представлены в виде векторов и матриц. Производная скаляра по вектору - это вектор, который представляет, как на скаляр влияет изменение каждого элемента вектора; производная вектора по другому вектору - это матрица Якоби, которая представляет, как на каждый элемент вектора влияет изменение каждого элемента другого вектора. Без доказательства градиенты показаны ниже:

Здесь W — матрица весов, x — входной вектор, а y — вектор выходного произведения.

Градиент кросс-энтропийной потери (необязательно)

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

Проход вперед

При прямом проходе вектор 𝑦⃗ = [y1, y2, y3, ..., yn] проходит через узел softmax, чтобы получить распределение вероятностей S = [S1, S2, S3, ..., Sn] в качестве выходных данных. Затем, предположим, что индекс истинности равен m, мы возьмем отрицательный логарифм Sm, чтобы вычислить потери: l = -log(Sm).

Функция softmax задается:

Обратный проход

Сложность здесь заключается в зависимости потерь от одного элемента вектора S. Итак, l = -log(Sm) и ∂l/∂Sm = -1/ Sm, где Sm представляет m-й элемент S, где m — метка истинности. Затем, возвращаясь назад, мы должны вычислить градиенты узла softmax. Здесь градиент вниз по течению равен ∂l/∂y,локальный градиентравенS/∂y, а восходящий градиент равенl/∂Sm.

Сначала вычислим локальный градиент ∂S/∂y. Теперь здесь S — вектор, а y — тоже вектор, поэтому ∂S/∂yбудет матрицей, представляющей, как на каждый элемент S влияет изменение каждого элемента y;но вы видите, потери зависит только от одного элемента S в наземном индексе истинности, поэтому нас интересует только то, как изменение каждого элемента влияет на один элемент S. г. Математически,нам нужно найтиSi/∂yгдеSi являетсяi элементом вектораS.Еще одна загвоздка:Siэто просто функция softmax, примененная к i-йэлемент 𝑦⃗, что означает, что Si больше зависит от yi и меньше от других элементов 𝑦⃗. Таким образом, мы не можем вычислить ∂Si/∂y напрямую. Итак, мы найдем Si/∂yj, где yj — произвольный элемент 𝑦⃗ и рассмотрим два случая, когда j = i и j ≠ i, как показано ниже:

Итак, наконец, мы можем написать:

Далее давайте вычислим нисходящий градиент ∂l/∂y. Теперь, поскольку нисходящий градиент является произведением локального градиента и восходящего градиента, давайте снова найдемl/∂yjи рассмотрим два случая, когда j = i и j ≠ i, как показано ниже:

Итак, наконец, мы можем написать:

Таким образом, если вектор 𝑦⃗ = [y1, y2, y3, ..., ym, ..., yn] проходит через узел softmax, чтобы получить распределение вероятностей S = [S1, S2, S3, ..., Sm, ..., Sn], то нисходящий градиент ∂l/∂y задается как [S1, S2, S3, ..., Sm — 1, ..., Sn], т. е. мы сохраняем все элементы вектора softmax как есть и вычесть 1 из элемента в индексе истинности основания. Это также может быть представлено как S - 1[по индексу m]. Поэтому в следующий раз, когда мы захотим выполнить обратное распространение через узел кросс-энтропийных потерь, мы можем просто вычислить нисходящий градиент как S – 1[по индексу m].

Хорошо, теперь, когда у нас есть прочная основа для обратного распространения и вычислительных графов, давайте посмотрим на обратное распространение в RNN.

Обратное распространение в RNN

Проход вперед

При прямом проходе на определенном временном шаге входной вектор и вектор скрытого состояния из предыдущего временного шага умножаются на соответствующие им весовые матрицы и суммируются узлом сложения. Далее они проходят через нелинейную функцию, а затем копируются: один из них идет как вход на следующий временной шаг, а другой идет в заголовок классификации, где он умножается на матрицу весов для получения логит-вектора перед вычисление кросс-энтропийной потери. Это типичная установка генеративной RNN, в которой мы моделируем сеть таким образом, что при заданном входном символе она предсказывает распределение вероятностей следующего подходящего символа. Если вам интересно создать RNN персонажа в pytorch, ознакомьтесь с моей другой статьей здесь. Уравнения прямого прохода показаны ниже:

Обратный проход

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

Затем они текут в обратном направлении к tanh узлу нелинейности, градиент которого можно вычислить как: ∂tanh(x)/∂x = 1−tanh²(x). Затем этот градиент проходит через узел сложения, где он распределяется как по узлам умножения матриц входного вектора, так и по предыдущему скрытому вектору состояния. Обычно мы не вычисляем градиент относительно входного вектора, если нет особых требований, но мы вычисляем градиент относительно предыдущего скрытого вектора состояния, который затем возвращается к предыдущему временному шагу. Пожалуйста, обратитесь к диаграмме для детальных математических шагов.

Посмотрим, как это выглядит в коде.

Код Python

Андрей Карпати реализовал символьную RNN с нуля в Python/Numpy, и его код блестяще фиксирует этап обратного распространения, который мы обсуждали, как показано ниже:

Полный код можно найти здесь, и читателю настоятельно рекомендуется ознакомиться с ним. Если вы ищете реализацию RNN в pytorch с примером, посмотрите мою другую статью здесь.

Почему обратное распространение в RNN неэффективно

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

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

Существует способ избежать проблемы взрывающегося градиента, по существу «отсекая» градиент, если он пересекает определенный порог. Однако RNN по-прежнему не может эффективно использоваться для длинных последовательностей.

Надеюсь, у вас есть четкое представление о том, как происходит обратное распространение в RNN. Дайте мне знать, если у вас есть какие-либо сомнения. Подключаемся по Twitter и LinkedIn.

Кредиты изображений

Все изображения, использованные в статье, сделаны автором.