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

Контролируемая нейронная сеть в самом высоком и простом представлении может быть представлена ​​в виде черного ящика с двумя методами обучения и прогнозирования следующим образом:

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

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

Простой числовой пример

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

+--------+-----------------+
| Input  |  Desired output |
+--------+-----------------+
|     0  |               0 |
|     1  |               2 |
|     2  |               4 |
|     3  |               6 |
|     4  |               8 |
+--------+-----------------+

В этом примере вам может показаться очевидным, что output = 2 x input, однако это не так для большинства реальных наборов данных (где взаимосвязь между вводом и выводом очень нестабильна. -линейный и не такой очевидный).

Шаг 1- Инициализация модели

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

Чтобы провести аналогию, возьмем, к примеру, человека, который никогда в жизни не играл в футбол. В самый первый раз, когда он пытается бросить мяч, он может просто выстрелить в случайном порядке. Точно так же для нашего численного исследования рассмотрим следующую случайную инициализацию: (Модель 1): y = 3.x. Число 3 здесь генерируется случайным образом. Другой случайной инициализацией может быть: (Модель 2): y = 5.x или (Модель 3): y = 0,5.x.
Мы позже мы рассмотрим, как в процессе обучения все эти модели могут сходиться к идеальному решению (y = 2.x) (которое мы пытаемся найти).

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

Шаг 2- Распространение вперед

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

+--------+------------------------------------+
| Input  |  Actual output of model 1 (y= 3.x) |
+--------+------------------------------------+
|     0  |                                  0 |
|     1  |                                  3 |
|     2  |                                  6 |
|     3  |                                  9 |
|     4  |                                 12 |
+--------+------------------------------------+

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

Шаг 3- Функция потерь

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

+--------+-----------------+-----------------+
| Input  |  Actual output  |  Desired output |
+--------+-----------------+-----------------+
|     0  |              0  |               0 |
|     1  |              3  |               2 |
|     2  |              6  |               4 |
|     3  |              9  |               6 |
|     4  |              12 |               8 |
+--------+-----------------+-----------------+

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

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

Наиболее интуитивно понятная функция потерь - это просто loss = (Желаемый результат - фактический результат). Однако эта функция потерь возвращает положительные значения, когда сеть выходит за пределы допустимого диапазона (прогноз ‹желаемый результат), и отрицательные значения, когда сеть выходит за пределы допустимого диапазона (прогноз› желаемый результат). Если мы хотим, чтобы функция потерь отражала абсолютную ошибку производительности, независимо от того, превышает ли она значение или недооценку, мы можем определить ее как:
loss = Абсолютное значение (желаемое - фактическое) .

Если мы вернемся к нашему примеру с футболистом, если наш новичок стреляет по мячу на 10 м вправо или на 10 м слева от ворот, в обоих случаях мы считаем, что он промахнулся на 10 м независимо от направления (вправо или влево). левый). В этом случае мы добавим в таблицу новый столбец - ›абсолютная ошибка.

Однако несколько ситуаций могут привести к одной и той же общей сумме ошибок: например, множество мелких ошибок или несколько больших ошибок могут привести к одному и тому же общему количеству ошибок. Поскольку мы хотим, чтобы предсказание работало в любой ситуации, предпочтительнее иметь распределение множества мелких ошибок, а не нескольких крупных.
Чтобы стимулировать NN чтобы сойтись к такой ситуации, мы можем определить функцию потерь как сумму квадратов абсолютных ошибок (которая является самой известной функцией потерь в NN). Таким образом, маленькие ошибки учитываются намного меньше, чем большие ошибки! (квадрат 2 равен 4, а квадрат 10 равен 100! Таким образом, ошибка 10 наказывается в 25 раз больше, чем ошибка 2, а не только 5 раз!). Наша таблица становится следующей:

+--------+----------+-----------+------------------+---------------+
| Input  |  actual  |  Desired  |  Absolute Error  |  Square Error |
+--------+----------+-----------+------------------+---------------+
| 0      |       0  |        0  |               0  |             0 |
| 1      |       3  |        2  |               1  |             1 |
| 2      |       6  |        4  |               2  |             4 |
| 3      |       9  |        6  |               3  |             9 |
| 4      |      12  |        8  |               4  |            16 |
| Total: |       -  |        -  |              10  |            30 |
+--------+----------+-----------+------------------+---------------+

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

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

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

Шаг 4 - Дифференциация

