主成分分析(Principle Component Analysis,簡稱:PCA)是一種非監督學習的機器算法,主要用於數據的降維。python
以有2個特徵的二維平面舉例,如圖:git
橫軸表示特徵1,縱軸表示特徵2,其中4個點表示二維的特徵樣本。若是要對樣本進行降維降到一維,能夠將這些點映射到橫軸、縱軸或者其它軸上如:github
映射到不一樣的軸後樣本間間距會不同,而要找的就是讓樣本間間距最大的軸。假定這個軸方向爲 $w = (w1, w2)$。算法
對於樣本間間距能夠用方差 $Var(x) = \frac{1}{m}\sum_{i=1}^m(x_i-\overline x)^2$ 來衡量,方差越大,樣本間越稀疏。app
接着進行均值歸0(demean)處理,即 $\overline x = 0$,使得 $Var(x) = \frac{1}{m}\sum_{i=1}^m(x_i)^2$。均值歸0處理使得原始樣本發生了變化轉換成了新的樣本 $X$。將其映射到軸 $w$ 上又獲得新的樣本 $X_{pr}$,此時的方差爲:dom
$$ 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)}||$,如圖所示:fetch
$||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$ 最大。
在梯度上升法中,$+\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$ 以直線表示):
前面求出了第一主成分,如何求出下一個主成分呢?以二維特徵爲例,如圖:
樣本 $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
,構造函數函數中 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 使用的不是梯度上升法,而是使用的相應的數學方法。使用方式以下:
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 的優點。首先使用一個正規的手寫數據集 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,不只正確率沒有下降還增長了,這是由於這裏的降維過程丟失的信息是無用的信息從而達到了降燥效果。