Eigen學習筆記25:稀疏矩陣操作

處理和解決稀疏問題的各種模塊,總結如下: 

模組 頭文件 內容
SparseCore
#include <Eigen / SparseCore>
SparseMatrix and SparseVector classes, matrix assembly, basic sparse linear algebra (including sparse triangular solvers)
SparseCholesky
#include <Eigen / SparseCholesky>
Direct sparse LLT and LDLT Cholesky factorization to solve sparse self-adjoint positive definite problems
SparseLU
#include <Eigen / SparseLU>
Sparse LU factorization to 解決通用的square sparse systems
SparseQR
#include <Eigen / SparseQR>
Sparse QR factorization解決稀疏線性最小二乘問題
IterativeLinearSolvers #include<Eigen/IterativeLinearSolvers>

迭代求解器用於求解大規模通用的線性square problems (including self-adjoint positive definite problems)

Sparse #include <Eigen/Sparse> 包括以上所有模塊

稀疏矩陣格式

在許多應用中(例如,有限元方法),通常需要處理非常大的矩陣,這樣的矩陣只有幾個係數不等於零。在這種情況下,通過使用僅存儲非零元素,可以減少內存消耗並提高性能。這樣的矩陣稱爲稀疏矩陣。

稀疏矩陣

SparseMatrix類是Eigen稀疏模塊的稀疏矩陣主要表示形式。它提供了高性能以及低內存使用率。

It implements a more versatile variant of the widely-used Compressed Column (or Row) Storage scheme. 

SparseMatrix包含了4個精簡的數組:

  • Values: 儲非零元素。
  • InnerIndices: 存儲非零元素的行/列索引.
  • OuterStarts: 存儲每一列/行第一個非零元素在上面兩個數組中的索引。
  • InnerNNZs: 存儲每一列/行中非零元素的數量。Inner在列優先矩陣中表示一個列,在行優先矩陣中表示一個行。Outer表示另一個方向。

在一個示例中可以更好地解釋此存儲方案。以下矩陣

0	3	0	0	0
22	0	0	0	17
7	5	0	1	0
0	0	0	0	0
0	0	14	0	8

and one of its possible sparse, column major representation:

Values: 22 7 _ 3 5 14 _ _ 1 _ 17 8
InnerIndices: 1 2 _ 0 2 4 _ _ 2 _ 1 4
OuterStarts: 0 3 5 8 10 12
InnerNNZs: 2 2 1 1 2

Currently the elements of a given inner vector are guaranteed to be always sorted by increasing inner indices. The "_" indicates available free space to quickly insert new elements. Assuming no reallocation is needed, the insertion of a random element is therefore in O(nnz_j) where nnz_j is the number of nonzeros of the respective inner vector. On the other hand, inserting elements with increasing inner indices in a given inner vector is much more efficient since this only requires to increase the respective InnerNNZs entry that is a O(1) operation.

本徵運算的結果始終會生成壓縮的稀疏矩陣。另一方面,在SparseMatrix中插入新元素會將其稍後轉換爲非壓縮模式。

如果中間沒有留未佔用的空間,就是壓縮模式。 對應於Compressed Column (or Row) Storage Schemes (CCS or CRS)。

任何SparseMatrix都可以通過SparseMatrix::makeCompressed() 方法變成稀疏模式。此時,InnerNNZs相對於OuterStarts而言,就是多餘的,因爲InnerNNZs[j] = OuterStarts[j+1]-OuterStarts[j]。因此SparseMatrix::makeCompressed()會釋放InnerNNZ存儲空間。

Eigen的操作總是返回壓縮模式的稀疏矩陣。當向一個SparseMatrix插入新元素時,會變成非壓縮模式。

SparseVector是SparseMatrix的一個特例,只有 Values 和 InnerIndices數組。沒有壓縮和非壓縮的區別。

這是以前以壓縮模式表示的矩陣:

Values:	22	7	3	5	14	1	17	8
InnerIndices:	1	2	0	2	4	2	1	4

輸出:

OuterStarts:	0	2	4	5	6	8

SparseVector is a special case of a SparseMatrix where only the Values and InnerIndices arrays are stored. There is no notion of compressed/uncompressed mode for a SparseVector.

第一個例子

#include <Eigen/Sparse>
#include <vector>
#include <iostream>
 
typedef Eigen::SparseMatrix<double> SpMat; 
typedef Eigen::Triplet<double> T;
 
