PCA算法全称是Principal Component Analysis,即主成分分析算法。它是一种维数约减(Dimensionality Reduction)算法,即把高维度数据在损失最小的情况下转换为低维度数据的算法。显然,PCA可以用来对数据进行压缩,可以在可控的失真范围内提高运算速度。
1.PCA算法原理
我们先从最简单的情况谈起。假设需要把一个二维数据降维成一维数据,要怎么做呢?如下图所示。
我们可以想办法找出一个向量,以便让二维数据的点(方形点)到这个向量所在的直线上的平均距离最短,即投射误差最小。
这样就可以在失真最小的情况下,把二维数据转换为向量所在直线上的一维数据。再进一步,假设需要把三维数据降为二维数据时,我们需要找出两个向量和,以便让三维数据的点在这两个向量决定的平面上的投射误差最小。
如果从数学角度来一般地描述PCA算法就是:当需要从n维数据降为k维数据时,需要找出k个向量,,……,,把n维的数据投射到这k个向量决定的线性空间里,最终使投射误差最小化的过程。
思考:什么情况下,进行PCA运算时误差为0?在上图中,当这些二维数据在同一条直线上时,进行PCA运算后,误差为0。
问题来了,怎样找出投射误差最小的k个向量呢?要完整的用数学公式推导这个方法,涉及较多高级线性代数的知识,这里不再详述。感兴趣的话可以参考后面扩展部分的内容。下面我们直接介绍PCA算法求解的一般步骤。
假设有一个数据集,用m x n维的矩阵A表示。矩阵中每一行表示一个样本,每一列表示一个特征,总共有m个样本,每个样本有n个特征。我们的目标是减少特征个数,保留最重要的k个特征。
1.数据归一化和缩放
数据归一化和缩放是一种数学技巧,旨在提高PCA运算时的效率。数据归一化的目标是使特征的均值为0。数据归一化公式为:
其中,是指第i个样本的第j个特征的值,表示的是第j个特征的均值。当不同的特征值不在同一个数量级上的时候,还需要对数据进行缩放。数据归一化在缩放的公式为:
其中,是指第i个样本的第j个特征的值,表示的是第j个特征的均值,表示第j个特征的范围,即。
2.计算协方差矩阵的特征向量
针对预处理后的矩阵X,先计算其协方差矩阵(Covariance Matrix):
其中,表示协方差矩阵,用大写的Sigma表示,是一个n * n维的矩阵。
接着通过奇异值分解来计算协方差矩阵的特征向量:
其中,svd是奇异值分解(Singular Value Decomposition)运算,是高级线性代数的内容。经过奇异值分解后,有3个返回值,其中矩阵U是一个n * n的矩阵,如果我们选择U的列作为向量,那么我们将得到n个列向量,,……,,这些向量就是协方差矩阵的特征向量。它表示的物理意义是,协方差矩阵可以由这些特征向量进行线性组合得到。
3.数据降维和恢复
得到特征矩阵后,就可以对数据进行降维处理了。假设降维前的值是,降维后是,那么
其中,,...,,它选取自矩阵U的前k个向量,称为主成分特征矩阵,它是数据降维和恢复的关键中间变量。看一下数据维度,是n * k的矩阵,因此是k * n的矩阵,是n * 1的向量,因此是k * 1的向量。这样即完成了数据的降维操作。
也可以用矩阵运算一次性转换多个向量,提高效率。假设X是行向量组成的矩阵,则
其中,X是m * n的矩阵,降维后的矩阵Z是一个m * k的矩阵。
从物理意义角度来看,就是在构成的线性空间的投射,并且其投射误差最小。要从数学上正面这个结论,将是一个非常复杂的过程。对其原理感兴趣的话,可以参考后面扩展阅读部分的内容。
数据降维后,怎么恢复呢?从前面的计算公式我们知道,降维后的数据计算公式。所以如果要还原数据,可以使用下面的公式:
其中,是n * k的矩阵,是k维列向量。这样算出来的就是n维列向量。
矩阵化数据恢复运算公式为:
其中,是还原回来的数据,是一个m * n的矩阵,每行表示一个训练样例。Z是一个m * k的矩阵,是降维后的数据。
2.PCA算法示例
假设我们的数据集总共有5个记录,每个记录有2个特征,这样构成的矩阵A为:
我们的目标是把二维数据降为一维数据。为了更好地理解PCA的计算过程,分别使用Numpy和sklearn对同一个数据进行PCA降维处理。
1.使用Numpy模拟PCA计算过程
import numpy as np
A = np.array([[3,2000],
[2,3000],
[4,5000],
[5,8000],
[1,2000]],dtype='float')
# 数据归一化,axis=0表示按列归一化
mean = np.mean(A,axis=0)
norm = A - mean
# 数据缩放
score = np.max(norm,axis=0)-np.min(norm,axis=0)
norm = norm / score
norm
由于两个特征的均值不在同一个数量级,所以对数据进行了缩放。输出如下:
array([[ 0. , -0.33333333],
[-0.25 , -0.16666667],
[ 0.25 , 0.16666667],
[ 0.5 , 0.66666667],
[-0.5 , -0.33333333]])
接着,对协方差矩阵进行奇异值分解,求解其特征向量:
U,S,V = np.linalg.svd(np.dot(norm.T,norm))
U
输出如下:
array([[-0.67710949, -0.73588229],
[-0.73588229, 0.67710949]])
由于需要把二维数据降为一维数据,因此只取特征矩阵的第一列(前k列)来构造主成分特征矩阵:
U_reduce = U[:,0].reshape(2,1)
U_reduce
输出如下:
array([[-0.67710949],
[-0.73588229]])
有了主成分特征矩阵,就可以对数据进行降维了:
R = np.dot(norm,U_reduce)
R
输出如下:
array([[ 0.2452941 ],
[ 0.29192442],
[-0.29192442],
[-0.82914294],
[ 0.58384884]])
这样就把二维的数据降为一维的数据了。如果需要还原数据,依照PCA数据恢复的计算公式,可得:
Z = np.dot(R,U_reduce.T)
Z
输出如下:
array([[-0.16609096, -0.18050758],
[-0.19766479, -0.21482201],
[ 0.19766479, 0.21482201],
[ 0.56142055, 0.6101516 ],
[-0.39532959, -0.42964402]])
由于我们在数据预处理阶段对数据进行了归一化,并且做了缩放处理,所以需要进一步还原才能得到原始数据,这一步是数据预处理的逆运算。
A1 = np.multiply(Z,scope)+mean
A1
其中,np.multiply是矩阵对应元素相乘,np.dot是矩阵的行乘以矩阵的列。输出如下:
array([[2.33563616e+00, 2.91695452e+03],
[2.20934082e+00, 2.71106794e+03],
[3.79065918e+00, 5.28893206e+03],
[5.24568220e+00, 7.66090960e+03],
[1.41868164e+00, 1.42213588e+03]])
与原始矩阵A相比,恢复后的数据A1还是存在一定程度的失真,这种失真是不可避免的。
2.使用sklearn进行PCA降维运算
在sklearn工具包里,类sklearn.decomposition.PCA实现了PCA算法,使用方便,不需要了解具体的PCA的运算步骤。但需要注意的是,数据的预处理需要自己完成,其PCA算法本身不进行数据预处理(归一化和缩放)。此处,我们选择MinMaxScaler类进行数据预处理。
from sklearn.decomposition import PCA
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import MinMaxScaler
def std_PCA(**argv):
scaler = MinMaxScaler()
pca = PCA(**argv)
pipeline = Pipeline([('scaler', scaler),
('pca', pca)])
return pipeline
pca = std_PCA(n_components=1)
R2 = pca.fit_transform(A)
R2
输出如下:
array([[-0.2452941 ],
[-0.29192442],
[ 0.29192442],
[ 0.82914294],
[-0.58384884]])
这个输出值就是矩阵A经过预处理以及PCA降维后的数值。我们发现,这里的输出结果和上面使用Numpy方式的输出结果符号相反,这其实不是错误,只是降维后选择的坐标方向不同而已。
接着把数据恢复回来:
A2 = pca.inverse_transform(R2)
A2
这里的pca是一个Pipeline实例,其逆运算inverse_transform()是逐级进行的,即先进行PCA还原,再执行预处理的逆运算。即先调用PCA.inverse_transform(),然后再调用MinMaxScaler.inverse_transform()。输出如下:
array([[2.33563616e+00, 2.91695452e+03],
[2.20934082e+00, 2.71106794e+03],
[3.79065918e+00, 5.28893206e+03],
[5.24568220e+00, 7.66090960e+03],
[1.41868164e+00, 1.42213588e+03]])
可以看到,这里还原回来的数据和前面Numpy方式还原回来的数据是一致的。
3.PCA的物理含义
我们可以把前面例子中的数据在一个坐标轴上全部画出来,从而仔细观察PCA降维过程的物理含义。如下图所示。
import matplotlib.pyplot as plt
plt.figure(figsize=(8, 8), dpi=144)
plt.title('Physcial meanings of PCA')
ymin = xmin = -1
ymax = xmax = 1
plt.xlim(xmin, xmax)
plt.ylim(ymin, ymax)
ax = plt.gca() # gca 代表当前坐标轴,即 'get current axis'
ax.spines['right'].set_color('none') # 隐藏坐标轴
ax.spines['top'].set_color('none')
plt.scatter(norm[:, 0], norm[:, 1], marker='s', c='b')
plt.scatter(Z[:, 0], Z[:, 1], marker='o', c='r')
plt.arrow(0, 0, U[0][0], U[1][0], color='r', linestyle='-')
plt.arrow(0, 0, U[0][1], U[1][1], color='r', linestyle='--')
plt.annotate(r'$U_{reduce} = u^{(1)}$',
xy=(U[0][0], U[1][0]), xycoords='data',
xytext=(U_reduce[0][0] + 0.2, U_reduce[1][0] - 0.1), fontsize=10,
arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
plt.annotate(r'$u^{(2)}$',
xy=(U[0][1], U[1][1]), xycoords='data',
xytext=(U[0][1] + 0.2, U[1][1] - 0.1), fontsize=10,
arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
plt.annotate(r'raw data',
xy=(norm[0][0], norm[0][1]), xycoords='data',
xytext=(norm[0][0] + 0.2, norm[0][1] - 0.2), fontsize=10,
arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
plt.annotate(r'projected data',
xy=(Z[0][0], Z[0][1]), xycoords='data',
xytext=(Z[0][0] + 0.2, Z[0][1] - 0.1), fontsize=10,
arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2"))
plt.show()
图中正方形的点是原始数据经过预处理后(归一化、缩放)的数据,圆形的点是从一维恢复到二维后的数据。同时,我们画出主成分特征向量,。根据上图,来介绍几个有意思的结论:首先,圆形的点实际上就是方形的点在向量所在直线上的投影。所谓PCA数据恢复,并不是真正的恢复,只是把降维后的坐标转换为原坐标系中的坐标而已。针对我们的例子,只是把由向量决定的一维坐标系中的坐标转换为原始二维坐标系中的坐标。其次,主成分特征向量,是相互垂直的。再次,方形点和圆形点之间的距离,就是PCA数据降维后的误差。
3.PCA的数据还原率及应用
PCA算法可以用来对数据进行压缩,可以在可控的失真范围内提高运算速度。
1.数据还原率
使用PCA对数据进行压缩时,涉及失真的度量问题,即压缩后的数据能够在多大程度上还原出原数据,我们称这一指标为数据还原率,用百分比表示。假设我们要求失真度不超过1%,即数据还原率达到99%,怎么来实现这个要求呢?k是主成分分析法中主成分的个数。可以用下面的公式作为约束条件,从而选择合适的误差范围,最合适的k值:
其中,分子部分表示平均投射误差的平方;分母部分表示所有训练样本到原点距离的平均值。这里的物理意义用术语可以描述为99%的数据真实性被保留下来了。简单的理解为压缩后的数据还原出原数据的准确度为99%。另外,常用的比率还有0.05,这个时候数据还原率就是95%。在实际应用中,可以根据要解决的问题的场景来决定这个比率。
假设我们的还原率要求是99%,那么用下面的算法来选择参数k:
(1)让k=1
(2)运行PCA算法,计算出
(3)利用计算投射误差,并判断是否满足要求,如果不满足要求,则令k=k+1,然后继续步骤(2);如果满足要求,k即是我们选择的参数。
这个算法容易理解,但实际上效率非常低,因为每做一次循环都要运行一遍PCA算法。另一个更高效的做法是,利用协方差矩阵进行奇异值分解返回的S矩阵:。其中,S是n * n的对角矩阵,即只有对角线上的元素是非0值,其他元素都是0。
从数学上可以证明,投射误差率也可以使用下面的公式计算:
这样运算效率大大提高了,我们只需要进行一次svd运算即可。
2.加快监督机器学习算法的运算速度
PCA算法的一个典型应用就是用来加快监督学习的速度。
例如,我们有m个训练数据,,……,,其中,是10000维的数据,想象一下,如果这是个图片分类的问题,比如输入的图片是100 * 100分辨率的,那么我们就有10000维的输入数据。
使用PCA算法来加快算法运算速度时,我们把输入数据分解出来,,……,,然后运用PCA算法对输入数据进行降维压缩,得到降维后的数据,,……,,最后得到新的训练样本为,,……,。利用新的训练样本训练出关于压缩后的变量z的预测函数。
思考:针对图片分类问题,使用PCA算法进行数据降维,与直接把图片进行缩放处理相比,有什么异同点?
需要注意的是,PCA算法只用来处理训练样本,运行PCA算法得到的转换参数可以直接用来对交叉验证数据集及测试数据集进行转换。当然,还需要相应地对数据进行归一化处理以及对数据进行缩放。
4.人脸识别
本节使用英国剑桥AT&T实验室的研究人员自拍的一组照片(AT&TLaboratories Cambridge),来开发一个特定的人脸识别系统。人脸识别,本质上是个分类问题,需要把人脸图片当成训练数据集,对模型进行训练。训练好的模型,就可以对新的人脸照片进行类别预测。这就是人脸识别系统的原理。
1.加载数据集
从下面的地址可以下面数据集:http://www.cl.cam.ac.uk/research/dtg/attarchive/facesataglance.html,查看数据集里所有400张照片的缩略图。数据集总共包含40位人员的照片,每个人10张照片。
下载完照片,就可以使用下面的代码来加载这些照片了:
import time
import logging
from sklearn.datasets import fetch_olivetti_faces
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
data_home='datasets/'
logging.info('Start to load dataset')
faces = fetch_olivetti_faces(data_home=data_home)
logging.info('Done with load dataset')
输出如下:
2019-06-23 21:45:13,639 Start to load dataset
2019-06-23 21:45:13,666 Done with load dataset
加载的图片数据集保存在faces变量里,scikit-learn已经替我们把每张照片做了初步的处理,剪裁成64×64大小且人脸居中显示。这一步至关重要,否则我们的模型将被大量的噪声数据,即图片背景干扰。因为人脸识别的关键是五官纹理和特征,每张照片的背景都不同,人的发型也可能经常变化,这些特征都应该尽量排除在输入特征之外。最后,要成功加载数据集,还需要安装Python的图片处理工具包Pillow。
成功加载数据后,其data里保存的就是按照scikit-learn要求的训练数据集,target里保存的就是类别目标索引。我们通过下面的代码,将数据集的概要信息显示出来:
import numpy as np
X = faces.data
y = faces.target
targets = np.unique(faces.target)
target_names = np.array(["c%d" % t for t in targets])
n_targets = target_names.shape[0]
n_samples, h, w = faces.images.shape
print('Sample count: {}\nTarget count: {}'.format(n_samples, n_targets))
print('Image size: {}x{}\nDataset shape: {}\n'.format(w, h, X.shape))
输出如下:
Sample count: 400
Target count: 40
Image size: 64x64
Dataset shape: (400, 4096)
从输出可知,总共有40位人物的照片,图片总数是400张,输入特征有4096个。为了后续区分不同的人物,我们用索引号给目标人物命名,并保存在变量target_names里。为了更直观地观察数据,从每个人物的照片里随机选择一张显示出来。先定义一个函数来显示照片阵列:
%matplotlib inline
import matplotlib.pyplot as plt
def plot_gallery(images,titles,h,w,n_row=2,n_col=5):
"""显示图片阵列"""
plt.figure(figsize=(2*n_col,2*n_row),dpi=144)
plt.subplots_adjust(bottom=0,left=0.01,right=0.99,top=0.90,hspace=0.01)
for i in range(n_row*n_col):
plt.subplot(n_row,n_col,i+1)
plt.imshow(images[i].reshape((h,w)), cmap=plt.cm.gray)
plt.title(titles[i])
plt.axis('off')
输入参数images是一个二维数据,每一行都是一个图片数据。在加载数据时,fetch_olivetti_faces()函数已经帮我们做了预处理,图片的每个像素的RGB值都转换成了[0,1]的浮点数。因此,我们画出来的照片将是黑白的,而不是彩色的。在图片识别领域,一般情况下用黑白照片就可以了,可以减少计算量,也会让模型更准确。
接着分成两行显示出这些人物的照片:
n_row = 2
n_col = 6
sample_images = None
sample_titles = []
for i in range(n_targets):
people_images = X[y==i]
people_sample_index = np.random.randint(0, people_images.shape[0], 1)
people_sample_image = people_images[people_sample_index, :]
if sample_images is not None:
sample_images = np.concatenate((sample_images, people_sample_image), axis=0)
else:
sample_images = people_sample_image
sample_titles.append(target_names[i])
plot_gallery(sample_images, sample_titles, h, w, n_row, n_col)
代码中,X[y==i]可以选择出属于特定人物的所有照片,随机选择出来的照片都放在sample_images数组对象里,最后使用我们之前定义的函数plot_gallery()把照片画出来,如下图所示。
从图片中可以看到,fetch_olivetti_faces()函数帮我们剪裁了中间部分,只留下脸部特征。如果想对比原图,可以到主页http://www.cl.cam.ac.uk/research/dtg/attarchive/facedatabase.html下载原图对比。
最后,把数据集划分成训练数据集和测试数据集:
from sklearn.model_selection import train_test_split
X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.2,random_state=4)
2.一次失败的尝试
我们使用支持向量机来实现人脸识别:
from sklearn.svm import SVC
from time import time
start = time()
print('Fitting train datasets ...')
clf = SVC(class_weight='balanced')
clf.fit(X_train,y_train)
print('Done in {0:.2f}s'.format(time()-start))
输出如下:
Fitting train datasets ...
Done in 0.92s
指定SVC的class_weight参数,让SVC模型能根据训练样本的数量来均衡地调整权重,这对不均匀的数据集,即目标人物的照片数量相差较大的情况是非常有帮助的。由于总共只有400张照片,数据规模较小,模型很快就运行完了。
接着,针对测试数据集进行预测:
start = time()
print('Predicting test dataset ...')
y_pred = clf.predict(X_test)
print('Done in {0:.2f}s'.format(time()-start))
输出如下:
Predicting test dataset ...
Done in 0.10s
最后,分别使用confusion_matrix和classification_report来查看模型分类的准确性。
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test,y_pred,labels=range(n_targets))
print('confusion matrix:\n')
np.set_printoptions(threshold=np.nan)
print(cm)
np.set_printoptions()是为了确保完整地输出cm数组的内容,这是因为这个数组是40×40的,默认情况下不会全部输出。输出如下:
confusion matrix:
[[0 0 0 0 0 0 0 0 0 0 0 1 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 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 1 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 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 1 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 1 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 0 0 0 4 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 0 0 0 2 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 0 0 0 4 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 0 0 0 2 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 0 0 0 1 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 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 0 0 0 0 0 0 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 4 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 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 1 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 1 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 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 2 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 0 0 0 2 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 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 1 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 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 3 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 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 3 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 0 0 0 2 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 0 0 0 2 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 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 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 2 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 0 0 0 2 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 3 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 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 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 0 0 0 2 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 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 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 1 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 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0 0 0 2 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]]
confusion matrix理想的输出,是矩阵的对角线上有数字,其他地方都没有数字。但我们的结果显示不是这样的。可以明显看出,很多图片都被预测成索引为12的类别了。结果看起来完全不对,这是怎么回事呢?我们再看一下classification_report的结果:
from sklearn.metrics import classification_report
print(classification_report(y_test,y_pred,target_names=target_names))
输出如下:
precision recall f1-score support
c0 0.00 0.00 0.00 1
c1 0.00 0.00 0.00 3
c2 0.00 0.00 0.00 2
c3 0.00 0.00 0.00 1
c4 0.00 0.00 0.00 1
c5 0.00 0.00 0.00 1
c6 0.00 0.00 0.00 4
c7 0.00 0.00 0.00 2
c8 0.00 0.00 0.00 4
c9 0.00 0.00 0.00 2
c10 0.00 0.00 0.00 1
c11 0.00 0.00 0.00 0
c12 0.00 0.00 0.00 4
c13 0.00 0.00 0.00 4
c14 0.00 0.00 0.00 1
c15 0.00 0.00 0.00 1
c16 0.00 0.00 0.00 3
c17 0.00 0.00 0.00 2
c18 0.00 0.00 0.00 2
c19 0.00 0.00 0.00 2
c20 0.00 0.00 0.00 1
c21 0.00 0.00 0.00 2
c22 0.00 0.00 0.00 3
c23 0.00 0.00 0.00 2
c24 0.00 0.00 0.00 3
c25 0.00 0.00 0.00 3
c26 0.00 0.00 0.00 2
c27 0.00 0.00 0.00 2
c28 0.00 0.00 0.00 0
c29 0.00 0.00 0.00 2
c30 0.00 0.00 0.00 2
c31 0.00 0.00 0.00 3
c32 0.00 0.00 0.00 2
c33 0.00 0.00 0.00 2
c34 0.00 0.00 0.00 0
c35 0.00 0.00 0.00 2
c36 0.00 0.00 0.00 3
c37 0.00 0.00 0.00 1
c38 0.00 0.00 0.00 2
c39 0.00 0.00 0.00 2
avg / total 0.00 0.00 0.00 80
40个类别里,查准率、召回率、F1 Score全为0,不能有更差的预测结果了。为什么?哪里出了差错?
答案是,我们把每个像素都作为一个输入特征来处理,这样的数据噪声太严重了,模型根本没有办法对训练数据集进行拟合。想想看,我们总共有4096个特征,可是数据集大小才400个,比特征个数还少,而且我们还需要把数据集分出20%来作为测试数据集,这样训练数据集就更小了。这样的状况下,模型根本无法进行准确地训练和预测。
3.使用PCA来处理数据集
解决上述问题的一个办法是使用PCA来给数据降维,只选择前k个最重要的特征。问题来了,选择多少个特征合适呢?即怎么确定k的值?PCA算法可以通过下面的公式来计算失真幅度:
在scikit-learn里,可以从PCA模型的explained_variance_ratio_变量里获取经PCA处理后的数据还原率。这是一个数组,所有元素求和即可知道我们选择的k值的数据还原率,数值越大说明失真越小,随着k值的增大,数值会无限接近于1。
利用这一特性,可以让k取值10~300之间,每隔30进行一次取样。在所有的k值样本下,计算经过PCA算法处理后的数据还原率。然后根据数据还原率要求,来确定合理的k值。针对我们的情况,选择失真度小于5%,即PCA处理后能保留95%的原数据信息。其代码如下:
from sklearn.decomposition import PCA
print("Exploring explained variance ratio for dataset ...")
candidate_components = range(10,300,30)
explained_ratios = []
start = time()
for c in candidate_components:
pca = PCA(n_components=c)
X_pca = pca.fit_transform(X)
explained_ratios.append(np.sum(pca.explained_variance_ratio_))
print('Done in {0:.2f}s'.format(time()-start))
输出如下:
Exploring explained variance ratio for dataset ...
Done in 0.75s
根据不同的k值,构建PCA模型,然后调用fit_transform()函数来处理数据集,再把模型处理后数据还原率,放入explained_ratios数组。接着把这个数组画出来:
plt.figure(figsize=(10,6),dpi=144)
plt.grid()
plt.plot(candidate_components,explained_ratios)
plt.xlabel('Number of PCA Components')
plt.ylabel('Explained Variance Ratio')
plt.title('Explained variance ratio for PCA')
plt.yticks(np.arange(0.5,1.05,0.05))
plt.xticks(np.arange(0,300,20))
上图中横坐标表示k值,纵坐标表示数据还原率。从图中可以看出,要保留95%以上的数据还原率,k值选择140即可。根据上图,也可以非常容易地找出不同的数据还原率所对应的k值。为了更直观地观察和对比在不同数据还原率下的数据,我们选择数据还原率分别在95%、90%、80%、70%、60%的情况下,这些数据还原率对应的k值分别是140、75、37、19、8,画出经PCA处理后的图片。
为了方便,这里直接选择前面的人物图片中的前5位作为我们的样本图片。每行画出5个图片,先画出原图,接着再画出每行在不同数据还原率下对应的图片。
def title_prefix(prefix, title):
return "{}: {}".format(prefix, title)
n_row = 1
n_col = 5
sample_images = sample_images[0:5]
sample_titles = sample_titles[0:5]
plotting_images = sample_images
plotting_titles = [title_prefix('orig', t) for t in sample_titles]
candidate_components = [140, 75, 37, 19, 8]
for c in candidate_components:
print("Fitting and projecting on PCA(n_components={}) ...".format(c))
start = time()
pca = PCA(n_components=c)
pca.fit(X)
X_sample_pca = pca.transform(sample_images)
X_sample_inv = pca.inverse_transform(X_sample_pca)
plotting_images = np.concatenate((plotting_images, X_sample_inv), axis=0)
sample_title_pca = [title_prefix('{}'.format(c), t) for t in sample_titles]
plotting_titles = np.concatenate((plotting_titles, sample_title_pca), axis=0)
print("Done in {0:.2f}s".format(time() - start))
print("Plotting sample image with different number of PCA conpoments ...")
plot_gallery(plotting_images, plotting_titles, h, w,
n_row * (len(candidate_components) + 1), n_col)
代码里,我们把所有的图片收集进plotting_images数组,然后调用前面定义的plot_gallery()函数一次性地画出来。如下图所示。
图中第1行显示的是原图,第2行显示的是数据还原度在95%处,即k=140的图片;第2行显示的是数据还原度在90%处,即k=90的图片;依此类推。可以直观地观察到,原图和95%数据还原率的图片没有太大差异。另外,即使在k=8时,图片依然能比较清楚地反映出人物的脸部特征轮廓。
4.最终结果
接下来问题就变得简单了。我们选择k=140作为PCA参数,对训练数据集和测试数据集进行特征提取。
n_components = 140
print("Fitting PCA by using training data ...")
start = time()
pca = PCA(n_components=n_components, svd_solver='randomized', whiten=True).fit(X_train)
print("Done in {0:.2f}s".format(time() - start))
print("Projecting input data for PCA ...")
start = time()
X_train_pca = pca.transform(X_train)
X_test_pca = pca.transform(X_test)
print("Done in {0:.2f}s".format(time() - start))
输出如下:
Fitting PCA by using training data ...
Done in 0.08s
Projecting input data for PCA ...
Done in 0.01s
接着使用GridSearchCV来选择一个最佳的SVC模型参数,然后使用最佳参数对模型进行训练。
from sklearn.model_selection import GridSearchCV
print("Searching the best parameters for SVC ...")
param_grid = {'C': [1, 5, 10, 50, 100],
'gamma': [0.0001, 0.0005, 0.001, 0.005, 0.01]}
clf = GridSearchCV(SVC(kernel='rbf', class_weight='balanced'), param_grid, verbose=2, n_jobs=4)
clf = clf.fit(X_train_pca, y_train)
print("Best parameters found by grid search:")
print(clf.best_params_)
这一步执行时间比较长,因为GridSearchCV使用矩阵式搜索法,对每组参数组合进行一次训练,然后找出最好的参数的模型。我们通过设置n_jobs=4来启动4个线程并发执行,同时设置verbose=2来输出一些过程信息。最终选择出来的最佳模型参数如下:
Best parameters found by grid search:
{'C': 5, 'gamma': 0.001}
接着使用这一模型对测试样本进行预测,并且使用confusion_matrix输出预测准确性信息。
start = time()
print("Predict test dataset ...")
y_pred = clf.best_estimator_.predict(X_test_pca)
cm = confusion_matrix(y_test, y_pred, labels=range(n_targets))
print("Done in {0:.2f}.\n".format(time()-start))
print("confusion matrix:")
np.set_printoptions(threshold=np.nan)
print(cm)
输出如下:
Predict test dataset ...
Done in 0.01.
confusion matrix:
[[1 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 0 0 0]
[1 2 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 0 0]
[0 0 1 0 1 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 0 0 1 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]
[0 0 0 0 1 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 0 0 0 0 1 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 0 0 0 0 3 0 0 0 0 0 0 0 0 0 1 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 2 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 0 0 0 0 4 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 0 0 0 0 2 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 0 0 0 0 1 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 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 0 0 0 0 0 0 0 0 0 4 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 0 0 0 0 4 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 0 0 0 0 1 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 0 0 0 0 1 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 0 0 0 0 3 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 0 0 0 0 2 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 0 0 0 0 2 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 0 0 0 0 2 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 0 0 0 0 1 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 0 0 0 0 2 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 0 0 0 0 3 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 0 0 0 0 2 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 0 0 0 0 3 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 0 1 0 0 2 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 0 0 0 0 2 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 0 0 0 0 2 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 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 0 0 0 0 0 0 0 0 0 2 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 0 0 0 0 2 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 0 0 0 0 3 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 0 0 0 0 2 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 0 0 0 0 2 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 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 0 0 0 0 0 0 0 0 0 2 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 0 0 0 0 3 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 0 0 0 0 1 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 0 0 0 0 2 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 0 0 0 0 2]]
从输出的对角线上的数据可以看出,大部分预测结果都正确。我们再使用classification_report输出分类报告,查看测准率,召回率及F1 Score。
print(classification_report(y_test, y_pred))
输出如下:
precision recall f1-score support
0 0.50 1.00 0.67 1
1 1.00 0.67 0.80 3
2 1.00 0.50 0.67 2
3 1.00 1.00 1.00 1
4 0.50 1.00 0.67 1
5 1.00 1.00 1.00 1
6 1.00 0.75 0.86 4
7 1.00 1.00 1.00 2
8 1.00 1.00 1.00 4
9 1.00 1.00 1.00 2
10 1.00 1.00 1.00 1
12 1.00 1.00 1.00 4
13 1.00 1.00 1.00 4
14 1.00 1.00 1.00 1
15 1.00 1.00 1.00 1
16 0.75 1.00 0.86 3
17 1.00 1.00 1.00 2
18 1.00 1.00 1.00 2
19 1.00 1.00 1.00 2
20 1.00 1.00 1.00 1
21 1.00 1.00 1.00 2
22 0.75 1.00 0.86 3
23 1.00 1.00 1.00 2
24 1.00 1.00 1.00 3
25 1.00 0.67 0.80 3
26 1.00 1.00 1.00 2
27 1.00 1.00 1.00 2
29 1.00 1.00 1.00 2
30 1.00 1.00 1.00 2
31 1.00 1.00 1.00 3
32 1.00 1.00 1.00 2
33 1.00 1.00 1.00 2
35 1.00 1.00 1.00 2
36 1.00 1.00 1.00 3
37 1.00 1.00 1.00 1
38 1.00 1.00 1.00 2
39 1.00 1.00 1.00 2
avg / total 0.97 0.95 0.95 80
在总共只有400张图片,每位目标人物只有10张图片的情况下,测准率和召回率平均达到了0.95以上,这是一个非常了不起的性能。
5.拓展阅读
PCA算法的推导涉及大量的线性代数的知识。张洋先生的一篇博客《PCA的数学原理》,基本上做到了从最基础的内容谈起,一步步地推导出PCA算法,值得一读。
此外,孟岩先生的几篇博客中也介绍了矩阵及其相关运算的物理含义,深入浅出,读后犹如醍醐灌顶,这些博文分别是《理解矩阵(一)》,《理解矩阵(二)》和《理解矩阵(三)》。
最后推荐的是网易公开课的一个视频课程:麻省理工公开课《线性代数》,是一个质量很高的线性代数课程,感兴趣的可以查阅。