这一部分主要是最后的Problem比较重要。
这个类使用户可以在使用封装好的代价函数的同时,对残差值加入一定的条件限制。举个例子,现在已经有一个代价函数可以产生N个值,但是用户希望的总代价,不是这N个值的简单的平方和。比如对某个特定残差值项赋予一定的系数来改变其在总残差值中的权重。具体代码如下:
// my_cost_function 生成N个残差值。
CostFunction* my_cost_function = ...
CHECK_EQ(N, my_cost_function->num_residuals());
vector conditioners;
// Make N 1x1 cost functions (1 parameter, 1 residual)
// 将其构造成N个1x1的代价函数。
CostFunction* f_1 = ...
conditioners.push_back(f_1);
CostFunction* f_N = ...
conditioners.push_back(f_N);
ConditionedCostFunction* ccf =
new ConditionedCostFunction(my_cost_function, conditioners);
现在ccf
中的residual[i]
(i=0…N-1)会被传递到对应的第i个conditioner中。
ccf_residual[i] = f_i(my_cost_function_residual[i])
然后雅可比矩阵的计算相会相应的被调整。
没看明白条件(权重)到底如何给定。
该类将代价函数返回的雅可比矩阵与使用有限差分估计的导数进行比较。 它的作用是是作为单元测试的工具,给你比求解器选项中的check_gradients
更细致的控制选项。强制执行的条件是:
// my_cost_function takes two parameter blocks. The first has a local
// parameterization associated with it.
CostFunction* my_cost_function = ...
LocalParameterization* my_parameterization = ...
NumericDiffOptions numeric_diff_options;
std::vector local_parameterizations;
local_parameterizations.push_back(my_parameterization);
local_parameterizations.push_back(NULL);
std::vector parameter1;
std::vector parameter2;
// Fill parameter 1 & 2 with test data...
std::vector<double*> parameter_blocks;
parameter_blocks.push_back(parameter1.data());
parameter_blocks.push_back(parameter2.data());
GradientChecker gradient_checker(my_cost_function,
local_parameterizations, numeric_diff_options);
GradientCheckResults results;
if (!gradient_checker.Probe(parameter_blocks.data(), 1e-9, &results) {
LOG(ERROR) << "An error has occurred:\n" << results.error_log;
}
class NormalPrior: public CostFunction {
public:
// Check that the number of rows in the vector b are the same as the
// number of columns in the matrix A, crash otherwise.
NormalPrior(const Matrix& A, const Vector& b);
virtual bool Evaluate(double const* const* parameters,
double* residuals,
double** jacobians) const;
};
这个类实现了一个如下形式的代价函数:
A matrix is said to have full rank if its rank is either equal to its number of columns or to its number of rows (or to both). A matrix that does not have full rank is said to be rank deficient.
一个矩阵的秩等于行数或者列数,那么这个矩阵是满秩矩阵,否则就称这个矩阵亏秩。
对于最小二乘问题有时候会遇到一些异常的测量值,这时候应用损失函数来减小这些异常值的影响就显得十分重要了。
下面考虑一个运动问题的求解。未知数是三维空间点和相机参数,测量值是空间点再相机成像平面上重投影的坐标。例如,我们希望用参数未知的移动的相机来观察并模拟大街上消防栓和汽车的几何形状。我们关心的唯一点是消防栓的顶尖尖端。图像处理算法可以侦测到几乎每一帧图像中的消防栓的尖端(除了某一帧将汽车的车尾灯当成了消防栓的尖端),并且将测量结果输入到Ceres。如果我们不把那个错误识别结果提出,那么错误的值将使最终结果大大偏离最优解,从而引起较大误差。
一个好的损失函数可以减小异常残值对总代价的影响力,再上面的例子中,它仅仅使异常外围点的影响力减小,不会对最终结果产生过度影响。
class LossFunction {
public:
virtual void Evaluate(double s, double out[3]) const = 0;
};
损失函数的关键部分是LossFunction::Evaluate()
方法,
so that they mimic the squared cost for small residuals.最后这一段没看懂。
缩放
略
Ceres包含了大量已经定义好的损失函数。为了简化我们只介绍他们的非放缩的版本(unscaled versions)。下图图形化的描绘了他们的形状。
class TrivialLoss
class HuberLoss
class SoftLOneLoss
class CauchyLoss
class ArctanLoss
class TolerantLoss
class ComposedLoss
给定两个损失函数f
和g
。那么最终的损失函数是h(s) = f(g(s))
。
class ComposedLoss : public LossFunction {
public:
explicit ComposedLoss(const LossFunction* f,
Ownership ownership_f,
const LossFunction* g,
Ownership ownership_g);
};
class ScaledLosss
有时候你可能希望对强化器(robustifier,貌似是指的损失函数)的输出进行放缩。比如,你可能想要对不同的误差赋予不同的权重。这里我们给定一个损失函数 ρ(s) ρ ( s ) 和一个放缩参数 a a ,ScaledLoss
实现了函数a \rho(s)
。
因为我们把Null
损失函数视为恒等损失函数, ρ= ρ = `Null`是一个有效的输入值,并且会将输入放缩 a a 倍后输出。这个函数为放缩残差块提供了一个很简单的方法。
class LossFunctionWrapper
有时在优化问题建立之后,我们希望改变损失函数的规模。 例如对具有异常值的数据进行计算的时候,通过大规模的开始-优化问题-然后减小规模的方式可以提高收敛性。这样可以比仅使用具有小规模的损失函数获得更好的收敛过程。
这个模板化的类允许用户实现一个优化问题后规模可变的损失函数,
Problem problem;
// Add parameter blocks
CostFunction* cost_function =
new AutoDiffCostFunction < UW_Camera_Mapper, 2, 9, 3>(
new UW_Camera_Mapper(feature_x, feature_y));
LossFunctionWrapper* loss_function(new HuberLoss(1.0), TAKE_OWNERSHIP);
problem.AddResidualBlock(cost_function, loss_function, parameters);
Solver::Options options;
Solver::Summary summary;
Solve(options, &problem, &summary);
loss_function->Reset(new HuberLoss(1.0), TAKE_OWNERSHIP);
Solve(options, &problem, &summary);
下面让我们考虑一个最简单情况。
its possible to re-weight the residual and the Jacobian matrix such that the corresponding linear least squares problem for the robustified Gauss-Newton step.这句翻译的不好。
设 α α 是下列方程的根:
更多相关内容可以参考B. Triggs, P. F. Mclauchlan, R. I. Hartley & A. W. Fitzgibbon, Bundle Adjustment: A Modern Synthesis, Proceedings of the International Workshop on Vision Algorithms: Theory and Practice, pp. 298-372, 1999.
通过这一简单的放缩,人们就可以对强化后的非线性最小二乘法问题使用任何基于非线性最小二乘算法的雅可比行列式了。
这一部分看不太明白。
class LocalParameterization {
public:
virtual ~LocalParameterization() {}
virtual bool Plus(const double* x,
const double* delta,
double* x_plus_delta) const = 0;
virtual bool ComputeJacobian(const double* x, double* jacobian) const = 0;
virtual bool MultiplyByJacobian(const double* x,
const int num_rows,
const double* global_matrix,
double* local_matrix) const;
virtual int GlobalSize() const = 0;
virtual int LocalSize() const = 0;
};
这一段看不懂,不理解,翻译了也没意义。原文如下:
Sometimes the parameters x can overparameterize a problem. In that case it is desirable to choose a parameterization to remove the null directions of the cost. More generally, if x lies on a manifold of a smaller dimension than the ambient space that it is embedded in, then it is numerically and computationally more effective to optimize it using a parameterization that lives in the tangent space of that manifold at each point.
For example, a sphere in three dimensions is a two dimensional manifold, embedded in a three dimensional space. At each point on the sphere, the plane tangent to it defines a two dimensional tangent space. For a cost function defined on this sphere, given a point x, moving in the direction normal to the sphere at that point is not useful. Thus a better way to parameterize a point on a sphere is to optimize over two dimensional vector Δx in the tangent space at the point on the sphere point and then “move” to the point x+Δx, where the move operation involves projecting back onto the sphere. Doing so removes a redundant dimension from the optimization, making it numerically more robust and efficient.
More generally we can define a function
TODO
Problem
保持了非线性最小二乘问题的强化的边界。要创建最小二乘问题,可以使用Problem::AddResidualBlock()
和Problem::AddParameterBlock()
。例如,下面这个Problem包含了三个参数块,维度分别为3,4,5。同时有两个残差块,维度分别是2和6。
double x1[] = { 1.0, 2.0, 3.0 };
double x2[] = { 1.0, 2.0, 3.0, 5.0 };
double x3[] = { 1.0, 2.0, 3.0, 6.0, 7.0 };
Problem problem;
problem.AddResidualBlock(new MyUnaryCostFunction(...), x1);
problem.AddResidualBlock(new MyBinaryCostFunction(...), x2, x3);
Problem::AddResidualBlock()
,顾名思义,就是把残差块加入到Problem当中。它添加了一个CostFunction
,一个LossFunction
(非必要)并且把CostFunction
链接到一系列的参数块上。
代价函数包含了关于期望的参数块大小的信息。该函数检查他们是否与parameter_blocks
一致。如果不匹配,程序将中止。loss_function
可以是Null
,这种情况下这一项的代价就是残差的平方。
用户可以选择使用Problem::AddParameterBlock)
显式添加参数块。 这会调用额外的正确性检查。然而,Problem :: AddResidualBlock()
其实也可以隐含地添加参数块(如果它们不存在),因此显式调用Problem::AddParameterBlock()
并非必要。作为选项,用户可以把LocalParameterization
对象和参数块连接起来。重复调用的相同参数会被忽略。重复地用相同的双精度类型指针但用不同的大小的结果会导致未定义的行为。
Repeated calls with the same double pointer but a different size results in undefined behavior.
用户可以使用Problem::SetParameterBlockConstant()
把任何参数块设为常数,并且使用SetParameterBlockVariable()
来撤销这一操作。
实际上,您可以将任意数量的参数块设置为常量。并且Ceres足够聪明,可以根据可自由更改的参数块来确定您构建的问题的哪一部分取决于参数块,并且把时间用于解决它。 例如,如果您构造了一个有100万个参数块和200万个残余块的问题,但是将除了一个参数块之外的所有参数块都设为常量,并且只有10个残余块依赖于这个非常量参数块。如果你之前已经定义了另一个参数块和10个残留块的问题, 那么,Ceres花在解决这两个问题上的时间相同。
所有权Ownership
Problem
默认掌握cost_function
,loss_function
和local_parameterization
指针的所有权。这些对象在Problem
的整个生命周期都保持活动状态。如果用户希望控制这些对象的销毁行为,那么他们可以通过在Problem :: Options
中设置相应的选项来实现。
注意,虽然Problem
掌握这cost_function
和loss_function
的所有权。它不排除用户在另一个残留块中重新使用它们。析构函数只管销毁一次每个cost_function
或loss_function
指针,而不管有多少个残留块仍然引用它们。
ResidualBlockId Problem::AddResidualBlock(CostFunction *cost_function, LossFunction *loss_function, const vector parameter_blocks)
ResidualBlockId Problem::AddResidualBlock(CostFunction *cost_function, LossFunction *loss_function, double *x0, double *x1, ...)
为整个代价函数添加一个残差块。代价函数包含了关于期望的参数块大小的信息。该函数检查他们是否与parameter_blocks
一致。如果不匹配,程序将中止。loss_function
可以是Null
,这种情况下这一项的代价就是残差的平方。参数块有可能整体的以vector
的形式传递,或者分开用double *
指针传递。
用户可以选择使用Problem :: AddParameterBlock()
显式添加参数块。 这会调用额外的正确性检查。然而,Problem :: AddResidualBlock()
其实也可以隐含地添加参数块(如果它们不存在),因此显式调用Problem :: AddParameterBlock()
并非必要。作为选项,用户可以把LocalParameterization
对象和参数块连接起来。这样可以避免重复调用相同参数。重复地用相同的双精度类型指针但用不同的大小的结果会导致未定义的行为。
注意,虽然Problem
掌握这cost_function
和loss_function
的所有权。它不排除用户在另一个残留块中重新使用它们。析构函数只管销毁一次每个cost_function
或loss_function
指针,而不管有多少个残留块仍然引用它们。
这个函数非常重要也非常基础,在前面介绍过,所以很多内容是重复的。
用法案例:
double x1[] = {1.0, 2.0, 3.0};
double x2[] = {1.0, 2.0, 5.0, 6.0};
double x3[] = {3.0, 6.0, 2.0, 5.0, 1.0};
vector v1;
v1.push_back(x1);
vector v2;
v2.push_back(x2);
v2.push_back(x1);
Problem problem;
problem.AddResidualBlock(new MyUnaryCostFunction(...), NULL, x1);
problem.AddResidualBlock(new MyBinaryCostFunction(...), NULL, x2, x1);
problem.AddResidualBlock(new MyUnaryCostFunction(...), NULL, v1);
problem.AddResidualBlock(new MyBinaryCostFunction(...), NULL, v2);
void Problem::AddParameterBlock(double *values, int size, LocalParameterization *local_parameterization)
void Problem::AddParameterBlock(double *values, int size)
把参数块及其各个维度添加到Problem当中。重复调用的相同参数会被忽略。重复地用相同的双精度类型指针但用不同的大小的结果会导致未定义的行为。
void Problem::RemoveResidualBlock(ResidualBlockId residual_block)
把一个残差块从问题中移除。残差块所依赖的参数不会被移除。针对这个残差块的代价函数和损失函数不会立即被移除,但是直到Problem本身被删除不会运行。如果Problem::Options::enable_fast_removal
是True
,那么移除非常迅速。否则,移除一个残差块将引起对整个问题的完整扫描,以便确认该残差块有意义。
注意:移除残差或参数块将会破坏隐式排序,从解算器返回的雅可比矩阵或残差可能变得不可解读。 如果你的运算依赖于雅可比矩阵,不要使用移除残差块! 这可能会在未来的版本中发生变化。
void Problem::RemoveParameterBlock(double *values)
把一个参数块从问题中移除。参数块的参数化(如果有的话)会保存到整个问题的删除。依赖于这些参数的残差块也会被移除,如同RemoveResidualBlock()
一样。如果Problem::Options::enable_fast_removal
是True
,那么移除非常迅速。否则,移除一个残差块将引起对整个问题的完整扫描,以便确认该残差块有意义。
注意:移除残差或参数块将会破坏隐式排序,从解算器返回的雅可比矩阵或残差可能变得不可解读。 如果你的运算依赖于雅可比矩阵,不要使用移除残差块! 这可能会在未来的版本中发生变化。
void Problem::SetParameterBlockConstant(double *values)
令指定的参数块在整个优化计算过程中保持常数。
void Problem::SetParameterBlockVariable(double *values)
允许指定的参数块在优化计算过程中发生改变。
void Problem::SetParameterization(double *values, LocalParameterization *local_parameterization)
为其中一个参数块设置本地参数设置。默认Problem拥有local_parameterization的所有权。可以为多个参数设置相同的参数; 析构函数注意只删除一次本地参数化。 本地参数设置每个参数只能设置一次,一旦设置就不能更改。
LocalParameterization *Problem::GetParameterization(double *values) const
获取与此参数块关联的本地参数化对象。 如果没有关联的参数化对象,则返回NULL
void Problem::SetParameterLowerBound(double *values, int index, double lower_bound)
为参数块*values
中位置为index
的参数设置下限。 默认情况下下限是 −∞ − ∞ 。
void Problem::SetParameterUpperBound(double *values, int index, double upper_bound)
为参数块*values
中位置为index
的参数设置上限。 默认情况下上限是 ∞ ∞ 。
int Problem::NumParameterBlocks() const
返回Problem中参数块的个数。永远等于parameter_blocks().size()
和parameter_block_sizes().size()
。
int Problem::NumParameters() const
返回参数向量的大小,包含所有参数块内参数个数的总和。
int Problem::NumResidualBlocks() const
返回Problem中残差块的数量,永远等于residual_blocks().size()
。
iint Problem::NumResiduals() const
返回残差向量的大小,包含所有残差块内残差个数的总和。
int Problem::ParameterBlockSize(const double *values) const
返回某参数块*values
的大小,即其内参数个数。
int Problem::ParameterBlockLocalSize(const double *values) const
返回某本地参数化的参数块*values
的大小,即其内参数个数。如果这个参数块没有相关的本地参数化,那么ParameterBlockLocalSize
=ParameterBlockSize
。
bool Problem::HasParameterBlock(const double *values) const
返回某参数块*values
是否属于某Problem。
void Problem::GetParameterBlocks(vector *parameter_blocks) const
把目前属于某Problem的参数块指针赋给函数参数*parameter_blocks
。执行之后parameter_block.size()
= NumParameterBlocks
void Problem::GetResidualBlocks(vector *residual_blocks) const
把目前属于某Problem的残差块指针赋给函数参数*residual_blocks
。执行之后residual_blocks.size()
= NumResidualBlocks
void Problem::GetParameterBlocksForResidualBlock(const ResidualBlockId residual_block, vector *parameter_blocks) const
获得给定残差块依赖的所有参数块。
void Problem::GetResidualBlocksForParameterBlock(const double *values, vector *residual_blocks) const
获得所有依赖于给定参数块的残差块。
const CostFunction *GetCostFunctionForResidualBlock(const ResidualBlockId residual_block) const
获得给定残差块的代价函数CostFunction
。
const LossFunction *GetLossFunctionForResidualBlock(const ResidualBlockId residual_block) const
获得给定残差块的损失函数LossFunction
。
bool Problem::Evaluate(const Problem::EvaluateOptions &options, double *cost, vector *residuals, vector *gradient, CRSMatrix *jacobian)
评估计算这个Problem。任何的输出都可能是Null
。残差块和参数块受到Problem::EvaluateOptions
的控制。
注意:
评估计算将会使用创建这个Problem时的参数块指针所指向的存储地址的值。例如:
Problem problem;
double x = 1;
problem.Add(new MyCostFunction, NULL, &x);
double cost = 0.0;
problem.Evaluate(Problem::EvaluateOptions(), &cost, NULL, NULL, NULL);
代价计算时使用x = 1
。如果你希望计算时x = 2
,那么:
x = 2;
problem.Evaluate(Problem::EvaluateOptions(), &cost, NULL, NULL, NULL);
我的理解就是参数块的赋值,要在调用Evaluate函数之前完成。
注意2:
如果没有进行本地参数化,那么梯度向量的大小就是所有参数块的大小的总和。如果有本地参数化,那么将会把LocakSize
设给梯度向量。
注意3:
这个函数不能在问题计算过程中调用。
class Problem::EvaluateOptions
用于控制Problem::Evaluate()
的选项结构。
vector
参与计算的参数块的集合。这个向量决定了参数块在梯度向量或者雅可比矩阵(列方向)中出现的顺序。如果为空,那么则默认其包含所有参数块。一般来说,参数块的排序取决于他们被add到Problem中的顺序以及用户是否认为移除了某些参数块。
注意:这个向量不应该指向除了已经加入Problem的参数块外任何新的位置,否则可能会有不好的后果。
vector
参与计算的残差块的集合。这个向量决定了残差块的计算次序,以及雅可比矩阵(行方向)中出现的顺序。如果为空,那么则默认其包含所有残差块。
bool Problem::EvaluateOptions::apply_loss_function
false
,那么即使残差块中包含了损失函数,那么损失函数也不会发挥作用,即直接输出代价函数。这个选项主要用于分析解的质量。int Problem::EvaluateOptions::num_threads
这一部分主要介绍了一系列Ceres提供的用于计算空间旋转的模板函数,可以应用到自动微分法。这一块我没有用到,所以就省略了。
在很多优化问题中涉及到表格形式的函数,比如在图片中。评估这类方程及其导数需要进行插值。虽然已经有很多库实现了差值运算。但是它们相对于Ceres自动微分法的兼容性很差,往往使用起来十分困难。所以Ceres提供了一维和二维的插值算法来解决这一问题。具体的可以参考官方指南这一部分的介绍以及Pascal Getreuer.的文章Linear Methods for Image Interpolation