«… .. Безусловно, лучшая стратегия для многого обучения, которую я обнаружил, заключалась в том, чтобы реализовать все. Мне довелось много делать в Javascript для lols. Всякий раз, когда я что-то читаю, я впадаю в иллюзию понимания этого и заставляю себя заново реализовать это, всегда обнаруживая интересное понимание. Это мой любимый способ учиться », Андрей Карпатий.

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

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

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

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

ПРИМЕЧАНИЕ; здесь есть математическое введение в автоматическое дифференцирование градиента, иллюстрация, которую я сделал, - это просто думать об этом с точки зрения концепции программирования.

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

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

Помните, я сказал, что мы не будем видеть такие математические функции, как сложение, умножение или тензор, как математические переменные, а как объект. В идеале такой язык, как python, упрощает рассмотрение некоторых функций, таких как сложение, умножение и т. Д., Как свойства тензора. Но в javascript таких привилегий не будет, и это заставит нас задуматься о различных способах представления этой математической функции и даже самих тензоров, о которых мы узнаем позже.

Наша основная цель - получить фрагмент кода для рисунка выше;

var x = new Tensor(-2,true);
var y = new Tensor(5,true);
var z = new Tensor(-4,true);
var q = new add(x,y);
var f = new multi(q,z);
f.grad(1)
f.backward()

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

function Tensor(arr,require_grad){
    this.item = arr;
    this.require_grad = require_grad;
    this.gradv = 0;
}
Tensor.prototype = {
    grad: function(g){
         this.gradv = g;
    }
}

Вышеупомянутая концепция будет знакома пользователям PyTorch. Свойство item обозначает значение тензора, а require_grad - логическое свойство тензора, оно определяет, должен ли градиент течь или нет. А для тех, кто уже хорошо знаком с нейронной сетью, идеология замораживания слоя исходит из установки require_grads на false. gradv хранит принадлежность градиента к тензору. А свойство функции grad помогает установить градиент или может рассматриваться как помощь в сборе градиента для полета.

Теперь мы можем создать объект / класс для некоторой базовой функции, и, поскольку в примере, который мы используем, используем addition и multiplication, мы создадим только класс для них.

function add(x,y){
     this.x = x;
     this.y = y;
     this.require_grad=true;
     this.item = x.item + y.item
     this.gradv = 0;
}
add.prototype = {
     backward: function(){
         if(this.x.require_grad){
           this.x.grad(1*this.gradv);
           if("backward" in this.x){
                this.x.backward()
           }
         }
         if(this.y.require_grad){
             this.y.grad(1*this.gradv);
            if("backward" in this.y){
                  this.y.backward()
            }
          }
      },
     grad: function(g){
          this.gradv = g;
      }
}

Обратите внимание, что объект add содержит обратную функцию, а Tensor этого не делает, потому что мы различаем не значение в исчислении, а функцию. идея в том, что вы не можете различать только x, но можете f(x) = x, это основная причина, по которой тензор не имеет обратной функции, поскольку градиент только течет в них.

Функция добавления принимает две переменные, а градиент для функции добавления равен 1 (если вы прошли через материал cs231n). Градиент, текущий в объект добавления gradv, умножается на 1. А вы Посмотрите, что мы вычислили градиент для двух входных данных, сначала проверив, установлено ли для require_grads значение Истина, а если истинно, установлен градиент входа, а затем также проверив, содержит ли вход свойство backward.

Та же структура, которая использовалась для создания объекта сложения, будет использоваться для создания всех остальных классов математических операторов. Следовательно, давайте создадим объект для умножения.

function multi(x, y) {
     this.item = x.item * y.item;
     this.x = x;
     this.y = y;
     this.gradv = 0;
     this.require_grad = true;
}
multi.prototype = {
     backward: function () {
           if (this.x.require_grad) {
                  this.x.grad(this.y.item * this.gradv);
                  if ("backward" in this.x) {
                       // console.log("True")
                       this.x.backward()
                   }
            }
            if (this.y.require_grad) {
                  this.y.grad(this.x.item * this.gradv);
                  if ("backward" in this.y) {
                        this.y.backward()
                   }
             }
      },
      grad: function (g) {
             this.gradv =  g;
       }
}

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

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