void buildProblem(std::vector<T>& coefficients, Eigen::VectorXd& b, int n);
void saveAsBitmap(const Eigen::VectorXd& x, int n, const char* filename);
 
int main(int argc, char** argv)
{
  if(argc!=2) {
    std::cerr << "Error: expected one and only one argument.\n";
    return -1;
  }
  
  int n = 300;  // size of the image
  int m = n*n;  // number of unknows (=number of pixels)
 
  // Assembly:
  std::vector<T> coefficients;            
  Eigen::VectorXd b(m);                   
  buildProblem(coefficients, b, n);
 
  SpMat A(m,m);
  A.setFromTriplets(coefficients.begin(), coefficients.end());
 
  // Solving:
  Eigen::SimplicialCholesky<SpMat> chol(A);  
  Eigen::VectorXd x = chol.solve(b);         
 
  // Export the result to a file:
  saveAsBitmap(x, n, argv[1]);
 
  return 0;
}
 
void buildProblem(std::vector<T>& coefficients, Eigen::VectorXd& b, int n)
{
	b.setZero();
	Eigen::ArrayXd boundary = Eigen::ArrayXd::LinSpaced(n, 0,M_PI).sin().pow(2);
	for(int j=0; j<n; ++j)
	{
		for(int i=0; i<n; ++i)
		{
			int id = i+j*n;
			insertCoefficient(id, i-1,j, -1, coefficients, b, boundary);
			insertCoefficient(id, i+1,j, -1, coefficients, b, boundary);
			insertCoefficient(id, i,j-1, -1, coefficients, b, boundary);
			insertCoefficient(id, i,j+1, -1, coefficients, b, boundary);
			insertCoefficient(id, i,j,    4, coefficients, b, boundary);
		}
	}
}

void saveAsBitmap(const Eigen::VectorXd& x, int n, const char* filename)
{
	Eigen::Array<unsigned char,Eigen::Dynamic,Eigen::Dynamic> bits = 
(x*255).cast<unsigned char>();
	QImage img(bits.data(), n,n,QImage::Format_Indexed8);
	img.setColorCount(256);
	for(int i=0;i<256;i++) img.setColor(i,qRgb(i,i,i));
	img.save(filename);
}

在此示例中,我們首先定義元素爲double類型的稀疏矩陣SparseMatrix<double>

和類型相同的三元組列表Triplet<double>

A triplet is a simple object representing a non-zero entry as the triplet: row index, column index, value.

SparseMatrix類

矩陣矢量特性
SparseMatrix類和SparseVector類有3個模板參數元素類型,存儲順序,(ColMajor(默認)或RowMajor)和內索引類型(默認int)。

對於密集Matrix類,構造函數的參數是對象的大小。

SparseMatrix <std::complex <float>> mat(1000,2000); //聲明一個複雜的<float>的1000x2000列主壓縮稀疏矩陣
SparseMatrix <double,RowMajor>墊(1000,2000); //聲明一個double的1000x2000行主壓縮稀疏矩陣
SparseVector <std::complex <float>> vec(1000); //聲明大小爲1000的complex <float>的列稀疏向量
SparseVector <double,RowMajor> vec(1000); //聲明大小爲1000的double的行稀疏向量
下面的代碼,演示瞭如何訪問稀疏矩陣和向量的維度:

迭代所有的非零元素

使用coeffRef(i,j)函數可以隨機訪問稀疏對象的元素,但是用到的二叉搜索比較浪費時間。

大多數情況下,我們僅需迭代所有的非零元素。這時,可以先迭代外維度,然後迭代當前內維度向量的非零元素。非零元素的訪問順序和存儲順序相同?

SparseMatrix<double> mat(rows,cols);
for (int k=0; k<mat.outerSize(); ++k)
  for (SparseMatrix<double>::InnerIterator it(mat,k); it; ++it)
  {
    it.value();
    it.row();   // row index
    it.col();   // col index (here it is equal to k)
    it.index(); // inner index, here it is equal to it.row()
  }
SparseVector<double> vec(size);
for (SparseVector<double>::InnerIterator it(vec); it; ++it)
{
  it.value(); // == vec[ it.index() ]
  it.index();
}

For a writable expression, 可以使用valueRef()函數修改參考值。如果稀疏矩陣或向量的類型取決於模板參數,則typename需要關鍵字來指示InnerIterator表示類型;有關詳細信息,請參見C ++中的template和typename關鍵字

