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

Тем не менее, остается место для загадки, поскольку не так очевидно, как эта вещь на самом деле может учиться сама по себе и даже делать это «глубоко» без какого-либо прямого вмешательства программиста. Попробуем это понять.

Коротко о нейронной сети.

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

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

Но прежде чем мы посмотрим на реальную реализацию, важно понять, какова цель всех этих слоев, также известных как скрытые слои. Проблема XOR дает понять. Как вы можете видеть на рисунке 2, вы не можете найти никакой линейной функции, которая могла бы отделить область A от области B, как вы можете сделать это с помощью И и ИЛИ. Между ними есть пересечение, которое не позволяет нам решить, находимся мы в сегменте A или B.

Чтобы найти ответ, мы расширяем наше 2D-пространство дополнительным измерением (или многими другими измерениями), чтобы вы могли в конечном итоге отделить одну функцию от другой.

С точки зрения нейронной сети дополнительное измерение - это просто еще один скрытый слой. Итак, все, что нам нужно - это выяснить, позволяет ли это другое измерение решить нашу проблему XOR. Для этого мы применим алгоритм Backpropagation - ключевую концепцию, опубликованную в 1975 году, чтобы позволить взаимосвязанным слоям узнать свои собственные веса или, другими словами, узнать, насколько они значимы, чтобы помочь нам отделить A от B в XOR.

Имея на борту алгоритм обратного распространения ошибки, мы применим в основном 3 шага, чтобы наше «глубокое» обучение выполняло свою работу:

  1. Вперед пас
  2. Обратный проход
  3. Обновить веса

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

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

Реализация на Scala.

В коде Scala этот трехэтапный процесс может выглядеть так:

Давайте подробнее рассмотрим наш первый шаг - функцию forward. Мы реализуем его как рекурсивный:

Передний пас заботится о:

1. применение весов к сетевым уровням

2. прохождение этого взвешенного слоя через функцию сигмовидной активации.

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

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

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

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

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

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

Если вы запустите пример реализации, предоставленный для этого блога, вы увидите:

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

Небольшая уловка.

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

  1. Скалярное произведение ввода и веса: net = np.dot (input, weight)
  2. Активация этого продукта с помощью функции sigmoid: 1 / (1+ np.exp (-net))

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

net = np.dot (ввод, вес) + b

где b обозначает смещение или порог и должен быть другим тензором, ответственным за регулирование результирующей сети до того, как она будет активирована сигмоидом. На самом деле нам также нужно иметь смещение, а не только веса, как мы делали раньше. Похоже, нам нужно реализовать еще несколько вещей. Но здесь есть хитрость.

Чтобы избежать этой дополнительной сложности, мы просто делаем следующее:

  1. добавить дополнительный столбец единиц в наш тензор обучающего набора (Листинг 6. Строка 3)
  2. расширить веса слоев с помощью того же столбца единиц (Листинг 6. Строка 11)

и таким образом интегрировать предвзятость в нашу задачу оптимизации.

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

Заключение.

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

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

И может быть несколько полезных ссылок:

PS. Думаю, стоит упомянуть, что это не сообщение в блоге о готовой к производству реализации нейронной сети, написанной на Scala. Может быть, в следующий раз;) Основной упор здесь был сделан на то, чтобы показать основные паттерны как можно более прозрачными и очевидными. Надеюсь, вам понравилось.