對稱矩陣的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的正、負特徵值的個數(知識回顧:兩個二次型之間存在可逆線性變換的充要條件是它們的正、負慣性指數相同)。

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