
Привет, Хабр!
Алгоритм backpropagation, или обратное распространение ошибки, является некой базой для тренировки многослойных перцептронов и других типов искусственных нейронных сетей. Этот алгоритм впервые был предложен Полем Вербосом в 1974 году, а позже популяризирован Дэвидом Румельхартом, Джеффри Хинтоном и Рональдом Уильямсом в 1986 году.
В этой статье реализуем алгоритм на Питоне.
Немного про алгоритм
Backpropagation состоит из двух основных фаз: forward propagation (прямое распространение) и backward propagation (обратное распространение ошибки).
-
Прямое распространение:
-
На этом этапе входные данные проходят через все слои нейронной сети, вычисляя выходные значения для каждого нейрона. Каждый нейрон активируется с помощью активационной функции, такой как сигмоида или ReLU.
-
Результат прямого распространения — предсказанные значения (выходы сети), которые затем сравниваются с истинными значениями (метками) для вычисления ошибки.
-
-
Обратное распространение ошибки:
-
На этом этапе вычисляется градиент ошибки относительно каждого веса сети, начиная с выходного слоя и двигаясь обратно к входному.
-
Используя метод градиентного спуска, веса корректируются таким образом, чтобы минимизировать ошибку. Градиенты рассчитываются с помощью производных функций активации, что позволяет определить, как небольшие изменения в весах повлияют на итоговую ошибку.
-
Backpropagation позволяет нейронным сетям обучаться на данных, корректируя свои внутренние параметры для улучшения точности предсказаний.
Реализация алгоритма на Python
Инициализируем нейронную сеть
Создадим основные классы, которые будут представлять нейроны, слои и сеть. Основная идея состоит в том, чтобы каждый слой содержал нейроны, а сеть состояла из нескольких слоев.
Класс Neuron будет содержать параметры каждого нейрона, включая его веса и функцию активации:
import numpy as np class Neuron: def __init__(self, input_size): self.weights = np.random.randn(input_size + 1) * 0.1 # +1 для смещения (bias) def activate(self, inputs): z = np.dot(inputs, self.weights[:-1]) + self.weights[-1] # линейная комбинация входов и весов return self.sigmoid(z) def sigmoid(self, x): return 1 / (1 + np.exp(-x)) def sigmoid_derivative(self, x): return x * (1 - x)
Класс Layer будет содержать несколько нейронов и методы для активации всех нейронов в слое:
class Layer: def __init__(self, num_neurons, input_size): self.neurons = [Neuron(input_size) for _ in range(num_neurons)] def forward(self, inputs): return np.array([neuron.activate(inputs) for neuron in self.neurons])
Класс Network будет содержать несколько слоев и методы для прямого и обратного распространения:
class Network: def __init__(self, layers): self.layers = layers def forward(self, X): for layer in self.layers: X = layer.forward(X) return X def backward(self, X, y, output, learning_rate): # обратное распространение ошибки (подробности ниже) pass
Далии создадим слои и определим количество нейронов в каждом слое. Для этого можно использовать список, где каждый элемент указывает на слой и количество нейронов в нем:
# Параметры сети input_size = 3 # количество входов hidden_size = 5 # количество нейронов в скрытом слое output_size = 1 # количество выходов # Создание слоев layer1 = Layer(hidden_size, input_size) layer2 = Layer(output_size, hidden_size) # Создание сети network = Network([layer1, layer2]) # Пример входных данных X = np.array([0.5, 0.1, 0.4]) output = network.forward(X) print("Output:", output)
Создали два слоя сети: скрытый и выходой, а также процесс прямого распространения сигнала через сеть.
Добавим прямое распространение
В каждом нейроне входной сигнал сначала умножается на веса, затем суммируется и добавляется смещение. Затем результат передается через активационную функцию, которая нелинейно преобразует его для дальнейшего использования в сети.
Существует несколько активационных функций, каждая из которых имеет свои преимущества, добавим три основные функции: сигмоида, tanh и ReLU.
-
Сигмоида
Сигмоида преобразует входное значение в диапазон от 0 до 1. Их юзают в выходных слоях бинарных классификаторов:
def sigmoid(x): return 1 / (1 + np.exp(-x)) def sigmoid_derivative(x): return x * (1 - x) -
Tanh
Гиперболический тангенс преобразует входное значение в диапазон от -1 до 1. Её юзают в скрытых слоях, т.к обычно имеет лучшее центральное распределение значений по сравнению с сигмоидой:
def tanh(x): return np.tanh(x) def tanh_derivative(x): return 1 - np.tanh(x)**2 -
ReLU
ReLU — это наиболее часто используемая функция активации. Она преобразует отрицательные входные значения в ноль, оставляя положительные значения без изменений:
def relu(x): return np.maximum(0, x) def relu_derivative(x): return np.where(x > 0, 1, 0)
Для каждого нейрона вычисляется линейная комбинация входных данных и весов, к которой добавляется смещение. Затем результат передается через активационную функцию. Этот процесс повторяется для всех нейронов в каждом слое сети.
import numpy as np class Neuron: def __init__(self, input_size, activation='sigmoid'): self.weights = np.random.randn(input_size + 1) * 0.1 # +1 для смещения (bias) self.activation_function = self._get_activation_function(activation) self.activation_derivative = self._get_activation_derivative(activation) def _get_activation_function(self, activation): if activation == 'sigmoid': return lambda x: 1 / (1 + np.exp(-x)) elif activation == 'tanh': return np.tanh elif activation == 'relu': return lambda x: np.maximum(0, x) def _get_activation_derivative(self, activation): if activation == 'sigmoid': return lambda x: x * (1 - x) elif activation == 'tanh': return lambda x: 1 - np.tanh(x)**2 elif activation == 'relu': return lambda x: np.where(x > 0, 1, 0) def activate(self, inputs): z = np.dot(inputs, self.weights[:-1]) + self.weights[-1] return self.activation_function(z)
Класс Layer остается практически таким же, но теперь он может принимать разные функции активации для разных слоев.
class Layer: def __init__(self, num_neurons, input_size, activation='sigmoid'): self.neurons = [Neuron(input_size, activation) for _ in range(num_neurons)] def forward(self, inputs): return np.array([neuron.activate(inputs) for neuron in self.neurons])
В классе Network реализуем метод forward, который проходит через все слои сети:
class Network: def __init__(self, layers): self.layers = layers def forward(self, X): for layer in self.layers: X = layer.forward(X) return X
Пример использования:
# параметры сети input_size = 3 hidden_size = 5 output_size = 1 # создание слоев layer1 = Layer(hidden_size, input_size, activation='relu') layer2 = Layer(output_size, hidden_size, activation='sigmoid') # создание сети network = Network([layer1, layer2]) # пример входных данных X = np.array([0.5, 0.1, 0.4]) output = network.forward(X) print("Output:", output)
Создали нейронную сеть с двумя слоями и различными функциями активации, а также процесс прямого распространения сигнала через сеть.
Реализуем обратное распространение
Обратное распространение ошибки состоит из нескольких этапов:
-
Вычисление ошибки на выходном слое.
-
Передача ошибки обратно через слои сети.
-
Обновление весов на основе вычисленных градиентов.
Ошибка на выходном слое рассчитывается как разница между предсказанными значениями и истинными значениями Для этого часто используют функцию потерь, например, MSE.
def mean_squared_error(y_true, y_pred): return np.mean((y_true - y_pred) ** 2)
На этом этапе будем использовать производные активационных функций для расчета градиентов. Рассмотрим пример для функции сигмоида:
def sigmoid_derivative(x): return x * (1 - x)
Для каждого нейрона в сети вычисляется ошибка и градиент. В обратном порядке (от выходного слоя к входному) обновляются веса нейронов.
Обновление весов выполняется с использованием градиентного спуска. Веса корректируются на величину, пропорциональную градиенту и скорости обучения:
learning_rate = 0.1 def update_weights(weights, gradients): return weights - learning_rate * gradients
Расширим ранее созданный класс Network, добавив метод backward, который будет выполнять обратное распространение ошибки и обновление весов:
class Network: def __init__(self, layers): self.layers = layers def forward(self, X): for layer in self.layers: X = layer.forward(X) return X def backward(self, X, y, learning_rate): output = self.forward(X) error = y - output for i in reversed(range(len(self.layers))): layer = self.layers[i] if i == len(self.layers) - 1: layer.error = error layer.delta = layer.error * sigmoid_derivative(output) else: next_layer = self.layers[i + 1] layer.error = np.dot(next_layer.delta, np.array([neuron.weights for neuron in next_layer.neurons])) layer.delta = layer.error * sigmoid_derivative(layer.output) for i in range(len(self.layers)): layer = self.layers[i] inputs = X if i == 0 else self.layers[i - 1].output for neuron in layer.neurons: for j in range(len(neuron.weights) - 1): neuron.weights[j] += learning_rate * layer.delta[j] * inputs[j] neuron.weights[-1] += learning_rate * layer.delta[-1] class Neuron: def __init__(self, input_size, activation='sigmoid'): self.weights = np.random.randn(input_size + 1) * 0.1 # +1 для смещения (bias) self.activation_function = self._get_activation_function(activation) self.activation_derivative = self._get_activation_derivative(activation) def _get_activation_function(self, activation): if activation == 'sigmoid': return lambda x: 1 / (1 + np.exp(-x)) elif activation == 'tanh': return np.tanh elif activation == 'relu': return lambda x: np.maximum(0, x) def _get_activation_derivative(self, activation): if activation == 'sigmoid': return lambda x: x * (1 - x) elif activation == 'tanh': return lambda x: 1 - np.tanh(x)**2 elif activation == 'relu': return lambda x: np.where(x > 0, 1, 0) def activate(self, inputs): z = np.dot(inputs, self.weights[:-1]) + self.weights[-1] self.output = self.activation_function(z) return self.output class Layer: def __init__(self, num_neurons, input_size, activation='sigmoid'): self.neurons = [Neuron(input_size, activation) for _ in range(num_neurons)] def forward(self, inputs): self.output = np.array([neuron.activate(inputs) for neuron in self.neurons]) return self.output
Метод backward выполняет следующее:
-
Вычисляет ошибку на выходном слое.
-
Передает ошибку обратно через слои.
-
Обновляет веса каждого нейрона с учетом вычисленных градиентов.
Итак, в итоге мы получили такой код:
Полный код
import numpy as np # активационные функции и их производные def sigmoid(x): return 1 / (1 + np.exp(-x)) def sigmoid_derivative(x): return x * (1 - x) def tanh(x): return np.tanh(x) def tanh_derivative(x): return 1 - np.tanh(x)**2 def relu(x): return np.maximum(0, x) def relu_derivative(x): return np.where(x > 0, 1, 0) # класс Neuron class Neuron: def __init__(self, input_size, activation='sigmoid'): self.weights = np.random.randn(input_size + 1) * 0.1 # +1 для смещения (bias) self.activation_function = self._get_activation_function(activation) self.activation_derivative = self._get_activation_derivative(activation) self.output = None def _get_activation_function(self, activation): if activation == 'sigmoid': return sigmoid elif activation == 'tanh': return tanh elif activation == 'relu': return relu def _get_activation_derivative(self, activation): if activation == 'sigmoid': return sigmoid_derivative elif activation == 'tanh': return tanh_derivative elif activation == 'relu': return relu_derivative def activate(self, inputs): z = np.dot(inputs, self.weights[:-1]) + self.weights[-1] self.output = self.activation_function(z) return self.output # класс Layer class Layer: def __init__(self, num_neurons, input_size, activation='sigmoid'): self.neurons = [Neuron(input_size, activation) for _ in range(num_neurons)] self.output = None self.error = None self.delta = None def forward(self, inputs): self.output = np.array([neuron.activate(inputs) for neuron in self.neurons]) return self.output # класс Network class Network: def __init__(self, layers): self.layers = layers def forward(self, X): for layer in self.layers: X = layer.forward(X) return X def backward(self, X, y, learning_rate): # прямое распространение для получения выходных значений output = self.forward(X) # вычисление ошибки на выходном слое error = y - output self.layers[-1].error = error self.layers[-1].delta = error * self.layers[-1].neurons[0].activation_derivative(output) # передача ошибки обратно через слои for i in reversed(range(len(self.layers) - 1)): layer = self.layers[i] next_layer = self.layers[i + 1] layer.error = np.dot(next_layer.delta, np.array([neuron.weights[:-1] for neuron in next_layer.neurons])) layer.delta = layer.error * np.array([neuron.activation_derivative(neuron.output) for neuron in layer.neurons]) # обновление весов for i in range(len(self.layers)): layer = self.layers[i] inputs = X if i == 0 else self.layers[i - 1].output for j, neuron in enumerate(layer.neurons): for k in range(len(neuron.weights) - 1): neuron.weights[k] += learning_rate * layer.delta[j] * inputs[k] neuron.weights[-1] += learning_rate * layer.delta[j] # Обновление смещения def train(self, X, y, learning_rate, epochs): for epoch in range(epochs): for xi, yi in zip(X, y): self.backward(xi, yi, learning_rate) # создание и обучение сети input_size = 3 hidden_size = 5 output_size = 1 layer1 = Layer(hidden_size, input_size, activation='relu') layer2 = Layer(output_size, hidden_size, activation='sigmoid') network = Network([layer1, layer2]) # пример данных для тренировки X = np.array([[0.5, 0.1, 0.4], [0.9, 0.7, 0.3], [0.2, 0.8, 0.6]]) y = np.array([[1], [0], [1]]) # параметры обучения learning_rate = 0.1 epochs = 10000 # обучение сети network.train(X, y, learning_rate, epochs) # тестирование сети for xi in X: output = network.forward(xi) print("Input:", xi, "Output:", output)
Для успешного применения backpropagation в продакшене необходимо учитывать несколько моментов:
-
Выбор правильной функции активации: каждая функция активации имеет свои преимущества и недостатки в зависимости от задачи.
-
Настройка гиперпараметров: скорость обучения, количество эпох и другие параметры очень влияют на производительность модели.
-
Обработка данных: качественная подготовка и нормализация данных могут значительно улучшить результаты модели.
В завершение хочу порекомендовать бесплатный вебинар, в рамках которого эксперты OTUS расскажут про задачу поиска аномалий и про то, как с помощью методов ML можно очищать данные от выбросов. Регистрация на вебинар доступна по ссылке.
ссылка на оригинал статьи https://habr.com/ru/articles/816667/
Добавить комментарий