Neural Networks Basics

Вступ до нейронних мереж — від персептрона до багатошарових мереж

Еволюція від одношарового персептрона до багатошарових нейронних мереж. Функції активації, прямий та зворотний прохід, оптимізатори.

Від одного нейрона до мереж — чому лінійності недостатньо

Уявіть, що ви програміст у 1950-х роках і отримуєте завдання: навчити комп'ютер розпізнавати рукописні цифри. У вас немає готових бібліотек, лише базові математичні операції. З чого почати?

Інтуїція підказує: кожен піксель зображення — це число (яскравість). Якщо помножити кожен піксель на свою «вагу важливості» та просумувати, отримаємо «оцінку схожості» на певну цифру. Саме так народилася ідея персептрона — першої нейронної мережі.

Але швидко з'ясувалося: одного персептрона недостатньо. Він може розв'язати лише найпростіші задачі — ті, де класи можна розділити прямою лінією. Більшість реальних задач — складніші. Потрібна мережа персептронів, де виходи одних стають входами для інших. Так з'явилися багатошарові нейронні мережі.

🎯 Мета статті

  • Зрозуміти структуру та обмеження одношарового персептрона
  • Опанувати концепцію багатошарового персептрона (MLP) та роль прихованих шарів
  • Вивчити функції активації та їхню критичну роль у створенні нелінійності
  • Розібрати алгоритми прямого поширення (forward propagation) та зворотного поширення помилки (backpropagation)
  • Познайомитися з оптимізаторами SGD, Momentum та Adam

🔑 Ключові терміни

  • Персептрон (Perceptron): найпростіша нейронна мережа з одним шаром ваг
  • MLP (Multi-Layer Perceptron): багатошарова нейронна мережа з прихованими шарами
  • Функція активації (Activation Function): нелінійна функція, що перетворює зважену суму нейрона
  • Прямий прохід (Forward Propagation): обчислення виходу мережі від входу до виходу
  • Зворотний прохід (Backpropagation): обчислення градієнтів втрат по всіх вагах мережі
  • Оптимізатор (Optimizer): алгоритм оновлення ваг на основі градієнтів

Архітектура персептрона — перший штучний нейрон

Персептрон — це математична модель біологічного нейрона, запропонована Френком Розенблаттом у 1957 році. Його структура надзвичайно проста, але концептуально революційна для свого часу.

Loading diagram...

@startuml skinparam backgroundColor #FFFFFF skinparam defaultFontName -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif skinparam roundcorner 8 skinparam shadowing false skinparam handwritten false

!define BIO_COLOR EFF6FF !define MATH_COLOR F0FDF4

rectangle "Біологічний нейрон" as bio #BIO_COLOR { card "Дендрити (Dendrites)\nПрийом вхідних електрохімічних імпульсів" as d card "Тіло клітини / Сома (Soma)\nСумація та інтеграція потенціалів" as s card "Аксон (Axon) & Синапс (Synapse)\nГенерація спайку при перевищенні порогу" as a d -right-> s s -right-> a }

rectangle "Математичний персептрон Розенблатта" as math #MATH_COLOR { card "Входи x₁, x₂, ..., xₙ\nЧислові ознаки з вагами wᵢ" as x card "Суматор Σ (Linear Sum)\nz = Σ wᵢ·xᵢ + b" as sum card "Функція активації σ(z)\nПороговий перехід: ŷ = 1 якщо z ≥ 0, інакше 0" as act x -right-> sum sum -right-> act }

d .down.> x : аналог s .down.> sum : аналог a .down.> act : аналог

@enduml

Структура персептрона

Персептрон складається з чотирьох компонентів:

Крок 1. Вхідний шар (Input Layer)

Кожна ознака об'єкта — це окремий вхід Завантаження.... Наприклад, для класифікації квіта Ірис:

  • Завантаження... = довжина пелюстки
  • Завантаження... = ширина пелюстки
  • Завантаження... = довжина чашолистка
  • Завантаження... = ширина чашолистка

Крок 2. Ваги (Weights) та зсув (Bias)

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

Інтуїція: Якщо довжина пелюстки дуже важлива для розпізнавання виду, то Завантаження... буде великою вагою. Якщо ознака неінформативна — вага буде близькою до нуля.

Крок 3. Зважена сума (Weighted Sum)

Обчислюється лінійна комбінація входів та ваг:

Завантаження...

Або у векторній формі:

Завантаження...

Крок 4. Функція активації (Activation Function)

Зважена сума Завантаження... перетворюється функцією активації Завантаження.... Для класичного персептрона це функція кроку (step function):

Завантаження...
\hat{y} = \sigma(z) = \begin{cases} 1, & \text{якщо } z \geq 0 \ 0, & \text{якщо } z < 0 \end{cases}

Результат Завантаження... — це передбачення класу: 1 або 0.

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

Завантаження...
\hat{y} = f(\mathbf{x}) = \sigma(\mathbf{w}^T \mathbf{x} + b) = \begin{cases} 1, & \text{якщо } \mathbf{w}^T \mathbf{x} + b \geq 0 \ 0, & \text{якщо } \mathbf{w}^T \mathbf{x} + b < 0 \end{cases}

Або у розгорнутій формі через суму всіх вхідних ознак:

Завантаження...
\hat{y} = f(\mathbf{x}) = \sigma\left(\sum*^{n} w_i x_i + b\right) = \begin{cases} 1, & \text{якщо } \sum*^{n} wi x_i + b \geq 0 \ 0, & \text{якщо } \sum^{n} w_i x_i + b < 0 \end{cases}

Візуалізація персептрона

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "SF Pro Display, Arial"

!define INPUT_COLOR DBEAFE
!define NEURON_COLOR FEF3C7
!define OUTPUT_COLOR DCFCE7

rectangle "Вхідні ознаки" as input {
  (x₁) #INPUT_COLOR
  (x₂) #INPUT_COLOR
  (x₃) #INPUT_COLOR
  (x₄) #INPUT_COLOR
}

rectangle "Нейрон (Персептрон)" as neuron #NEURON_COLOR {
  card "z = Σ wᵢxᵢ + b" as sum
  card "σ(z)" as activation
  sum -down-> activation : активація
}

rectangle "Вихід" as output {
  (ŷ) #OUTPUT_COLOR
}

(x₁) -right-> sum : w₁
(x₂) -right-> sum : w₂
(x₃) -right-> sum : w₃
(x₄) -right-> sum : w₄

activation -right-> (ŷ) : клас 0 або 1

note right of sum
  Зважена сума:
  **z = w₁·x₁ + w₂·x₂ + w₃·x₃ + w₄·x₄ + b**
end note

note right of activation
  Функція кроку:
  **σ(z) = 1 якщо z ≥ 0, інакше 0**
end note

@enduml

Приклад: персептрон для логічного АБО (OR)

Розглянемо найпростішу задачу: навчити персептрон логічній операції OR.

Таблиця істинності OR:

x₁x₂y (OR)
000
011
101
111

Питання: чи можна знайти такі ваги Завантаження..., щоб персептрон правильно класифікував всі 4 приклади?

Відповідь: так! Наприклад, Завантаження....

Перевірка:

perceptron_or.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
import numpy as np

def step_function(z):
    """Функція кроку: 1 якщо z >= 0, інакше 0"""
    return 1 if z >= 0 else 0

# Ваги персептрона для OR
w1, w2, b = 1.0, 1.0, -0.5

# Перевірка всіх комбінацій
inputs = [(0, 0), (0, 1), (1, 0), (1, 1)]

print("x₁  x₂  | z = w₁·x₁ + w₂·x₂ + b | σ(z) | Очікується")
print("-" * 55)

for x1, x2 in inputs:
    z = w1 * x1 + w2 * x2 + b
    output = step_function(z)
    expected = int(x1 or x2)
    status = "✓" if output == expected else "✗"
    print(f"{x1}   {x2}   |  {z:6.2f}               | {output}    | {expected}        {status}")
Out[1]:
$x_1$$x_2$$z = w_1 x_1 + w_2 x_2 + b$$\sigma(z)$ (вихід)Очікуване (OR)Статус тесту
00-0.5000✅ Успішно (клас 0)
01+0.5011✅ Успішно (клас 1)
10+0.5011✅ Успішно (клас 1)
11+1.5011✅ Успішно (клас 1)
Геометрична інтерпретація: Персептрон для OR знаходить пряму лінію Завантаження..., яка розділяє простір на два класи. Точки вище лінії — клас 1, нижче — клас 0.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "SF Pro Display, Arial"

