基于MATLAB稀疏表示求解的人脸识别

一、理论部分

考虑到 x \bold x x的各个维度无关,因此对于问题:
a r g min ⁡ x ∣ ∣ x − y ∣ ∣ 2 + λ ∣ ∣ x ∣ ∣ 1 arg\min_x||\bold x-\bold y||^2+\lambda||\bold x||_1 argxminxy2+λx1
只要使得对于每个维度下,都有:
a r g min ⁡ x i ∣ ∣ x i − y i ∣ ∣ 2 + λ ∣ ∣ x i ∣ ∣ 1 , i = 1 , 2 , . . . arg\min_{x_i}||x_i-y_i||^2+\lambda||x_i||_1,i=1,2,... argximinxiyi2+λxi1,i=1,2,...
考虑函数 f ( x ) = ( x − y ) 2 + λ ∣ x ∣ f(x)=(x-y)^2+\lambda|x| f(x)=(xy)2+λx,该函数在 x = 0 x=0 x=0时不可导。 f ( x ) f(x) f(x)的导数为:
Cannot read property 'type' of undefined
又因为 λ > 0 \lambda>0 λ>0,因此导数正负存在以下几种情况:

  • y > λ 2 y>\frac\lambda2 y>2λ时, f ′ ( x ) f'(x) f(x) ( − ∞ , 0 ) (-\infin,0) (,0)上为负, ( 0 , y − λ 2 ) (0,y-\frac\lambda2) (0,y2λ)上为负, ( y − λ 2 , + ∞ ) (y-\frac\lambda2,+\infin) (y2λ,+)上为正, f ( x ) f(x) f(x) x = y − λ 2 x=y-\frac\lambda2 x=y2λ时取最小值
  • − λ 2 < y < λ 2 -\frac\lambda2<y<\frac\lambda2 2λ<y<2λ时, f ′ ( x ) f'(x) f(x) ( − ∞ , 0 ) (-\infin,0) (,0)上为负, ( 0 , + ∞ ) (0,+\infin) (0,+)上为正, f ( x ) f(x) f(x) x = 0 x=0 x=0时取最小值
  • y < − λ 2 y<-\frac\lambda2 y<2λ时, f ′ ( x ) f'(x) f(x) ( − ∞ , y + λ 2 ) (-\infin,y+\frac\lambda2) (,y+2λ)上为负, ( y + λ 2 , 0 ) (y+\frac\lambda2,0) (y+2λ,0)上为正, ( 0 , + ∞ ) (0,+\infin) (0,+)上为正, f ( x ) f(x) f(x) x = y + λ 2 x=y+\frac\lambda2 x=y+2λ时取最小值

因此,要 f ( x ) = ( x − y ) 2 + λ ∣ x ∣ f(x)=(x-y)^2+\lambda|x| f(x)=(xy)2+λx取得最小值, x x x的取值为:
x ∗ = s i g n ( y ) ( ∣ y ∣ − λ 2 ) + x^*=sign(y)(|y|-\frac\lambda2)_+ x=sign(y)(y2λ)+
如果向量 x \bold x x的每个维度都满足使得当前维度 i i i上有 ∣ ∣ x i − y i ∣ ∣ 2 + λ ∣ ∣ x i ∣ ∣ 1 ||x_i-y_i||^2+\lambda||x_i||_1 xiyi2+λxi1最小,则整体的 ∣ ∣ x − y ∣ ∣ 2 + λ ∣ ∣ x ∣ ∣ 1 ||\bold x-\bold y||^2+\lambda||\bold x||_1 xy2+λx1也最小。每个维度都有:
x i ∗ = s i g n ( y i ) ( ∣ y i ∣ − λ 2 ) + x_i^*=sign(y_i)(|y_i|-\frac\lambda2)_+ xi=sign(yi)(yi2λ)+
此时,向量 x \bold x x的解为:
x ∗ = s i g n ( y ) ( ∣ y ∣ − λ 2 ) + \bold x^*=sign(\bold y)(|\bold y|-\frac\lambda2)_+ x=sign(y)(y2λ)+
得证。


二、编程部分

2.1 算法流程

