На создание этой статьи меня вдохновил Андрей Карпати. Я настоятельно рекомендую просмотреть приведенный ниже плейлист.
Так как это самое пошаговое объяснение Back Propagation и обучения нейронных сетей.
Обратное распространение:
- Это метод расчета градиента функции потерь по отношению к весам нейронной сети.
- Обратное распространение – это способ вычисления градиентов выражений посредством рекурсивного применения цепного правила.
- Что оно делает ? Он используется для точной настройки веса нейронных сетей, что, в свою очередь, снижает потери.
- Концепция производной очень важна в обратном распространении.
Производная:
- Это означает наклон функции по отношению к некоторой переменной.
- С точки зрения непрофессионала, производная измеряет эффект увеличения переменной на очень маленькое значение (например, 0,001), что приводит к увеличению или уменьшению значения функции.
- Если наклон положительный, это означает, что переменная оказывает положительное (возрастающее) влияние на результирующую функцию.
- Если наклон отрицательный, это означает, что переменная оказывает отрицательное (убывающее) влияние на результирующую функцию.
- В случае переменных обратного распространения будут веса и смещения, а функция будет функцией потерь.
- Короче говоря, он определяет влияние весов на функцию потерь, что, в свою очередь, помогает точно настроить веса таким образом, чтобы уменьшить результат функции потерь.
def f(x): return 3*(x**2)-(4*x)+5 # Derivative with respect to x is given by df_dx = (f(x+h)-f(x))/h
Нейронные сети:
- Персептрон — это модуль нейронной сети, который выполняет определенные вычисления для обнаружения признаков.
- Perceptron вдохновлен биологическими нейронами, которые моделируют математические операции.
- х- входы; w- веса; б-смещение; сигма — функция активации.
- Теперь мы будем шаг за шагом повторять работу персептрона.
- Также мы увидим, какой класс содержит Value в следующем разделе.
w1=Value(2.13,label='w1') x1=Value(3,label='x1') w2=Value(3,label='w2') x2=Value(1,label='x2') b = Value(5);b.label='b' ##modeling perceptron w1x1=w1*x1 ;w1x1.label='w1*x1' w2x2=w2*x2 ;w2x2.label='w2*x2' w1x1w2x2 = w1x1+w2x2 ; w1x1w2x2.label='(w1*x1)+(w2*x2)' z = w1x1w2x2+b ; n.label='z' y = n.relu() ; y.label='y'
- Визуальное представление приведенного выше кода.
Обратное распространение (на одном нейроне):
- Теперь мы собираемся обратно распространяться по нейрону.
- В типичной настройке нейронной сети мы действительно фокусируемся на вычислении производной по весам... потому что мы фокусируемся на изменении весов как части оптимизации.
- Также для простоты мы берем один нейрон, но в нейронных сетях у нас было бы много нейронов, связанных друг с другом.
- В конце концов появится функция потерь, которая измеряет потери нейронной сети, и мы будем использовать обратное распространение по отношению к функции потерь, чтобы уменьшить ее.
- Суть обратного распространения — цепное правило производной.
- Пример цепного правила: если автомобиль движется в два раза быстрее, чем велосипед, а велосипед в 4 раза быстрее, чем идущий человек, как мы можем сравнить скорость автомобиля со скоростью идущего человека? Это было бы в 4x2 = 8 раз быстрее по сравнению с идущим человеком.
- Пример цепного правила можно увидеть ниже:
# set some inputs x = -2; y = 5; z = -4 # perform the forward pass q = x + y # q becomes 3 f = q * z # f becomes -12 # perform the backward pass (backpropagation) in reverse order: # first backprop through f = q * z dfdz = q # df/dz = q, so gradient on z becomes 3 dfdq = z # df/dq = z, so gradient on q becomes -4 dqdx = 1.0 dqdy = 1.0 # now backprop through q = x + y dfdx = dfdq * dqdx # The multiplication here is the chain rule! dfdy = dfdq * dqdy
- Теперь мы собираемся создать класс с именем value, который будет хранить одно скалярное значение и его градиент (наклон относительно функции потерь).
class Value: """ stores a single scalar value and its gradient """ def __init__(self, data, _children=(), _op='',label=''): self.data = data self.grad = 0 # internal variables used for autograd graph construction self._backward = lambda: None self._prev = set(_children) self._op = _op # the op that produced this node, for graphviz / debugging / etc self.label='' def __add__(self, other): other = other if isinstance(other, Value) else Value(other) out = Value(self.data + other.data, (self, other), '+') def _backward(): self.grad += out.grad other.grad += out.grad out._backward = _backward return out def __sub__(self, other): other = other if isinstance(other, Value) else Value(other) out = Value(self.data - other.data, (self, other), '-') def _backward(): self.grad += out.grad other.grad += out.grad out._backward = _backward return out def __mul__(self, other): other = other if isinstance(other, Value) else Value(other) out = Value(self.data * other.data, (self, other), '*') def _backward(): self.grad += other.data * out.grad other.grad += self.data * out.grad out._backward = _backward return out def __pow__(self, other): assert isinstance(other, (int, float)), "only supporting int/float powers for now" out = Value(self.data**other, (self,), f'**{other}') def _backward(): self.grad += (other * self.data**(other-1)) * out.grad out._backward = _backward return out def relu(self): out = Value(0 if self.data < 0 else self.data, (self,), 'ReLU') def _backward(): self.grad += (out.data > 0) * out.grad out._backward = _backward return out def backward(self): # topological order all of the children in the graph topo = [] visited = set() def build_topo(v): if v not in visited: visited.add(v) for child in v._prev: build_topo(child) topo.append(v) build_topo(self) # go one variable at a time and apply the chain rule to get its gradient self.grad = 1 for v in reversed(topo): v._backward()
- class значение также отслеживает дочерние узлы, на которых мы выполняем обратное топологическое упорядочение.
- Из этого обратного топологического порядка дочерних узлов мы выполняем обратное распространение и вычисляем его градиент относительно функции потерь.
- При этом мы собираемся добавить узлы математических операций, используя магические методы.
- Все эти узлы математических операций имеют собственный шаблон вычисления и обновления градиента своих дочерних узлов.
Паттерны обратного потока:
- Элемент добавления всегда берет градиент на своем выходе и равномерно распределяет его по всем своим входам, независимо от того, какими были их значения во время прямого прохода.
self.grad += out.grad other.grad += out.grad # out is o/p node gradient wrt to loss # self and other are inputs for which slope have to calculated wrt to loss
- В multiply gate его локальные градиенты являются входными значениями (кроме переключаемых), и они умножаются на градиент на его выходе во время цепного правила.
self.grad += other.data * out.grad other.grad += self.data * out.grad # here we can observe that data of i/p values are swtiched
- relu gate направляет градиент. В отличие от вентиля добавления, который распределял градиент без изменений на все свои входы, вентиль relu распределяет градиент (без изменений), если выход этого узла больше 0.
self.grad += (out.data > 0) * out.grad
Функция потерь и обновление весов для уменьшения потерь:
- Теперь мы знаем, как выполнить обратное распространение и вычислить градиент по отношению к функции потерь.
- Мы собираемся создать функцию, которая будет вычислять потери по отношению к фактическому значению из прогнозируемого значения.
def cal_loss(actual_val): """ Forward pass""" x1=Value(3,label='x1') x2=Value(1,label='x2') w1x1=w1*x1 ;w1x1.label='w1*x1' w2x2=w2*x2 ;w2x2.label='w2*x2' w1x1w2x2 = w1x1+w2x2 ; w1x1w2x2.label='(w1*x1)+(w2*x2)' z = w1x1w2x2+b ;z.label='z' ypred = z.relu() ; ypred.label='y' # Calculating the losses l = (ypred-actual_val)**2; l.label='loss' print(ypred.data) return l
- Для расчета потерь, во-первых, мы должны выполнить прямой проход.
- После прямого прохода мы получаем предсказанное значение, из которого будем рассчитывать потери.
Корректировка весов для уменьшения потерь:
- После расчета потерь мы проведем обратное распространение и рассчитаем степень влияния каждого веса по отношению к функции потерь.
actual_val=1000 for i in range(10): # number of iteration l= cal_loss(actual_val) # we will recalculate losses w.r.t new weights l.backward() #we will calculate the gradient w.r.t loss func wit new weights w1.data=w1.data-0.01*w1.grad # updation of weights to decrease the losses w2.data=w2.data-0.01*w2.grad #>>> ypred getting updated for each iteration of forward pass and backward pass ypred=14.39 211.512 369.16960000000006 495.29568000000006 596.196544 676.9172352000002 741.4937881600001 793.1550305280001 834.4840244224 867.54721953792
- Теперь мы собираемся обновить веса, чтобы потери уменьшились, а значения прогноза соответствовали фактическому значению.
- Мы можем наблюдать, что прогноз y становится все ближе и ближе к фактическому значению после каждой итерации.
😁 Понравилась история? 💬 Дайте мне знать в комментариях и поставьте 👏!! Поделись с друзьями 👯!! Это требует много времени и усилий, поэтому отзывы очень ценятся! ❤️