Универсальная нейронная сеть на JavaScript.
Метод обратного распространения ошибки
+ 4 простые задачи для демонстрации.

21 июня 2021, автор: Елена Позднякова
Создаем универсальную нейросеть на JavaScript, которая будет способна решать различные задачи на классификацию.
Её универсальность заключается в том, что мы можем произвольно задавать архитектуру нейронной сети (число слоев и число нейронов в каждом слое) и передавать р а з л и ч н ы е учебные наборы. В зависимости от переданного учебного набора, нейросеть обучится решать поставленную задачу. Обучение нейросети мы будем производить методом обратного распространения ошибки.

Демонстрацию работы нейросети произведем на 4-х разных задачах (и, соответственно, будем использовать 4 разных набора учебных данных):
1) логическая операция XOR
2) три поля
3) горизонтальные и вертикальные линии 3х3
4) квадратные цифры 3х5.
Для изучения данной темы требуется знание основ языка JavaScript.

Оглавление:

Видео по теме будет опубликовано позже (если из статьи что-то не понятно, спрашивайте в комментариях).

Описание задачи и наборы данных

Универсальная нейронная сеть может принимать любую структуру (задается с помощью переменной netSize).
В каждом слое, кроме последнего, первый нейрон является нейроном смещения и равен 1. Входные данные также включают нейрон смещения (первый элемент).
Функция активации для каждого нейрона нейронной сети: Сигмоида.
В языке JavaScript формула выглядит так: 1 / (1 + Math.exp( -summator))

Обучение нейросети производится методом обратного распространения ошибки:
Формулы метода
обратного распространения ошибки
для разных слоев
Ошибка нейронов ПОСЛЕДНЕГО слоя рассчитывается по формуле:
"Правильный ответ" минус "Ответ нейрона" и полученный результат умножить на "Производную функции активации для ответа нейрона".
Ошибка=(Правильный ответ - Ответ нейрона)*Производная функции активации
В коде формула выглядит так:
error [ l ][ i ] =(y[ i ]-N[ l ] [ i ])*N[ l ][ i ]*(1-N [ l ][ i ]),
где N[ l ] [ i ] - значение текущего нейрона,
y[ i ] - правильный ответ из учебного набора
Ошибка нейронов ПРЕДПОСЛЕДНЕГО слоя и ОСТАЛЬНЫХ скрытых слоев рассчитывается по формуле (формула учитывает все ошибки, входящие с последующего слоя):

error[ l ] [ i ] = summator*N[ l ][ i ]*(1-N[ l ][ i ]),
где N[ l ] [ i ] - значение текущего нейрона,
summator - это сумма произведений всех входящих ошибок с последующего слоя на веса связи с текущим нейроном.

Предпоследний слой отличается от других скрытых слоев тем, что в последнем слое нет нейрона смещения, для которого ошибка не рассчитывается.
А теперь переходим в наборам данных:
Набор №1 "XOR"
Логическая операция (или то, или это, но не оба сразу). 4 примера.
Минимальная структура нейросети:
netSize = [ 3,3,1 ]
let trainingSet =
[
  [  [1,1,1],  [0]  ],
  [  [1,1,0],  [1]  ],
  [  [1,0,1],  [1]  ],
  [  [1,0,0],  [0] ]
]

