对称矩阵的RtDR分解(LDLt分解)C代码

最近碰到求解线性方程组以及求矩阵的特征值等问题,OpenCV自带的算法实在是太慢了,另外我还试了Eigen库,比OpenCv虽然快了一倍,但是比Matlab还是慢了一个量级不止。。。因此我决定自己编写几个程序以满足我的特定需要。这篇博文将给出一个对称矩阵的RtDR分解方法(书里面一般都是LDLt分解,我直接求得是转置,即R=L')。

数值计算不同于基本数学理论,《线性代数》以及《高等数学基础》是工科的数学课程,里面介绍了很多一般线性代数问题的解法,但是那些只是在理论上可行,并没有考虑计算机存储的舍入误差的影响,如果照着课本上的思路来实现算法,则在计算效率和计算结果的精度上都与Matlab相差甚远。为此,我花了一些时间学习了两本数值分析的书,其中《Matrix Computations》(4th Edition)是非常经典的书了,写得也很赞。根据书中介绍的方法写了一些函数,在处理中小型矩阵(e.g.N在200以内)时,具有较快的速度和较高的精度。

本文将给出这样一个函数:输入一个实对称矩阵A,计算它的RtDR分解:A=R'*D*R,其中R是单位上三角矩阵,D是对角矩阵。该方法要求A的所有顺序主子式都是非奇异的。

这是Matlab代码 version#1:

function  [L,D] = LDLtDecomp(A)
% A = L*D*L'
% A is symmetric
% L is unit lower triangular
% D is diagonal
n = size(A,1);
v = zeros(n,1);
A(2:n,1) = A(2:n,1) / A(1,1);
for i = 2:n
    v(1:i-1) = A(i,1:i-1) .* diag(A(1:i-1,1:i-1))';
    A(i,i) = A(i,i) - A(i,1:i-1) * v(1:i-1);
    A(i+1:n,i) = (A(i+1:n,i) - A(i+1:n,1:i-1) * v(1:i-1)) / A(i,i);
end
D = diag(diag(A));
L = A;
for i = 1:n
    L(i,i) = 1;
    for j = i+1:n
        L(i,j) = 0;
    end
end

这是Matlab代码version#2:

function  [R,D] = RtDRdecomp(A)
% The transposed version of LDLtDecomp()
% A = R'*D*R
% A is symmetric
% R is unit upper triangular
% D is diagonal
assert(norm(A-A',1) < 1e-10); % A should be symmetric
n = size(A,1);
A(1,2:n) = A(1,2:n) / A(1,1);
for i = 2:n
    v = A(1:i-1,i) .* diag(A(1:i-1,1:i-1));
    A(i,i:n) = A(i,i:n) - v' * A(1:i-1,i:n);
    A(i,i+1:n) = A(i,i+1:n) / A(i,i);
end
D = diag(diag(A));
R = A;
for i = 1:n
    R(i,i) = 1;
    R(i,1:i-1) = 0;
end

这是C代码:

//Here we introduce another symmetric matrix decomposition that does not require a definiteness property.
//by: yuxianguo, 2018/11/30
int //return 0 means failure; otherwise success (require A to have an LU factorization)
yuRtDRdecomp( //Compute the decomposion: A=R'*D*R, where R is unit upper triangular and D is diagonal; D is stored in A.diagonal, R is stored in A.upperTriangular
	double *A, int N) //A[N*(N+1)/2] is the upper triangular part of a symmetric matrix; require all the principle submatrices to be non-singular, or the decomposition fails
{
	int i, j, k, Ni = N;
	double a, b, *p = A, *q;
	for(i = 0; i < N; i++, Ni--) {
		//Ni = N - i; p points to A[i][i];
		//for(j=0;j<i;j++)//here we use i-j as the loop index
		for(j = i, q = A; j; j--) {
			a = *q; q += j; b = *q++;
			a *= -b; *p += a * b;
			for(k = 1; k < Ni; k++)
				p[k] += a * *q++;
		}
		a = *p++;
		if(!a)
			return 0; //fail
		a = 1.0 / a;
		for(k = 1; k < Ni; k++)
			*p++ *= a;
	}
	return 1;
}

关于C代码的补充说明:

(1)在我的设置里,所有对称矩阵只保存其上三角部分,一个N-by-N大小的矩阵,只需保存N*(N+1)/2个元素;

(2)上述代码将输入矩阵A的分解结果D和R分别覆写到A的内存中,其中D为A的对角线部分,R为A的上三角部分(R对角线元素全是1);

(3)我以前写程序喜欢用模板、SSE(AVX)、OpenMP(多核并行)、pthread(多线程)。现在更喜欢写纯C代码,因为好移植,也好修改;

(4)上述算法不太适合处理大型矩阵,比如N>300的情形。书上介绍说处理大型矩阵一般都考虑并行算法,并行算法不仅仅是使用多核或多线程,还需要从算法设计上考虑并行,在矩阵计算方面,一般使用矩阵分块方法。我目前还没有处理大矩阵的需求,即使是大数据,往往也只有小矩阵(e.g.协方差矩阵);

(5)我喜欢用返回值0表示函数失败,这与很多经典的逻辑不一致,因为我有很多函数都是返回的指针,在那些函数里我可以用返回NULL表示函数失败。

下面是一个简单的例子:

(1)用Matlab产生一个对称矩阵

n = randi(90) + 15;
U = rand(n) * - 0.5;
D = rand(n,1) * 2 - 0.5;
A = U * diag(D) * U';
yusave('A',A);

其中yusave是我写的一个函数,将Matlab矩阵保存到二进制文件中。

(2)使用C代码处理上面生成的矩阵,并保存结果

int main() {
	Buffer Abuf; reserve(&Abuf, 0);
	int N;
	yuLoad("A", 1, &Abuf, &N, 0, 0, 0);
	double *A1 = (double*)Abuf.p;
	double *A = new double[N*(N + 1) / 2], *p = A;
	for(int i = 0; i < N; i++)
		for(int j = i; j < N; j++)
			*p++ = A1[i * N + j];
	
	if(!yuRtDRdecomp(A, N)) {
		printf("failed!");
		return getchar();
	}
	p = A;
	memset(A1, 0, N * N << DBLShift);
	for(int i = 0; i < N; i++)
		for(int j = i; j < N; j++)
			A1[i * N + j] = *p++;
	yuSave("X", A1, N, N, 1, DOUBLE64);

	release(&Abuf);
	delete[] A;
	return 0;
}

main函数先把数据文件读进来,然后提取矩阵的上三角部分,再调用函数进行矩阵分解,最后将分解后的矩阵恢复到方阵形式以便保存到文本中。

(3)在Matlab中查看结果

X = ld('X');
D = diag(diag(X));
R = X;
for i = 1:n
    R(i,i) = 1;
    R(i,1:i-1) = 0;
end
E = abs(A - R'*D*R);
max(E(:))

在我们的测试中,n=104,max(E(:))=5.9508e-14

注:double数据的舍入误差大概是DBL_EPSILON=2e-16,基于double类型的编程求解算法结果精度不会比2e-16更高。另外Matlab自己也有计算误差,在上面的例子中,即使我们的R和D是完全正确的,max(E(:))也可能不是0.

 

总结:

最后小结一下。这篇博文旨在抛砖引玉,大家在编写数值计算程序时,最好能了解一点相关的知识。数值计算方法和传统的数学方法并不完全一样。(PS:本文的例子可能不够好,或许用QR分解作为案例来讲解会更恰当一些~~~。管他呢,代码请随便拿去用,只求别抹掉我的署名!)

另外啰嗦一下,个人以为RtDR分解(或LDLt分解)要比Cholesky分解(A=L'*L,其中L为下三角矩阵)更有用:首先二者的计算量是相同的,都是O(n^3/3)的水平;RtDR分解只要求A对称且存在LU分解,而Cholesky分解要求A为对称正定矩阵,因此存在Cholesky分解的矩阵必然能进行RtDR分解,反之则不然;最后,RtDR分解能得到一个对角阵,由此很容易得到A的正、负特征值的个数(知识回顾:两个二次型之间存在可逆线性变换的充要条件是它们的正、负惯性指数相同)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章