Eigen入門之密集矩陣 9 - 別名混亂Aliasing

簡介

別名混亂Aliasing是指在賦值表達式中,一個Eigen對象(矩陣、數組、向量)同時出現在左值和右值表達式中,比如v = v*2; m = m.transpose();;

別名混亂會引起錯誤,從而產生問題,比如m = m.transpose();

這裏將介紹什麼是別名混亂,如何來避免發生錯誤的情況。

aliasing 示例

先給一個示例,從例子中來獲得直觀印象。本示例期望將左上角的2X2的塊覆蓋右下角的塊。

//matrix_aliasing1.cpp
#include <iostream>
#include <Eigen/Dense>

using namespace std;
using namespace Eigen;


int main()
{
    MatrixXi mat(3,3); 
    mat << 1, 2, 3,   4, 5, 6,   7, 8, 9;
    
    cout << "Here is the matrix mat:\n" << mat << endl;
    
    // This assignment shows the aliasing problem
    mat.bottomRightCorner(2,2) = mat.topLeftCorner(2,2);
    
    cout << "After the assignment, mat = \n" << mat << endl;
}

執行的結果如下:

$ g++   -I /usr/local/include/eigen3 matrix_aliasing1.cpp -o matrix_aliasing1
promote:eigen david$ 
promote:eigen david$ ./matrix_aliasing1 
Here is the matrix mat:
1 2 3
4 5 6
7 8 9
After the assignment, mat = 
1 2 3
4 1 2
7 4 1

從結果可以看到,實際的結果並不如我們鎖期望。很明顯,mat.bottomRightCorner(2,2) = mat.topLeftCorner(2,2);的寫法有問題。
係數mat(1,1)在bottomRightCorner(2,2) , topLeftCorner(2,2)都存在,在賦值後,右下角的塊的係數(2,2)將保存爲原mat(1,1)的值5。但實際其值爲1。問題是Eigen使用的是lazy evaluation延遲求值。所以,操作的過程類似:


mat(1,1) = mat(0,0);
mat(1,2) = mat(0,1);
mat(2,1) = mat(1,0);
mat(2,2) = mat(1,1);

於是,mat(2,2) 被賦予的新值最終來自於mat(0,0), mat(1,1) <-- mat(0,0),所以結果不是預期的樣子。

在收縮(shrink)一個矩陣時,更容易有代碼產生別名混亂。就比如:表達式語句 vec = vec.head(n) ; mat = mat.block(i,j,r,c).都產生錯誤。

比較麻煩的是,別名混亂aliasing 在編譯時,編譯器並不能識別處理。然而,Eigen可以在執行時偵測到某些別名混亂。
下面的示例,

// 
Matrix2i a; 
a << 1, 2, 3, 4;
cout << "Here is the matrix a:\n" << a << endl;

a = a.transpose(); // !!! do NOT do this !!!
cout << "and the result of the aliasing effect:\n" << a << endl;

如果編譯運行,執行的效果應該如下(但需要多一點操作,請看下面):

Here is the matrix a:
1 2
3 4
and the result of the aliasing effect:
1 2
2 4

當然,我們知道了這有別名混亂的問題,結果也確實如此。 但Eigen在執行時,使用了運行時斷言,給出了提升消息。如此這樣:

