Тесты

L1- и L2-регуляризация в машинном обучении

Джеймс Маккафри

Исходный код можно скачать по ссылке

James McCaffreyL1- и L2-регуляризация — эта два тесно связанных метода, которые можно применять в алгоритмах машинного обучения (machine learning, ML) для уменьшения степени переобучения модели (model overfitting). Исключение переобучения приводит к генерации модели, которая обеспечивает более качественное прогнозирование. В этой статье я расскажу, что такое регуляризация с точки зрения разработчика ПО. Объяснить идеи, лежащие в основе регуляризации, весьма непросто — и не потому, что они трудны, а скорее потому, что существует несколько не связанных друг с другой идей.

Я проиллюстрирую регуляризацию с помощью классификации по логистической регрессии (logistic regression, LR), но регуляризацию можно использовать со многими типами машинного обучения, в частности для классификации нейронной сети. Цель LR-классификации — создать модель, предсказывающую значение переменной, которая принимает одно из двух возможных значений. Допустим, вам требуется спрогнозировать результат футбольной команды (проигрыш = 0, выигрыш = 1) в предстоящей игре, основываясь на текущей доле выигрышей команды (x1), расположении поля (x2) и количестве игроков, отсутствующих из-за травм (x3).

Если Y — прогнозируемое значение, то LR-модель для этой задачи принимает вид:

z = b0 + b1(x1) + b2(x2) + b3(x3)
Y = 1.0 / (1.0 + e^-z)

Здесь b0, b1, b2 и b3 — веса, которые являются просто числовыми значениями, которые нужно определить. Если на словах, то вы вычисляете значение z, которое представляет собой сумму входных значений, помноженных на b-веса, потом добавляете константу b0 и передаете значение z в уравнение, которое использует математическую константу e. Оказывается, что Y всегда будет находиться между 0 и 1. Если Y меньше 0.5, вы заключаете, что прогнозируемый вывод равен 0, а если Y больше 0.5 — что такой вывод равен 1. Заметьте, что для n функций (features) будет n+1 b-весов.

Например, доля выигрышей команды на данный момент составляет 0.75, она будет играть на поле противника (–1), а три игрока выбыли из-за травм. Теперь предположим, что b0 = 5.0, b1 = 8.0, b2 = 3.0, and b3 = –2.0. Тогда z = 5.0 + (8.0)(0.75) + (3.0)(–1) + (–2.0)(3) = 2.0, а значит Y = 1.0 / (1.0 + e^–2.0) = 0.88. Поскольку Y больше 0.5, вы прогнозируете, что команда победит в предстоящей игре.

Полагаю, что лучший способ объяснить регуляризацию — исследовать конкретный пример. Взгляните на экранный снимок демонстрационной программы (рис. 1). Вместо использования реальных данных демонстрационная программа начинает с генерации 1000 элементов синтетических данных. В каждом элементе имеется 12 переменных-предикторов (часто называемых функциями [features] в терминологии ML). Значение зависимой переменной находится в последнем столбце. После создания 1000 элементов данных этот набор случайным образом разбивается на обучающий набор с 800 элементами (для нахождения b-весов модели) и проверочный (тестовый) набор с 200 элементами (для оценки качества получаемой модели).

Регуляризация с классификацией по логистической регрессии
Рис. 1. Регуляризация с классификацией по логистической регрессии

Затем программа обучила LR-классификатор без применения регуляризации. Полученная модель имела точность 85.00% на обучающих данных и 80.50% на проверочных. Величина в 80.50% более релевантна из этих двух значений и является примерной оценкой того, какую точность вы могли бы ожидать от модели на новых данных. Как я вскоре поясню, модель была переобучена, что привело к посредственной точности прогнозирования.

Потом программа выполнила кое-какую обработку, чтобы найти хорошее весовое значение L1-регуляризации и хорошее весовое значение L2-регуляризации. Веса регуляризации — это отдельные числовые значения, используемые в процессе регуляризации. В демонстрации хорошее L1-весовое значение было определено равным 0.005, а хорошее L2-весовое значение — 0.001.

Сначала программа выполнила обучение с использованием L1-регуляризации, а затем повторила его с применением L2-регуляризации. В случае L1-регуляризации полученная LR-модель давала точность 95.00% на проверочных данных, а в случае L2-регуляризации LR-модель обеспечивала точность 94.50% на тех же данных. Оба вида регуляризации существенно улучшили точность прогнозирования.

