Введение
В первой части этой серии статей мы подробно изложили основы теории обратного распространения ошибки и сделали простую реализацию, включающую сложение, вычитание и топологическую сортировку. В этой статье мы подробнее рассмотрим умножение, деление, возведение в степень и некоторые популярные функции активации.
Так как мы убрали теоретический материал из предыдущей статьи, которую я бы порекомендовал сначала прочитать здесь, мы погрузимся непосредственно в реализацию.
Выполнение
В этой статье мы расширим класс 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] для быстрого пересмотра и понимания механизма обратного пробагирования. Код извлекается и переназначается по мере необходимости из микрограда, вся заслуга исходного создателя, Андрея Карпати.