Фаза 03 · урок 02

Многослойные сети и прямой проход

Цель урока: Один нейрон рисует линию. И всё. Одну прямую линию в ваших данных. Каждая реальная задача ИИ — распознавание изображений, понимание языка, игра в го — требует кривых. Получать кривые позволяет объединение нейронов в слои.

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

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

Один нейрон рисует линию. Сложите их друг на друга — и сможете нарисовать что угодно.

Тип: Сборка Языки: Python Предварительные требования: Фаза 01 (математические основы), урок 03.01 (перцептрон) Время: ~90 минут

Цели обучения

  • Построить с нуля многослойную сеть с классами Layer и Network, выполняющими полный прямой проход
  • Прослеживать размерности матриц на каждом слое сети и выявлять несовпадения форм
  • Объяснять, как наложение нелинейных активаций позволяет сети изучать криволинейные границы решений
  • Решить задачу XOR с архитектурой 2-2-1 и вручную настроенными весами сигмоиды

Проблема

Один нейрон рисует линию. И всё. Одну прямую линию в ваших данных. Каждая реальная задача ИИ — распознавание изображений, понимание языка, игра в го — требует кривых. Получать кривые позволяет объединение нейронов в слои.

В 1969 году Минский и Пейперт доказали, что это ограничение фатально: однослойная сеть не может выучить XOR. Не «с трудом обучается» — математически не может. В таблице истинности XOR точки [0,1] и [1,0] находятся по одну сторону, а [0,0] и [1,1] — по другую. Ни одна прямая не разделяет их.

Это убило финансирование нейронных сетей более чем на десятилетие. Ретроспективно исправление очевидно: перестать использовать один слой. Складывать нейроны в слои. Пусть первый слой вырезает из входного пространства новые признаки, а второй объединяет эти признаки в решения, которые не могла бы принять ни одна прямая.

Такой стек и есть многослойная сеть. Он лежит в основе каждой используемой сегодня в продакшене модели глубокого обучения. Прямой проход (forward pass) — прохождение данных от входа через скрытые слои к выходу — первое, что нужно построить, прежде чем заработает всё остальное.

Концепция

Слои: входной, скрытый, выходной

У многослойной сети есть три вида слоёв:

Входной слой — в действительности не совсем слой. Он хранит исходные данные. Два признака означают два входных узла. Вычислений здесь не происходит.

Скрытые слои — место, где выполняется работа. Каждый нейрон берёт все выходы предыдущего слоя, применяет веса и смещение, затем передаёт результат через функцию активации. Они «скрытые», потому что вы никогда не видите эти значения непосредственно в обучающих данных.

Выходной слой — окончательный ответ. Для бинарной классификации это один нейрон с сигмоидой. Для многоклассовой — по одному нейрону на класс.

Диаграмма к уроку «Многослойные сети и прямой проход»

Это сеть 2-3-1: два входа, три скрытых нейрона, один выход. Каждое соединение несёт вес. Каждый нейрон (кроме входного) содержит смещение.

Каждый слой создаёт вектор чисел, называемый скрытым состоянием. Для текста скрытые состояния увеличивают размерность: кодируют слово 768 числами, чтобы передать семантическое значение. Для изображений они уменьшают размерность: сжимают миллионы пикселей в удобное представление. Скрытое состояние — место, где живёт обучение.

Нейроны и активации

Каждый нейрон делает три вещи:

  1. Умножает каждый вход на соответствующий ему вес
  2. Складывает все произведения и добавляет смещение
  3. Передаёт сумму через функцию активации

Пока что функцией активации будет сигмоида:

sigmoid(z) = 1 / (1 + e^(-z))

Сигмоида сжимает любое число в диапазон (0, 1). Большие положительные входы приближают её к 1. Большие отрицательные — к 0. Ноль отображается в 0.5. Именно эта плавная кривая делает обучение возможным: в отличие от жёсткого шага перцептрона, сигмоида имеет градиент везде.

Прямой проход: как текут данные