var x = new Tensor(-2,true);
var y = new Tensor(5,true);
var z = new Tensor(-4,true);
var q = new add(x,y);
var f = new multi(q,z);
console.log(f.item) 
output: -12

Поскольку f является объектом, давайте посмотрим, из чего он состоит, это может дать нам лучшее представление о том, что это за объект.

{ item: -12,
  x:
   { x: { item: -2, require_grad: true, gradv: 0 },
     y: { item: 5, require_grad: true, gradv: 0 },
     require_grad: true,
     item: 3,
     gradv: 0 },
  y: { item: -4, require_grad: true, gradv: 0 },
  gradv: 0,
  require_grad: true }

Вы можете видеть, что этот объект f содержит собственное свойство item, равное -12, а также содержит ввод, который x, y и x является оператором, содержащим ввод x и y. Чтобы увидеть имя этого оператора в выходных данных, давайте добавим указанное ниже свойство к оператору и тензорному объекту. С помощью хорошего инструмента визуализации мы сможем построить основу графика на узле.

this.name = "name of operator or tensor"

Добавив указанное выше свойство к каждому объекту, мы теперь можем увидеть, что такое x и y;

{ item: -12,
  x:
   { x: { item: -2, require_grad: true, gradv: 0, name: '<Tensor>' },
     y: { item: 5, require_grad: true, gradv: 0, name: '<Tensor>' },
     require_grad: true,
     item: 3,
     gradv: 0,
     name: '<Add>' },
  y: { item: -4, require_grad: true, gradv: 0, name: '<Tensor>' },
  gradv: 0,
  require_grad: true,
  name: '<Multi>' }

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

Теперь давайте посмотрим, что происходит, когда мы делаем обратную передачу объекта f

f.grad(1)
f.backward()
console.log(f)

Градиент объекта f сначала устанавливается на единицу, потому что дифференциация функции относительно самой функции равна 1, но при построении нейронной сети нам не нужно устанавливать наш выходной градиент на 1.

Вывод предыдущего блока кода дает нам;

{ item: -12,
  x:
   { x:
      { item: -2, require_grad: true, gradv: -4, name: '<Tensor>' },
     y: { item: 5, require_grad: true, gradv: -4, name: '<Tensor>' },
     require_grad: true,
     item: 3,
     gradv: -4,
     name: '<Add>' },
  y: { item: -4, require_grad: true, gradv: 3, name: '<Tensor>' },
  gradv: 1,
  require_grad: true,
  name: '<Multi>' }

Теперь вы можете проверить пример изображения, который мы пытаемся реализовать снова, и вы увидите, что у них одинаковое значение градиента. Объект <Add> имеет собственный градиент, а его входные значения x и y имеют собственный градиент.

Теперь у нас есть возможность получить доступ к градиенту отдельного оператора и ввода

console.log(x.gradv)
//-4

Вы можете попробовать проверить gradv для каждой создаваемой переменной. Чтобы лучше понять идею require_grad, установите для input x require_grads значение false.

var x = new Tensor(-2,false);
var y = new Tensor(5,true);
var z = new Tensor(-4,true);
var q = new add(x,y);
var f = new multi(q,z);
f.grad(1)
f.backward()
console.log(x.gradv) //output is 0

console.log(f)
{ item: -12,
  x:
   { x:
      { item: -2, require_grad: false, gradv: 0, name: '<Tensor>' },
     y: { item: 5, require_grad: true, gradv: -4, name: '<Tensor>' },
     require_grad: true,
     item: 3,
     gradv: -4,
     name: '<Add>' },
  y: { item: -4, require_grad: true, gradv: 3, name: '<Tensor>' },
  gradv: 1,
  require_grad: true,
  name: '<Multi>' }

Выходной градиент add не перетекал в x.

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

