Перейти к содержанию

Модуль 8. Полносвязные сети и обратное распространение ошибки

Чему вы научитесь в этом модуле

  • Объяснять, почему без нелинейной функции активации между слоями нейронная сеть остаётся обыкновенной линейной моделью.
  • Самостоятельно выводить формулы обратного прохода для полносвязного слоя, а не копировать их из чужого кода.
  • Писать нейронную сеть с нуля на NumPy и проверять её корректность с помощью gradient check из модуля 4.
  • Демонстрировать на числах, почему нулевая инициализация весов не работает, а слишком большая ломает обучение.
  • Честно сравнивать нейросеть с градиентным бустингом из модуля 6 и принимать результат, каким бы он ни оказался.

Время: примерно две недели. Пререквизиты: модуль 7. Ноутбук: открыть в Colab · notebooks/08-neural-networks-and-backprop.ipynb

Зачем нужен этот модуль

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

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

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

Почему одного слоя недостаточно

Линейная модель проводит гиперплоскость в пространстве признаков. Но существуют данные, которые ей принципиально не разделить. Такой пример строится за минуту: четыре точки, задающие операцию XOR (исключающее «или»), где класс определяется несовпадением значений двух признаков.

Гиперплоскости, которая разделила бы эти четыре точки на два класса, не существует. Не «трудно найти» — её математически не существует.

Естественная мысль: поставить два линейных слоя один за другим. К сожалению, это не поможет.

\[W_2(W_1 x) = (W_2 W_1)\,x = W x\]

Композиция двух линейных преобразований — это снова линейное преобразование. Из модуля 3 мы знаем: произведение двух матриц есть матрица. Даже сто линейных слоёв подряд дадут ровно то же самое, что один.

Между слоями обязательно должна стоять нелинейная функция активации. Не «для красоты» и не «чтобы лучше училось» — без неё никакой глубины (то есть выразительной силы, растущей с числом слоёв) попросту не возникает.

Функции активации

Формула Производная Основная проблема
Сигмоида \(\sigma(z) = \frac{1}{1+e^{-z}}\) \(\sigma(1-\sigma)\) максимум производной равен 0.25 — градиент затухает при передаче через слои
Гиперболический тангенс \(\tanh z\) \(1 - \tanh^2 z\) максимум производной равен 1, но на больших значениях аргумента функция «насыщается» (выходит на плато)
ReLU \(\max(0, z)\) 0 или 1 при \(z < 0\) нейрон может «умереть» навсегда (его градиент становится постоянным нулём)

Про сигмоиду самое важное: её производная нигде не превышает 0.25. Цепное правило дифференцирования перемножает производные всех слоёв, и в сети из десяти сигмоидных слоёв градиент для первого слоя умножается на коэффициент \(0.25^{10} \approx 10^{-6}\). Это означает, что первые (ближайшие к входу) слои фактически перестают обучаться.

Это явление называется затуханием градиента (vanishing gradient problem), и именно оно не давало обучать глубокие нейросети вплоть до конца 2000-х годов.

ReLU решает эту проблему тем, что её производная равна ровно единице на всей положительной части числовой оси. Умножение единиц ничего не затухает — градиент свободно проходит через слои. Плата за это — если конкретный нейрон «ушёл» в отрицательную область (получает отрицательный вход) на всех обучающих примерах, его градиент постоянно равен нулю, и он фактически мёртв — больше никогда не обучится.

Прямой проход (forward pass)

Рассмотрим нейронную сеть из \(L\) слоёв. Для слоя с номером \(l\) вычисления устроены так:

\[z^{(l)} = W^{(l)} a^{(l-1)} + b^{(l)}, \qquad a^{(l)} = f(z^{(l)})\]

где \(a^{(0)} = x\) — входные данные, \(f\) — функция активации, а на последнем слое активация обычно не применяется: вместо неё ставят сигмоиду для задачи с двумя классами, softmax для задачи с несколькими классами, или не ставят ничего для задачи регрессии.

Вот и всё. Прямой проход по сети — это цикл, состоящий из двух строк вычислений на каждом слое.

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