Прямой проход проталкивает входные данные через сеть, слой за слоем, пока они не достигнут выхода. Во время прямого прохода обучение не происходит. Это чистое вычисление: умножить, сложить, активировать, повторить.

Диаграмма к уроку «Многослойные сети и прямой проход»

На каждом слое последовательно происходят три операции:

z = W * input + b       (linear transformation)
a = sigmoid(z)           (activation)

Выход одного слоя становится входом следующего. В этом и состоит весь прямой проход.

Размерности матриц

Отслеживание размерностей — важнейший навык отладки в глубоком обучении. Вот сеть 2-3-1:

Шаг Операция Размерности Форма результата
Вход x (2,)
Линейный скрытый слой W1 * x + b1 W1: (3, 2), b1: (3,) (3,)
Активация скрытого слоя sigmoid(z1) (3,)
Линейный выходной слой W2 * h + b2 W2: (1, 3), b2: (1,) (1,)
Активация выхода sigmoid(z2) (1,)

Правило: матрица весов W на слое k имеет форму (neurons_in_layer_k, neurons_in_layer_k_minus_1). Строки соответствуют текущему слою. Столбцы соответствуют предыдущему слою. Если формы не согласуются, у вас ошибка.

Теорема об универсальной аппроксимации

В 1989 году Джордж Кибенко доказал нечто примечательное: нейронная сеть с одним скрытым слоем и достаточным числом нейронов может аппроксимировать любую непрерывную функцию с любой требуемой точностью.

Это не означает, что один скрытый слой всегда лучше. Это означает, что такая архитектура теоретически способна на это. На практике более глубокие сети (больше слоёв, меньше нейронов на слой) изучают те же функции с гораздо меньшим общим числом параметров, чем неглубокие широкие сети. Поэтому глубокое обучение и работает.

Интуиция такова: каждый нейрон скрытого слоя выучивает один «бугорок» или признак. Достаточное число бугорков в правильных местах может аппроксимировать любую гладкую кривую. Больше нейронов — больше бугорков — лучше аппроксимация.

Диаграмма к уроку «Многослойные сети и прямой проход»

Компонуемость

Нейронные сети компонуемы. Их можно складывать в стек, соединять в цепочки, запускать параллельно. Модель Whisper использует сеть-кодировщик для обработки аудио и отдельную сеть-декодер для генерации текста. Современные LLM бывают только декодерными. BERT — только кодировщик. T5 — кодировщик-декодер. Выбор архитектуры определяет, что может делать модель.

mlp-forward

Соберите

Чистый Python. Без numpy. Каждая матричная операция написана с нуля.

Шаг 1: сигмоидальная активация

import math

def sigmoid(x):
    x = max(-500.0, min(500.0, x))
    return 1.0 / (1.0 + math.exp(-x))

Ограничение диапазоном [-500, 500] предотвращает переполнение. math.exp(500) велико, но конечно. math.exp(1000) — бесконечность.

Шаг 2: класс Layer

Важнейшая операция во всём глубоком обучении — умножение матриц. Каждый слой, каждая голова внимания, каждый прямой проход — в основе всего лежат матричные умножения. Линейный слой берёт входной вектор, умножает его на матрицу весов и добавляет вектор смещения: y = Wx + b. На это единственное уравнение приходится 90% вычислений нейронной сети.

Слой хранит матрицу весов и вектор смещения. Его метод forward принимает входной вектор и возвращает активированный выход.

class Layer:
    def __init__(self, n_inputs, n_neurons, weights=None, biases=None):
        if weights is not None:
            self.weights = weights
        else:
            import random
            self.weights = [
                [random.uniform(-1, 1) for _ in range(n_inputs)]
                for _ in range(n_neurons)
            ]
        if biases is not None:
            self.biases = biases
        else:
            self.biases = [0.0] * n_neurons

    def forward(self, inputs):
        self.last_input = inputs
        self.last_output = []
        for neuron_idx in range(len(self.weights)):
            z = sum(
                w * x for w, x in zip(self.weights[neuron_idx], inputs)
            )
            z += self.biases[neuron_idx]
            self.last_output.append(sigmoid(z))
        return self.last_output