!define INPUT_COLOR DBEAFE
!define GATE_COLOR FEF3C7
!define OUTPUT_COLOR DCFCE7

rectangle "Логічний вентиль OR (АБО)" as or_gate #GATE_COLOR {
  card "w₁ = 1.0, w₂ = 1.0\nb = -0.5" as or_weights
  card "Межа розділення:\nx₁ + x₂ - 0.5 = 0" as or_line
}

rectangle "Логічний вентиль AND (І)" as and_gate #GATE_COLOR {
  card "w₁ = 1.0, w₂ = 1.0\nb = -1.5" as and_weights
  card "Межа розділення:\nx₁ + x₂ - 1.5 = 0" as and_line
}

node "(0,0) → 0\n(0,1) → 1\n(1,0) → 1\n(1,1) → 1" as or_truth #INPUT_COLOR
node "(0,0) → 0\n(0,1) → 0\n(1,0) → 0\n(1,1) → 1" as and_truth #INPUT_COLOR

or_truth -down-> or_gate : лінійно роздільні
and_truth -down-> and_gate : лінійно роздільні

card "Висновок: обидві задачі успішно розв'язуються\nоднією прямою гіперплощиною" as conclusion #OUTPUT_COLOR
or_gate -down-> conclusion
and_gate -down-> conclusion

@enduml

Проблема XOR — межа лінійної роздільності

Персептрон успішно вирішує логічні операції AND, OR, NOT. Але існує одна проста операція, яка повністю розбиває одношаровий персептрон: XOR (виключне АБО).

Таблиця істинності XOR:

x₁x₂y (XOR)
000
011
101
110

Питання: чи можна знайти ваги Завантаження... для персептрона, щоб правильно класифікувати XOR?

Відповідь: НІ! Це математично неможливо.


Чому персептрон не може розв'язати XOR?

Персептрон — це лінійний класифікатор. Він може провести лише одну пряму лінію для розділення класів. Подивимося на візуалізацію XOR:

xor_visualization.ipynb
Python 3.11 (ipykernel)
Code
In [2]:
0.31s
import matplotlib.pyplot as plt
import numpy as np

# Дані XOR
x_coords = [0, 0, 1, 1]
y_coords = [0, 1, 0, 1]
colors = ['red', 'blue', 'blue', 'red']  # клас 0 — червоний, клас 1 — синій
labels = [0, 1, 1, 0]

plt.figure(figsize=(8, 6))
plt.scatter(x_coords, y_coords, c=colors, s=300, edgecolors='black', linewidths=2, alpha=0.7)

# Додаємо підписи точок
for i, (x, y, label) in enumerate(zip(x_coords, y_coords, labels)):
    plt.text(x, y, f'  ({x}, {y})\n  y={label}', fontsize=12, ha='left')

# Спроба провести лінію розділення (завжди помилиться)
plt.axline((0.5, 0), slope=1, color='gray', linestyle='--', linewidth=2, alpha=0.5, label='Неможлива лінія розділення')

plt.xlim(-0.3, 1.5)
plt.ylim(-0.3, 1.5)
plt.xlabel('x₁', fontsize=14)
plt.ylabel('x₂', fontsize=14)
plt.title('Проблема XOR: класи не є лінійно роздільними', fontsize=16, fontweight='bold')
plt.legend(fontsize=12)
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
Out[2]:
Критичне обмеження персептрона:
  • Персептрон може розв'язувати лише лінійно роздільні задачі — ті, де класи можна розділити гіперплощиною (у 2D — прямою лінією, у 3D — площиною).
  • Для XOR червоні точки (клас 0) розташовані діагонально від синіх (клас 1). Жодна пряма лінія не може їх розділити — потрібна крива або дві лінії. Одношаровий персептрон на це не здатний.
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "SF Pro Display, Arial"

!define CLASS0_COLOR FEE2E2
!define CLASS1_COLOR DBEAFE
!define LIMIT_COLOR FEF3C7

package "Простір входів XOR (x₁, x₂)" {
  node "(0, 0)\ny = 0" as p00 #CLASS0_COLOR
  node "(1, 1)\ny = 0" as p11 #CLASS0_COLOR
  node "(0, 1)\ny = 1" as p01 #CLASS1_COLOR
  node "(1, 0)\ny = 1" as p10 #CLASS1_COLOR
}

package "Обмеження одношарового персептрона" as limit #LIMIT_COLOR {
  card "Лінійна модель: w₁·x₁ + w₂·x₂ + b = 0" as model
  card "Пряма лінія може розділити лише сусідні точки,\nале точки однакових класів розташовані діагонально!" as conflict
}

p00 ..> conflict : діагональ 1
p11 ..> conflict : діагональ 1
p01 ..> conflict : діагональ 2
p10 ..> conflict : діагональ 2

card "Рішення: Багатошаровий персептрон (MLP)\nіз нелінійною активацією" as solution #DCFCE7
conflict -down-> solution : подолання обмеження
@enduml

Історичний контекст: «зима AI» через XOR

У 1969 році математики Марвін Мінський та Сеймур Паперт опублікували книгу «Персептрони», де математично довели, що одношаровий персептрон принципово не може розв'язати XOR.

Це викликало першу «зиму штучного інтелекту» (1970–1980-ті роки) — різке скорочення фінансування досліджень AI, бо вважалося, що нейронні мережі — тупиковий шлях.

Аналогія з будівництвом:

Уявіть, що ви маєте лише прямокутні цеглини. З них можна збудувати стіни, але неможливо створити арку чи купол — потрібні цеглини інших форм. Одношаровий персептрон — це «прямокутні цеглини». Для складних форм (нелінійних задач) потрібні багатошарові мережі — цеглини різних форм і розмірів.

Рішення проблеми: додати приховані шари нейронів між входом і виходом. Так з'являються багатошарові нейронні мережі (Multi-Layer Perceptron, MLP).

Loading diagram...

@startuml skinparam backgroundColor #FFFFFF skinparam defaultFontName -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif skinparam roundcorner 8 skinparam shadowing false skinparam handwritten false

skinparam state { BackgroundColor #F8FAFC BorderColor #94A3B8 FontColor #0F172A FontSize 12 ArrowColor #64748B }

title Еволюційний шлях: від ейфорії персептрона до подолання AI Winter

state "1957: Ейфорія (Френк Розенблатт)" as S1 #EFF6FF : Винахід персептрона\nУспіх на лінійно роздільних задачах (OR, AND) state "1969: Криза (Мінський і Пейперт)" as S2 #FEF2F2 : Математичний доказ:\nодношаровий персептрон не може розв'язати XOR state "1970-і: Перша «Зима ШІ» (AI Winter)" as S3 #F1F5F9 : Замороження фінансування\nСкептицизм академічної спільноти state "1986: Відродження (Rumelhart, Hinton, Williams)" as S4 #ECFDF5 : Багатошаровий персептрон (MLP)\n+ Алгоритм Backpropagation розв'язують XOR

S1 -down-> S2 : Виявлення межі лінійності S2 -down-> S3 : Втрата інтересу та грантів S3 -down-> S4 : Поява прихованих шарів та градієнтного спуску

@enduml


Багатошаровий персептрон (MLP) — прорив у нелінійності

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

Loading diagram...

@startuml skinparam backgroundColor #FFFFFF skinparam defaultFontName -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif skinparam roundcorner 8 skinparam shadowing false skinparam handwritten false

skinparam rectangle { RoundCorner 8 BorderColor #94A3B8 FontSize 12 }

rectangle "Вхідний шар (Input Layer)" as in_layer #EFF6FF { card "Ознаки x₁...xₙ\n(Сирі числові дані)" as in_nodes }

rectangle "Приховані шари (Hidden Layers)" as hid_layer #FEF3C7 { card "Прихований шар 1\nНелінійне комбінування: a^1 = σ(W^1x + b^1)" as h1 card "Прихований шар 2\nІєрархічні абстрактні ознаки: a^2 = σ(W^2a^1 + b^2)" as h2 h1 -right-> h2 : активації }

rectangle "Вихідний шар (Output Layer)" as out_layer #ECFDF5 { card "Фінальний вихід ŷ\nЙмовірність класу / регресійне число" as out_nodes }

in_nodes -right-> h1 : W^1, b^1 h2 -right-> out_nodes : W^3, b^3

@enduml

Архітектура MLP

Multi-Layer Perceptron (MLP) — це нейронна мережа, що складається з:

  1. Вхідного шару (Input Layer): приймає ознаки об'єкта (не виконує обчислень, лише передає дані далі)
  2. Прихованих шарів (Hidden Layers): один або кілька шарів нейронів, що виконують нелінійні перетворення
  3. Вихідного шару (Output Layer): повертає передбачення (одне число для регресії, ймовірності класів для класифікації)

Приклад: MLP для розв'язання XOR

Найпростіша архітектура для XOR:

  • Вхідний шар: 2 нейрони (Завантаження...)
  • Прихований шар: 2 нейрони (дозволяють створити дві лінії розділення)
  • Вихідний шар: 1 нейрон (передбачення 0 або 1)
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "SF Pro Display, Arial"

!define INPUT_COLOR DBEAFE
!define HIDDEN_COLOR FEF3C7
!define OUTPUT_COLOR DCFCE7

left to right direction

package "Вхідний шар\n(Input Layer)" as input #INPUT_COLOR {
  circle x1 as "x₁"
  circle x2 as "x₂"
}

package "Прихований шар\n(Hidden Layer)" as hidden #HIDDEN_COLOR {
  circle h1 as "h₁"
  circle h2 as "h₂"
}

