1 使用矩阵作为函数参数介绍

文章来源Writing Functions Taking %Eigen Types as Parameters
Eigen为了在函数中传递不同的类型使用了表达式模板技术。如果你传递一个表达式到函数时使用了Matrix作为参数,你的表达式会被隐含的作为Matrix模板传递给这个函数。这意味着你丢掉了表达式模板所带来的好处,这有如下两个缺点:

  1. 评估进模板的参数可能是无用或者无效的。
  2. 这只允许函数从表达式中读取而无法写入。

幸运的是这些表达式类型都有一个共同特点就是他们都继承于几个通用的、模板化的基类。通过让你的函数使用这些基类作为模板参数,你可以让它们很好的和表达式模板结合。

2 一些简单的例子

这一段将会提供一些简单的例子用来说明Eigen中提供的几种不同类型对象。在开始介绍这些例子之前,我们首先需要概括如何使用这些基类(当然也可以查看官方的手册类的层次)。

  1. MatrixBase: 它是所有稠密矩阵表达式的基类(和array表达式、稀疏矩阵表达式、特殊矩阵表达式相对应)。在函数中使用它作为参数意味着它只能传递稠密矩阵。
  2. ArrayBase: 它是稠密array表达式的基类(和matrix表达式相对应等)。在函数中使用它作为参数意味着它只能传递array.
  3. DenseBase: 它是所有密集表达式的基类,也就是说它包含了MatrixBase和ArrayBase.它可以作为函数参数传递matrices和arrays.
  4. EigenBase: 它是所有矩阵表达式统一的基类,它可以被应用在矩阵或者arrays,比如特殊矩阵(比如对角矩阵、转置矩阵等).它被应用在函数中作为参数意味着所有这种类型的参数都可以被传递。

2.1 EigenBase使用例程

打印Eigen中通用对象的维数。它可能是任何形式的矩阵表达式,任何稠密或稀疏或array的形式。

#include "Eigen/Core"
#include "iostream"

using namespace Eigen;
using namespace std;

template <typename Derived>
void print_size(const EigenBase<Derived>& b)
{
	cout<<"size(rows,cols):"<<b.size()<<" ("<<b.rows()
		<<","<<b.cols()<<")"<<std::endl;
}

int main()
{
	Vector3f v;
	print_size(v);
	//v.asDiagonal()returns a 3x3 diagonal matrix pseudo-expression
	print_size(v.asDiagonal());
}

执行结果如下:

size(rows,cols):3 (3,1)
size(rows,cols):9 (3,3)

2.2 DenseBase使用示例

打印一个稠密表达式的子块,接收任何稠密matrix和array表达式。但是稀疏类型和特殊类型的matrix类(如DiagonalMatrix)不能接收。

#include "Eigen/Core"
#include "iostream"

using namespace Eigen;
using namespace std;

template<typename Derived>
void print_block(const DenseBase<Derived>& b,int x,int y,int r,int c)
{
	cout<<"block:\n"<<b.block(x,y,r,c)<<endl;
}

int main()
{
	Matrix3f v;
	v<<	1,2,3,
		4,5,6,
		7,8,9;
	print_block(v,1,1,2,2);
}

执行结果如下:

block:
5 6
8 9

2.3 ArrayBase使用示例

打印array或其表达式中的最大元素。

#include "Eigen/Core"
#include "iostream"

using namespace Eigen;
using namespace std;

template <typename Derived>
void PrintMaxCoeff(const ArrayBase<Derived>& a)
{
	cout<<"max: "<<a.maxCoeff()<<endl;
}

int main()
{
	Array33f v;
	v<<	1,2,3,
		4,5,6,
		7,8,9;
	PrintMaxCoeff(v);
}

输出结果:

max: 9

2.4 MatrixBase使用示例

打印给定矩阵或矩阵表达式的条件数倒数(inverse condition number).

#include "Eigen/SVD"
#include "iostream"

using namespace Eigen;
using namespace std;