let fullCheckSet = trainingSet
Набор №2. Три поля.
Определить цвет ячейки по номеру ряда и столбца. 300 примеров.
Минимальная структура нейросети:
netSize = [ 3,6,3 ]
let trainingSet =
[
[[1,4,1],[1,0,0]],
[[1,3,1],[1,0,0]],
[[1,10,2],[1,0,0]],
[[1,1,19],[0,1,0]],[[1,15,12],[0,0,1]],[[1,14,13],[0,0,1]],[[1,13,10],[0,0,1]],[[1,5,12],[0,1,0]],[[1,9,15],[0,1,0]],[[1,2,20],[0,1,0]],[[1,9,3],[1,0,0]],[[1,5,20],[0,1,0]],[[1,11,6],[0,0,1]],[[1,15,7],[0,0,1]],[[1,3,15],[0,1,0]],[[1,8,12],[0,1,0]],[[1,13,6],[0,0,1]],[[1,7,7],[1,0,0]],[[1,1,7],[1,0,0]],[[1,4,14],[0,1,0]],[[1,13,3],[0,0,1]],[[1,5,14],[0,1,0]],[[1,10,4],[1,0,0]],[[1,3,11],[0,1,0]],[[1,10,13],[0,1,0]],[[1,4,13],[0,1,0]],[[1,4,5],[1,0,0]],[[1,12,11],[0,0,1]],[[1,5,16],[0,1,0]],[[1,2,13],[0,1,0]],[[1,13,14],[0,0,1]],[[1,6,17],[0,1,0]],[[1,11,19],[0,0,1]],[[1,11,17],[0,0,1]],[[1,9,10],[1,0,0]],[[1,7,14],[0,1,0]],[[1,12,5],[0,0,1]],[[1,15,1],[0,0,1]],[[1,8,4],[1,0,0]],[[1,3,16],[0,1,0]],[[1,6,5],[1,0,0]],[[1,3,17],[0,1,0]],[[1,3,2],[1,0,0]],[[1,6,11],[0,1,0]],[[1,4,12],[0,1,0]],[[1,13,11],[0,0,1]],[[1,15,8],[0,0,1]],[[1,6,9],[1,0,0]],[[1,7,18],[0,1,0]],[[1,10,9],[1,0,0]],[[1,12,19],[0,0,1]],[[1,2,3],[1,0,0]],[[1,1,2],[1,0,0]],[[1,7,15],[0,1,0]],[[1,11,8],[0,0,1]],[[1,13,13],[0,0,1]],[[1,14,10],[0,0,1]],[[1,5,8],[1,0,0]],[[1,13,9],[0,0,1]],[[1,9,5],[1,0,0]],[[1,4,7],[1,0,0]],[[1,15,4],[0,0,1]],[[1,13,18],[0,0,1]],[[1,11,16],[0,0,1]],[[1,6,3],[1,0,0]],[[1,3,20],[0,1,0]],[[1,12,7],[0,0,1]],[[1,12,16],[0,0,1]],[[1,3,4],[1,0,0]],[[1,15,11],[0,0,1]],[[1,4,16],[0,1,0]],[[1,12,18],[0,0,1]],[[1,11,12],[0,0,1]],[[1,5,11],[0,1,0]],[[1,13,5],[0,0,1]],[[1,2,8],[1,0,0]],[[1,3,9],[1,0,0]],[[1,11,13],[0,0,1]],[[1,4,17],[0,1,0]],[[1,2,19],[0,1,0]],[[1,4,8],[1,0,0]],[[1,14,2],[0,0,1]],[[1,9,8],[1,0,0]],[[1,14,7],[0,0,1]],[[1,2,2],[1,0,0]],[[1,10,7],[1,0,0]],[[1,14,8],[0,0,1]],[[1,15,19],[0,0,1]],[[1,10,19],[0,1,0]],[[1,8,17],[0,1,0]],[[1,8,16],[0,1,0]],[[1,11,11],[0,0,1]],[[1,14,11],[0,0,1]],[[1,5,9],[1,0,0]],[[1,7,8],[1,0,0]],[[1,12,1],[0,0,1]],[[1,5,2],[1,0,0]],[[1,10,12],[0,1,0]],[[1,6,19],[0,1,0]],[[1,9,16],[0,1,0]],[[1,9,6],[1,0,0]],[[1,8,18],[0,1,0]],[[1,15,5],[0,0,1]],[[1,14,15],[0,0,1]],[[1,15,3],[0,0,1]],[[1,15,18],[0,0,1]],[[1,5,15],[0,1,0]],[[1,4,10],[1,0,0]],[[1,2,9],[1,0,0]],[[1,11,20],[0,0,1]],[[1,4,15],[0,1,0]],[[1,14,1],[0,0,1]],[[1,9,4],[1,0,0]],[[1,6,15],[0,1,0]],[[1,12,2],[0,0,1]],[[1,3,5],[1,0,0]],[[1,6,13],[0,1,0]],[[1,8,7],[1,0,0]],[[1,8,1],[1,0,0]],[[1,13,19],[0,0,1]],[[1,1,20],[0,1,0]],[[1,4,18],[0,1,0]],[[1,12,6],[0,0,1]],[[1,14,6],[0,0,1]],[[1,2,4],[1,0,0]],[[1,8,13],[0,1,0]],[[1,1,11],[0,1,0]],[[1,6,18],[0,1,0]],[[1,10,17],[0,1,0]],[[1,3,7],[1,0,0]],[[1,4,11],[0,1,0]],[[1,7,1],[1,0,0]],[[1,5,3],[1,0,0]],[[1,7,16],[0,1,0]],[[1,1,12],[0,1,0]],[[1,9,11],[0,1,0]],[[1,1,13],[0,1,0]],[[1,6,20],[0,1,0]],[[1,11,3],[0,0,1]],[[1,10,14],[0,1,0]],[[1,5,10],[1,0,0]],[[1,11,7],[0,0,1]],[[1,8,14],[0,1,0]],[[1,6,4],[1,0,0]],[[1,4,4],[1,0,0]],[[1,4,19],[0,1,0]],[[1,9,14],[0,1,0]],[[1,15,6],[0,0,1]],[[1,9,18],[0,1,0]],[[1,14,12],[0,0,1]],[[1,11,5],[0,0,1]],[[1,9,7],[1,0,0]],[[1,10,10],[1,0,0]],[[1,15,16],[0,0,1]],[[1,2,10],[1,0,0]],[[1,1,1],[1,0,0]],[[1,15,9],[0,0,1]],[[1,7,17],[0,1,0]],[[1,8,2],[1,0,0]],[[1,10,8],[1,0,0]],[[1,5,6],[1,0,0]],[[1,2,7],[1,0,0]],[[1,10,18],[0,1,0]],[[1,9,9],[1,0,0]],[[1,9,13],[0,1,0]],[[1,8,5],[1,0,0]],[[1,15,2],[0,0,1]],[[1,15,13],[0,0,1]],[[1,5,17],[0,1,0]],[[1,6,12],[0,1,0]],[[1,3,8],[1,0,0]],[[1,9,12],[0,1,0]],[[1,1,6],[1,0,0]],[[1,6,8],[1,0,0]],[[1,5,13],[0,1,0]],[[1,12,15],[0,0,1]],[[1,1,5],[1,0,0]],[[1,7,20],[0,1,0]],[[1,8,15],[0,1,0]],[[1,2,14],[0,1,0]],[[1,7,13],[0,1,0]],[[1,13,2],[0,0,1]],[[1,3,10],[1,0,0]],[[1,14,9],[0,0,1]],[[1,13,7],[0,0,1]],[[1,3,19],[0,1,0]],[[1,8,6],[1,0,0]],[[1,2,6],[1,0,0]],[[1,6,7],[1,0,0]],[[1,2,12],[0,1,0]],[[1,15,14],[0,0,1]],[[1,9,17],[0,1,0]],[[1,14,18],[0,0,1]],[[1,5,19],[0,1,0]],[[1,8,3],[1,0,0]],[[1,7,12],[0,1,0]],[[1,7,9],[1,0,0]],[[1,11,1],[0,0,1]],[[1,15,20],[0,0,1]],[[1,3,14],[0,1,0]],[[1,10,1],[1,0,0]],[[1,4,6],[1,0,0]],[[1,11,15],[0,0,1]],[[1,1,9],[1,0,0]],[[1,1,4],[1,0,0]],[[1,14,20],[0,0,1]],[[1,7,4],[1,0,0]],[[1,14,16],[0,0,1]],[[1,5,4],[1,0,0]],[[1,14,5],[0,0,1]],[[1,3,13],[0,1,0]],[[1,2,16],[0,1,0]],[[1,5,1],[1,0,0]],[[1,1,17],[0,1,0]],[[1,7,11],[0,1,0]],[[1,6,14],[0,1,0]],[[1,9,20],[0,1,0]],[[1,10,11],[0,1,0]],[[1,3,3],[1,0,0]],[[1,13,20],[0,0,1]],[[1,13,1],[0,0,1]],[[1,4,20],[0,1,0]],[[1,8,19],[0,1,0]],[[1,4,3],[1,0,0]],[[1,15,17],[0,0,1]],[[1,12,17],[0,0,1]],[[1,6,6],[1,0,0]],[[1,13,16],[0,0,1]],[[1,10,20],[0,1,0]],[[1,1,8],[1,0,0]],[[1,12,3],[0,0,1]],[[1,11,4],[0,0,1]],[[1,10,5],[1,0,0]],[[1,14,4],[0,0,1]],[[1,2,1],[1,0,0]],[[1,11,18],[0,0,1]],[[1,15,10],[0,0,1]],[[1,1,15],[0,1,0]],[[1,13,8],[0,0,1]],[[1,14,17],[0,0,1]],[[1,3,18],[0,1,0]],[[1,2,5],[1,0,0]],[[1,12,10],[0,0,1]],[[1,13,17],[0,0,1]],[[1,2,17],[0,1,0]],[[1,1,14],[0,1,0]],[[1,7,5],[1,0,0]],[[1,9,19],[0,1,0]],[[1,11,10],[0,0,1]],[[1,9,2],[1,0,0]],[[1,9,1],[1,0,0]],[[1,14,19],[0,0,1]],[[1,10,15],[0,1,0]],[[1,12,9],[0,0,1]],[[1,15,15],[0,0,1]],[[1,2,15],[0,1,0]],[[1,6,2],[1,0,0]],[[1,2,18],[0,1,0]],[[1,11,2],[0,0,1]],[[1,14,14],[0,0,1]],[[1,12,4],[0,0,1]],[[1,7,2],[1,0,0]],[[1,5,18],[0,1,0]],[[1,13,12],[0,0,1]],[[1,8,8],[1,0,0]],[[1,1,10],[1,0,0]],[[1,10,3],[1,0,0]],[[1,4,9],[1,0,0]],[[1,11,9],[0,0,1]],[[1,12,13],[0,0,1]],[[1,1,16],[0,1,0]],[[1,1,3],[1,0,0]],[[1,13,15],[0,0,1]],[[1,7,10],[1,0,0]],[[1,2,11],[0,1,0]],[[1,8,11],[0,1,0]],[[1,8,9],[1,0,0]],[[1,6,1],[1,0,0]],[[1,10,16],[0,1,0]],[[1,10,6],[1,0,0]],[[1,1,18],[0,1,0]],[[1,5,5],[1,0,0]],[[1,3,12],[0,1,0]],[[1,6,16],[0,1,0]],[[1,12,20],[0,0,1]],[[1,12,8],[0,0,1]],[[1,5,7],[1,0,0]],[[1,4,2],[1,0,0]],[[1,8,20],[0,1,0]],[[1,7,19],[0,1,0]],[[1,6,10],[1,0,0]],[[1,12,12],[0,0,1]],[[1,13,4],[0,0,1]],[[1,7,3],[1,0,0]],[[1,7,6],[1,0,0]],[[1,3,6],[1,0,0]],[[1,12,14],[0,0,1]],[[1,14,3],[0,0,1]],[[1,8,10],[1,0,0]],[[1,11,14],[0,0,1]]
]

