Алгоритм Backpropagation на Python (2024)

Алгоритм Backpropagation на Python (1)

Привет, Хабр!

Алгоритм backpropagation, или обратное распространение ошибки, является некой базой для тренировки многослойных перцептронов и других типов искусственных нейронных сетей. Этот алгоритм впервые был предложен Полем Вербосом в 1974 году, а позже популяризирован Дэвидом Румельхартом, Джеффри Хинтоном и Рональдом Уильямсом в 1986 году.

В этой статье реализуем алгоритм на Питоне.

Немного про алгоритм

Backpropagation состоит из двух основных фаз: forward propagation (прямое распространение) и backward propagation (обратное распространение ошибки).

  1. Прямое распространение:

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

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

  2. Обратное распространение ошибки:

    • На этом этапе вычисляется градиент ошибки относительно каждого веса сети, начиная с выходного слоя и двигаясь обратно к входному.

    • Используя метод градиентного спуска, веса корректируются таким образом, чтобы минимизировать ошибку. Градиенты рассчитываются с помощью производных функций активации, что позволяет определить, как небольшие изменения в весах повлияют на итоговую ошибку.

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

Реализация алгоритма на Python

Инициализируем нейронную сеть

Создадим основные классы, которые будут представлять нейроны, слои и сеть. Основная идея состоит в том, чтобы каждый слой содержал нейроны, а сеть состояла из нескольких слоев.

Класс Neuron будет содержать параметры каждого нейрона, включая его веса и функцию активации:

import numpy as npclass 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.

  1. Сигмоида

    Сигмоида преобразует входное значение в диапазон от 0 до 1. Их юзают в выходных слоях бинарных классификаторов:

    def sigmoid(x): return 1 / (1 + np.exp(-x))def sigmoid_derivative(x): return x * (1 - x)
  2. Tanh

    Гиперболический тангенс преобразует входное значение в диапазон от -1 до 1. Её юзают в скрытых слоях, т.к обычно имеет лучшее центральное распределение значений по сравнению с сигмоидой:

    def tanh(x): return np.tanh(x)def tanh_derivative(x): return 1 - np.tanh(x)**2
  3. ReLU

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

    def relu(x): return np.maximum(0, x)def relu_derivative(x): return np.where(x > 0, 1, 0)

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

import numpy as npclass 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 = 3hidden_size = 5output_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)

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

Реализуем обратное распространение

Обратное распространение ошибки состоит из нескольких этапов:

  1. Вычисление ошибки на выходном слое.

  2. Передача ошибки обратно через слои сети.

  3. Обновление весов на основе вычисленных градиентов.

Ошибка на выходном слое рассчитывается как разница между предсказанными значениями и истинными значениями Для этого часто используют функцию потерь, например, 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.1def 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.outputclass 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 выполняет следующее:

  1. Вычисляет ошибку на выходном слое.

  2. Передает ошибку обратно через слои.

  3. Обновляет веса каждого нейрона с учетом вычисленных градиентов.

Итак, в итоге мы получили такой код:

Полный код
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)**2def relu(x): return np.maximum(0, x)def relu_derivative(x): return np.where(x > 0, 1, 0)# класс Neuronclass 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# класс Layerclass 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# класс Networkclass 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 = 3hidden_size = 5output_size = 1layer1 = 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.1epochs = 10000# обучение сетиnetwork.train(X, y, learning_rate, epochs)# тестирование сетиfor xi in X: output = network.forward(xi) print("Input:", xi, "Output:", output)

Для успешного применения backpropagation в продакшене необходимо учитывать несколько моментов:

  • Выбор правильной функции активации: каждая функция активации имеет свои преимущества и недостатки в зависимости от задачи.

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

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

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

Алгоритм Backpropagation на Python (2024)

References

Top Articles
Latest Posts
Article information

Author: Corie Satterfield

Last Updated:

Views: 5949

Rating: 4.1 / 5 (42 voted)

Reviews: 89% of readers found this page helpful

Author information

Name: Corie Satterfield

Birthday: 1992-08-19

Address: 850 Benjamin Bridge, Dickinsonchester, CO 68572-0542

Phone: +26813599986666

Job: Sales Manager

Hobby: Table tennis, Soapmaking, Flower arranging, amateur radio, Rock climbing, scrapbook, Horseback riding

Introduction: My name is Corie Satterfield, I am a fancy, perfect, spotless, quaint, fantastic, funny, lucky person who loves writing and wants to share my knowledge and understanding with you.