Что такое нейросеть и зачем её писать с нуля
Нейросеть — это математическая модель, вдохновлённая устройством биологических нейронов. Она состоит из искусственных нейронов, объединённых в слои: входной, скрытые и выходной. На входной слой подаются данные, скрытые слои преобразуют их, а выходной слой выдаёт результат. Каждый нейрон связан с другими через входы, у которых есть веса — числовые коэффициенты, определяющие силу влияния одного нейрона на другой.
Писать нейросеть с нуля на 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-learnNumPy — фундамент для работы с многомерными массивами и матричными операциями. 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, но они не обязательны для первой нейросети.
Что такое обратное распространение ошибки простыми словами?
Это алгоритм, который вычисляет, насколько каждый вес влияет на итоговую ошибку, и корректирует его в сторону уменьшения. Сначала сеть делает предсказание, затем сравнивает его с правильным ответом, и ошибка «распространяется» назад от выходного слоя к входному, обновляя веса. Этот процесс повторяется много раз, пока ошибка не станет минимальной.