
Идея для написания этой статьи возникла прошлым летом, когда я слушал доклад на конференции 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:
var brain = require('brain.js'); var net = new brain.NeuralNetwork(); net.train([{input: [0, 0], output: [0]}, {input: [0, 1], output: [1]}, {input: [1, 0], output: [1]}, {input: [1, 1], output: [0]}]); var output = net.run([1, 0]); // [0.987] console.log(output);
запишем все в файл simple1.js, чтоб пример заработал, поставим модуль brain и запустим
npm install brain.js node simple1.js # [ 0.9331839217737243 ]
У нас 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
npm install https://github.com/ApelSYN/mnist
Все готово, обуча��м сеть
const brain = require('brain.js'); var net = new brain.NeuralNetwork(); const fs = require('fs'); const mnist = require('mnist'); const set = mnist.set(1000, 0); const trainingSet = set.training; net.train(trainingSet, { errorThresh: 0.005, // error threshold to reach iterations: 20000, // maximum training iterations log: true, // console.log() progress periodically logPeriod: 1, // number of iterations between logging learningRate: 0.3 // learning rate } ); let wstream = fs.createWriteStream('./data/mnistTrain.json'); wstream.write(JSON.stringify(net.toJSON(),null,2)); wstream.end(); console.log('MNIST dataset with Brain.js train done.')
Создаем сеть, получаем 1000 элементов обучающей выборки, вызываем метод .train, который производит обучение сети — сохраняем все в файл './data/mnistTrain.json' (не забудьте создать папку "./data").
Если все сделали правильно, получите приблизительно такой результат:
[root@HomeWebServer nn]# node train.js iterations: 0 training error: 0.060402555338691676 iterations: 1 training error: 0.02802436102035996 iterations: 2 training error: 0.020358600820106914 iterations: 3 training error: 0.0159533285799183 iterations: 4 training error: 0.012557029942873513 iterations: 5 training error: 0.010245175822114688 iterations: 6 training error: 0.008218147206099617 iterations: 7 training error: 0.006798613211310184 iterations: 8 training error: 0.005629051609641436 iterations: 9 training error: 0.004910207736789503 MNIST dataset with Brain.js train done.
Все можно распознавать
Осталось написать совсем немного кода — и система распознавания готова!
const brain = require('brain.js'), mnist = require('mnist'); var net = new brain.NeuralNetwork(); const set = mnist.set(0, 1); const testSet = set.test; net.fromJSON(require('./data/mnistTrain')); var output = net.run(testSet[0].input); console.log(testSet[0].output); console.log(output);
Получаем 1 случайный тестовый пример из выборки 10 000 записей, загружаем натренированную ранее модель, передаем на вход сети тестовую запись и смотрим правильно ли она распозналась.
Вот пример выполнения
[ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 ] [ 0.0002863506627761867, 0.00002389940760904011, 0.00039954062883041345, 0.9910109896013567, 7.562879202664903e-7, 0.0038756598319246837, 0.000016752919557362786, 0.0007205981595354964, 0.13699517762991756, 0.0011053963693377692 ]
В примере в сеть на входящие нейроны поступила оцифрованная тройка (первый масив это идеальный ответ), на выходе сети мы получили массив елементов, один из которых близок к единице (0.9910109896013567) это тоже третий бит. Обратите внимание на четвертый бит там 7.56… в -7 степени, это такая форма записи чисел с плавающей точкой в JavaScript.
Ну что же, распознавание прошло правильно. Поздравляю, наша сеть заработала!
Немного «причешем» наши результаты функцией softmax, которую я взял из одного примера по машинному обучению:
function softmax(output) { var maximum = output.reduce(function(p,c) { return p>c ? p : c; }); var nominators = output.map(function(e) { return Math.exp(e - maximum); }); var denominator = nominators.reduce(function (p, c) { return p + c; }); var softmax = nominators.map(function(e) { return e / denominator; }); var maxIndex = 0; softmax.reduce(function(p,c,i){if(p<c) {maxIndex=i; return c;} else return p;}); var result = []; for (var i=0; i<output.length; i++) { if (i==maxIndex) result.push(1); else result.push(0); } return result; }
Функцию можно поместить в начало нашего кода и последнюю строку заменить на
console.log(softmax(output));
Все друзья — теперь все работает красиво:
[root@HomeWebServer nn]# node simpleRecognize.js [ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 ] [ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 ] [root@HomeWebServer nn]# node simpleRecognize.js [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 ] [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 ] [root@HomeWebServer nn]# node simpleRecognize.js [ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 ] [ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0 ]
Иногда сеть может давать неправильный результат (мы взяли небольшую выборку и поставили не достаточно строгую погрешность).
А как распознать цифру, которую напишете вы?
Конечно, тут нет никакой подтасовки, но все же хочется самому проверить «на прочность» то, что получилось.
С помощью HTML5 Canvas и все тем же brain.js-ом с сохраненной моделью мне удалось сделать реализацию распознавания в браузере, часть кода для отрисовки и дизайн интерфейса я позаимствовал в интернете. Можете попробовать вживую. В мобильном устройстве рисовать можно пальцем.
Ссылки по теме
- Библиотеки на JavaScript для работы с нейронными сетями
- Все примеры из статьи на github
- Живой пример распознавания цифры в браузере
Нейронные сети на JS. Создавая сеть с нуля
- [Eng] Почему стоит использовать библиотеку brain.js а не brain
UPD: Альтернативные реализации живого примера 1, 2 на JavaScript из комментариев и личной переписки.
