Ceres Solvel库函数的解读
关于微分计算(On Derivatives)
与所有基于梯度的优化算法一样,Ceres Solver也是基于评估域中任意点的目标函数及其导数。事实上,Ceres的核心就是确定目标函数机器雅可比行列式。雅可比行列式求解的正确性和效率是评判算法优劣的关键指标。用户可以灵活的从一下三种微分算法中选择:
- 解析微分算法(Analytic Derivatives):用户自己手动或者借助Maple或者Mathematica等工具求解导数,然后写到CostFunction里面。
- 数值微分算法(Numeric Derivatives):Ceres用有限差分数值计算导数
- 自动微分算法(Automatic Dericatives):Ceres用C++模板和操作符重载自动分析计算微分。
应该使用这三种方法中的哪一种(单独或组合)取决于用户愿意做出的情况和权衡。官方给出了一个简单粗暴的建议:
优先选用自动微分算法,某些情况可能需要用到解析微分算法,尽量避免数值微分算法。
Spivak标记
为了简化阅读和推理,引入Spivak标记。
对于单自变量函数$f$,$f(a)$表示它在$a$处的函数值。$Df$表示它的一阶导数,那么$Df(a)$就是函数在$a$处的一阶导数。即,
$$Df(a) = \frac{d}{dx}f(x)|_{x=a}$$
$D^kf$表示$f$的第$k$阶导数。
对于双自变量函数$g(x,y)$,$D_1g$和$D_2g$分别表示关于$g$的两个偏微分。即,
$$D_1g = \frac{\partial}{\partial x}g(x,y)andD_2g = \frac{\partial}{\partial y}g(x,y)$$
$D_g$表示$g$的雅可比矩阵
雅可比矩阵即
$$
\begin{bmatrix}
\frac{\partial y_1}{\partial x_1}& \cdots & \frac{\partial y_1}{\partial x_n}\
\vdots & \ddots & \vdots\
\frac{\partial y_m}{\partial x_1}& \cdots & \frac{\partial y_m}{\partial x_n}
\end{bmatrix}
$$
这里的$y_1$即$g$,其他的行不存在,因为只存在一个因变量$g$。即雅可比矩阵是一个一行两列的矩阵。即,
$$Dg = \begin{bmatrix}
D_1g& D_2g
\end{bmatrix}$$
更一般的,如果$g:\mathbb{R}^{n} \longrightarrow \mathbb{R}^{m}$,那么$Dg$表示的就是一个$m \times n$的雅可比矩阵。
自动微分法(Automatic Derivatices)
我们来思考一个相对复杂的曲线拟合问题。待确定参数方程如下:
$$ y = \frac{b_1}{(1+e^{b_2 - b_3x})^{1/b_4}}$$
现在给定一系列的对应数据点$\left{x_i,y_i\right },\forall i=1,\cdots,n$。我们面临的问题就是求解$b_1,b_2,b_3,b_4$使下列表达式的取值最小:
$$ \begin{aligned}
E\left(b_{1}, b_{2}, b_{3}, b_{4}\right) &=\sum_{i} f^{2}\left(b_{1}, b_{2}, b_{3}, b_{4} ; x_{i}, y_{i}\right) \
&=\sum_{i}\left(\frac{b_{1}}{\left(1+e^{b_{2}-b_3x_i}\right)^{1 / b_{4}}}-y_{i}\right)^{2}
\end{aligned}$$
下面我们使用自动微分算法,它是一种可以快速计算精确导数的算法。下面的代码片段实现了上述问题的CostFunction。
struct Rat43CostFunctor {
Rat43CostFunctor(const double x, const double y) : x_(x), y_(y) {}
template <typename T>
bool operator()(const T* parameters, T* residuals) const {
const T b1 = parameters[0];
const T b2 = parameters[1];
const T b3 = parameters[2];
const T b4 = parameters[3];
residuals[0] = b1 * pow(1.0 + exp(b2 - b3 * x_), -1.0 / b4) - y_;
return true;
}
private:
const double x_;
const double y_;
};
CostFunction* cost_function =
new AutoDiffCostFunction<Rat43CostFunctor, 1, 4>(
new Rat43CostFunctor(x, y));
接下来我们对自动微分算法的原理进行研究。为了研究其工作原理,必须要学习 二元数(Dual number) 和 **射流(Jet)**。
二元数和射流
二元数(Dual number) 是实数的一个延伸,类似于复数。复数则通过引入虚数来增加实数,比如$i$,二元数引入了一个极小二元数单位,比如$\epsilon$,且$\epsilon^2 = 0$(平方后太小可以忽略)。一个二元数$a+\upsilon\epsilon$包含两个分量,实分量$a$和极小分量$\upsilon$。令人惊喜的是,这个简单的变化带来了一种方便的计算精确导数的方法,而不需要考虑复杂的符号表达式。
例如,考虑函数
$$ f(x) = x^2$$
然后
$$ \begin{aligned}
f(10+\epsilon) &=(10+\epsilon)^{2} \
&=100+20 \epsilon+\epsilon^{2} \
&=100+20 \epsilon
\end{aligned}$$
观察$\epsilon$的系数,我们发现$Df(10) = 20$。事实上,这个规律可以推广到不是多项式的函数。考虑一个任意可微函数$f(x)$,然后我们计算$f(x+ \epsilon)$,通过在$x$附近做泰勒展开,这就得到了无穷级数
$$
\begin{array}{l}
f(x+\epsilon)=f(x)+D f(x) \epsilon+D^{2} f(x) \frac{\epsilon^{2}}{2}+D^{3} f(x) \frac{\epsilon^{3}}{6}+\cdots \
f(x+\epsilon)=f(x)+D f(x) \epsilon
\end{array}
$$
记住,$\epsilon^2 = 0$。
射流(Jet) 是一个$n$维二元数。我们定义$n$个极小单位$\epsilon_i,i=1,\cdots,n$。并且存在性质$\forall i,j:\epsilon_i\epsilon_j=0$。射流数由实数$a$和$n$维极小分量组成。
$$x = a + \sum_j \upsilon_j \epsilon_j$$
为了简化我们改写为这种形式
$$x = a + \mathbf{v}$$
然后使用泰勒级数展开,我们可以看到:
$$f(a+\mathbf{v}) = f(a) +Df(a)\mathbf{v} $$
对多变量函数$f : \mathbb{R^n} \longrightarrow \mathbb{R^m} $相似。对于自变量$x_i = a_i + \mathbf{v}_i, \forall i = 1,\cdots,n :$
$$f(x_1,\cdots,x_n) = f(a_1,\cdots,a_n) + \sum_i D_if(a_1,\cdots,a_n)\mathbf{v}_i $$
如果每个选取的极小量$\mathbf{v}_i = e_i $是$i^{th}$标准基向量,那么上面的表达式就可以简化为
$$
f(x_1,\cdots,x_n) = f(a_1,\cdots,a_n) + \sum_i D_if(a_1,\cdots,a_n) \epsilon_i
$$
我们可以查找$\epsilon_i$的系数来提取雅可比矩阵的坐标。
实现射流(Jet)
为了让上面学到的内容在实践中发挥作用,我们需要能够计算函数$f$的值,不仅在自变量是实数的时候,也需要在自变量是二元数的情况下适用。但是通常我们并非通过泰勒展开式来求函数值。这也就是为什么我们需要用到C++模板和操作符重载。下面的代码段实现了Jet类以及对该类的一些操作和函数。
template<int N> struct Jet {
double a;
Eigen::Matrix<double, 1, N> v;
};
template<int N> Jet<N> operator+(const Jet<N>& f, const Jet<N>& g) {
return Jet<N>(f.a + g.a, f.v + g.v);
}
template<int N> Jet<N> operator-(const Jet<N>& f, const Jet<N>& g) {
return Jet<N>(f.a - g.a, f.v - g.v);
}
template<int N> Jet<N> operator*(const Jet<N>& f, const Jet<N>& g) {
return Jet<N>(f.a * g.a, f.a * g.v + f.v * g.a);
}
template<int N> Jet<N> operator/(const Jet<N>& f, const Jet<N>& g) {
return Jet<N>(f.a / g.a, f.v / g.a - f.a * g.v / (g.a * g.a));
}
template <int N> Jet<N> exp(const Jet<N>& f) {
return Jet<T, N>(exp(f.a), exp(f.a) * f.v);
}
// This is a simple implementation for illustration purposes, the
// actual implementation of pow requires careful handling of a number
// of corner cases.
template <int N> Jet<N> pow(const Jet<N>& f, const Jet<N>& g) {
return Jet<N>(pow(f.a, g.a),
g.a * pow(f.a, g.a - 1.0) * f.v +
pow(f.a, g.a) * log(f.a); * g.v);
}
有了这些重载的函数,我们现在可以用一个Jets数组来调用Rat43CostFunctor,而不是double双精度类型。将其与初始化的Jets结合起来,我们就可以计算雅可比矩阵了:
class Rat43Automatic : public ceres::SizedCostFunction<1,4> {
public:
Rat43Automatic(const Rat43CostFunctor* functor) : functor_(functor) {}
virtual ~Rat43Automatic() {}
virtual bool Evaluate(double const* const* parameters,
double* residuals,
double** jacobians) const {
// Just evaluate the residuals if Jacobians are not required.
if (!jacobians) return (*functor_)(parameters[0], residuals);
// 初始化Jets,四个待求参数
ceres::Jet<4> jets[4];
for (int i = 0; i < 4; ++i) {
jets[i].a = parameters[0][i];
jets[i].v.setZero();
jets[i].v[i] = 1.0;
}
ceres::Jet<4> result;
(*functor_)(jets, &result);
// 把Jet的值(前面提到的,极小单位分量的系数)复制出啦.
residuals[0] = result.a;
for (int i = 0; i < 4; ++i) {
jacobians[0][i] = result.v[i];
}
return true;
}
private:
std::unique_ptr<const Rat43CostFunctor> functor_;
};
这就是AutoDiffCostFunction的核心工作原理。
陷阱
自动微分使用户不必计算和推理Jacobians的符号表达式,但是这个捷径是有代价的。例如,考虑以下简单的函数:
struct Functor {
template <typename T> bool operator()(const T* x, T* residual) const {
residual[0] = 1.0 - sqrt(x[0] * x[0] + x[1] * x[1]);
return true;
}
};
查看计算残差的代码,没有人预见到任何问题。但是,如果我们看一下雅可比矩阵的解析表达式:
$$
\begin{aligned}
y &=1-\sqrt{x_{0}^{2}+x_{1}^{2}} \
D_{1} y &=-\frac{x_{0}}{\sqrt{x_{0}^{2}+x_{1}^{2}}}, D_{2} y=-\frac{x_{1}}{\sqrt{x_{0}^{2}+x_{1}^{2}}}
\end{aligned}
$$
我们发现它在$x_0 = 0,x_1 = 0$处是不确定的。
这个问题没有完美的解决方案。在某些情况下,我们需要明确地指出可能出现的不确定的点,并使用使用L’Hopital rule(这里要去Ceres Solvel官网的rotation.h文件中的转换例程),在其他情况下,可能需要对表达式进行正则化,以消除这些点。