let fullCheckSet = trainingSet
Набор №3. Вертикальные и горизонтальные линии.
Определить наличие вертикальных и горизонтальных линий в поле 3х3. 24 примера.
Минимальная структура нейросети:
netSize = [ 10,...,2 ]
let trainingSet =
[
[[1,1,1,1,0,0,0,0,0,0],[1,0]],
[[1,0,0,0,1,1,1,0,0,0],[1,0]],
[[1,0,0,0,0,0,0,1,1,1],[1,0]],
[[1,1,0,0,1,0,0,1,0,0],[0,1]],
[[1,0,1,0,0,1,0,0,1,0],[0,1]],[[1,0,0,1,0,0,1,0,0,1],[0,1]],[[1,0,0,1,0,0,1,1,1,1],[1,1]],[[1,1,0,0,1,0,0,1,1,1],[1,1]],[[1,1,1,1,1,0,0,1,0,0],[1,1]],[[1,1,1,1,0,0,1,0,0,1],[1,1]],[[1,0,1,0,1,1,1,0,1,0],[1,1]],[[1,0,0,1,1,1,1,0,0,1],[1,1]],[[1,1,0,0,1,1,1,1,0,0],[1,1]],[[1,1,1,1,0,1,0,0,1,0],[1,1]],[[1,1,1,1,0,1,0,0,1,0],[1,1]],[[1,1,1,1,0,0,0,1,1,1],[1,0]],[[1,1,0,1,1,0,1,1,0,1],[0,1]],[[1,0,0,0,1,1,1,1,1,1],[1,0]],[[1,1,1,1,1,1,1,0,0,0],[1,0]],[[1,1,1,0,1,1,0,1,1,0],[0,1]],[[1,0,1,1,0,1,1,0,1,1],[0,1]],[[1,1,1,1,1,1,1,1,1,1],[1,1]],[[1,1,1,1,0,1,0,1,1,1],[1,1]],[[1,1,0,1,1,1,1,1,0,1],[1,1]],[[1,0,0,0,0,0,0,0,0,0],[0,0]],]
]

