Привет, ребята! Добро пожаловать в пятую часть этой серии статей о создании библиотеки глубокого обучения с нуля. В этом посте мы рассмотрим код части библиотеки, связанной с автоматической дифференциацией. Автоматическое дифференцирование обсуждалось в предыдущем посте, поэтому ознакомьтесь с ним, если вы не знаете, что такое Autodiff.
Репозиторий github для этой серии:….
ashwins-code / Zen-Deep-Learning-Library
Библиотека глубокого обучения, написанная на Python. Содержит код для моей серии блогов о создании библиотеки глубокого обучения.
Zen — библиотека глубокого обучения
Библиотека глубокого обучения, написанная на Python.
Содержит код для моей серии блогов, в которой мы создаем эту библиотеку с нуля.
- mnist.py содержит пример распознавателя цифр MNIST с использованием библиотеки
- rnn.py содержит пример рекуррентной нейронной сети, которая учится вписываться в график sin(x) * cos(x).
Подход
Автоматическое дифференцирование полагается на вычислительный граф для вычисления производных.
В конечном счете, он сводится к узлам с некоторыми связями, и мы обходим эти узлы определенным образом для вычисления производных.
В нашей библиотеке мы будем строить вычислительный граф «на лету», то есть все выполняемые вычисления будут записываться в вычислительный граф.
Когда у нас есть граф, нам нужно найти способ использовать его для вычисления производных всех переменных в этом графе.
Например, допустим, мы получили следующий график…
Это представляет собой…
Теперь, используя график, наша цель — найти производную от e относительно всех переменных на этом графике (a,b,c,d,e).
Для моей реализации я обнаружил, что проще всего для вычисления производных пройти по графу в глубину.
Итак, сначала мы начинаем с e и находим dede по отношению к e (которое равно 1).
Затем мы рассматриваем узел c, то есть теперь нам нужно вычислить dcde. Мы видим, что e является результатом умножения между c и d, то есть dcde=d (так как мы рассматриваем все, кроме переменной, на которой мы находимся, как константу).
Помня, что мы обходим в глубину, следующим узлом, на который мы перейдем, будет узел a, а значит, мы вычислим dade. Это немного сложнее, так как a не имеет прямой связи с e. Однако, используя правило цепочки, мы знаем, что dade=dcdedadc. Мы только что вычислили dcde, поэтому все, что нам теперь нужно вычислить, это dc. Мы видим, что c является сложением a и b, поэтому 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.