template <typename Derived>
void print_inv_cond(const MatrixBase<Derived>& a)
{
  const typename JacobiSVD<typename Derived::PlainObject>::SingularValuesType&
    sing_vals = a.jacobiSvd().singularValues();
  std::cout << "inv cond: " << sing_vals(sing_vals.size()-1) / sing_vals(0) << std::endl;
}

int main()
{
	Matrix3f m;
	m<<	1,2,3,
		4,5,6,
		7,8,9;
	print_inv_cond(m);
}

执行结果:

inv cond: 1.11994e-008

2.5 多模板参数使用示例

计算两点之间的欧式距离。

#include "Eigen/Core"
#include "iostream"

using namespace Eigen;
using namespace std;

template <typename Derived1,typename Derived2>
typename Derived1::Scalar SquareDist(const MatrixBase<Derived1>& p1,
		const MatrixBase<Derived2>& p2)
{
	return (p1-p2).squaredNorm();
}

int main()
{
	Vector3f p1;
	Vector3f p2;
	p1<<1,2,3;
	p2<<4,5,6;
	
	cout<<"p1和p2之间的欧式距离为: "<<SquareDist(p1,p2)<<endl;
}

执行结果如下:

p1和p2之间的欧式距离为: 27

上面的几个简单的例子给读者留下一个第一印象——如何用一个普通的和不变的Matrix或Array作为函数的参数。也故意给读者留下了一个印象,在函数参数通用基类的最佳选项是什么。在下面一节的例子中我们会着眼于更加细节的地方以及实现它的不同方式,同时讨论各种实现过程中的存在的问题和优势。对于下面的讨论,Matrix和Array以及MatrixBase和ArrayBase可以被交换并且所有的参数仍然成立。

3 如何写一个通用的但是不是模板的函数?

在之前的所有例子中,函数都被写成了模板的形式。这种方法允许我们写一下非常通用的代码,但是如果能写出一种函数不使用模板同时还能保证一定程度的通用性这会是我们更加满意,因为这可以避免很多愚蠢的参数拷贝。这有一个典型的例子来写一个函数能同时接受MatrixXf和MatrixXf的块。这正是Ref类的用途。例程如下:

#include "Eigen/SVD"
#include "iostream"

using namespace Eigen;
using namespace std;

float InvCond(const Ref<const MatrixXf>& a)
{
	const VectorXf sing_vals = a.jacobiSvd().singularValues();
	return sing_vals(sing_vals.size() - 1)/sing_vals(0);
}

int main()
{
	Matrix4f m = Matrix4f::Random();
	cout<<"matrix m:"<<endl<<m<<endl<<endl;
	cout<<"InvCond(m):"<<InvCond(m)<<endl;
	cout<<"InvCond(m(1:3,1:3)): "<<InvCond(m.topLeftCorner(3,3))<<endl;
	cout<<"InvCond(m+1): "<<InvCond(m+Matrix4f::Identity())<<endl;
}

执行结果如下:

matrix m:
 -0.997497   0.170019    0.64568   0.421003
  0.127171 -0.0402539    0.49321  0.0270699
 -0.613392  -0.299417  -0.651784   -0.39201
  0.617481   0.791925   0.717887  -0.970031

InvCond(m):0.150053
InvCond(m(1:3,1:3)): 0.163871
InvCond(m+1): 0.202112

上面的例子中前两次调用InvCond并没有产生类的拷贝,因为参数的存储布局(memory layout) 和 Ref<MatrixXf>的存储布局相匹配。但是,最后一次调用时,我们的表达式会自动的被计算同时将值存入到一个由Ref<>对象决定的MatrixXf临时变量中。
一个Ref对象同时也是可以被写入的。下面这个例子是计算两个矩阵的协方差,其中每一列是一个随机变量,每一行是一组测量数据。

#include "Eigen/SVD"
#include "iostream"

using namespace Eigen;
using namespace std;