let fullCheckSet = trainingSet
Набор №4. Квадратные цифры.
Определить цифру от 9 до 9 в поле 3х5. 10 примеров.
Минимальная структура нейросети:
netSize = [ 16,26,10 ]
let trainingSet =
[
  [[1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,1],[1,0,0,0,0,0,0,0,0,0]],
  [[1,1,1,1,0,0,1,1,1,1,1,0,0,1,1,1],[0,1,0,0,0,0,0,0,0,0]],
  [[1,1,1,1,0,0,1,1,1,1,0,0,1,1,1,1],[0,0,1,0,0,0,0,0,0,0]],
  [[1,1,0,1,1,0,1,1,1,1,0,0,1,0,0,1],[0,0,0,1,0,0,0,0,0,0]],
  [[1,1,1,1,1,0,0,1,1,1,0,0,1,1,1,1],[0,0,0,0,1,0,0,0,0,0]],
  [[1,1,1,1,1,0,0,1,1,1,1,0,1,1,1,1],[0,0,0,0,0,1,0,0,0,0]],
  [[1,1,1,1,0,0,1,0,0,1,0,0,1,0,0,1],[0,0,0,0,0,0,1,0,0,0]],
  [[1,1,1,1,1,0,1,1,1,1,1,0,1,1,1,1],[0,0,0,0,0,0,0,1,0,0]],
  [[1,1,1,1,1,0,1,1,1,1,0,0,1,1,1,1],[0,0,0,0,0,0,0,0,1,0]],
  [[1,1,1,1,1,0,1,1,0,1,1,0,1,1,1,1],[0,0,0,0,0,0,0,0,0,1]]
]

