Как написать простую нейросеть на Python: пошаговое руководство для начинающих

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

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

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

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

В этом руководстве мы создадим нейросеть прямого распространения (feedforward) на чистом Python с использованием библиотеки NumPy. Наша сеть будет решать задачу XOR — классический пример, который показывает, что однослойный перцептрон не справляется, а двухслойный — легко.

Архитектура простой нейросети: слои, нейроны, веса

Любая нейросеть состоит из слоёв:

  • Входной слой — принимает исходные данные. Количество нейронов равно числу признаков.
  • Скрытые слои — выполняют основные преобразования. В нашей сети будет один скрытый слой из 4 нейронов.
  • Выходной слой — выдаёт результат. Для XOR это один нейрон (0 или 1).

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

Изначально веса и смещения инициализируются случайными значениями. В процессе обучения они корректируются так, чтобы минимизировать ошибку предсказания.

Математически работа одного нейрона выглядит так: z = w₁·x₁ + w₂·x₂ + ... + wₙ·xₙ + b y = f(z) где f — функция активации.

Функции активации: зачем они нужны и какие бывают

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

Сигмоида — классическая функция, которая сжимает любое число в диапазон от 0 до 1: sigmoid(x) = 1 / (1 + exp(-x)) Она хороша для задач бинарной классификации, но страдает от проблемы «исчезающего градиента» при большом количестве слоёв.

Производная сигмоиды используется при обратном распространении ошибки: sigmoid_derivative(x) = x * (1 - x)

Другие популярные функции:

  • ReLU — возвращает максимум из 0 и x. Простая, эффективная, но может «убивать» нейроны.
  • Tanh — похожа на сигмоиду, но с диапазоном от -1 до 1.

В нашем примере мы будем использовать сигмоиду для скрытого и выходного слоёв, так как она наглядна и проста в реализации.

Подготовка данных: датасет для XOR

Логическая операция XOR (исключающее ИЛИ) возвращает 1, если входные сигналы различаются, и 0, если они совпадают. Таблица истинности:

  • (0, 0) → 0
  • (0, 1) → 1
  • (1, 0) → 1
  • (1, 1) → 0

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

На Python подготовка данных выглядит так:

import numpy as np

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

Массив X содержит все 4 комбинации входов, y — правильные ответы. Эти данные мы будем подавать на вход сети и сравнивать её предсказания с истинными значениями.

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

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

Алгоритм:

  1. Умножаем входной вектор X на матрицу весов между входным и скрытым слоем (weights1).
  2. Применяем функцию активации (сигмоиду) к полученной сумме — получаем выход скрытого слоя.
  3. Умножаем выход скрытого слоя на матрицу весов между скрытым и выходным слоем (weights2).
  4. Снова применяем сигмоиду — получаем финальное предсказание.

Код:

def forward_pass(X, weights1, weights2):
    hidden_input = np.dot(X, weights1)
    hidden_output = sigmoid(hidden_input)
    output_input = np.dot(hidden_output, weights2)
    predicted_output = sigmoid(output_input)
    return hidden_output, predicted_output

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

Функция потерь: как измерить ошибку сети

Чтобы понять, насколько хорошо сеть предсказывает, нужна функция потерь (loss function). Она вычисляет разницу между предсказанным значением и истинным.

Самая простая функция — среднеквадратичная ошибка (MSE): loss = (y_true - y_pred)²

Для нескольких примеров берём среднее:

def loss(y_true, y_pred):
    return ((y_true - y_pred) ** 2).mean()

Чем меньше loss, тем точнее предсказания. В процессе обучения мы будем стремиться минимизировать эту величину.

Другие популярные функции потерь:

  • Binary Cross-Entropy — для бинарной классификации.
  • Categorical Cross-Entropy — для многоклассовой классификации.
  • MAE (Mean Absolute Error) — для регрессии.

В нашем примере MSE достаточно, так как задача простая и наглядная.

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

Обратное распространение ошибки (backpropagation) — ключевой механизм обучения нейросети. Он отвечает на вопрос: «Как каждый вес повлиял на итоговую ошибку?»

Алгоритм:

  1. Вычисляем ошибку на выходе: output_error = y_true - y_pred.
  2. Находим «дельту» для выходного слоя: output_delta = output_error * sigmoid_derivative(predicted_output).
  3. Распространяем ошибку на скрытый слой: hidden_error = output_delta.dot(weights2.T).
  4. Вычисляем дельту для скрытого слоя: hidden_delta = hidden_error * sigmoid_derivative(hidden_output).
  5. Обновляем веса: weights += входной_сигнал.T.dot(дельта) * learning_rate.

Код:

def backward_pass(X, y, hidden_output, predicted_output, weights1, weights2, lr):
    output_error = y - predicted_output
    output_delta = output_error * sigmoid_derivative(predicted_output)
    hidden_error = output_delta.dot(weights2.T)
    hidden_delta = hidden_error * sigmoid_derivative(hidden_output)
    weights2 += hidden_output.T.dot(output_delta) * lr
    weights1 += X.T.dot(hidden_delta) * lr
    return weights1, weights2

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

Цикл обучения и запуск нейросети

Теперь объединим все компоненты в единый процесс обучения.

Шаги:

  1. Инициализируем веса случайными числами.
  2. Для каждой эпохи (полного прохода по всем данным):
  • Выполняем прямое распространение.
  • Вычисляем ошибку.
  • Выполняем обратное распространение и обновляем веса.
  1. Повторяем, пока ошибка не станет достаточно малой.

Код цикла:

learning_rate = 0.1
epochs = 10000

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

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

print("Результат после обучения:")
hidden_output, predicted_output = forward_pass(X, weights1, weights2)
print(np.round(predicted_output))

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

Практические советы и типичные ошибки новичков

При написании первой нейросети часто допускают следующие ошибки:

  1. Забывают про нормализацию данных. Если признаки имеют разный масштаб, сеть может обучаться медленно или вообще не сойтись.
  2. Неправильно выбирают learning rate. Слишком большой — расходится, слишком маленький — не учится. Начинайте с 0.01 и корректируйте.
  3. Путают функции активации. Для скрытых слоёв лучше использовать ReLU, для выходного — сигмоиду или softmax в зависимости от задачи.
  4. Не проверяют размерности матриц. Ошибка в shape — одна из самых частых причин неработающего кода.
  5. Обучают слишком мало эпох. Для XOR нужно несколько тысяч итераций, для более сложных задач — десятки тысяч.

Советы:

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

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

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

  • Многослойные перцептроны — добавление нескольких скрытых слоёв.
  • Свёрточные нейросети (CNN) — для работы с изображениями.
  • Рекуррентные нейросети (RNN) — для последовательностей (текст, временные ряды).

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

  • TensorFlow / Keras — мощный и гибкий.
  • PyTorch — популярен в исследованиях.
  • Scikit-learn — для простых задач машинного обучения.

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

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

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

При наличии базовых знаний Python и понимания математики (умножение матриц, производные) написание простой нейросети для XOR занимает около часа. Большая часть времени уходит на отладку и подбор гиперпараметров.

Какие библиотеки нужны для создания нейросети на Python?

Минимально необходим только NumPy для работы с матрицами. Для визуализации можно использовать Matplotlib. В реальных проектах применяют TensorFlow, PyTorch или Keras.

Почему однослойная нейросеть не может решить задачу XOR?

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

Что такое learning rate и как его выбрать?

Learning rate (скорость обучения) определяет, насколько сильно изменяются веса на каждом шаге. Слишком большое значение приводит к расходимости, слишком маленькое — к медленному обучению. Обычно начинают с 0.01–0.1 и корректируют по графику ошибки.

Нужно ли нормализовать данные перед обучением нейросети?

Да, желательно. Если признаки имеют разный масштаб, градиентный спуск может работать неэффективно. Для простых задач достаточно привести данные к диапазону [0, 1] или стандартизировать (среднее 0, дисперсия 1).

Как понять, что нейросеть обучилась правильно?

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

Можно ли использовать эту нейросеть для других задач, кроме XOR?

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