СтатьиКниги
13.08.2019 6696
Нейронные сети на Javascript

Идея для написания этой статьи возникла прошлым летом, когда я слушал доклад на конференции 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-ом с сохраненной моделью мне удалось сделать реализацию распознавания в браузере, часть кода для отрисовки и дизайн интерфейса я позаимствовал в интернете. Можете попробовать в живую. В мобильном устройстве рисовать можно пальцем.