跟我一起学scikit-learn21:PCA算法

PCA算法全称是Principal Component Analysis,即主成分分析算法。它是一种维数约减(Dimensionality Reduction)算法,即把高维度数据在损失最小的情况下转换为低维度数据的算法。显然,PCA可以用来对数据进行压缩,可以在可控的失真范围内提高运算速度。

1.PCA算法原理

我们先从最简单的情况谈起。假设需要把一个二维数据降维成一维数据,要怎么做呢?如下图所示。


figure10_1.png

我们可以想办法找出一个向量u^{(1)},以便让二维数据的点(方形点)到这个向量所在的直线上的平均距离最短,即投射误差最小。

这样就可以在失真最小的情况下,把二维数据转换为向量u^{(1)}所在直线上的一维数据。再进一步,假设需要把三维数据降为二维数据时,我们需要找出两个向量u^{(1)}u^{(2)},以便让三维数据的点在这两个向量决定的平面上的投射误差最小。

如果从数学角度来一般地描述PCA算法就是:当需要从n维数据降为k维数据时,需要找出k个向量u^{(1)}u^{(2)},……,u^{(k)},把n维的数据投射到这k个向量决定的线性空间里,最终使投射误差最小化的过程。

思考:什么情况下,进行PCA运算时误差为0?在上图中,当这些二维数据在同一条直线上时,进行PCA运算后,误差为0。

问题来了,怎样找出投射误差最小的k个向量呢?要完整的用数学公式推导这个方法,涉及较多高级线性代数的知识,这里不再详述。感兴趣的话可以参考后面扩展部分的内容。下面我们直接介绍PCA算法求解的一般步骤。

假设有一个数据集,用m x n维的矩阵A表示。矩阵中每一行表示一个样本,每一列表示一个特征,总共有m个样本,每个样本有n个特征。我们的目标是减少特征个数,保留最重要的k个特征。

1.数据归一化和缩放

数据归一化和缩放是一种数学技巧,旨在提高PCA运算时的效率。数据归一化的目标是使特征的均值为0。数据归一化公式为:
x_j^{(i)}=a_j^{(i)}-\mu_j

其中,a_j^{(i)}是指第i个样本的第j个特征的值,\mu_j表示的是第j个特征的均值。当不同的特征值不在同一个数量级上的时候,还需要对数据进行缩放。数据归一化在缩放的公式为:
x_j^{(i)}=\frac{a_j^{(i)}-\mu_j}{s_j}

其中,a_j^{(i)}是指第i个样本的第j个特征的值,\mu_j表示的是第j个特征的均值,s_j表示第j个特征的范围,即s_j=max(a_j^{(i)})-min(a_j^{(i)})

2.计算协方差矩阵的特征向量

针对预处理后的矩阵X,先计算其协方差矩阵(Covariance Matrix):
\Sigma=\frac{1}{m}X^TX

其中,\Sigma表示协方差矩阵,用大写的Sigma表示,是一个n * n维的矩阵。

接着通过奇异值分解来计算协方差矩阵的特征向量:
[U,S,V]=svd(\Sigma)

其中,svd是奇异值分解(Singular Value Decomposition)运算,是高级线性代数的内容。经过奇异值分解后,有3个返回值,其中矩阵U是一个n * n的矩阵,如果我们选择U的列作为向量,那么我们将得到n个列向量u^{(1)}u^{(2)},……,u^{(n)},这些向量就是协方差矩阵的特征向量。它表示的物理意义是,协方差矩阵\Sigma可以由这些特征向量进行线性组合得到。

3.数据降维和恢复

得到特征矩阵后,就可以对数据进行降维处理了。假设降维前的值是x^{(i)},降维后是z^{(i)},那么
z^{(i)}=U_{reduce}^{T}x^{(i)}

其中,U_{reduce}=[u^{(1)},u^{(2)},...,u^{(k)}],它选取自矩阵U的前k个向量,U_{reduce}称为主成分特征矩阵,它是数据降维和恢复的关键中间变量。看一下数据维度,U_{reduce}是n * k的矩阵,因此U_{reduce}^T是k * n的矩阵,x^{(i)}是n * 1的向量,因此z^{(i)}是k * 1的向量。这样即完成了数据的降维操作。

也可以用矩阵运算一次性转换多个向量,提高效率。假设X是行向量x^{(i)}组成的矩阵,则
Z=XU_{reduce}

其中,X是m * n的矩阵,降维后的矩阵Z是一个m * k的矩阵。

从物理意义角度来看,z^{(i)}就是x^{(i)}U_{reduce}构成的线性空间的投射,并且其投射误差最小。要从数学上正面这个结论,将是一个非常复杂的过程。对其原理感兴趣的话,可以参考后面扩展阅读部分的内容。

数据降维后,怎么恢复呢?从前面的计算公式我们知道,降维后的数据计算公式z^{(i)}=U_{reduce}^Tx^{(i)}。所以如果要还原数据,可以使用下面的公式:
x_{approx}^{(i)}=U_{reduce}z^{(i)}

其中,U_{reduce}是n * k的矩阵,z^{(i)}是k维列向量。这样算出来的x^{(i)}就是n维列向量。

矩阵化数据恢复运算公式为:
X_{approx}=ZU_{reduce}^T

其中,X_{approx}是还原回来的数据,是一个m * n的矩阵,每行表示一个训练样例。Z是一个m * k的矩阵,是降维后的数据。

2.PCA算法示例

假设我们的数据集总共有5个记录,每个记录有2个特征,这样构成的矩阵A为:
A=\begin{bmatrix} 3 & 2000 \\ 2 & 3000 \\ 4 & 5000 \\ 5 & 8000 \\ 1 & 2000 \end{bmatrix}

