Простая нейросеть пример: как написать и обучить первую сеть на Python с нуля

Разбираем простую нейросеть на примере: архитектура, функции активации, обучение на Python с NumPy. Пошаговый код, объяснение ошибок и ответы на вопросы.

Что такое нейросеть и зачем нужен простой пример

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

Для новичка нейросети часто кажутся чем-то недоступным из-за обилия математики: градиентный спуск, обратное распространение ошибки, матричные операции. Однако простейший пример можно реализовать буквально в несколько десятков строк кода на Python, используя только библиотеку NumPy. Такой пример позволяет увидеть основные принципы работы: прямое распространение, вычисление ошибки и корректировку весов.

Простые примеры полезны не только для понимания теории, но и как отправная точка для собственных экспериментов. Разобравшись с базовой реализацией, вы сможете перейти к более сложным архитектурам и фреймворкам вроде TensorFlow или PyTorch.

Из чего состоит нейросеть: слои, веса и функции активации

Любая нейросеть, даже самая простая, состоит из трёх основных типов слоёв:

  • Входной слой — принимает исходные данные. В простейшем случае это несколько чисел, например, 0 или 1. Входной слой не выполняет вычислений, он лишь передаёт сигналы дальше.
  • Скрытый слой — здесь происходят основные преобразования. Количество нейронов в нём может быть любым; в нашем примере возьмём четыре. Каждый нейрон скрытого слоя связан со всеми нейронами входного слоя.
  • Выходной слой — выдаёт результат. В задаче классификации это обычно одно число, которое интерпретируется как вероятность принадлежности к классу.

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

Функция активации — ключевой элемент, который добавляет нелинейность. Если просто перемножать входные сигналы на веса, сеть останется линейной и не сможет решать сложные задачи. Самая популярная простая функция — сигмоида: она сжимает любое число в диапазон от 0 до 1. Это удобно для интерпретации результата как вероятности. Для обучения также нужна производная сигмоиды, которая вычисляется просто: x * (1 - x).

Как подготовить данные для обучения нейросети

Данные — это фундамент любой нейросети. Для простого примера мы возьмём классическую задачу — логическую операцию XOR (исключающее ИЛИ). Таблица истинности для XOR выглядит так:

  • 0 XOR 0 = 0
  • 0 XOR 1 = 1
  • 1 XOR 0 = 1
  • 1 XOR 1 = 0

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

В коде на Python с использованием NumPy данные задаются так:

import numpy as np

X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
y = np.array([[0], [1], [1], [0]])

Массив X содержит входные комбинации, массив y — правильные ответы. Важно, что данные представлены в виде матриц, потому что нейросеть оперирует матричными операциями. Для более сложных задач данные обычно нормализуют, но в нашем случае они уже находятся в диапазоне 0–1.

Инициализация весов и архитектура простой сети

Перед обучением нужно создать структуру сети и задать начальные веса. В нашем примере сеть имеет два входа, четыре нейрона в скрытом слое и один выход. Веса между входным и скрытым слоем — матрица размером 2×4, между скрытым и выходным — матрица 4×1.

Инициализация весов случайными числами — стандартный подход. Это даёт сети отправную точку, от которой она будет отталкиваться в процессе обучения. В Python это делается так:

def initialize_weights(input_size, hidden_size, output_size):
    np.random.seed(42)  # для воспроизводимости
    weights1 = np.random.randn(input_size, hidden_size)
    weights2 = np.random.randn(hidden_size, output_size)
    return weights1, weights2

Использование np.random.seed гарантирует, что при каждом запуске программы веса будут одинаковыми — это удобно для отладки. В реальных проектах seed часто не фиксируют, чтобы получить разные результаты.

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

Прямое распространение: как сеть делает предсказание

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

В коде это выглядит так:

def forward_pass(X, weights1, weights2):
    hidden_layer_input = np.dot(X, weights1)
    hidden_layer_output = sigmoid(hidden_layer_input)
    output_layer_input = np.dot(hidden_layer_output, weights2)
    predicted_output = sigmoid(output_layer_input)
    return hidden_layer_output, predicted_output

Сначала входные данные умножаются на веса первого слоя, затем применяется сигмоида. Полученный результат — выход скрытого слоя — умножается на веса второго слоя, и снова применяется сигмоида. На выходе получается число от 0 до 1, которое можно интерпретировать как вероятность.

На начальном этапе, когда веса случайны, предсказания будут далеки от правильных. Например, для XOR сеть может выдать 0.5 для всех комбинаций. Это нормально — обучение как раз и заключается в том, чтобы скорректировать веса так, чтобы предсказания стали точными.

Обратное распространение ошибки: как сеть учится

Обратное распространение ошибки (backpropagation) — это алгоритм, который позволяет сети понять, насколько каждый вес виноват в ошибке, и скорректировать его. Процесс состоит из нескольких шагов:

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

В коде это реализовано так:

def backward_pass(X, y, hidden_layer_output, predicted_output, weights1, weights2, learning_rate):
    output_error = y - predicted_output
    output_delta = output_error * sigmoid_derivative(predicted_output)
    hidden_layer_error = output_delta.dot(weights2.T)
    hidden_layer_delta = hidden_layer_error * sigmoid_derivative(hidden_layer_output)
    weights2 += hidden_layer_output.T.dot(output_delta) * learning_rate
    weights1 += X.T.dot(hidden_layer_delta) * learning_rate
    return weights1, weights2

Скорость обучения (learning rate) — важный гиперпараметр. Если она слишком велика, сеть будет «перепрыгивать» оптимальные веса и ошибка не уменьшится. Если слишком мала, обучение займёт много времени. В нашем примере используем значение 0.1.

