Использование Python для создания бинарных и мультиклассовых классификаторов, оптимизированных с градиентным спуском

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

Большая часть работы в этой статье была вдохновлена ​​уроками из курса машинного обучения на Coursera, который провел Эндрю Нг.

Мы будем использовать Python для всего кода в этой статье, поэтому давайте импортируем все пакеты, которые нам понадобятся:

Как сделать дискретный прогноз?

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

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

Мы можем закодировать эту функцию на Python и построить ее следующим образом:

Как показано на графике, сигмовидная функция быстро изменяется от выходного значения около 0 до почти 1 около x = 0. Поскольку выходные значения симметричны относительно y = 0,5, мы можем использовать это как порог для принятия решения, где y ≥ 0,5 выходов 1 и y ‹0,5 выходов 0.

Чтобы увидеть это в действии, давайте натренируем данные!

Создание данных

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

Мы составим набор случайных оценок пользователей и критиков для 100 фильмов:

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

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

Теперь мы можем построить наши исходные данные, где оранжевый кружок представляет фильм, который понравился пользователю, а синий кружок - фильм, который не понравился:

Определение качества границы принятия решения

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

Когда P ≥ 0,5, мы выводим 1, а когда P ‹0,5, мы выводим 0, где w₀, w₁ и w₂ - веса, для которых мы оптимизируем.

Чтобы штрафовать за неправильную классификацию, мы можем воспользоваться функцией логарифмирования, поскольку log (1) = 0 и log (0) → -∞. Мы можем использовать это для создания двух штрафных функций следующим образом:

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

Используя тот факт, что наши выходные (y) будут либо 0, либо 1, мы можем элегантно объединить эти две штрафные функции в одно выражение, охватывающее оба случая:

Теперь, когда наш результат равен 1 и мы прогнозируем что-то близкое к 0 (первый член), мы получаем резкий штраф - аналогично, тот же сценарий происходит, когда наш результат равен 0, и мы предсказываем что-то близкое к 1 (второй член).

Теперь мы можем взять нашу штрафную функцию и обобщить ее на m обучающих примеров - метка i-го обучающего примера обозначена (i ). Мы также разделим общую стоимость на m, чтобы получить средний штраф (как и в случае среднеквадратичной ошибки в линейной регрессии). Это последнее выражение также известно как функция стоимости. Мы будем называть наши две функции (оценка пользователей и оценка критиков) как x₁ и x₂.

Минимизация затрат

Чтобы найти оптимальную границу решения, мы должны минимизировать эту функцию затрат, что мы можем сделать с помощью алгоритма, называемого градиентный спуск, который, по сути, выполняет две вещи: (1) находит направление максимального уменьшения на вычисление градиента функции стоимости и (2) обновление значений весов, перемещаясь по этому градиенту. Для обновления весов мы также предоставляем скорость обучения (α), которая определяет, насколько мы продвигаемся по этому градиенту. При выборе скорости обучения приходится идти на компромисс - слишком малая, и наш алгоритм слишком долго сходится; слишком большой, и наш алгоритм может действительно расходиться. Более подробную информацию о градиентном спуске и скорости обучения можно найти в связанных статьях. Поэтому у нас есть следующее правило обновления для каждого веса:

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

Теперь мы можем собрать все это вместе, чтобы получить следующее выражение градиента и правило обновления градиентного спуска:

Теперь мы можем инициализировать наши переменные, чтобы минимизировать функцию стоимости.

Инициализация переменных

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

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

Наши переменные можно инициализировать следующим образом (для начала мы установим все веса равными 0):

Теперь, когда длинная стена математики не мешает, давайте обучим нашу модель классификатора!

Обучение нашей модели

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

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

Теперь, после всей этой тяжелой работы, наша модель обучается следующей строкой!

J, w_train = gradient_descent(X, y, w, 0.5, 2000)

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

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

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

И вуаля!

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

А теперь давайте сделаем несколько прогнозов на новых примерах:

X_new = np.asarray([[1, 3.4, 4.1], [1, 2.5, 1.7], [1, 4.8, 2.3]])
print(predict(X_new, w_train))
>>> [[1.]  
     [0.]  
     [1.]]

Мультиклассовая классификация

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

Давайте построим модель из нашего двоичного примера - на этот раз пользователь может дать фильму оценку 0 звезд, 1 звезду или 2 звезды, и мы пытаемся определить границы принятия решения на основе оценок пользователей и критиков.

Сначала мы снова генерируем данные и применяем две границы решения:

Наш набор данных выглядит следующим образом, где синие круги обозначают 0 звезд, оранжевые круги обозначают 1 звезду, а красные круги обозначают 2 звезды:

Для каждого двоичного классификатора, который мы обучаем, нам нужно будет изменить метку данных таким образом, чтобы выходные данные для нашего интересующего класса были установлены на 1, а все другие метки были установлены на 0. Например, если у нас есть 3 группы A (0) , B (1) и C (2) - мы должны сделать три бинарных классификатора:

(1) A установлено на 1, B и C установлено на 0

(2) B установить на 1, A и C установить на 0

(3) C установить на 1, A и B установить на 0

У нас есть функция для переназначения наших данных для каждого классификатора:

Теперь мы проводим то же обучение модели, что и в разделе бинарной классификации ранее, но трижды:

На этот раз определение границ принятия решения немного сложнее. Мы должны вычислить значения прогнозов в каждой точке нашей сетки для каждого из наших обученных классификаторов. Затем мы выбираем максимальное значение прогноза в каждой точке и присваиваем ему соответствующий класс в зависимости от того, какой из классификаторов привел к этому максимальному значению. Контурные линии, которые мы выбираем для построения, имеют размер 0,5 (от 0 до 1) и 1,5 (от 1 до 2):

И после долгих ожиданий, вот границы, которые мы подготовили для принятия решений!

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

X_new = np.array([[1, 1, 1], [1, 1, 4.2], [1, 4.5, 2.5]])
print(predict_multi(X_new, [w_class1, w_class2, w_class3]))
>>> [[0]  
     [2]  
     [1]]

Вот оно! Мультиклассовый классификатор, сделанный полностью с нуля!

Заключительные замечания

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