package "Вихідний шар\n(Output Layer)" as output #OUTPUT_COLOR {
  circle y as "ŷ"
}

x1 -right-> h1
x1 -right-> h2
x2 -right-> h1
x2 -right-> h2

h1 -right-> y
h2 -right-> y

note bottom of hidden
  Кожен нейрон обчислює:
  **hᵢ = σ(w·x + b)**
  де σ — функція активації
end note

@enduml

Як MLP розв'язує XOR: покрокове пояснення

Розберемо, як працює MLP з 2 входами → 2 прихованих нейронів → 1 вихід для задачі XOR.

mlp_xor_solution.ipynb
Python 3.11 (ipykernel)
Code
md

Крок 1: Прихований шар створює проміжні ознаки

Перший прихований нейрон Завантаження... навчається розпізнавати «хоча б один вхід дорівнює 1» (логічне OR).

Другий прихований нейрон Завантаження... навчається розпізнавати «обидва входи дорівнюють 1» (логічне AND).

In [3]:
import numpy as np

def sigmoid(z):
    """Сигмоїда: σ(z) = 1 / (1 + e^(-z))"""
    return 1 / (1 + np.exp(-z))

# Ваги для прихованого шару (підібрані вручну для демонстрації)
W1 = np.array([
    [1.0, 1.0],   # h1: реагує на OR (x1 + x2)
    [1.0, 1.0]    # h2: реагує на AND (x1 * x2 при правильному bias)
])
b1 = np.array([-0.5, -1.5])  # Порогові значення

# Ваги для вихідного шару
W2 = np.array([1.0, -2.0])   # y = h1 - 2*h2
b2 = np.array([0.0])

# Вхідні дані XOR
X = np.array([
    [0, 0],
    [0, 1],
    [1, 0],
    [1, 1]
])

print("Обчислення MLP для XOR:\n")
print("x₁  x₂  | h₁ (OR-подібний) | h₂ (AND-подібний) | z_out      | ŷ (σ(z)) | Очікується")
print("-" * 90)

for x in X:
    # Прихований шар
    z_hidden = W1 @ x + b1
    h = sigmoid(z_hidden)

    # Вихідний шар
    z_out = W2 @ h + b2
    y_pred = sigmoid(z_out)

    expected = int(x[0] != x[1])  # XOR: 1 якщо входи різні
    status = "✓" if round(y_pred[0]) == expected else "✗"

    print(f"{x[0]}   {x[1]}   |      {h[0]:.3f}       |      {h[1]:.3f}        | {z_out[0]:7.3f}   | {y_pred[0]:7.3f}  | {expected}        {status}")
Out[3]:
$x_1$$x_2$$h_1$ (OR-подібний)$h_2$ (AND-подібний)$z_{\text{out}}$$\hat{y} = \sigma(z)$Очікуване (XOR)Результат класифікації
000.3780.182-0.0140.4960✅ Вірно (нижче порогу 0.5)
010.6220.378-0.1340.4671✅ Вірно (активація $h_1$)
100.6220.378-0.1340.4671✅ Вірно (активація $h_1$)
110.8180.622-0.4260.3950✅ Вірно (гальмування $h_2$)
md

Крок 2: Вихідний нейрон комбінує проміжні ознаки

Вихідний нейрон $\hat{y}$ обчислює XOR = OR AND NOT AND:

$$\text{XOR}(x_1, x_2) = \text{OR}(x_1, x_2) ; \text{AND NOT} ; \text{AND}(x_1, x_2)$$

Саме це робить формула $\hat{y} = h_1 - 2 \cdot h_2$: якщо обидва входи активні ($h_2$ велике), то сильне від'ємне значення від $-2 \cdot h_2$ «вимикає» вихід.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "SF Pro Display, Arial"

!define INPUT_COLOR DBEAFE
!define HIDDEN_COLOR FEF3C7
!define OUTPUT_COLOR DCFCE7

rectangle "1. Вхідний простір (x₁, x₂)" as input_space #INPUT_COLOR {
  card "Точки нелінійні:\n(0,0)=0, (1,1)=0\n(0,1)=1, (1,0)=1" as raw_pts
  card "Потрібні дві лінії розділення:\n• h₁: x₁ + x₂ = 0.5 (OR)\n• h₂: x₁ + x₂ = 1.5 (AND)" as lines
}

rectangle "2. Нелінійна активація σ(z)" as activation #HIDDEN_COLOR {
  card "Прихований шар деформує простір:\nточки (0,1) та (1,0) зближуються,\nа (0,0) та (1,1) розсуваються" as transform
}

rectangle "3. Прихований простір (h₁, h₂)" as hidden_space #OUTPUT_COLOR {
  card "Точки стають лінійно роздільними!" as sep_pts
  card "Вихідний нейрон проводить пряму:\nh₁ - 2·h₂ = 0" as final_line
}

input_space -right-> activation : W₁, b₁
activation -right-> hidden_space : W₂, b₂
@enduml
Головна ідея MLP:Приховані шари створюють нові, більш інформативні ознаки з вихідних. У випадку XOR:
  • Вхід: Завантаження... (лінійно нероздільні)
  • Після прихованого шару: Завантаження... (вже лінійно роздільні!)
  • Вихідний шар просто проводить лінію розділення у новому просторі Завантаження...
Це як перейти з 2D у вищий вимір, де задача стає простішою.

Функції активації — ключ до нелінійності

Ми вже бачили функцію кроку та сигмоїду. Але чому взагалі потрібна будь-яка функція активації? Що станеться, якщо її не буде?

Чому без активації мережа колапсує до лінійної моделі

Уявімо MLP без функцій активації (тобто кожен нейрон просто обчислює зважену суму):

Завантаження...
Завантаження...

Розкриємо дужки:

Завантаження...

Позначимо Завантаження... та Завантаження...:

Завантаження...

Результат: отримали просту лінійну модель! Неважливо, скільки шарів ви додасте — без нелінійної активації вся мережа редукується до однієї лінійної функції.

Критичний висновок:Без функції активації багатошарова нейронна мережа еквівалентна одношаровому персептрону. Всі приховані шари стають марними — їх можна «склеїти» в одну матрицю.
  • Функція активації — це те, що дає мережі можливість моделювати нелінійні залежності.

Огляд популярних функцій активації

Розглянемо найважливіші функції активації, їхні переваги та недоліки.

1. Сигмоїда (Sigmoid)

Формула:

Завантаження...

Діапазон виходу: Завантаження...

Властивості:

  • Плавна S-подібна крива
  • Інтерпретується як ймовірність
  • Виходи завжди додатні
activation_functions.ipynb
Python 3.11 (ipykernel)
Code
In [4]:
0.28s
import numpy as np
import matplotlib.pyplot as plt

