LLE是一个很经典的降维方法,关于它的理论我就不赘述了。最近我在学习LLE的时候,查看了很多LLE相关的论文,最初偷懒想在CNKI上找两篇中文文献作为入门,发现那些论文都是纯扯蛋,竟然连原文的意思都没有搞懂就敢发论文,对于这些混吃等S的教授,只能竖着中指说无数遍“SHIT“才能泄我心头之恨啊。
先推荐一篇LLE原论文作者对LLE的详细阐述的文章,http://www.cs.nyu.edu/~roweis/lle/publications.html, 在这个网址可以找到“Nonlinear dimensionality reduction by locally linear embedding”这篇论文,还可以看到一篇对LLE做二次阐述的长文“An Introduction to Locally Linear Embedding.”,认真地看完这两篇论文,并试着推导它的每一个公式,你一定会受益匪浅。
很多人可能在看了论文后依然心存各种疑惑,毕竟论文很多地方都是略写了,这时候看看Matlab源码可能会对你带来很大帮助。这篇文章就是为了引导大家更好地理解LLE的算法,对源码做了一些分析,有些地方我理解的不一定正确,欢迎大家拍砖。
% LLE ALGORITHM (using K nearest neighbors)
%
% [Y] = lle(X,K,dmax)
%
% X = data as D x N matrix (D = dimensionality, N = #points)
% K = number of neighbors
% dmax = max embedding dimensionality
% Y = embedding as dmax x N matrix
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
先对算法的输入和输出做简单介绍,输入D*N的一个矩阵,D是每个样本的维数,N代表有N个样本。注意一下数据是怎么放的,列是样本,行是每一维度的数据。假如你有1000幅关于人脸的大小为128*128的灰度图像,那么D=128*128=16384,N=1000。每一列是一个16384的向量,代表一张人脸图像,每一行代表不同人脸中某个位置的像素。dmax是你希望降维之后获得的维数,K是你采用K近邻算法设置的K。输出Y是dmax*N的矩阵,如果你把人脸图像降成2维,最后就是2*N的矩阵,如果你的人脸是由向左转或向右转的数据组成,最后在左边的可能就是往左转,在右边就是往右转。
function [Y] = lle(X,K,d)
[D,N] = size(X);
fprintf(1,'LLE running on %d points in %d dimensions\n',N,D);
X是D*N的样本矩阵,把维数D放在D,样本数N放在N。接下来就是构建K近邻图。
% STEP1: COMPUTE PAIRWISE DISTANCES & FIND NEIGHBORS
fprintf(1,'-->Finding %d nearest neighbours.\n',K);
X2 = sum(X.^2,1);
distance = repmat(X2,N,1)+repmat(X2',1,N)-2*X'*X;
[sorted,index] = sort(distance);
neighborhood = index(2:(1+K),:);%取出每个点的K个邻居(即距离最小的前K个邻居),因为第一个是自身到自身的距离0,所以从第二个开始。获取一个K*N的矩阵
Matlab代码都是非常精简的,当然,这也跟使用他的人有很大关系,上面这么几行代码就完成了距离矩阵构建、距离排序、构建邻接图与索引这么多事,Amazing!
在Matlab中X.^2是对X矩阵中每个元素做平方运算,对于Matlab不熟悉的童鞋可以自己尝试以下代码:
获得的结果就是:
sum(X.^2,1)是将X各元素平方后按列求和,摆成一个1*N的行向量X2。将各个列向量组成的样本看成一个整体,实际上就是:
接下来我们看下一行:
distance = repmat(X2,N,1)+repmat(X2',1,N)-2*X'*X;
首先说明一下repmat(matrix,m,n)函数,它是将matrix矩阵填充到一个m*n的矩阵,这个矩阵的每个元素就是matrix。所以repmat(X2,N,1)是摆成一个N*1的矩阵,每个元素是X2,
最后就是如下矩阵:
repmat(X2',1,N)得到的矩阵如下:
X‘*X得到的矩阵是:
我们知道两个向量的距离公式为:
所以distance就在一行代码上完成了距离矩阵的计算。继续来看这行代码:
[sorted,index] = sort(distance);
matlab的sort函数默认是对列排序,即对每一列的元素从小到大排序,可以看下面的例子:
sorted就是按列排序后的矩阵,index对应排序之前的列索引。
neighborhood = index(2:(1+K),:);
因为index保存是相应的索引,所以对于第i列来说(实际上就是考察第i个样本),前面的K+1个数就是到样本i距离最小的K+1个样本,这里包括了自身,因为自己到自己的距离为0,所以肯定会出现在前面K个,这就是取K+1个的原因,从第2到K+1取出最近K邻居放在neighborhood。neighborhood就是截取了index矩阵的第2行起到第K+1行,形成一个K*N的矩阵。
第二步开始,求权值矩阵W。
% STEP2: SOLVE FOR RECONSTRUCTION WEIGHTS
fprintf(1,'-->Solving for reconstruction weights.\n');
if(K>D)
fprintf(1,' [note: K>D; regularization will be used]\n');
tol=1e-3; % regularlizer in case constrained fits are ill conditioned
else
tol=0;
end
W = zeros(K,N);
for ii=1:N
z = X(:,neighborhood(:,ii))-repmat(X(:,ii),1,K); % shift ith pt to origin
C = z'*z; % local covariance
C = C + eye(K,K)*tol*trace(C); % regularlization (K>D)
W(:,ii) = C\ones(K,1); % solve Cw=1
W(:,ii) = W(:,ii)/sum(W(:,ii)); % enforce sum(w)=1
end;
因为K>D的时候,C是奇异矩阵,不能求逆,所以对C进行了调整,但这种调整不能过大,以免获得的结果偏离真值过远。这里采取的方法是加上这么一个矩阵:
这是K*K的矩阵,对角线的元素值非常小,也就是保证在求解W过程中对原来的W影响非常小。
我们先回到论文,看看怎么求得权值矩阵W。在“An Introduction to locally linear embedding”这篇论文中 是这样表述的:
上式是目标函数,在下述限制条件下求得最小值:
其中C是由下式获得的:
上面的式子都是显而易见的,接下来的问题是如何通过上述条件怎么求得W。这里要用到拉格朗日乘子法,关于什么是拉格朗日乘子法请自行查阅相关资料。总之,我们会得到一个如下的函数,称为朗格朗日函数:
对这个函数求极值获得的W就是使epsilon获得最小值的W。对各变量分别求偏导,得到如下方程组:
将上述方程组写成矩阵的形式,得到下式:
这里求得的C矩阵和论文的C有点出入,作者的C矩阵对角线元素是没有2倍因子的,因为作者并没有写出推导过程,直说用拉格朗日乘子法就可以得到答案,也没说这个是近似解还是精确解,这是我很不解的地方。如果在求K近邻的时候,把自己算上的话,C矩阵的对角线元素是为0的,那样的话我这个C对角元素的两倍因子也是可以去掉的。可以进一步推出:
这就求出了跟第ii个样本相关的W(ii),这是一个1*K的向量。现在回过头来看看Matlab源码。
W = zeros(K,N);
这是获得一个所有元素为0的K*N矩阵W。
for ii=1:N
z = X(:,neighborhood(:,ii))-repmat(X(:,ii),1,K); % shift ith pt to origin
C = z'*z; % local covariance
C = C + eye(K,K)*tol*trace(C); % regularlization (K>D)
W(:,ii) = C\ones(K,1); % solve Cw=1
W(:,ii) = W(:,ii)/sum(W(:,ii)); % enforce sum(w)=1
end;
上面的Z是这样获得的,先取出X的所有邻接节点,即K个邻居,然后每个邻居减去X(ii),求解过程如下:
C=z'*z是什么也就很清楚了。得到的C矩阵的每一项就是论文所述的C矩阵:
求出C矩阵后,还进行了调整,这是为了避免C是奇异矩阵的情况。
C = C + eye(K,K)*tol*trace(C); % regularlization (K>D)
这一行代码就是调整,已在前面说过。
W(:,ii) = C\ones(K,1);
上面这一行就是一个所有元素为1的K*1的行向量乘以C的逆。
W(:,ii) = W(:,ii)/sum(W(:,ii));
上面这一行就是使W(ii)各元素相加的和为1。
经过上述过程,第二步求解W权值矩阵就算完结了。现在看第三步,怎么求解Y。
% STEP 3: COMPUTE EMBEDDING FROM EIGENVECTS OF COST MATRIX M=(I-W)'(I-W)
fprintf(1,'-->Computing embedding.\n');
% M=eye(N,N); % use a sparse matrix with storage for 4KN nonzero elements
M = sparse(1:N,1:N,ones(1,N),N,N,4*K*N);
for ii=1:N
w = W(:,ii);
jj = neighborhood(:,ii);
M(ii,jj) = M(ii,jj) - w';
M(jj,ii) = M(jj,ii) - w;
M(jj,jj) = M(jj,jj) + w*w';
end;
M是一个N*N的稀疏单位矩阵矩阵,即对角线元素为1,其余元素均为0。它的构建方式不同,存储方式不同,但作用还是跟普通矩阵一样,不必深究稀疏矩阵是什么,当普通矩阵用就是了。
w = W(:,ii);
这是取出W(ii),没什么好解释道的。
jj = neighborhood(:,ii);
取出ii的所有邻居放在jj这个向量里,它当然是K*1的向量。
根据论文的推导,M这个矩阵元素如下:
因为M已经是一个单位矩阵,所有前面的delta就可以去掉了。反过来思考,假设我们现在取出了W(ii),然后去修改跟W(ii)相关的M元素也是等价的。可以由一下推导过程给出:
M(ii,jj) = M(ii,jj) - w';
M(jj,ii) = M(jj,ii) - w;
M(jj,jj) = M(jj,jj) + w*w';
其实这几行代码实现的功能就是上述过程。M本来是N*N的矩阵,但是在处理的时候,只用到了跟第ii个节点相邻的K个分量。获得了M,再对M求特征值、特征向量就比较简单了。
% CALCULATION OF EMBEDDING
options.disp = 0; options.isreal = 1; options.issym = 1;
[Y,eigenvals] = eigs(M,d+1,0,options);
Y = Y(:,2:d+1)'*sqrt(N); % bottom evect is [1,1,1,1...] with eval 0
Matlab的eigs是求特征值与特征向量的函数,详细资料请自行查阅相关资料。
最后取出对应Y的最小的d个特征值的特征向量即为Y。M是对称矩阵,并且行列式为0,所以它一定有特征值0,但是有特征值0就一定有特征向量[1 1 ...1]吗?这一直搞不明白。