Цикл обучения: эпохи и отслеживание ошибки

Обучение нейросети — это многократное повторение прямого и обратного проходов. Каждый полный проход по всем обучающим примерам называется эпохой. В нашем примере мы запустим 10 000 эпох, что достаточно для XOR.

Цикл обучения выглядит так:

learning_rate = 0.1
epochs = 10000

weights1, weights2 = initialize_weights(2, 4, 1)

for i in range(epochs):
    hidden_layer_output, predicted_output = forward_pass(X, weights1, weights2)
    weights1, weights2 = backward_pass(X, y, hidden_layer_output, predicted_output, weights1, weights2, learning_rate)
    if i % 1000 == 0:
        error = np.mean(np.abs(y - predicted_output))
        print(f"Эпоха {i}, Ошибка: {error:.6f}")

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

После завершения обучения можно проверить результат:

hidden_layer_output, predicted_output = forward_pass(X, weights1, weights2)
print(np.round(predicted_output))

Ожидаемый вывод: [[0.], [1.], [1.], [0.]] — сеть правильно предсказала все четыре комбинации XOR.

Упрощённый вариант: нейросеть в 9 строк кода

Для тех, кто хочет увидеть самую минимальную реализацию, существует вариант нейросети всего в 9 строк кода. Этот пример, популяризированный Мило Спенсер-Харпером, использует один нейрон и решает более простую задачу — предсказание по трём входам.

Код выглядит так:

from numpy import exp, array, random, dot

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

random.seed(1)
synaptic_weights = 2 * random.random((3, 1)) - 1

for iteration in range(10000):
    output = 1 / (1 + exp(-(dot(training_set_inputs, synaptic_weights))))
    synaptic_weights += dot(training_set_inputs.T, (training_set_outputs - output) * output * (1 - output))

print(1 / (1 + exp(-(dot(array([1, 0, 0]), synaptic_weights)))))

Этот код обучает один нейрон предсказывать результат, где выход равен первому входу. После обучения сеть на входе [1, 0, 0] выдаёт значение около 0.9999, что очень близко к 1.

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

Практические советы: как избежать типичных ошибок

При написании первой нейросети новички часто сталкиваются с несколькими типичными проблемами:

  • Ошибка NameError: name 'xrange' is not defined — возникает при переходе с Python 2 на Python 3. Замените xrange на range.
  • Синтаксические ошибки — в Python 3 функция print требует круглые скобки: print("text").
  • Слишком маленькая или слишком большая скорость обучения — если ошибка не уменьшается или «скачет», попробуйте изменить learning rate.
  • Недостаточное количество эпох — для XOR может потребоваться несколько тысяч итераций, не останавливайтесь слишком рано.
  • Отсутствие нормализации данных — если входные значения большие, сигмоида может насыщаться, и обучение замедлится.

Также полезно фиксировать seed для воспроизводимости результатов. Если вы используете Jupyter Notebook, убедитесь, что все ячейки выполняются в правильном порядке.

Для визуализации процесса обучения можно использовать Matplotlib — построить график ошибки в зависимости от эпохи. Это поможет понять, сходится ли сеть.

Что дальше: от простой сети к реальным проектам

Разобравшись с простейшей нейросетью, вы можете двигаться дальше. Вот несколько направлений для развития:

  • Добавление большего количества слоёв — глубокие сети с несколькими скрытыми слоями способны решать более сложные задачи.
  • Использование других функций активации — ReLU, tanh, softmax для многоклассовой классификации.
  • Работа с реальными данными — например, распознавание рукописных цифр из набора MNIST.
  • Использование фреймворков — TensorFlow, PyTorch, Keras позволяют строить сети без ручной реализации обратного распространения.
  • Изучение других архитектур — свёрточные сети для изображений, рекуррентные для последовательностей, трансформеры для текста.

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

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

Вопросы и ответы

Сколько времени нужно, чтобы обучить простую нейросеть на Python?

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

Почему для XOR нужен скрытый слой?

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

Что такое функция активации и зачем она нужна?

Функция активации добавляет нелинейность в нейросеть. Без неё сеть оставалась бы линейной, и многослойная архитектура не давала бы преимуществ. Сигмоида сжимает значения в диапазон от 0 до 1, что удобно для вероятностной интерпретации. Другие функции, например ReLU, помогают бороться с затуханием градиента.

Как выбрать скорость обучения (learning rate)?

Скорость обучения — гиперпараметр, который подбирается экспериментально. Слишком большое значение приводит к тому, что веса «перескакивают» оптимум, и ошибка не уменьшается. Слишком маленькое — обучение идёт медленно. Обычно пробуют значения 0.1, 0.01, 0.001 и смотрят на динамику ошибки.

Можно ли использовать эту простую сеть для реальных задач?

Вряд ли. Простая сеть с одним скрытым слоем подходит только для игрушечных примеров. Для реальных задач — распознавание изображений, обработка текста, прогнозирование — нужны более глубокие архитектуры, большие объёмы данных и фреймворки вроде TensorFlow или PyTorch. Однако понимание базовых принципов поможет быстрее освоить эти инструменты.

Что такое эпоха в обучении нейросети?

Эпоха — это один полный проход всех обучающих примеров через сеть. В нашем примере 4 примера, поэтому одна эпоха — это обработка всех четырёх комбинаций. Количество эпох — гиперпараметр, определяющий, сколько раз сеть увидит данные. Слишком мало эпох — недообучение, слишком много — переобучение.