Нейронна мережа на JavaScript за 7 хвилин

7 хв. читання

Всім привіт! У своїй попередній статті я розповідав про класифікацію зображень за допомогою згорткових нейронних мереж. Можливо, ті з вас, хто тільки почав знайомитись із нейронними мережами, вирішили, що це дуже складна тема і туди не варто лізти взагалі 😅 Сьогодні я спробую довести вам, що це не так 😁.

Для хорошого розуміння та засвоєння матеріалу ви маєте знати наступні речі:

  • впевнені знання ООП, JavaScript та ES6;
  • базова математика 🤗;
  • основи лінійної алгебри (операції з векторами та матрицями).

Отож, пишемо нейронну мережу на JavaScript.

Трохи теорії

Під нейронною мережею розуміють об'єднання нейронів між собою за допомогою зв'язків, що називаються синапсами. Нейрон можна розглядати як певну функцію, що отримує вхідні дані та видає якийсь результат.

Нейронна мережа на JavaScript за 7 хвилин
Нейрон

Кожен синапс має свою вагу. Чим більший ваговий коефіцієнт, тим більший вплив даний нейрон буде здійснювати на інші нейрони. Отож, основними елементами нейронної мережі є нейрони, які об'єднуються в шари певним чином.

У кожній нейромережі існує один вхідний шар, хоча б один прихований та один вихідний шар. Коли нейрони кожного шару з'єднуються з кожним нейроном наступного шару, то вони утворюють багатошаровий перцептрон (MLP — multilayer perceptron). Якщо мережа має більше ніж один прихований шар, то її прийнято називати глибокою.

Нейронна мережа на JavaScript за 7 хвилин
Багатошаровий перцептрон

На рисунку зображено MLP типу 6-4-3-1, тобто 6 нейронів у вхідному шарі, 4 у першому прихованому, 3 у другому прихованому та 1 — у вихідному.

Прямий прохід

Як ви вже зрозуміли, нейрон може мати один чи декілька входів, що мають свою вагу. Цими входами можуть бути виходи інших нейронів.

Нейронна мережа на JavaScript за 7 хвилин
Нейрон

Поясню рисунок:

  • X1 та X2 — вхідні дані;
  • w1 та w2 — вага синапсів;
  • f(x1, x2) — функція нейрона, що перетворює вхідні дані;
  • Y — вихідне значення.

Вхідне значення нейрона (також називають сукупний ввід) обчислюється за наступною формулою:

\\[ net = \sum_{i=1}^n x_iw_i + b\\]

де n — кількість входів, x — вхід, w — вага, b — зміщення (ми не будемо його використовувати). Тобто, для кожного вхідного зразка нам потрібно перемножити вхідні дані на вагу, а потім додати всі добутки. Отримане значення і буде вхідним значенням для нейрона. Далі, це значення необхідно «пропустити» через функцію, що називається функцією активації. Вихідне значення і буде виходом нейрона. Цю операцію потрібно повторити для всіх нейронів у вашій мережі. Це називається прямим проходом (forward propagation).

Зворотний прохід

Для оцінки якості роботи моделі використовується зворотний прохід (back propagation), що складається з декількох кроків, які потрібно повторити для кожного нейрона у кожному шарі.

  • Спочатку потрібно порахувати помилку нейрона у вихідному шарі за формулою:

\\[ error = target - output \\]

де target — правильне значення, яке має видавати мережа, output — значення, що видала мережа.

  • Далі, нам потрібно порахувати «дельту», тобто значення на яке потрібно буде змінити вагові коефіцієнти:

