Первое обучение — перцептрон на NumPy
Первое обучение — перцептрон на NumPy
Эта статья — короткий практический мост между теорией из Нейрон и полноценным машинным обучением. Если массивы NumPy ещё не знакомы — сначала NumPy — массивы и матрицы (np.array, @, mean). Здесь один выходной нейрон с сигмоидой учится отличать два класса по четырём бинарным признакам (как упрощённые "черты" цифры 0 и 1).
Интерактивная схема прохождения сигнала — в статье Нейрон (демо "Neural Network Demo" на play.spirzen.ru). Ниже — два уровня: классический персептрон со ступенчатой активацией, затем обучаемый нейрон с сигмоидой (~40 строк).
Классический персептрон (ступенчатая активация)
Правило обновления: w ← w + lr · (y_true − y_pred) · x, b ← b + lr · (y_true − y_pred). При верном ответе веса не меняются; при ошибке сдвигаются в сторону признака. Работает только для линейно разделимых данных (одна гиперплоскость); XOR — контрпример (см. Нейрон — XOR).
import numpy as np
import matplotlib.pyplot as plt
class Perceptron:
def __init__(self, learning_rate=0.1, n_iters=100):
self.lr = learning_rate
self.n_iters = n_iters
self.weights = None
self.bias = None
def fit(self, X, y):
n_samples, n_features = X.shape
self.weights = np.zeros(n_features)
self.bias = 0.0
for _ in range(self.n_iters):
for idx, x_i in enumerate(X):
z = np.dot(x_i, self.weights) + self.bias
y_pred = 1 if z >= 0 else -1
update = self.lr * (y[idx] - y_pred)
self.weights += update * x_i
self.bias += update
def predict(self, X):
z = np.dot(X, self.weights) + self.bias
return np.where(z >= 0, 1, -1)
X = np.array([[0, 0], [1, 1], [0, 1], [2, 3], [3, 2], [2, 2]])
y = np.array([-1, -1, -1, 1, 1, 1])
p = Perceptron(learning_rate=0.1, n_iters=50)
p.fit(X, y)
plt.scatter(X[y == -1, 0], X[y == -1, 1], c="red", label="Класс −1")
plt.scatter(X[y == 1, 0], X[y == 1, 1], c="blue", label="Класс 1")
xx = np.linspace(-1, 4, 100)
yy = -(p.weights[0] * xx + p.bias) / p.weights[1] # граница Z = 0
plt.plot(xx, yy, "k--", label="Разделяющая прямая")
plt.legend(); plt.grid(alpha=0.3); plt.show()
Уравнение прямой выводится из w₁·x + w₂·y + b = 0. Пример «купить / отказ» без обучения — в Нейрон.
Самопроверка: восемь точек в форме «X» (пересекающиеся кластеры) — персептрон не разделит все классы одной прямой. Сохраните график и объясните, почему.
Задача
Есть восемь обучающих примеров. У каждого четыре входа (0 или 1) и целевой ответ:
- 0 — "похоже на ноль" (четыре шаблона);
- 1 — "похоже на единицу" (четыре шаблона).
Сеть должна выдать число близко к 0 или близко к 1, а не жёсткий порог с первого раза.
Сигмоида
Сигмоида сжимает любую сумму входов в диапазон от 0 до 1 — удобно для вероятности класса "1":
import numpy as np
def nonlin(x, deriv=False):
"""Сигмоида; deriv=True — производная для обратного прохода."""
if deriv:
return x * (1 - x)
return 1 / (1 + np.exp(-x))
Данные и матрица весов syn0
Входы складываем в матрицу X (8 строк × 4 столбца). Ответы — столбец y. Матрица syn0 размером 4×1 хранит веса связей "признак → нейрон"; в начале — случайные маленькие значения:
Код ITЗагрузка примера кода…
Цикл обучения
На каждой итерации:
- Прямой проход — считаем выход
l1 = σ(X · syn0). - Ошибка —
y - l1. - Поправка весов — пропорциональна ошибке и производной сигмоиды (упрощённый обратный проход для одного слоя).
for _ in range(10_000):
l0 = X
l1 = nonlin(np.dot(l0, syn0))
l1_error = y - l1
l1_delta = l1_error * nonlin(l1, deriv=True)
syn0 += np.dot(l0.T, l1_delta)
После обучения ответы на тех же примерах выглядят примерно так (не ровно 0 и 1 — это нормально для сигмоиды):
[0.02, 0.01, 0.99, 0.01, 0.99, 0.99, 0.99, 0.98]
Больше итераций — выше точность
При 100 000 итерациях значения ещё ближе к 0 и 1 (например, 0.005 вместо 0.02 для класса "ноль"). Закономерность: чем дольше учим на одних и тех же данных, тем меньше ошибка — пока не начнётся переобучение на сложных задачах.
Проверка на новых данных
Подставляем векторы, которых не было в обучении:
def predict(features, weights):
return nonlin(np.dot(np.array(features, dtype=float), weights)).item()
# Похож на обучающий "ноль"
print(predict([0, 1, 0, 1], syn0)) # ~0.01
# Похож на обучающую "единицу"
print(predict([1, 1, 1, 1], syn0)) # ~0.98
# Чужой шаблон — сеть "не уверена"
print(predict([0, 0, 0, 0], syn0)) # ~0.50
Знакомый шаблон даёт уверенный ответ около 0 или 1. Абстрактный [0, 0, 0, 0] попадает между классами — выход около 0.5: модель не видела таких примеров и не должна выдавать жёсткую классификацию. В продакшене такие случаи отправляют на проверку человеку или помечают как "низкая уверенность".
Ландшафт ошибки (MSE)
Перед Keras полезно увидеть, куда «скатывается» оптимизатор. Для линейной модели y_pred = w·x + b функция потерь MSE — поверхность над парой (w, b):
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D # noqa: F401
x = np.array([1, 3, 5, 2, 10, 9], dtype=float)
y = np.array([3, 9, 15, 6, 30, 27], dtype=float)
def mse(w, b):
return np.mean((y - (w * x + b)) ** 2)
w_vals = np.linspace(-2, 8, 50)
b_vals = np.linspace(-5, 10, 50)
W, B = np.meshgrid(w_vals, b_vals)
Z = np.array([mse(w, b) for w, b in zip(W.ravel(), B.ravel())]).reshape(W.shape)
fig = plt.figure(figsize=(10, 4))
ax = fig.add_subplot(121, projection="3d")
ax.plot_surface(W, B, Z, cmap="viridis", alpha=0.7)
ax.set_xlabel("w"); ax.set_ylabel("b"); ax.set_zlabel("MSE")
ax2 = fig.add_subplot(122)
cf = ax2.contourf(W, B, Z, levels=20, cmap="jet")
plt.colorbar(cf, ax=ax2, label="MSE")
i_min = np.unravel_index(np.argmin(Z), Z.shape)
ax2.scatter(w_vals[i_min[1]], b_vals[i_min[0]], c="lime", s=80, label="минимум")
ax2.set_xlabel("w"); ax2.set_ylabel("b"); ax2.legend()
plt.tight_layout(); plt.show()
Минимум «чаши» — целевые веса. Градиентный спуск итеративно сдвигает (w, b) вниз по склону.
Градиентный спуск руками
Keras считает производные автоматически; здесь — тот же шаг явно. Для MSE: ∂MSE/∂w = −2/n · Σ x·(y − y_pred), ∂MSE/∂b = −2/n · Σ (y − y_pred).
import numpy as np
import matplotlib.pyplot as plt
x = np.array([1, 3, 5, 2, 10, 9], dtype=float)
y = np.array([3, 9, 15, 6, 30, 27], dtype=float)
w, b = np.random.randn(), np.random.randn()
lr = 0.01
loss_history = []
for epoch in range(100):
y_pred = w * x + b
error = y - y_pred
mse = np.mean(error ** 2)
loss_history.append(mse)
grad_w = -2 / len(x) * np.sum(error * x)
grad_b = -2 / len(x) * np.sum(error)
w -= lr * grad_w
b -= lr * grad_b
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.plot(loss_history, marker="o", markersize=3)
plt.xlabel("Эпоха"); plt.ylabel("MSE"); plt.title("Кривая обучения"); plt.grid(True)
plt.subplot(1, 2, 2)
plt.scatter(x, y, label="Данные")
plt.plot(x, w * x + b, "r", label=f"y = {w:.2f}x + {b:.2f}")
plt.legend(); plt.grid(True); plt.tight_layout(); plt.show()
Самопроверка: на одном графике постройте loss_history для lr = 0.001, 0.05 и 0.5. Какой шаг даёт быструю сходимость, какой — расходимость или «застой»?
Полный скрипт
Скопируйте в файл perceptron_demo.py и запустите (pip install numpy):
Код ITЗагрузка примера кода…
Что дальше
| Шаг | Куда идти |
|---|---|
| Ландшафт MSE, градиентный спуск, Keras | Keras и TensorFlow |
| Несколько слоёв, ReLU, свёртки | Нейрон, Основные концепции ИИ |
| Датасеты, метрики, sklearn | Машинное обучение |
| Откуда взялся термин ИИ и маркетинг | Что такое ИИ на самом деле |