Очевидно, мы можем использовать любой метод оптимизации, который изменяет внутренние веса нейронных сетей, чтобы минимизировать функцию общих потерь, которую мы определили ранее. Эти методы могут включать генетические алгоритмы или жадный поиск или даже простой поиск методом перебора:
В нашем простом числовом примере с одним параметром веса для оптимизации W мы можем искать из - От 1000,0 до +1000,0 с шагом 0,001, при котором W имеет наименьшую сумму квадратов ошибок в наборе данных.

Это может сработать, если у модели очень мало параметров и нас не заботит точность. Однако, если мы обучаем NN по массиву входов 600x400 (например, при обработке изображений), мы можем очень легко достичь моделей с миллионами весов для оптимизации, а грубая сила даже представить себе не может, поскольку это пустая трата времени. вычислительные ресурсы!

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

Чтобы увидеть эффект производной, мы можем задать себе следующий вопрос: насколько изменится общая ошибка, если мы изменим внутренний вес нейронной сети на определенное малое значение δW. Для простоты будем считать δW = 0,0001. на самом деле он должен быть еще меньше !.

Давайте пересчитаем сумму квадратов ошибок при очень незначительном изменении веса W:

+--------+----------+-------+-----------+------------+---------+
| Input  |  Output  |  W=3  |  sq.e(3)  |  W=3.0001  |   sq.e  |
+--------+----------+-------+-----------+------------+---------+
| 0      |       0  |    0  |        0  |         0  |       0 |
| 1      |       2  |    3  |        1  |    3.0001  |  1.0002 |
| 2      |       4  |    6  |        4  |    6.0002  |  4.0008 |
| 3      |       6  |    9  |        9  |    9.0003  |  9.0018 |
| 4      |       8  |   12  |       16  |   12.0004  | 16.0032 |
| Total: |        - |     - |       30  |         -  |  30.006 |
+--------+----------+-------+-----------+------------+---------+

Как видно из этой таблицы, если мы увеличим W с 3 до 3,0001, сумма квадратов ошибки увеличится с 30 до 30,006. Поскольку мы знаем, что лучшая функция, которая соответствует этой модели, - y = 2.x, увеличение весов с 3 до 3.0001, очевидно, должно привести к немного большей ошибке (потому что мы идем дальше от интуитивно-правильного вес 2. Имеем: 3.0001 ›3› 2, значит погрешность выше). Но что нас действительно волнует, так это скорость, с которой ошибка изменяется относительно изменений веса. По сути, здесь этот коэффициент представляет собой увеличение общей ошибки на 0,006 на каждые 0,0001 увеличения веса - ›это коэффициент 0,006 / 0,0001 = 60x! Это работает в обоих направлениях, поэтому, если мы уменьшим веса на 0,0001, мы также сможем уменьшить общую ошибку на 0,006! Вот доказательство: если мы снова запустим расчет при W = 2,9999, мы получим ошибку 29,994. Нам удалось уменьшить общую ошибку на 0,006, мы кое-что узнали! мы улучшили модель!

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

Вот как выглядит наша функция потерь:

  • Если w = 2, мы имеем потерю 0, поскольку фактические выходные данные нейронной сети идеально подходят для обучающей выборки.
  • Если w ‹2, у нас есть положительная функция потерь, но производная отрицательна, что означает, что увеличение веса уменьшит функцию потерь.
  • При w = 2 потери равны 0, а производная равна 0, мы достигли идеальной модели, ничего не нужно.
  • Если w> 2, потери снова становятся положительными, но производная также положительна, а это означает, что дальнейшее увеличение веса приведет к еще большему увеличению потерь !!

Если мы инициализируем сеть случайным образом, мы помещаем любую случайную точку на этой кривой (скажем, w = 3). Процесс обучения на самом деле говорит следующее:

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

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

Шаг 5- Обратное распространение

В этом примере мы использовали только один слой внутри нейронной сети между входами и выходами. Во многих случаях требуется больше слоев, чтобы достичь большего количества вариаций в функциональности нейронной сети.
Конечно, мы всегда можем создать одну сложную функцию, которая представляет композицию по всем слоям сети. Например, если уровень 1 выполняет: 3.x для генерации скрытого вывода z, а уровень 2 выполняет: z² для генерации окончательного вывода, составная сеть будет выполнять (3.x) ² = 9.x². Однако в большинстве случаев составление функций очень сложно. Кроме того, для каждой композиции необходимо вычислять выделенную производную композиции (которая совсем не масштабируется и очень подвержена ошибкам).

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

ввод - ›3.x -› 2.x - ›вывод.

Изменение дельты 0,001 на входе будет преобразовано в изменение дельты 0,003 после первого слоя, а затем в изменение дельты 0,006 на выходе.
Это тот случай, если мы скомпонуем обе функции в одну:

вход - ›6.x -› выход.

Точно так же ошибка на выходе 0,006 может быть передана обратно до ошибки 0,003 на среднем скрытом этапе, а затем до 0,001 на входе.
Если мы создадим библиотеку дифференцируемых функции или уровни, где для каждой функции мы знаем, как распространять в прямом направлении (путем прямого применения функции) и как распространять в обратном направлении (путем вычисления производной функции), мы можем составить любую сложную нейронную сеть. Нам нужно только сохранить стек вызовов функций во время прямого прохода и их параметры, чтобы знать способ обратного распространения ошибок с использованием производных этих функций. Это можно сделать путем декомпозиции через вызовы функций. Этот метод называется автоматическим дифференцированием и требует только, чтобы каждая функция имела реализацию своей производной. В одном из будущих постов блога мы объясним, как ускорить автоматическое дифференцирование путем реализации основных математических операций над векторами / матрицами / и тензорами.

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

На этом рисунке показан процесс обратного распространения ошибок по этой схеме:
Вход - ›Прямые вызовы -› Функция потерь - ›производная -› Обратное распространение ошибок. На каждом этапе получаем дельты на весах этого этапа.

Шаг 6 - Обновление веса

Как мы представили ранее, производная - это просто скорость изменения ошибки относительно изменения веса. В числовом примере, представленном ранее, эта скорость составляет 60x. Это означает, что 1 единица изменения веса приводит к 60 общим единицам изменения функции потерь.
И поскольку мы знаем, что ошибка потерь в настоящее время составляет 30 единиц, путем экстраполяции скорости, чтобы уменьшить ошибку до 0 нам нужно уменьшить веса на 0,5 единицы.
Однако для реальных задач мы не должны обновлять веса такими большими шагами. Поскольку существует много нелинейностей, любое большое изменение веса приведет к хаотическому поведению. Мы не должны забывать, что производная является локальной только в точке, где мы вычисляем производную.

Таким образом, общим правилом обновления веса является правило дельты:

Новый вес = старый вес - Производная * скорость обучения

Скорость обучения вводится как постоянная (обычно очень маленькая), чтобы вес обновлялся очень плавно и медленно (чтобы избежать больших шагов и хаотичного поведения). (Помните: Учись медленно и уверенно!)

Чтобы проверить это уравнение:

  • Если ставка производной положительна, это означает, что увеличение веса увеличит ошибку, поэтому новый вес должен быть меньше.
  • Если ставка производной отрицательна, это означает, что увеличение веса уменьшит ошибку, поэтому нам нужно увеличить веса.
  • Если производная равна 0, это означает, что мы находимся в устойчивом минимуме. Таким образом, обновление весов не требуется - ›мы достигли стабильного состояния.

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

В числовом примере, который мы представили здесь, у нас есть только 5 обучающих наборов ввода / вывода. На самом деле у нас могут быть миллионы записей. Ранее мы говорили о минимизации функции стоимости ошибки (функции потерь) для всего набора данных. Это называется пакетным обучением и может быть очень медленным для больших данных. Вместо этого мы можем обновлять веса каждые B atchSize = N обучения, при условии, что набор данных перемешивается случайным образом. Это называется мини-пакетным градиентным спуском. И если N = 1, мы называем этот случай полным онлайн-обучением или стохастическим градиентным спуском, поскольку мы обновляем веса после каждого наблюдаемого входного выхода!
Любой оптимизатор может работать с этими тремя режимы (полный онлайн / мини-пакет / полный пакет).

Шаг 7 - итерации до сходимости

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

Сколько итераций нужно, чтобы сойтись?

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

  • Это зависит от случайной инициализации сети. Возможно, если вам повезет, вы инициализируете сеть с ** W = 1,99 **, и вы всего в 0,01 шаге от оптимального решения.
  • Это зависит от качества обучающего набора. Если вход и выход не имеют корреляции между собой, нейронная сеть не будет творить чудеса и не сможет изучить случайную корреляцию.

Общая картина

Подводя итог, вот как выглядит процесс обучения в нейронных сетях (полная картина):

Пример и код

Пример и сверхпростая реализация нейронной сети представлены в этом сообщении в блоге.

Если у вас остались какие-либо вопросы, не стесняйтесь оставлять комментарии или связываться со мной по адресу: [email protected]

Я всегда буду рад ответить вам, улучшить эту статью или сотрудничать, если у вас есть какие-либо идеи. Если вам понравилось читать, подпишитесь на нас: Facebook, Twitter, LinkedIn