Простая нейросеть на Python: создание с нуля и обучение для начинающих

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

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

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

Многие начинающие разработчики сразу переходят к фреймворкам вроде TensorFlow или PyTorch, не понимая внутреннего устройства нейросетей. Написание сети с нуля на чистом Python с использованием только библиотеки NumPy даёт глубокое понимание того, как работают веса, функции активации и алгоритмы обучения. Это помогает в дальнейшем осознанно выбирать архитектуры и настраивать гиперпараметры.

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

Нейрон: базовый элемент сети

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

Математически это выглядит так:

y = f(w1*x1 + w2*x2 + ... + wn*xn + b)

Здесь x — входные значения, w — веса, b — порог, f — функция активации. Веса определяют, насколько сильно каждый вход влияет на выход нейрона. Порог позволяет сдвигать функцию активации.

В качестве функции активации часто используют сигмоиду:

f(x) = 1 / (1 + exp(-x))

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

Реализация нейрона на Python с NumPy занимает всего несколько строк:

import numpy as np

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

class Neuron:
    def __init__(self, weights, bias):
        self.weights = weights
        self.bias = bias

    def feedforward(self, inputs):
        total = np.dot(self.weights, inputs) + self.bias
        return sigmoid(total)

Пример: если веса равны [0, 1], порог равен 4, а входы — [2, 3], то выход нейрона будет 0.999. Это означает, что нейрон почти уверен в положительном классе.

Собираем нейронную сеть из нейронов

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

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

Процесс передачи данных от входа к выходу называется прямой связью (feedforward). Для сети с одинаковыми весами [0, 1] и нулевым порогом на всех нейронах при входе [2, 3] вычисления будут такими:

  • h1 = sigmoid(02 + 13 + 0) = sigmoid(3) ≈ 0.9526
  • h2 = sigmoid(02 + 13 + 0) = sigmoid(3) ≈ 0.9526
  • o1 = sigmoid(0h1 + 1h2 + 0) = sigmoid(0.9526) ≈ 0.7216

Код для такой сети:

class OurNeuralNetwork:
    def __init__(self):
        weights = np.array([0, 1])
        bias = 0
        self.h1 = Neuron(weights, bias)
        self.h2 = Neuron(weights, bias)
        self.o1 = Neuron(weights, bias)

    def feedforward(self, x):
        out_h1 = self.h1.feedforward(x)
        out_h2 = self.h2.feedforward(x)
        out_o1 = self.o1.feedforward(np.array([out_h1, out_h2]))
        return out_o1

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

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

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

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

MSE = (1/n) * Σ(y_true - y_pred)²

Где n — количество примеров, y_true — правильный ответ, y_pred — предсказание сети. Чем меньше MSE, тем лучше сеть.

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

Реализация MSE на Python:

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

Цель обучения — минимизировать функцию потерь, подбирая веса и пороги сети.

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

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

Рассмотрим простой случай: сеть с одним скрытым нейроном и одним выходным. Функция потерь для одного примера: L = (y_true - y_pred)². Нам нужно найти ∂L/∂w1 — как изменится ошибка при изменении веса w1.

Используя цепное правило:

∂L/∂w1 = (∂L/∂y_pred) * (∂y_pred/∂h1) * (∂h1/∂w1)

Где:

  • ∂L/∂y_pred = -2 * (y_true - y_pred)
  • ∂y_pred/∂h1 = w5 * f'(w5*h1 + w6*h2 + b3)
  • ∂h1/∂w1 = x1 * f'(w1*x1 + w2*x2 + b1)

Производная сигмоиды выражается через саму функцию:

f'(x) = f(x) * (1 - f(x))

Это удобно для вычислений.

После того как все частные производные найдены, веса обновляются по правилу градиентного спуска:

w_new = w_old - learning_rate * ∂L/∂w

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

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

Пошаговый пример обучения сети на Python

Рассмотрим конкретный пример: обучим сеть предсказывать пол человека по росту и весу. Пусть у нас есть данные четырёх человек:

| Имя | Вес (фунты) | Рост (дюймы) | Пол | |-------|-------------|--------------|-----| | Алиса | 133 | 65 | Ж | | Боб | 160 | 72 | М | | Чарли | 152 | 70 | М | | Диана | 120 | 60 | Ж |

Закодируем пол: женский = 1, мужской = 0. Для упрощения сдвинем данные, вычтя средние значения (135 для веса, 66 для роста). Получим:

| Имя | Вес (сдвиг) | Рост (сдвиг) | Пол | |-------|-------------|--------------|-----| | Алиса | -2 | -1 | 1 | | Боб | 25 | 6 | 0 | | Чарли | 17 | 4 | 0 | | Диана | -15 | -6 | 1 |

Теперь создадим сеть с двумя входами, скрытым слоем из двух нейронов и одним выходным нейроном. Инициализируем веса случайными числами. Затем в цикле из нескольких тысяч эпох будем:

  1. Выполнять прямой проход для всех примеров.
  2. Вычислять MSE.
  3. Выполнять обратный проход для расчёта градиентов.
  4. Обновлять веса и пороги.

После обучения проверим сеть на новых данных. Например, для человека с весом 140 фунтов и ростом 68 дюймов (сдвинутые значения: 5 и 2) сеть должна предсказать пол, близкий к 0 или 1.

Важно: для реальных задач используют более сложные архитектуры и нормализацию данных, но этот пример отлично иллюстрирует принцип обучения.

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

При создании нейронной сети с нуля важно учитывать несколько моментов:

  • Инициализация весов. Случайные веса должны быть небольшими, чтобы избежать насыщения сигмоиды. Обычно используют значения из равномерного распределения на отрезке [-0.5, 0.5] или [-1, 1].
  • Скорость обучения. Слишком высокая скорость может привести к тому, что градиентный спуск будет «перескакивать» минимум. Слишком низкая — замедлит обучение. Рекомендуется начинать с 0.1 или 0.01 и корректировать.
  • Количество эпох. Недостаточное количество эпох не позволит сети обучиться, избыточное — приведёт к переобучению. Следите за значением функции потерь на тестовой выборке.
  • Нормализация данных. Если признаки имеют разный масштаб (например, вес в фунтах и рост в дюймах), градиентный спуск может работать нестабильно. Лучше привести все признаки к одному диапазону, например, [0, 1] или [-1, 1].
  • Отсутствие нейрона смещения. Порог (bias) позволяет сдвигать функцию активации. Без него сеть может не справиться с некоторыми задачами.
  • Ошибки в реализации обратного распространения. Даже небольшая опечатка в формулах приводит к неправильному обучению. Рекомендуется проверять градиенты численно (метод конечных разностей).

Понимание этих ограничений поможет избежать типичных проблем при разработке собственных нейросетей.

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

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

Преимущества написания с нуля:

  • Полное понимание каждого шага.
  • Возможность экспериментировать с нестандартными архитектурами.
  • Отсутствие зависимости от внешних библиотек (кроме NumPy).

Недостатки:

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

Когда стоит писать с нуля:

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

Когда лучше использовать фреймворки:

  • Для промышленных проектов.
  • Для работы с большими объёмами данных.
  • Для использования современных архитектур (свёрточные, рекуррентные сети).

В любом случае, понимание внутреннего устройства нейросетей, полученное при написании с нуля, поможет эффективнее работать с любыми библиотеками.

Пример простой нейросети в 9 строк кода

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

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)))
    error = training_set_outputs - output
    adjustment = dot(training_set_inputs.T, error * output * (1 - output))
    synaptic_weights += adjustment

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

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

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

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

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

При наличии базовых знаний Python и понимания математики (умножение матриц, производные) написание простой сети с нуля может занять от 30 минут до нескольких часов. Всё зависит от сложности архитектуры и объёма данных.

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

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

Почему сигмоида часто используется как функция активации?

Сигмоида преобразует любое число в диапазон от 0 до 1, что удобно для интерпретации выхода как вероятности. Кроме того, её производная легко вычисляется через саму функцию, что упрощает обратное распространение ошибки.

Что такое переобучение и как его избежать?

Переобучение — это ситуация, когда сеть слишком хорошо запоминает обучающие данные, но плохо обобщает на новые. Чтобы избежать этого, используют регуляризацию, dropout, раннюю остановку (early stopping) и увеличение объёма данных.

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

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

Как выбрать количество скрытых слоёв и нейронов?

Универсального правила нет. Обычно начинают с одного скрытого слоя и постепенно увеличивают количество нейронов, пока качество на валидационной выборке не перестанет улучшаться. Для простых задач достаточно 1–2 скрытых слоёв.

Что делать, если сеть не обучается (ошибка не уменьшается)?

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