Библиотека глубокого обучения с нуля 5: Автоматическая дифференциация Продолжение

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

Репозиторий github для этой серии:….

ashwins-code / Zen-Deep-Learning-Library

Библиотека глубокого обучения, написанная на Python. Содержит код для моей серии блогов о создании библиотеки глубокого обучения.

Zen — библиотека глубокого обучения

Библиотека глубокого обучения, написанная на Python.

Содержит код для моей серии блогов, в которой мы создаем эту библиотеку с нуля.

  • mnist.py содержит пример распознавателя цифр MNIST с использованием библиотеки
  • rnn.py содержит пример рекуррентной нейронной сети, которая учится вписываться в график sin(x) * cos(x).

Посмотреть на GitHub

Подход

Автоматическое дифференцирование полагается на вычислительный граф для вычисления производных.

В конечном счете, он сводится к узлам с некоторыми связями, и мы обходим эти узлы определенным образом для вычисления производных.

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

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

Например, допустим, мы получили следующий график…

Это представляет собой…

c=a+be=cdc = a + b newlinee = c * d newlinec=a+be=c∗d

Теперь, используя график, наша цель — найти производную от e относительно всех переменных на этом графике (a,b,c,d,e).

Для моей реализации я обнаружил, что проще всего для вычисления производных пройти по графу в глубину.

Итак, сначала мы начинаем с e и находим dedefrac{de}{de}dede по отношению к e (которое равно 1).

Затем мы рассматриваем узел c, то есть теперь нам нужно вычислить dedcfrac{de}{dc}dcde. Мы видим, что ee e является результатом умножения между cc c и dd d, то есть dedc=dfrac{de}{dc} = ddcde=d (так как мы рассматриваем все, кроме переменной, на которой мы находимся, как константу).

Помня, что мы обходим в глубину, следующим узлом, на который мы перейдем, будет узел a, а значит, мы вычислим dedafrac{de}{da}dade. Это немного сложнее, так как a не имеет прямой связи с e. Однако, используя правило цепочки, мы знаем, что deda=dedcdcdafrac{de}{da} = frac{de}{dc}frac{dc}{da}dade=dcdedadc. Мы только что вычислили dedcfrac{de}{dc}dcde, поэтому все, что нам теперь нужно вычислить, это dcdafrac{dc}{da}dc. Мы видим, что c является сложением a и b, поэтому deda=dedcdcda=dfrac{de}{da} = frac{de}{dc}frac{dc}{da} = d dade=dcdedadc=d

Надеюсь, теперь вы видите, как мы можем использовать график для нахождения производных всех переменных на этом графике.

Класс тензоров

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

import numpy as np
import string
import random

def id_generator(size=10, chars=string.ascii_uppercase + string.digits):
    return ''.join(random.choice(chars) for _ in range(size))


np.seterr(invalid='ignore')

def is_matrix(o):
    return type(o) == np.ndarray

def same_shape(s1, s2):
    for a, b in zip(s1, s2):
        if a != b:
            return False

    return True

class Tensor:
    __array_priority__ = 1000
    def __init__(self, value, trainable=True):
        self.value = value
        self.dependencies = []
        self.grads = []
        self.grad_value = None
        self.shape = 0
        self.matmul_product = False
        self.gradient = 0
        self.trainable = trainable
        self.id = id_generator()

        if is_matrix(value):
            self.shape = value.shape
Войти в полноэкранный режим Выход из полноэкранного режима

Что здесь происходит?

def id_generator(size=10, chars=string.ascii_uppercase + string.digits):
    return ''.join(random.choice(chars) for _ in range(size))
Войти в полноэкранный режим Выход из полноэкранного режима

Функция, генерирующая уникальный идентификатор с помощью случайных символов

def is_matrix(o):
    return type(o) == np.ndarray
Войти в полноэкранный режим Выход из полноэкранного режима

