基于pca的人脸识别

楼下早餐店老板最近总夸我“记性越来越差脸越来越灵”——以前他得眯着眼睛记我常要热豆浆加半颗茶叶蛋,现在机器“叮”一声,屏幕上跳出我半年前拍的皱巴巴会员照,直接自动扣钱递卡套。这种瞬间总让我有点恍惚:高中啃《数学选修》里那堆“特征脸”时,哪能想到这玩意儿居然成了帮我省掉报菜名(以及偶尔忘带手机)尴尬的救星?

其实早餐机用的大概率不是纯PCA,但它绝对是“从一堆像素里扒人脸核心特征”的祖师爷级算法——说白了,就是把一张几百几千像素的人脸,压缩成几十几百个小数字,还能靠这些小数字认出“这是谁”。

先别慌着回忆矩阵正交变换,我们用大白话加个超简化的“像素脸”例子玩明白。假设现在有个超小的像素摄像头,只能拍3x3=9个像素的黑白照片,黑是0,白是1,半黑半白我们用0.5这种中间数(方便计算)。

现在我找了三个“模特”拍了两张照片:

模特1是个只有眼睛白、其他全黑的小圆脸(哦不对,是方块脸),两张是:

[

[0, 1, 0],

[1, 1, 1],

[0, 1, 0]

]

模特2是只有额头和下巴白的长脸,两张是:

[

[1, 1, 1],

[0, 0, 0],

[1, 1, 1]

]

模特3是左半脸黑、右半脸白的阴阳脸,两张是:

[

[0, 0, 0.5, 1, 1, 1],

[0, 0, 0.5, 1, 1, 1],

[0, 0, 0.5, 1, 1, 1]

基于pca的人脸识别

]

哦等下阴阳脸搞错了尺寸,统一3x3,改改:

[

[0, 0.5, 1],

[0, 0.5, 1],

[0, 0.5, 1]

]

现在第一步,把所有照片拍扁——PCA才不管你是圆是长是阴阳,它只认一串数字。模特1的第一张就是[0,1,0,1,1,1,0,1,0],第二张差不多?哦为了让例子更真实,给模特1的第二张加个小“表情变化”——比如左眼眨了一下,左上角1变成0.8:[0,0.8,0,1,1,1,0,1,0]。

第二步,求“平均脸”——把刚才6张(3人x2)拍扁的像素加起来除以6。算一下大概:

额头第一行的三个数:(模特1两张0+0 + 模特2两张1+1 + 模特3两张0+0)/6?不对模特3额头第一个是0第二个0.5第三个1!哦对,分每个位置加:

比如第1个像素(左上角):0+0 +1+1 +0+0 =2 → 2/6≈0.33

第2个(第一行中间):1+0.8 +1+1 +0.5+0.5=4.8 → 4.8/6=0.8

第3个(右上角):0+0 +1+1 +1+1=4 → 4/6≈0.67

第4个(第二行第一个):1+1 +0+0 +0+0=2 → 0.33

……以此类推,最后得到一串9个数字的“平均脸”,大概就是三个模特揉成一团、没有明显表情/左右偏的样子。

第三步,把每张脸减去平均脸——这一步叫“去中心化”,去掉所有人脸的“共性”,剩下的就是“个性”:比如小圆脸剩下的就是眼睛周围比平均亮、额头下巴暗;长脸剩下的是额头下巴亮、中间暗;阴阳脸剩下的是左边暗右边亮。

第四步,也是最硬核但我们可以“偷懒”理解的一步——找主成分(PC,也就是网上常说的“特征脸”)。这些主成分其实就是刚才剩下的“个性像素串”里最能区分不同人的方向:比如第一个主成分可能是“左右脸谁亮”(刚好戳中阴阳脸),第二个是“上下脸谁亮”(戳中长脸和小圆脸的区别),第三个可能是“表情——比如眼睛眨不眨”(戳中模特1两张照片的区别)……

到这里,我们就可以把原来9维的像素串,投影到前k个主成分上,变成k维的小数字了。比如k=3,每张脸就从9个数字变成3个,还能画在三维空间里看——阴阳脸在一个轴,长脸和小圆脸在另一个轴,模特1的两张照片在第三个轴附近,完美分开。

哦对了,不能光说不练,上一段Python代码试试手——这次不用超简化的3x3,用现成的小数据集sklearn.datasets.fetchlfwpeople,就是LFW(Labeled Faces in the Wild)里的名人脸,每张是250x250的,但我们可以缩小点(不然算死电脑)。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_lfw_people
from sklearn.model_selection import train_test_split
from sklearn.decomposition import PCA
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score

lfw_people = fetch_lfw_people(min_faces_per_person=20, resize=0.25)
X = lfw_people.data  # 拍扁后的像素串:(n_samples, n_pixels),这里应该是(1560, 32*32=1024)
y = lfw_people.target  # 每个人的编号
target_names = lfw_people.target_names  # 编号对应的名字,比如Tony Blair, George W Bush这些