Концепция

  1. Все тензоры в autograd имеют grad_fn метод (функцию), собирающий градиентный поток. И у них также есть grad свойства, в которых хранится входящий градиент. require_grad используется, чтобы определить, должен ли градиент течь в тензор.
  2. Все математические операторы содержат прямой проход, который является фактическим вычислением относительно оператора, например, если оператор addition, тогда прямой проход представляет собой сложение двух чисел.
  3. А еще математический оператор содержит backward проход; в котором вычисляется обратное распространение и на вход тензоров назначается градиент. Например, градиент добавляемого объекта - это 1 * входящий градиентный поток.
  4. У математического оператора также есть метод grad_fn, который собирает градиентные потоки и присваивает их свойству grad оператора.

Тензоры

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

Первой ошибкой, которую я сделал ранее при попытке создать тензоры в javascript, было представление о том, что у тензора есть вложенный список, например, как они реализованы в python как;

array([
    [2, 3, 4],
    [5, 6, 7],
    [8, 9,10]
])

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

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

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

Давайте создадим объект Tensor и его свойства

var Tensor = function (n, d, require_grad) {
         this.n = n;
         this.d = d;
         this.out = zeros(n * d);
         this.dout = zeros(n * d);
         this.require_grad = true;
}
Tensor.prototype = {
          get: function (row, col) {
               var ix = (this.d * row) + col;
               assert(ix >= 0 && ix < this.w.length);
                return this.out[ix];
          },
          set: function (row, col, v) {
               var ix = (this.d * row) + col;
               assert(ix >= 0 && ix < this.w.length);
               this.out[ix] = v;
           },
           setFrom: function (arr) {
                for (var i = 0, n = arr.length; i < n; i++) {
                    this.out[i] = arr[i];
                }
            },
           randn: function (mu, std) {
                 fillRandn(this.out, mu, std);
                  return this;
           },
           grad: function (grad) {
                this.dout = grad;
            }
    }

В тензорном объекте n обозначает количество строк, а d обозначает количество столбцов, он также представляет depth в случае 3dim. И мы устанавливаем исходящую матрицу и ее градиент до нуля, используя функцию полезности zeros.

ПРИМЕЧАНИЕ: лучший способ пройти через это - просто следовать концепции, блок кода для служебной функции не будет вставлен, но вы можете получить их здесь

Теперь наше основное внимание должно быть сосредоточено на свойствах get и set, это показывает, как осуществляется доступ к значению тензора. Чтобы лучше понять это, давайте поработаем с базовым примером.

//given a matrix 3 x 3
[[2, 3, 4],
 [5, 6, 7],
 [8, 9, 10]]

Каким образом цель состоит в том, чтобы уменьшить указанную выше матрицу до;

[2,3,4,5,6,7,8,9,10]

Итак, чтобы получить доступ к этому массиву, мы умножаем количество столбцов и строку вместе, а затем перемещаемся в направлении столбца.

(col * row) + col_i

Приведенный выше код имеет значение при умножении точек. Теперь посмотрим, как это работает

//for 3 x 3 matrix we have n_col = 3, n_rows = 3
//hence the length of the matrix is 9
//to access any of the value, let say we want to access all values 
//for row 0
//values for row 0
3 * 0 + 0 = 0
3 * 0 + 1 = 1
3 * 0 + 2 = 2   // this are the index of the value of row 0
//value for row 1
3 * 1 + 0 = 3
3 * 1 + 1 = 4
3 * 1 + 2 = 5
//value for row 3
3 * 2 + 0 = 6
3 * 2 + 1 = 7
3 * 2 + 2 = 8

Приведенный выше код проходит через столбец и добавляет индекс к умножению столбцов и строк.

Поскольку тензорный объект был создан, теперь мы можем создать операторный объект для add и multi, что мы сделали для функции единственного значения (тензор), которую мы создали ранее.

function add(x, y) {
       assert(x.w.length === y.w.length);
       this.items = new Mat(1,x.w.length);
       for (var i = 0; i < x.w.length; i++) {
             this.items.out[i] = x.out[i] + y.out[i];
       }
      this.x = x;
      this.y = y;
      this.require_grad = true;
      this.out = this.items.out;
      this.dout = this.items.dout;
      this.n = this.items.n;
      this.d = this.items.d;
}
add.prototype = {
     backward: function () {
           if (this.x.require_grad) {
                 this.x.grad(this.dw);
                 if ("backward" in this.x) {
                           this.x.backward()
                 }
           }
          if (this.y.require_grad) {
                this.y.grad(this.dw);
                if ("backward" in this.y) {
                        this.y.backward()
                }
          }
      },
      grad: function (g) {
           assert(this.items.dw.length === g.length);
           this.dw = g;
      }
}