对于一个N分类的人脸识别问题,依据论文《Robust Face Recognition via Sparse Representation 》中的理论,可以将求解过程由如下流程表示:

  1. 对于N中的每一类,将图片矩阵按列组合,每个样本化为一个列向量。对于第 i 类,将该类的样本化为列向量得到 v i , 1 , v i , 2 , . . . , v i , n i v_{i,1},v_{i,2},...,v_{i,n_i} vi,1,vi,2,...,vi,ni将这些列按行拼接,得到矩阵 A i = [ v i , 1 , v i , 2 , . . . , v i , n i ] ∈ R m , n i A_i=[v_{i,1},v_{i,2},...,v_{i,n_i}]\in\R^{m,n_i} Ai=[vi,1,vi,2,...,vi,ni]Rm,ni,其中 m m m为图片的像素数, n i n_i ni为该类的样本数。

  2. 将所有矩阵进行拼接得到整个训练集的矩阵 A = [ A 1 , A 2 , . . . , A N ] ∈ R m , n A=[A_1,A_2,...,A_N]\in\R^{m,n} A=[A1,A2,...,AN]Rm,n,其中 n n n为整个训练集的大小。

  3. 标准化 A 的列使得其有单位 l 2 l^2 l2范数(即各列所有元素的平方和为 1 )

  4. 对于测试集样本 y \bold y y,求解:

    x ^ 1 = a r g min ⁡ x ∣ ∣ x ∣ ∣ 1 \hat x_1=arg\min_{\bold x}||\bold x||_1 x^1=argxminx1
    满足 A x = y A\bold x=\bold y Ax=y

  5. 计算训练集每个类中的样本单独对测试样本的还原程度 r i ( y ) = ∣ ∣ y − A δ i ( x ^ 1 ) ∣ ∣ 2 r_i(\bold y)=||y-A\delta_i(\hat x_1)||_2 ri(y)=yAδi(x^1)2,其中 δ i \delta_i δi能筛选出第 i 类的训练样本,过滤掉其他类的样本。

  6. 最终结果即为使得 r i ( y ) r_i(\bold y) ri(y)最小的类 i 。

2.2 代码实现

我使用了matlab2020b版本实现了该算法。具体的程序文件组织方式请参照src文件夹下的README.txt文件。

程序的图片预处理部分用到了Image Processing Toolbox工具箱,稀疏求解部分用到了Optimization Toolbox工具箱。

2.2.1 训练和测试集的选择

训练集和测试集人脸图片均来自FLW,选用的是All images aligned with funneling的图片。在官网给出的统计文档中给出了图片库中所有的人名与对应图片的张数。我选取了图片数量最多的前50类:

1 ['George_W_Bush', 530]
2 ['Colin_Powell', 236]
3 ['Tony_Blair', 144]
4 ['Donald_Rumsfeld', 121]
5 ['Gerhard_Schroeder', 109]
6 ['Ariel_Sharon', 77]
7 ['Hugo_Chavez', 71]
8 ['Junichiro_Koizumi', 60]
9 ['Jean_Chretien', 55]
10 ['John_Ashcroft', 53]
11 ['Jacques_Chirac', 52]
12 ['Serena_Williams', 52]
13 ['Vladimir_Putin', 49]
14 ['Luiz_Inacio_Lula_da_Silva', 48]
15 ['Gloria_Macapagal_Arroyo', 44]
16 ['Arnold_Schwarzenegger', 42]
17 ['Jennifer_Capriati', 42]
18 ['Laura_Bush', 41]
19 ['Lleyton_Hewitt', 41]
20 ['Alejandro_Toledo', 39]
21 ['Hans_Blix', 39]
22 ['Nestor_Kirchner', 37]
23 ['Andre_Agassi', 36]
24 ['Alvaro_Uribe', 35]
25 ['Megawati_Sukarnoputri', 33]
26 ['Silvio_Berlusconi', 33]
27 ['Tom_Ridge', 33]
28 ['Kofi_Annan', 32]
29 ['Roh_Moo-hyun', 32]
30 ['Vicente_Fox', 32]
31 ['David_Beckham', 31]
32 ['John_Negroponte', 31]
33 ['Guillermo_Coria', 30]
34 ['Recep_Tayyip_Erdogan', 30]
35 ['Bill_Clinton', 29]
36 ['Mahmoud_Abbas', 29]
37 ['Jack_Straw', 28]
38 ['Juan_Carlos_Ferrero', 28]
39 ['Ricardo_Lagos', 27]
40 ['Gray_Davis', 26]
41 ['Rudolph_Giuliani', 26]
42 ['Tom_Daschle', 25]
43 ['Atal_Bihari_Vajpayee', 24]
44 ['Jeremy_Greenstock', 24]
45 ['Winona_Ryder', 24]
46 ['Jose_Maria_Aznar', 23]
47 ['Saddam_Hussein', 23]
48 ['Tiger_Woods', 23]
49 ['George_Robertson', 22]
50 ['Hamid_Karzai', 22]

