Посібник
Машинне навчання · ⏱ ≈ 20 хв читання · Останнє оновлення: 5 липня 2026 р.

Створення нейронної мережі з нуля на JavaScript

Сучасні фреймворки глибокого навчання ховають тисячі рядків за єдиним викликом model.fit(). Цей посібник прибирає все зайве. Ми будуємо повнозв'язну нейронну мережу на чистому JavaScript — множення матриць, функції активації, прямий прохід, функцію втрат за середньоквадратичною похибкою, зворотне поширення та SGD — без жодних зовнішніх залежностей і з повним вихідним кодом, який можна запустити в будь-якому браузері.

1. Нейрони та шари

Окремий штучний нейрон обчислює зважену суму своїх входів плюс зсув, потім застосовує нелінійну функцію активації:

z = w₁x₁ + w₂x₂ + … + wₙxₙ + b (передактивація) a = σ(z) (післяактивація / вихід)

Шар — це стовпець нейронів, що паралельно обробляють той самий вхідний вектор. Усе його обчислення — це один матрично-векторний добуток:

z = W · x + b W ∈ ℝ^{m×n} (матриця ваг, m нейронів, n входів) b ∈ ℝ^m (вектор зсувів) x ∈ ℝ^n (вхідний вектор)

Мережа зчіплює ці шари: вихід одного стає входом наступного. Оскільки кожен шар застосовує лінійне перетворення, за яким іде нелінійність, така композиція є універсальним апроксиматором функцій (теорема про універсальну апроксимацію, Цибенко, 1989).

2. Операції з матрицями

Для швидкості ми подаємо матриці як плоскі Float64Array. Потрібні ключові операції:

// Легка бібліотека матриць — рядково-мажорна, плоский масив
function mat(rows, cols, data) {
  return { rows, cols, d: data ? new Float64Array(data) : new Float64Array(rows * cols) };
}
function matMul(A, B) {
  // C = A (m×k) · B (k×n)
  const C = mat(A.rows, B.cols);
  for (let i = 0; i < A.rows; i++)
    for (let j = 0; j < B.cols; j++) {
      let s = 0;
      for (let k = 0; k < A.cols; k++) s += A.d[i * A.cols + k] * B.d[k * B.cols + j];
      C.d[i * B.cols + j] = s;
    }
  return C;
}
function matT(A) {
  const B = mat(A.cols, A.rows);
  for (let i = 0; i < A.rows; i++)
    for (let j = 0; j < A.cols; j++)
      B.d[j * A.rows + i] = A.d[i * A.cols + j];
  return B;
}
function matAdd(A, b) { // додає (broadcast) вектор-стовпець b до кожного стовпця A
  const C = mat(A.rows, A.cols);
  for (let i = 0; i < A.rows; i++)
    for (let j = 0; j < A.cols; j++)
      C.d[i * A.cols + j] = A.d[i * A.cols + j] + b.d[i];
  return C;
}

3. Функції активації

Sigmoid

σ(z) = 1/(1+e⁻ᶻ) — стискає до (0,1), класична, але схильна до зникнення градієнтів. σ'(z) = σ(z)(1−σ(z)).

Tanh

tanh(z) — вихід, центрований навколо нуля, у (−1,1), зазвичай кращий за сигмоїду для прихованих шарів. tanh'(z) = 1−tanh²(z).

ReLU

max(0,z) — без насичення для додатних значень, розріджена активація, проста похідна: 0 або 1. Вибір за замовчуванням для глибоких мереж.

Softmax

eᶻᵢ/Σeᶻⱼ — перетворює вектор логітів на розподіл імовірностей, використовується у вихідному шарі для багатокласової класифікації.

const activations = {
  sigmoid: {
    f:  z => 1 / (1 + Math.exp(-z)),
    df: a => a * (1 - a)   // a = σ(z)
  },
  tanh: {
    f:  z => Math.tanh(z),
    df: a => 1 - a * a    // a = tanh(z)
  },
  relu: {
    f:  z => Math.max(0, z),
    df: a => a > 0 ? 1 : 0
  }
};

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

Прямий прохід пропускає вхід крізь кожен шар, зберігаючи передактивацію Z та післяактивацію A на кожному шарі — вони знадобляться під час зворотного поширення:

Для шару ℓ = 1 … L: Z[ℓ] = W[ℓ] · A[ℓ−1] + b[ℓ] A[ℓ] = σ(Z[ℓ]) (поелементно) A[0] = X (сирий вхід) Ŷ = A[L] (вихід мережі)

5. Функції втрат

Функція втрат вимірює, наскільки прогноз мережі Ŷ помилковий порівняно з істинним значенням Y:

MSE (регресія): L = (1/n) Σ (ŷᵢ − yᵢ)² BCE (бінарна класифікація): L = −(1/n) Σ [yᵢ log ŷᵢ + (1−yᵢ) log(1−ŷᵢ)] CE (багатокласова): L = −(1/n) Σ Σ yᵢⱼ log ŷᵢⱼ Градієнт MSE за Ŷ: dL/dŶ = 2(Ŷ − Y)/n

Градієнт функції втрат за активаціями вихідного шару є відправною точкою для зворотного поширення.

6. Зворотне поширення

Зворотне поширення застосовує ланцюгове правило, щоб поширити градієнт втрат назад крізь кожен шар. Для шару ℓ (рухаючись від L униз до 1):

δ[L] = dL/dŶ ⊙ σ'(Z[L]) (сигнал помилки вихідного шару) Для ℓ = L−1 … 1: δ[ℓ] = (W[ℓ+1]ᵀ · δ[ℓ+1]) ⊙ σ'(Z[ℓ]) Градієнти: dW[ℓ] = δ[ℓ] · A[ℓ−1]ᵀ / n db[ℓ] = mean(δ[ℓ], axis=samples) Оновлення SGD: W[ℓ] ← W[ℓ] − lr · dW[ℓ] b[ℓ] ← b[ℓ] − lr · db[ℓ]
позначає поелементний (адамарів) добуток. Ключова ідея в тому, що δ[ℓ] повідомляє кожному шару, наскільки він спричинив загальну помилку, дозволяючи кожній вазі отримати цілеспрямовану корекцію.

7. Повна реалізація мережі

// Повнозв'язна мережа — чистий JS, без залежностей
class DenseLayer {
  constructor(inSize, outSize, activation = 'sigmoid') {
    this.activation = activations[activation];
    // Ініціалізація Ксав'є / Глоро
    const scale = Math.sqrt(2 / (inSize + outSize));
    this.W = mat(outSize, inSize,
      Array.from({length: outSize * inSize}, () => (Math.random() * 2 - 1) * scale));
    this.b = mat(outSize, 1);  // нулі
    this.Z = null; this.A = null; this.A_prev = null;
  }
  forward(A_prev) {
    this.A_prev = A_prev;
    this.Z = matAdd(matMul(this.W, A_prev), this.b);
    this.A = mat(this.Z.rows, this.Z.cols,
      this.Z.d.map(this.activation.f));
    return this.A;
  }
  backward(delta_next) {
    // delta_next — це δ із шару вище (вже помножене на W[ℓ+1]ᵀ, якщо є)
    const dZ = mat(this.Z.rows, this.Z.cols,
      delta_next.d.map((v, i) => v * this.activation.df(this.A.d[i])));
    const m = this.A_prev.cols;
    this.dW = scalarDiv(matMul(dZ, matT(this.A_prev)), m);
    this.db = rowMean(dZ);
    return matMul(matT(this.W), dZ);  // δ для передачі шару ℓ−1
  }
  update(lr) {
    for (let i = 0; i < this.W.d.length; i++) this.W.d[i] -= lr * this.dW.d[i];
    for (let i = 0; i < this.b.d.length; i++) this.b.d[i] -= lr * this.db.d[i];
  }
}

class NeuralNetwork {
  constructor(...layers) { this.layers = layers; }
  predict(X) {
    let A = X;
    for (const layer of this.layers) A = layer.forward(A);
    return A;
  }
  train(X, Y, lr = 0.1) {
    const Yhat = this.predict(X);
    // Градієнт MSE: 2*(Yhat-Y)/n
    const m = X.cols;
    let delta = mat(Yhat.rows, Yhat.cols,
      Yhat.d.map((v, i) => 2 * (v - Y.d[i]) / m));
    for (let i = this.layers.length - 1; i >= 0; i--)
      delta = this.layers[i].backward(delta);
    for (const layer of this.layers) layer.update(lr);
    return mseLoss(Yhat, Y);
  }
}

// Допоміжне: ділення на скаляр, середнє по рядку
function scalarDiv(M, s) { return mat(M.rows, M.cols, M.d.map(v => v / s)); }
function rowMean(M) {
  const b = mat(M.rows, 1);
  for (let i = 0; i < M.rows; i++) {
    let s = 0;
    for (let j = 0; j < M.cols; j++) s += M.d[i * M.cols + j];
    b.d[i] = s / M.cols;
  }
  return b;
}
function mseLoss(Yhat, Y) {
  return Yhat.d.reduce((s, v, i) => s + (v - Y.d[i]) ** 2, 0) / Yhat.d.length;
}

8. Навчання на XOR

XOR — це класична лінійно нероздільна задача, яку одношаровий перцептрон розв'язати не може. Прихований шар принаймні з 2 нейронів — може:

// Набір даних XOR — 4 зразки, пакетне навчання
// Входи як стовпці (2 рядки × 4 зразки)
const X = mat(2, 4, [0,0,1,1,  0,1,0,1]);
const Y = mat(1, 4, [0,1,1,0]);

const net = new NeuralNetwork(
  new DenseLayer(2, 4, 'tanh'),   // прихований: 4 нейрони
  new DenseLayer(4, 1, 'sigmoid') // вихідний: 1 нейрон
);

for (let epoch = 0; epoch < 10000; epoch++) {
  const loss = net.train(X, Y, 0.5);
  if (epoch % 1000 === 0)
    console.log(`Epoch ${epoch}: loss = ${loss.toFixed(5)}`);
}

// Після навчання прогнози мають наближатися до [0, 1, 1, 0]
const pred = net.predict(X);
console.log('Predictions:', [...pred.d].map(v => v.toFixed(3)));

Запуск цього в консолі браузера навчає за мілісекунди. Після 10 000 епох мережа надійно видає значення, близькі до [0.02, 0.97, 0.97, 0.02] — правильно розв'язуючи XOR взагалі без бібліотек.

Наступний крок: див. Зворотне поширення — ланцюгове правило крок за кроком для глибшого погляду на обчислювальні графи, якобіани та автоматичне диференціювання.
🧠 Відкрити нейронну мережу →