Что такое нейросеть и зачем её писать самому
Нейронная сеть — это математическая модель, вдохновлённая устройством биологических нейронов. Она состоит из множества связанных между собой искусственных нейронов, которые преобразуют входные данные в выходные. Простыми словами, это «чёрный ящик», который учится отображать пространство входных признаков в пространство выходных значений.
Написание нейросети с нуля — лучший способ понять, как работают современные библиотеки вроде 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) — это процесс, при котором входные данные проходят через все слои сети и превращаются в выходной сигнал.
Алгоритм:
- Умножаем входной вектор X на матрицу весов между входным и скрытым слоем (weights1).
- Применяем функцию активации (сигмоиду) к полученной сумме — получаем выход скрытого слоя.
- Умножаем выход скрытого слоя на матрицу весов между скрытым и выходным слоем (weights2).
- Снова применяем сигмоиду — получаем финальное предсказание.
Код:
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) — ключевой механизм обучения нейросети. Он отвечает на вопрос: «Как каждый вес повлиял на итоговую ошибку?»
Алгоритм:
- Вычисляем ошибку на выходе: output_error = y_true - y_pred.
- Находим «дельту» для выходного слоя: output_delta = output_error * sigmoid_derivative(predicted_output).
- Распространяем ошибку на скрытый слой: hidden_error = output_delta.dot(weights2.T).
- Вычисляем дельту для скрытого слоя: hidden_delta = hidden_error * sigmoid_derivative(hidden_output).
- Обновляем веса: 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, weights2Learning rate — скорость обучения. Если она слишком велика, сеть будет «перепрыгивать» оптимум. Если слишком мала — обучение затянется. Обычно выбирают значения от 0.01 до 0.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.
Практические советы и типичные ошибки новичков
При написании первой нейросети часто допускают следующие ошибки:
- Забывают про нормализацию данных. Если признаки имеют разный масштаб, сеть может обучаться медленно или вообще не сойтись.
- Неправильно выбирают learning rate. Слишком большой — расходится, слишком маленький — не учится. Начинайте с 0.01 и корректируйте.
- Путают функции активации. Для скрытых слоёв лучше использовать ReLU, для выходного — сигмоиду или softmax в зависимости от задачи.
- Не проверяют размерности матриц. Ошибка в shape — одна из самых частых причин неработающего кода.
- Обучают слишком мало эпох. Для 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.