上面数据每一行的第一个数字为编号,之后的列表中第一个元素的人名,第二个为其对应的图片张数。所有的类别图片都大于20张,且在图片库中从0001开始进行了编号。样本划分如下:

  • 训练集是每一类的前20张图片,即编号为00010020的部分
  • 若当前类除去训练集的20张图,剩余图片不足20张,则剩下的图片全部划分到测试集
  • 若当前类除去训练集的20张图,剩余图片大于等于20张,则编号为00210040的20张图片划入测试集

2.2.2 训练集的读取和矩阵A的生成

程序的启动文件为FaceRecognition.m

图片库中所有图片的规格一致,且人脸都移动到了图像中心。因此,为了简单起见,我使用统一的裁剪和下采样方法处理所有图片。设置如下:

% 设置图片裁剪范围
row_range = 96:175;
col_range = 101:160;
% 设定下采样的长和宽
row = 16; 
col = 12;

裁剪范围设定在图片的中央处,基本能裁剪出人脸,大小为 80 * 60。为了稀疏求解能得到解,需要对图片进行下采样。预设下采样后的大小为 16 * 12。接下来调用LoadTrainSet函数:

% 读入训练集,返回样本列组成的矩阵A以及全部类别class_name
[A, class_name] = LoadTrainSet(row, col);

该函数在文件LoadTrainSet.m中实现,能够读入全部的训练集图片并处理,返回全部训练样本的矩阵 A,以及数组class_nameclass_name的下标为类别的编号,索引结果为该编号对应的人名。

LoadTrainSet函数中,首先定义 A 的大小。行数为每个样本的大小,设置为下采样结果大小row * col,列数为所有的样本数。共50类,每类20个样本。class_name初始化为空的 cell 数组。接下来,对每个类调用LoadOnePerson函数。每调用一次,将某个人名(即一类)的全部样本读入、处理,并更新 A 和class_name

function  [A, class_name] = LoadTrainSet(row, col)
    A = zeros(row * col, 50 * 20);  % 每个样本数据row * col维,共50类,每类20个样本
    class_name = {};                % 将类别编号与人脸姓名对应
    [A, class_name] = LoadOnePerson('Alejandro_Toledo', A, class_name, 0, row, col);
    [A, class_name] = LoadOnePerson('Alvaro_Uribe', A, class_name, 1, row, col);
    ...	% 共50个类,总计需要调用LoadOnePerson 50次。限于篇幅这里进行省略。
end

loadOnePerson的函数原型如下:

function [A, class_name] = LoadOnePerson(name, A, class_name, no, row, col)

参数列表依次为:当前人名name、矩阵 A、class_name、当前是第几类no、下采样图片大小rowcol。返回更新后的 A 和class_name

首先对class_name进行更新,将类别(数字)与人名对应:

	class_name(no+1) = {name};

接着依次读取该类下的20张图片:

    for i = 1:20
        if i<10
            img = imread(['train_set/',name,'/',name,'_000',num2str(i),'.jpg']);
        else
            img = imread(['train_set/',name,'/',name,'_00',num2str(i),'.jpg']);
        end
        ...
    end

img即为当前的图片。首先将图片进行单位化:

		img = im2double(img);

对图片进行裁剪,再将图像由RGB空间转换为灰度图:

		img = im2gray(img(row_range, col_range));

使用线性差值的方法,对图片进行下采样:

		img = imresize(img,[row, col],'bilinear');

将图片转换为列向量, l 2 l^2 l2范数化为1,并插入矩阵 A 中:

		img = reshape(img, [row * col, 1]);
		img = img / norm(img);
        A(:, 20 * no + i) = img;

如此一来,就能够读入并处理全部的训练样本,并生成矩阵 A 了。