Матрица весов имеет форму (n_neurons, n_inputs). Каждая строка содержит веса одного нейрона по всем входам. Метод forward перебирает нейроны, вычисляет взвешенную сумму со смещением, применяет сигмоиду и собирает результаты.

Шаг 3: класс Network

Сеть — это список слоёв. Прямой проход соединяет их в цепочку: выход слоя k поступает на вход слоя k+1.

class Network:
    def __init__(self, layers):
        self.layers = layers

    def forward(self, inputs):
        current = inputs
        for layer in self.layers:
            current = layer.forward(current)
        return current

Это и есть весь прямой проход. Четыре строки логики. Данные входят, проходят через каждый слой и выходят с другой стороны.

Шаг 4: XOR с вручную настроенными весами

В уроке 01 мы решили XOR, объединив перцептроны OR, NAND и AND. Теперь сделайте то же самое с классами Layer и Network. Архитектура 2-2-1: два входа, два скрытых нейрона, один выход.

hidden = Layer(
    n_inputs=2,
    n_neurons=2,
    weights=[[20.0, 20.0], [-20.0, -20.0]],
    biases=[-10.0, 30.0],
)

output = Layer(
    n_inputs=2,
    n_neurons=1,
    weights=20.0, 20.0,
    biases=[-30.0],
)

xor_net = Network([hidden, output])

xor_data = [
    ([0, 0], 0),
    ([0, 1], 1),
    ([1, 0], 1),
    ([1, 1], 0),
]

for inputs, expected in xor_data:
    result = xor_net.forward(inputs)
    predicted = 1 if result[0] >= 0.5 else 0
    print(f"  {inputs} -> {result[0]:.6f} (rounded: {predicted}, expected: {expected})")

Большие веса (20, -20) заставляют сигмоиду вести себя как ступенчатая функция. Первый скрытый нейрон приближает OR. Второй приближает NAND. Выходной нейрон объединяет их в AND, что даёт XOR.

Шаг 5: классификация круга

Более сложная задача: классифицировать двумерные точки как находящиеся внутри или вне круга радиуса 0.5 с центром в начале координат. Для этого нужна криволинейная граница решений — невозможная для одного перцептрона.

import random
import math

random.seed(42)

data = []
for _ in range(200):
    x = random.uniform(-1, 1)
    y = random.uniform(-1, 1)
    label = 1 if (x * x + y * y) < 0.25 else 0
    data.append(([x, y], label))

circle_net = Network([
    Layer(n_inputs=2, n_neurons=8),
    Layer(n_inputs=8, n_neurons=1),
])

Со случайными весами сеть будет классифицировать плохо. Но прямой проход всё равно выполняется. В этом суть: прямой проход — лишь вычисление. Обучение правильных весов — это обратное распространение ошибки, которое появится в уроке 03.

correct = 0
for inputs, expected in data:
    result = circle_net.forward(inputs)
    predicted = 1 if result[0] >= 0.5 else 0
    if predicted == expected:
        correct += 1

print(f"Accuracy with random weights: {correct}/{len(data)} ({100*correct/len(data):.1f}%)")

Случайные веса дают низкую точность — нередко хуже, чем угадывание мажоритарного класса. После обучения (урок 03) эта же архитектура с восемью скрытыми нейронами нарисует криволинейную границу, отделяющую внутреннюю часть от внешней.

Используйте

PyTorch делает всё перечисленное выше в четыре строки:

import torch
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(2, 8),
    nn.Sigmoid(),
    nn.Linear(8, 1),
    nn.Sigmoid(),
)

x = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
output = model(x)
print(output)

nn.Linear(2, 8) — это ваш класс Layer: матрица весов формы (8, 2), вектор смещения формы (8,). nn.Sigmoid() — ваша сигмоидальная функция, применяемая поэлементно. nn.Sequential — ваш класс Network: он соединяет слои по порядку.

Разница — в скорости и масштабе. PyTorch работает на GPU, обрабатывает батчи из миллионов образцов и автоматически вычисляет градиенты для обратного распространения. Но логика прямого прохода идентична той, которую вы только что собрали с нуля.

