主成分分析(Principle Component Analysis,簡稱:PCA)是一種非監督學習的機器算法,主要用於數據的降維。
PCA 基本原理
以有2個特徵的二維平面舉例,如圖:
橫軸表示特徵1,縱軸表示特徵2,其中4個點表示二維的特徵樣本。如果要對樣本進行降維降到一維,可以將這些點映射到橫軸、縱軸或者其它軸上如:
映射到不同的軸後樣本間間距會不一樣,而要找的就是讓樣本間間距最大的軸。假定這個軸方向爲 $w = (w1, w2)$。
對於樣本間間距可以用方差 $Var(x) = \frac{1}{m}\sum_{i=1}^m(x_i-\overline x)^2$ 來衡量,方差越大,樣本間越稀疏。
接着進行均值歸0(demean)處理,即 $\overline x = 0$,使得 $Var(x) = \frac{1}{m}\sum_{i=1}^m(x_i)^2$。均值歸0處理使得原始樣本發生了變化轉換成了新的樣本 $X$。將其映射到軸 $w$ 上又得到新的樣本 $X_{pr}$,此時的方差爲:
$$ Var(X_{pr}) = \frac{1}{m}\sum_{i=1}^m(X_{pr}^{(i)})^2 $$
因爲 $X_{pr}^{(i)}$ 是一個向量 $(X_{pr1}^{(i)}, X_{pr2}^{(i)})$,所以實際的方差表示爲:
$$ Var(X_{pr}) = \frac{1}{m}\sum_{i=1}^m||X_{pr}^{(i)}||^2 $$
最後計算 $||X_{pr}^{(i)}||$,如圖所示:
$||X_{pr}^{(i)}|| = X^{(i)}·w$ ($w$ 爲單位向量),如此得到了想要的目標函數:
求 $w$ 使得 $Var(X_{pr}) = \frac{1}{m}\sum_{i=1}^m(X^{(i)}·w)^2$ 最大;對於多維特徵,即 $\frac{1}{m}\sum_{i=1}^m(X_1^{(i)}w_1+X_2^{(i)}w_2+...+X_n^{(i)}w_n)^2$ 最大。
梯度上升法解決PAC問題
在梯度上升法中,$+\eta\nabla f$ 代表了 $f$ 增大的方向。
對於 PAC 的目標函數:求 $w$ 使得 $f(X) = \frac{1}{m}\sum_{i=1}^m(X_1^{(i)}w_1+X_2^{(i)}w_2+...+X_n^{(i)}w_n)^2$ 最大,可以使用梯度上升法來解決。
$f(X)$ 的梯度爲:
$$ \nabla f = \begin{pmatrix} \frac{\partial f}{\partial w_1} \\\ \frac{\partial f}{\partial w_2} \\\ \cdots \\\ \frac{\partial f}{\partial w_n} \end{pmatrix} = \frac{2}{m} \begin{pmatrix} \sum_{i=1}^{m}(X_1^{(i)}w_1+X_2^{(i)}w_2+...+X_n^{(i)}w_n)X_1^{(i)} \\\ \sum_{i=1}^{m}(X_1^{(i)}w_1+X_2^{(i)}w_2+...+X_n^{(i)}w_n)X_2^{(i)} \\\ \cdots \\\ \sum_{i=1}^{m}(X_1^{(i)}w_1+X_2^{(i)}w_2+...+X_n^{(i)}w_n)X_n^{(i)} \end{pmatrix} = \frac{2}{m} \begin{pmatrix} \sum_{i=1}^{m}(X^{(i)}w)X_1^{(i)} \\\ \sum_{i=1}^{m}(X^{(i)}w)X_2^{(i)} \\\ \cdots \\\ \sum_{i=1}^{m}(X^{(i)}w)X_n^{(i)} \end{pmatrix} = \frac{2}{m}·X^T(Xw) $$
注:這裏略去了轉換過程。
利用梯度公式 $\nabla f = \frac{2}{m}·X^T(Xw)$ 的梯度上升法實際上是批量梯度上升法(還有隨機梯度上升法和小批量梯度上升法,本文不涉及)。
梯度上升法求主成分
求第一主成分
首先定義一組有兩個特徵的數據集 $X$,共100個樣本:
import numpy as np
X = np.empty((100, 2))
X[:, 0] = np.random.uniform(0., 100., size=100)
X[:, 1] = 0.75 * X[:, 0] + 3. + np.random.normal(0, 10., size=100)
$X$ 可視化如圖:
demean()
方法對 $X$ 進行均值歸0處理:
def demean(X):
return X - np.mean(X, axis=0)
X_demean = demean(X)
均值歸0處理後的 $X\_demean$ 可視化如圖:
f()
方法求函數 $f$ 的值:
def f(w, X):
return np.sum(X.dot(w) ** 2) / len(X)
df()
方法根據梯度公式 $\nabla f = \frac{2}{m}·X^T(Xw)$ 求梯度:
def df(w, X):
return X.T.dot(X.dot(w)) * 2 / len(X)
gradient_ascent()
方法爲梯度上升法的過程,在 n_iters
次循環中,每次獲得新的 $w$,如果新的 $w$ 和舊的 $w$ 對應的函數 $f$ 的值的差值滿足精度 epsilon
,則表示找到了合適的 $w$:
def gradient_ascent(X, initial_w, eta, n_iters=1e4, epsilon=1e-8):
w = direction(initial_w)
cur_iter = 0
while cur_iter < n_iters:
gradient = df(w, X)
last_w = w
w = w + eta * gradient
w = direction(w) # 將w轉換成單位向量
if(abs(f(w, X) - f(last_w, X)) < epsilon):
break
cur_iter += 1
return w
其中調用了一個方法 direction()
,用於將 $w$ 轉換成單位向量:
def direction(w):
return w / np.linalg.norm(w)
接着使用梯度上升法,先設定一個初始的 $w$ 和學習率 $\eta$:
initial_w = np.random.random(X.shape[1])
eta = 0.001
直接調用 gradient_ascent()
方法:
w = gradient_ascent(X_demean, initial_w, eta)
將得到的 $w$ 與樣本可視化如圖($w$ 以直線表示):
求前n個主成分
前面求出了第一主成分,如何求出下一個主成分呢?以二維特徵爲例,如圖:
樣本 $X^{(i)}$ 在第一主成分 $w$ 上的分量爲 $(X_{pr1}^{(i)}, X_{pr2}^{(i)})$,下一個主成分上的分量 $X^{'(i)} = X^{(i)} - (X_{pr1}^{(i)}, X_{pr2}^{(i)})$,因此只需將數據在第一主成分上的分量去掉,再對新的數據求第一主成分即可。
first_n_component()
方法用於求 $X$ 的前 n 個主成分,在 for 循環中每次求主成分時都設定一個隨機的 $w$(initial_w),得到一個主成分後就去掉在這個主成分上的分量 X_pca = X_pca - X_pca.dot(w).reshape(-1, 1) * w
:
def first_n_component(n, X, eta=0.001, n_iters=1e4, epsilon=1e-8):
X_pca = X.copy()
X_pca = demean(X_pca)
res = []
for i in range(n):
initial_w = np.random.random(X_pca.shape[1])
w = gradient_ascent(X_pca, initial_w, eta)
res.append(w)
X_pca = X_pca - X_pca.dot(w).reshape(-1, 1) * w
return res
降維映射
得到了多個主成分後,就可以通過這些主成分將高維數據映射到低維數據。
對於有 m 個樣本,n 個特徵的高維數據 $X$(m*n),主成分分析後得到前 k(k<n)個主成分 $W_k$(n*k):
$$ X = \begin{pmatrix} X_1^{(1)} X_2^{(1)} \cdots X_n^{(1)} \\\ X_1^{(2)} X_2^{(2)} \cdots X_n^{(2)} \\\ \cdots \cdots \cdots \cdots \\\ X_1^{(m)} X_2^{(m)} ... X_n^{(m)} \end{pmatrix}, W_k= \begin{pmatrix} W_1^{(1)} W_2^{(1)} \cdots W_n^{(1)} \\\ W_1^{(2)} W_2^{(2)} \cdots W_n^{(2)} \\\ \cdots \cdots \cdots \cdots \\\ W_1^{(k)} W_2^{(k)} ... W_n^{(k)} \end{pmatrix} $$
n 維降維到 k 維後的新數據爲:$X_k = X·W_k^T$。
在這個降維的過程中可能會丟失信息。如果原先的數據中本身存在一些無用信息,降維也可能會有降燥效果。
也可以將降維後的新數據恢復到原來的維度:$X_m = X_k·W_k$。
實現 PCA
定義一個類 PCA
,構造函數函數中 n_components
表示主成分個數即降維後的維數,components_
表示主成分 $W_k$;
函數 fit()
與上面的 first_n_component()
方法一樣,用於求出 $W_k$;
函數 transform()
將 $X$ 映射到各個主成分分量中,得到 $X_k$,即降維;
函數 transform()
將 $X_k$ 映射到原來的特徵空間,得到 $X_m$。
import numpy as np
class PCA:
def __init__(self, n_components):
self.n_components = n_components
self.components_ = None
def fit(self, X, eta=0.001, n_iters=1e4):
def demean(X):
return X - np.mean(X, axis=0)
def f(w, X):
return np.sum(X.dot(w) ** 2) / len(X)
def df(w, X):
return X.T.dot(X.dot(w)) * 2 / len(X)
def direction(w):
return w / np.linalg.norm(w)
def first_component(X, initial_w, eta, n_iters, epsilon=1e-8):
w = direction(initial_w)
cur_iter = 0
while cur_iter < n_iters:
gradient = df(w, X)
last_w = w
w = w + eta * gradient
w = direction(w)
if(abs(f(w, X) - f(last_w, X)) < epsilon):
break
cur_iter += 1
return w
X_pca = demean(X)
self.components_ = np.empty(shape=(self.n_components, X.shape[1]))
for i in range(self.n_components):
initial_w = np.random.random(X_pca.shape[1])
w = first_component(X_pca, initial_w, eta, n_iters)
self.components_[i:] = w
X_pca = X_pca - X_pca.dot(w).reshape(-1, 1) * w
return self
def transform(self, X):
return X.dot(self.components_.T)
def inverse_transform(self, X):
return X.dot(self.components_)
Scikit Learn 中的 PCA
Scikit Learn 中的 PCA 使用的不是梯度上升法,而是使用的相應的數學方法。使用方式如下:
from sklearn.decomposition import PCA
pca = PCA(n_components=1)
參數 n_components
表示主成分個數。
由於 PCA 降維會丟失原數據的信息,Scikit Learn 中的 PCA 在 0 < n_components < 1 時表示降維的過程保留多少信息,如:
pca = PCA(0.95)
即保留原數據 95% 的信息,此時程序會自動得到一個 components_
。
藉助 kNN 看 PCA
現在藉助於 kNN 分類算法來看一看 PCA 的優勢。首先使用一個正規的手寫數據集 MNIST 數據集,獲取樣本數據集的程序爲:
import numpy as np
from sklearn.datasets import fetch_mldata
mnist = fetch_mldata("MNIST original")
X, y = mnist['data'], mnist['target']
# MNIST 數據集已經默認分配好了訓練數據集和測試數據集,即前60000個數據爲訓練數據
X_train = np.array(X[:60000], dtype=float)
y_train = np.array(y[:60000], dtype=float)
X_test = np.array(X[60000:], dtype=float)
y_test = np.array(y[60000:], dtype=float)
先不使用 PCA 來看看 kNN 算法的效率:
from sklearn.neighbors import KNeighborsClassifier
knn_clf = KNeighborsClassifier()
%time knn_clf.fit(X_train, y_train)
此時 fit
的過程消耗的時間爲:
CPU times: user 29.6 s, sys: 306 ms, total: 29.9 s
Wall time: 30.7 s
接着驗證測試數據集的準確性:
%time knn_clf.score(X_test, y_test)
此過程消耗的時間爲:
CPU times: user 10min 23s, sys: 2.05 s, total: 10min 25s
Wall time: 10min 31s
並且測試數據集的正確率爲0.9688。
然後在來看看 PCA 的優勢,使用 PCA 時,設定保留信息爲 90%:
from sklearn.decomposition import PCA
pca = PCA(0.9)
pca.fit(X_train)
X_train_reduction = pca.transform(X_train)
knn_clf = KNeighborsClassifier()
%time knn_clf.fit(X_train_reduction, y_train)
此時 fit
的過程消耗的時間爲:
CPU times: user 317 ms, sys: 5.31 ms, total: 323 ms
Wall time: 323 ms
接着驗證測試數據集的準確性:
X_test_reduction = pca.transform(X_test)
%time knn_clf.score(X_test_reduction, y_test)
此過程消耗的時間爲:
CPU times: user 1min 15s, sys: 469 ms, total: 1min 15s
Wall time: 1min 17s
可以看出使用 PCA 後時間的消耗大大減少,而測試數據集的正確率爲0.9728,不僅正確率沒有降低還增加了,這是因爲這裏的降維過程丟失的信息是無用的信息從而達到了降燥效果。