На создание этой статьи меня вдохновил Андрей Карпати. Я настоятельно рекомендую просмотреть приведенный ниже плейлист.

Так как это самое пошаговое объяснение 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 становится все ближе и ближе к фактическому значению после каждой итерации.

😁 Понравилась история? 💬 Дайте мне знать в комментариях и поставьте 👏!! Поделись с друзьями 👯!! Это требует много времени и усилий, поэтому отзывы очень ценятся! ❤️