Стэнфордский курс: лекция 3. Функция потерь и оптимизация
На прошлой лекции мы разобрались, как работает классификация изображений. Что же может «потеряться» в процессе и можно ли этого избежать? Из сегодняшнего урока вы узнаете, как сделать обучение классификатора более точным и эффективным с помощью функции потерь и оптимизации.
Предыдущие лекции:
Лекция 1. Введение
Лекция 2. Классификация изображений
Что делать, если “cat” классифицируется как “dog”
Вспомним о линейном классификаторе. Мы выяснили, что он состоит из двух частей: входных данных X и набора весов W. Но мы не раскрыли главный вопрос: как именно нужно использовать обучающие данные, чтобы определить значения W, с которыми классификатор будет работать лучше всего?
Рассмотрим простой пример. Пусть обучающие данные состоят всего из трёх изображений и 10 исходных классов, а параметры W выбраны произвольно. Вы можете заметить, что некоторые из полученных оценок сильно выше или ниже, чем другие:
Например, изображение с кошкой получило оценку 2.9 для метки “cat” и 8.02 для метки “dog”. Картинка с лягушкой и вовсе получила отрицательную оценку для своего класса. А фото с автомобилем действительно достигает наивысшей оценки для метки “automobile”. Это говорит о том, что наш классификатор работает не очень хорошо.
Чтобы автоматически определить, какие значения W будут лучшими, нам нужен какой-то способ количественной оценки «непригодности» для любого W. И этим способом будет специальная функция, которая принимает на вход значения W и определяет, насколько плохо они работают. В машинном обучении она называется функцией потерь (loss function).
Мы рассмотрим несколько примеров, которые вы можете использовать для классификации изображений. Если функция потерь выдаёт слишком большие числа — значит, выбранные параметры работают плохо. Поиск правильных весов W, при которых функция достигает минимума, называется оптимизацией.
Функция потерь. Немного математики
Упростим задачу и из 10 классов оставим только три. Для линейного классификатора f(x,W) = Wx с произвольными W получим следующее:
Оценки снова не очень хорошие.
Дадим более конкретное определение функции потерь. Обычно у нас есть набор из N значений обучающих данных X и y, где X — исходные изображения (а точнее, значения их пикселей), а y — те категории, которые алгоритм должен предсказать (их называют метки классов или целевые переменные). В процессе классификации мы получаем оценки от 1 до 10 (или от 0 до 9, в зависимости от языка программирования) для каждой из категорий, которые показывают вероятность принадлежности изображения xi к классу yi.
Обозначим функцию потерь как Li. Наш классификатор f берёт пример xi, весовую матрицу W и делает прогноз для класса yi. Функция потерь Li обрабатывает полученные результаты, сравнивая их с заранее известными истинными метками. Это поможет оценить, насколько плохо или хорошо выполняется прогноз для текущего обучающего примера xi.
Наконец, общие потери L можно посчитать как среднюю сумму потерь для всех обучающих данных:
Пример работы функции потерь
Пример, который хорошо подойдёт для классификации изображений, — функция мультиклассовых потерь SVM (Support Vector Machine, метод опорных векторов). Разберёмся, как она работает.
Снова возьмём наши обучающие примеры xi и метки классов yi. Представим прогнозы классификатора (оценки) в виде векторов: s = f(xi, W). Для каждого отдельного примера просуммируем значения оценок всех y, кроме истинной метки yi. То есть, мы получим сумму значений всех неправильных категорий.
Теперь посчитаем разницу между ложными прогнозами и истинной оценкой: sj − syi; j≠yi. Если истинная оценка больше, чем сумма всех неправильных прогнозов плюс некоторый дополнительный «зазор» (установим его равным 1) — значит, полученный балл для правильной категории намного превосходит любую ошибочную оценку. Это и будет наша функция потерь.
В символьном виде это выглядит так:
То же самое можно представить в виде функции максимума (вторая строка на картинке выше). Посмотрим на пример с нашими тремя классами:
Истинная метка для изображения — “cat”, её оценка — 3.2. Мы вычитаем правильную оценку из всех неправильных и сравниваем: если разность больше нуля, значит, метка определена ошибочно и потери присутствуют, если меньше — всё хорошо, потери нулевые. После этого суммируем результаты. Для класса “cat” значение функции потерь равно 2.9.
То же самое для автомобиля:
В этом случае потери оказались нулевыми, потому что метка определена корректно. Но вот для лягушки всё совсем плохо:
Если мы посчитаем общие потери по всем классам, то получим следующее:
Классификатор получил оценку 5.27 — значит, он работает неважно. Из предыдущих примеров ясно, что потери должны стремиться к нулю.
Разминка: вопросы и ответы
Попробуйте ответить на несколько вопросов, которые помогут вам интуитивно понять, что же делают потери. Считать ничего не надо, только немного подумать:
1. Что произойдёт, если совсем немного изменить оценки классов для изображения с машиной? (Ответ: ничего, потери останутся нулевыми.)
2. Какие могут быть максимальные и минимальные SVM-потери? (Ответ: 0 и ∞. Верхней границы для потерь нет, а вот нижняя ограничена нулём.)
3. Если все оценки будут примерно равны нулю и примерно равны между собой, чему будут равны потери? (Ответ: значение потерь будет на единицу меньше, чем число классов. Не забудьте, что помимо sj − syi есть ещё + 1.)
4. Как изменятся потери, если суммировать все оценки, включая истинную метку yi? (Ответ: увеличатся на единицу, потому что syi − syi + 1 = 1.)
5. Что будет, если для общих потерь вместо суммы использовать среднее значение? (Ответ: ничего не изменится, по сути эта формула — и есть поиск среднего.)
Пишите в комментариях, получилось ли у вас самостоятельно ответить на вопросы. Если возникли затруднения — укажите, что вам показалось непонятным, и мы обязательно всё разъясним.
Для наглядности — пример кода на Python, иллюстрирующий работу функции потерь:
Как видите, описанный выше алгоритм реализуется в несколько строк. Чтобы получить минимальные потери, можно поместить эту функцию в цикл, подбирая и отправляя в неё различные W.
Теперь ещё один вопрос — представьте, что вам повезло и вы нашли веса W, при которых потери оказались нулевыми. Будут ли значения W уникальными?
Правильный ответ — нет. Не забудьте, что W — это матрица, для которой работают правила линейной зависимости. Поэтому вы получите нулевые потери и для 2W, и для 4W, и так далее.
Наглядное подтверждение:
Проблема переобучения
Основной результат, который даёт функция потерь — оценка того, насколько хорошо классификатор работает на обучающей выборке. Но мы помним, что главная цель машинного обучения — заставить алгоритм правильно работать на тестовой выборке. Поэтому хорошо справляющийся с обучающими данных метод может совершенно не работать на новых объектах. Это явление называется «переобучение».
Представим, что наш датасет состоит из некоторых точек. Если мы заставим алгоритм идеально подстраиваться под каждую из точек с нулевыми потерями, то график классификации превратится в извилистую кривую:
Но это плохой результат, потому что нам важна точность на тестовой выборке, а не на обучающей. Если мы проверим работу на новых данных (отмечены зелёными квадратами на рисунке ниже), то синяя кривая станет абсолютно неправильной:
Скорее всего, классификатор должен найти какую-то прямую линию, которая примерно соответствовала бы и тем, и другим данным:
В этом заключается фундаментальная проблема машинного обучения. Для её решения обычно применяют различные методы регуляризации.
Регулируй и властвуй
Выше было сказано, что есть множество вариантов оптимальных весов W. Но как именно функция потерь должна выбрать из них один?
Специально для этого вводится параметр регуляризации — он заставляет модель выбирать такие веса W, при которых решение будет наиболее простым. Понятие простоты зависит от задачи, решаемой алгоритмом.
Эта идея относится к принципу «бритвы Оккама» — если ваши наблюдения можно объяснить несколькими гипотезами, следует выбрать из них самую простую. В случае классификации нужно выбирать наиболее простое разделение данных — в примере выше это должна быть прямая линия, а не кривая.
Итак, наша функция теперь состоит из двух компонент: потерь на обучающих данных (суммы всех Li) и потерь регуляризации R(W):
Гиперпараметр лямбда отвечает за то, насколько сильно регуляризация будет влиять на модель. Подробнее о гиперпараметрах мы говорили на прошлой лекции.
Теперь посмотрим, какие бывают функции R(W) и как они работают.
Примеры регуляризации
На практике используется довольно много видов регуляризации, но мы приведём только наиболее популярные.
Регуляризация L2
Применяется в машинном обучении довольно часто. Это обычная евклидова норма параметров W (иногда берётся квадрат или половина квадрата нормы). Идея в том, что вы просто добавляете «штраф» к весам, чтобы их значения не оказались слишком большими. Функция R(W) выглядит следующим образом:
Регуляризация L1
L1 использует манхэттенскую норму W и тоже добавляет штраф к весовым коэффициентам. С регуляризацией L1 матрица параметров будет более разреженной.
Эластичная сеть
Это комбинация регуляризаций L1 и L2:
Существуют и другие виды регуляризаций, такие как максимум нормы, Dropout, пакетная нормализация и стохастическая глубина. Они применяются в более узких задачах глубокого обучения, которые мы рассмотрим на следующих лекциях.
Больше примеров
Если вы всё равно не поняли, как работает регуляризация, — не волнуйтесь. Сейчас мы объясним на ещё более простом примере.
Возьмём примитивный набор обучающих данных x, состоящий из четырёх единиц. Также создадим два вектора весов w1=[1,0,0,0] и w2=[0.25, 0.25, 0.25, 0.25]. Поскольку мы рассматриваем случай линейной классификации f(x,W) = Wx, то можем увидеть, что если умножить любой из векторов w1 или w2 на вектор x, результат будет одинаковый:
Внимание, вопрос: какой из этих двух векторов должен выбрать регуляризатор L2?
Ответ: вектор w2, поскольку он имеет наименьшую норму.
На предыдущей лекции мы упоминали, что веса W определяют, насколько конкретный элемент обучающей выборки соответствует одному из классов. Цель регуляризации L2 — убедиться, что это правило выполняется для всех элементов. В примере выше значения вектора x одинаковые и явно должны относиться к одному классу, поэтому и веса для них должны быть равны.
Оптимизация
Теперь мы знаем, что значения параметров W надо выбирать с умом. Но мы всё ещё не нашли ответ на вопрос: «Как именно нужно искать те самые веса W, которые минимизируют потери?» Рассмотрим несколько примеров.
Представьте, что вы идёте по большой долине среди гор, полей и рек. И высота каждого объекта этого ландшафта соответствует объёму потерь, которые появляются при определённой настройке W. Поскольку вы хотите достичь наименьших потерь, вам каким-то образом нужно найти самую низкую точку в долине.
К сожалению, не существует специального минимизатора, который телепортирует вас к этой точке. Поэтому на практике процесс оптимизации выглядит как набор итеративных операций, где в качестве начальной точки берётся случайное решение, которое вы начинаете постепенно улучшать.
Стратегия 1. Первое, что приходит в голову — рандомный поиск. Попробуем просто генерировать случайные значения W и смотреть, насколько хорошо они работают. Спойлер: это плохая идея, которую не стоит применять на практике.
Например, для 10 классов датасета CIFAR-10 вероятность того, что вы найдёте подходящие параметры W, составит 10%. И это очень плохой показатель.
Стратегия 2. Вернёмся к нашей долине. Возможно, вы не видите прямого пути к её самой низкой точке, но вы можете оценить геометрию ландшафта и предположить, в каком направлении следует идти.
Стоя на склоне, вы начнёте искать спуск.
В математике аналогом поиска спуска является производная. Взяв одномерную функцию f(x) и вычислив её производную в какой-либо точке, мы можем определить, возрастает или убывает функция в этой точке.
Но в машинном обучении x обычно является вектором, поэтому обобщённым аналогом спуска будет вектор частных производных или градиент. Градиент указывает направление возрастания функции, поэтому для поиска её убывания используют отрицательный градиент.
В поисках градиента
Итак, теперь у нас есть вектор параметров W, и наша цель — вычислить вектор градиента 𝛁W. Можно просто взять производные для каждого отдельного элемента. Так мы узнаем, насколько изменятся потери, если мы переместимся на бесконечно малую величину h в одном из направлений координат. Такой подход называется «числовой градиент».
В большинстве реальных случаев эти расчёты будут слишком медленными. Ведь в обучающих данных иногда содержатся миллионы примеров, а алгоритмы их обработки могут быть гораздо сложнее. Для решения проблемы существует так называемый «аналитический градиент».
Если вы когда-то проходили курс математического анализа, то знаете, что благодаря этим двум людям вы можете просто записать выражение потерь, а затем использовать магию дифференциального исчисления и сразу же найти необходимый градиент.
Исаак Ньютон (слева) и Готфрид Вильгельм Лейбниц (справа)
Это становится возможным благодаря аналитической производной, которая не требует подстановки значений, а работает сразу со всей функцией. Здесь стоит вспомнить определение производной через предел.
Определение производной через предел
На практике лучше пользоваться аналитическим градиентом, но проверять решение с помощью числового. Это называется градиентной проверкой.
Градиентный спуск
Теперь вы знаете, как вычисляется градиент. Осталось совсем немного теории: мы плавно подошли к очень простому алгоритму, который лежит в основе обучения даже самых больших и сложных нейросетей, — градиентному спуску.
Сначала мы инициализируем W случайными значениями, вычисляем потери и градиент, а затем обновляем веса в соответствии с отрицательным направлением градиента.
step_size — это гиперпараметр, задающий величину шага, на которую мы смещаемся при каждом новом вычислении градиента. Иногда его также называют скоростью обучения. И это одна из самых важных настроек в машинном обучении.
Посмотрим, как это выглядит визуально. На графике ниже большая разноцветная область — это наша функция потерь. Участок красного цвета — минимальные значения, которых мы хотим достигнуть, а синий и зелёный означают высокие потери.
Мы начинаем с исходного W в рандомной точке и вычисляем отрицательное направление градиента, которое должно шаг за шагом привести нас к минимальным потерям.
Это самый тривиальный пример пошагового градиентного спуска. Обычно на практике используются чуть более сложные приёмы, которые мы рассмотрим в следующих лекциях.
Стохастический градиентный спуск
Когда вы вычисляете градиент 𝛁W на каждом шаге, вам приходится учитывать все обучающие примеры в наборе данных. Потому что 𝛁W — это сумма отдельных градиентов, вызванных каждым элементом обучения. Но их число может доходить до миллиона, и в этом случае обычный градиентный спуск будет работать очень медленно. Поэтому на практике часто используется стохастический градиентный спуск.
Его отличие в том, что на каждом шаге потери и градиент вычисляются не по всему датасету, а на небольшом наборе примеров — мини-пакете (а иногда всего на одном примере). То есть, параметры W обновляются после обработки нескольких объектов, а не после прохода по всему датасету. Это значительно ускоряет процесс оптимизации, а в код добавляется всего одна строчка:
Вы можете посмотреть, как работает градиентный спуск, в веб-демонстрации. Попробуйте поменять веса, величину шага или положение точек и посмотреть, как будет проходить оптимизация.
Надеемся, что погружение в настройку и оптимизацию классификации было полезным, ведь эти вещи в дальнейшем помогут вам создавать сложные нейросети. В следующий раз мы наконец-то познакомимся с основами нейросетей и поговорим о методе обратного распространения ошибки.С оригинальной лекцией можно ознакомиться на YouTube.
Следующие лекции (список будет дополняться по мере появления материалов):
Лекция 4. Введение в нейронные сети
Лекция 5. Свёрточные нейронные сети
Лекция 6. Обучение нейросетей, часть 1
Лекция 7. Обучение нейросетей, часть 2
Лекция 8. ПО для глубокого обучения
Лекция 9. Архитектуры CNN
Лекция 10. Рекуррентные нейронные сети
С оригинальной лекцией можно ознакомиться на YouTube.