Допустим, у нас есть данные, созданные функцией y = 2x + 1.

var data = [1, 3, 5, 7, 9];

Хотя мы знаем функцию, которая сгенерировала данные, это не всегда так. Там будут некоторые данные, и мы захотим узнать, какой процесс за них отвечает. Мы хотим изучить функцию true, которая лежит в основе данных (в данном случае 2x + 1).

Один из подходов к этому называется одномерная линейная регрессия градиентным спуском. Название звучит пугающе, но на самом деле это просто означает найти строку, которая лучше всего объясняет данные (с наименьшим количеством ошибок или потерь). Это означает уравнение вида y = ax + b.

Для этого мы делаем предварительное предположение о весах. Веса в параметрическом уравнении, таком как приведенное выше, являются факторами, которые «взвешивают» важность параметров с точки зрения их влияния на выходную переменную. В нашем случае у нас есть два веса: a и b. В литературе они обычно обозначаются как w0, w1 и т. Д., Но, поскольку я предпочитаю условные обозначения, которые я усвоил в математике в средней школе, я буду придерживаться их. Итак, веса нашей скрытой истинной функции равны a = 2 и b = 1.

Поскольку нам не полагается знать истинную функцию, мы должны сделать первоначальное предположение о том, что это такое. Допустим, мы предполагаем, что это y = 2,3x + 0,5. Мы можем генерировать данные из этой модели, используя следующие переменные функции и состояния:

let a = 2.3;
let b = .5;
function imperfect(x) {
  return (a * x) + b;
};

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

function lossFunction() { 
  let totalLoss = 0;
  for (let x = 0; x < trueData.length; x += 1) {
    let y = trueData[x];
    let h = imperfect(x);
    let loss = (y - h) ** 2;
    totalLoss +=  loss;
  }
  return totalLoss;
}

Давайте попробуем построить эту функцию потерь для различных значений a, используя наши данные:

var labels = [];
var losses = [];
for (let i = 0; i < 5; i += 1) {
  a = i; // try different 'a' values
  var loss = lossFunction(); // calculate total loss
  losses.push(loss);
  labels.push(i);
}
var ctx = document.getElementById("chart").getContext('2d');
function generateGraph() {
  var h1Chart = new Chart(ctx, {
    type: 'line',
    data: {
    labels,
    datasets: [{
      label: 'losses for different values of a',
      data: losses,
      borderColor: 'red'
    }]
    }
  });
}
generateGraph();

Если вы хотите попробовать себя, вам также понадобится index.html из последней статьи:

<!DOCTYPE html>
<html>
<head>
  <title>Posterior Estimation</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script   src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.min.js"></script>
</head>
<body>
  <canvas id="chart" width="400" height="400"></canvas>
  <script src="index.js"></script>
</body>
</html>

Мы знаем, что истинное значение a должно быть 2…

Вполне нормально! По-прежнему есть некоторые базовые потери из-за того, что мы еще ничего не сделали с нашей первоначальной оценкой для b. Вот почему оптимальное значение для a здесь не 2, а что-то немного больше 2.

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

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

h (xj) выше - это «несовершенная» линейная функция, которую мы определили ранее. Теперь нам нужно сделать что-то, что звучит сложно, но на самом деле это не так. Дело в том, что мы сохраняем один параметр постоянным и берем производную по другому. Это называется взятием частной производной. Выглядит это так:

Это приблизительно говорит нам, что производная x² равна 2x. Поскольку функция потерь выглядит как парабола, этого и следовало ожидать. Частная производная почти такая же, но даже проще: вместо того, чтобы b быть константой на шаге 3, axj остается постоянной. Таким образом, вся правая часть становится 1, а производная становится:

Эти уравнения могут вызвать у вас тошноту, но, поверьте, они намного привлекательнее, если их смоделировать. Вот как я превратил их в код:

function dloss_da() {
  let dloss_da_total = 0;
  for (let x = 0; x < data.length; x += 1) {
    let y = data[x];
    let h = imperfect(x);
    let dloss = -2 * (y - h) * x;
    dloss_da_total +=  dloss;
  }
  return dloss_da_total;
}
function dloss_db() {
  let dloss_db_total = 0;
  for (let x = 0; x < data.length; x += 1) {
    let y = data[x];
    let h = imperfect(x);
    let dloss = -2 * (y - h);
    dloss_db_total +=  dloss;
  }
  
  return dloss_db_total;
}

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

Давайте попробуем построить график только для одного из весов, a:

var losses = [];
var derivatives = [];
var labels = [];
var step = -.01;
for (let trial = 0; trial < 10; trial += 1) {
  var loss = lossFunction();
  var derivative = dloss_da();
  losses.push(loss);
  derivatives.push(derivative);
  labels.push(trial + 1);
  a += step*derivative;  
}
var ctx = document.getElementById("chart").getContext('2d');
function generateGraph() {
  var h1Chart = new Chart(ctx, {
    type: 'line',
    data: {
      labels,
      datasets: [{
        label: 'losses for trial',
        data: losses,
        borderColor: 'red'
      }, {
        label: 'derivative with respect to a',
        data: derivatives,
        borderColor: 'blue'
      }]
    }
  });
}
generateGraph();

И убыток, и скорость изменения убытка приближаются к нулю по мере увеличения количества раундов. Мы можем сделать то же самое с другим весом b. Просто измените dloss_da на dloss_db и a на b:

Эффект менее выражен, потому что b оказывает меньшее влияние, чем a на вывод. Теперь давайте объединим результаты и будем корректировать веса для a и b в каждом раунде:

var losses = [];
var derivatives_a = [];
var derivatives_b = [];
var labels = [];
var step = -.01;
for (let trial = 0; trial < 10; trial += 1) {
  var loss = lossFunction();
  var derivative_a = dloss_da();
  var derivative_b = dloss_db();
  losses.push(loss);
  derivatives_a.push(derivative_a);
  derivatives_b.push(derivative_b);
  labels.push(trial + 1);
  a += step*derivative;
  b += step*derivative_b;
}
var ctx = document.getElementById("chart").getContext('2d');
function generateGraph() {
  var h1Chart = new Chart(ctx, {
    type: 'line',
    data: {
      labels,
      datasets: [{
        label: 'losses for trial',
        data: losses,
        borderColor: 'red'
      }, {
        label: 'derivative with respect to a',
        data: derivatives_a,
        borderColor: 'blue'
      }, {
        label: 'derivative with respect to b',
        data: derivatives_b,
        borderColor: 'yellow'
      }]
    }
  });
}
generateGraph();

Если мы увеличим количество попыток до 1000, я получу a = 2,000000000000000338 и b = 0,9999999999999039.

Заключение

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

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

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

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

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