В этой статье предполагается, что вы умеете программировать хотя бы на среднем уровне, но ничего не знаете об L1- или L2-регуляризации. Демонстрационная программа написана на C#, но у вас не должно возникнуть особых проблем, если вы захотите выполнить рефакторинг кода под другой язык вроде Visual Basic .NET или Python. Демонстрационная программа слишком длинная, чтобы ее можно было представить в статье во всей ее полноте, но вы можете найти полный исходный код в сопутствующем этой статье пакете кода. Я убрал стандартную обработку ошибок, чтобы по возможности не затуманивать основные идеи и сократить размер кода в статье.

Общая структура программы

Общая структура программы с небольшими правками для экономии места представлена на рис. 2. Чтобы создать демонстрационную программу, я запустил Visual Studio и создал новое консольное приложение на C# с названием Regularization. В этой программе нет значимых зависимостей от .NET, поэтому подойдет любая недавняя версия Visual Studio.

Рис. 2. Общая структура программы

using System;
namespace Regularization
{
  class RegularizationProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin L1 and L2 Regularization demo");
      int numFeatures = 12;
      int numRows = 1000;
      int seed = 42;
      Console.WriteLine("Generating " + numRows +
        " artificial data items with " + numFeatures + " features");
      double[][] allData = MakeAllData(numFeatures, numRows, seed);
      Console.WriteLine("Creating train and test matrices");
      double[][] trainData;
      double[][] testData;
      MakeTrainTest(allData, 0, out trainData, out testData);
      Console.WriteLine("Training data: ");
      ShowData(trainData, 4, 2, true);
      Console.WriteLine("Test data: ");
      ShowData(testData, 3, 2, true);
      Console.WriteLine("Creating LR binary classifier");
      LogisticClassifier lc = new LogisticClassifier(numFeatures);
      int maxEpochs = 1000;
      Console.WriteLine("Starting training using no regularization");
      double[] weights = lc.Train(trainData, maxEpochs,
        seed, 0.0, 0.0);
      Console.WriteLine("Best weights found:");
      ShowVector(weights, 3, weights.Length, true);
      double trainAccuracy = lc.Accuracy(trainData, weights);
      Console.WriteLine("Prediction accuracy on training data = " +
        trainAccuracy.ToString("F4"));
      double testAccuracy = lc.Accuracy(testData, weights);
      Console.WriteLine("Prediction accuracy on test data = " +
        testAccuracy.ToString("F4"));
      Console.WriteLine("Seeking good L1 weight");
      double alpha1 = lc.FindGoodL1Weight(trainData, seed);
      Console.WriteLine("L1 weight = " + alpha1.ToString("F3"));
      Console.WriteLine("Seeking good L2 weight");
      double alpha2 = lc.FindGoodL2Weight(trainData, seed);
      Console.WriteLine("L2 weight = " + alpha2.ToString("F3"));
      Console.WriteLine("Training with L1 regularization, " +
        "alpha1 = " + alpha1.ToString("F3"));
      weights = lc.Train(trainData, maxEpochs, seed, alpha1, 0.0);
      Console.WriteLine("Best weights found:");
      ShowVector(weights, 3, weights.Length, true);
      trainAccuracy = lc.Accuracy(trainData, weights);
      Console.WriteLine("Prediction accuracy on training data = " +
        trainAccuracy.ToString("F4"));
      testAccuracy = lc.Accuracy(testData, weights);
      Console.WriteLine("Prediction accuracy on test data = " +
        testAccuracy.ToString("F4"));
      Console.WriteLine("Training with L2 regularization, " +
        "alpha2 = " + alpha2.ToString("F3"));
      weights = lc.Train(trainData, maxEpochs, seed, 0.0, alpha2);
      Console.WriteLine("Best weights found:");
      ShowVector(weights, 3, weights.Length, true);
      trainAccuracy = lc.Accuracy(trainData, weights);
      Console.WriteLine("Prediction accuracy on training data = " +
        trainAccuracy.ToString("F4"));
      testAccuracy = lc.Accuracy(testData, weights);
      Console.WriteLine("Prediction accuracy on test data = " +
        testAccuracy.ToString("F4"));
      Console.WriteLine("End Regularization demo");
      Console.ReadLine();
    }
    static double[][] MakeAllData(int numFeatures,
      int numRows, int seed) { . . }
    static void MakeTrainTest(double[][] allData, int seed,
      out double[][] trainData, out double[][] testData) { . . }
    public static void ShowData(double[][] data, int numRows,
      int decimals, bool indices) { . . }
    public static void ShowVector(double[] vector, int decimals,
      int lineLen, bool newLine) { . . }
  }
  public class LogisticClassifier
  {
    private int numFeatures;
    private double[] weights;
    private Random rnd;
    public LogisticClassifier(int numFeatures) { . . }
    public double FindGoodL1Weight(double[][] trainData,
      int seed) { . . }
    public double FindGoodL2Weight(double[][] trainData,
      int seed) { . . }
    public double[] Train(double[][] trainData, int maxEpochs,
      int seed, double alpha1, double alpha2) { . . }
    private void Shuffle(int[] sequence) { . . }
    public double Error(double[][] trainData, double[] weights,
      double alpha1, double alpha2) { . . }
    public double ComputeOutput(double[] dataItem,
      double[] weights) { . . }
    public int ComputeDependent(double[] dataItem,
      double[] weights) { . . }
    public double Accuracy(double[][] trainData,
      double[] weights) { . . }
    public class Particle { . . }
  }
} // ns

