Обучаем XOR с нуля: backprop вживую
Кульминация курса: собираем всё вместе и обучаем сеть решать XOR с нуля, наблюдая, как падает ошибка.
Эта программа — целая нейросеть на чистом Python: инициализация весов, forward pass, обратное распространение и градиентный спуск в одном цикле обучения.
Постановка
В первом разделе мы доказали, что один нейрон не решает XOR. Теперь у нас есть всё, чтобы решить его сетью 2 → 2 → 1: forward pass, функция потерь, backprop и градиентный спуск. Веса инициализируем случайно (с фиксированным seed для воспроизводимости), и сеть сама найдёт решение за тысячи маленьких шагов.
Полный цикл обучения
import math, random
def sigmoid(z): return 1.0 / (1.0 + math.exp(-z))
def dsigmoid(a): return a * (1.0 - a) # производная через выход a
random.seed(1)
rw = lambda: random.uniform(-1, 1)
# веса: скрытый слой (2 нейрона по 2 входа) и выходной (1 нейрон, 2 входа)
W1 = [[rw(), rw()] for _ in range(2)]
b1 = [rw() for _ in range(2)]
W2 = [rw(), rw()]
b2 = rw()
data = [((0,0),0), ((0,1),1), ((1,0),1), ((1,1),0)]
lr = 0.5
def forward(x):
h = [sigmoid(W1[j][0]*x[0] + W1[j][1]*x[1] + b1[j]) for j in range(2)]
out = sigmoid(W2[0]*h[0] + W2[1]*h[1] + b2)
return h, out
for epoch in range(1, 10001):
total_loss = 0.0
for x, y in data:
h, out = forward(x)
total_loss += (out - y) ** 2
# --- backprop ---
d_out = (out - y) * dsigmoid(out)
gW2 = [d_out * h[0], d_out * h[1]]
d_h = [d_out * W2[0] * dsigmoid(h[0]),
d_out * W2[1] * dsigmoid(h[1])]
gW1 = [[d_h[0]*x[0], d_h[0]*x[1]],
[d_h[1]*x[0], d_h[1]*x[1]]]
# --- шаг градиентного спуска ---
for j in range(2):
W1[j][0] -= lr * gW1[j][0]
W1[j][1] -= lr * gW1[j][1]
b1[j] -= lr * d_h[j]
W2[0] -= lr * gW2[0]
W2[1] -= lr * gW2[1]
b2 -= lr * d_out
if epoch == 1 or epoch % 2000 == 0:
print("Эпоха", epoch, "потеря:", round(total_loss, 4))
print("\nПроверка обученной сети:")
for x, y in data:
_, out = forward(x)
print(x, "-> предсказание", round(out, 3), "(цель", y, ")")
Вывод:
Эпоха 1 потеря: 1.0778 Эпоха 2000 потеря: 0.0086 Эпоха 4000 потеря: 0.0033 Эпоха 6000 потеря: 0.002 Эпоха 8000 потеря: 0.0014 Эпоха 10000 потеря: 0.0011 Проверка обученной сети: (0, 0) -> предсказание 0.018 (цель 0 ) (0, 1) -> предсказание 0.984 (цель 1 ) (1, 0) -> предсказание 0.984 (цель 1 ) (1, 1) -> предсказание 0.017 (цель 0 )
Что мы увидели
Это — настоящее обучение нейросети. Стартовав со случайных весов (потеря 1.08), сеть за 10 000 эпох снизила ошибку почти до нуля и теперь уверенно решает XOR: для (0,1) и (1,0) выдаёт ~0.98, для (0,0) и (1,1) — ~0.02. Задачу, непосильную одному нейрону, два скрытых нейрона освоили полностью — и никто не подсказывал им веса.
Разберём цикл по косточкам
- Forward считает
hиout, запоминая активации скрытого слоя. - d_out — дельта выхода: ошибка
(out - y)на производную sigmoid. - d_h — дельты скрытого слоя: дельта выхода, протащенная назад через веса
W2. - gW1, gW2 — градиенты: дельта приёмника на активацию источника.
- Шаг спуска вычитает
lr * градиентиз каждого веса.
Что делает скрытый слой внутри
Если после обучения распечатать активации скрытого слоя, обнаружится красивая вещь: два нейрона научились вычислять промежуточные признаки, близкие к логическим OR и AND. Выходной нейрон затем комбинирует их в XOR (грубо говоря, «OR, но не AND»). Никто не задавал эти признаки руками — сеть сама нашла такое разбиение задачи, минимизируя потерю. Это в миниатюре и есть то, ради чего нужны скрытые слои: они автоматически изобретают полезные промежуточные представления, из которых складывается ответ.
Заметьте также форму кривой потери: резкое падение вначале, затем долгое медленное дошлифовывание. Это типичный профиль обучения нейросетей — большинство прогресса приходится на первые эпохи, а дальше идёт тонкая настройка. Поэтому на практике следят за кривой и останавливаются, когда улучшения становятся пренебрежимо малы.
Поэкспериментируйте
Запустите код, затем попробуйте: уменьшить lr до 0.1 (сходимость замедлится) или число эпох до 500 (сеть не успеет обучиться, потеря останется большой). Так вы почувствуете роль гиперпараметров на живом примере.
Итог
- Цикл обучения = forward + backprop + шаг спуска, повторённые много раз.
- Сеть 2-2-1 с нуля выучивает XOR, снижая потерю с ~1.08 до ~0.001.
- Падение потери по эпохам — наглядный «пульс» обучения.