Фаза 03 · урок 01

Перцептрон

Цель урока: Вы знаете векторы и скалярные произведения. Вы знаете, что матрица преобразует входы в выходы. Но как машина учится , какое преобразование использовать?

Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.

Курс
AI Engineering from Scratch
Фаза
Основы глубокого обучения
Чтение
13 мин.
Проверено
Содержание урока
  1. Цели обучения
  2. Проблема
  3. Концепция
  4. Один нейрон, одно решение
  5. Граница решения
  6. Правило обучения
  7. Проблема XOR
  8. Соберите это
  9. Шаг 1: класс Perceptron
  10. Шаг 2: обучите на логических вентилях
  11. Шаг 3: посмотрите, как XOR терпит неудачу
  12. Шаг 4: решите XOR двумя слоями
  13. Шаг 5: обучите двухслойную сеть
  14. Используйте это
  15. Выпустите это
  16. Упражнения
  17. Ключевые термины
  18. Дополнительное чтение

Перцептрон — атом нейронных сетей. Раскройте его — и найдёте веса, смещение и решение.

Тип: Сборка Язык: 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 — навык, объясняющий, когда нужны однослойные и многослойные архитектуры

Упражнения

  1. Обучите перцептрон на вентиле NAND (универсальном вентиле: любую логическую схему можно построить из NAND). Проверьте, что его веса и смещение задают допустимую границу решения.
  2. Измените класс Perceptron, чтобы он отслеживал границу решения (w1*x1 + w2*x2 + b = 0) в каждой эпохе. Выведите, как эта прямая смещается при обучении на вентиле AND.
  3. Постройте перцептрон с тремя входами, который выдаёт 1, только когда хотя бы 2 из 3 входов равны 1 (функция голосования большинства). Линейно ли она разделима? Почему?

Ключевые термины

Термин Как обычно говорят Что это действительно означает
Перцептрон «Искусственный нейрон» Линейный классификатор: скалярное произведение входов и весов плюс смещение, пропущенное через ступенчатую функцию
Вес «Насколько важен вход» Множитель, масштабируемый вклад каждого входа в решение
Смещение «Порог» Константа, сдвигающая границу решения и позволяющая перцептрону срабатывать даже при нулевых входах
Функция активации «То, что сжимает значения» Функция, применяемая после взвешенной суммы: ступенчатая для перцептронов, sigmoid/ReLU для современных сетей
Линейная разделимость «Можно провести между ними прямую» Набор данных, где одна гиперплоскость может идеально разделить классы
Проблема XOR «То, чего перцептроны не умеют» Доказательство, что однослойные сети не способны учить нелинейно разделимые функции
Граница решения «Где классификатор переключается» Гиперплоскость w*x + b = 0, делящая входное пространство на два класса
Многослойный перцептрон «Настоящая нейронная сеть» Перцептроны, сложенные в слои, где выход каждого слоя поступает на вход следующего

Дополнительное чтение


Источник: The Perceptron Фаза 3 — Основы глубокого обучения · Полный каталог · 03.02 — Многослойные сети и прямой проход