let fullCheckSet = trainingSet

Переменные и функции:

Размер нейронной сети (константа "Вектор индексов")
const netSize = [3,3,1]
Вектор индексов - это самая первая константа, объявленная в коде нашей сети. Она задается до начала обучения, и, при необходимости, эту константу можно изменять с целью подбора оптимальных параметров сети.
Константа "Вектор индексов" задает размер и структуру сети. Каждый элемент обозначает слой.
Число элементов - это количество слоев.
Значение элемента - количество нейронов в слое.
Данный вектор используется для того, чтобы ссылаясь на него, можно было указать адреса элементов основных массивов сети, таких, как:
N (значения нейронов исходя из последнего рассчитанного примера),
error (значения ошибок для каждого нейрона исходя из последнего рассчитанного примера),
w (значения весовых коэффициентов).
Соответственно, вектор индексов netSize используется и при инициализации всех указанных массивов, для того, чтобы определять число слоев, элементов и т.д.
Узнать число слоев в сети можно с помощью команды netSize.length
Узнать число элементов в конкретном слое можно с помощью команды netSize [ l ], где l - индекс слоя.
Посмотреть структуру сети в консоли можно с помощью вспомогательной функции showStructureOfNet ().

В приведенном примере:
Слой 0 - это входные данные: netSize[0]
Если указать в командной строке netSize[0], то ответом будет число 3 - это число элементов в слое 0.
Слой 1 - скрытый слой: netSize[1] //3
Слой 2 - выходные данные: netSize[2] //1
function showStructureOfNet () //Показать структуру сети
function showStructureOfNet () {
for (let l=0; l<netSize.length; l++) {
console.log(`Слой [${l}],${(l==0)?`входные данные;`:``} элементы:`);
for(let i=0; i<netSize[l]; i++ ) {
console.log(`N [${l}][${i}]${(l!=(netSize.length-1)&&i==0)?`=1, нейрон смещения(bias)`:``}`);
}
}
console.log(`-----------------------------------
Всего слоев - ${netSize.length}.`);
}
let trainingSet //учебный набор
Учебный набор должен включать попарные массивы:
1) ВХОДНЫЕ ДАННЫЕ: число элементов соответствует [0]-му слою, элемент[0]=1
2) ПРАВИЛЬНЫЕ ОТВЕТЫ: число элементов соответствует последнему слою
let fullCheckSet //проверочный набор
В 4-х приведенных простых задачах нейросеть учит данные наизусть, поэтому учебный набор равен проверочному
startCheck() //Проверка правильности внесенных данных
let w = [] //массив весов
function createArray_w () //Инициализация начальных весов при запуске кода
Инициализация трехмерной матрицы весов
Адрес каждого веса между нейроном1 и нейроном2 [l][i][b],
где l - слой нейрона2, i - индекс нейрона2, b - индекс нейрона1
let N=[] //значение нейронов последнего рассчитанного примера
Создаем массив данных N, который сохраняет значения всех рассчитанных нейронов.
Сначала он пустой, но после каждого рассчитанного примера,
будут сохранены последние значения, включая входные данные
function createArray_N () //Создание массива N при запуске кода
let error=[] //массив ошибок
Создаем массив данных Error, который сохраняет значения ошибок для всех рассчитанных нейронов.
Сначала он пустой, но после каждого рассчитанного примера будут оставаться все значения, первый слой - NaN
function createArray_Error () // Создание массива error при запуске кода
let report = [['число эпох','ошибка сети']] //Отчет сети
google.charts.load('current', {'packages':['corechart']}) //Подключаем диаграмму Google Charts, чтобы смотреть отчет сети
function drawChart() {
var data = google.visualization.arrayToDataTable(report);
var options = {
title: 'Отчет сети',
curveType: 'function',
legend: { position: 'bottom' }
};
var chart = new google.visualization.LineChart(document.getElementById('curve_chart'));
chart.draw(data, options);
}
function answer(x=trainingSet[0][0]) //ответ сети
function checkAll(set=fullCheckSet)//Проверка всех примеров
по умолчанию в качестве параметра передается проверочная выборка, но можно вручную подставить и другую
function calculateError (y=trainingSet[0][1]) //Расчет ошибок
function train( epoch = 1, t = 0.3 ) //Обучение сети

Обозначения переменных,
которые используются в циклах:

l - номер слоя.
Переменная не объявляется в качестве глобальной и используется в качестве локальной переменной при переборе циклов. Если требуется сослаться на номер слоя или перебрать слои, то в качестве переменной будем выбирать l.
i - порядковый номер элемента в слое.
Переменная не объявляется в качестве глобальной и используется в качестве локальной переменной при переборе циклов. Если требуется сослаться на номер элемента в слое или перебрать элементы в слое, то в качестве переменной будем выбирать i. Элемент с индексом 0 в каждом слое является нейроном смещения и всегда равен 1 (кроме последнего слоя, в котором нейрон смещения не используется). Адрес любого нейрона слоя выглядит так: [ l ] [ i ]
b - порядковый номер связи нейрона с нейроном предыдущего слоя
Это также локальная переменная, используемая для перебора циклов. Нейроны двух любых соседних слоев связаны по принципу "каждый с каждым", поэтому любой нейрон имеет столько входящих связей, сколько нейронов содержит предыдущий слой. Исключение составляет слой входных данных, который не имеет входящих связей, а еще исключение составляют нейроны смещения, которые также не имеют входящих связей. Чисто с технической точки зрения, для того, чтобы все матрицы сети правильно накладывались друг на друга, адреса связей нейрона смещения все-таки существуют, но их значение в матрице весов равно NaN.

Адреса связей любого нейрона выглядят так: [ l ] [ i ] [ b ]

Ссылки:

Бесплатный редактор кода Atom
КОД (по умолчанию подключен набор №1 XOR). Скачать 2 файла и поместить в одну папку:
index.html
script.js

Первый файл (index.html) - Открыть с помощью - Google Chrome (или другой браузер)
Второй файл с кодом (script.js) - Открыть с помощью - Atom (или другой редактор, можно блокнот)

Файл script.js подключен к файлу index.html (для этого они должны быть в одной папке).
Если вы хотите написать код с нуля, перенесите script.js в другую папку, а в текущей папке создайте пустой текстовый документ и переименуйте его в script.js

Полный текст кода с комментариями:

index.html
<!DOCTYPE html>
<html lang="en" >
      <head>
          <meta charset="UTF-8">
          <title>Универсальная нейросеть на JavaScript</title>

      </head>
      <body>
        Подробное описание этого кода приведено в статье <br/>
 <a href='https://megabyte.ga/na-puti-k-nejroseti/universalnaya-nejroset-set-na-javascript'
 target='_blank'> Универсальная нейросеть на JavaScript </a> <br/><br/>
 Демонстрация в браузере:<br/>
 1) Открыть в Chrome файл index.html<br/>
 2) Открыть консоль F12<br/>
 3) Для обучения нейросети нужно подключить один из 4-х демонстрационных учебных наборов или СВОЙ<br/>
 Набор для обучения записан в переменную trainingSet, набор для проверки в переменную fullCheckSet<br/>
 Вызовите переменную trainingSet, чтобы проверить, что в учебном наборе.<br/>
 4) Архитектуру нейросети требуется предварительно задать в переменную netSize <br/>
 Функция startCheck() автоматически запускается при загрузке кода и проверяет число входов и выходов сети на соответствие учебным наборам<br/>
 5) Запустить функцию checkAll(), которая проверяет все примеры из набора для проверки fullCheckSet и покажет ошибку сети<br/>
 6) Запустить train(100), 100 эпох обучения нейрона на учебном наборе trainingSet <br/>
 5) Повторно запустить checkAll()

  <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
  <script  src="./script.js"></script>
  <div id="curve_chart" style="width: 900px; height: 500px"></div>
    </body>
</html>
script.js
//Задача: Сделать схему универсальной нейронной сети прямого распространения (персептрон, классификация)
//Применить функцию обратного распространения ошибки

//Константа:ВЕКТОР ИНДЕКСОВ
const netSize = [3,3,1];
/*Размер нейронной сети: порядковые номера слоев определяются по индексу: [0], [1], [2]...
слой с индексом [0] - входные данные
netSize[l] - число нейронов в слое l
*/

//Показать структуру сети
function showStructureOfNet () {
  for (let l=0; l<netSize.length; l++) {
    console.log(`Слой [${l}],${(l==0)?`входные данные;`:``} элементы:`);
    for(let i=0; i<netSize[l]; i++ ) {
      console.log(`N [${l}][${i}]${(l!=(netSize.length-1)&&i==0)?`=1, нейрон смещения(bias)`:``}`);
    }
  }
    console.log(`-----------------------------------
Всего слоев - ${netSize.length}.`);
}

/////////////////////////УЧЕБНЫЙ НАБОР/////////////////////////////////////
/*Учебный набор должен включать попарные массивы:
1) ВХОДНЫЕ ДАННЫЕ: число элементов соответствует [0]-му слою, элемент[0]=1
2) ПРАВИЛЬНЫЕ ОТВЕТЫ: число элементов соответствует последнему слою
*/

