Идея для написания этой статьи возникла прошлым летом, когда я слушал доклад на конференции BigData по нейронным сетям. Лектор «посыпал» слушателей непривычными словечками «нейрон», «обучающая выборка», «тренировать модель»… «Ничего не понял — пора в менеджеры», — подумал я. Но недавно тема нейронных сетей все же коснулась моей работы и я решил на простом примере показать, как использовать этот инструмент на языке JavaScript.
Мы создадим нейронную сеть, с помощью которой будем распознавать ручное написание цифры от 0 до 9. Рабочий пример займет несколько строк. Код будет понятен даже тем программистам, которые не имели дело с нейронными сетями ранее. Как это все работает, можно будет посмотреть прямо в браузере.
Если вы уже знаете что такое Perceptron, следующую главу нужно пропустить.
Совсем немного теории
Нейронные сети возникли из исследований в области искусственного интеллекта, а именно, из попыток воспроизвести способность биологических нервных систем обучаться и исправлять ошибки, моделируя низкоуровневую структуру мозга. В простейшем случае она состоит из нескольких соединенных между собой нейронов.
Математический нейрон
Несложный автомат, преобразующий входные сигналы в результирующий выходной сигнал.
Сигналы x1, x2, x3 … xn, поступая на вход, преобразуются линейным образом, т.е. к телу нейрона поступают силы: w1x1, w2x2, w3x3 … wnxn, где wi — веса соответствующих сигналов. Нейрон суммирует эти сигналы, затем применяет к сумме некоторую функцию f(x) и выдаёт полученный выходной сигнал y.
В качестве функции f(x) чаще всего используется сигмоидная или пороговая функции.
Пороговая функция может принимать только два дискретных значения 0 или 1. Смена значения функции происходит при переходе через заданный порог T.+
Сигмоидная — непрерывная функция, может принимать бесконечно много значений в диапазоне от 0 до 1.
UPD: В комментариях также упоминаются функции ReLU и MaxOut как более современные.
Архитектура нейронной сети может быть разной, мы рассмотрим одну из простых реализаций нейронной сети — Perceptron
Архитектура Perceptron
Есть слой входных нейронов (где информация поступает из вне), слой выходных нейронов (откуда можно взять результат) и ряд, так-называемых, скрытых слоев между ними. Нейроны могут быть расположены в несколько слоёв. Каждая связь между нейронами имеет свой вес Wij
Входные и выходные сигналы
Перед тем, как подавать сигналы на нейроны входящего слоя сети нам их нужно нормализовать. Нормализация входных данных — это процесс, при котором все входные данные проходят процесс «выравнивания», т.е. приведения к интервалу [0,1] или [-1,1]. Если не провести нормализацию, то входные данные будут оказывать дополнительное влияние на нейрон, что приведет к неверным решениям. Другими словами, как можно сравнивать величины разных порядков?
На нейронах выходного слоя у нас тоже не будет чистой «1» или «0», это нормально. Есть некий порог, при котором мы будем считать, что получили «1» или «0». Про интерпретацию результатов поговорим позже.
«Пример в студию, а то уже засыпаю»
Для удобства я рекомендую себе поставить nodejs и npm.
Мы будем описывать сеть с помощью библиотеки Brain.js. В конце статьи я также дам ссылки на другие библиотеки, которые можно будет сконфигурировать похожим образом. Brain.js мне понравился своей скоростью и возможностью сохранять натренированную модель.
Давайте попробуем пример из документации — эмулятор функции XOR:
Запишем все в файл simple1.js
, чтоб пример заработал, поставим модуль brain
и запустим.
У нас 2 входящих нейрона и один нейрон на выходе, библиотека brain.js
сама сконфигурирует скрытый слой и установит там столько нейронов, сколько сочтет нужным (в этом примере 3 нейрона).
То, что мы передали в метод .train
называется обучающей выборкой, каждый элемент которой состоит из массива объектов со свойством input и output (массив входящих и выходящих параметров). Мы не проводили нормализацию входящих данных, так как сами данные уже приведены в нужную форму.
Обратите внимание: мы на выходе получаем не [0.987] а [0.9331…]. У вас может быть немного другое значение. Это нормально, так как алгоритм обучения использует случайные числа при подборе весовых коэффициентов.
Метод .run
применяется для получения ответа нейронной сети на заданный в аргументе массив входящих сигналов.
Другие простые примеры можно посмотреть в документации brain.
Распознаем цифры
В начале нам нужно получить изображения с рукописными цифрами, приведенными к одному размеру. В нашем примере мы будем использовать модуль MNIST digits, набор тысяч 28x28px бинарных изображений рукописных цифр от 0 до 9:
Оригинальная база данных MNIST содержит 60 000 примеров для обучения и 10 000 примеров для тестирования, ее можно можно загрузить с сайта LeCun. Автор MNIST digits сделал доступной часть этих примеров для языка JavaScript, в библиотеке уже проведена нормализация входящих сигналов. С помощью этого модуля мы можем получать обучающую и тестовую выборку автоматически.
Мне пришлось клонировать библиотеку MNIST digits, так как там есть небольшая путаница с данными. Я повторно загрузил 10 000 примеров из оригинальной базы данных, так что использовать надо MNIST digits из моего репозитория.
Конфигурация сети
Во входном слое нам необходимо 28x28=784 нейрона, на выходе 10 нейронов. Скрытый слой brain.js
сконфигурирует сам. Забегая наперед, уточню: там будет 392 нейрона. Обучающая выборка будет сформирована модулем mnist
.
Тренируем модель
Установим mnist
Все готово, обучаем сеть
Создаем сеть, получаем 1000 элементов обучающей выборки, вызываем метод .train
, который производит обучение сети — сохраняем все в файл ‘./data/mnistTrain.json
’ (не забудьте создать папку “./data
”).
Если все сделали правильно, получите приблизительно такой результат:
Все можно распознавать
Осталось написать совсем немного кода — и система распознавания готова!
Получаем 1 случайный тестовый пример из выборки 10 000 записей, загружаем натренированную ранее модель, передаем на вход сети тестовую запись и смотрим правильно ли она распозналась.
Вот пример выполнения:
В примере в сеть на входящие нейроны поступила оцифрованная тройка (первый массив это идеальный ответ), на выходе сети мы получили массив элементов, один из которых близок к единице (0.9910109896013567) это тоже третий бит. Обратите внимание на четвертый бит там 7.56… в -7 степени, это такая форма записи чисел с плавающей точкой в JavaScript.
Ну что же, распознавание прошло правильно. Поздравляю, наша сеть заработала!
Немного «причешем» наши результаты функцией softmax, которую я взял из одного примера по машинному обучению:
Функцию можно поместить в начало нашего кода и последнюю строку заменить на console.log(softmax(output));
Все друзья — теперь все работает красиво:
Иногда сеть может давать неправильный результат (мы взяли небольшую выборку и поставили не достаточно строгую погрешность).
А как распознать цифру, которую напишете вы?
Конечно, тут нет никакой подтасовки, но все же хочется самому проверить «на прочность» то, что получилось.
С помощью HTML5 Canvas и все тем же brain.js-ом с сохраненной моделью мне удалось сделать реализацию распознавания в браузере, часть кода для отрисовки и дизайн интерфейса я позаимствовал в интернете. Можете попробовать в живую. В мобильном устройстве рисовать можно пальцем.