Введение

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

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

Выполнение

В этой статье мы расширим класс MyTensor из предыдущей статьи новыми функциями и, в конце, проверим результаты на фоне Pytorch как и в прошлый раз.

Умножение

Для умножения self * other градиент self равен значению other:

а градиент other равен значению self:

А вот код:

def __mul__(self, other):
  other = other if isinstance(other, MyTensor) else MyTensor(other)
  out = MyTensor(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 __rmul__(self, other): # other * self
  return self * other

Разделение

Для деления self / other градиент self равен:

а градиент other рассчитывается как:

Код снова прямолинеен:

def __truediv__(self, other): # self / other
  other = other if isinstance(other, MyTensor) else MyTensor(other)
  out = MyTensor(self.data / other.data, (self, other), '/')

  def _backward():
      self.grad += (1 / other.data) * out.grad
      other.grad += (-self.data / other.data ** 2) * out.grad
  out._backward = _backward

  return out

Возведение в степень

Для возведения в степень градиент рассчитывается как

А для кода имеем:

def __pow__(self, other):
  assert isinstance(other, (int, float)), "only supporting int/float powers for now"
  out = MyTensor(self.data**other, (self,), f'**{other}')

  def _backward():
      self.grad += other * (self.data ** (other - 1)) * out.grad
  out._backward = _backward

  return out

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

Разумный способ

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

def __truediv__(self, other): # self / other
  return self * other**-1

и для отрицания; представлено в предыдущей статье, но теперь, когда мы реализовали умножение, мы можем переписать MyTesnor(self.data * -1) в:

def __neg__(self): # -self
  return self * -1

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

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

ReLU (выпрямленная линейная единица) — одна из наиболее часто используемых функций активации, и она довольно проста. Часто ReLU обозначается как f(x) = max(0,x), но поскольку нас интересует вычисление его производной, мы будем использовать следующие обозначения:

и его производная:

Геометрически ReLU выглядит так:

А вот реализация кода (да, когда вы умножаете логическое значение на число, Python считает False as 0 и True as 1):

def relu(self):
  out = MyTensor(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 sigmoid(self):
  x = self.data
  t = 1 / (1 + math.exp(-x))
  out = MyTensor(t, (self, ), 'sigmoid')

  def _backward():
    self.grad += t * (1 - t) * out.grad
  out._backward = _backward

  return out

Гиперболический тангенс (tanh) — еще одна относительно популярная функция активации. Его можно рассчитать как:

а его производная определяется уравнением:

Геометрически это выглядит так:

Наконец, код tanh:

def tanh(self):
  x = self.data
  t = (math.exp(2*x) - 1)/(math.exp(2*x) + 1)
  out = MyTensor(t, (self, ), 'tanh')
  
  def _backward():
    self.grad += (1 - t**2) * out.grad
  out._backward = _backward
  
  return out

Модульное тестирование

Мы еще раз проверим нашу реализацию на автограде Pytorch. Никаких сюрпризов, как в первой статье.

# pytorch's implementation
w_t = torch.Tensor([-3.0]).double(); w_t.requires_grad=True
v_t = torch.Tensor([3.0]).double(); v_t.requires_grad=True
x_t = torch.Tensor([-1.0]).double(); x_t.requires_grad=True
u_t = torch.Tensor([4.0]).double(); u_t.requires_grad=True

raw_out_t = (x_t * w_t + v_t) / u_t
out_t = torch.sigmoid(raw_out_t)

# our implementation
w = MyTensor(-3.0, label='w')
v = MyTensor(3.0, label='v')
x = MyTensor(-1.0, label='x')
u = MyTensor(4.0, label='u')

raw_out = (x * w + v) / u
out = raw_out.sigmoid()

# confirm that gradients are identical
assert w.grad == w_t.grad.item()
assert v.grad == v_t.grad.item()
assert x.grad == x_t.grad.item()
assert u.grad == u_t.grad.item()

print('Test Passed!')

Вы можете найти код, использованный в этой статье, по адресу: Google Colab — Реализация обратного распространения с нуля — Часть 2

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

Источники:

  1. Прописанное введение в нейронные сети и обратное распространение: строим микроград
  2. АВТОГРАД МЕХАНИКА
  3. Нейронные сети: вывод сигмовидной производной с помощью цепных и частных правил

Раскрытие информации

Эта серия статей предназначена для использования в качестве быстрого подведения итогов микрокрадовского проекта Андрея Карпати [1] для быстрого пересмотра и понимания механизма обратного пробагирования. Код извлекается и переназначается по мере необходимости из микрограда, вся заслуга исходного создателя, Андрея Карпати.