void cov(const Ref<const MatrixXf> x,const Ref<const MatrixXf> y,Ref<MatrixXf> C)
{
	const float num_observations = static_cast<float>(x.rows());
	const RowVectorXf x_mean = x.colwise().sum()/num_observations;
	const RowVectorXf y_mean = y.colwise().sum()/num_observations;
	C = (x.rowwise() - x_mean).transpose()*(y.rowwise() - y_mean)/num_observations;
}

int main()
{
	Matrix<float,4,3> m,n;
	MatrixXf conMn;
	m<<	1,2,3,
		4,5,6,
		7,8,9,
		10,11,12;
	n = m;
	conMn.resize(3,3);		//在函数内部不允许修改大小,必须在调用之前给出大小
	cov(m,n,conMn);
	cout<<conMn<<endl;
}

执行结果如下:

11.25 11.25 11.25
11.25 11.25 11.25
11.25 11.25 11.25

当然我们也可以使用矩阵的子块作为参数conv(m.leftCols<3>(),n.leftCols<3>(),conMn.topLeftCorner<3,3>());.
Ref<>类还有其他两个模板参数运行控制存储布局(memory layout)以便接收参数的同时不进行任何拷贝。查看Ref文档以便了解更多细节。

4 什么情况下函数传递一个普通的Matrix或Array参数能工作?

即不使用模板也不使用Ref类,上面例子的一个狭窄实现可能是下面这个样子。

MatrixXf cov(const MatrixXf& x, const MatrixXf& y)
{
  const float num_observations = static_cast<float>(x.rows());
  const RowVectorXf x_mean = x.colwise().sum() / num_observations;
  const RowVectorXf y_mean = y.colwise().sum() / num_observations;
  return (x.rowwise() - x_mean).transpose() * (y.rowwise() - y_mean) / num_observations;
}

和一些人的第一直觉相反,这个实现能很好的工作除非你需要一个通用的实现让double类型的matrix也工作或者除非你不关心模板对象。为什么呢?临时变量放在哪呢?下面这段代码又是怎么运行的呢?

MatrixXf x,y,z;
MatrixXf C = conv(x,y+z);

在这个特殊情况下,这个例子能正常工作是因为所有的参数都声明为const reference.编译器创建一个临时变量并且计算并保存x+z.一旦程序运行完成,临时变量就会被释放掉同时将结果返回给C。
注意:在使用了临时变量的代价下,函数对Matrix(或Array)使用const引用可以运行表达式。

5 什么情况下函数使用普通的Matrix或Array作为参数会失败?

这里我们对上面的例子进行小小的修改,我们不想让函数有返回值而是通过传递一个额外的非const参数来保存结果,一个狭窄的实现可能是下面这个样子:

//Note: This code is flawed!
void cov(const MatrixXf& x, const MatrixXf& y, MatrixXf& C)
{
  const float num_observations = static_cast<float>(x.rows());
  const RowVectorXf x_mean = x.colwise().sum() / num_observations;
  const RowVectorXf y_mean = y.colwise().sum() / num_observations;
  C = (x.rowwise() - x_mean).transpose() * (y.rowwise() - y_mean) / num_observations;
}

接下来我们尝试执行下面的语句:

MatrixXf C = MatrixXf::Zero(3,6);
cov(x,y, C.block(0,0,3,3));

这时会编译失败,因为不可能转换一个非const的MatrixXf&表达式给MatrixXf::block().这是因为编译器为了保护你的程序安全会避免你向一个临时变量写入结果。这里虽然我们很想向::block()里写入返回结果。如何解决这个问题呢?
这里优先选择的一个解决方案需要一点骇客手段。需要传递一个const引用这个matrix然后在内部使用去const的手段去掉const。在C98编译器下一个正确的实现如下所示:

template <typename Derived, typename OtherDerived>
void cov(const MatrixBase<Derived>& x, const MatrixBase<Derived>& y, MatrixBase<OtherDerived> const & C)
{
  typedef typename Derived::Scalar Scalar;
  typedef typename internal::plain_row_type<Derived>::type RowVectorType;
  const Scalar num_observations = static_cast<Scalar>(x.rows());
  const RowVectorType x_mean = x.colwise().sum() / num_observations;
  const RowVectorType y_mean = y.colwise().sum() / num_observations;
  const_cast< MatrixBase<OtherDerived>& >(C) =
    (x.rowwise() - x_mean).transpose() * (y.rowwise() - y_mean) / num_observations;
}

上面这个实现中不但允许向临时表达式里写入数据,同时也允许使用任意浮点类型矩阵作为参数的函数。
注意:去const的骇客手段只能在模板函数中工作,它不能在MatrixXf实现中工作因为不可能转换一个Block表达式到MatrixXf的引用。

6 在一般的实现中如何修改matrices的大小?

到这里我们所有的工作都做完了吗?前面我们例子中了解到在函数调用时不能改变matrix的大小,然后对于一般的应用,我们更习惯使用如下的方式调用前面的协方差函数。

MatrixXf x = MatrixXf::Random(100,3);
MatrixXf y = MatrixXf::Random(100,3);
MatrixXf C;
cov(x,y,C);

上面情况会因为C的维数改变而出现错误,在我们使用implementation作为MatrixBase的参数时。通常,Eigen支持自动的维数转换但不能通过表达式这样做。为什么应该允许改变一个矩阵块的维数?它是一个到子矩阵的引用,我们真的不应该去修改它的维数(试想一下,对于一个2x2的矩阵,如果我们将它子矩阵(1,1)变成2x2那结果还能是矩阵吗?)。那么如何不再MatrixBase的基础上改变维数呢?在这个实现中办法是改变derived object的维数。

#include "Eigen/SVD"
#include "iostream"

using namespace Eigen;
using namespace std;

template <typename Derived,typename OtherDerived>
void cov(const MatrixBase<Derived>& x,const MatrixBase<Derived>& y,
		MatrixBase<OtherDerived> const& C_)
{
	typedef typename Derived::Scalar Scalar;
	typedef typename internal::plain_row_type<Derived>::type RowVectorType;
	
	const Scalar num_observations = static_cast<Scalar>(x.rows());
	
	const RowVectorType x_mean = x.colwise().sum()/num_observations;
	const RowVectorType y_mean = y.colwise().sum()/num_observations;
	
	MatrixBase<OtherDerived>& C = const_cast<MatrixBase<OtherDerived>&>(C_);
	
	C.derived().resize(x.cols(),x.cols());		//resize the derived object
	C = (x.rowwise() - x_mean).transpose()*(y.rowwise() - y_mean)/num_observations;
}
int main()
{
	Matrix<float,4,3> m,n;
	MatrixXf conMn;
	m<<	1,2,3,
		4,5,6,
		7,8,9,
		10,11,12;
	n = m;
	// conMn.resize(3,3);		//在函数内部不允许修改大小,必须在调用之前给出大小
	cov(m,n,conMn);
	cout<<conMn<<endl;
}

执行结果如下:

11.25 11.25 11.25
11.25 11.25 11.25
11.25 11.25 11.25

上面这个实现中不但可以让表达式作为参数,也可以让有错误维数的矩阵作为参数。
上面的讨论对Matrix,Array,MatrixBase,ArrayBase进行交换所有结论依然成立。

7 总结

  1. 实现使用不可写(const referenced)对象作为参数的函数不是一个大问题,同时也不会在编译和运行时引起任何问题。但是,一个狭窄的实现可能引入不必要中间对象。通过模板使用到MatrixBase或ArrayBase(const)引用可以避免上述情况。
  2. 函数使用了一个可写参数必须使用const引用然后在函数体中去除const。
  3. 函数使用了MatrixBase(或ArrayBase)对象,如果有可能改变它们的维数,必须在derived类上调用resize()然后返回derived().

ps: 这篇随便是边看官方手册边实验边记录的,有些地方当时也不太懂,另外限于本人中文表达能力和英文阅读能力都有限,可能有些地方翻译错误,建议英语水平高的同学还是直接看官网手册比较好。

posted on 2017-03-05 19:59  学习时间轴  阅读(3714)  评论(0编辑  收藏  举报