Простая функция, которая проверяет, является ли значение массивом numpy или нет.

self.value = value
Войти в полноэкранный режим Выход из полноэкранного режима

Эта строка не требует пояснений, она просто хранит значение, которое передается тензору.

self.dependencies = [] 
Войти в полноэкранный режим Выйти из полноэкранного режима

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

self.grads = []
Войти в полноэкранный режим Выйти из полноэкранного режима

Это свойство будет содержать список производных каждой из зависимостей тензора по отношению к тензору.

self.shape = 0
...
if is_matrix(value):
            self.shape = value.shape
Войти в полноэкранный режим Выйти из полноэкранного режима

self.shape хранит форму значения тензора. Только массивы numpy могут иметь форму, поэтому по умолчанию она равна 0.

self.matmul_product = False
Вход в полноэкранный режим Выйти из полноэкранного режима

Указывает, был ли тензор результатом умножения матрицы или нет (это поможет позже, так как правило цепочки работает по-другому для умножения матриц).

self.gradient = np.ones_like(self.value)
Войти в полноэкранный режим Выход из полноэкранного режима

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

self.trainable = trainable
Войти в полноэкранный режим Выход из полноэкранного режима

Некоторые узлы графа не нуждаются в вычислении своих производных, поэтому это свойство определяет, так это или нет для данного тензора.

self.id = id_generator()
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Операции с тензорами

class Tensor:
    __array_priority__ = 1000
    def __init__(self, value, trainable=True):
        self.value = value
        self.dependencies = []
        self.grads = []
        self.grad_value = None
        self.shape = 0
        self.matmul_product = False
        self.gradient = 0
        self.trainable = trainable
        self.id = id_generator()

        if is_matrix(value):
            self.shape = value.shape

    def depends_on(self, target):
        if self == target:
            return True

        dependencies = self.dependencies

        for dependency in dependencies:
            if dependency == target:
                return True
            elif dependency.depends_on(target):
                return True

        return False

    def __mul__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(self.value * other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)
        var.grads.append(other.value)
        var.grads.append(self.value)
        return var

    def __rmul__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(self.value * other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)
        var.grads.append(other.value)
        var.grads.append(self.value)
        return var

    def __add__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(self.value + other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)
        var.grads.append(np.ones_like(self.value))
        var.grads.append(np.ones_like(other.value))
        return var

    def __radd__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(self.value + other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)
        var.grads.append(np.ones_like(self.value))
        var.grads.append(np.ones_like(other.value))
        return var

    def __sub__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other)

        var = Tensor(self.value - other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)
        var.grads.append(np.ones_like(self.value))
        var.grads.append(-np.ones_like(other.value))
        return var

    def __rsub__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(other.value - self.value)
        var.dependencies.append(other)
        var.dependencies.append(self)
        var.grads.append(np.ones_like(other.value))
        var.grads.append(-np.one_like(self.value))
        return var

    def __pow__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(self.value ** other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)

        grad_wrt_self = other.value * self.value ** (other.value - 1)
        var.grads.append(grad_wrt_self)

        grad_wrt_other = (self.value ** other.value) * np.log(self.value)
        var.grads.append(grad_wrt_other)

        return var

    def __rpow__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(other.value ** self.value)
        var.dependencies.append(other)
        var.dependencies.append(self)

        grad_wrt_other = self.value * other.value ** (self.value - 1)
        var.grads.append(grad_wrt_other)

        grad_wrt_self = (other.value ** self.value) * np.log(other.value)
        var.grads.append(grad_wrt_self)

        return var


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

    def __rtruediv__(self, other):
        return other * (self ** -1)

    def __matmul__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(self.value @ other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)
        var.grads.append(other.value.T)
        var.grads.append(self.value.T)

        var.matmul_product = True
        return var

    def __rmatmul__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(other.value @ self.value)
        var.dependencies.append(other)
        var.dependencies.append(self)
        var.grads.append(self.value.T)
        var.grads.append(other.value.T)

        var.matmul_product = True

        return var