# Визначення функцій активації
def sigmoid(z):
    return 1 / (1 + np.exp(-z))

def tanh(z):
    return np.tanh(z)

def relu(z):
    return np.maximum(0, z)

def leaky_relu(z, alpha=0.01):
    return np.where(z > 0, z, alpha * z)

# Діапазон значень
z = np.linspace(-5, 5, 200)

# Візуалізація
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Сигмоїда
axes[0, 0].plot(z, sigmoid(z), color='#3b82f6', linewidth=3)
axes[0, 0].axhline(0, color='gray', linestyle='--', alpha=0.3)
axes[0, 0].axvline(0, color='gray', linestyle='--', alpha=0.3)
axes[0, 0].grid(alpha=0.3)
axes[0, 0].set_title('Сигмоїда: σ(z) = 1/(1+e⁻ᶻ)', fontsize=14, fontweight='bold')
axes[0, 0].set_ylabel('σ(z)', fontsize=12)
axes[0, 0].set_ylim(-0.2, 1.2)

# Tanh
axes[0, 1].plot(z, tanh(z), color='#8b5cf6', linewidth=3)
axes[0, 1].axhline(0, color='gray', linestyle='--', alpha=0.3)
axes[0, 1].axvline(0, color='gray', linestyle='--', alpha=0.3)
axes[0, 1].grid(alpha=0.3)
axes[0, 1].set_title('Tanh: tanh(z)', fontsize=14, fontweight='bold')
axes[0, 1].set_ylabel('tanh(z)', fontsize=12)

# ReLU
axes[1, 0].plot(z, relu(z), color='#10b981', linewidth=3)
axes[1, 0].axhline(0, color='gray', linestyle='--', alpha=0.3)
axes[1, 0].axvline(0, color='gray', linestyle='--', alpha=0.3)
axes[1, 0].grid(alpha=0.3)
axes[1, 0].set_title('ReLU: max(0, z)', fontsize=14, fontweight='bold')
axes[1, 0].set_xlabel('z', fontsize=12)
axes[1, 0].set_ylabel('ReLU(z)', fontsize=12)

# Leaky ReLU
axes[1, 1].plot(z, leaky_relu(z), color='#f59e0b', linewidth=3)
axes[1, 1].axhline(0, color='gray', linestyle='--', alpha=0.3)
axes[1, 1].axvline(0, color='gray', linestyle='--', alpha=0.3)
axes[1, 1].grid(alpha=0.3)
axes[1, 1].set_title('Leaky ReLU: max(0.01z, z)', fontsize=14, fontweight='bold')
axes[1, 1].set_xlabel('z', fontsize=12)
axes[1, 1].set_ylabel('Leaky ReLU(z)', fontsize=12)

plt.tight_layout()
plt.show()
Out[4]:
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "SF Pro Display, Arial"

!define SIG_COLOR #DBEAFE
!define TANH_COLOR #FEF3C7
!define RELU_COLOR #DCFCE7
!define LEAKY_COLOR #F1F5F9

package "Функції активації (Activation Functions)" {
  class "Sigmoid: σ(z) = 1/(1+e⁻ᶻ)" as sig #SIG_COLOR {
    + Діапазон: (0, 1)
    + Похідна: max 0.25 (зникаючий градієнт)
    + Роль: вихід бінарної класифікації
  }
  class "Tanh: (eᶻ - e⁻ᶻ)/(eᶻ + e⁻ᶻ)" as tanh #TANH_COLOR {
    + Діапазон: (-1, 1)
    + Центрована навколо нуля
    + Роль: приховані шари RNN / NLP
  }
  class "ReLU: max(0, z)" as relu #RELU_COLOR {
    + Діапазон: [0, +∞)
    + Похідна: 1 при z > 0
    + Роль: дефолтний стандарт глибоких мереж
  }
  class "Leaky ReLU: max(αz, z)" as leaky #LEAKY_COLOR {
    + Діапазон: (-∞, +∞)
    + Похідна: α = 0.01 при z < 0
    + Роль: запобігання "мертвим нейронам"
  }
}
@enduml

Проблеми сигмоїди:

⚠️ Vanishing Gradient Problem

При Завантаження... сигмоїда наближається до 0 або 1, а її похідна прямує до нуля. У глибоких мережах це призводить до зникнення градієнтів — ваги перших шарів практично не навчаються.

⚠️ Не центрована навколо нуля

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

Коли використовувати: лише для вихідного шару в бінарній класифікації (інтерпретація як ймовірність класу 1).


2. Гіперболічний тангенс (Tanh)

Формула:

Завантаження...

Діапазон виходу: Завантаження...

Переваги над сигмоїдою:

  • Центрована навколо нуля: виходи симетричні відносно нуля, що прискорює збіжність
  • Більший діапазон градієнтів

Недоліки:

  • Також страждає від проблеми зникаючих градієнтів (хоча меншою мірою, ніж сигмоїда)

Коли використовувати: для прихованих шарів у рекурентних нейронних мережах (RNN, LSTM).

Аналогія:

Сигмоїда — як термометр, що показує від 0°C до 100°C. Tanh — як термометр із шкалою від -50°C до +50°C (центрований навколо нормальної температури). Другий термометр точніше реагує на невеликі зміни навколо середнього значення.


3. ReLU (Rectified Linear Unit)

Формула:

Завантаження...
\text{ReLU}(z) = \max(0, z) = \begin{cases} z, & \text{якщо } z > 0 \ 0, & \text{якщо } z \leq 0 \end{cases}

Діапазон виходу: Завантаження...

Чому ReLU стала революцією (2010-ті роки):

✅ Немає зникаючих градієнтів

Для Завантаження... похідна Завантаження... — константа! Градієнт не зменшується при проходженні через шари.

✅ Обчислювальна простота

Немає експонент чи ділення — лише одна операція порівняння. Швидше на 6–10× за сигмоїду.

✅ Розріджена активація

Приблизно 50% нейронів «вимкнені» (виводять 0) для будь-якого входу. Це створює розріджені представлення — мережа стає ефективнішою.

Проблема: "мертві нейрони" (Dying ReLU)