//Учебный набор XOR x1 ⊕ x2:
let trainingSet =
[
  [  [1,1,1],  [0]  ],
  [  [1,1,0],  [1]  ],
  [  [1,0,1],  [1]  ],
  [  [1,0,0],  [0] ]
]

let fullCheckSet =
[
  [  [1,1,1],  [0]  ],
  [  [1,1,0],  [1]  ],
  [  [1,0,1],  [1]  ],
  [  [1,0,0],  [0] ]
]





//////////////////////////ПРОВЕРКА ПРАВИЛЬНОСТИ ВНЕСЕННЫХ ДАННЫХ//////////////
function startCheck() {
  if
  (netSize[0]!=trainingSet[0][0].length   ||    netSize[netSize.length-1]!=trainingSet[0][1].length)
    {
      alert('Проверьте число элементов в учебной выборке trainingSet ')
    }
      else if
      (netSize[0]!=fullCheckSet[0][0].length   ||   netSize[netSize.length-1]!=fullCheckSet[0][1].length)
      {
          alert('Проверьте число элементов в проверочной выборке fullCheckSet ')
          }
    };

startCheck();



/////////////////////////////////////ИНИЦИАЛИЗАЦИЯ ВЕСОВ////////////////////////
let w = [];

/*Инициализация трехмерной матрицы весов
Адрес каждого веса между нейроном1 и нейроном2 [l][i][b],
где l - слой нейрона2, i - индекс нейрона2, b - индекс нейрона1*/
function createArray_w (){
  w[0]=NaN;
for (let l=1; l<netSize.length; l++) { //перебираем слои
w[l] = []
for (let i=0; i<netSize[l]; i++) { //перебираем нейроны каждого слоя
  w[l][i] = [];
  for (let b=0; b<netSize[(l-1)]; b++) { //перебираем связи каждого нейрона

if ((l!=netSize.length-1)&&i==0) {
  w[l][i][b] = NaN}
else {w[l][i][b] = Math.random()} //Math.random()-0.5
}}}}


createArray_w();




/////////////////////////////////////////////////////////////////////////
/*Создаем массив данных N, который сохраняет значения всех рассчитанных нейронов
Сначала он пустой, но после каждого рассчитанного примера,
 будут сохранены последние значения, включая входные данные*/
 let N=[];

      function createArray_N () {
      for (let l=0; l<netSize.length; l++) { //перебираем слои
        N[l] = [];
        }
      }

createArray_N ()

//Так создавать массив нельзя, потом глючит и заполняет элементы не по одному а сразу все
// let N = new Array(netSize.length);
// N.fill([]);


/////////////////////////////////////////////////////////////////////////////////
//Создаем массив данных Error, который сохраняет значения ошибок для всех рассчитанных нейронов
//Сначала он пустой, но после каждого рассчитанного примера будут оставаться все значения, первый слой - NaN
let error=[];

function createArray_Error (){
  error[0]=NaN;
for (let l=1; l<netSize.length; l++) { //перебираем слои
error[l] = []
 }
}

createArray_Error();


////////////////////////////ОТЧЕТ СЕТИ//////////////////////////////////////////

let report = [['число эпох','ошибка сети']];//report.push([1000,0.5])

let numberOfEpochs = 0;//накопительная переменная
let netError;

// function showReport() {
//   report.push([1000,0.5])
//   google.charts.setOnLoadCallback(drawChart);
// }

google.charts.load('current', {'packages':['corechart']});
     //google.charts.setOnLoadCallback(drawChart);

      function drawChart() {
        var data = google.visualization.arrayToDataTable(report);

        var options = {
          title: 'Отчет сети',
          curveType: 'function',
          legend: { position: 'bottom' }
        };

        var chart = new google.visualization.LineChart(document.getElementById('curve_chart'));

        chart.draw(data, options);
      }


/////////////////////////////////ОТВЕТ СЕТИ/////////////////////////////////////

      function answer(x=trainingSet[0][0]) {
        //в качестве параметра по умолчанию передаем первую часть первого примера из учебного набора
        //заполняем нулевой слой значений массива N входными данными
        N[0]=x;

        for (let l = 1; l < netSize.length; l++) {// перебираем слои, начиная с 1
          for (let i = 0; i < netSize[l]; i++) {//перебираем нейроны

              let summator = 0;

                for (let b = 0; b < netSize[l-1]; b++) {

                    summator+=N[l-1][b]*w[l][i][b]
                }

              N[l][i]= 1 / (1 + Math.exp( -summator));

              //заменяем значения нейронов смещения на 1
                if (l!= netSize.length-1)

                { N[l][0] = 1 }

                //Раскомментировать строчку ниже, если требуется отчет: "Значение каждого нейрона"
                //console.log(`N[${l}][${i}]=${N[l][i]}`);
              }
            }
          }