填充稀疏矩陣

由於稀疏矩陣的特殊存儲格式,添加新的非零元素時需要特別注意。例如,隨機插入一個非零元素的複雜度是O(nnz),nnz表示非零元素的個數。

最簡單的保證性能的創建稀疏矩陣的方法是,先創建一個三元組列表,然後轉換成SparseMatrix。如下所示:

typedef Eigen::Triplet<double> T;
std::vector<T> tripletList;
tripletList.reserve(estimation_of_entries);
for(...)
{
  // ...
  tripletList.push_back(T(i,j,v_ij));
}
SparseMatrixType mat(rows,cols);
mat.setFromTriplets(tripletList.begin(), tripletList.end());
// mat is ready to go!

std::vector三胞胎的可能包含以任意順序的元素,甚至可能包含將由setFromTriplets來概括重複元素()。見稀疏矩陣:: setFromTriplets()函數和類三聯的更多細節。

The std::vector of triplets 可能包含以任意順序的元素, and might even contain duplicated elements that will be summed up by setFromTriplets().

See the SparseMatrix::setFromTriplets() function and class Triplet for more details.

但是,在某些情況下,直接將非零元素插入到目標矩陣的方法更加高效,節省內存。如下所示:

1: SparseMatrix<double> mat(rows,cols);         // default is column major
2: mat.reserve(VectorXi::Constant(cols,6));
3: for each i,j such that v_ij != 0
4:   mat.insert(i,j) = v_ij;                    // alternative: mat.coeffRef(i,j) += v_ij;
5: mat.makeCompressed();                        // optional

第2行,我們爲每列保留6個非零的空間。在許多情況下,可以很容易地預先知道每列或每行的非零數。

 If it varies significantly for each inner vector, then it is possible to specify a reserve size for each inner vector by providing a vector object with an operator[](int j) returning the reserve size of the j-th inner vector (e.g., via a VectorXi or std::vector<int>).如果只能獲得每個內部向量的非零數目的粗略估計,則強烈建議過高估計而不是相反。

如果省略此行,則第一次插入新元素將爲每個內部向量保留2個元素的空間。

第4行執行排序插入。在此示例中,理想的情況是is not full and contains non-zeros whose inner-indices are smaller than i.In this case, this operation boils down to trivial O(1) operation.

調用insert(i,j)時,元素i j必須不存在,otherwise use the coeffRef(i,j) method that will allow to, e.g., accumulate values. 如果該元素尚不存在,則此方法首先執行二進制搜索,最後調用insert(i,j)。它比insert()更靈活,但代價也更高。

第5行抑制了剩餘的空白空間並將矩陣轉換爲壓縮的列存儲。

支持的操作和函數

由於其特殊的存儲格式,稀疏矩陣無法提供與密集矩陣相同級別的靈活性。

Eigen的稀疏矩陣模塊僅僅實現了密集矩陣API的一個子集。

在下文中,sm表示稀疏矩陣,sv表示稀疏向量,dm表示密集矩陣,而dv表示密集向量。

基本操作

稀疏矩陣支持大多數的逐元素的一元操作符和二元操作符。

sm1.real()   sm1.imag()   -sm1                    0.5*sm1
sm1+sm2      sm1-sm2      sm1.cwiseProduct(sm2)
注意:強制約束條件:它們的存儲順序必須匹配(相同)。
比如,sm4 = sm1 + sm2 + sm3;,sm1,sm2,sm3必須都是行優先或列優先的;sm4沒有要求。
計算$A^T+A$時,必須先將前一項生成一個兼容順序的臨時矩陣,然後再計算。如下所示:
SparseMatrix<double> A, B;
B = SparseMatrix<double>(A.transpose()) + A;
二元操作符支持稀疏矩陣和密集矩陣的混合操作。
sm2 = sm1.cwiseProduct(dm1);
dm2 = sm1 + dm1;
dm2 = dm1-sm1;
爲了提升性能,稀疏矩陣加減法可以分成兩步進行。好處是,完全發揮密集矩陣存儲的高性能特性,僅在稀疏矩陣的非零元素上花費時間。僅如下所示。例如,最好不要這樣dm2 = sm1 + dm1寫,而是這樣寫:
dm2 = dm1;
dm2 + = sm1;

稀疏矩陣也支持轉置,但沒有原地計算的轉置函數transposeInPlace()。

