Eigen学习笔记25:稀疏矩阵操作

处理和解决稀疏问题的各种模块,总结如下: 

模组 头文件 内容
SparseCore
#include
SparseMatrix and SparseVector classes, matrix assembly, basic sparse linear algebra (including sparse triangular solvers)
SparseCholesky
#include
Direct sparse LLT and LDLT Cholesky factorization to solve sparse self-adjoint positive definite problems
SparseLU
#include
Sparse LU factorization to 解决通用的square sparse systems
SparseQR
#include
Sparse QR factorization解决稀疏线性最小二乘问题
IterativeLinearSolvers #include

迭代求解器用于求解大规模通用的线性square problems (including self-adjoint positive definite problems)

Sparse #include 包括以上所有模块

稀疏矩阵格式

在许多应用中(例如,有限元方法),通常需要处理非常大的矩阵,这样的矩阵只有几个系数不等于零。在这种情况下,通过使用仅存储非零元素,可以减少内存消耗并提高性能。这样的矩阵称为稀疏矩阵。

稀疏矩阵

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

A 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 
#include 
#include 
 
typedef Eigen::SparseMatrix SpMat; 
typedef Eigen::Triplet T;
 
void buildProblem(std::vector& 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 coefficients;            
  Eigen::VectorXd b(m);                   
  buildProblem(coefficients, b, n);
 
  SpMat A(m,m);
  A.setFromTriplets(coefficients.begin(), coefficients.end());
 
  // Solving:
  Eigen::SimplicialCholesky 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& 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 bits = 
(x*255).cast();
	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

和类型相同的三元组列表Triplet

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 > mat(1000,2000); //声明一个复杂的的1000x2000列主压缩稀疏矩阵
SparseMatrix 垫(1000,2000); //声明一个double的1000x2000行主压缩稀疏矩阵
SparseVector > vec(1000); //声明大小为1000的complex 的列稀疏向量
SparseVector  vec(1000); //声明大小为1000的double的行稀疏向量
下面的代码,演示了如何访问稀疏矩阵和向量的维度:

Eigen学习笔记25:稀疏矩阵操作_第1张图片

迭代所有的非零元素

使用coeffRef(i,j)函数可以随机访问稀疏对象的元素,但是用到的二叉搜索比较浪费时间。

大多数情况下,我们仅需迭代所有的非零元素。这时,可以先迭代外维度,然后迭代当前内维度向量的非零元素。非零元素的访问顺序和存储顺序相同?

SparseMatrix mat(rows,cols);
for (int k=0; k::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 vec(size);
for (SparseVector::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 T;
std::vector 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 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).如果只能获得每个内部向量的非零数目的粗略估计,则强烈建议过高估计而不是相反。

如果省略此行,则第一次插入新元素将为每个内部向量保留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 A, B;
B = SparseMatrix(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() * dm1;     // if only the upper part of A is stored
dm2 = A.selfadjointView() * dm1;     // if only the lower part of A is stored

稀疏矩阵间的乘法

dm2 = sm1.selfadjointView<>() * dm1;        // if all coefficients of A are stored
dm2 = A.selfadjointView() * dm1;     // if only the upper part of A is stored
dm2 = A.selfadjointView() * 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 P = ...;
sm2 = P * sm1;
sm2 = sm1 * P.inverse();
sm2 = sm1.transpose() * P;

块操作

  • 关于读操作,稀疏矩阵支持类似于密集矩阵相同的接口来访问块,列,行。
  • 但由于性能的原因,稀疏子矩阵的写操作非常受限。当前仅列(列)优先稀疏矩阵的连续列(或行)可写。此外,在编译时必须知道这些信息。
  • SparseMatrix类的写操作API如下所示:
SparseMatrix sm1;
sm1.col(j) = ...;
sm1.leftCols(ncols) = ...;
sm1.middleCols(j,ncols) = ...;
sm1.rightCols(ncols) = ...;
 
SparseMatrix 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(dm1);
dv2 = sm1.transpose().triangularView(dv1);
selfadjointView()函数支持不同的操作。
dm2 = sm1.triangularView(dm1);
dv2 = sm1.transpose().triangularView(dv1);
  • optimized sparse-dense matrix products:
dm2 = sm1.selfadjointView<>() * dm1;        // if all coefficients of A are stored
dm2 = A.selfadjointView() * dm1;     // if only the upper part of A is stored
dm2 = A.selfadjointView() * dm1;     // if only the lower part of A is stored
  • 复制三角部分
sm2 = sm1.selfadjointView(); 
 // makes a full selfadjoint matrix from the upper triangular part
sm2.selfadjointView() = sm1.selfadjointView();     
// copies the upper triangular part to the lower triangular part
  • 对称排列
PermutationMatrix P = ...;
sm2 = A.selfadjointView().twistedBy(P);                                
// compute P S P' from the upper triangular part of A, and make it a full matrix
sm2.selfadjointView() = A.selfadjointView().twistedBy(P);       
// compute P S P' from the lower triangular part of A, and then only compute the lower part

 

你可能感兴趣的:(Eigen)