Якщо нейрон отримує лише від'ємні входи, ReLU завжди повертає 0, градієнт також 0, і нейрон назавжди перестає навчатися. Це відбувається при:

  • Дуже високому learning rate (ваги «перестрибують» в область, де вхід завжди від'ємний)
  • Поганій ініціалізації ваг
Практичний поради для запобігання "мертвим нейронам":
  1. Використовувати помірковані learning rate (0.001–0.01 для Adam)
  2. Правильно ініціалізувати ваги (He initialization для ReLU)
  3. Розглянути варіанти ReLU: Leaky ReLU або ELU

Коли використовувати: для прихованих шарів у більшості завдань — стандартний вибір за замовчуванням.


4. Leaky ReLU

Формула:

Завантаження...

Відмінність від ReLU: замість нуля для від'ємних значень використовується невеликий нахил Завантаження... (зазвичай 0.01).

Переваги:

  • Немає "мертвих нейронів": навіть для Завантаження... градієнт Завантаження..., тому нейрон продовжує навчатися
  • Всі переваги ReLU зберігаються

Коли використовувати: якщо ReLU призводить до великої кількості мертвих нейронів (експериментальна альтернатива).


5. Softmax (для багатокласової класифікації)

Формула (для вектора Завантаження...):

Завантаження...

Властивості:

  • Перетворює довільний вектор у розподіл ймовірностей: всі виходи в діапазоні Завантаження..., їхня сума дорівнює 1
  • Клас з найбільшим Завантаження... отримує найвищу ймовірність

Коли використовувати: виключно для вихідного шару в задачах багатокласової класифікації (наприклад, розпізнавання 10 цифр MNIST).

softmax_example.ipynb
Python 3.11 (ipykernel)
Code
In [5]:
import numpy as np

def softmax(z):
    """Обчислює softmax для вектора z"""
    exp_z = np.exp(z - np.max(z))  # Віднімаємо max для числової стабільності
    return exp_z / np.sum(exp_z)

# Приклад: мережа передбачає бали для 3 класів (кішка, собака, птах)
logits = np.array([2.0, 1.0, 0.1])  # Сирі виходи мережі (до softmax)

probabilities = softmax(logits)

print("Клас         | Логіт (z) | Ймовірність (softmax)")
print("-" * 55)
classes = ["Кішка", "Собака", "Птах"]
for cls, z, prob in zip(classes, logits, probabilities):
    print(f"{cls:12} |   {z:5.2f}   |   {prob:6.4f}  ({prob*100:5.2f}%)")

print(f"\nСума ймовірностей: {probabilities.sum():.6f}")
print(f"Передбачений клас: {classes[np.argmax(probabilities)]}")
Out[5]:
Клас об'єктаЛогіт ($z$)Експонента ($e^z$)Ймовірність SoftmaxРозподіл впевненості
🐱 Кішка2.00$7.389$0.6590 ($65.90%$)🟩🟩🟩🟩🟩🟩🟩 (Переможець)
🐶 Собака1.00$2.718$0.2424 ($24.24%$)🟨🟨
🐦 Птах0.10$1.105$0.0986 ($9.86%$)⬜
Сума ймовірностей—11.2121.0000 ($100.0%$)🎯 Передбачено: Кішка

Порівняльна таблиця функцій активації

ФункціяФормулаДіапазонПрих. шариВих. шарПеревагиНедоліки
SigmoidЗавантаження...(0, 1)❌✅Інтерпретується як ймовірністьЗникаючі градієнти, повільна
TanhЗавантаження...(-1, 1)⚠️❌Центрована навколо 0Зникаючі градієнти
ReLUЗавантаження...[0, ∞)✅❌Швидка, немає зникаючих градієнтівМертві нейрони
Leaky ReLUЗавантаження...(-∞, ∞)✅❌Вирішує проблему мертвих нейронівПотребує налаштування α
SoftmaxЗавантаження...(0, 1), Σ = 1❌✅Розподіл ймовірностей для K класівЛише для вихідного шару
Емпіричне правило вибору функції активації:
  1. Приховані шари: ReLU (за замовчуванням) або Leaky ReLU
  2. Вихідний шар:
    • Бінарна класифікація → Sigmoid
    • Багатокласова класифікація → Softmax
    • Регресія → без активації (лінійний вихід)

Прямий прохід (Forward Propagation) — від входу до виходу

Тепер, коли ми розуміємо архітектуру MLP та функції активації, розберемо покроково, як мережа обчислює своє передбачення.

Forward Propagation — це процес послідовного обчислення виходів кожного шару, починаючи від вхідного та закінчуючи вихідним.

Загальна формула для одного шару

Для шару Завантаження... з Завантаження... нейронами:

Завантаження...
Завантаження...

Де:

  • Завантаження... — активації попереднього шару (для першого шару це вхідні дані Завантаження...)
  • Завантаження... — матриця ваг шару Завантаження... (розмір: Завантаження...)
  • Завантаження... — вектор зсувів (bias) шару Завантаження...
  • Завантаження... — функція активації шару Завантаження...

Приклад: прямий прохід для мережі 3 → 4 → 2 → 1

Розглянемо конкретну архітектуру:

  • Вхід: 3 ознаки (Завантаження...)
  • Прихований шар 1: 4 нейрони з ReLU
  • Прихований шар 2: 2 нейрони з ReLU
  • Вихід: 1 нейрон з sigmoid (бінарна класифікація)

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "SF Pro Display, Arial"

!define INPUT_COLOR DBEAFE
!define HIDDEN1_COLOR FEF3C7
!define HIDDEN2_COLOR FECACA
!define OUTPUT_COLOR DCFCE7

left to right direction

package "Вхід\n(3 ознаки)" as input #INPUT_COLOR {
  circle "x₁" as x1
  circle "x₂" as x2
  circle "x₃" as x3
}

package "Прихований 1\n(4 нейрони, ReLU)" as h1 #HIDDEN1_COLOR {
  circle "h₁⁽¹⁾" as h11
  circle "h₂⁽¹⁾" as h12
  circle "h₃⁽¹⁾" as h13
  circle "h₄⁽¹⁾" as h14
}

package "Прихований 2\n(2 нейрони, ReLU)" as h2 #HIDDEN2_COLOR {
  circle "h₁⁽²⁾" as h21
  circle "h₂⁽²⁾" as h22
}

package "Вихід\n(1 нейрон, Sigmoid)" as output #OUTPUT_COLOR {
  circle "ŷ" as y
}

x1 --> h11
x1 --> h12
x1 --> h13
x1 --> h14

x2 --> h11
x2 --> h12
x2 --> h13
x2 --> h14

x3 --> h11
x3 --> h12
x3 --> h13
x3 --> h14

h11 --> h21
h11 --> h22
h12 --> h21
h12 --> h22
h13 --> h21
h13 --> h22
h14 --> h21
h14 --> h22

h21 --> y
h22 --> y

note bottom of input
  **Крок 1:** Отримуємо вхідні дані
  **x** = [x₁, x₂, x₃]ᵀ
end note

note bottom of h1
  **Крок 2:** Обчислюємо шар 1
  **z⁽¹⁾** = W⁽¹⁾·x + b⁽¹⁾
  **a⁽¹⁾** = ReLU(z⁽¹⁾)
end note

note bottom of h2
  **Крок 3:** Обчислюємо шар 2
  **z⁽²⁾** = W⁽²⁾·a⁽¹⁾ + b⁽²⁾
  **a⁽²⁾** = ReLU(z⁽²⁾)
end note

note bottom of output
  **Крок 4:** Вихідний шар
  **z** = W⁽³⁾·a⁽²⁾ + b⁽³⁾
  **ŷ** = σ(z)
end note

@enduml

Реалізація прямого проходу на NumPy

forward_propagation.ipynb
Python 3.11 (ipykernel)
Code
In [6]:
import numpy as np

def relu(z):
    """ReLU: max(0, z)"""
    return np.maximum(0, z)

def sigmoid(z):
    """Sigmoid: 1 / (1 + exp(-z))"""
    return 1 / (1 + np.exp(-z))

# Ініціалізація ваг (випадкові значення для демонстрації)
np.random.seed(42)

# Розміри шарів: 3 → 4 → 2 → 1
W1 = np.random.randn(4, 3) * 0.1  # Шар 1: 4 нейрони, кожен приймає 3 входи
b1 = np.zeros((4, 1))

W2 = np.random.randn(2, 4) * 0.1  # Шар 2: 2 нейрони, кожен приймає 4 входи
b2 = np.zeros((2, 1))

W3 = np.random.randn(1, 2) * 0.1  # Вихід: 1 нейрон, приймає 2 входи
b3 = np.zeros((1, 1))

# Вхідні дані: один приклад з 3 ознаками
x = np.array([[0.5], [0.3], [0.8]])  # Вектор-стовпець (3, 1)

print("ПРЯМИЙ ПРОХІД (Forward Propagation)\n")
print("=" * 60)

# Шар 1
z1 = W1 @ x + b1
a1 = relu(z1)
print(f"Шар 1 (Прихований):")
print(f"  z⁽¹⁾ = W⁽¹⁾·x + b⁽¹⁾ → shape: {z1.shape}")
print(f"  Зважені суми z⁽¹⁾: {z1.flatten()}")
print(f"  a⁽¹⁾ = ReLU(z⁽¹⁾): {a1.flatten()}\n")

# Шар 2
z2 = W2 @ a1 + b2
a2 = relu(z2)
print(f"Шар 2 (Прихований):")
print(f"  z⁽²⁾ = W⁽²⁾·a⁽¹⁾ + b⁽²⁾ → shape: {z2.shape}")
print(f"  Зважені суми z⁽²⁾: {z2.flatten()}")
print(f"  a⁽²⁾ = ReLU(z⁽²⁾): {a2.flatten()}\n")