2.2.3 稀3疏求解

读入一个测试样本(以Alejandro_Toledo的第22张图片为例):

test_img = imread('test_set/Alejandro_Toledo/Alejandro_Toledo_0022.jpg');

同样要进行灰度处理、裁剪、下采样、转换为列向量、向量单位化。

test_img = im2double(test_img);	 					% 灰度图化
test_img = im2gray(test_img(row_range, col_range)); % 裁剪出面部     
test_img = imresize(test_img,[row, col],'bilinear');% 二线性差值下采样
test_img = reshape(test_img, [row * col, 1]);    	% 列向量化
test_img = test_img / norm(test_img);   			%向量单位化

下面要进行Ax=y的求解,其中 A 为之前求得的 A 矩阵,y 为列向量test_img。具体求解的代码我参考了一篇博客,用基追踪BP方法,对满足使得 x x x l 1 l^1 l1范数最小的 x x x 进行求解:

x = BP_linprog(test_img, A);

求解部分代码的实现在BP_linprog.m文件中:

function [ alpha ] = BP_linprog( s,Phi )
    [s_rows,s_columns] = size(s);  
    if s_rows<s_columns  
        s = s';%s should be a column vector  
    end 
    p = size(Phi,2);
    %according to section 3.1 of the reference
    c = ones(2*p,1);
    A = [Phi,-Phi];
    b = s;
    lb = zeros(2*p,1);
    x0 = linprog(c,[],[],A,b,lb);
    alpha = x0(1:p) - x0(p+1:2*p);
end

通过稀疏求解,得到了系数表达系数向量 x ^ \hat x x^

2.2.4 残差计算与输出结果

使用 50 维的向量r表示x在各个类上的残差:

r = zeros(50,1);

依据当前的类别,将向量x在其他类别上的数改为0即可得到经 δ i \delta_i δi筛选后的结果,记为delta_x。将delta_x与 A 相乘,求其与测试样本的差的 l 2 l^2 l2范数并记录:

for i = 1:50
    delta_x = zeros(1000, 1);
    delta_x((i-1)*20+1 : i*20) = x((i-1)*20+1 : i*20);
    r(i) = norm(test_img - A * delta_x, 2);
end

如此一来,就得到了所有类的训练样本对训练集的还原能力,即所有类的残差。选择残差最小的类进行输出,这就是最终答案:

for i = 1:50
    if r(i) == min(min(r))
        res = class_name(i);
        disp(res);
        break
    end
end

2.2.5 全部测试集的测试与统计

上面的代码只是对单个测试样本进行了测试。下面,对全部的样本进行测试并进行统计。调用以下函数:

TestAll(A);

该函数在TestAll.m文件中实现。该函数会记录所有测试样本测试后的正确个数、错误个数、TP、TN、FP、FN值:

function TestAll(A)
    total_right = 0;
    total_wrong = 0;
    ROC = zeros(50, 4);    % 分别记录50个类的TP、TN、FP、FN值
    [total_right, total_wrong, ROC] = test('Alejandro_Toledo', 1, 39, ROC, total_right, total_wrong, A);
    [total_right, total_wrong, ROC] = test('Alvaro_Uribe', 2, 35, ROC, total_right, total_wrong, A);
    ... % 共50个类,总计需要调用test函数50次。限于篇幅这里进行省略。
    %打印结果
    disp(total_right);
    disp(total_wrong);
    total = sum(sum(ROC(:,:)));
    disp(sum(ROC(:,1)) / total);
    disp(sum(ROC(:,2)) / total);
    disp(sum(ROC(:,3)) / total);
    disp(sum(ROC(:,4)) / total);

total_righttotal_wrong统计全部的正确分类数和错误分类数,ROC为 50 * 4 的矩阵,第一维指示50类中的一类,该类下的四个元素分别为该类对应的TP、TN、FP、FN值。程序最后输出total_righttotal_wrong,以及50个类平均下来得到的TP、TN、FP、FN值。

通过调用test函数,对单个类的全部样本进行测试。函数原型为:

function [total_right, total_wrong, ROC] = test(name, id, num, ROC, total_right, total_wrong, A)

参数列表中的nameid表示当前的类别人名和编号,num表示当前类下测试样本的个数。之后是统计参数和矩阵 A。返回更新后的total_righttotal_wrongROC

