Понимание сути магии PyTorch

Источник: https://www.cs.toronto.edu/~rgrosse/courses/csc321_2018/slides/lec10.pdf

Источник: http://bumpybrains.com/comics.php?comic=34

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

Чтобы иметь дело с гиперплоскостями в 14-мерном пространстве, визуализируйте трехмерное пространство и очень громко скажите себе «четырнадцать». Все так делают - Джеффри Хинтон

Именно здесь на помощь приходит автограф PyTorch. Он абстрагирует сложную математику и помогает нам «волшебным образом» вычислять градиенты многомерных кривых с помощью всего лишь нескольких строк кода. В этом посте делается попытка описать магию автограда.

Основы PyTorch

Прежде чем двигаться дальше, нам нужно знать о некоторых основных концепциях PyTorch.

Тензоры. Проще говоря, это просто n-мерный массив в PyTorch. Тензоры поддерживают некоторые дополнительные улучшения, которые делают их уникальными: кроме ЦП, они могут быть загружены или ГП для более быстрых вычислений. При установке .requires_grad = True они начинают формировать обратный график, который отслеживает каждую операцию, применяемую к ним для вычисления градиентов, используя нечто, называемое графом динамических вычислений (DCG) (поясняется далее в посте).

В более ранних версиях PyTorch классtorch.autograd.Variable использовался для создания тензоров, поддерживающих вычисления градиента и отслеживание операций, но начиная с PyTorch v0.4.0 класс переменных устарел. torch.Tensor и torch.autograd.Variable теперь одного класса. Точнее, torch.Tensor может отслеживать историю и ведет себя как старый Variable

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

Autograd: Этот класс представляет собой механизм для вычисления производных (точнее, произведение векторов Якоби). Он записывает график всех операций, выполняемых с тензором с включенным градиентом, и создает ациклический граф, называемый динамическим вычислительным графом. Листья этого графа являются входными тензорами, а корни - выходными тензорами. Градиенты вычисляются путем отслеживания графика от корня к листу и умножения каждого градиента способом с использованием правила цепочки.

Нейронные сети и обратное распространение

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

В некотором смысле, обратное распространение - это просто причудливое название цепного правила - Джереми Ховард.

Создание и обучение нейронной сети включает следующие важные шаги:

  1. Определите архитектуру
  2. Прямое распространение по архитектуре с использованием входных данных
  3. Рассчитайте убыток
  4. Обратное распространение для расчета градиента для каждого веса
  5. Обновите веса, используя скорость обучения

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

Это делается итеративным способом. Для каждой итерации вычисляется несколько градиентов и строится так называемый граф вычислений для хранения этих функций градиента. PyTorch делает это путем построения динамического вычислительного графа (DCG). Этот график строится с нуля на каждой итерации, обеспечивая максимальную гибкость при вычислении градиента. Например, для прямой операции (функции) Mul обратная операция (функция), называемая MulBackward, динамически интегрируется в обратный график для вычисления градиента.

Динамический вычислительный граф

Тензоры (переменные) с включенным градиентом вместе с функциями (операциями) объединяются для создания динамического вычислительного графа. Поток данных и операции, применяемые к данным, определяются во время выполнения, следовательно, вычислительный граф строится динамически. Этот график динамически создается классом autograd под капотом. Вам не нужно кодировать все возможные пути перед началом обучения - то, что вы бежите, - это то, что вы отличаетесь.

Простая DCG для умножения двух тензоров будет выглядеть так:

Каждая пунктирная рамка на графике - это переменная, а фиолетовая прямоугольная рамка - операция.

Каждый объект переменной имеет несколько членов, некоторые из которых:

Данные: данные, которые хранятся в переменной. x содержит тензор 1x1 со значением 1,0, а y - 2,0. z содержит произведение двух, т. е. 2,0.

requires_grad: этот член, если он имеет значение true, начинает отслеживать всю историю операций и формирует обратный график для вычисления градиента. Для произвольного тензора a им можно управлять на месте следующим образом: a.requires_grad_(True).

grad: grad содержит значение градиента. Если requires_grad имеет значение False, он будет содержать значение None. Даже если requires_grad имеет значение True, он будет содержать значение None, если функция .backward() не вызывается из какого-либо другого узла. Например, если вы вызываете out.backward() для некоторой переменной out, которая участвует в вычислениях x, тогда x.grad будет содержать ∂out / ∂x.

grad_fn: это обратная функция, используемая для вычисления градиента.