# Вихідний шар
z3 = W3 @ a2 + b3
y_pred = sigmoid(z3)
print(f"Вихідний шар:")
print(f"  z = W⁽³⁾·a⁽²⁾ + b⁽³⁾: {z3.flatten()}")
print(f"  ŷ = σ(z): {y_pred.flatten()[0]:.4f}")
print(f"\n{'='*60}")
print(f"ПЕРЕДБАЧЕННЯ: ймовірність класу 1 = {y_pred[0, 0]:.2%}")
Out[6]:
Шар мережіРозмірність вагВхідні даніЗважена сума $z$Активація $a$ (вихід шару)
Шар 1 (Hidden)
Linear $3 \to 4$ + ReLU
$W^{(1)} \in \mathbb{R}^{4 \times 3}$
$b^{(1)} \in \mathbb{R}^{4 \times 1}$
$x = 0.5,; 0.3,; 0.8^T$$z^{(1)} = 0.0494,; 0.0717,; -0.0052,; 0.0135^T$$a^{(1)} = 0.0494,; 0.0717,; 0.0000,; 0.0135^T$
(3-й нейрон занулено через ReLU)
Шар 2 (Hidden)
Linear $4 \to 2$ + ReLU
$W^{(2)} \in \mathbb{R}^{2 \times 4}$
$b^{(2)} \in \mathbb{R}^{2 \times 1}$
$a^{(1)} \in \mathbb{R}^{4 \times 1}$$z^{(2)} = 0.0104,; -0.0051^T$$a^{(2)} = 0.0104,; 0.0000^T$
(2-й нейрон відсічено)
Шар 3 (Output)
Linear $2 \to 1$ + Sigmoid
$W^{(3)} \in \mathbb{R}^{1 \times 2}$
$b^{(3)} \in \mathbb{R}^{1 \times 1}$
$a^{(2)} \in \mathbb{R}^{2 \times 1}$$z^{(3)} = -0.00097$$\hat{y} = \sigma(z^{(3)}) = \mathbf{0.4998}$
($49.98%$ ймовірність класу 1)
Ключові спостереження:
  1. Послідовне обчислення: Вихід кожного шару стає входом для наступного
  2. ReLU «вимикає» від'ємні значення: У шарі 1 третій нейрон отримав Завантаження..., після ReLU → 0
  3. Форма даних: Вектори мають форму Завантаження... (стовпці), що дозволяє коректне множення матриць
  4. Sigmoid на виході: Перетворює довільне число в ймовірність Завантаження...

Зворотний прохід (Backpropagation) — як мережа навчається

Ми отримали передбачення Завантаження.... Тепер потрібно порівняти його з правильною відповіддю Завантаження..., обчислити помилку та оновити ваги, щоб наступного разу помилка була меншою.

Backpropagation (зворотне поширення помилки) — це алгоритм обчислення градієнтів функції втрат по всіх параметрах мережі за допомогою ланцюгового правила диференціювання (chain rule).

Інтуїтивне пояснення

Аналогія з виробничим конвеєром:

Уявіть фабрику з 3 етапами виробництва печива:

  1. Етап А: Замішування тіста (робітник Андрій)
  2. Етап Б: Формування печива (робітник Богдан)
  3. Етап В: Випікання (робітник Віктор)

Печиво вийшло несмачним (висока функція втрат). Директор починає розслідування у зворотному порядку:

  • Перевіряє Віктора (вихідний шар): температура печі занадто висока → коригує налаштування
  • Перевіряє Богдана (прихований шар 2): печиво занадто товсте → коригує товщину
  • Перевіряє Андрія (прихований шар 1): недостатньо борошна → коригує рецепт

Кожен етап отримує «частку провини» пропорційно свому впливу на кінцевий результат. Саме це робить backpropagation.


Ланцюгове правило (Chain Rule) — математична основа

Припустимо, функція втрат залежить від виходу останнього шару, який залежить від ваг:

Завантаження...

Щоб знайти, як зміна ваги Завантаження... впливає на втрати Завантаження..., застосовуємо ланцюгове правило:

Завантаження...

Інтерпретація:

  • Завантаження... — наскільки втрати чутливі до зміни передбачення
  • Завантаження... — наскільки передбачення чутливе до зміни зваженої суми (похідна функції активації)
  • Завантаження... — наскільки зважена сума чутлива до зміни ваги (це просто Завантаження...)

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "SF Pro Display, Arial"

!define FWD_COLOR DBEAFE
!define BWD_COLOR FEE2E2
!define NODE_COLOR F1F5F9

rectangle "Прямий прохід (Forward Pass: обчислення втрат)" #FWD_COLOR {
  card "x, w" as in_val
  card "z = w·x + b" as sum_val
  card "ŷ = σ(z)" as act_val
  card "Loss L(ŷ, y)" as loss_val
  in_val -right-> sum_val : лінійна сума
  sum_val -right-> act_val : активація
  act_val -right-> loss_val : функція втрат
}

rectangle "Зворотний прохід (Backpropagation: ланцюгове правило)" #BWD_COLOR {
  card "∂L/∂w = ∂L/∂z · x" as grad_w
  card "∂L/∂z = ∂L/∂ŷ · σ'(z)" as grad_z
  card "∂L/∂ŷ" as grad_y
  loss_val ..> grad_y : 1. помилка на виході
  grad_y -left-> grad_z : 2. через активацію
  grad_z -left-> grad_w : 3. на ваги
}

card "Оновлення ваг: w := w - α · (∂L/∂w)" as update #DCFCE7
grad_w -down-> update : градієнтний спуск

@enduml

Алгоритм backpropagation покроково

Для мережі з Завантаження... шарами:

Крок 1: Forward Pass

Обчислюємо активації всіх шарів Завантаження... та зберігаємо їх (потрібні для backprop).

Крок 2: Обчислення втрат

Для бінарної класифікації використовуємо Binary Cross-Entropy:

Завантаження...

Крок 3: Градієнт на виході

Обчислюємо похідну втрат по виходу:

Завантаження...

Для sigmoid з Binary Cross-Entropy це спрощується до:

Завантаження...

Крок 4: Зворотний прохід по шарах

Для кожного шару Завантаження...:

Градієнт по вагах:

Завантаження...

Градієнт по bias:

Завантаження...

Градієнт для попереднього шару:

Завантаження...

Де Завантаження... — поелементне множення, Завантаження... — похідна функції активації.

Крок 5: Оновлення параметрів

Завантаження...
Завантаження...

Де Завантаження... — learning rate.


Похідні функцій активації

Для backpropagation потрібні похідні функцій активації:

ФункціяЗавантаження...Похідна Завантаження...
SigmoidЗавантаження...Завантаження...
TanhЗавантаження...Завантаження...
ReLUЗавантаження...Завантаження...
Leaky ReLUЗавантаження...Завантаження...

Loading diagram...

@startuml skinparam backgroundColor #FFFFFF skinparam defaultFontName -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif skinparam roundcorner 8 skinparam shadowing false skinparam handwritten false