# 先看看数据集长啥样,画9张平均脸?不对画9张原始名人脸
plt.figure(figsize=(10, 10))
for i in range(9):
    plt.subplot(3, 3, i+1)
    plt.imshow(X[i].reshape(32, 32), cmap='gray')
    plt.title(target_names[y[i]], fontsize=8)
    plt.axis('off')
plt.show()

# 2. 求平均脸(PCA其实会自动帮我们去中心化,但手动算一下看看更直观)
mean_face = X.mean(axis=0)
plt.figure(figsize=(4, 4))
plt.imshow(mean_face.reshape(32, 32), cmap='gray')
plt.title("Average Face of LFW (at least 20 pics)")
plt.axis('off')
plt.show()

# 3. 找主成分!取前150个吧(一般LFW人脸识别k取100-200就行)
pca = PCA(n_components=150, whiten=True)  # whiten=True就是把投影后的主成分标准化,方便后面分类
X_pca = pca.fit_transform(X)  # 训练PCA + 把原始X投影到150维

# 看看前9个主成分(也就是特征脸)长啥样,有意思的地方来了
plt.figure(figsize=(10, 10))
for i in range(9):
    plt.subplot(3, 3, i+1)
    plt.imshow(pca.components_[i].reshape(32, 32), cmap='gray')
    plt.title(f"PC {i+1}", fontsize=8)
    plt.axis('off')
plt.show()

# 4. 来个简单的KNN分类试试人脸识别效果!
# 先把数据分成训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X_pca, y, test_size=0.3, random_state=42)

# 训练KNN
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_train, y_train)

# 预测测试集
y_pred = knn.predict(X_test)

# 看看准确率
print(f"Accuracy: {accuracy_score(y_test, y_pred):.2f}")

# 画几张预测对的和错的
def plot_prediction(indices, title):
    plt.figure(figsize=(10, 5))
    for i, idx in enumerate(indices):
        plt.subplot(2, 5, i+1)
        # 这里要注意!X_pca是投影后的,要恢复原始脸得用pca.inverse_transform
        # 不过我们这里直接用X_test对应的原始X更清楚
        original_idx = np.where(X_pca == X_test[idx])[0][0]  # 偷懒找原始索引的方法,别学太死
        plt.imshow(X[original_idx].reshape(32, 32), cmap='gray')
        true_name = target_names[y[original_idx]]
        pred_name = target_names[y_pred[idx]]
        plt.title(f"True: {true_name}\nPred: {pred_name}", fontsize=6)
        plt.axis('off')
    plt.suptitle(title, fontsize=12)
    plt.show()

# 找前5个预测对的
correct_indices = np.where(y_pred == y_test)[0][:5]
# 找前5个预测错的
wrong_indices = np.where(y_pred != y_test)[0][:5]

plot_prediction(correct_indices, "Correct Predictions")
plot_prediction(wrong_indices, "Wrong Predictions")

等下跑这段代码的话,首先得安装numpy matplotlib scikit-learn这三个库,然后耐心等LFW下载(第一次可能慢,是从国外服务器拉的,换个镜像或者挂个小梯子也行)。

那这段代码的结果大概是啥样的?首先前9张原始名人脸应该能认出几个政客;然后平均脸——哦对LFW里很多美国人白人,尤其是George W Bush照片特别多(大概有500多张),所以平均脸会有点像他,但又模糊不清,所有五官都揉在一起;然后最有意思的特征脸来了:PC1大概是“整体亮度+头发多少”,PC2是“左右脸不对称?或者性别?”(哦不对性别在PCA k比较小的时候可能体现不出来,但k大一点会有),PC3可能是“表情,比如皱眉或者笑”;然后准确率大概在0.7-0.8之间?主要是因为我们把照片缩得太小了,而且KNN是个很简单的分类器,要是换成SVM(支持向量机,以前LFW人脸识别常用的),准确率能冲到0.85-0.9。

再看看预测错的那些脸:要么是光线太暗,要么是表情太夸张(比如吐舌头、戴墨镜),要么是和另一个人长得太像——这也是纯PCA的缺点:它是线性的、基于全局像素的,只看“整体五官怎么分布”,不管“局部细节(比如眼睛旁边的痣、牙齿的排列)”,也不管“姿势变化(比如歪头、侧脸)”“光照变化(比如背光、侧光)”——这些现在都是用深度学习(比如CNN,卷积神经网络)解决的,但PCA作为入门算法,能把“降维+特征提取+分类”这一套流程讲得明明白白,绝对是每个想学机器学习/计算机视觉的人必踩的坑(哦不,必走的路)。

最后再补个冷知识:PCA用于人脸识别的方法最早是由Sirovich和Kirby在1987年提出来的,当时叫“Eigenfaces(特征脸)”,后来Turk和Pentland在1991年把它改进成了实用的人脸识别系统——那时候深度学习还没影呢,这个算法火了好多年,直到2014年Facebook发布DeepFace(用CNN的,准确率和人差不多)才慢慢淡出主流。但直到现在,很多小场景(比如考勤机的辅助识别、人数不多的门禁系统)还是会用PCA+KNN/SVM,因为它简单、不需要大量数据训练、跑起来快。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