Теперь главная тема модуля.

Нам нужно вычислить частные производные функции потерь \(\partial L/\partial W^{(l)}\) для каждого слоя — это градиенты, по которым будут обновляться веса. Вычисление производится с помощью цепного правила, применяемого от последнего слоя к первому (справа налево по вычислительному графу).

Для удобства введём обозначение \(\delta^{(l)} = \partial L / \partial z^{(l)}\) — это мера того, насколько функция потерь чувствительна к изменению входа активации слоя \(l\). Через эту величину все нужные градиенты выражаются компактно.

Последний слой. Для пары «сигмоида + log loss» из модуля 5 происходит алгебраическое сокращение, и получается:

\[\delta^{(L)} = a^{(L)} - y\]

Это та самая формула \((p - y)\), которая уже встречалась нам раньше. Сокращение происходит не случайно: функцию потерь и активацию на последнем слое специально подбирают в пару именно для того, чтобы получить такой простой градиент.

Шаг назад через один слой. Зная \(\delta^{(l)}\) для текущего слоя, можно вычислить \(\delta\) для предыдущего:

\[\delta^{(l-1)} = \left( W^{(l)\mathsf{T}} \delta^{(l)} \right) \odot f'(z^{(l-1)})\]

Эту формулу полезно прочитать по частям. Первый множитель, \(W^{\mathsf{T}}\delta\), — это ошибка, «разложенная обратно» по нейронам предыдущего слоя пропорционально весам связей, через которые они повлияли на эту ошибку. Второй множитель, поэлементное умножение на \(f'\) (производную активации), — это учёт того, что нейрон, находящийся в области насыщения активационной функции, почти не влияет на выход, а значит и «виноват» в ошибке тоже почти нет.

Градиенты по параметрам (весам и смещениям).

\[\frac{\partial L}{\partial W^{(l)}} = \delta^{(l)} a^{(l-1)\mathsf{T}}, \qquad \frac{\partial L}{\partial b^{(l)}} = \delta^{(l)}\]

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

Что такое backpropagation на самом деле

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

Обучает модель по-прежнему градиентный спуск из модуля 4. Backprop лишь доставляет ему градиент, и его единственная заслуга — вычислительная эффективность: он переиспользует уже вычисленные значения \(\delta\) вместо того, чтобы считать производную по каждому весу заново с нуля.

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

Инициализация весов

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

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

С нулевой инициализацией ситуация ещё хуже. Градиент для первого слоя вычисляется через обратный проход, который включает умножение на веса второго слоя — формула \(W^{\mathsf{T}}\delta\) выше. Если эти веса нулевые, результат умножения тождественно равен нулю, и первый слой не сдвигается ни на один шаг. В ноутбуке это наглядно видно: после трёхсот шагов обучения все веса первого слоя остаются ровно нулями.

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

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

Работающее правило — поддерживать дисперсию сигнала примерно постоянной при прохождении через слои. Из этого требования выводятся два стандартных способа инициализации:

\[\text{Xavier: } \mathrm{Var}(W) = \frac{1}{n_{\text{вход}}}, \qquad \text{He: } \mathrm{Var}(W) = \frac{2}{n_{\text{вход}}}\]

Xavier используется для симметричных функций активации (таких как tanh), He — для ReLU. Двойка в формуле He компенсирует тот факт, что ReLU обнуляет примерно половину входных значений (отрицательные), и поэтому дисперсия выхода слоя с ReLU составляет половину дисперсии входа.

Gradient check

В модуле 4 мы познакомились с этим приёмом, а модуль 8 даёт задачу, ради которой он по-настоящему необходим.

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

Правило: написали новый слой — проверьте его формулы с помощью gradient check, прежде чем запускать обучение. Относительное расхождение между аналитическим и численным градиентами менее \(10^{-7}\) означает, что формула верна. Расхождение более \(10^{-4}\) сигнализирует об ошибке.

На проверку уходит одна минута. На поиск той же ошибки без gradient check может уйти целый рабочий день.

Нейросеть против бустинга

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

В модуле 6 градиентный бустинг обыграл линейную модель на табличных данных примерно вдвое. Теперь у нас есть нейронная сеть. Победит ли она бустинг?

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

Результат не скрывается, потому что скрывать результаты — это ровно то, против чего написан весь курс: на табличных данных нейросеть не обыгрывает градиентный бустинг. Причины этого были разобраны в модуле 6, и они носят структурный характер.

Полезный вывод здесь не в том, что нейросети «плохие». Он в том, что умение обучить нейросеть не означает, что её нужно обучать для данной конкретной задачи. В последующих модулях появятся задачи, где нейросеть выигрывает с разгромным счётом: обработка изображений, анализ последовательностей, работа с текстом. Там в данных присутствует пространственная или последовательная структура, из которой сеть извлекает пользу. В таблице такой структуры нет.

Практическая часть

Часть 1. Работа с ноутбуком

Откройте notebooks/08-neural-networks-and-backprop.ipynb.

Что содержится внутри:

  1. XOR: линейная модель не справляется, а сеть с одним скрытым слоем — справляется. На визуализации видна граница решения.
  2. Слой как объект: прямой проход реализуется буквально тремя строками кода.
  3. Обратный проход и gradient check. Затем — намеренно испорченная формула обратного прохода, чтобы убедиться, что gradient check действительно обнаруживает ошибку.
  4. Инициализация: нулями, слишком большими значениями, по методу He. Три кривые обучения для сравнения.
  5. Затухание градиента по глубине: сигмоида против ReLU — градиент на первом слое, выраженный в конкретных числах.
  6. Нейросеть против бустинга на табличных данных модуля 6, с доверительными интервалами.

Часть 2. Самостоятельная реализация

  1. Добавьте в код ноутбука ещё одну функцию активации — tanh или Leaky ReLU.
  2. Проверьте её корректность с помощью gradient check. Не запускайте обучение, пока проверка не пройдена.
  3. Постройте кривые обучения для трёх различных активационных функций на одних и тех же данных и с одним и тем же начальным зерном.
  4. Найдите глубину сети, при которой сигмоидная нейросеть полностью перестаёт обучаться. Вычислите норму градиента первого слоя и сравните с теоретическим ожиданием \(0.25^{\text{глубина}}\).

Задание

  1. Выведите на бумаге формулу \(\delta^{(l-1)}\) через \(\delta^{(l)}\). Не подглядывая в текст модуля.
  2. Докажите, что два линейных слоя без функции активации между ними математически эквивалентны одному линейному слою. Для доказательства достаточно двух строк.
  3. Реализуйте softmax и многоклассовую кросс-энтропию. Покажите, что \(\delta^{(L)}\) на последнем слое снова принимает вид \(a - y\).
  4. Инициализируйте нейросеть нулевыми весами и покажите численно, что все нейроны скрытого слоя остаются одинаковыми после ста шагов обучения.
  5. Вернитесь к своей табличной задаче из модуля 6 и проведите честное сравнение нейросети с бустингом. Запишите вывод в формате модуля 1: величина, условия, база сравнения.

Пятый пункт повторяет задание из модуля 6, и это сделано намеренно. Теперь в вашем арсенале на одну модель больше, а правила честного сравнения не изменились.

Проверка усвоения

  1. Почему без нелинейной активации увеличение глубины сети ничего не даёт?
  2. Чему равен максимум производной сигмоиды и какое следствие из этого для глубоких сетей?
  3. Что такое \(\delta^{(l)}\) — объясните словами?
  4. Объясните по отдельности два компонента формулы обратного прохода: \(W^{\mathsf{T}}\delta\) и поэлементное умножение на \(f'\).
  5. Backpropagation — это алгоритм обучения? Если нет, то что это?
  6. Почему нельзя инициализировать все веса нулями?
  7. Откуда в инициализации по методу He берётся двойка?
  8. Нейросеть проиграла бустингу на табличных данных. Какой вывод из этого следует, а какой — нет?

Что дальше

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

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