\\[ \Delta error = f'(output) * error \\]

де f'(x) — похідна функції активації (охх, ця математика 😆).

  • Наступним кроком є обчислення помилки нейрона у прихованому шарі:

\\[error_h = \Delta error* synapse_{hidden\ o output}\\]

де synapse — вага синапса, що з'єднує нейрон прихованого шару та вихідний нейрон.

  • І знову рахуємо дельту, але вже для нейронів прихованого шару:

\\[ \Delta error_h = f'(output_h) * error_h\\]

де output — вихід нейрона у прихованому шарі.

  • Тепер саме час оновити вагові коефіцієнти 🙄:

\\[synapse_{hidden\ o output} = synapse_{hidden\ o output} + output_h * \Delta error * lrate \\] \\[synapse_{input\ o hidden} = synapse_{input\ o hidden} + input_{data} * \Delta error_h * lrate \\]

де lrate — норма (швидкість) навчання.

Ми використали метод зворотного поширення помилки та градієнтний спуск 😯 Для більшого розуміння даних методів ви можете переглянути це відео:

Нарешті, з математикою розібралися. Тепер переходимо до практики 😉.

Практика

Отже, будемо писати нейронну мережу (зокрема, MLP) для вирішення класичної проблеми XOR 😄. Маємо такі вхідні та вихідні дані:

Вхід Вихід
0 0 0
0 1 1
1 0 1
1 1 0

Будемо використовувати Node.js та бібліотеку math.js — щось подібне до Numpy у Python. Отож, запустіть наступні команди у терміналі:

mkdir mlp && cd mlp
npm init
npm install babel-cli babel-preset-env mathjs

Спочатку створимо файл activations.js, де опишемо функції активації. Використовуватимемо логістичну функцію (все по-класиці 😁).

import {exp} from 'mathjs'

export function sigmoid(x, derivative) {
	let fx = 1 / (1 + exp(-x));
	if (derivative)
		return fx * (1 - fx);
	return fx;
}

Далі створюємо файл nn.js в якому розміщуємо клас NeuralNetwork.

import {random, multiply, dotMultiply, mean, abs, subtract, transpose, add} from 'mathjs'
import * as activation from './activations'

export class NeuralNetwork {
    constructor(...args) {
       this.input_nodes = args[0]; //кількість вхідних нейронів
       this.hidden_nodes = args[1]; //кількість нейронів у прихованому шарі
       this.output_nodes = args[2]; //кількість вихідних нейронів

       this.epochs = 50000; //кількість епох навчання
       this.activation = activation.sigmoid; //функція активації
       this.lr = .5; //норма навчання
       this.output = 0; //вихід нейромережі

       //генеруємо матриці синапсів (вагових коефіцієнтів) із значеннями в інтервалі від -1.0 до 1.0
       this.synapse0 = random([this.input_nodes, this.hidden_nodes], -1.0, 1.0); //синапси від вхідного шару до прихованого
       this.synapse1 = random([this.hidden_nodes, this.output_nodes], -1.0, 1.0); //синапси від прихованого шару до вихідного

   }
}	

Додамо метод train до нашого класу:

train(input, target) {
	for (let i = 0; i < this.epochs; i++) {
		//пряме поширення
		let input_layer = input; //вхідні дані
		let hidden_layer = multiply(input_layer, this.synapse0).map(v => this.activation(v, false)); //вихід нейронів прихованого шару (матриця!)
		let output_layer = multiply(hidden_layer, this.synapse1).map(v => this.activation(v, false)); // вихід нейронів вихідного шару (матриця!)
		
		//зворотнє поширення
		let output_error = subtract(target, output_layer); //обчислюємо помилку (матриця!)       
		let output_delta = dotMultiply(output_error, output_layer.map(v => this.activation(v, true))); //обчислюємо дельту (вектор!)
		let hidden_error = multiply(output_delta, transpose(this.synapse1)); //обчислюємо помилку нейронів прихованого шару (матриця!)
		let hidden_delta = dotMultiply(hidden_error, hidden_layer.map(v => this.activation(v, true))); //обчислюємо дельту (вектор!)
    
		//оновлюємо ваги (градієнтний спуск!)
		this.synapse1 = add(this.synapse1, multiply(transpose(hidden_layer), multiply(output_delta, this.lr)));
		this.synapse0 = add(this.synapse0, multiply(transpose(input_layer), multiply(hidden_delta, this.lr)));
		this.output = output_layer;

		if (i % 10000 == 0)
			console.log(`Error: ${mean(abs(output_error))}`);
		}
}

Також додамо метод predict для виводу результату:

predict(input) {
	let input_layer = input;
	let hidden_layer = multiply(input_layer, this.synapse0).map(v => this.activation(v, false));
	let output_layer = multiply(hidden_layer, this.synapse1).map(v => this.activation(v, false));
	return output_layer;
}

Ну і вкінці створимо файл index.js, де поєднаємо все, що ми створили:

import {NeuralNetwork} from './nn' 
import {matrix} from 'mathjs'

const input = matrix([[0,0], [0,1], [1,0], [1,1]]);
const target = matrix([[0], [1], [1], [0]]);

const nn = new NeuralNetwork(2, 4, 1); //2 входи, бо на вхід йде 2 числа, 1 вихід — на виході 1 число
nn.train(input, target);
console.log(nn.predict(input));

Запускаємо npm start:

Нейронна мережа на JavaScript за 7 хвилин
Результат

Як бачите, помилка зменшується і в кінці наша нейромережа видає результати дуже близькі до істинних! Звичайно, ця нейромережа дуже проста, але головне, що це працює 😉 Надіюся, що вам було цікаво! Якщо виникають якісь питання, або щось не до кінця зрозуміло — пишіть в коментарі.

Вихідний код на GitHub.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 629
Приєднався: 1 рік тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід