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

Как написать простейшую нейросеть на Python с нуля: теория, код, обучение на примерах OR и XOR, сравнение подходов и частые ошибки.

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

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

Писать нейросеть с нуля на Python полезно не ради продакшена, а ради понимания. Когда вы реализуете прямое распространение и обратное распространение ошибки вручную, вы перестаёте воспринимать библиотеки как «чёрный ящик». Это особенно важно для начинающих: после такой практики TensorFlow или PyTorch становятся инструментами, а не магией.

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

Подготовка среды: что нужно установить

Для запуска примеров понадобится Python версии 3.7 или новее. Рекомендуется создать виртуальное окружение, чтобы изолировать зависимости проекта:

python -m venv neural_net_env
# Windows
neural_net_env\Scripts\activate
# macOS/Linux
source neural_net_env/bin/activate

Затем установите базовые библиотеки:

pip install numpy matplotlib pandas scikit-learn

NumPy — фундамент для работы с многомерными массивами и матричными операциями. Matplotlib пригодится для визуализации процесса обучения, pandas — для обработки данных, а scikit-learn — для сравнения с готовыми алгоритмами. Однако для самой простой нейросети достаточно только NumPy, а в самом минимальном варианте можно обойтись и без него, используя встроенный модуль random.

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

Минимальная нейросеть в 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)))))

Разберём, что здесь происходит. Мы создаём массив входов (4 примера по 3 признака) и ожидаемые выходы. Веса инициализируются случайно, но с фиксированным зерном (random.seed(1)), чтобы результаты были воспроизводимы. Затем 10 000 раз повторяется цикл: вычисляется предсказание через сигмоиду, находится ошибка, и веса корректируются на величину, пропорциональную ошибке и градиенту сигмоиды.

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

Классовая реализация: структура сети

Для более сложных задач одного нейрона недостаточно. Например, операция XOR (исключающее ИЛИ) не решается линейно, поэтому нужен скрытый слой. Классовая реализация помогает организовать код и легко расширять архитектуру.

Создадим классы для сети, слоя, нейрона и входа:

from math import ceil
from random import randint

class NeuroNetwork:
    def __init__(self, input_l_size, output_l_size, hidden_layers_count=1):
        self.selected_layer = None
        self.l_count = hidden_layers_count + 2  # входной + скрытые + выходной
        hidden_l_size = min(input_l_size * 2 - 1, ceil(input_l_size * 2 / 3 + output_l_size))
        self.layers = [self.add_layer(i, input_l_size, output_l_size, hidden_l_size) for i in range(self.l_count)]
        self.selected_layer = None

    def add_layer(self, i, in_size, out_size, hl_size):
        count = i + 1
        if 1 < count < self.l_count:  # скрытый слой
            self.selected_layer = Layer(hl_size, self.selected_layer, self)
            return self.selected_layer
        if count == 1:  # входной слой
            self.selected_layer = Layer(in_size, None, self)
            return self.selected_layer
        # выходной слой
        self.selected_layer = Layer(out_size, self.selected_layer, self)
        return self.selected_layer

class Layer:
    def __init__(self, layer_size, prev_layer, parent_network):
        self.prev_layer = prev_layer
        self._network = parent_network
        self.neurons = [Neuron(self, prev_layer) for _ in range(layer_size)]

class Neuron:
    def __init__(self, layer, previous_layer):
        self._layer = layer
        if previous_layer:
            self.inputs = [Input(prev_neuron, randint(0, 10) / 10) for prev_neuron in previous_layer.neurons]
        else:
            self.inputs = []

class Input:
    def __init__(self, prev_neuron, weight):
        self.prev_neuron = prev_neuron
        self.weight = weight

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

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

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

В простейшем случае используется сигмоида:

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

Сигмоида преобразует любое число в диапазон от 0 до 1, что удобно для задач бинарной классификации. Её производная равна x * (1 - x), что упрощает вычисление градиентов.

Для класса NeuroNetwork метод прямого распространения может выглядеть так:

def forward(self, input_values):
    # Устанавливаем значения входных нейронов
    for neuron, value in zip(self.layers[0].neurons, input_values):
        neuron.value = value
    # Проходим по остальным слоям
    for layer in self.layers[1:]:
        for neuron in layer.neurons:
            total = sum(inp.prev_neuron.value * inp.weight for inp in neuron.inputs)
            neuron.value = sigmoid(total)
    return [neuron.value for neuron in self.layers[-1].neurons]

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

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

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

Формула корректировки веса в простейшем случае:

weight += input_value * error * sigmoid_derivative(output)

Здесь error — разница между ожидаемым и фактическим выходом, а sigmoid_derivative — производная сигмоиды, которая показывает, насколько «уверен» нейрон. Если нейрон выдал значение близкое к 0 или 1, градиент мал, и вес корректируется слабо.

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

def train(self, dataset, iters=1000):
    for _ in range(iters):
        for inputs, expected in dataset:
            output = self.forward(inputs)
            error = expected - output
            # Обновляем веса, начиная с последнего слоя
            for layer in reversed(self.layers[1:]):
                for neuron in layer.neurons:
                    for inp in neuron.inputs:
                        inp.weight += inp.prev_neuron.value * error * output * (1 - output)

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

Практический пример: обучение на операции OR

Логическая операция OR (ИЛИ) — классическая задача для первой нейросети. Таблица истинности:

| A | B | A OR B | |---|---|--------| | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | 1 | | 1 | 1 | 1 |

Для решения этой задачи достаточно сети с двумя входными нейронами, одним скрытым слоем (например, из трёх нейронов) и одним выходным нейроном. Используем нашу классовую реализацию:

nn = NeuroNetwork(input_l_size=2, output_l_size=1, hidden_layers_count=1)
dataset = [
    ([0, 0], [0]),
    ([0, 1], [1]),
    ([1, 0], [1]),
    ([1, 1], [1])
]
nn.train(dataset, iters=1000)
print(nn.forward([0, 0]))  # ~0.1
print(nn.forward([1, 1]))  # ~0.9

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

Сравнение с библиотеками: TensorFlow и PyTorch

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

Пример на TensorFlow/Keras для задачи XOR:

import tensorflow as tf
from tensorflow import keras
import numpy as np

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

model = keras.Sequential([
    keras.layers.Dense(4, input_dim=2, activation='relu'),
    keras.layers.Dense(1, activation='sigmoid')
])
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
model.fit(X, y, epochs=1000, verbose=0)
print(model.predict(X))

Аналогичный пример на PyTorch:

import torch
import torch.nn as nn
import torch.optim as optim

X = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=torch.float32)
y = torch.tensor([[0], [1], [1], [0]], dtype=torch.float32)

class XORModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer1 = nn.Linear(2, 4)
        self.layer2 = nn.Linear(4, 1)
        self.relu = nn.ReLU()
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        return self.sigmoid(self.layer2(self.relu(self.layer1(x))))

model = XORModel()
criterion = nn.BCELoss()
optimizer = optim.Adam(model.parameters(), lr=0.1)

for epoch in range(10000):
    optimizer.zero_grad()
    loss = criterion(model(X), y)
    loss.backward()
    optimizer.step()

TensorFlow предлагает более высокоуровневый API (Keras), что удобно для быстрого прототипирования. PyTorch ближе к стилю Python и NumPy, что облегчает отладку. Для исследований чаще выбирают PyTorch, для промышленного развёртывания — TensorFlow.

Частые ошибки и советы для начинающих

При написании первой нейросети легко наступить на типичные грабли. Вот несколько рекомендаций:

  • Не забывайте про нормализацию данных. Если входные значения сильно различаются по масштабу, обучение может быть нестабильным. Приводите данные к диапазону [0, 1] или [-1, 1].
  • Используйте фиксированное зерно случайности. Это позволит воспроизводить результаты и отлаживать код.
  • Следите за переобучением. Если сеть слишком точно запоминает тренировочные данные, она может плохо работать на новых. Используйте регуляризацию или увеличивайте объём данных.
  • Не выбирайте слишком большую скорость обучения. Большой шаг может привести к расходимости. Начните с 0.1 и уменьшайте при необходимости.
  • Проверяйте размерности матриц. Ошибки в умножении матриц — самая частая причина неработающего кода. Используйте print(shape) для отладки.
  • Начинайте с простых задач. OR, AND, XOR — идеальные полигоны. Не пытайтесь сразу решать распознавание изображений без понимания основ.

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

Что дальше: пути развития после первой нейросети

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

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

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

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

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

Для новичка с базовыми знаниями Python — от 30 минут до нескольких часов. Минимальная версия в 9 строк кода занимает около 15 минут, включая установку NumPy. Классовая реализация с обучением на OR или XOR потребует больше времени, но в рамках одного вечера вполне реально получить работающий пример.

Нужно ли знать математику для написания нейросети?

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

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

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

Чем отличается сигмоида от ReLU и когда какую использовать?

Сигмоида выдаёт значения от 0 до 1 и хороша для бинарной классификации, но страдает от затухания градиента при больших значениях. ReLU (rectified linear unit) выдаёт 0 для отрицательных входов и само значение для положительных, что ускоряет обучение и часто используется в скрытых слоях. Для выходного слоя при бинарной классификации обычно берут сигмоиду, для многоклассовой — softmax.

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

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

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

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

Что такое обратное распространение ошибки простыми словами?

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