Ceres学习笔记应用篇003_使用Ceres进行曲线拟合

(非线性)最小二乘分析的另一个应用是根据离散数据点来拟合曲线。

1 生成数据

离散数据点通过下述方法生成:

y = e 0.3 x + 0.1 + N ( 0 , σ 2 ) y=e^{0.3x+0.1}+N(0,\sigma^2) y=e0.3x+0.1+N(0,σ2) (1)

式中, N ( 0 , σ 2 ) N(0,\sigma^2) N(0,σ2)表示标准差为 σ \sigma σ的零均值高斯分布噪声。

注:Ceres官方文档中说的是高斯噪声,但样例代码中给出的数据实际上是随机噪声,因此,本文给出的数据与官方样例代码中的数据会不太一样,不过这都是细节,几乎没有影响。

2 构建最小二乘问题

对于Ceres使用基本步骤,本文不再赘述,可以参考之前的文章。
未知参数的曲线函数表达式如下:

y = e m x + c y=e^{mx+c} y=emx+c (2)

用通过式(1)生成的数据点来拟合上述曲线,因此构建如下非线性最小二乘问题:
min ∑ i 1 2 ∣ ∣ y i − e m x i + c ∣ ∣ 2 \text{min}\sum_i\frac{1}{2}||y_i-e^{mx_i+c}||^2 mini21∣∣yiemxi+c2 (3)

3 代码实践

以下分别根据自动微分法和解析解法来实现。

3.1 自动微分法

完整代码如下:

#include "ceres/ceres.h"
#include "glog/logging.h"
#include "opencv2/core.hpp"

// 用户自定义残差计算模型
struct ExponentialResidual 
{
    // // 样本数据观测值通过构造函数传入
    ExponentialResidual(double x, double y) : x_(x), y_(y) {}

    template <typename T>
    bool operator()(const T* const m, const T* const c, T* residual) const 
    {
        // 输出残差维度为1
        // 输入2个参数块,每个参数块的维度为1
        residual[0] = T(y_) - exp(m[0] * T(x_) + c[0]);
        return true;
    }

private:
    const double x_;
    const double y_;
};

int main(int argc, char** argv) 
{
    google::InitGoogleLogging(argv[0]);

    // 生成数据
    const int kNumObservations = 67;    // 67个点
    double sigma = 0.2;     // 标准差
    cv::RNG rng;        // OpenCV随机数产生器
    double data[2 * kNumObservations];      // 数据容器[x1,y1,x2,y2...]
    for (int i = 0; i < kNumObservations; ++i) {
        double x = 0.075 * i;
        double y = exp(0.3 * x + 0.1);
        double noise = rng.gaussian(sigma);
        data[2 * i] = x;
        data[2 * i + 1] = y + noise;
    }

    // 设置参数初始值
    // 输入2个参数块,每个参数块的维度为1
    double m = 0.;
    double c = 0.;

    // 构建非线性最小二乘问题
    ceres::Problem problem;
    for (int i = 0; i < kNumObservations; ++i) {
        // 添加残差块,需要依次指定代价函数,损失函数,参数块
        // 本例中损失函数为单位函数
        problem.AddResidualBlock(
            // 输出残差维度为1,输出参数块有2个,每个参数块维度都为1
            new ceres::AutoDiffCostFunction<ExponentialResidual, 1, 1, 1>(
                new ExponentialResidual(data[2 * i], data[2 * i + 1])),
            NULL,   // 损失函数,单位函数
            &m,     // 第一个参数块
            &c);    // 第二个参数块
    }

    // 配置求解器参数
    ceres::Solver::Options options;
    // 设置最大迭代次数
    options.max_num_iterations = 25;
    // 指定线性求解器来求解问题
    options.linear_solver_type = ceres::DENSE_QR;
    // 输出每次迭代的信息
    options.minimizer_progress_to_stdout = true;

    // 输出日志内容
    ceres::Solver::Summary summary;

    // 开始优化求解
    ceres::Solve(options, &problem, &summary);

    // 输出优化过程及结果
    std::cout << summary.FullReport() << "\n";
    std::cout << "Initial m: " << 0.0 << " c: " << 0.0 << "\n";
    std::cout << "Final   m: " << m << " c: " << c << "\n";

    std::system("pause");
    return 0;
}

输出结果如下:

iter      cost      cost_change  |gradient|   |step|    tr_ratio  tr_radius  ls_iter  iter_time  total_time
   0  1.204244e+02    0.00e+00    3.60e+02   0.00e+00   0.00e+00  1.00e+04        0    2.71e-03    3.09e-03
   1  2.425961e+03   -2.31e+03    3.60e+02   8.02e-01  -1.95e+01  5.00e+03        1    9.01e-04    4.39e-03
   2  2.422258e+03   -2.30e+03    3.60e+02   8.02e-01  -1.95e+01  1.25e+03        1    4.25e-04    4.90e-03
   3  2.400245e+03   -2.28e+03    3.60e+02   7.98e-01  -1.93e+01  1.56e+02        1    3.99e-04    5.37e-03
   4  2.210383e+03   -2.09e+03    3.60e+02   7.66e-01  -1.77e+01  9.77e+00        1    3.74e-04    5.80e-03
   5  8.483095e+02   -7.28e+02    3.60e+02   5.71e-01  -6.32e+00  3.05e-01        1    3.68e-04    6.23e-03
   6  3.404435e+01    8.64e+01    4.10e+02   3.12e-01   1.37e+00  9.16e-01        1    3.55e-03    9.84e-03
   7  7.242644e+00    2.68e+01    1.84e+02   1.27e-01   1.11e+00  2.75e+00        1    3.40e-03    1.35e-02
   8  3.933925e+00    3.31e+00    5.81e+01   3.45e-02   1.03e+00  8.24e+00        1    3.12e-03    1.67e-02
   9  2.333679e+00    1.60e+00    2.52e+01   9.77e-02   9.90e-01  2.47e+01        1    2.81e-03    1.96e-02
  10  1.419436e+00    9.14e-01    8.73e+00   1.16e-01   9.83e-01  7.42e+01        1    2.76e-03    2.25e-02
  11  1.245322e+00    1.74e-01    1.43e+00   6.69e-02   9.89e-01  2.22e+02        1    3.79e-03    2.64e-02
  12  1.237976e+00    7.35e-03    9.21e-02   1.61e-02   9.91e-01  6.67e+02        1    3.36e-03    2.98e-02
  13  1.237935e+00    4.11e-05    9.96e-04   1.28e-03   9.90e-01  2.00e+03        1    3.19e-03    3.31e-02

Solver Summary (v 2.0.0-eigen-(3.3.8)-no_lapack-eigensparse-no_openmp)

                                     Original                  Reduced
Parameter blocks                            2                        2
Parameters                                  2                        2
Residual blocks                            67                       67
Residuals                                  67                       67

Minimizer                        TRUST_REGION

Dense linear algebra library            EIGEN
Trust region strategy     LEVENBERG_MARQUARDT

                                        Given                     Used
Linear solver                        DENSE_QR                 DENSE_QR
Threads                                     1                        1
Linear solver ordering              AUTOMATIC                        2

Cost:
Initial                          1.204244e+02
Final                            1.237935e+00
Change                           1.191865e+02

Minimizer iterations                       14
Successful steps                            9
Unsuccessful steps                          5

Time (in seconds):
Preprocessor                         0.000380

  Residual only evaluation           0.002301 (14)
  Jacobian & residual evaluation     0.023362 (9)
  Linear solver                      0.004008 (14)
Minimizer                            0.033459

Postprocessor                        0.000041
Total                                0.033880

Termination:                      CONVERGENCE (Function tolerance reached. |cost_change|/cost: 1.219979e-08 <= 1.000000e-06)
Initial m: 0 c: 0
Final   m: 0.301681 c: 0.0907709

根据输出结果可以看出,参数迭代初始值为m = 0, c = 0,初始目标函数残差为120.424,最终参数收敛于m = 0.301681, c = 0.0907709,残差为1.237935,最终估计出的参数并不完全为m = 0.3, c = 0.1,这样的偏差是符合预期的,因为生成的数据中包含了噪声,事实上,如果使用m = 0.3, c = 0.1计算残差的话,残差值为1.241615,比m = 0.301681, c = 0.0907709时的残差值1.237935更大。
数据及曲线拟合效果如下:
image.png

注:图片取自于Ceres官网,偷个懒,没有用自己生成的高斯噪声数据绘图,不要在意这些细节,^\/^。

3.2 解析解法

对于 f = y − e m x + c f=y-e^{mx+c} f=yemx+c,偏导函数如下:

{ ∂ f ∂ m = − x ∗ e m x + c ∂ f ∂ c = − e m x + c \left\{\begin{matrix} \begin{aligned} &\frac{\partial f}{\partial m}=-x*e^{mx+c}\\ &\frac{\partial f}{\partial c}=-e^{mx+c} \end{aligned} \end{matrix}\right. mf=xemx+ccf=emx+c (4)

完整代码如下:

#include "ceres/ceres.h"
#include "glog/logging.h"
#include "opencv2/core.hpp"

class ExponentialResidual
    : public ceres::SizedCostFunction<1, /*输出(resudual)维度大小*/\
    1, /*第1个输入参数块维度大小*/\
    1 /*第2个输入参数块维度大小*/>
{
public:
    ExponentialResidual(double x, double y) : x_(x), y_(y) {}
    virtual ~ExponentialResidual() {}

    // 用户自定义残差计算方法
    virtual bool Evaluate(double const* const* x, /*输入参数块*/\
        double* residuals, /*输出残差*/\
        double** jacobians /*输出雅克比矩阵*/) const
    {
        // 本例中有两个输入参数块,每个参数块中有1个参数
        double m = x[0][0];
        double c = x[1][0];

        // 本例中输出残差维度为1
        double y0 = exp(m * x_ + c);
        residuals[0] = y_ - y0;

        if (jacobians == NULL) {
            return true;
        }
        // 残差对第1个参数块中的参数依次求偏导,即对m求偏导
        if (jacobians[0] != NULL) {
            jacobians[0][0] = -y0 * x_;
        }
        // 残差对第2个参数块中的参数依次求偏导,即对c求偏导
        if (jacobians[1] != NULL) {
            jacobians[1][0] = -y0;
        }
        return true;
    }

private:
    const double x_;
    const double y_;
};

int main(int argc, char** argv)
{
    google::InitGoogleLogging(argv[0]);

    // 生成数据
    const int kNumObservations = 67;    // 67个点
    double sigma = 0.2;     // 标准差
    cv::RNG rng;        // OpenCV随机数产生器
    double data[2 * kNumObservations];      // 数据容器[x1,y1,x2,y2...]
    for (int i = 0; i < kNumObservations; ++i) {
        double x = 0.075 * i;
        double y = exp(0.3 * x + 0.1);
        double noise = rng.gaussian(sigma);
        data[2 * i] = x;
        data[2 * i + 1] = y + noise;
    }

    // 设置参数初始值
    // 输入2个参数块,每个参数块的维度为1
    double m = 0.0;
    double c = 0.0;

    // 构建非线性最小二乘问题
    ceres::Problem problem;
    for (int i = 0; i < kNumObservations; ++i) {
        // 添加残差块,需要依次指定代价函数,损失函数,参数块
        // 本例中损失函数为单位函数
        problem.AddResidualBlock(
            new ExponentialResidual(data[2 * i], data[2 * i + 1]),
            NULL,   // 损失函数,单位函数
            &m,     // 第一个参数块
            &c);    // 第二个参数块
    }

    // 配置求解器参数
    ceres::Solver::Options options;
    // 设置最大迭代次数
    options.max_num_iterations = 25;
    // 指定线性求解器来求解问题
    options.linear_solver_type = ceres::DENSE_QR;
    // 输出每次迭代的信息
    options.minimizer_progress_to_stdout = true;

    // 输出日志内容
    ceres::Solver::Summary summary;

    // 开始优化求解
    ceres::Solve(options, &problem, &summary);

    // 输出优化过程及结果
    std::cout << summary.FullReport() << "\n";
    std::cout << "Initial m: " << 0.0 << " c: " << 0.0 << "\n";
    std::cout << "Final   m: " << m << " c: " << c << "\n";

    std::system("pause");
    return 0;
}

结果与自动微分法一致。

4 鲁棒最小二乘拟合曲线

假设数据存在一些外点(完全不符合曲线模型的噪点),如果还是使用上述代码进行最小二乘拟合,拟合得到的曲线将如图所示:
image.png
为了减少异常值外点outliers)对非线性最小二乘优化问题的影响,常用的技巧是使用损失函数,损失函数能够抑制很大的残差,而通常只有外点才会有很大的残差值,相当于减少外点的权重,也就是鲁棒拟合。
Ceres自带的损失函数有TrivialLossHuberLossSoftLOneLossCauchyLossArctanLossTolerantLossTukeyLossComposedLossScaledLoss
CauchyLoss为例,使用方法如下:

 problem.AddResidualBlock(
            new ExponentialResidual(data[2 * i], data[2 * i + 1]), //Y用户自定义成本函数
            new ceres::CauchyLoss(0.5),   // 损失函数,CauchyLoss函数,0.5表示尺度
            &m,     // 第一个参数块
            &c);    // 第二个参数块

加入损失函数后的曲线拟合效果如下图:
image.png

你可能感兴趣的:(#,Ceres,学习)