рекомендации

суббота, 1 августа 2020 г.

Глубокое погружение в математику нейронных сетей


Тайны нейронных сетей, часть I

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


Примечание: благодаря любезности Jung Yi Lin вы также можете прочитать эту статью на китайском языке. Вы также можете проверить текст на португальском языке благодаря помощи Davi Candido. Исходный код, использованный для создания визуализаций, которые использовались в этой статье, доступен в моем GitHub.

Введение

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

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


Рисунок 2. Архитектура нейронной сети

Решение KERAS

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

  • from keras.models import Sequential
  • from keras.layers import Dense
  •  
  • model = Sequential()
  • model.add(Dense(4, input_dim=2,activation='relu'))
  • model.add(Dense(6, activation='relu'))
  • model.add(Dense(6, activation='relu'))
  • model.add(Dense(4, activation='relu'))
  • model.add(Dense(1, activation='sigmoid'))
  •  
  • model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
  • model.fit(X_train, y_train, epochs=50, verbose=0)

Ааааа вот и все. Как я уже упоминал во введении, нескольких импортов и нескольких строк кода достаточно для создания и обучения модели, которая затем сможет классифицировать записи из нашего тестового набора с почти 100% точностью. Наша задача сводится к предоставлению гиперпараметров (количество слоев, количество нейронов в слое, функции активации или количество эпох) в соответствии с выбранной архитектурой. Давайте теперь посмотрим, что произошло за кулисами. Ох... И классная визуализация, которую я создал в процессе обучения, которая, надеюсь, не даст вам уснуть.

Рисунок 3. Визуализация областей, квалифицированных для соответствующих классов во время обучения

Что такое нейронные сети?

Давайте начнем с ответа на этот ключевой вопрос: что такое нейронная сеть? Это вдохновленный биологией метод создания компьютерных программ, которые способны учиться и самостоятельно находить связи в данных. Как показано на рисунке 2, сети представляют собой набор программных «нейронов», расположенных в слоях, соединенных вместе таким образом, чтобы обеспечить связь друг с другом.

Одиночный нейрон

Каждый нейрон получает набор значений x (пронумерованных от 1 до n) в качестве входных данных и вычисляет предсказанное значение y-hat. Вектор x фактически содержит значения характеристик в одном из m примеров из обучающего набора. Более того, каждый из нейронов имеет свой собственный набор параметров, обычно называемый w (вектор столбцов весов) и b (смещение), который изменяется в процессе обучения. На каждой итерации нейрон вычисляет средневзвешенное значение значений вектора x на основе его текущего вектора веса w и добавляет смещение. Наконец, результат этого вычисления передается через нелинейную функцию активации g. Я расскажу немного о самых популярных функциях активации в следующей части статьи.


Рисунок 4. Одиночный нейрон

Одиночный слой

Теперь давайте немного уменьшим масштаб и рассмотрим, как выполняются вычисления для всего слоя нейронной сети. Мы будем использовать наши знания о том, что происходит внутри одного нейрона, и векторизовать весь слой, чтобы объединить эти вычисления в матричные уравнения. Чтобы унифицировать обозначения, уравнения будут записаны для выбранного слоя [l]. Кстати, индекс i помечает индекс нейрона в этом слое.

Рисунок 5. Одиночный слой

Еще одно важное замечание: когда мы писали уравнения для одного нейрона, мы использовали x и y-hat, которые были соответственно вектором столбца признаков и прогнозируемой величиной. При переключении на общие обозначения для слоя мы используем вектор а - означающий активацию соответствующего слоя. Следовательно, вектор x является активацией для слоя 0 - входного слоя. Каждый нейрон в слое выполняет аналогичный расчет в соответствии со следующими уравнениями:


Для ясности запишем уравнения, например, для слоя 2:


Как видите, для каждого из слоев нам нужно выполнить ряд очень похожих операций. Использование for-loop для этой цели не очень эффективно, поэтому для ускорения вычислений мы будем использовать векторизацию. Прежде всего, сложив вместе горизонтальные векторы весов w (транспонированные), мы построим матрицу W. Аналогично, мы сложим вместе смещение каждого нейрона в слое, создав вертикальный вектор b. Теперь ничто не мешает нам строить матричные уравнения, которые позволяют нам выполнять вычисления для всех нейронов слоя одновременно. Давайте также запишем размеры матриц и векторов, которые мы использовали.





Векторизация по нескольким примерам