Вход в полноэкранный режим Выход из полноэкранного режима

Чтобы понять, что здесь происходит, давайте рассмотрим один из методов

def __mul__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(self.value * other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)
        var.grads.append(other.value)
        var.grads.append(self.value)
        return var
Войти в полноэкранный режим Выйти из полноэкранного режима

Во-первых, __mul__ — это перегрузчик оператора. Это означает, что каждый раз, когда мы хотим умножить тензор на что-то другое, будет вызван этот метод. Вы также видите __rmul__, который является тем же самым, но вызывается, когда объект Tensor находится справа от операции.

Например.

t = Tensor(10)
t * 5 #__mul__ is called
5 * t #__rmul__ is called
Войти в полноэкранный режим Выход из полноэкранного режима
if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)
Войти в полноэкранный режим Выйти из полноэкранного режима

other представляет то, на что умножается данный тензор. Если other не является тензором, мы хотим преобразовать его в тензор, сохранив значение other. Поскольку other еще не был тензором, это означает, что он является константой, поэтому нам не нужно вычислять его производную, так как мы хотим вычислить производную только в том случае, если нам нужно изменить ее значение, обычно при обучении модели. Вот почему trainable=False.

var = Tensor(self.value * other.value)
var.dependencies.append(self)
var.dependencies.append(other)
Вход в полноэкранный режим Выход из полноэкранного режима

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

var.grads.append(other.value) # dvar/dother
var.grads.append(self.value) # dvar/dself
return var
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь это добавляет производные двух операндов относительно var к grads var. Эти строки, очевидно, будут отличаться в других методах класса, поскольку производные зависят от применяемой операции (в данном случае это умножение). Обратите внимание, что порядок их добавления соответствует порядку тензоров в свойстве dependencies. Мы не храним их как тензоры, поскольку вычисление производных будет гораздо быстрее, если использовать необработанные значения, а не наш класс тензоров.

Вычисление градиентов с помощью графика!

def get_gradients(self, grad = None):
        grad = np.ones_like(self.value) if grad is None else grad
        grad = np.float32(grad)

        for dependency, _grad in zip(self.dependencies, self.grads):
            if dependency.trainable:
                local_grad = np.float32(_grad)

                if self.matmul_product:                
                    if dependency == self.dependencies[0]:
                        local_grad = grad @ local_grad
                    else:
                        local_grad = local_grad @ grad
                else:
                    if dependency.shape != 0 and not same_shape(grad.shape, local_grad.shape):
                        ndims_added = grad.ndim - local_grad.ndim
                        for _ in range(ndims_added):
                            grad = grad.sum(axis=0)

                        for i, dim in enumerate(dependency.shape):
                            if dim == 1:
                                grad = grad.sum(axis=i, keepdims=True)

                    local_grad = local_grad * np.nan_to_num(grad)


                dependency.gradient += local_grad
                dependency.get_gradients(local_grad)
Вход в полноэкранный режим Выход из полноэкранного режима

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

grad хранит входящие градиенты/рассчитанный ранее градиент на предыдущем «уровне» графика.

for dependency, _grad in zip(self.dependencies, self.grads):
        if dependency.trainable:
           local_grad = np.float32(_grad)

            if self.matmul_product:                
                if dependency == self.dependencies[0]:
                    local_grad = grad @ local_grad
                else:
                    local_grad = local_grad @ grad
            else:
                if dependency.shape != 0 and not same_shape(grad.shape, local_grad.shape):
                    ndims_added = grad.ndim - local_grad.ndim
                    for _ in range(ndims_added):
                        grad = grad.sum(axis=0)

                    for i, dim in enumerate(dependency.shape):
                        if dim == 1:
                            grad = grad.sum(axis=i, keepdims=True)

                    local_grad = local_grad * np.nan_to_num(grad)
