1 От системы уравнений к нейрону

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

Знакомая постановка: в практике 4 ровно так подбиралась прямая y = kx + b по точкам — минимизацией среднеквадратичной ошибки градиентным спуском. В общем виде: пусть имеется m наблюдений, в каждом зафиксированы значения n признаков и значение целевой величины. Линейная модель зависимости задаётся коэффициентами w₁, …, wₙ и свободным членом w₀:

yi = w1 xi1 + w2 xi2 + ⋯ + wn xin + w0,    i = 1, …, m

Наблюдений больше, чем неизвестных, — система переопределена, и из-за шума измерений точного решения, как правило, нет. Для линейной модели и квадратичной меры ошибки приближённое решение выписывается аналитически — это метод наименьших квадратов. Но не любую закономерность удаётся угадать и явно записать формулой: зависимость класса объекта от яркостей пикселей изображения или границ фонем от звуковой волны не сводится к линейному уравнению — её нужно извлечь из множества примеров.

Вот несколько таких задач — во всех случаях вход и выход описываются числами, но явной формулы зависимости нет:

Вход — изображение сцены; выход — класс каждого пикселя (сегментация)
Вход — изображение сцены; выход — класс каждого пикселя (сегментация)
Вход — звуковая волна; выход — границы и метки фонем в произнесённом слове
Вход — звуковая волна; выход — границы и метки фонем в произнесённом слове
Вход — изображение и стиль; выход — изображение, перерисованное в этом стиле
Вход — изображение и стиль; выход — изображение, перерисованное в этом стиле

Нейросетевой метод — это способ находить такие зависимости вычислительно, когда явно выписать уравнение невозможно или непрактично. Обратите внимание: это в чистом виде эвристический алгоритм в смысле определения из практики 13 — его правильность для всех возможных случаев не доказана, гарантий сходимости к наилучшей модели нет, но на практике он даёт достаточно хорошее решение для огромного класса плохо формализованных задач. Дальше рассматривается минимальный вычислительный элемент, из которого строится это решение, — нейрон.

Из презентации курса «Нейросети: начало»
Из презентации курса «Нейросети: начало»

2 Формальное определение нейрона

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

Пусть на вход нейрона j поступают сигналы x₁, …, x_N от N предшествующих элементов сети. Комбинированный ввод нейрона j определяется как

netj = Σi=1N xi wij + w0j

где w_ij — вес связи, идущей от элемента i к нейрону j, а w_0j — свободный член (смещение, англ. bias) нейрона j. Один нейрон с линейной активацией — это в точности линейная модель из предыдущего раздела, оформленная в виде вычислительного элемента.

Нейрон 4 комбинирует сигналы x₁, x₂, x₃ входных элементов с весами w₁₄, w₂₄, w₃₄ — иллюстрация из презентации курса «Нейросети: начало»
Нейрон 4 комбинирует сигналы x₁, x₂, x₃ входных элементов с весами w₁₄, w₂₄, w₃₄ — иллюстрация из презентации курса «Нейросети: начало»

Веса связей между двумя слоями сети записывают весовой матрицей W; такая запись удобна тем, что вычисление комбинированного ввода всех нейронов слоя сразу становится одним матричным умножением: net = W·x + w₀.

Функция активации

Для каждого элемента нейронной сети имеется определённое правило, по которому из значения комбинированного ввода вычисляется выходное значение; это правило называется функцией активации. Выходной сигнал нейрона j получают применением функции активации f к комбинированному вводу:

yj = f(netj)

Употребительные функции активации: линейная f(x) = x, сигмоида f(x) = 1 / (1 + e^{-x}), гиперболический тангенс и ReLU(x) = max(0, x).

Сигмоида монотонно преобразует любое вещественное число в интервал (0, 1), что удобно интерпретировать как вероятность принадлежности одному из двух классов: значения выше 0.5 относят к одному классу, ниже — к другому. Порог 0.5 соответствует нулевому значению комбинированного ввода: f(0) = 0.5.

Сигмоида как классификатор — иллюстрация из презентации курса «Нейросети: начало»
Сигмоида как классификатор — иллюстрация из презентации курса «Нейросети: начало»

Тренажёр · Один нейрон: комбинированный ввод и функция активации