После загрузки кода шаблона в редактор Visual Studio я переименовал в окне Solution Explorer файл Program.cs в более описательный RegularizationProgram.cs, и Visual Studio автоматически переименовала класс Program за меня. В начале кода я удалил все выражения using, которые указывали на ненужные пространства имен, оставив только ссылку на пространство имен верхнего уровня System.

Вся логика логистической регрессии содержится в классе LogisticClassifier. В этот класс вложен вспомогательный класс Particle, инкапсулирующий оптимизацию роя частиц (particle swarm optimization, PSO) — алгоритм оптимизации, применяемый для обучения. Заметьте, что в классе LogisticClassifier есть метод Error, который принимает параметры с именами alpha1 и alpha2. Эти параметры являются весовыми значениями для L1- и L2-регуляризации соответственно.

В методе Main синтетические данные создаются следующими выражениями:

int numFeatures = 12;
int numRows = 1000;
int seed = 42;
double[][] allData = MakeAllData(numFeatures, numRows, seed);

Значение seed, равное 42, использовалось только потому, что это значение давало репрезентативный демонстрационный вывод. Метод MakeAllData генерирует 13 случайных весовых значений между –10.0 и +10.0 (по одному весовому значению на каждую «функцию» плюс весовое значение b0). Затем метод осуществлял итерации 1000 раз. На каждой итерации генерировался случайный набор из 12 входных значений, после чего вычислялось промежуточное выходное значение логистической регрессии, используя случайные весовые значения. К выводу добавлялось дополнительное случайное значение, чтобы сделать данные менее очевидными и более подверженными переобучению.

Данные разбиваются на обучающий набор с 800 элементами и набор из 200 элементов для оценки модели с помощью этих выражений:

double[][] trainData;
double[][] testData;
MakeTrainTest(allData, 0, out trainData, out testData);

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

LogisticClassifier lc = new LogisticClassifier(numFeatures);
int maxEpochs = 1000;
double[] weights = lc.Train(trainData, maxEpochs, seed, 0.0, 0.0);
ShowVector(weights, 4, weights.Length, true);

Переменная maxEpochs — это счетчик цикла, ограничивающий значение для алгоритма обучения PSO. Два аргумента 0.0, передаваемые методу Train, являются весовыми значениями для L1- и L2- регуляризации. При значениях 0.0 регуляризация не используется. Качество модели оценивается так:

double trainAccuracy = lc.Accuracy(trainData, weights);
double testAccuracy = lc.Accuracy(testData, weights);

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

double alpha1 = lc.FindGoodL1Weight(trainData, seed);
weights = lc.Train(trainData, maxEpochs, seed, alpha1, 0.0);
trainAccuracy = lc.Accuracy(trainData, weights);
testAccuracy = lc.Accuracy(testData, weights);

Выражения для обучения LR-классификатора с применением L2-регуляризации точно такие же, как в случае L1-регуляризации:

double alpha2 = lc.FindGoodL2Weight(trainData, seed);
weights = lc.Train(trainData, maxEpochs, seed, 0.0, alpha2);
trainAccuracy = lc.Accuracy(trainData, weights);
testAccuracy = lc.Accuracy(testData, weights);

В демонстрационной программе значения alpha1 и alpha2 были определены с помощью открытых методов FindGoodL1Weight и FindGoodL2Weight LR-объекта, а затем переданы методу Train. Альтернативный вариант — вызов этого кода:

bool useL1 = true;
bool useL2 = false:
lc.Train(traiData, maxEpochs, useL1, useL2);

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

Понимание регуляризации

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

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

Переобучение модели
Рис. 3. Переобучение модели

Ground truth fit model Модель, соответствующая контрольным данным
Overfitted model Переобученная модель

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

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

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

computed  desired
  0.60      1.0
  0.30      0.0
  0.80      1.0

А такой была бы среднеквадратичная ошибка:

((0.6 - 1.0)^2 + (0.3 - 0.0)^2 + (0.8 - 1.0)^2) / 3 =
(0.16 + 0.09 + 0.04) / 3 =
0.097

В символическом выражении среднеквадратичную ошибку можно описать как:

E = Sum(o - t)^2 / n

где Sum представляет кумулятивную сумму по всем обучающим данным, o — вычисленный вывод, t — целевой вывод, а n — количество элементов в обучающих данных. Ошибка — это то, что минимизируется обучением с помощью одного из примерно десятка численных методов вроде градиентного спуска (gradient descent), итерационного алгоритма Ньютона-Рафсона (iterative Newton-Raphson), L-BFGS, обратного распространения ошибок (back-propagation) и оптимизации роя (swarm optimization).

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

E = Sum(o - t)^2 / n + Sum(Abs(w))

L2-регуляризация выполняет аналогичную операцию добавлением суммы их квадратов к ошибке. В символьном виде это выглядит следующим образом:

E = Sum(o - t)^2 / n + Sum(w^2)

Допустим в этом примере, что нам нужно определить четыре весовых значения и что их текущие значения (2.0, –3.0, 1.0, –4.0). Штраф в L1, добавленный к 0.097 (среднеквадратичной ошибке) был бы (2.0 + 3.0 + 1.0 + 4.0) = 10.0, а штраф в L2 составил бы 2.0^2 + –3.0^2 + 1.0^2 + –4.0^2 = 4.0 + 9.0 + 1.0 + 16.0 = 30.0.

Итак, большие весовые значения модели могут приводить к переобучению, что в свою очередь ведет к низкой точности прогнозирования. Регуляризация ограничивает величину весовых значений модели, добавляя штраф для весов к функции ошибки модели. В L1-регуляризации используется сумма абсолютных значений весовых значений, а в L2-регуляризации — сумма квадратов весовых значений.

Зачем нужны два вида регуляризации?

L1- и L2-регуляризация — процессы схожие. Какой из них лучше? Хотя существуют кое-какие теоретические правила насчет того, какая регуляризация лучше для определенных задач, по моему мнению, на практике вам придется поэкспериментировать, чтобы найти, какой тип регуляризации лучше в вашем случае и стоит ли вообще использовать какую-либо регуляризацию.

Как выяснилось, применение L1-регуляризации иногда может давать полезный побочный эффект, вызывающий стремление одного или более весовых значений к 0.0, а это означает, что соответствующая функция (feature) не требуется. Это одна из форм того, что называют селекцией функций (feature selection). Например, в демонстрационной программе на рис. 1 с L1-регуляризацией последнее весовое значение модели равно 0.0. То есть последнее значение предиктора не вносит вклад в LR-модель. L2-регуляризация ограничивает весовые значения модели, но обычно не приводит к полному обнулению этих значений.

Поэтому может показаться, что L1-регуляризация лучше L2-регуляризации. Однако недостаток применения L1-регуляризации в том, что этот метод не так-то просто использовать с некоторыми алгоритмами ML-обучения, в частности с теми, в которых используются численные методы для вычисления так называемого градиента. L2-регуляризацию можно использовать с любым типом алгоритма обучения.

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

Реализация регуляризации