Уравнение, которое мы составили, содержит только один пример. В процессе обучения нейронной сети вы обычно работаете с огромными наборами данных, до миллионов записей. Поэтому следующим шагом будет векторизация для нескольких примеров. Предположим, что в нашем наборе данных есть m записей с nx функциями каждый. Прежде всего, мы соединим вертикальные векторы x, a и z каждого слоя, создавая матрицы X, A и Z соответственно. Затем мы переписываем ранее выложенное уравнение с учетом вновь созданных матриц.




Что такое функция активации и зачем она нам нужна?

Функции активации являются одним из ключевых элементов нейронной сети. Без них наша нейронная сеть стала бы комбинацией линейных функций, поэтому она была бы просто линейной функцией. Наша модель будет иметь ограниченное применение, не превосходящее логистическую регрессию. Элемент нелинейности обеспечивает большую гибкость и создание сложных функций в процессе обучения. Функция активации также оказывает существенное влияние на скорость обучения, что является одним из основных критериев их выбора. На рисунке 6 показаны некоторые из наиболее часто используемых функций активации. В настоящее время наиболее популярной для скрытых слоев является, вероятно, ReLU. Мы по-прежнему иногда используем сигмоиду, особенно в выходном слое, когда имеем дело с двоичной классификацией и хотим, чтобы значения, возвращаемые из модели, находились в диапазоне от 0 до 1.


Рисунок 6. Диаграммы наиболее популярных функций активации вместе с их производными.

Функция потерь

Основным источником информации о ходе обучения является значение функции потерь. Вообще говоря, функция потерь предназначена для того, чтобы показать, насколько мы далеки от «идеального» решения. В нашем случае мы использовали binary crossentropy, но в зависимости от проблемы, с которой мы имеем дело, могут применяться разные функции. Используемая нами функция описывается следующей формулой, а изменение ее значения в процессе обучения визуализируется на рисунке 7. На нем показано, как с каждой итерацией значение функции потерь уменьшается и точность увеличивается.



Рисунок 7. Изменение значений точности и потерь в процессе обучения

Как обучаются нейронные сети?

Процесс обучения заключается в изменении значений параметров W и b таким образом, чтобы функция потерь была минимизирована. Чтобы достичь этой цели, мы обратимся за помощью к математическому анализу и используем метод градиентного спуска, чтобы найти минимум функции. На каждой итерации мы будем вычислять значения частных производных функции потерь по каждому из параметров нашей нейронной сети. Для тех, кто слабо знаком с этим типом вычислений, я только упомяну, что у производной есть фантастическая способность описать наклон графика функции. Благодаря этому мы знаем, как управлять переменными, чтобы двигаться вниз по графику. Стремясь сформировать интуитивное представление о том, как работает градиентный спуск (и помешать вам снова заснуть), я подготовил небольшую визуализацию. Вы можете видеть, как с каждой последовательной эпохой мы движемся к минимуму. В нашей нейронной сети это работает таким же образом - градиент, рассчитанный на каждой итерации, показывает нам направление, в котором мы должны двигаться. Основное отличие состоит в том, что в нашей тестовой нейронной сети у нас гораздо больше параметров для манипуляции. Точно ... Как рассчитать такие сложные производные?


Рисунок 8. Градиентный спуск в действии

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

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


В приведенных выше уравнениях α представляет собой скорость обучения - гиперпараметр, который позволяет контролировать значение выполненной корректировки. Выбор скорости обучения имеет решающее значение - если мы устанавливаем ее слишком низкой, наша нейронная сеть будет учиться очень медленно, если мы устанавливаем ее слишком высокой, мы не сможем достичь минимума. dW и db рассчитываются с использованием правила цепочки, частных производных функции потерь по W и b. Размеры dW и db такие же, как у W и b соответственно. На рисунке 9 показана последовательность операций внутри нейронной сети. Мы ясно видим, как прямое и обратное распространение работают вместе для оптимизации функции потерь.




Рисунок 9. Прямое и обратное распространение

Вывод

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

Поздравляю, если вам удалось добраться сюда. Это было, конечно, не самое легкое чтение. Если вам понравилась эта статья, следите за мной в Twitter и Medium и смотрите другие проекты, над которыми я работаю, на GitHub и Kaggle. Эта статья является второй частью серии «Тайны нейронных сетей». Если у вас еще не было возможности, прочитайте другие статьи. Оставайтесь любопытными!

Комментариев нет:

Отправить комментарий