Метод главных компонент (PCA)#

Ковариационная матрица#

Рассмотрим подробнее матрицу объекты-признаки.

\[\begin{split} X= \left(\begin{array}{cccc} f_1\left(x_1\right) & f_2\left(x_1\right) & \ldots & f_m\left(x_1\right) \\ f_1\left(x_2\right) & f_2\left(x_2\right) & \ldots & f_m\left(x_2\right) \\ \ldots & \ldots & \ldots & \ldots \\ f_1\left(x_N\right) & f_2\left(x_N\right) & \ldots & f_m\left(x_N\right) \end{array}\right) \end{split}\]

А точнее следующее её преобразование:

\[ \left(X^\top X\right)_{ij}=\sum^m_{k=1} f_i(x_k) \cdot f_j(x_k) \]

Видим, что это буквально выглядит как ковариация! Только в ковариации надо, чтобы случайные величины (а наши признаки - это по сути случайные величины) имели нулевое среднее. Так давайте тогда перейдём к таким признакам, чтобы их среднее на выборке было ноль.

Внимание! Здесь и далее подразумеваем, что средние значения столбцов в матрице Х равны нулю. Столбец Intercept, естественно, надо будет отбросить совсем - для этого также вычтем среднее из столбца лэйблов \(y_i\). Такая штука называется центрированием данных:

\[ f_k(x) \stackrel{\text{замена}}{\longmapsto} f_k(x) - \frac{1}{N}\sum^N_{i=1} f_k(x_i) \]
\[ y_i \stackrel{\text{замена}}{\longmapsto} y_i - \frac{1}{N}\sum^N_{i=1} y_i \]

Тогда в чистом виде, видим ковариационную матрицу

\[ \left(X^\top X\right)_{ij} = \sum^m_{k=1} f_i(x_k) \cdot f_j(x_k) = \text{cov}(f_i, f_j) \]

А тогда, из свойств матрицы \(X^\top X\) понимаем, что существует такое преобразование признаков, что в новых «координатах» ковариационная матрица имеет диагональный вид (это спектральное разложение \(X^\top X\)):

\[ X^\top X = U \cdot \Lambda \cdot U^\top, \quad U - \text{унитарная} \]

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

Более того, собственные числа \(\Lambda\) матрицы \(X^\top X\) (или сингулярные числа \(\sqrt{\Lambda}\) матрицы \(X\)) - будут скоррелированы с важностью того или иного признака (Чем больше дисперсия признака, тем он «важнее»). Таким образом мы можем даже отсекать «неважные» признаки.

В этом и есть суть PCA.

alt

Метод главных компонент (PCA - principal component analysis)#

  • Как работает преобразование признаков?

По сути, заметим, что:

\[ \underbrace{X}_{(N, m)} \cdot \underbrace{V}_{(m,m)} = \underbrace{Z}_{(N,m)}, \quad V - \text{унитарная} \]

Определяет поворот в пространстве признаков.

Из нашей цели диагонализации \(X^\top X\), мы хотим видим:

\[ X^\top X = V \cdot Z^\top Z \cdot V^\top = \left|V ≡ U - \text{собств. вектора $X^\top X$} \right| = U \cdot \underbrace{Z^\top Z}_{\Lambda} \cdot U^\top \]

То есть \(Z = X \cdot U\) - действительно определяет нужное нам преобразование к максимально ортогональным друг другу признакам.

На практике это выглядит как

\[X \cdot \mathbf{w} \stackrel{\text{want}}{=} Y\]
\[Z \cdot U^\top \cdot \mathbf{w} \stackrel{\text{want}}{=} Y\]
\[Z \cdot \widetilde{\mathbf{w}} \stackrel{\text{want}}{=} Y\]
  • Обрезка признаков

Вспоминая сингулярное разложение,

\[\begin{split} X=\underbrace{V}_{(N, m)} \cdot \underbrace{\sqrt{\Lambda}}_{(m, m)} \cdot \underbrace{U^{T}}_{(m, N)}=V\left(\begin{array}{ccc} \sqrt{\lambda_1} & & \\ & \ddots & & \\ & & &\ddots & \\ & & & & \sqrt{\lambda_m} \end{array}\right) U^{T}, \quad \text{где } U, V \text{- унитарные, и } \lambda_1 \geqslant \lambda_2 \geqslant \lambda_3 \geqslant \ldots \geqslant \lambda_m \end{split}\]

Возьмём и модифицируем центральную матрицу, убрав самые наименьшие сингулярные числа, оставив \(k\) наибольших. Составим таким образом новую матрицу объекты-признаки:

\[\begin{split} \widetilde{X}=V\left(\begin{array}{cccc} \sqrt{\lambda_1} & & & \\ & \ddots & & \\ & & \sqrt{\lambda_k} & & \\ & & & \ddots & \\ & & & & 0\\ \end{array}\right) U^{T}=\underbrace{V \cdot \sqrt{\widetilde{\Lambda}}}_{\widetilde{Z}} \cdot U^T=\widetilde{Z} \cdot U^{T} \end{split}\]

Теорема:: Эта \(\widetilde{Z}\) наилучшим образом решает задачу о малоранговом приближении

\[ \left\|\widetilde{Z} \cdot \widetilde{U}^{T}-X\right\| \longrightarrow \underset{\widetilde{Z}, \widetilde{U}}{\min} \]

