PCA: как использовать на практике и что под капотом?
PCA (анализ главных компонент) — один из самых простых методов уменьшения размерности. Может быть очень удобно в тех случаях, когда многомерные данные необходимо визуально оценить в меньшем пространстве, чем исходное измерение данных. Или, например, когда вы хотите быстро уменьшить количество предикторов, подаваемых в какой-то ML-алгоритм, но таким образом, чтобы оставшиеся предикторы объясняли наибольшую долю дисперсии (читай, сохраняли наибольший объем информации) исходного набор данных.
Эта статья разделена на 2 раздела. В первом разделе будет продемонстрировано, как можно применять PCA с помощью python и scikit-learn к искусственному набору данных. Во втором разделе будет показано, как шаг за шагом, используя только карандаш и лист бумаги, сделать то же самое, что и аналитические структуры.
Раздел 1 — прикладное использование PCA
В каких случаях PCA полезен с визуальной точки зрения? Например, для быстрого графического анализа существующего набора данных. Предположим, у вас есть гипотеза о том, что некоторые образцы вашего набора данных должны каким-то образом отличаться от других образцов на основе некоторых переменных. Наборы данных, содержащие мошеннические записи, являются хорошим примером, потому что поведение мошенников явно чем-то отличается от поведения обычных людей. Ну допустим, у нас есть какой-то онлайн-сервис, через который клиенты могут совершать любые покупки, вносить и снимать деньги — какой-то онлайн-банк. И предположим, что время от времени в аккаунты клиентов заходят не их настоящие владельцы, а мошенники, чтобы украсть как можно больше с личных аккаунтов жертв. Что ж, звучит интересно, не правда ли? Но при чем здесь PCA?
Допустим, у нас есть очень большой набор данных с непрерывными переменными, которые так или иначе характеризуют поведение наших пользователей. Пусть среди этих переменных будет время от входа в систему до выхода в секундах; отношение остатка на счете после вывода средств к остатку на счете до вывода средств; количество страниц, просмотренных в приложении за сеанс; количество часов, прошедших с момента регистрации клиента, и еще 100 случайно сгенерированных переменных, которые могут иметь какой-то смысл в реальном мире. Было бы здорово, если бы мы могли отображать все эти переменные на одном графике одновременно, но в реальном мире мы ограничены трехмерным пространством. Таким образом, мы можем визуально анализировать не более 3-х различных переменных одновременно.
С помощью PCA мы можем создать новую систему координат, выбрать необходимое количество осей и отразить на них существующий набор данных. Таким образом, мы можем превратить исходный набор данных в новый набор данных всего с двумя (например) новыми переменными, которые объяснят максимально возможную долю дисперсии. И кроме того, мы можем удобно отразить это на двухмерном графике. Иногда это позволяет увидеть какие-то скопления точек, что помогает сформировать какие-то гипотезы.
Поскольку мы собираемся генерировать данные, мы можем сделать несколько предположений о том, как мошенники должны вести себя по отношению к обычным пользователям. Скорее всего, после входа в чужой аккаунт мошенники постараются как можно быстрее вывести все имеющиеся средства, чтобы не попасть под алгоритмы автоматического обнаружения. Исходя из этого предположения, мы можем сгенерировать две переменные: second_from_login и ratio_balances:
данные формируются специально так, чтобы первая половина записей имела смещение ближе к нулю, это наше гипотетическое мошенничество

Что касается переменных, описывающих количество просмотренных страниц в приложении и количество часов, прошедших с момента регистрации клиента, то мы будем рассматривать их как непрерывные переменные. Например, если количество просмотренных страниц равно 2,134, это означает, что третья страница была прокручена только на 13,4%. Для этих переменных сделаем такое же допущение, что их значения должны различаться между классом мошенников и обычными клиентами:
Что касается оставшихся 100 переменных, предположим, что в целом они могут вообще не различаться между мошенниками и немошенниками. Поэтому будем генерировать их из одного дистрибутива:
Соберем имеющиеся данные в 1 кадр данных и пометим первую половину строк как мошеннические записи, как договаривались выше:

Отлично, у нас есть набор данных со 104 различными непрерывными переменными и 1 меткой. Теперь мы хотим отобразить их в меньшем пространстве. Например, по прямой или по плоскости, чтобы отразить как можно больше полезной информации. С помощью python и scikit-learn это легко. Все, что для этого нужно сделать, — это создать объект класса PCA, указав количество главных компонент, по которым будет сжат исходный набор данных. То есть, если мы укажем 1 компонент, то все 104 переменные будут сжаты таким образом, чтобы отображались на 1 оси, сохраняя максимально возможную информацию; если укажем 2, то по 2 осям и так далее. Как создаются компоненты и как именно сжимается пространство, будет объяснено во втором разделе со всей математикой, стоящей за этим.


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

Атрибут объясненная_вариантность_отношение_ для созданного объекта класса PCA возвращает процент всей объясненной дисперсии данных, где порядковый номер элемента списка является номером основного компонента.

Таким образом, почти 100% дисперсии объясняются всего двумя компонентами. И, например, при построении модели классификации мы можем использовать просто 2-мерный набор данных вместо исходного 104-мерного.
Нужно ли вам ограничиваться 1–3 основными компонентами при использовании PCA? Конечно, нет. 1–3 компонента удобно визуализировать, чтобы понять какие-то базовые закономерности, если они есть, в ваших данных. Однако визуальный анализ — далеко не единственное применение PCA. Вы можете сжать пространство таким образом, чтобы сохранить как можно больше объяснимой дисперсии при минимальном количестве ваших функций. В этом случае вы уменьшите время обучения ваших алгоритмов.
В зависимости от наборов данных количество главных компонентов может сильно различаться. Вы можете определить для себя необходимую пропорцию объясненной дисперсии, например, 90% или 95% и посмотреть, насколько уменьшится место.
Теперь мы будем обучать 2 алгоритма RandomForest без настройки гиперпараметров. Первый на исходном пространстве данных, второй на пространстве, сжатом с помощью PCA. Оценим время обучения, а также итоговое качество:


Мы немного потеряли в качестве, но сократили время обучения на 64%. В реальном мире часто возникают ситуации, когда вам нужно попробовать множество разных прототипов за ограниченное время. В таких ситуациях сокращение времени обучения может быть очень важным, особенно если мы почти не теряем качество.
Раздел 2 — создание PCA с нуля
В этом разделе мы построим PCA шаг за шагом, используя математику, лежащую в основе этого метода. Настоятельно советую вам взять лист бумаги и ручку, потому что решая все самостоятельно, вы сможете намного лучше погрузиться в логику, которая есть в этом методе. Я также рекомендую посмотреть это видео от 3Blue1Brown, чтобы понять интуицию метода.
В качестве набора данных будем использовать двумерную матрицу 10x2, состоящую из 2-х векторов x и y:
Отразим этот набор данных на плоскости:

Обратите внимание на синий крест. Это точка, координаты которой являются средним значением x и y соответственно.
Шаг 1 — центрирование данных
Для центрирования набора данных нужно сделать так, чтобы среднее значение вектора x и y стало равным 0, для этого из каждого x[i] нужно вычесть среднее x, из каждого y[i] нужно вычесть среднее у:

Обратите внимание на синий крест. При центрировании он перемещается на место оранжевого креста и становится в начало системы координат (0; 0). Соответствующий сдвиг делает каждая синяя точка, занимающая место оранжевой.

Шаг 2 — рассчитать ковариационную матрицу
Ковариационная матрица — это матрица, в которой дисперсии находятся на главной диагонали, а ковариации между рассматриваемыми переменными — в остальных ячейках.
Ковариация — это мера линейной зависимости между рассматриваемыми случайными величинами. Как и линейная корреляция, только ее значения могут быть меньше -1 и больше 1.
Ковариация рассчитывается по следующей формуле, где n — длина соответствующего столбца:

Но так как мы уже центрировали данные, а средние x и y равны 0, то формула упрощается до вида:

А сама ковариационная матрица будет выглядеть так:

Используя python, вычислить ковариационную матрицу очень просто, достаточно вызвать одну функцию:

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

Значения совпадают. Поэтому мы убедились, что в нем нет никакой магии.
Шаг 3 — вычислить собственные значения ковариационной матрицы
Проще говоря, когда мы умножаем матрицу на вектор, мы применяем линейное преобразование к пространству, в котором находится вектор. Такие преобразования могут быть разными: поворот, масштабирование, искажение и т. д. Если после применения линейного преобразования к вектор, остается на той же оси, то этот вектор называется собственным вектором. А коэффициент, на который масштабировалась длина этого вектора, называется собственным значением матрицы.
На канале 3Blue1Brown есть отличное объяснение этой концепции. Здесь я постараюсь кратко объяснить суть на небольшом примере:

У нас есть исходная фигура ABCD, которую мы зеркально отражаем по оси Y. В результате цифра занимает место AB*C*D*. Как видите, вектор x остался на той же оси, только изменил направление (стал x*). Следовательно, этот вектор является собственным вектором, и собственное значение этого вектора будет равно -1.
Теперь давайте посмотрим на вектор y и его зеркальное отражение — y*. Очевидно, что они лежат на совершенно разных осях. Следовательно, вектор y не является собственным вектором этого линейного преобразования.
Для вычисления собственных векторов и собственных значений необходимо использовать следующую формулу:

где A — матрица линейного преобразования, вектор x — собственный вектор этой матрицы, lambda — собственное значение собственного вектора x.
В PCA матрица A — это ковариационная матрица cov , которую мы определили на шаге 2, поэтому наша формула будет следующей:

Решая это уравнение, находим собственные значения ковариационной матрицы:

В этом случае либо вектор x равен 0 (что для нас не интересно, так как мы ищем ненулевые векторы), либо матрица (cov-lambda*I) = 0 (и этот случай как раз тот, что нам нужно). Матрица может быть равна нулю, когда ее определитель равен 0. А определитель матрицы равен 0, когда в матрице пропорциональные или нулевые строки или столбцы, что как раз и свидетельствует о сжатии исходного пространства. Таким образом:

Решая это квадратное уравнение, находим его корни:
лямбда1 = 1,3
лямбда2 = 3,3
Давайте проверим расчеты с помощью numpy:

Отлично, мы нашли собственные значения примененного линейного преобразования! Теперь на их основе найдем сами собственные векторы.
Шаг 4 — найти собственные векторы ковариационной матрицы
Для нахождения собственных векторов необходимо подставить найденные собственные значения в уравнение, которое в формуле 6 выделено красным цветом. Решая поочередно полученные системы уравнений, мы как раз и найдем собственные векторы применяемого линейного преобразования.
Для лямбда1:

Допустим, x1 = 1, тогда:

Для лямбда2 считается аналогично. В ходе расчетов получаем, что второй собственный вектор будет равен:

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

Подставляя в формулу координаты каждого собственного вектора, получаем, что норма первого вектора = 1,31, норма второго вектора 1,55. Разделив каждый собственный вектор на его норму, получим, что нормированные координаты первого вектора равны:

И нормированные координаты второго вектора:

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

Давайте убедимся, что наши расчеты верны, используя numpy:

С поправкой на ошибки округления, это именно те векторы, которые мы рассчитали!
Пусть вас не смущает тот факт, что наши векторы кажутся умноженными на -1, потому что это точно такие же векторы, так как они лежат на одной прямой. Для тех, кто хочет более подробных объяснений, на stackoverflow есть много топиков: link1 , link2 , link3 .
Шаг 5 — трансформируем исходное пространство
Для отображения исходных данных о найденных главных компонентах необходимо центрированную исходную матрицу умножить на матрицу собственных векторов. При этом матрица собственных векторов должна иметь столько столбцов, сколько основных компонентов требуется для отображения исходных данных:

Убедитесь, что наши расчеты верны, используя класс PCA из sklearn:

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