Вход в полноэкранный режим Выход из полноэкранного режима

Эта часть кода проходит через каждую зависимость тензора вместе с производной тензора относительно этой зависимости (хранится в _grad). Если зависимость является обучаемой, то проверяется, является ли она результатом матричного умножения или нет.

Затем применяется правило цепочки, используя ранее вычисленный градиент grad и градиент текущей зависимости _grad. local_grad сохраняет результат после применения правила цепочки.

def same_shape(s1, s2):
    for a, b in zip(s1, s2):
        if a != b:
            return False

    return True
Вход в полноэкранный режим Выход из полноэкранного режима
if dependency.shape != 0 and not same_shape(grad.shape, local_grad.shape):
                ndims_added = grad.ndim - local_grad.ndim
                for _ in range(ndims_added):
                    grad = grad.sum(axis=0)

                for i, dim in enumerate(dependency.shape):
                    if dim == 1:
                        grad = grad.sum(axis=i, keepdims=True)
Войти в полноэкранный режим Выход из полноэкранного режима

Если мы сосредоточимся на этой части кода, то она обрабатывает любой случай, когда local_grad и grad не совпадают по форме (они должны совпадать, чтобы правило цепочки было применено). Такое несоответствие форм возникает, если в каком-либо из вычислений была выполнена трансляция. Трансляция — это термин, используемый для описания того, как numpy будет выполнять операции с массивами разной формы. Подробнее об этом можно прочитать в документации numpy. Все, что делает эта часть кода, это суммирует по транслируемой оси grad, чтобы уменьшить его форму до соответствия форме local_grad.

dependency.gradient += local_grad
dependency.get_gradients(local_grad)
Вход в полноэкранный режим Выход из полноэкранного режима

Затем градиент записывается в свойство gradient зависимости. Затем продолжается обход по глубине, вызывая метод get_gradients для зависимости, передавая только что вычисленный градиент.

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

import numpy as np
import string
import random

def id_generator(size=10, chars=string.ascii_uppercase + string.digits):
    return ''.join(random.choice(chars) for _ in range(size))


np.seterr(invalid='ignore')

def is_matrix(o):
    return type(o) == np.ndarray

def same_shape(s1, s2):
    for a, b in zip(s1, s2):
        if a != b:
            return False

    return True