Нейрон с двумя входами вычисляет net = w₁x₁ + w₂x₂ + w₀, затем пропускает результат через функцию активации: y = f(net). Двигайте входы и веса, переключайте функции — слева пересчитывается вся цепочка, справа на графике активации отмечена текущая точка (net, y). Обратите внимание: сигмоида сжимает любой net в интервал (0, 1), а пороговая функция реагирует только на знак.

Типичная ошибка

Забывают, что порог принятия решения 0.5 для сигмоиды соответствует net = 0, а не какому-то отдельному настраиваемому параметру: изменить порог классификации — значит изменить веса или смещение так, чтобы точка net = 0 сдвинулась в нужное место, а не подставить другое число вместо 0.5 при уже вычисленном net.

3 Линейная разделимость: нейрон как классификатор

Возьмём пороговую функцию активации (функцию Хевисайда) — историческую функцию активации персептрона Розенблатта (1958). Это простейшая кусочно-линейная функция: она меняет своё значение скачком, когда аргумент достигает порога:

f(x) = {1,x > 00,x ≤ 0
Пороговая функция активации — иллюстрация из презентации курса «Нейросети: начало»
Пороговая функция активации — иллюстрация из презентации курса «Нейросети: начало»

Для нейрона с двумя входами x₁, x₂ условие net = 0, то есть

w1 x1 + w2 x2 + w0 = 0

задаёт на плоскости (x₁, x₂) прямую. Нейрон относит точки по одну сторону прямой к классу 1, по другую — к классу 0. Значит, единственный нейрон с пороговой активацией способен точно реализовать только линейно разделимую функцию — такую, для которой точки обоих классов можно разделить одной прямой (в общем случае — гиперплоскостью).

Логические функции двух аргументов И и ИЛИ линейно разделимы. Функция «исключающее ИЛИ» (XOR) — нет: на плоскости (x₁, x₂) её единичные значения (точки (0,1) и (1,0)) и нулевые (точки (0,0) и (1,1)) расположены на противоположных диагоналях единичного квадрата, и никакая прямая не отделяет одну диагональ от другой. Это обстоятельство — не техническое ограничение конкретной реализации, а математический факт: для любых w₁, w₂, w₀ хотя бы одна из четырёх точек будет классифицирована неверно. Именно этот факт лёг в основу критики персептрона в работе М. Минского и С. Пейперта «Perceptrons» (1969) и на десятилетия снизил интерес к нейросетевым методам.

Тренажёр · Линейная разделимость: нейрон-классификатор

Нейрон с пороговой активацией относит точку к классу 1, если net = w₁x₁ + w₂x₂ + w₀ > 0. Линия на плоскости — граница net = 0; закрашенные точки — цель класса 1, незакрашенные — класса 0. Подберите ползунками веса для И и ИЛИ (это удаётся), затем переключитесь на «исключающее ИЛИ» и убедитесь, что 4 из 4 не достигаются ни при каком подборе: точки классов XOR лежат на противоположных диагоналях квадрата, и одной прямой их не разделить.

Типичная ошибка

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

Задание 1

Убедитесь в этом программно: переберите веса w₁, w₂, w₀ по сетке от −2 до 2 с шагом 0.25 (17³ = 4913 комбинаций) и найдите наибольшее число совпадений выхода нейрона с таблицей истинности XOR. Перебор даёт максимум 3 из 4 — сравните с И, ИЛИ и И-НЕ, для которых легко находится 4 из 4.

Решение XOR сетью из нескольких нейронов

Раз одной прямой не хватает, задачу решают, вводя промежуточный (скрытый) слой, который нелинейно преобразует входы, после чего выходной нейрон разделяет уже преобразованные точки. Один из вариантов — представить XOR через более простые линейно разделимые функции:

XOR(x1, x2) = AND(OR(x1, x2), NAND(x1, x2))

Зафиксируем веса вручную (ниже будет показано, как такие веса находятся автоматически, обучением, а не подбором): скрытый нейрон h₁ с весами (1, 1, −0.5) реализует OR, скрытый нейрон h₂ с весами (−1, −1, 1.5) — NAND, а выходной нейрон с весами (1, 1, −1.5) — AND над h₁ и h₂.

Проверка на всех четырёх входах:

x₁x₂h₁ (OR)h₂ (NAND)yXOR(x₁,x₂)
000100
011111
101111
111000

Сеть из двух скрытых нейронов и одного выходного точно воспроизводит таблицу истинности XOR. Общий вывод: если функция не линейно разделима, отдельный нейрон принципиально не может её вычислить, но сеть из нескольких нейронов — может, поскольку скрытый слой задаёт новое пространство признаков (h₁, h₂), в котором задача становится линейно разделимой. Соединение нейронов в слои — входной, скрытые, выходной — и есть нейронная сеть; исторически такие конструкции называются многослойными персептронами (англ. multilayer perceptron).

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

def step(x):
    """Пороговая функция активации (функция Хевисайда)."""
    return 1 if x > 0 else 0

print(" x1 x2 | h1(OR) h2(NAND) |  y")
for x1, x2 in INPUTS:
    h1 = step(x1 + x2 - 0.5)        # OR
    h2 = step(-x1 - x2 + 1.5)       # NAND
    y = step(h1 + h2 - 1.5)         # AND над h1, h2
    print("  %d  %d |   %d       %d     |  %d" % (x1, x2, h1, h2, y))
#include <iostream>

// Пороговая функция активации (функция Хевисайда)
int step(double x) { return x > 0 ? 1 : 0; }

int main() {
    std::cout << " x1 x2 | h1(OR) h2(NAND) |  y\n";
    for (int x1 = 0; x1 <= 1; ++x1)
        for (int x2 = 0; x2 <= 1; ++x2) {
            int h1 = step(x1 + x2 - 0.5);    // OR
            int h2 = step(-x1 - x2 + 1.5);   // NAND
            int y  = step(h1 + h2 - 1.5);    // AND над h1, h2
            std::cout << "  " << x1 << "  " << x2 << " |   " << h1
                      << "       " << h2 << "     |  " << y << "\n";
        }
    return 0;
}

4 Обучение нейрона: дельта-правило

Веса для XOR выше подобраны вручную — задача с двумя входами и известной структурой это ещё позволяет. Для сети с тысячами весов и незнакомой заранее зависимостью подбор руками невозможен. Нужен автоматический способ находить веса, минимизирующие ошибку на обучающих данных, — это и называется обучением нейросети.

Инструмент уже есть: метод градиентного спуска из практики 4. Целевая функция — квадратичная ошибка E = ½(y − ŷ)², где y — правильный ответ из обучающей выборки, а ŷ = f(net) — текущий ответ нейрона. Ошибка зависит от веса wᵢ через цепочку промежуточных величин (wᵢ влияет на net, net — на ŷ, ŷ — на E), и по цепному правилу дифференцирования частная производная равна произведению трёх сомножителей:

∂E/∂wi = -(y - ŷ) · f'(net) · xi

Для сигмоиды производная выражается через саму функцию: f'(net) = ŷ·(1 − ŷ). Подставив всё в правило градиентного спуска, получаем дельта-правило — формулу изменения веса после каждого показа обучающего примера:

Δ wi = α · (y - ŷ) · ŷ (1 - ŷ) · xi

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

from numpy import exp, array, random, dot

# обучающая выборка: 4 примера, по 3 входа в каждом
training_set_inputs = array([[0, 0, 1], [1, 1, 1], [1, 0, 1], [0, 1, 1]])
# целевой выход — совпадает со значением первого входа
training_set_outputs = array([[0, 1, 1, 0]]).T

# инициализация весов случайными числами из [-1, 1]
random.seed(1)
synaptic_weights = 2 * random.random((3, 1)) - 1

for iteration in range(100000):
    # прямой проход: net = X * w, затем сигмоида
    output = 1 / (1 + exp(-dot(training_set_inputs, synaptic_weights)))

    # дельта-правило в матричном виде: X^T * ((y - out) * out * (1 - out))
    synaptic_weights += dot(
        training_set_inputs.T,
        (training_set_outputs - output) * output * (1 - output)
    )

print(synaptic_weights)

# проверка на новом примере [1, 0, 0]
print(1 / (1 + exp(-dot(array([1, 0, 0]), synaptic_weights))))
#include <iostream>
#include <cmath>
#include <random>

int main() {
    // обучающая выборка: 4 примера, по 3 входа; цель = первый вход
    double X[4][3] = {{0, 0, 1}, {1, 1, 1}, {1, 0, 1}, {0, 1, 1}};
    double Y[4] = {0, 1, 1, 0};

    // инициализация весов случайными числами из [-1, 1]
    std::mt19937 gen(1);
    std::uniform_real_distribution<> uni(-1.0, 1.0);
    double w[3] = {uni(gen), uni(gen), uni(gen)};

    for (int it = 0; it < 100000; ++it) {
        double dw[3] = {0, 0, 0};
        for (int s = 0; s < 4; ++s) {
            // прямой проход: net = x * w, затем сигмоида
            double net = 0;
            for (int i = 0; i < 3; ++i) net += X[s][i] * w[i];
            double out = 1.0 / (1.0 + std::exp(-net));
            // дельта-правило: (y - out) * out * (1 - out) * x_i
            double delta = (Y[s] - out) * out * (1 - out);
            for (int i = 0; i < 3; ++i) dw[i] += delta * X[s][i];
        }
        for (int i = 0; i < 3; ++i) w[i] += dw[i];
    }

    std::cout << "Веса: " << w[0] << ", " << w[1] << ", " << w[2] << "\n";

    // проверка на новом примере [1, 0, 0]
    std::cout << "Ответ на [1, 0, 0]: "
              << 1.0 / (1.0 + std::exp(-w[0])) << "\n";
    return 0;
}

После обучения веса приходят примерно к значениям (12.0, −0.2, −5.8): большой положительный вес первого входа означает «смотри на первый вход», близкий к нулю второй — «второй вход не важен», отрицательный третий компенсирует то, что третий вход во всех обучающих примерах равен единице. На новом примере [1, 0, 0], которого нейрон никогда не видел, он выдаёт 0.9999… — уверенное «да», совпадающее со значением первого входа. Число проходов по всей обучающей выборке (здесь — 100000) принято называть числом эпох.

Типичная ошибка

Не задав random.seed(1), получают при каждом запуске разные начальные веса и, соответственно, слегка разные (хотя и близкие) итоговые веса. Для воспроизводимости результатов seed фиксируют; это не влияет на то, чему в итоге обучится нейрон, только на конкретные числа.

Полученная конструкция замыкает сюжет практики: нейрон — эвристический алгоритм, который не выводит зависимость аналитически, а подбирает её градиентным спуском по примерам. В практике 15 появится ещё один взгляд на обучение — эволюционный: там параметры решения подбирает генетический алгоритм, а агент в игре учится методом обучения с подкреплением.

Задание 2
  1. В программе обучаемого нейрона поменяйте целевой столбец так, чтобы ответ совпадал со вторым входом, и заново обучите нейрон. Как изменились веса? Дайте интерпретацию каждому из трёх чисел.
  2. Замените сигмоиду на ReLU в прямом проходе (и её производную в дельта-правиле: f'(net) = 1 при net > 0, иначе 0). Сходится ли обучение? Какая скорость обучения нужна?
  3. Попробуйте обучить один нейрон функции XOR (входы — пары (x₁, x₂), третий вход — константа 1). Пронаблюдайте, что ошибка перестаёт уменьшаться, и объясните результат через линейную разделимость.

5 Вопросы для закрепления

Почему переопределённая система линейных уравнений обычно не имеет точного решения?

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

Из каких двух шагов состоит вычисление выхода нейрона?

Линейное комбинирование входов с весами (комбинированный ввод net — взвешенная сумма плюс смещение) и применение функции активации к результату: y = f(net).

Почему нейросетевой метод относят к эвристическим алгоритмам?

Его правильность для всех случаев не доказана: обучение градиентным спуском не гарантирует нахождение наилучших весов (возможны локальные минимумы), но на практике метод даёт достаточно хорошие решения для широкого класса плохо формализованных задач — в точности по определению эвристического алгоритма.

В каком диапазоне лежит выход сигмоиды и почему это удобно для классификации?

В интервале (0, 1); значение удобно интерпретировать как вероятность принадлежности одному из двух классов, а порог 0.5 соответствует net = 0.

Почему один нейрон с пороговой активацией не может точно вычислить XOR?

Условие net = 0 задаёт прямую на плоскости входов, а единичные и нулевые значения XOR расположены на противоположных диагоналях единичного квадрата — их не разделить одной прямой ни при каком выборе весов и смещения.

Как всё же решить линейно неразделимую задачу нейронами?

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

Что делает дельта-правило и откуда оно берётся?

Изменяет каждый вес пропорционально ошибке, производной активации и входному сигналу: Δwᵢ = α(y − ŷ)ŷ(1 − ŷ)xᵢ. Это обычный градиентный спуск по квадратичной ошибке, в котором частная производная расписана по цепному правилу.

Навигация
Содержание