函数内依次读入各个测试样本,进行预处理,通过上述方法得出结果。代码和之前的测试单个样本基本相同,不再重复展示。下面终点说明统计参数的更新方法。同样,计算各个样本的残差后,寻找残差最小的类:

        for j = 1:50
            if r(j) == min(min(r))
           	...

若分类正确,total_right增加1,同时更新TP、TN、FP、FN值。

                % 若分类正确
                if j == id
                    right = right + 1;
                    for k = 1:50
                        % 本类的TP+1
                       if k == id
                           ROC(k, 1) = ROC(k, 1) + 1;
                       % 其他类的TN+1
                       else
                           ROC(k, 2) = ROC(k, 2) + 1;
                       end
                    end

当前样本对本类来说是正类,分类正确,因此TP增大1,对其他类来说是负类,分类正确,因此TN增大1。

如果分类错误,则total_wrong增加1,同时更新TP、TN、FP、FN值。

                % 若分类错误
                else
                    wrong = wrong + 1;
                    for k = 1:50
                        % 所属类的FN+1
                        if k == id
                            ROC(k, 4) = ROC(k, 4) + 1;
                        % 被分到的类的FP+1
                        elseif k == j
                            ROC(k, 3) = ROC(k, 3) + 1;
                        % 其他类的TN+1
                        else    
                            ROC(k, 2) = ROC(k, 2) + 1;
                        end
                    end
                end

对于样本所属的类,该样本为正类,分类错误,因此FN+1,而对于被错误分到的类,该样本为负类,分类错误,因此FP增加1。对于其他类来说,该样本为负类,没有分到本类,因此TN增大1。

将全部样本分类完成后,数值统计也同步完成。在TestAll函数最后输出全部的统计结果即可。

2.3 代码运行结果

2.3.1 单个测试样本的结果

使用Alejandro_Toledo的编号为0022的图片进行测试,将原图、图片预处理后(图片矩阵转换为列向量前)的结果、稀疏表达系数图 、类别表示残差图依次打印,得到如下结果:

可以看到,测试图片经过预处理,能基本截取到人脸,并通过灰度化和下采样得到测试的人脸图像。Alejandro_Toledo在我的程序的模型中属于第一类,从稀疏表达系数统计图中可以看到,系数最为稠密的的地方处于最前端,即第一类的20张训练样本对应的位置。从类别表示残差图中也可以看出,第一类的残差明显小于其他类的残差。因此,该图像会被归类到第一类。通过class_name数组,程序会在控制台输出该类对应的人名:

分类结果为Alejandro_Toledo,结果正确。

我又从第二类、第三类各找了一张测试集图片,结果分别如下:

2.3.2 全部测试集的统计结果

在程序最后调用TestAll(A);测试全部的测试集并输出统计结果。测试集样本大小为666,最终输出结果如下:

六个数据分别为正确分类的样本数、误分类的样本数、全部类的TP、TN、FP、FN平均值占总样本数的比例。

准确度: 441 / 666 = 66.22 % 441 / 666=66.22\% 441/666=66.22%

查全率:
T P R = T P / ( T P + F N ) = 0.66 TPR = TP / (TP+FN) =0.66 TPR=TP/(TP+FN)=0.66
查准率:
T N / ( T N + F P ) = 0.66 TN/(TN+FP)=0.66 TN/(TN+FP)=0.66
6459)]

[外链图片转存中…(img-7NCBIsxo-1653223586460)]

2.3.2 全部测试集的统计结果

在程序最后调用TestAll(A);测试全部的测试集并输出统计结果。测试集样本大小为666,最终输出结果如下:

六个数据分别为正确分类的样本数、误分类的样本数、全部类的TP、TN、FP、FN平均值占总样本数的比例。

准确度: 441 / 666 = 66.22 % 441 / 666=66.22\% 441/666=66.22%

查全率:
T P R = T P / ( T P + F N ) = 0.66 TPR = TP / (TP+FN) =0.66 TPR=TP/(TP+FN)=0.66
查准率:
T N / ( T N + F P ) = 0.66 TN/(TN+FP)=0.66 TN/(TN+FP)=0.66

猜你喜欢

转载自blog.csdn.net/newlw/article/details/124916167