class Tensor:
    __array_priority__ = 1000
    def __init__(self, value, trainable=True):
        self.value = value
        self.dependencies = []
        self.grads = []
        self.grad_value = None
        self.shape = 0
        self.matmul_product = False
        self.gradient = 0
        self.trainable = trainable
        self.id = id_generator()

        if is_matrix(value):
            self.shape = value.shape

    def depends_on(self, target):
        if self == target:
            return True

        dependencies = self.dependencies

        for dependency in dependencies:
            if dependency == target:
                return True
            elif dependency.depends_on(target):
                return True

        return False

    def __mul__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(self.value * other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)
        var.grads.append(other.value)
        var.grads.append(self.value)
        return var

    def __rmul__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(self.value * other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)
        var.grads.append(other.value)
        var.grads.append(self.value)
        return var

    def __add__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(self.value + other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)
        var.grads.append(np.ones_like(self.value))
        var.grads.append(np.ones_like(other.value))
        return var

    def __radd__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(self.value + other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)
        var.grads.append(np.ones_like(self.value))
        var.grads.append(np.ones_like(other.value))
        return var

    def __sub__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other)

        var = Tensor(self.value - other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)
        var.grads.append(np.ones_like(self.value))
        var.grads.append(-np.ones_like(other.value))
        return var

    def __rsub__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(other.value - self.value)
        var.dependencies.append(other)
        var.dependencies.append(self)
        var.grads.append(np.ones_like(other.value))
        var.grads.append(-np.one_like(self.value))
        return var

    def __pow__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(self.value ** other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)

        grad_wrt_self = other.value * self.value ** (other.value - 1)
        var.grads.append(grad_wrt_self)

        grad_wrt_other = (self.value ** other.value) * np.log(self.value)
        var.grads.append(grad_wrt_other)

        return var

    def __rpow__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(other.value ** self.value)
        var.dependencies.append(other)
        var.dependencies.append(self)

        grad_wrt_other = self.value * other.value ** (self.value - 1)
        var.grads.append(grad_wrt_other)

        grad_wrt_self = (other.value ** self.value) * np.log(other.value)
        var.grads.append(grad_wrt_self)

        return var


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

    def __rtruediv__(self, other):
        return other * (self ** -1)

    def __matmul__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(self.value @ other.value)
        var.dependencies.append(self)
        var.dependencies.append(other)
        var.grads.append(other.value.T)
        var.grads.append(self.value.T)

        var.matmul_product = True
        return var

    def __rmatmul__(self, other):
        if not (isinstance(other, Tensor)):
            other = Tensor(other, trainable=False)

        var = Tensor(other.value @ self.value)
        var.dependencies.append(other)
        var.dependencies.append(self)
        var.grads.append(self.value.T)
        var.grads.append(other.value.T)

        var.matmul_product = True

        return var

    def grad(self, target, grad = None):
        grad = self.value / self.value if grad is None else grad
        grad = np.float32(grad)

        if not self.depends_on(target):
            return 0

        if self == target:
            return grad

        final_grad = 0

        for dependency, _grad in zip(self.dependencies, self.grads):
            local_grad = np.float32(_grad) if dependency.depends_on(target) else 0

            if local_grad is not 0:
                if self.matmul_product:                
                    if dependency == self.dependencies[0]:
                        local_grad = grad @ local_grad
                    else:
                        local_grad = local_grad @ grad
                else:
                    if dependency.shape != 0 and not same_shape(grad.shape, local_grad.shape):
                        ndims_added = grad.ndim - local_grad.ndim
                        for _ in range(ndims_added):
                            grad = grad.sum(axis=0)

                        for i, dim in enumerate(local_grad.shape):
                            if dim == 1:
                                grad = grad.sum(axis=i, keepdims=True)

                    local_grad *= grad

            final_grad += dependency.grad(target, local_grad)

        return final_grad


    def get_gradients(self, grad = None):
        grad = np.ones_like(self.value) if grad is None else grad
        grad = np.float32(grad)

        for dependency, _grad in zip(self.dependencies, self.grads):
            if dependency.trainable:
                local_grad = np.float32(_grad)

                if self.matmul_product:                
                    if dependency == self.dependencies[0]:
                        local_grad = grad @ local_grad
                    else:
                        local_grad = local_grad @ grad
                else:
                    if dependency.shape != 0 and not same_shape(grad.shape, local_grad.shape):
                        ndims_added = grad.ndim - local_grad.ndim
                        for _ in range(ndims_added):
                            grad = grad.sum(axis=0)

                        for i, dim in enumerate(dependency.shape):
                            if dim == 1:
                                grad = grad.sum(axis=i, keepdims=True)

                    local_grad = local_grad * np.nan_to_num(grad)


                dependency.gradient += local_grad
                dependency.get_gradients(local_grad)

    def __repr__(self):
        return f"Tensor ({self.value})"
Вход в полноэкранный режим Выход из полноэкранного режима

И его можно использовать следующим образом…

a = Tensor(10)
b = Tensor(5)
c = 2
d = (a*b)**c
d.get_gradients()

print (a.gradient, b.gradient)
Войти в полноэкранный режим Выход из полноэкранного режима
OUTPUT:
500.0 1000.0
Войти в полноэкранный режим Выйти из полноэкранного режима

Спасибо

Спасибо, что дочитали эту статью до конца! Код этого поста можно посмотреть в репозитории Github по ссылке в начале в файле autodiff.py.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *