参考
Eigen3 主页,Eigen3 官网教程
矩阵的本质,通过多种矩阵的应用去感受矩阵本质
3Blue1Brown 的线性代数,用可视化方法来表现线性代数的特性,强推
如何理解复数和虚数,有动画方便理解复数的意义
相关文章
前言
本文主要内容基于 Eigen3 官网 3.3.9 版本教程,对相关数学知识和特殊需要注意的知识点进行简单的拓展。需要学习不同版本教程可以在官网主页左侧的 Documentation 选择版本。
本地测试代码的 Eigen3 版本是 3.3.7,环境是 ubuntu20.04。
linux 中可以通过下面指令查看本地 Eigen3 版本:
pkg-config --modversion eigen3
Eigen3 基础数据类型和操作
矩阵的核心是向量和线性代数,而线性代数的核心是向量加法和标量乘法。
Matrix 矩阵类
Matrix 其实是个类模板,有 6 个参数,前 3 个强制参数,后 3 个默认参数。
Eigen::Matrix< Scalar_, Rows_, Cols_, Options_, MaxRows_, MaxCols_ >
-
Scalar_ 数据类型,可以是 double,float,int,或者复数 std::complex<float> 等。
-
Rows_ 矩阵行数,或者动态行数 Dynamic。动态矩阵在编译期间矩阵尺寸可以是未知的。
-
Cols_ 矩阵列数,或者动态列数 Dynamic。动态矩阵在编译期间矩阵尺寸可以是未知的。
-
Options_ 配置,有存储顺序配置 storage order:行优先 RowMajor 和 列优先 ColMajor。默认是列优先存储。另外有对齐配置:自动对齐 AutoAlign 和不对齐 DontAlign。默认是自动对齐。
-
MaxRows_ 最大行数,默认是 Rows_。当 Rows_ 是 Dynamic 时候,用于限制动态矩阵最大行数。
-
MaxCols_ 最大列数,默认是 Cols_。当 Cols_ 是 Dynamic 时候,用于限制动态矩阵最大列数。
存储配置
矩阵有行列,是维数的,但是内存地址是一维连续的。存储顺序就是指矩阵系数在内存上是一行一行存储,还是一列一列存储。
void TestStorageOrder()
{
setlocale(LC_ALL, "zh_CN.UTF-8"); // 支持 ros 打印中文
Eigen::Matrix<int, 3, 3, Eigen::RowMajor> rowmajorm;
rowmajorm << 0, 1, 2,
3, 4, 5,
6, 7, 8;
ROS_INFO_STREAM("矩阵:" << std::endl << rowmajorm);
int* rowmajorm_data = rowmajorm.data(); // 获取矩阵系数数据存储地址
ROS_INFO_STREAM("行优先存储在内存上数据分布:");
for (size_t i = 0; i < rowmajorm.size(); i++) {
ROS_INFO_STREAM("index[" << i << "]:" << rowmajorm_data[i]);
}
Eigen::Matrix<int, 3, 3, Eigen::ColMajor> colmajorm;
colmajorm = rowmajorm;
int* colmajorm_data = colmajorm.data();
ROS_INFO_STREAM("列优先存储在内存上数据分布:");
for (size_t i = 0; i < colmajorm.size(); i++) {
ROS_INFO_STREAM("index[" << i << "]:" << colmajorm_data[i]);
}
}
对齐配置
对齐配置一般都选择 AutoAlign 自动对齐,只有内存受限或者特定硬件情况才需要修改为不对齐。另外 Eigen3 的部分功能要求内存对齐,例如动态矩阵,如果没有对齐会导致崩溃。
void TestAlign()
{
setlocale(LC_ALL, "zh_CN.UTF-8"); // 支持 ros 打印中文
Eigen::Matrix<int, 3, 3, Eigen::AutoAlign> autoalignm;
autoalignm.Random(3, 3);
ROS_INFO_STREAM("自动对齐矩阵:" << std::endl << autoalignm);
Eigen::Matrix<int, 3, 3, Eigen::DontAlign> dontalignm;
dontalignm = autoalignm;
ROS_INFO_STREAM("不对齐矩阵:" << std::endl << dontalignm);
}
固定矩阵和动态矩阵
一般对于小于 16 个系数的矩阵,用固定矩阵效率更高。
对于不确定大小或系数大于 16 的矩阵,用动态矩阵效率更高。
矩阵的系数访问只能通过 () 括号表达式访问,矩阵赋值可以用逗号表达式赋值具体系数,动态矩阵能够自动适应右值矩阵的大小。
void TestDynamic()
{
setlocale(LC_ALL, "zh_CN.UTF-8"); // 支持 ros 打印中文
Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic> dynamicm(2, 3); // 行列初始化
dynamicm.Random(2, 3);
ROS_INFO_STREAM("动态矩阵, 随机赋值:" << std::endl << dynamicm);
dynamicm << 1.2, 2.3, 3.4, // 逗号表达式赋值
4.5, 5.6, 6.7;
ROS_INFO_STREAM("动态矩阵,赋值:" << std::endl << dynamicm);
ROS_INFO_STREAM("动态矩阵,系数访问[0,2]:" << dynamicm(0,2)); // 矩阵系数访问方式
dynamicm(0,2) = 233;
ROS_INFO_STREAM("动态矩阵,系数访问[0,2]:" << dynamicm(0,2));
Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic> dynamicm44(4, 4);
ROS_INFO_STREAM("动态矩阵4x4,初始化:" << std::endl << dynamicm44);
dynamicm44 = dynamicm; // 4x4 矩阵自动适应 2x3 矩阵
ROS_INFO_STREAM("动态矩阵4x4,自适应大小:" << std::endl << dynamicm44);
}
注意,矩阵行列初始化的法并不会默认把系数初始化为 0!
矩阵系数访问不能通过 [] 索引来访问。
调整矩阵大小
resize 只能调整动态矩阵大小,如果大小无变化,那么没有任何操作。如果尺寸发生改变,原有数据会清除,但不是清零!
如果调整大小的时候想要保留原有的数据,请使用 conservativeResize()。
void TestResize()
{
setlocale(LC_ALL, "zh_CN.UTF-8"); // 支持 ros 打印中文
Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic> m3x3(3, 3); // 行列初始化
m3x3.Random(3, 3);
ROS_INFO_STREAM("矩阵3x3, 随机赋值:" << std::endl << m3x3);
m3x3.resize(5, 5);
ROS_INFO_STREAM("调整矩阵大小5x5:" << std::endl << m3x3);
m3x3.conservativeResize(2, 2);
ROS_INFO_STREAM("调整矩阵大小2x2:" << std::endl << m3x3);
}
预定义好的矩阵模类
MatrixNt 格式:N*N 方块矩阵,例如 MatrixXi
是 Matrix<int, Dynamic, Dynamic>
;
MatrixXNt 格式:X*N 行动态列固定的矩阵,例如 MatrixX3i
是 Matrix<int, Dynamic, 3>
;
MatrixNXt 格式:N*X 行固定列动态的矩阵,例如 Matrix4Xd
是 Matrix<double, 4, Dynamic>
;
VectorNt 格式:N 大小的列向量,例如 Vector2f
是 Matrix<float, 2, 1>
;
RowVectorNt 格式:N 大小的行向量,例如 RowVector3d
是 Matrix<double, 1, 3>
;
维数大小 N ∈ {2,3,4,X}。
数据类型 t ∈ {i,f,d,cf,cd}。分别是 int ,float,double,complex<float>(复数),complex<double>(复数)。
void TestDefaultTemplate()
{
setlocale(LC_ALL, "zh_CN.UTF-8"); // 支持 ros 打印中文
// Eigen::Matrix<int, Eigen::Dynamic, Eigen::Dynamic>
Eigen::MatrixXi imx(3, 3);
ROS_INFO_STREAM("int 动态矩阵 3x3:" << std::endl << imx);
// Eigen::Matrix<float, 2, 2>
Eigen::Matrix2f fm2x2;
ROS_INFO_STREAM("float 固定矩阵 2x2:" << std::endl << fm2x2);
// Eigen::Matrix<double, Eigen::Dynamic, 2>
Eigen::MatrixX2d dmx2(3, 2);
ROS_INFO_STREAM("double 动态行,2列矩阵 3x2:" << std::endl << dmx2);
// Eigen::Matrix<double, 1, Eigen::Dynamic>
Eigen::Matrix2Xd dm2x(2, 3);
ROS_INFO_STREAM("double 2行,动态列矩阵 2x3:" << std::endl << dm2x);
}
矩阵运算
-
矩阵加减运算要求操作数是相同大小的矩阵。
二元运算:a+b
,a-b
;一元运算:-a
;复合运算:a+=b
,a-=b
矩阵可以看做是多组向量,矩阵加法可以看做是多组向量分别计算。
矩阵 A 看做是向量u [1, 2] 和向量v [-1, 2],矩阵 B 看做是向量w [2, 0] 和向量 a [-2, -2],A+B 看做是 u+w 和 v+a,分别得到向量 b [3, 2] 和向量 c [-3, 0]。如下图:
-
标量乘除运算。
二元运算:矩阵m*标量x
,标量x*矩阵m
,矩阵m/标量x
;复合运算:矩阵m *= 标量x
,矩阵m /= 标量x
矩阵的标量乘法可以看做是多组向量的标量乘法计算。
矩阵 A 看做是向量d [3, 1] 和 e [-2, 1],1.5*A 看做 d 和 e 分别乘 1.5 得到向量f [4.5, 1.5] 和向量g [-3, 1.5]。如下图:
void TestBaseOperation()
{
setlocale(LC_ALL, "zh_CN.UTF-8"); // 支持 ros 打印中文
Eigen::Matrix3i m0;
m0 << 2, 1, 3,
6, 8, 0,
0, 9, 12;
ROS_INFO_STREAM("3x3 m0:" << std::endl << m0);
Eigen::Matrix3i m1;
m1 << 6, 1, 13,
2, 0, 4,
9, 11, 7;
ROS_INFO_STREAM("3x3 m1:" << std::endl << m1);
ROS_INFO_STREAM("m0+m1=" << std::endl << m0+m1);
ROS_INFO_STREAM("3*m0=" << std::endl << 3*m0);
ROS_INFO_STREAM("m1/2=" << std::endl << m1/2);
}
表达式模板
运算并不会在 + - * / 时候计算,而是在最后返回赋值的时候。使用 eigen 进行复杂计算时候会执行更多优化操作。
VectorXf a(50), b(50), c(50), d(50);
a = 3*b + 4*c + 5*d;
实际计算效果如下:
for(int i = 0; i < 50; ++i)
a[i] = 3*b[i] + 4*c[i] + 5*d[i];
-
转置
矩阵转置是一个将矩阵的行和列互换的操作。如果原始矩阵是 A,其转置矩阵通常表示为A^T。
转置操作 transpose()
并不会实际执行,而是返回一个代理对象。在写入转置结果的才会计算转置的结果:
void TestTranspose()
{
setlocale(LC_ALL, "zh_CN.UTF-8"); // 支持 ros 打印中文
Eigen::Matrix3i m;
m << 2, 1, 3,
6, 8, 0,
0, 9, 3;
ROS_INFO_STREAM("3x3 m:" << std::endl << m);
Eigen::Matrix3i tm = m.transpose();
ROS_INFO_STREAM("after transpose, tm:" << std::endl << tm);
m.transposeInPlace();
ROS_INFO_STREAM("m.transposeInPlace():" << std::endl << m);
}
不要执行m=m.transpose()
操作!
-
共轭,转置共轭
矩阵共轭(Conjugate of a matrix)通常指的是复数矩阵中的元素被各自的共轭复数所替换。对于一个复数矩阵 A,其共轭矩阵 A^* 是通过将 A 中的每个元素替换为其共轭复数。
复数用 a + bi
表达,其中 a
是实部,b
是虚部,i
是虚数单位,定义为 i^2=-1 。
复数的出现完善数系,弥补了一些计算的空白。例如:
,
等等。
复数使得我们能够观察到系统在虚数部分的运作,进而推断实数部分的结果。
打个比如,一个系统仅存在一维数据(这个系统的实部只有1维信息)。我们很难直接观察并预测系统在实数部分的运行规律。如下图。
![](https://i-blog.csdnimg.cn/direct/f8854be725624294bdae8f49cee2a3b9.png)
但如果计算扩展出一个新的维度,得到第二维的信息,如下图。我们能够有更充分的信息去分析,预测系统。
![](https://i-blog.csdnimg.cn/direct/3f250e7c2c2346a09870e5c9dddb4106.png)
共轭运算 conjugate()
,转置共轭运算 adjoint()
。
void TestAdjoint()
{
setlocale(LC_ALL, "zh_CN.UTF-8"); // 支持 ros 打印中文
Eigen::Matrix<std::complex<float>, 2, 2> m;
m << std::complex<float>(3, 2), std::complex<float>(5, 1),
std::complex<float>(7, 3), std::complex<float>(4, 2);
ROS_INFO_STREAM("2x2 complex m:" << std::endl << m);
Eigen::Matrix<std::complex<float>, 2, 2> cm = m.conjugate();
ROS_INFO_STREAM("after conjugate, cm:" << std::endl << cm);
Eigen::Matrix<std::complex<float>, 2, 2> am = m.adjoint();
ROS_INFO_STREAM("after adjoint, am:" << std::endl << am);
m.adjointInPlace();
ROS_INFO_STREAM("m.adjointInPlace(), m:" << std::endl << m);
}
同样不要执行m=m.adjoint()
的操作。
-
矩阵乘法
只有当第一个矩阵的列数与第二个矩阵的行数相等时,这两个矩阵才能相乘。
二元运算:a*b
;复合运算:a*=b
矩阵乘法是一种变换,A*B=C表示矩阵B经过变换矩阵A后,得到矩阵C。
从几何上看,矩阵乘法可以看做是矩阵 B 的向量组,在以变换矩阵 A 向量组作为基向量的坐标系中的表示。这么说比较绕,看看下面的图解。
矩阵 B 可以看做向量 u [1, 1] 和向量 v [-2, -1]
变换矩阵 A 可以看做由向量 i [2, -1]和向量 j [-1, 3] 构成的基向量坐标系
新的基向量坐标系用红蓝虚线表示:
以 i 和 j 作为基向量,矩阵 B 的向量组表示为:向量 uu [1, 1] 和向量 vv [-2, -1],下图紫色向量 :
而向量 uu 和 vv 用标准基向量表示则是 [1, 2] 和 [-3, -1],这正是矩阵 B 向量组经过变换矩阵 A 变换后的向量组。