Идея для написания этой статьи возникла прошлым летом, когда я слушал доклад на конференции BigData по нейронным сетям. Лектор «посыпал» слушателей непривычными словечками «нейрон», «обучающая выборка», «тренировать модель»… «Ничего не понял — пора в менеджеры», — подумал я. Но недавно тема нейронных сетей все же коснулась моей работы и я решил на простом примере показать, как использовать этот инструмент на языке JavaScript.
Мы создадим нейронную сеть, с помощью которой будем распознавать ручное написание цифры от 0 до 9. Рабочий пример займет несколько строк. Код будет понятен даже тем программистам, которые не имели дело с нейронными сетями ранее. Как это все работает, можно будет посмотреть прямо в браузере.
Если вы уже знаете что такое Perceptron, следующую главу нужно пропустить.
Совсем немного теории
Нейронные сети возникли из исследований в области искусственного интеллекта, а именно, из попыток воспроизвести способность биологических нервных систем обучаться и исправлять ошибки, моделируя низкоуровневую структуру
Математический нейрон
Несложный автомат, преобразующий входные сигналы в результирующий выходной сигнал.
Сигналы x1, x2, x3 … xn, поступая на вход, преобразуются линейным образом, т.е. к телу нейрона поступают силы: w1x1, w2x2, w3x3 … wnxn, где wi – веса соответствующих сигналов. Нейрон суммирует эти сигналы, затем применяет к сумме некоторую функцию f(x) и выдаёт полученный выходной сигнал y.
В качестве функции f(x) чаще всего используется сигмоидная или пороговая функции.
Пороговая функция может принимать только два дискретных значения 0 или 1. Смена значения функции происходит при переходе через заданный порог T.+
Сигмоидная – непрерывная функция, может принимать бесконечно много значений в диапазоне от 0 до 1.
Архитектура нейронной сети может быть разной, мы рассмотрим одну из простых реализаций нейронной сети — 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'),
fs = require('fs'),
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
- Живой пример распознавания цифры в браузере
- [Eng] Почему стоит использовать библиотеку brain.js а не brain
Автор: apelsyn