Фаза 03 · урок 01
Перцептрон
Цель урока: Вы знаете векторы и скалярные произведения. Вы знаете, что матрица преобразует входы в выходы. Но как машина учится , какое преобразование использовать?
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Цели обучения
- Проблема
- Концепция
- Один нейрон, одно решение
- Граница решения
- Правило обучения
- Проблема XOR
- Соберите это
- Шаг 1: класс Perceptron
- Шаг 2: обучите на логических вентилях
- Шаг 3: посмотрите, как XOR терпит неудачу
- Шаг 4: решите XOR двумя слоями
- Шаг 5: обучите двухслойную сеть
- Используйте это
- Выпустите это
- Упражнения
- Ключевые термины
- Дополнительное чтение
Перцептрон — атом нейронных сетей. Раскройте его — и найдёте веса, смещение и решение.
Тип: Сборка Язык: Python Предварительные требования: Фаза 1 (Интуиция линейной алгебры) Время: ~60 минут
Цели обучения
- Реализовать перцептрон с нуля на Python, включая правило обновления весов и ступенчатую функцию активации
- Объяснить, почему один перцептрон может решать только линейно разделимые задачи, и продемонстрировать случай неудачи XOR
- Построить многослойный перцептрон, объединив вентили OR, NAND и AND для решения XOR
- Обучить двухслойную сеть с сигмоидальной активацией и обратным распространением ошибки, чтобы она автоматически выучила XOR
Проблема
Вы знаете векторы и скалярные произведения. Вы знаете, что матрица преобразует входы в выходы. Но как машина учится, какое преобразование использовать?
Ответ даёт перцептрон. Это простейшая возможная обучающаяся машина: взять входы, умножить их на веса, добавить смещение и принять бинарное решение. Затем скорректировать параметры. Вот и всё. Любая когда-либо построенная нейронная сеть — это слои этой идеи, поставленные друг на друга.
Понимать перцептрон — значит понимать, что в коде на самом деле означает «обучение»: корректировать числа, пока выход не начнёт соответствовать реальности.
Концепция
Один нейрон, одно решение
Перцептрон принимает n входов, умножает каждый на вес, суммирует результаты, добавляет смещение и пропускает результат через функцию активации.
Ступенчатая функция беспощадна: если взвешенная сумма вместе со смещением >= 0, на выходе 1. В противном случае — 0.
step(z) = 1 if z >= 0
0 if z < 0
Это линейный классификатор. Веса и смещение задают прямую (или гиперплоскость в пространствах большей размерности), которая делит входное пространство на две области.
Граница решения
Для двух входов перцептрон проводит прямую в двумерном пространстве:
x2
┤
│ Class 1 /
│ (0) /
│ /
│ / w1·x1 + w2·x2 + b = 0
│ /
│ / Class 2
│ / (1)
┼───────────/──────────── x1
Всё по одну сторону прямой даёт 0. Всё по другую — 1. При обучении эта прямая перемещается, пока корректно не разделит классы.
Правило обучения
Правило обучения перцептрона просто:
For each training example (x, y_true):
y_pred = predict(x)
error = y_true - y_pred
For each weight:
w_i = w_i + learning_rate * error * x_i
bias = bias + learning_rate * error
Если предсказание верно, error = 0 и ничего не меняется. Если модель предсказывает 0, хотя должна 1, веса увеличиваются. Если предсказывает 1, хотя должна 0, веса уменьшаются. Скорость обучения определяет величину каждой корректировки.
Проблема XOR
Именно здесь всё ломается. Посмотрите на эти логические вентили:
AND gate: OR gate: XOR gate:
x1 x2 out x1 x2 out x1 x2 out
0 0 0 0 0 0 0 0 0
0 1 0 0 1 1 0 1 1
1 0 0 1 0 1 1 0 1
1 1 1 1 1 1 1 1 0
AND и OR линейно разделимы: можно провести одну прямую, чтобы отделить нули от единиц. XOR — нет. Никакая одна прямая не может отделить [0,1] и [1,0] от [0,0] и [1,1].
AND (separable): XOR (not separable):
x2 x2
1 ┤ 0 1 1 ┤ 1 0
│ / │
0 ┤ 0 / 0 0 ┤ 0 1
┼──/──────── x1 ┼──────────── x1
line works! no single line works!
Это фундаментальное ограничение. Один перцептрон способен решать только линейно разделимые задачи. Мински и Пейперт доказали это в 1969 году, и их результат почти на десятилетие остановил исследования нейронных сетей.
Исправление: складывать перцептроны в слои. Многослойный перцептрон может решить XOR, комбинируя два линейных решения в нелинейное.
perceptron-boundary
Соберите это
Шаг 1: класс Perceptron
class Perceptron:
def __init__(self, n_inputs, learning_rate=0.1):
self.weights = [0.0] * n_inputs
self.bias = 0.0
self.lr = learning_rate
def predict(self, inputs):
total = sum(w * x for w, x in zip(self.weights, inputs))
total += self.bias
return 1 if total >= 0 else 0
def train(self, training_data, epochs=100):
for epoch in range(epochs):
errors = 0
for inputs, target in training_data:
prediction = self.predict(inputs)
error = target - prediction
if error != 0:
errors += 1
for i in range(len(self.weights)):
self.weights[i] += self.lr * error * inputs[i]
self.bias += self.lr * error
if errors == 0:
print(f"Converged at epoch {epoch + 1}")
return
print(f"Did not converge after {epochs} epochs")
Шаг 2: обучите на логических вентилях
and_data = [
([0, 0], 0),
([0, 1], 0),
([1, 0], 0),
([1, 1], 1),
]
or_data = [
([0, 0], 0),
([0, 1], 1),
([1, 0], 1),
([1, 1], 1),
]
not_data = [
([0], 1),
([1], 0),
]
print("=== AND Gate ===")
p_and = Perceptron(2)
p_and.train(and_data)
for inputs, _ in and_data:
print(f" {inputs} -> {p_and.predict(inputs)}")
print("\n=== OR Gate ===")
p_or = Perceptron(2)
p_or.train(or_data)
for inputs, _ in or_data:
print(f" {inputs} -> {p_or.predict(inputs)}")
print("\n=== NOT Gate ===")
p_not = Perceptron(1)
p_not.train(not_data)
for inputs, _ in not_data:
print(f" {inputs} -> {p_not.predict(inputs)}")
Шаг 3: посмотрите, как XOR терпит неудачу
xor_data = [
([0, 0], 0),
([0, 1], 1),
([1, 0], 1),
([1, 1], 0),
]
print("\n=== XOR Gate (single perceptron) ===")
p_xor = Perceptron(2)
p_xor.train(xor_data, epochs=1000)
for inputs, expected in xor_data:
result = p_xor.predict(inputs)
status = "OK" if result == expected else "WRONG"
print(f" {inputs} -> {result} (expected {expected}) {status}")
Он никогда не сойдётся. Это строгое доказательство того, что один перцептрон не может выучить XOR.
Шаг 4: решите XOR двумя слоями
Хитрость такова: XOR = (x1 OR x2) AND NOT (x1 AND x2). Объединим три перцептрона:
def xor_network(x1, x2):
or_neuron = Perceptron(2)
or_neuron.weights = [1.0, 1.0]
or_neuron.bias = -0.5
nand_neuron = Perceptron(2)
nand_neuron.weights = [-1.0, -1.0]
nand_neuron.bias = 1.5
and_neuron = Perceptron(2)
and_neuron.weights = [1.0, 1.0]
and_neuron.bias = -1.5
hidden1 = or_neuron.predict([x1, x2])
hidden2 = nand_neuron.predict([x1, x2])
output = and_neuron.predict([hidden1, hidden2])
return output
print("\n=== XOR Gate (multi-layer network) ===")
for inputs, expected in xor_data:
result = xor_network(inputs[0], inputs[1])
print(f" {inputs} -> {result} (expected {expected})")
Все четыре случая верны. Размещение перцептронов в слоях создаёт границы решения, которые один перцептрон создать не способен.
Шаг 5: обучите двухслойную сеть
На шаге 4 веса были заданы вручную. Для XOR это работает, но не для реальных задач, где правильные веса заранее неизвестны. Исправление: заменить ступенчатую функцию сигмоидой и автоматически обучить веса обратным распространением ошибки.
class TwoLayerNetwork:
def __init__(self, learning_rate=0.5):
import random
random.seed(0)
self.w_hidden = [[random.uniform(-1, 1), random.uniform(-1, 1)] for _ in range(2)]
self.b_hidden = [random.uniform(-1, 1), random.uniform(-1, 1)]
self.w_output = [random.uniform(-1, 1), random.uniform(-1, 1)]
self.b_output = random.uniform(-1, 1)
self.lr = learning_rate
def sigmoid(self, x):
import math
x = max(-500, min(500, x))
return 1.0 / (1.0 + math.exp(-x))
def forward(self, inputs):
self.inputs = inputs
self.hidden_outputs = []
for i in range(2):
z = sum(w * x for w, x in zip(self.w_hidden[i], inputs)) + self.b_hidden[i]
self.hidden_outputs.append(self.sigmoid(z))
z_out = sum(w * h for w, h in zip(self.w_output, self.hidden_outputs)) + self.b_output
self.output = self.sigmoid(z_out)
return self.output
def train(self, training_data, epochs=10000):
for epoch in range(epochs):
total_error = 0
for inputs, target in training_data:
output = self.forward(inputs)
error = target - output
total_error += error ** 2
d_output = error * output * (1 - output)
saved_w_output = self.w_output[:]
hidden_deltas = []
for i in range(2):
h = self.hidden_outputs[i]
hd = d_output * saved_w_output[i] * h * (1 - h)
hidden_deltas.append(hd)
for i in range(2):
self.w_output[i] += self.lr * d_output * self.hidden_outputs[i]
self.b_output += self.lr * d_output
for i in range(2):
for j in range(len(inputs)):
self.w_hidden[i][j] += self.lr * hidden_deltas[i] * inputs[j]
self.b_hidden[i] += self.lr * hidden_deltas[i]
net = TwoLayerNetwork(learning_rate=2.0)
net.train(xor_data, epochs=10000)
for inputs, expected in xor_data:
result = net.forward(inputs)
predicted = 1 if result >= 0.5 else 0
print(f" {inputs} -> {result:.4f} (rounded: {predicted}, expected {expected})")
Есть два ключевых отличия от шага 4. Во-первых, сигмоида заменяет ступенчатую функцию — она гладкая, поэтому существуют градиенты. Во-вторых, метод train распространяет ошибку назад, от выхода к скрытому слою, и корректирует каждый вес пропорционально его вкладу в ошибку. Вот обратное распространение ошибки в 20 строках.
Это мост к уроку 03. Математика за d_output и hidden_deltas — правило цепочки, применённое к графу сети. Мы строго выведем её в следующем уроке.
Используйте это
Всё, что вы только что собрали с нуля, доступно через один импорт:
from sklearn.linear_model import Perceptron as SkPerceptron
import numpy as np
X = np.array([[0,0],[0,1],[1,0],[1,1]])
y = np.array([0, 0, 0, 1])
clf = SkPerceptron(max_iter=100, tol=1e-3)
clf.fit(X, y)
print([clf.predict([x])[0] for x in X])
Пять строк. Ваш класс Perceptron из 30 строк делает то же самое. Версия sklearn добавляет проверки сходимости, несколько функций потерь и поддержку разреженных входов, но основной цикл идентичен: взвешенная сумма, ступенчатая функция, обновление веса при ошибке.
Настоящая разница проявляется в масштабе. Что меняется в промышленных сетях:
- Ступенчатую функцию заменяют сигмоидой, ReLU или другими гладкими активациями
- Веса автоматически обучаются обратным распространением ошибки (урок 03)
- Слоёв становится больше: 3, 10, 100+
- Принцип остаётся тем же: каждый слой создаёт новые признаки из выходов предыдущего слоя
Один перцептрон умеет рисовать только прямые. Складывайте их в слои — и сможете рисовать любую форму.
Выпустите это
Этот урок создаёт:
outputs/skill-perceptron.md— навык, объясняющий, когда нужны однослойные и многослойные архитектуры
Упражнения
- Обучите перцептрон на вентиле NAND (универсальном вентиле: любую логическую схему можно построить из NAND). Проверьте, что его веса и смещение задают допустимую границу решения.
- Измените класс Perceptron, чтобы он отслеживал границу решения (
w1*x1 + w2*x2 + b = 0) в каждой эпохе. Выведите, как эта прямая смещается при обучении на вентиле AND. - Постройте перцептрон с тремя входами, который выдаёт 1, только когда хотя бы 2 из 3 входов равны 1 (функция голосования большинства). Линейно ли она разделима? Почему?
Ключевые термины
| Термин | Как обычно говорят | Что это действительно означает |
|---|---|---|
| Перцептрон | «Искусственный нейрон» | Линейный классификатор: скалярное произведение входов и весов плюс смещение, пропущенное через ступенчатую функцию |
| Вес | «Насколько важен вход» | Множитель, масштабируемый вклад каждого входа в решение |
| Смещение | «Порог» | Константа, сдвигающая границу решения и позволяющая перцептрону срабатывать даже при нулевых входах |
| Функция активации | «То, что сжимает значения» | Функция, применяемая после взвешенной суммы: ступенчатая для перцептронов, sigmoid/ReLU для современных сетей |
| Линейная разделимость | «Можно провести между ними прямую» | Набор данных, где одна гиперплоскость может идеально разделить классы |
| Проблема XOR | «То, чего перцептроны не умеют» | Доказательство, что однослойные сети не способны учить нелинейно разделимые функции |
| Граница решения | «Где классификатор переключается» | Гиперплоскость w*x + b = 0, делящая входное пространство на два класса |
| Многослойный перцептрон | «Настоящая нейронная сеть» | Перцептроны, сложенные в слои, где выход каждого слоя поступает на вход следующего |
Дополнительное чтение
- Frank Rosenblatt, «The Perceptron: A Probabilistic Model for Information Storage and Organization in the Brain» (1958) — исходная статья, с которой всё началось
- Minsky & Papert, «Perceptrons» (1969) — книга, доказавшая неразрешимость XOR для однослойных сетей и на десятилетие остановившая исследования перцептронов
- Michael Nielsen, «Neural Networks and Deep Learning», глава 1 — бесплатное онлайн-изложение с лучшей визуальной интуицией того, как перцептроны объединяются в сети
Источник: The Perceptron Фаза 3 — Основы глубокого обучения · Полный каталог · 03.02 — Многослойные сети и прямой проход