(非线性)最小二乘分析的另一个应用是根据离散数据点来拟合曲线。
离散数据点通过下述方法生成:
注:Ceres官方文档中说的是高斯噪声,但样例代码中给出的数据实际上是随机噪声,因此,本文给出的数据与官方样例代码中的数据会不太一样,不过这都是细节,几乎没有影响。
对于Ceres
使用基本步骤,本文不再赘述,可以参考之前的文章。
未知参数的曲线函数表达式如下:
以下分别根据自动微分法和解析解法来实现。
完整代码如下:
#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
更大。
数据及曲线拟合效果如下:
注:图片取自于Ceres官网,偷个懒,没有用自己生成的高斯噪声数据绘图,不要在意这些细节,^\/^。
对于 f = y − e m x + c f=y-e^{mx+c} f=y−emx+c,偏导函数如下:
#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;
}
结果与自动微分法一致。
假设数据存在一些外点(完全不符合曲线模型的噪点),如果还是使用上述代码进行最小二乘拟合,拟合得到的曲线将如图所示:
为了减少异常值或外点(outliers
)对非线性最小二乘优化问题的影响,常用的技巧是使用损失函数,损失函数能够抑制很大的残差,而通常只有外点才会有很大的残差值,相当于减少外点的权重,也就是鲁棒拟合。Ceres
自带的损失函数有TrivialLoss
,HuberLoss
,SoftLOneLoss
,CauchyLoss
,ArctanLoss
,TolerantLoss
,TukeyLoss
,ComposedLoss
,ScaledLoss
。
以CauchyLoss
为例,使用方法如下:
problem.AddResidualBlock(
new ExponentialResidual(data[2 * i], data[2 * i + 1]), //Y用户自定义成本函数
new ceres::CauchyLoss(0.5), // 损失函数,CauchyLoss函数,0.5表示尺度
&m, // 第一个参数块
&c); // 第二个参数块
加入损失函数后的曲线拟合效果如下图: