lab
Neural Network
Русский

Создавайте и обучайте нейронные сети с нуля

Обзор

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


Чтобы быть более интуитивным и понятным, мы следуем следующим принципам:

  1. Не используйте сторонние библиотеки для упрощения логики;
  2. Отсутствие оптимизации производительности: избегайте введения дополнительных концепций и методов, увеличивающих сложность;

Набор данных

Во-первых, нам нужен набор данных. Чтобы облегчить визуализацию, мы используем двоичную функцию в качестве целевой функции, а затем генерируем набор данных путем выборки на нем.


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

Целевая функция

o(x,y)={1x2+y2<10иначеo(x, y) = \begin{cases} 1 & x^2 + y^2 <1 \\ 0 & \text{иначе}\end{cases}

Код показан ниже:

def o(x, y): return 1.0 if x*x + y*y < 1 else 0.0

Создать набор данных

sample_density = 10 xs = [ [-2.0 + 4 * x/sample_density, -2.0 + 4 * y/sample_density] for x in range(sample_density+1) for y in range(sample_density+1) ] dataset = [ (x, y, o(x, y)) for x, y in xs ]

Созданный набор данных: [[-2.0, -2.0, 0.0], [-2.0, -1.6, 0.0], ...]

Изображение набора данных

Построить нейронную сеть

Функция активации

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

Нейрон

from random import seed, random seed(0) class Neuron: def __init__(self, num_inputs): self.weights = [random()-0.5 for _ in range(num_inputs)] self.bias = 0.0 def forward(self, inputs): # z = wx + b z = sum([ i * w for i, w in zip(inputs, self.weights) ]) + self.bias return sigmoid(z)

Выражение нейрона:

sigmoid(wx+b)\text{sigmoid}(\mathbf w \mathbf x + b)
  • w\mathbf w: вектор, соответствующий массиву весов в коде
  • bb: соответствует смещению в коде

Примечание: параметры нейрона инициализируются случайным образом. Однако для обеспечения воспроизводимости экспериментов устанавливается случайное начальное число (seed(0))

Нейронные сети

class MyNet: def __init__(self, num_inputs, hidden_shapes): layer_shapes = hidden_shapes + [1] input_shapes = [num_inputs] + hidden_shapes self.layers = [ [ Neuron(pre_layer_size) for _ in range(layer_size) ] for layer_size, pre_layer_size in zip(layer_shapes, input_shapes) ] def forward(self, inputs): for layer in self.layers: inputs = [ neuron.forward(inputs) for neuron in layer ] # return the output of the last neuron return inputs[0]

Постройте нейронную сеть следующим образом:

net = MyNet(2, [4])

На данный момент у нас есть нейронная сеть (сеть), которая может вызывать свою функцию нейронной сети:

print(net.forward([0, 0]))

Получите значение функции 0,55 ..., нейронная сеть на данный момент является необученной сетью.

Исходное изображение функции нейронной сети

Обучаем нейронную сеть

Функция потерь

Сначала определите функцию потерь:

def square_loss(predict, target): return (predict-target)**2

Рассчитать градиент

Расчет градиента сложен, особенно для глубоких нейронных сетей. Алгоритм обратного распространения - это алгоритм, специально разработанный для расчета градиента нейронной сети.


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


Определите производную функцию:

def sigmoid_derivative(x): _output = sigmoid(x) return _output * (1 - _output) def square_loss_derivative(predict, target): return 2 * (predict-target)

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

class Neuron: ... def forward(self, inputs): self.inputs_cache = inputs # z = wx + b self.z_cache = sum([ i * w for i, w in zip(inputs, self.weights) ]) + self.bias return sigmoid(self.z_cache) def zero_grad(self): self.d_weights = [0.0 for w in self.weights] self.d_bias = 0.0 def backward(self, d_a): d_loss_z = d_a * sigmoid_derivative(self.z_cache) self.d_bias += d_loss_z for i in range(len(self.inputs_cache)): self.d_weights[i] += d_loss_z * self.inputs_cache[i] return [d_loss_z * w for w in self.weights] class MyNet: ... def zero_grad(self): for layer in self.layers: for neuron in layer: neuron.zero_grad() def backward(self, d_loss): d_as = [d_loss] for layer in reversed(self.layers): da_list = [ neuron.backward(d_a) for neuron, d_a in zip(layer, d_as) ] d_as = [sum(da) for da in zip(*da_list)]
  • Частные производные хранятся в d_weights и d_bias соответственно
  • Функция zero_grad используется для очистки градиента, включая каждую частную производную
  • Функция backward используется для вычисления частной производной и накопления ее значения.

Обновить параметры

Используйте метод градиентного спуска для обновления параметров:

class Neuron: ... def update_params(self, learning_rate): self.bias -= learning_rate * self.d_bias for i in range(len(self.weights)): self.weights[i] -= learning_rate * self.d_weights[i] class MyNet: ... def update_params(self, learning_rate): for layer in self.layers: for neuron in layer: neuron.update_params(learning_rate)

Выполните обучение

def one_step(learning_rate): net.zero_grad() loss = 0.0 num_samples = len(dataset) for x, y, z in dataset: predict = net.forward([x, y]) loss += square_loss(predict, z) net.backward(square_loss_derivative(predict, z) / num_samples) net.update_params(learning_rate) return loss / num_samples def train(epoch, learning_rate): for i in range(epoch): loss = one_step(learning_rate) if i == 0 or (i+1) % 100 == 0: print(f"{i+1} {loss:.4f}")

Обучение 2000 шагов:

train(2000, learning_rate=10)

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

Изображение функции нейросети после обучения
log y\text{log y}
Кривая потерь

Вывод

После обучения модель можно использовать для вывода:

def inference(x, y): return net.forward([x, y]) print(inference(1, 2))

См. Полный код: nn_from_scratch.py

Резюме

Шаги этой практики следующие:

  1. Постройте виртуальную целевую функцию: o(x,y)o(x, y);
  2. Выборка на o(x,y)o(x, y) для получения набора данных, то есть функции набора данных: d(x,y)d(x, y)
  3. Построена полносвязная нейронная сеть со скрытым слоем, то есть функция нейронной сети: f(x,y)f(x, y)
  4. Используйте метод градиентного спуска для обучения нейронной сети так, чтобы f(x,y)f(x, y) приближалось d(x,y)d(x, y)

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


В лабораторных экспериментах 3D классификация второй набор данных очень похож на тот, который используется в этой практике, так что вы можете войти и использовать его.