sm1 = sm2.transpose();
sm1 = sm2.adjoint();
However, there is no transposeInPlace() method.

矩陣乘法

Eigen supports various kind of sparse matrix products which are summarize below:

  • 稀疏矩陣和密集矩陣乘法:
dv2 = sm1 * dv1;
dm2 = dm1 * sm1.adjoint();
dm2 = 2. * sm1 * dm1;
  • 對稱稀疏矩陣和密集矩陣乘法:
dm2 = sm1.selfadjointView<>() * dm1;        // if all coefficients of A are stored
dm2 = A.selfadjointView<Upper>() * dm1;     // if only the upper part of A is stored
dm2 = A.selfadjointView<Lower>() * dm1;     // if only the lower part of A is stored

稀疏矩陣間的乘法

dm2 = sm1.selfadjointView<>() * dm1;        // if all coefficients of A are stored
dm2 = A.selfadjointView<Upper>() * dm1;     // if only the upper part of A is stored
dm2 = A.selfadjointView<Lower>() * dm1;     // if only the lower part of A is stored

稀疏矩陣之間的乘法有兩種算法。稀疏矩陣之間的乘法有兩種算法。默認的方法是一種保守的方法,保存可能出現的零。如下所示:

sm3 = (sm1 * sm2).pruned();                  // removes numerical zeros
sm3 = (sm1 * sm2).pruned(ref);               // removes elements much smaller than ref
sm3 = (sm1 * sm2).pruned(ref,epsilon);       // removes elements smaller than ref*epsilon
  • 排列 稀疏矩陣也可以進行排列操作。
PermutationMatrix<Dynamic,Dynamic> P = ...;
sm2 = P * sm1;
sm2 = sm1 * P.inverse();
sm2 = sm1.transpose() * P;

塊操作

  • 關於讀操作,稀疏矩陣支持類似於密集矩陣相同的接口來訪問塊,列,行。
  • 但由於性能的原因,稀疏子矩陣的寫操作非常受限。當前僅列(列)優先稀疏矩陣的連續列(或行)可寫。此外,在編譯時必須知道這些信息。
  • SparseMatrix類的寫操作API如下所示:
SparseMatrix<double,ColMajor> sm1;
sm1.col(j) = ...;
sm1.leftCols(ncols) = ...;
sm1.middleCols(j,ncols) = ...;
sm1.rightCols(ncols) = ...;
 
SparseMatrix<double,RowMajor> sm2;
sm2.row(i) = ...;
sm2.topRows(nrows) = ...;
sm2.middleRows(i,nrows) = ...;
sm2.bottomRows(nrows) = ...;

稀疏矩陣的2個方法SparseMatrixBase::innerVector() 和 SparseMatrixBase::innerVectors(),當稀疏矩陣是列優先存儲方式時,這兩個方法綁定到col/middleCols方法,當稀疏矩陣是行優先存儲方式時,這兩個方法綁定到row/middleRows方法。

三角和selfadjoint

triangularView()函數可以用於解決矩陣的而三角部分,給定右邊密集矩陣求解三角**。

dm2 = sm1.triangularView<Lower>(dm1);
dv2 = sm1.transpose().triangularView<Upper>(dv1);
selfadjointView()函數支持不同的操作。
dm2 = sm1.triangularView<Lower>(dm1);
dv2 = sm1.transpose().triangularView<Upper>(dv1);
  • optimized sparse-dense matrix products:
dm2 = sm1.selfadjointView<>() * dm1;        // if all coefficients of A are stored
dm2 = A.selfadjointView<Upper>() * dm1;     // if only the upper part of A is stored
dm2 = A.selfadjointView<Lower>() * dm1;     // if only the lower part of A is stored
  • 複製三角部分
sm2 = sm1.selfadjointView<Upper>(); 
 // makes a full selfadjoint matrix from the upper triangular part
sm2.selfadjointView<Lower>() = sm1.selfadjointView<Upper>();     
// copies the upper triangular part to the lower triangular part
  • 對稱排列
PermutationMatrix<Dynamic,Dynamic> P = ...;
sm2 = A.selfadjointView<Upper>().twistedBy(P);                                
// compute P S P' from the upper triangular part of A, and make it a full matrix
sm2.selfadjointView<Lower>() = A.selfadjointView<Lower>().twistedBy(P);       
// compute P S P' from the lower triangular part of A, and then only compute the lower part

 

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