Статья публикуется в переводе, автор оригинального текста Victor Zhou.
Пишем нейросеть на Python с нуля
Термин "нейронные сети" сейчас можно услышать из каждого утюга, и многие верят, будто это что-то очень сложное. На самом деле нейронные сети совсем не такие сложные, как может показаться! Мы разберемся, как они работают, реализовав одну сеть с нуля на Python.
Эта статья предназначена для полных новичков, не имеющих никакого опыта в машинном обучении. Поехали!
1. Составные элементы: нейроны
Прежде всего нам придется обсудить нейроны, базовые элементы нейронной сети. Нейрон принимает несколько входов, выполняет над ними кое-какие математические операции, а потом выдает один выход. Вот как выглядит нейрон с двумя входами:
Внутри нейрона происходят три операции. Сначала значения входов умножаются на веса:
Затем взвешенные входы складываются, и к ним прибавляется значение порога b:
Наконец, полученная сумма проходит через функцию активации:
Функция активации преобразует неограниченные значения входов в выход, имеющий ясную и предсказуемую форму. Одна из часто используемых функций активации – сигмоида:
Сигмоида выдает результаты в интервале (0, 1). Можно представить, что она «упаковывает» интервал от минус бесконечности до плюс бесконечности в (0, 1): большие отрицательные числа превращаются в числа, близкие к 0, а большие положительные – к 1.
Простой пример
Допустим, наш двухвходовой нейрон использует сигмоидную функцию активации и имеет следующие параметры:
w=[0, 1] – это всего лишь запись w1=0, w2=1 в векторном виде. Теперь зададим нашему нейрону входные данные: x=[2, 3]. Мы используем скалярное произведение векторов, чтобы записать формулу в сжатом виде:
Наш нейрон выдал 0.999 при входах x=[2, 3]. Вот и все! Процесс передачи значений входов дальше, чтобы получить выход, называется прямой связью (feed forward).
Пишем код для нейрона
Настало время написать свой нейрон! Мы используем NumPy, популярную и мощную расчетную библиотеку для Python, которая поможет нам с вычислениями:
Узнаете эти числа? Это тот самый пример, который мы только что рассчитали! И мы получили тот же результат – 0.999.
2. Собираем нейронную сеть из нейронов
Нейронная сеть – это всего лишь несколько нейронов, соединенных вместе. Вот как может выглядеть простая нейронная сеть:
У этой сети два входа, скрытый слой с двумя нейронами (h1 и h2) и выходной слой с одним нейроном (o1). Обратите внимание, что входы для o1 – это выходы из h1 и h2. Именно это создает из нейронов сеть.
Пример: прямая связь
Давайте используем сеть, изображенную выше, и будем считать, что все нейроны имеют одинаковые веса w=[0, 1], одинаковые пороговые значения b=0, и одинаковую функцию активации – сигмоиду. Пусть h1, h2 и o1 обозначают выходные значения соответствующих нейронов.
Что получится, если мы подадим на вход x=[2, 3]?
Если подать на вход нашей нейронной сети x=[2, 3], на выходе получится 0.7216. Достаточно просто, не правда ли?
Нейронная сеть может иметь любое количество слоев, и в этих слоях может быть любое количество нейронов. Основная идея остается той же: передавайте входные данные по нейронам сети, пока не получите выходные значения. Для простоты мы будем использовать сеть, показанную выше, до конца статьи.
Пишем код нейронной сети
Давайте реализуем прямую связь для нашей нейронной сети. Напомним, как она выглядит:
Мы снова получили 0.7216! Похоже, наша сеть работает.
3. Обучаем нейронную сеть (часть 1)
Допустим, у нас есть следующие измерения:
Имя | Вес (в фунтах) | Рост (в дюймах) | Пол |
Алиса | 133 (54.4 кг) | 65 (165,1 см) | Ж |
Боб | 160 (65,44 кг) | 72 (183 см) | М |
Чарли | 152 (62.2 кг) | 70 (178 см) | М |
Диана | 120 (49 кг) | 60 (152 см) | Ж |
Давайте обучим нашу нейронную сеть предсказывать пол человека по его росту и весу.
Мы будем представлять мужской пол как 0, женский – как 1, а также сдвинем данные, чтобы их было проще использовать:
Имя | Вес (минус 135) | Рост (минус 66) | Пол |
Алиса | -2 | -1 | 1 |
Боб | 25 | 6 | 0 |
Чарли | 17 | 4 | 0 |
Диана | -15 | -6 | 1 |
Потери
Прежде чем обучать нашу нейронную сеть, нам нужно как-то измерить, насколько "хорошо" она работает, чтобы она смогла работать "лучше". Это измерение и есть потери (loss).
Мы используем для расчета потерь среднюю квадратичную ошибку (mean squared error, MSE):
Давайте рассмотрим все используемые переменные:
- n – это количество измерений, в нашем случае 4 (Алиса, Боб, Чарли и Диана).
- y представляет предсказываемое значение, Пол.
- ytrue – истинное значение переменной ("правильный ответ"). Например, для Алисы ytrue будет равна 1 (женский пол).
- ypred – предсказанное значение переменной. Это то, что выдаст наша нейронная сеть.
(ytrue-ypred)2 называется квадратичной ошибкой. Наша функция потерь просто берет среднее значение всех квадратичных ошибок – поэтому она и называется средней квадратичной ошибкой. Чем лучшими будут наши предсказания, тем меньшими будут наши потери!
Лучшие предсказания = меньшие потери.
Обучение нейронной сети = минимизация ее потерь.
Пример расчета потерь
Предположим, что наша сеть всегда возвращает 0 – иными словами, она уверена, что все люди мужчины. Насколько велики будут наши потери?
Имя | ytrue | ypred | (ytrue-ypred)2 |
Алиса | 1 | 0 | 1 |
Боб | 0 | 0 | 0 |
Чарли | 0 | 0 | 0 |
Диана | 1 | 0 | 1 |
Пишем функцию средней квадратичной ошибки
Вот небольшой кусок кода, который рассчитает наши потери. Если вы не понимаете, почему он работает, прочитайте в руководстве NumPy про операции с массивами.
Отлично. Идем дальше!
4. Обучаем нейронную сеть (часть 2)
Теперь у нас есть четкая цель: минимизировать потери нейронной сети. Мы знаем, что можем изменять веса и пороги нейронов, чтобы изменить ее предсказания, но как нам делать это таким образом, чтобы минимизировать потери?
Для простоты давайте представим, что в нашем наборе данных только одна Алиса.
Имя | Вес (минус 135) | Рост (минус 66) | Пол |
Алиса | -2 | -1 | 1 |
Тогда средняя квадратичная ошибка будет квадратичной ошибкой только для Алисы:
Другой метод – это рассматривать функцию потерь как функцию от весов и порогов. Давайте отметим все веса и пороги нашей нейронной сети:
Теперь мы можем записать функцию потерь как функцию от нескольких переменных:
Предположим, мы хотим отрегулировать w1. Как изменится значение потери L при изменении w1? На этот вопрос может ответить частная производная dL/dw1. Как мы ее рассчитаем?
Прежде всего, давайте перепишем эту частную производную через dypred/dw1, воспользовавшись цепным правилом:
Мы можем рассчитать dL/dypred, поскольку мы уже выяснили выше, что L=(1-ypred)2:
Теперь давайте решим, что делать с dypred/dw1. Обозначая выходы нейронов, как прежде, h1, h2 и o1, получаем:
Вспомните, что f() – это наша функция активации, сигмоида. Поскольку w1 влияет только на h1 (но не на h2), мы можем снова использовать цепное правило и записать:
Мы можем сделать то же самое для dh1/dw1, снова применяя цепное правило:
В этой формуле x1 – это вес, а x2 – рост. Вот уже второй раз мы встречаем f'(x) – производную сигмоидной функции! Давайте вычислим ее:
Мы используем эту красивую форму для f'(x) позже. На этом мы закончили! Мы сумели разложить dL/dw1 на несколько частей, которые мы можем рассчитать:
Такой метод расчета частных производных "от конца к началу" называется методом обратного распространения (backpropagation).
Уффф. Здесь было очень много символов, так что не страшно, если вы пока не все понимаете. Давайте покажем, как это работает, на практическом примере!
Пример. Считаем частную производную
Мы по-прежнему считаем, что наш набор данных состоит из одной Алисы:
Имя | Вес (минус 135) | Рост (минус 66) | Пол |
Алиса | -2 | -1 | 1 |
Давайте инициализируем все веса как 1, а все пороги как 0. Если мы выполним прямой проход по нейронной сети, то получим:
Наша сеть выдает ypred=0.524, что находится примерно на полпути между Мужским полом (0) и Женским (1). Давайте рассчитаем dL/dw1:
Вот и все! Результат говорит нам, что при увеличении w1, функция ошибки чуть-чуть повышается.
Обучение: стохастический градиентный спуск
Теперь у нас есть все нужные инструменты для обучения нейронной сети! Мы используем алгоритм оптимизации под названием стохастический градиентный спуск (stochastic gradient descent), который определит, как мы будем изменять наши веса и пороги для минимизации потерь. Фактически, он заключается в следующей формуле обновления:
Скорость обучения определяет, как быстро наша сеть учится. Все, что мы делаем – это вычитаем eta*dL/dw1 из w1:
- Если dL/dw1 положительна, w1 уменьшится, что уменьшит L.
- Если dL/dw1 отрицательна, w1 увеличится, что также уменьшит L.
Если мы сделаем то же самое для каждого веса и порога в сети, потери будут постепенно уменьшаться, и наша сеть будет выдавать более точные результаты.
Процесс обучения сети будет выглядеть примерно так:
- Выбираем одно наблюдение из набора данных. Именно то, что мы работаем только с одним наблюдением, делает наш градиентный спуск стохастическим.
- Считаем все частные производные функции потерь по всем весам и порогам (dL/dw1, dL/dw2 и т.д.)
- Используем формулу обновления, чтобы обновить значения каждого веса и порога.
- Снова переходим к шагу 1.
Пишем код всей нейронной сети
Наконец настало время реализовать всю нейронную сеть.
Имя | Вес (минус 135) | Рост (минус 66) | Пол |
Алиса | -2 | -1 | 1 |
Боб | 25 | 6 | 0 |
Чарли | 17 | 4 | 0 |
Диана | -15 | -6 |
По мере обучения сети ее потери постепенно уменьшаются:
Теперь мы можем использовать нашу сеть для предсказания пола:
Что теперь?
Вы сделали это! Давайте перечислим все, что мы с вами сделали:
- Определили нейроны, составные элементы нейронных сетей.
- Использовали сигмоидную функцию активации для наших нейронов.
- Увидели, что нейронные сети – это всего лишь несколько нейронов, соединенных друг с другом.
- Создали набор данных, в котором Вес и Рост были входными данными (или признаками), а Пол – выходным (или меткой).
- Узнали о функции потерь и средней квадратичной ошибке (MSE).
- Поняли, что обучение нейронной сети – это всего лишь минимизация ее потерь.
- Использовали метод обратного распространения (backpropagation) для расчета частных производных.
- Использовали стохастический градиентный спуск (SGD) для обучения нашей сети.
Перед вами – множество путей, на которых вас ждет масса нового и интересного:
- Экспериментируйте с большими и лучшими нейронными сетями, используя подходящие библиотеки вроде Tensorflow, Keras и PyTorch.
- Создайте свою первую нейронную сеть с помощью Keras.
- Прочитайте остальные статьи из серии "Нейронные сети с нуля".
- Исследуйте другие функции активации, кроме сигмоиды, например, Softmax.
- Исследуйте другие оптимизаторы, кроме стохастического градиентного спуска.
Спасибо за внимание!
На Python создают прикладные приложения, пишут тесты и бэкенд веб-приложений, автоматизируют задачи в системном администрировании, его используют в нейронных сетях и анализе больших данных. Язык можно изучить самостоятельно, но на это придется потратить немало времени. Если вы хотите быстро понять основы программирования на Python, обратите внимание на онлайн-курс «Библиотеки программиста». За 30 уроков (15 теоретических и 15 практических занятий) под руководством практикующих экспертов вы не только изучите основы синтаксиса, но и освоите две интегрированные среды разработки (PyCharm и Jupyter Notebook), работу со словарями, парсинг веб-страниц, создание ботов для Telegram и Instagram, тестирование кода и даже анализ данных. Чтобы процесс обучения стал более интересным и комфортным, студенты получат от нас обратную связь. Кураторы и преподаватели курса ответят на все вопросы по теме лекций и практических занятий.
Комментарии