我们的目标是把二维数据降为一维数据。为了更好地理解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_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()
image.png

图中正方形的点是原始数据经过预处理后(归一化、缩放)的数据,圆形的点是从一维恢复到二维后的数据。同时,我们画出主成分特征向量u^{(1)}u^{(2)}。根据上图,来介绍几个有意思的结论:首先,圆形的点实际上就是方形的点在向量u^{(1)}所在直线上的投影。所谓PCA数据恢复,并不是真正的恢复,只是把降维后的坐标转换为原坐标系中的坐标而已。针对我们的例子,只是把由向量u^{(1)}决定的一维坐标系中的坐标转换为原始二维坐标系中的坐标。其次,主成分特征向量u^{(1)}u^{(2)}是相互垂直的。再次,方形点和圆形点之间的距离,就是PCA数据降维后的误差。

3.PCA的数据还原率及应用

PCA算法可以用来对数据进行压缩,可以在可控的失真范围内提高运算速度。

1.数据还原率

使用PCA对数据进行压缩时,涉及失真的度量问题,即压缩后的数据能够在多大程度上还原出原数据,我们称这一指标为数据还原率,用百分比表示。假设我们要求失真度不超过1%,即数据还原率达到99%,怎么来实现这个要求呢?k是主成分分析法中主成分的个数。可以用下面的公式作为约束条件,从而选择合适的误差范围,最合适的k值:
\frac{\frac{1}{m}\sum_{i=1}^{m}||x^{(i)}-x_{approx}^{(i)}||^2}{\frac{1}{m}\sum_{i=1}^{m}||x^{(i)}||} \leq 0.01

其中,分子部分表示平均投射误差的平方;分母部分表示所有训练样本到原点距离的平均值。这里的物理意义用术语可以描述为99%的数据真实性被保留下来了。简单的理解为压缩后的数据还原出原数据的准确度为99%。另外,常用的比率还有0.05,这个时候数据还原率就是95%。在实际应用中,可以根据要解决的问题的场景来决定这个比率。

假设我们的还原率要求是99%,那么用下面的算法来选择参数k:

(1)让k=1
(2)运行PCA算法,计算出U_{reduce}, z^{(1)}, z^{(2)}, ..., z^{(m)}, x_{approx}^{(1)}, x_{approx}^{(2)}, ..., x_{approx}^{(m)}
(3)利用\frac{\frac{1}{m}\sum_{i=1}^{m}||x^{(i)}-x_{approx}^{(i)}||^2}{\frac{1}{m}\sum_{i=1}^{m}||x^{(i)}||}计算投射误差,并判断是否满足要求,如果不满足要求,则令k=k+1,然后继续步骤(2);如果满足要求,k即是我们选择的参数。

这个算法容易理解,但实际上效率非常低,因为每做一次循环都要运行一遍PCA算法。另一个更高效的做法是,利用协方差矩阵进行奇异值分解返回的S矩阵:[U,S,V]=svd(\Sigma)。其中,S是n * n的对角矩阵,即只有对角线上的元素是非0值,其他元素都是0。

从数学上可以证明,投射误差率也可以使用下面的公式计算:
1-\frac{\sum_{i=1}^{k}S_{ii}}{\sum_{i=1}^{n}S_{ii}}

这样运算效率大大提高了,我们只需要进行一次svd运算即可。

2.加快监督机器学习算法的运算速度

PCA算法的一个典型应用就是用来加快监督学习的速度。

例如,我们有m个训练数据(x^{(1)},y^{(1)})(x^{(2)},y^{(2)}),……,(x^{(m)},y^{(m)}),其中,x^{(1)}是10000维的数据,想象一下,如果这是个图片分类的问题,比如输入的图片是100 * 100分辨率的,那么我们就有10000维的输入数据。

使用PCA算法来加快算法运算速度时,我们把输入数据分解出来x^{(1)}x^{(2)},……,x^{(m)},然后运用PCA算法对输入数据进行降维压缩,得到降维后的数据z^{(1)}z^{(2)},……,z^{(m)},最后得到新的训练样本为(z^{(1)},y^{(1)})(z^{(2)},y^{(2)}),……,(z^{(m)},y^{(m)})。利用新的训练样本训练出关于压缩后的变量z的预测函数h_{\theta}(z)

思考:针对图片分类问题,使用PCA算法进行数据降维,与直接把图片进行缩放处理相比,有什么异同点?

需要注意的是,PCA算法只用来处理训练样本,运行PCA算法得到的转换参数U_{reduce}可以直接用来对交叉验证数据集x_{cv}^{(i)}及测试数据集x_{test}^{(i)}进行转换。当然,还需要相应地对数据进行归一化处理以及对数据进行缩放。

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()把照片画出来,如下图所示。


image.png

从图片中可以看到,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算法可以通过下面的公式来计算失真幅度:
\frac{\frac{1}{m}\sum_{i=1}^{m}||x^{(i)}-x_{approx}^{(i)}||^2}{\frac{1}{m}\sum_{i=1}^{m}||x^{(i)}||}

在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))
image.png

上图中横坐标表示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()函数一次性地画出来。如下图所示。


image.png

图中第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算法,值得一读。

此外,孟岩先生的几篇博客中也介绍了矩阵及其相关运算的物理含义,深入浅出,读后犹如醍醐灌顶,这些博文分别是《理解矩阵(一)》《理解矩阵(二)》《理解矩阵(三)》

最后推荐的是网易公开课的一个视频课程:麻省理工公开课《线性代数》,是一个质量很高的线性代数课程,感兴趣的可以查阅。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,392评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,258评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,417评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,992评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,930评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,199评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,652评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,327评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,463评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,382评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,432评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,118评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,704评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,787评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,999评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,476评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,057评论 2 341

推荐阅读更多精彩内容