В этом новом объекте тензоров и оператора Istopped с использованием gradv для хранения градиента, я использую dout, чтобы вы знали, что градиент - это градиент out, который является выходом функции. Объект new Mat предназначен только для создания матрицы хранения. отметьте здесь, чтобы увидеть служебные функции.

И давайте создадим объект скалярного произведения с именем Multid

function Multid(x, y) {
        assert(x.d === y.n, "matmul dimension misaligned");
        this.n = x.n;
        this.d = y.d;
        this.x = x;
        this.y = y;
        this.require_grad = true;
        this.items = new Mat(this.n, this.d);
        this.out = this.items.out; 
        this.dout = this.items.dout
        for (var i = 0; i < x.n; i++) {
             for (var j = 0; j < y.d; j++) {
                 var dot = 0.0;
                 for (var k = 0; k < x.d; k++) {
                    dot += this.x.w[x.d * i + k] * this.y.w[y.d * k + j];
                  }
               this.w[this.d * i + j] = dot;
             }
       }
}
Multid.prototype = {
     backward: function () {
          if (this.x.require_grad) {
               for(var i = 0;i< this.x.n;i++){
                    for(var j=0;j<this.y.d;j++){
                        for(var k =0;k<this.x.d;k++){
                           var b = this.dout[this.y.d*i+j];
                           this.x.dout[this.x.d*i+k] += this.y.out[this.y.d*k+j] * b;
                     }
                }
           }
           if ("backward" in this.x) {
                  this.x.backward()
           }
           
          if (this.y.require_grad) {
              for(var i = 0;i< this.x.n;i++){
                 for(var j=0;j<this.y.d;j++){
                       for(var k =0;k<this.x.d;k++){
                         var b = this.dout[this.y.d*i+j];
                         this.y.dout[this.y.d*k+j] += this.x.out[this.x.d*i+k] * b;
                      }
            }
          }
          if ("backward" in this.y) {
                   this.y.backward()
           }
         }
    },
    grad: function (g) {
       assert(this.dw.length === g.length);
       this.dw = g;
     }
}

Теперь мы можем создать простой точечный продукт и пропустить его через него.

a = new TensorD(2,3)
a.setFrom([2,3,4,5,6,7])
b = new TensorD(3,4)
b.setFrom([4,5,6,7,8,9,1,2,4,7,1,4])
mult = new MultiD(a,b)
//output
[48, 65, 19, 36, 96, 128, 43, 75]

Выход [48, 65, 19, 36, 96, 128, 43, 75] изначально представлен как:

[[48, 65, 19, 36], 
 [96, 128, 43, 75]
]

Затем мы можем запустить обратный

gradm = new TensorD(2,4)
gradm.setFrom([1,1,1,1,1,1,1,1])
mult.grad(gradm)
mult.backward()

Вы можете проверить градиент a, который является скалярным произведением mult.dout и транспонированием b.out, что даст нам

[22, 20, 16, 22, 20, 16]
[[22, 20, 16], 
 [22, 20, 16]
]

Обратите внимание, что out и dout имеют одинаковую форму.

Я думаю, что этот простой подход и пример должны помочь нам познакомиться с концепцией Autograd.

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

var model = new Sequential([
         new Linear(2,3),
         new ReLU(),
        new Linear(3,2),
        new Softmax()
]);
var x = new Tensor(1,2,require_grad=true)
x.setFrom([2,3]);
model.forward(x)
var loss = new Loss(1,model.out)
loss.backward()
optim.step()

Фрагмент кода должен быть знаком пользователям pytorch.

Я мог бы вставить код, чтобы проследить за построением вышеуказанного блока кода, но при этом мы сделаем этот пост длинным, а формат javascript здесь не будет необычным.

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

Итак, чтобы получить полную картину того, как pytorch-подобная работа фреймов построена с помощью javascript, вы можете проверить реактивный документ здесь, чтобы прочитать и написать код вместе, или вы можете проверить его на github.