Внедрите

Этот урок создаёт многоразовый промпт для проектирования сетевых архитектур:

  • outputs/prompt-network-architect.md

Используйте его, когда нужно решить, сколько слоёв, сколько нейронов на слой и какие функции активации выбрать для конкретной задачи.

Упражнения

  1. Соберите сеть 2-4-2-1 (с двумя скрытыми слоями) и выполните прямой проход на данных XOR со случайными весами. Выведите промежуточные выходы скрытых слоёв, чтобы увидеть, как преобразуется представление на каждом слое.

  2. Измените размер скрытого слоя в классификаторе круга с 8 на 2, затем на 32. Каждый раз выполните прямой проход со случайными весами. Меняет ли число скрытых нейронов диапазон или распределение выходов? Почему?

  3. Реализуйте в классе Network метод count_parameters, возвращающий общее число обучаемых весов и смещений. Проверьте его на сети 784-256-128-10 (классическая архитектура MNIST). Сколько в ней параметров?

  4. Соберите прямой проход для сети 3-4-4-2. Подайте ей значения цветов RGB (нормированные к 0-1) и наблюдайте два выхода. Это архитектура простого классификатора цветов с двумя классами.

  5. Замените сигмоиду на функцию «протекающий шаг»: возвращайте 0.01 * z, если z < 0, иначе 1.0. Выполните прямой проход XOR с теми же вручную настроенными весами из шага 4. Продолжает ли он работать? Почему плавная сигмоида предпочтительнее жёстких отсечек?

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

Термин Как обычно говорят Что это действительно означает
Прямой проход «Запуск модели» Проталкивание входа через каждый слой — умножение на веса, добавление смещения, активация — чтобы получить выход
Скрытый слой «Средняя часть» Любой слой между входом и выходом, значения которого напрямую не наблюдаются в данных
Многослойная сеть «Глубокая нейронная сеть» Слои нейронов, последовательно сложенные друг за другом; выход каждого слоя подаётся на вход следующего
Функция активации «Нелинейность» Функция, применяемая после линейного преобразования и вносящая кривизну в границу решений
Сигмоида «S-образная кривая» sigma(z) = 1/(1+e^(-z)); сжимает любое вещественное число в (0,1), плавна и дифференцируема везде
Матрица весов «Параметры» Матрица W формы (current_layer_neurons, previous_layer_neurons), содержащая обучаемые силы соединений
Вектор смещения «Сдвиг» Вектор, добавляемый после умножения матриц и позволяющий нейронам активироваться, даже когда все входы равны нулю
Универсальная аппроксимация «Нейросети могут выучить что угодно» Один скрытый слой с достаточным количеством нейронов может аппроксимировать любую непрерывную функцию — но «достаточное» число может означать миллиарды
Линейное преобразование «Шаг матричного умножения» z = W * x + b, вычисление до активации, отображающее входы в новое пространство
Граница решений «Где классификатор переключается» Поверхность во входном пространстве, в которой выход сети пересекает порог классификации

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

  • Michael Nielsen, «Neural Networks and Deep Learning», главы 1–2 (http://neuralnetworksanddeeplearning.com/) — самое ясное бесплатное объяснение прямых проходов и структуры сети, с интерактивными визуализациями
  • Cybenko, «Approximation by Superpositions of a Sigmoidal Function» (1989) — исходная работа о теореме универсальной аппроксимации; на удивление доступна для чтения
  • 3Blue1Brown, «But what is a neural network?» (https://www.youtube.com/watch?v=aircAruvnKk) — 20-минутный визуальный разбор слоёв, весов и прямых проходов, формирующий правильную интуитивную модель
  • Goodfellow, Bengio, Courville, «Deep Learning», глава 6 (https://www.deeplearningbook.org/) — стандартный справочник по многослойным сетям, доступный бесплатно онлайн

Источник: Multi-Layer Networks and Forward Pass 03.01 — Перцептрон · Фаза 3 — Основы глубокого обучения · Полный каталог · 03.03 — Обратное распространение ошибки