Таким образом, остаётся просто решать задачу с новой матрицей объекты-признаки \(\widetilde{Z} = \widetilde{X} \cdot U\).

Пример PCA в борьбе с лишними признаками#

import numpy as np
from matplotlib import pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import PolynomialFeatures
from sklearn.metrics import mean_squared_error
# Пример с обрезкой лишних признаков

MAX_ORDER = 20 # Возьмём многочлены 19 степени и ниже (первые 20, считая от нуля)

xs = 2*np.random.random(200).reshape((-1, 1)) - 1
noise = 0.1*np.random.normal(size = xs.shape)
ys = -4 + 3*xs - 2*xs**2 + 5*xs**3 + noise

xs_train, xs_test, ys_train, ys_test = train_test_split(xs, ys, test_size=0.5, random_state=13)


poly_regresssor = PolynomialFeatures(degree=MAX_ORDER, include_bias=False) # А вот так просто можно фитить многочленами
X_train = poly_regresssor.fit_transform(xs_train.reshape(-1, 1)) # Или генерировать матрицу X
X_test = poly_regresssor.fit_transform(xs_test.reshape(-1, 1))


shift_x = X_train.mean(axis=0).reshape((1, -1)) # Сдвигаем на нулевое среднее
X_train = X_train - shift_x
X_test = X_test - shift_x


shift_y = ys_train.mean()
ys_train -= shift_y
ys_test -= shift_y
w = np.linalg.pinv(X_train) @ ys_train


y_train_predicted = X_train @ w
print('Train MSE', mean_squared_error(y_train_predicted, ys_train))

y_test_predicted = X_test @ w
print('Test MSE', mean_squared_error(y_test_predicted, ys_test))
Train MSE 0.008937804137053496
Test MSE 0.018415062974522566
import seaborn as sns
sns.barplot(x = np.arange(len(w)), y = w.flatten()) # Отобразим веса на барплоте для ясности
<Axes: >
../../_images/0613c5477b4988684d72e031038da5f30d724371d415c0c364128fe6fb573d03.png

Видим огромные значения весов - один из явных признаков переобучения.

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

Сделаем PCA и посмотрим, какие признаки с маленькой дисперсией можно обрезать.

from sklearn.decomposition import PCA # За нас уже всё реализовано

pca = PCA(n_components = 5) # Оставим только 10 наиважнейших признака
pca.fit(X_train)

X_train_reduced = pca.transform(X_train) # Преобразование координат
X_test_reduced = pca.transform(X_test)



w = np.linalg.pinv(X_train_reduced) @ ys_train

y_train_predicted = X_train_reduced @ w
print('Train MSE', mean_squared_error(y_train_predicted, ys_train))

y_test_predicted = X_test_reduced @ w
print('Test MSE', mean_squared_error(y_test_predicted, ys_test))
Train MSE 0.01686699427417666
Test MSE 0.01962030834633463
sing_values = pca.singular_values_

sns.barplot(x = np.arange(len(sing_values)), y = sing_values, log = True)

print("Видим, что 5 признак в 10 раз меньше самого важного - можно и обрезать признаки далее")
Видим, что 5 признак в 10 раз меньше самого важного - можно и обрезать признаки далее
../../_images/272773a7b1018741d9d070ead1db812d1ece0a4914e46afa8c3abd901dd9e79b.png
plt.scatter(xs, ys)
plt.scatter(xs_test, y_test_predicted + shift_y, c = 'r')
print('Срезка ненужных признаков помогла получить хорошую аппроксимацию')
Срезка ненужных признаков помогла получить хорошую аппроксимацию
../../_images/4dff5e256e3642852238ab4941a6ccc2973c98f0f9eb793d49b30cdc9a7f3b88.png

PCA в сжатии изображений#

Для плотных матриц требуется хранить \(N^2\) элементов. Аппроксимация ранга r (метод главных компонент) может уменьшить это число O(\(N_r\)) . В качестве примера рассмотрим сжатие изображений.

#!pip install scikit-image

from skimage import data
from skimage.color import rgb2gray
from numpy.linalg import svd
from skimage import img_as_float
cat = rgb2gray(img_as_float(data.chelsea()))
cat.shape
(300, 451)
plt.imshow(cat)
plt.tight_layout()
../../_images/22315961d5e34cd44368d3822216802e31f88abc1610f36cf00ec31aa6cfaea9.png
U, s, Vc = svd(cat, full_matrices=False)
def compress_and_show(k):
    os = cat.shape
    cat_compressed = U[:,:k] @ np.diag(s[:k]) @ Vc[:k,:]
    compression_ratio = 100.0* (k*(os[0] + os[1])+k)/(os[0]*os[1])
    plt.title("compression ratio={:.2f}".format(compression_ratio)+"%")
    plt.imshow(cat_compressed)
    plt.axis('off')
    plt.tight_layout()
    return cat_compressed
cat_compressed = compress_and_show(100)
cat_compressed.shape
(300, 451)
../../_images/00a1341ef727cd22e03dc7b4b28b75f60c3b03cdf21e4957d23764c8b2d1aee7.png
from ipywidgets import interact, widgets


def on_value_change(b):
    compress_and_show(b)

b_slider = widgets.IntSlider(value=90, min=1, max=100, step=1, description='b')

#interact(on_value_change, b=b_slider)
<function __main__.on_value_change(b)>