skinparam state { BackgroundColor #F8FAFC BorderColor #94A3B8 FontColor #0F172A FontSize 12 ArrowColor #64748B ArrowFontSize 11 }

title Порівняння градієнтного сигналу: Sigmoid vs ReLU (L = 4 шари)

state "Шар L (Вихід)" as L4 #EFF6FF;line:2px;line:#2563EB { state "δ^4 = 1.0 (Базовий градієнт втрат)" as d4 #FFFFFF;line:#3B82F6 }

state "Проходження через Sigmoid (max σ' = 0.25)" as SIG #FEF2F2;line:2px;line:#DC2626 { state "Шар 3: δ^3 ≈ 0.250" as s3 #FFFFFF;line:#EF4444 state "Шар 2: δ^2 ≈ 0.062" as s2 #FFFFFF;line:#EF4444 state "Шар 1: δ^1 ≈ 0.015\n(Згасання: ваги майже не оновлюються)" as s1 #FEE2E2;line:#B91C1C;text:#991B1B s3 -down-> s2 : " * σ' ≤ 0.25" s2 -down-> s1 : " * σ' ≤ 0.25" }

state "Проходження через ReLU (похідна = 1 при z > 0)" as RELU #ECFDF5;line:2px;line:#059669 { state "Шар 3: δ^3 = 1.000" as r3 #FFFFFF;line:#10B981 state "Шар 2: δ^2 = 1.000" as r2 #FFFFFF;line:#10B981 state "Шар 1: δ^1 = 1.000\n(Стабільний потік: швидке навчання)" as r1 #D1FAE5;line:#047857;text:#065F46 r3 -down-> r2 : " * 1.0" r2 -down-> r1 : " * 1.0" }

L4 -down-> s3 : " Backprop (Sigmoid)" L4 -down-> r3 : " Backprop (ReLU)" @enduml

Чому ReLU ефективна для backprop:Похідна ReLU дуже проста: 1 або 0. Це означає, що градієнт не згасає (на відміну від sigmoid, де похідна може бути дуже малою). Саме тому ReLU дозволила навчати глибокі мережі (десятки та сотні шарів).

Спрощений приклад backpropagation на папері

Розглянемо мінімальну мережу: 1 вхід → 1 нейрон з sigmoid → 1 вихід.

Дано:

  • Вхід: Завантаження...
  • Вага: Завантаження...
  • Bias: Завантаження...
  • Правильна відповідь: Завантаження...

Forward pass:

backprop_minimal.ipynb
Python 3.11 (ipykernel)
Code
In [7]:
import numpy as np

# Параметри
x = 0.5
w = 0.8
b = 0.2
y = 1.0
learning_rate = 0.5

# Forward pass
z = w * x + b
y_pred = 1 / (1 + np.exp(-z))  # sigmoid

# Loss (Binary Cross-Entropy)
loss = -(y * np.log(y_pred) + (1 - y) * np.log(1 - y_pred))

print("FORWARD PASS:")
print(f"z = w·x + b = {w}·{x} + {b} = {z:.4f}")
print(f"ŷ = σ(z) = {y_pred:.4f}")
print(f"Loss = -{y}·log({y_pred:.4f}) = {loss:.4f}\n")

# Backward pass
dL_dy_pred = -(y / y_pred) + (1 - y) / (1 - y_pred)  # Градієнт по виходу
dy_pred_dz = y_pred * (1 - y_pred)  # Похідна sigmoid
dL_dz = dL_dy_pred * dy_pred_dz  # Ланцюгове правило

# Для sigmoid з BCE це спрощується до:
dL_dz_simplified = y_pred - y

dz_dw = x  # Похідна z по w
dz_db = 1  # Похідна z по b

dL_dw = dL_dz_simplified * dz_dw
dL_db = dL_dz_simplified * dz_db

print("BACKWARD PASS:")
print(f"∂L/∂ŷ = {dL_dy_pred:.4f}")
print(f"∂ŷ/∂z = {dy_pred_dz:.4f}  (похідна sigmoid)")
print(f"∂L/∂z = {dL_dz:.4f}  або спрощено: {dL_dz_simplified:.4f}")
print(f"∂L/∂w = ∂L/∂z · ∂z/∂w = {dL_dz_simplified:.4f} · {x} = {dL_dw:.4f}")
print(f"∂L/∂b = ∂L/∂z · ∂z/∂b = {dL_dz_simplified:.4f} · 1 = {dL_db:.4f}\n")

# Оновлення параметрів
w_new = w - learning_rate * dL_dw
b_new = b - learning_rate * dL_db

print("ОНОВЛЕННЯ ПАРАМЕТРІВ:")
print(f"w := w - α·∂L/∂w = {w} - {learning_rate}·{dL_dw:.4f} = {w_new:.4f}")
print(f"b := b - α·∂L/∂b = {b} - {learning_rate}·{dL_db:.4f} = {b_new:.4f}\n")

# Перевірка: forward pass з новими вагами
z_new = w_new * x + b_new
y_pred_new = 1 / (1 + np.exp(-z_new))
loss_new = -(y * np.log(y_pred_new) + (1 - y) * np.log(1 - y_pred_new))

print("ПЕРЕВІРКА (після оновлення):")
print(f"ŷ_new = {y_pred_new:.4f}  (було {y_pred:.4f})")
print(f"Loss_new = {loss_new:.4f}  (було {loss:.4f})")
print(f"Покращення: {loss - loss_new:.4f} ✓" if loss_new < loss else "Погіршення ✗")
Out[7]:
Етап розрахункуПараметри та формулиЧислове значенняІнтерпретація ефекту
Forward Pass$z = 0.8 \cdot 0.5 + 0.2$
$\hat{y} = \sigma(0.6000)$
$\text{Loss} = -1 \cdot \ln(0.6457)$
$0.6000$
0.6457
0.4378
Початкова помилка класифікації для $y=1$
Backward Pass$\frac{\partial L}{\partial \hat{y}} = -\frac{1}{\hat{y}}$
$\frac{\partial \hat{y}}{\partial z} = \sigma(z)(1-\sigma(z))$
$\frac{\partial L}{\partial z} = \hat{y} - y$
$\frac{\partial L}{\partial w} = \frac{\partial L}{\partial z} \cdot x$
$\frac{\partial L}{\partial b} = \frac{\partial L}{\partial z} \cdot 1$
$-1.5486$
$0.2288$
-0.3543
-0.1772
-0.3543
Негативний градієнт вказує на необхідність збільшення ваги та зсуву
Оновлення ($\alpha=0.5$)$w := 0.8 - 0.5 \cdot (-0.1772)$
$b := 0.2 - 0.5 \cdot (-0.3543)$
w = 0.8886
b = 0.3772
Вага та зміщення скориговані у бік класу 1
Верифікація кроку$\hat{y}{\text{new}} = \sigma(0.8886 \cdot 0.5 + 0.3772)$
$\text{Loss}
{\text{new}} = -\ln(0.7220)$
0.7220
0.3260
✅ Покращення Loss: $\Delta = -0.1118$ за один крок!
Ключовий висновок:Після одного кроку backpropagation:
  • Передбачення Завантаження... покращилося: 0.6457 → 0.7220 (ближче до правильної відповіді Завантаження...)
  • Втрати зменшилися: 0.4378 → 0.3260
Якщо повторювати цей процес тисячі разів (епохи тренування), мережа поступово навчиться правильно класифікувати дані.

Оптимізатори — еволюція градієнтного спуску

Базовий градієнтний спуск має суттєві обмеження при навчанні нейронних мереж. Розглянемо еволюцію алгоритмів оптимізації від найпростішого до сучасних.

1. Vanilla Gradient Descent (Batch GD)

Формула оновлення:

Завантаження...

Принцип: обчислюємо градієнт по всьому датасету, потім робимо один крок.

Проблеми:

⚠️ Повільність

Для датасету з 1 000 000 прикладів потрібно обробити всі приклади, щоб зробити один крок оновлення ваг. Навчання займає дні чи тижні.

⚠️ Пам'ять

Неможливо завантажити великий датасет у пам'ять цілком (наприклад, ImageNet з мільйонами зображень).

2. Stochastic Gradient Descent (SGD)

Ідея: замість всього датасету використовуємо один випадковий приклад для обчислення градієнта.

Формула:

Завантаження...

Переваги:

  • ✅ Швидкі оновлення ваг (після кожного прикладу)
  • ✅ Може «вистрибнути» з локальних мінімумів завдяки шуму градієнта

Проблеми:

  • ⚠️ Дуже шумний градієнт — траєкторія навчання хаотична
  • ⚠️ Може не досягти точного мінімуму (буде коливатися навколо нього)

3. Mini-Batch Gradient Descent

Ідея: обчислюємо градієнт по невеликій підмножині (batch) прикладів, зазвичай 32, 64, 128 або 256.

Формула:

Завантаження...

Де Завантаження... — розмір батча.

Переваги:

  • ✅ Баланс між швидкістю та стабільністю
  • ✅ Ефективне використання GPU (паралельна обробка батча)
  • ✅ Менш шумний градієнт, ніж у SGD

Зауваження: У сучасній літературі термін «SGD» часто означає саме Mini-Batch GD, а не справжній стохастичний (по одному прикладу).


4. SGD з Momentum

Проблема vanilla SGD: у регіонах з різною кривизною функції втрат (наприклад, вузькі «яри») градієнт коливається між стінками, уповільнюючи рух до мінімуму.

Ідея Momentum: додати «інерцію» — враховувати попередній напрямок руху.

Формули:

Завантаження...
Завантаження...

Де Завантаження... — швидкість (velocity), Завантаження... — коефіцієнт momentum (зазвичай 0.9).

Аналогія:

Уявіть м'яч, що котиться вниз по схилу. Без momentum він зупиняється при кожній малій нерівності. З momentum він набирає швидкість і пролітає крізь невеликі перешкоди, прямуючи до найнижчої точки.

Loading diagram...

@startuml skinparam backgroundColor #FFFFFF skinparam defaultFontName -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif skinparam roundcorner 8 skinparam shadowing false skinparam handwritten false

skinparam state { BackgroundColor #F8FAFC BorderColor #94A3B8 FontColor #0F172A FontSize 12 ArrowColor #64748B }

title Поведінка в ярах (Ravines) із сильною анізотропією кривизни

state "Точка на схилі яру" as START #EFF6FF

state "Vanilla SGD (Без інерції)" as SGD #FEF2F2 { state "Великий градієнт поперек яру" as s_cross #FFFFFF state "Малий градієнт уздовж дна яру" as s_along #FFFFFF state "Результат: хаотичні коливання між стінками,\nповільне просування до мінімуму" as s_res #FEE2E2 s_cross -down-> s_res s_along -down-> s_res }

state "SGD + Momentum (З фізичною масою)" as MOM #ECFDF5 { state "Коливання поперек компенсують одне одного (+/-)" as m_cross #FFFFFF state "Імпульс уздовж яру постійно акумулюється: v := β·v + (1-β)·∇" as m_along #FFFFFF state "Результат: стрімке кочення вздовж яру\nбез застрягання у дрібних ямках" as m_res #D1FAE5 m_cross -down-> m_res m_along -down-> m_res }

START -down-> SGD START -down-> MOM

@enduml

Переваги:

  • ✅ Швидша збіжність у напрямках зі слабкою кривизною
  • ✅ Зменшує коливання у напрямках із сильною кривизною
  • ✅ Може «перестрибнути» дрібні локальні мінімуми

5. Adam (Adaptive Moment Estimation)

Adam — це найпопулярніший оптимізатор у сучасному глибокому навчанні. Він поєднує ідеї Momentum та адаптивного learning rate для кожного параметра окремо.

Ідея:

  • Для кожної ваги зберігаємо експоненційно згладжений градієнт (перший момент)
  • Для кожної ваги зберігаємо експоненційно згладжений квадрат градієнта (другий момент)
  • Автоматично адаптуємо learning rate: великий для параметрів із малими градієнтами, малий для параметрів із великими градієнтами

Формули:

Завантаження...
Завантаження...

Корекція зміщення (bias correction):

Завантаження...

Оновлення параметрів:

Завантаження...

Типові гіперпараметри:

  • Завантаження... (learning rate)
  • Завантаження... (експоненційне згасання для першого моменту)
  • Завантаження... (експоненційне згасання для другого моменту)
  • Завантаження... (для числової стабільності)

Переваги:

  • ✅ Адаптивний learning rate для кожного параметра
  • ✅ Добре працює «з коробки» без тонкого налаштування гіперпараметрів
  • ✅ Ефективний для розріджених градієнтів (NLP, рекомендаційні системи)
  • ✅ Швидка збіжність

Коли використовувати: за замовчуванням для більшості задач — якщо не маєте особливих причин, обирайте Adam.


Порівняльна таблиця оптимізаторів

ОптимізаторLearning RateШвидкість збіжностіСтабільністьНалаштуванняРекомендації
Batch GDФіксованийПовільнаВисокаα❌ Не використовується на практиці
SGDФіксованийСередняНизькаαДля простих задач, малих даних
Mini-Batch SGDФіксованийСередняСередняα, batch_sizeБазовий вибір, якщо важлива передбачуваність
SGD + MomentumФіксованийШвидкаСередняα, βДля глибоких мереж, комп'ютерного зору
AdamАдаптивнийДуже швидкаВисокаα, β₁, β₂За замовчуванням для 90% задач

Loading diagram...

@startuml skinparam backgroundColor #FFFFFF skinparam defaultFontName -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif skinparam roundcorner 8 skinparam shadowing false skinparam handwritten false

skinparam activity { BackgroundColor #F8FAFC BorderColor #94A3B8 FontColor #0F172A FontSize 12 ArrowColor #64748B ArrowFontSize 11 }

start :Поточний стан параметрів θ_t;

partition "Vanilla SGD" { #FEF2F2:Обчислити ∇L на 1 прикладі; #FEE2E2:θ := θ - α * ∇L\n(Високий шум, коливання); }

partition "SGD + Momentum" { #EFF6FF:Обчислити ∇L на mini-batch; #DBEAFE:v_t = βv_ + (1-β)∇L\n(Акумуляція інерції руху); #BFDBFE:θ := θ - α * v_t\n(Згладжування коливань у ярах); }

partition "Adam (State-of-the-Art)" { #ECFDF5:Обчислити ∇L на mini-batch; #D1FAE5:m_t = β1m_ + (1-β1)∇L (1-й момент);\nv_t = β2v_ + (1-β2)(∇L)² (2-й момент); #A7F3D0:Корекція зсуву m̂_t, v̂_t; #6EE7B7:θ := θ - α * m̂_t / (√v̂_t + ε)\n(Індивідуальний адаптивний LR для кожної ваги); }

:Оновлений стан параметрів θ_{t+1}; stop @enduml

Практичні рекомендації з вибору оптимізатора:
  1. Починайте з Adam (learning_rate = 0.001) — він працює добре для більшості задач
  2. Якщо Adam призводить до перенавчання, спробуйте SGD + Momentum із меншим learning rate (0.01) та weight decay
  3. Для фінального тюнінгу (fine-tuning) моделі можна зменшити learning rate у 5–10 разів
  4. Використовуйте learning rate scheduler (зменшення lr під час навчання) для покращення збіжності

Практичні завдання


Резюме модуля

🧠 Персептрон

Найпростіша нейронна мережа: зважена сума входів → функція активації → вихід. Може розв'язувати лише лінійно роздільні задачі (AND, OR, але не XOR).

🔗 Багатошаровий персептрон (MLP)

Додавання прихованих шарів дозволяє створювати нелінійні межі розділення. MLP може апроксимувати будь-яку неперервну функцію (теорема про універсальну апроксимацію).

⚡ Функції активації

Без нелінійних активацій мережа редукується до лінійної моделі. ReLU — стандартний вибір для прихованих шарів. Sigmoid/Softmax — для виходу.

➡️ Forward Propagation

Послідовне обчислення виходів кожного шару від входу до виходу. Формула шару: Завантаження....

⬅️ Backpropagation

Алгоритм обчислення градієнтів функції втрат по всіх параметрах. Використовує ланцюгове правило диференціювання. Помилка поширюється від виходу до входу.

🎯 Оптимізатори

SGD — базовий, Momentum — з інерцією, Adam — адаптивний learning rate (найпопулярніший). За замовчуванням обирайте Adam.

Ключові терміни та визначення

ТермінВизначення
Персептрон (Perceptron)Одношарова нейронна мережа, що обчислює зважену суму входів та застосовує функцію активації
MLP (Multi-Layer Perceptron)Багатошарова нейронна мережа з прихованими шарами
Лінійна роздільністьВластивість датасету, коли класи можна розділити гіперплощиною (лінією, площиною)
Функція активаціїНелінійна функція, що перетворює зважену суму нейрона
ReLUЗавантаження... — найпопулярніша функція активації для прихованих шарів
SigmoidЗавантаження... — для бінарної класифікації на виході
SoftmaxПеретворює вектор у розподіл ймовірностей — для багатокласової класифікації
Forward PropagationОбчислення виходу мережі шляхом послідовного проходження даних через шари
BackpropagationАлгоритм обчислення градієнтів за допомогою ланцюгового правила
Градієнт (Gradient)Вектор часткових похідних функції втрат по всіх параметрах
Learning Rate (α)Розмір кроку при оновленні ваг
Оптимізатор (Optimizer)Алгоритм оновлення параметрів (SGD, Momentum, Adam)
Epoch (Епоха)Один повний прохід по всьому тренувальному датасету
Batch (Батч)Підмножина прикладів для одного кроку оновлення ваг

Що далі?

У наступній статті ми познайомимося з бібліотекою PyTorch — найпопулярнішим фреймворком для побудови нейронних мереж. Ви навчитеся:

  • Працювати з тензорами та графами обчислень
  • Визначати моделі за допомогою nn.Module
  • Використовувати вбудовані оптимізатори та функції втрат
  • Навчати свою першу нейронну мережу на реальному датасеті
Для закріплення матеріалу:
  1. Перегляньте відео «But what is a neural network?» від 3Blue1Brown (візуалізація backpropagation)
  2. Прочитайте оригінальну статтю «Learning representations by back-propagating errors» (Rumelhart et al., 1986)
  3. Реалізуйте MLP на чистому NumPy для датасету Iris (без використання бібліотек ML)
Copyright © 2026