Реализовать L1- и L2-регуляризацию сравнительно легко. В демонстрационной программе используется обучение по алгоритму PSO с явной функцией ошибки, нужно лишь добавить весовые штрафы L1 и L2. Определение метода Error начинается так:

public double Error(double[][] trainData, double[] weights,
  double alpha1, double alpha2)
{
  int yIndex = trainData[0].Length - 1;
  double sumSquaredError = 0.0;
  for (int i = 0; i < trainData.Length; ++i)
  {
    double computed = ComputeOutput(trainData[i], weights);
    double desired = trainData[i][yIndex];
    sumSquaredError += (computed - desired) * (computed - desired);
  }
...

Первый шаг — вычисление среднеквадратичной ошибки суммированием квадратов разности между вычисленными и целевыми выходными значениями. (Другая распространенная форма ошибки называется ошибкой перекрестной энтропии [cross-entropy error].) Затем вычисляется штраф по L1:

double sumAbsVals = 0.0; // L1 penalty
for (int i = 0; i < weights.Length; ++i)
  sumAbsVals += Math.Abs(weights[i]);

и штраф по L2:

double sumSquaredVals = 0.0; // L2 penalty
for (int i = 0; i < weights.Length; ++i)
  sumSquaredVals += (weights[i] * weights[i]);

Метод Error возвращает MSE плюс штрафы:

...
  return (sumSquaredError / trainData.Length) +
    (alpha1 * sumAbsVals) +
    (alpha2 * sumSquaredVals);
}

Демонстрационная программа использует явную функцию ошибки. В некоторых алгоритмах обучения, таких как градиентный спуск и обратное распространение ошибок, функция ошибки применяется неявно за счет исчисления частного производного (calculus partial derivative) (называемого градиентом) функции ошибки. Для таких алгоритмов обучения использование L2-регуляризации (поскольку производное w^2 равно 2w) требует от вас лишь добавить член 2w к градиенту (хотя детали могут быть весьма хитроумными).

Нахождение хороших весовых значений регуляризации

Есть несколько способов найти хорошее (но не обязательно оптимальное) весовое значение регуляризации. Демонстрационная программа задает набор значений-кандидатов, вычисляет ошибку, связанную с каждым из кандидатов и возвращает лучший из найденных кандидатов. Метод поиска хорошего L1-веса начинается так:

public double FindGoodL1Weight(double[][] trainData, int seed)
{
  double result = 0.0;
  double bestErr = double.MaxValue;
  double currErr = double.MaxValue;
  double[] candidates = new double[] { 0.000, 0.001, 0.005,
    0.010, 0.020, 0.050, 0.100, 0.150 };
  int maxEpochs = 1000;
  LogisticClassifier c =
    new LogisticClassifier(this.numFeatures);

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

for (int trial = 0; trial < candidates.Length; ++trial) {
    double alpha1 = candidates[trial];
    double[] wts = c.Train(trainData, maxEpochs, seed, alpha1, 0.0);
    currErr = Error(trainData, wts, 0.0, 0.0);
    if (currErr < bestErr) {
      bestErr = currErr; result = candidates[trial];
    }
  }
  return result;
}

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

Заключение

Регуляризацию можно применять с любым методом ML-классификации, который основан на математическом уравнении. Примеры включают логистическую регрессию, пробит-классификацию (probit classification) и нейронные сети. Поскольку это уменьшает величину весовых значений в модели, регуляризацию иногда называют сокращением весов (weight decay). Основное преимущество применения регуляризации в том, что оно часто приводит к созданию более точной модели. Главный недостаток заключается во введении дополнительного параметра, значение которого нужно определить, — весового значения регуляризации. В случае логистической регрессии это не слишком серьезно, так как в этом алгоритм обычно используется лишь параметр скорости обучения, но при использовании более сложного метода классификации, в частности нейронных сетей, добавление еще одного так называемого гиперпараметра (hyperparameter) может потребовать массы дополнительной работы для подбора комбинации значений двух параметров.


Джеймс Маккафри (Dr. James McCaffrey) работает на Microsoft Research в Редмонде (штат Вашингтон). Принимал участие в создании нескольких продуктов Microsoft, в том числе Internet Explorer и Bing. С ним можно связаться по адресу jammc@microsoft.com.

Выражаю благодарность за рецензирование статьи эксперту Microsoft Ричарду Хьюзу (Richard Hughes).