PCA 算法
1、主成分分析
1.1、主成分分析介紹
主成分分析(PCA)是一種非常重要的無監督降維學習方法。其基本思想:找出原始數據最主要的方面來替代原始數據,使得在損失少部分原始信息的基礎上極大地降低原始數據的維度。
這一方法利用正交變換把由線性相關變量表示的觀測數據轉換爲由少數幾個由線性無關變量表示的數據,線性無關的變量稱爲主成分。主成分的個數通常小於原始變量的個數,所以主成分分析屬於降維方法。
主成分分析有以下特點:
-
是一個無監督的機器學習算法
-
主要用於數據的降維
-
通過降維,可以發現更便於理解的特徵
-
其他應用:可視化、去噪
對於二維平面的一些數據點:
對於上述樣本點降維,可以分別只保留特徵1和特徵2,即分別降到特徵1或者特徵2的維度上,如下圖:
降維後的效果如下圖:
由圖對比可知,左邊的降維方案更好,因爲點和點之間的距離比較大,也就是點與點之間有較大的區分度,並且保留了之前的區分度,而右側的區分度則比較小。
降維遵循兩個基本原則:
-
樣本點到降維後的方向直線的距離更近;
-
樣本點在降維後方向直線上的投影儘可能地分開,即同時滿足緊湊性和可分性的要求。
所以,就需要找到讓樣本間間距最大的軸,可以找到更好的降維方向,如下:
1.2、主成分分析降維方法
在主成分分析中採用方差的方法來定義樣本間距,從而找到樣本間間距最大的軸。方差是一個描述樣本分佈疏密程度的一個統計量。
所以,找到上述軸的方法,就是是的樣本空間的所有樣本點映射到這個軸之後,方差最大。方法有如下幾步:
第一步:將樣本數據的均值歸爲 0(demean),即相當於把座標軸進行了平移:
此時,樣本的均值 ,所以方差公式變爲
第二步:得到軸的方向,在二維平面中,如 ,使得所有樣本映射到 w 之後,有方差最大,此時方差爲(此時,映射後的均值):
使得 最大,推導如下:
即將原始樣本點 X 映射到 w 軸之後投影的長度,每個樣本點代表的向量與 w 軸方向向量進行點乘之後得到的投影平方和最大。
目標:求 使得 最大,對於 N 維情況下:
求一個目標函數的最優化問題,使用梯度上升法解決。梯度上升法即梯度代表方向,對應目標函數 增大的最快方向:
PCA 降維除了使用梯度上升法之外,同樣可以使用數學求解的方法。
同時,對於線性迴歸和主成分分析兩種方法區別的對比如下:
1.3、主成分分析法推導
目標:求 ,使得 最大。
上式進行展開並進行向量化化簡如下:
2、梯度上升法求解PCA
2.1、普通數據測試
- 數據準備:
import numpy as np
import matplotlib.pyplot as plt
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)
plt.scatter(X[:, 0], X[:, 1])
plt.show()
- demean :
def demean(X):
# X 是一個矩陣,每一行代表一個樣本,所以這裏操作實際是每一個樣本中的每一個特徵
# 都要減去這個特徵對應的均值
return X - np.mean(X, axis=0)
# axis=0 ,在行方向上求均值,最終求的結果就是每一列的均值,求均值得到一個1*n向量
X_demean = demean(X)
plt.scatter(X_demean[:, 0], X_demean[:, 1])
plt.show()
np.mean(X_demean[:,0]) = -7.531752999057062e-15 np.mean(X_demean[:,1]) = -1.2363443602225743e-14
- 梯度上升法實現
# 求解目標函數
def f(w, X):
return np.sum((X.dot(w)**2)) / len(X)
# 求目標函數梯度數學求解
def df_math(w, X):
return X.T.dot(X.dot(w)) * 2. / len(X)
# 求目標函數梯度調試方法
def df_debug(w, X, epsilon=0.0001):
# 在PCA中,w 是指的方向向量,模是1,每一個維度都很小,所以epsilon小一些
res = np.empty(len(w))
for i in range(len(w)):
w_1 = w.copy()
w_1[i] += epsilon
w_2 = w.copy()
w_2[i] -= epsilon
res[i] = (f(w_1, X) - f(w_2, X)) / (2 * epsilon)
return res
def direction(w): # w 應是單位方向向量,模應該等於1
# 在進行完每次w = w + eta * gradient之後,w的模有可能不爲1
#這裏對 w 的模進行歸一
return w / np.linalg.norm(w) # norm 即求向量模的函數
def gradient_ascent(df, 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) # 注意1:每次求一個單位方向,否則需要更多次的迭代搜索
if(abs(f(w, X) - f(last_w, X)) < epsilon):
break
cur_iter += 1
return w
- 調用梯度上升法
# 注意2:不能用0向量開始
initial_w = np.random.random(X.shape[1])
print(initial_w)
array([0.37409902, 0.48471663])
# 注意3:不能使用StandardScaler標準化數據,因爲PCA過程本身是要求一個軸,是原始數據
# 映射到這個軸上之後樣本的方差最大,而標準化之後數據的方差爲1,則方差最大值就不存在
# 這樣,就無法求PCA
eta = 0.001
gradient_ascent(df_math, X_demean, initial_w, eta)
array([0.80736806, 0.59004814])
plt.scatter(X_demean[:,0], X_demean[:,1])
# 由於 w 是單位向量,非常小,所以乘以30
plt.plot([0, w[0]*30], [0, w[1]*30], color='r')
plt.show()
2.2、極端數據測試
- 原始數據:
X2 = np.empty((100, 2))
X2[:,0] = np.random.uniform(0., 100., size=100) # 沒有噪音
X2[:,1] = 0.75 * X2[:,0] + 3.
plt.scatter(X2[:,0], X2[:,1])
plt.show()
- 進行PCA降維:
X2_demean = demean(X2)
w2 = gradient_ascent(df_math, X2_demean, initial_w, eta)
plt.scatter(X2_demean[:,0], X2_demean[:,1])
plt.plot([0, w2[0]*30], [0, w2[1]*30], color='r')
plt.show()
3、求前N個主成分
3.1、求其他主成分方法
求其他主成分的方法:對數據進行改變,例如求第二個主成分,即將數據在第二個主成分上的分量減去,在新的數據上求出的第一主成分也就是原來數據相應的第二主成分,同理,可求第二、第三主成分等。
3.2、求前N主成分實踐
- 原始數據
import numpy as np
import matplotlib.pyplot as plt
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)
def demean(X):
return X - np.mean(X, axis=0)
X = demean(X)
plt.scatter(X[:,0], X[:,1])
plt.show()
- 梯度上升法求主成分
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 = 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)
if(abs(f(w, X) - f(last_w, X)) < epsilon):
break
cur_iter += 1
return w
- 求解第一主成分
initial_w = np.random.random(X.shape[1])
eta = 0.01
w = first_component(X, initial_w, eta)
initial_w = array([0.62172241, 0.84712175]) w = array([0.76301162, 0.64638476])
- 原始數據減去第一主成分
X2 = np.empty(X.shape)
X2 = X - X.dot(w).reshape(-1, 1) * w
plt.scatter(X2[:,0], X2[:,1])
plt.show()
- 求解第二主成分
w2 = first_component(X2, initial_w, eta)
# 第一主成分方向和第二主成分方向是垂直的,點乘應該等於0 ,以下結果幾乎爲0
w.dot(w2)
w2 = array([-0.64638109, 0.76301474]) w.dot(w2) = 4.81511868927198e-06 ≈ 0
- 封裝求解前N主成分函數
def first_n_components(n, X, eta=0.01, 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 = first_component(X_pca, initial_w, eta)
res.append(w)
X_pca = X_pca - X_pca.dot(w).reshape(-1, 1) * w
return res
# 調用函數求第二主成分
first_n_components(2, X)
[array([0.7630117 , 0.64638468]), # 第一主成分 array([-0.64638205, 0.76301392])] # 第二主成分
4、高緯度向低緯度映射
4.1、推導
高緯度到低緯度降維:
低緯度到高緯度恢復, 低->高,恢復(有損失):
4.2、降維實踐
- 封裝降維代碼類:
# -*- coding: utf-8 -*-
'''
# @Time : 2019/7/29 22:17
# @Author : Hong Zhen
# @Software: PyCharm
'''
import numpy as np
class PCA:
def __init__(self, n_components):
"""初始化PCA"""
assert n_components >= 1, "n_components must be valid"
self.n_components = n_components
self.components_ = None
def fit(self, X, eta=0.01, n_iters=1e4):
"""獲得數據集X的前n個主成分"""
assert self.n_components <= X.shape[1], \
"n_components must not be greater than the feature number of X"
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=0.01, 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)
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):
# 初始搜索方向 w
initial_w = np.random.random(X_pca.shape[1])
# 搜索此時的 PCA 對應的主成分
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):
"""將給定的X,映射到各個主成分分量中"""
assert X.shape[1] == self.components_.shape[1] # 列
return X.dot(self.components_.T)
def inverse_transform(self, X):
"""將給定的X,反向映射回原來的特徵空間"""
assert X.shape[1] == self.components_.shape[0] # 行
return X.dot(self.components_)
def __repr__(self):
return "PCA(n_components=%d)" % self.n_components
- 降維測試
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)
pca = PCA(n_components=2)
pca.fit(X)
print(pca.components_)
# 兩個座標軸的方向 array([[ 0.79412129, 0.60775931], [ 0.60776146, -0.79411964]])
pca = PCA(n_components=1)
pca.fit(X)
X_reduction = pca.transform(X)
print(X_reduction.shape)
(100, 1)
# 數據恢復
X_restore = pca.inverse_transform(X_reduction)
print(X_restore.shape)
# 恢復數據後會丟失數據信息
plt.scatter(X[:,0], X[:,1], color='b', alpha=0.5)
plt.scatter(X_restore[:,0], X_restore[:,1], color='r', alpha=0.5)
plt.show()
(100, 2)
4.3、scikit-learn中的PCA
4.3.1、降維與數據恢復
from sklearn.decomposition import PCA
pca = PCA(n_components=1)
pca.fit(X)
# 和自己求的主成分的方向相反,因爲sklearn中實現pca的方向不是使用梯度上升法
# 由於只是方向相反,並不影響降維
pca.components_
array([[-0.79601531, -0.60527648]])
# 降維
X_reduction = pca.transform(X)
print(X_reduction.shape)
array([[-0.79601531, -0.60527648]])
# 恢復數據
X_reduction = pca.transform(X)
X_reduction.shape
(100, 2)
# 繪製結果相同
plt.scatter(X[:,0], X[:,1], color='b', alpha=0.5)
plt.scatter(X_restore[:,0], X_restore[:,1], color='r', alpha=0.5)
plt.show()
4.3.2、真實數據測試
- 準備數據
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
from sklearn.model_selection import train_test_split
digits = datasets.load_digits() # 使用手寫識別數據
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=666)
print(X_train.shape)
(1347, 64)
- 原始進行分類測試
%%time
from sklearn.neighbors import KNeighborsClassifier
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_train)
print(knn_clf.score(X_test, y_test))
Wall time: 22.9 ms 0.9866666666666667
- 使用PCA降維
pca = PCA(n_components=2)
pca.fit(X_train)
# 不能根據 X_test 再重新訓練一個 pca ,所以 必須使用訓練數據集得到的 pca
# transform(X_test) , 這樣才能驗證整個算法的準確度
X_train_reduction = pca.transform(X_train)
X_test_reduction = pca.transform(X_test)
%%time
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train_reduction, y_train)
print(knn_clf.score(X_test_reduction, y_test))
# 通過降維後將大大節省計算時間 Wall time: 998 µs # 精度也被降低 0.6066666666666667
4.3.3、PCA所解釋的方差
上面測試降維到2維之後,測試準確率只有0.606,過於低下,在sklearn-PCA中提供瞭解釋方差的方法。
pca.explained_variance_ratio_
array([0.14566817, 0.13735469])
上面的輸出代表降維到2維之後,在每個維度上保留的數據可以解釋的方差的比例,所以降到2維後,整個數據維度上可解釋的方差爲: 左右。
pca = PCA(n_components=X_train.shape[1]) # 64 個維度
pca.fit(X_train)
# 對於 64 個主成分來說,依次可以解釋的方差的值,可以表示每個維度相應的重要程度
pca.explained_variance_ratio_
array([1.45668166e-01, 1.37354688e-01, 1.17777287e-01, 8.49968861e-02, 5.86018996e-02, 5.11542945e-02, 4.26605279e-02, 3.60119663e-02, 3.41105814e-02, 3.05407804e-02, 2.42337671e-02, 2.28700570e-02, ... ... ... ... 1.23186515e-06, 1.05783059e-06, 6.06659094e-07, 5.86686040e-07, 1.71368535e-33, 7.44075955e-34, 7.44075955e-34, 7.15189459e-34])
上面可以求解出原始數據64個維度上,每個維度可解釋的方差爲比例爲多少。可以對各個維度可解釋方差累積和的圖像:
plt.plot([i for i in range(X_train.shape[1])],
[np.sum(pca.explained_variance_ratio_[:i+1]) for i in range(X_train.shape[1])])
plt.show()
4.3.4、選擇留可解釋方差比例降維
當不知道需要降至多少維,能夠滿足可解釋方差比例時,可以通過sklearn中的方式實現:
pca = PCA(0.95) # 代表要做pca之後能解釋95%以上的方差
pca.fit(X_train)
print(pca.n_components_)
28 # 代表降維至 28維可以滿足95%的可解釋方差比例
X_train_reduction = pca.transform(X_train)
X_test_reduction = pca.transform(X_test)
%%time
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train_reduction, y_train)
print(knn_clf.score(X_test_reduction, y_test))
Wall time: 3.02 ms 0.98
4.3.4、PCA對數據進行降維可視化
pca = PCA(n_components=2)
pca.fit(X)
X_reduction = pca.transform(X)
for i in range(10): # 自動爲每一次繪製指定不同顏色
plt.scatter(X_reduction[y==i,0], X_reduction[y==i,1], alpha=0.8)
plt.show()
5、MNIST 數據集實踐
- 下載MNIST數據集
import numpy as np
from sklearn.datasets import fetch_mldata
mnist = fetch_mldata('MNIST original')
mnist
{'COL_NAMES': ['label', 'data'], 'DESCR': 'mldata.org dataset: mnist-original', 'data': array([[0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], ..., [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0], [0, 0, 0, ..., 0, 0, 0]], dtype=uint8), 'target': array([ 0., 0., 0., ..., 9., 9., 9.])}
- 數據準備
X, y = mnist['data'], mnist['target']
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)
# 訓練集大小 X_train.shape = (60000, 784) y_train.shape = (60000,) # 測試集大小 X_test.shape = (10000, 784) y_test.shape = (10000,)
- 使用KNN分類
from sklearn.neighbors import KNeighborsClassifier
knnknn_clf = KNeighborsClassifier()
%time knn_clf.fit(X_train, y_train)
CPU times: user 57.6 s, sys: 681 ms, total: 58.3 s Wall time: 59.4 s
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=5, p=2, weights='uniform')
%time knn_clf.score(X_test, y_test)
CPU times: user 14min 20s, sys: 4.3 s, total: 14min 24s Wall time: 14min 29s
0.96879999999999999
- 使用 PCA 降維後進行分類
from sklearn.decomposition import PCA
pca = PCA(0.90)
pca.fit(X_train)
X_train_reduction = pca.transform(X_train)
X_test_reduction = pca.transform(X_test)
knn_clf = KNeighborsClassifier()
%time knn_clf.fit(X_train_reduction, y_train)
CPU times: user 588 ms, sys: 5.23 ms, total: 593 ms Wall time: 593 ms
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski', metric_params=None, n_jobs=1, n_neighbors=5, p=2, weights='uniform')
%time knn_clf.score(X_test_reduction, y_test)
CPU times: user 1min 55s, sys: 346 ms, total: 1min 56s Wall time: 1min 56s
降維去除了噪音,有可能準確率更高!
0.9728
6、PCA 降噪
降維的過程可以理解成是去噪
手寫識別的例子
- 準備數據
from sklearn import datasets
digits = datasets.load_digits()
X = digits.data
y = digits.target
# 添加噪音
noisy_digits = X + np.random.normal(0, 4, size=X.shape) # 均值爲0,方差爲4
- 繪製其中部分數字(加噪音)
example_digits = noisy_digits[y==0,:][:10]
for num in range(1,10):
example_digits = np.vstack([example_digits, noisy_digits[y==num,:][:10]])
print(example_digits.shape)
plot_digits(example_digits)
(10, 64)
- PCA 降噪
pca = PCA(0.5).fit(noisy_digits)
print(pca.n_components_)
components = pca.transform(example_digits)
filtered_digits = pca.inverse_transform(components)
plot_digits(filtered_digits)
12
7、特徵臉
7.1、人臉識別庫
- 導入數據
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_lfw_people
faces = fetch_lfw_people()
# 對於每一個樣本以二維的形式展現出來,第一個維度是樣本總數, 2914 = 62 * 47
faces.images.shape
(13233, 62, 47)
- 繪製數據
random_indexes = np.random.permutation(len(faces.data))
X = faces.data[random_indexes]
example_faces = X[:36,:] # 取出前 36 個
print(example_faces.shape) # (36, 2914)
plot_faces(example_faces)
7.2、特徵臉
特徵臉,即將主成分中的每一行都當做一個樣本來看待,如果存儲的是人臉圖像數據,那麼主成分越靠前越能反映人臉的特徵。
%%time
from sklearn.decomposition import PCA
pca = PCA(svd_solver='randomized') # 採用隨機的方式,求所有主成分
pca.fit(X)
CPU times: user 3min 24s, sys: 8.6 s, total: 3min 33s Wall time: 2min 2s
將主成分當做樣本,相當於每一張人臉都是各個主成分的線性組合。
plot_faces(pca.components_[:36,:]) # 越往後越清晰
文中實例及參考:
- 劉宇波老師《Python入門機器學習》
- 《機器學習基礎》