void Eigen::DenseBase<Derived>::checkTransposeAliasing(const OtherDerived&) const 
[with OtherDerived = Eigen::Transpose<Eigen::Matrix<int, 2, 2, 0, 2, 2> >, Derived = Eigen::Matrix<int, 2, 2, 0, 2, 2>]: 
Assertion `(!internal::check_transpose_aliasing_selector<Scalar,internal::blas_traits<Derived>::IsTransposed,OtherDerived>::run(internal::extract_data(derived()), other)) 
&& "aliasing detected during transposition, use transposeInPlace() or evaluate the rhs into a temporary using .eval()"' failed.

提示: 要關閉這樣的斷言,請定義一個宏:EIGEN_NO_DEBUG,並編譯程序。然後你就可以編譯運行,得到這樣的錯誤結果。

解決別名混亂

只有知道了別名混亂的根源,那麼就可以解決問題了。方法就是,對右值就行計算求值,並賦予臨時變量,再爲左值賦值,就可以解決問題了。對右值的求值,使用函數.eval()完成。

Eigen提供了示例:

MatrixXi mat(3,3); 
mat << 1, 2, 3,   4, 5, 6,   7, 8, 9;

cout << "Here is the matrix mat:\n" << mat << endl;

// The eval() solves the aliasing problem
mat.bottomRightCorner(2,2) = mat.topLeftCorner(2,2).eval();

cout << "After the assignment, mat = \n" << mat << endl;

現在,執行可以得到正確的結果:

Here is the matrix mat:
1 2 3
4 5 6
7 8 9
After the assignment, mat = 
1 2 3
4 1 2
7 4 5

上面問題示例中的a = a.transpose();會產生問題,簡單地修改爲a = a.transpose().eval();就可以正確了。但更通用,更好的方式是:使用Eigen提供的專用函數來操作 – Eigen提供了transposeInPlace()函數可以完成這個任務。

MatrixXf a(2,3); a << 1, 2, 3, 4, 5, 6;
cout << "Here is the initial matrix a:\n" << a << endl;

a.transposeInPlace();
cout << "and after being transposed:\n" << a << endl;

執行結果也是正確的:

Here is the initial matrix a:
1 2 3
4 5 6
and after being transposed:
1 4
2 5
3 6

只有一個操作有對應的Eigen函數xxxInPlace(),那就放心地使用,其更高效,更正確。下面列表列出了Eigen提供的對應函數:

原函數 In-place函數
MatrixBase::adjoint() MatrixBase::adjointInPlace()
DenseBase::reverse() DenseBase::reverseInPlace()
LDLT::solve() LDLT::solveInPlace()
LLT::solve() LLT::solveInPlace()
TriangularView::solve() TriangularView::solveInPlace()
DenseBase::transpose() DenseBase::transposeInPlace()

其他的情況:
在上面提到的a = a.head(n),可以使用函數conservativeResize()來執行操作。

別名混亂與面向組件的操作

如上所述,如果矩陣或者數組對象同時在賦值表達式的左右兩邊,這是就可能發生錯誤。但可以通過在右邊顯式調用求值函數eval()來改進。
然而,採樣面向組件的操作,比如矩陣相加,乘以標量,或者數組乘法,這些都是安全的。

下面示例只有面向組件的操作,它們是安全的,所有無需eval()求值的顯式調用。

MatrixXf mat(2,2); 
mat << 1, 2,  4, 7;
cout << "Here is the matrix mat:\n" << mat << endl << endl;

mat = 2 * mat;
cout << "After 'mat = 2 * mat', mat = \n" << mat << endl << endl;

mat = mat - MatrixXf::Identity(2,2);
cout << "After the subtraction, it becomes\n" << mat << endl << endl;

ArrayXXf arr = mat;
arr = arr.square();
cout << "After squaring, it becomes\n" << arr << endl << endl;

// Combining all operations in one statement:
mat << 1, 2,  4, 7;
mat = (2 * mat - MatrixXf::Identity(2,2)).array().square();

cout << "Doing everything at once yields\n" << mat << endl << endl;

執行結果如下:

Here is the matrix mat:
1 2
4 7

After 'mat = 2 * mat', mat = 
 2  4
 8 14

After the subtraction, it becomes
 1  4
 8 13

After squaring, it becomes
  1  16
 64 169

Doing everything at once yields
  1  16
 64 169
 

別名混亂與矩陣乘法

缺省狀態下,在Eigen內,在目標矩陣不調整尺寸大小時,矩陣乘法是唯一一個假定有別名混亂的操作。所以,如果matA是一個方陣,那麼matA = matA * matA;計算就是安全的。所有其它的操作都認爲是安全的,沒有別名混亂的問題,要麼是因爲計算結果賦予了一個不同的矩陣對象,要麼使用了面向組件的操作函數。

如下有簡單示例:

MatrixXf matA(2,2); 
matA << 2, 0,  0, 2;

matA = matA * matA;

cout << matA;

執行結果:

4 0
0 4

然而,這也帶來了一定的代價。比如matA = matA * matA;運算,這裏的矩陣乘法表達式,會賦予一個臨時變量對象,然後計算結果再賦予matA對象。而賦值表達式matB = matA * matA;也會一樣多使用一個臨時變量。這種情況下,更有效率的方式是直接計算乘積,然後賦值給matB對象,而不多用一個臨時變量。

這就需要使用noalias()函數來完成工作,其告知這裏沒有別名混亂。使用方式如此matB.noalias() = matA * matA;

示例:

MatrixXf matA(2,2), matB(2,2); 
matA << 2, 0,  0, 2;

// 這是使用了臨時變量週轉的計算賦值模式。
matB = matA * matA;
cout << matB << endl << endl;

// 這是比較高效的模式。
matB.noalias() = matA * matA;
cout << matB;

執行結果:

4 0
0 4

4 0
0 4

**注意:**從Eigen3.3開始,如果乘法計算時,目標矩陣的尺寸大小會發生變化時,也認爲不會有別名混亂的錯誤。這時,你需要自己處理。

MatrixXf A(2,2), B(3,2);
B << 2, 0,  0, 3, 1, 1;
A << 2, 0, 0, -2;

A = (B * A).cwiseAbs();

cout << A;

這裏執行(3X2)矩陣被(2X2)矩陣乘,執行結果得到(3X2)矩陣。矩陣A從(2X2)–> (3X2):

4 0
0 6
2 2

但這裏依然有錯。正確的實現方法應該如下:

MatrixXf A(2,2), B(3,2);
B << 2, 0,  0, 3, 1, 1;
A << 2, 0, 0, -2;

A = (B * A).eval().cwiseAbs();
cout << A;

注意這裏的A = (B * A).eval().cwiseAbs();,顯式調用了eval()

總結

別名混亂髮生於矩陣或數組對象在賦值操作時同時位於左右兩邊,對同樣的多個係數進行操作了。

  • 但在面向係數計算的別名混亂是無害的,這包括標量乘於矩陣或數組,矩陣或數組的加法。
  • 而在2個矩陣相乘時,Eigen假定會發生別名混亂。如果我們確定這裏並沒有發生別名混亂的錯誤,我們可以使用noalias()
  • 剩下的其他情況下,Eigen假定沒有別名混亂的問題,但如果,此時真實情況是有別名混亂的問題發生的,則會導致錯誤發生。爲防止此情況發生,必須調用eval()或者xxxInPlace()函數。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章