//////////////////////ПОЛНАЯ ПРОВЕРКА/////////////////

      function checkAll(set=fullCheckSet) {
      //по умолчанию в качестве параметра передается проверочная выборка, но можно вручную подставить и trainingSet
      netError = 0;

      for
        (let sample = 0; sample < fullCheckSet.length; sample++) {//перебираем примеры проверочной выборки

          answer(set[sample][0]);

            console.log(`
----------------------индекс примера: ${sample}
            вход:${set[sample][0]}, целевой ответ:${set[sample][1]},
            ответ сети:${N[netSize.length-1]}`);

            //определяем, правильный ли ответ
            //для этого перебираем правильные ответы и ответы сети. Оба показателя - это массивы

            for (let i = 0; i < set[sample][1].length; i++) {//

              if ((set[sample][1][i]-Math.round(N[netSize.length-1][i]))!=0)
                {
                  netError+=Math.abs(set[sample][1][i]-N[netSize.length-1][i]);

                        console.log(`[${i}] ошибка: ${set[sample][1][i]-N[netSize.length-1][i]}`);
                }

                  else {console.log(`[${i}] правильно`)};
              }
            }
            console.log(`------------------------------------------------------------
              ошибка сети: ${netError}`);

              // показать отчет по ошибке сети (если значение ошибки до обучения в таблице уже заполнено, то ничего не делать )

              // if (report[1]) {
              //
              // }
              //
              // else {
                report.push([numberOfEpochs,netError]) //добавить значение в таблицу

                google.charts.setOnLoadCallback(drawChart); //показать график ошибки
            //  }
            }





//////////////Вспомогательная функции для обучения ////////////////////////////
/////////////////////////Ошибка сети/////////////////////////////////////////


      function calculateError (y=trainingSet[0][1]) { //запускать после answer (x)

        //считаем ошибки последнего слоя (от скрытых слоев отличается формулой)
        for (let l = netSize.length-1, i=0; i < netSize[l]; i++) {
          error [l][i] =(y[i]-N[l][i])*N[l][i]*(1-N[l][i]);
          //Раскомментировать строчку ниже, если требуется отчет: "Значение ошибки каждого нейрона последнего слоя"
          //console.log(`error[${l}][${i}]=${error[l][i]}`);
        }


      /*считаем ошибки предпоследнего скрытого слоя
      (от других скрытых слоев данный слой отличается тем, что учитывает все ошибки,
      входящие с последнего слоя, в последнем слое нет нейрона смещения, для которого ошибка не рассчитывается
      */

        for (let l = netSize.length-2, i = 1; i < netSize[l]; i++) {
          let summator = 0;
            for (let b = 0; b < netSize[l+1]; b++) {
            summator+=error[l+1][b]*w[l+1][b][i];
          }
          error[l][i] = summator*N[l][i]*(1-N[l][i]);
          //Раскомментировать строчку ниже, если требуется отчет: "Значение ошибки каждого нейрона скрытого слоя"
          //console.log(`error[${l}][${i}]=${error[l][i]}`);
        }


        //считаем ошибку остальных скрытых слоев
        for (let l = netSize.length-3; l > 0; l--) {
          for (let i = 1; i < netSize[l]; i++) {
            let summator = 0;
              for (let b = 1; b < netSize[l+1]; b++) {
              summator+=error[l+1][b]*w[l+1][b][i];
            }
            error[l][i] = summator*N[l][i]*(1-N[l][i]);
            //Раскомментировать строчку ниже, если требуется отчет: "Значение ошибки каждого нейрона скрытого слоя"
            //console.log(`error[${l}][${i}]=${error[l][i]}`);
          }
          }

      }



///////////////////////ОБУЧЕНИЕ СЕТИ ////////////////////////////////////////



      function train( epoch = 1, t = 0.3 ) {
        for (let e = 0; e < epoch; e++) { //перебираем эпохи
          for (let sample = 0; sample < trainingSet.length; sample++) {//перебираем примеры обучающей выборки
            answer (trainingSet[sample][0]);
            calculateError(trainingSet[sample][1]);
            ////////////////////////КОРРЕКТИРОВКА ВЕСОВ//////////////////////
            for (let l = 1; l < netSize.length; l++) { //перебираем слои
              for (let i = 0; i < netSize[l]; i++) { //перебираем нейроны в слое
                for (let b = 0; b < netSize[l-1]; b++) { //перебираем связи с нейронами предыдущего слоя
                  //NaN игнорируем и тоже перебираем, потому что NaN останется NaN
                  w[l][i][b]+=t*error[l][i]*N[l-1][b]
                  }

                  }
                }
              }

            }
            numberOfEpochs+=epoch
            checkAll();
          }

Дополнительные материалы:

JavaScript на одном листе

JavaScript

Учебник по JavaScript

Полный курс по JavaScript за 6 часов
от Владилена Минина