is_leaf: узел является листовым, если:

  1. Он был явно инициализирован какой-то функцией вроде x = torch.tensor(1.0) или x = torch.randn(1, 1) (в основном все методы инициализации тензора, обсуждаемые в начале этого поста).
  2. Он создается после операций с тензорами, у которых все requires_grad = False.
  3. Он создается путем вызова метода .detach() на некотором тензоре.

При вызове backward() градиенты заполняются только для узлов, у которых есть и requires_grad, и is_leaf True. Градиенты относятся к выходному узлу, из которого вызывается .backward(), относительно других листовых узлов.

При включении requires_grad = True PyTorch начнет отслеживать операцию и сохранять функции градиента на каждом этапе следующим образом:

Код, который генерирует приведенный выше график под капотом PyTorch:

Источник: https://www.cs.toronto.edu/~rgrosse/courses/csc321_2018/slides/lec10.pdf

Чтобы PyTorch не отслеживал историю и не формировал обратный график, код можно поместить внутрь with torch.no_grad():. Это заставит код работать быстрее, когда отслеживание градиента не требуется.

Функция Backward ()

Backward - это функция, которая фактически вычисляет градиент, передавая его аргумент (единичный тензор 1x1 по умолчанию) через обратный граф вплоть до каждого листового узла, отслеживаемого от вызывающего корневого тензора. Затем вычисленные градиенты сохраняются в .grad каждого листового узла. Помните, обратный график уже создан динамически во время прямого прохода. Функция Backward только вычисляет градиент, используя уже созданный график, и сохраняет их в конечных узлах.

Давайте проанализируем следующий код

Важно отметить, что при вызове z.backward() тензор автоматически передается как z.backward(torch.tensor(1.0)). torch.tensor(1.0) - это внешний градиент, обеспечивающий прекращение умножения градиента по правилу цепочки. Этот внешний градиент передается в качестве входных данных в функцию MulBackward для дальнейшего вычисления градиента x. Размерность тензора, переданного в .backward(), должна быть такой же, как размерность тензора, градиент которого вычисляется. Например, если тензор x и y с включенным градиентом выглядит следующим образом:

x = torch.tensor([0.0, 2.0, 8.0], requires_grad = True)

y = torch.tensor([5.0 , 1.0 , 7.0], requires_grad = True)

и z = x * y

затем, чтобы вычислить градиенты z (тензор 1x3) по отношению к x или y, внешний градиент должен быть передан в z.backward()функцию следующим образом: z.backward(torch.FloatTensor([1.0, 1.0, 1.0])

z.backward() даст RuntimeError: grad can be implicitly created only for scalar outputs

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

tldr: Обратный график создается автоматически и динамически классом autograd во время прямого прохода. Backward() просто вычисляет градиенты, передавая свой аргумент уже созданному обратному графику.

Математика - якобианы и векторы

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

Примечание. В процессе PyTorch никогда не строит явно весь якобиан. Обычно проще и эффективнее вычислить JVP (векторное произведение Якоби) напрямую.

Если вектор X = [x1, x2,… .xn] используется для вычисления некоторого другого вектора f (X) = [f1, f2,…. fn] через функцию f, тогда матрица Якоби (J) просто содержит все комбинации частных производных следующим образом:

Матрица выше представляет градиент f (X) по отношению к X

Предположим, что градиент PyTorch включил тензоры X как:

X = [x1, x2,… .. xn] (пусть это будут веса некоторой модели машинного обучения)

X выполняет некоторые операции, чтобы сформировать вектор Y

Y = f(X) = [y1, y2, …. ym]

Y затем используется для вычисления скалярных потерь l. Предположим, вектор v является градиентом скалярных потерь l. относительно вектора Y следующим образом

Вектор v называется grad_tensor и передается функции backward() в качестве аргумента

Чтобы получить градиент потерь l относительно весов X, матрица Якоби J умножается на вектор. с вектором v

Этот метод вычисления матрицы Якоби и умножения ее на вектор v дает PyTorch возможность с легкостью подавать внешние градиенты даже для нескалярных выходных данных.

дальнейшее чтение

Обратное распространение: Быстрая проверка

PyTorch: Пакет автоматической дифференциации - torch.autograd

Исходный код Автограда

Видео: объяснение PyTorch Autograd - подробное руководство от Elliot Waite

Спасибо за чтение! Не стесняйтесь выражать любые вопросы в ответах.