最小二乘问题

在一类特殊的最优化问题中(例如曲线拟合问题),目标函数F(x)F(\boldsymbol x) 通常是由若干个函数的平方和构成:

F(x)=i=1mfi2(x),xRn,mnF(\boldsymbol x)=\sum_{i=1}^mf_i^2(\boldsymbol x),\quad\boldsymbol x\in\Bbb R^n,m\geq n

极小化此类目标函数minF(x)\min F(\boldsymbol x) 的问题就被称为最小二乘问题

  • fi(x)f_i(\boldsymbol x) 均为线性函数时,该问题是 线性最小二乘问题,反之则是非线性最小二乘问题.

实际上,最小二乘问题自然也是一类无约束的非线性规划问题,我们可以采用梯度下降、牛顿法等一般方法求解。🦄详见:【最优化】基于导数的最优化方法

不过,最小二乘问题因其特殊的目标函数结构,它也有专属于自己的一类算法可供求解,这也是本文的核心。其中,线性最小二乘问题可以归结为线性回归问题,本站的 经典回归模型汇总 | Regression 一文已经做了详尽的推导。本文将着重介绍非线性最小二乘问题及其求解算法。

非线性最小二乘

非线性最小二乘问题的求解方法仍然利用迭代策略,将函数fi(x)f_i(\boldsymbol x) 在当前点x(k)\boldsymbol x^{(k)} 进行 Taylor 一阶展开以将其近似为线性函数φi(x)\varphi_i(\boldsymbol x) 从而利用线性最小二乘的求解方法更新后继点x(k+1)\boldsymbol x^{(k+1)},达到不断逼近最优值的目的。

fi(x)φi(x)=fi(x(k))+fi(x(k))(xx(k))=fi(x(k))x[fi(x(k))x(k)fi(x(k))]\begin{aligned} f_i(\boldsymbol x)\approx\varphi_i(\boldsymbol x)&=f_i(\boldsymbol x^{(k)})+\nabla f_i(\boldsymbol x^{(k)})^\top(\boldsymbol x-\boldsymbol x^{(k)})\\ &=\nabla f_i(\boldsymbol x^{(k)})^\top\boldsymbol x-[\nabla f_i(\boldsymbol x^{(k)})^\top\boldsymbol x^{(k)}-f_i(\boldsymbol x^{(k)})] \end{aligned}

从而,有

φ(x)=[φ1(x)φ2(x)φm(x)]=[f1(x(k))f2(x(k))fm(x(k))]x[f1(x(k))f2(x(k))fm(x(k))]x(k)+f(k)=defAkx(Akx(k)f(k))=defAkxb\begin{aligned} \boldsymbol\varphi(\boldsymbol x)=\begin{bmatrix}\varphi_1(\boldsymbol x)\\\varphi_2(\boldsymbol x)\\\vdots\\\varphi_m(\boldsymbol x)\end{bmatrix}&= \begin{bmatrix}\nabla f_1(\boldsymbol x^{(k)})^\top\\\nabla f_2(\boldsymbol x^{(k)})^\top\\\vdots\\\nabla f_m(\boldsymbol x^{(k)})^\top\end{bmatrix} \boldsymbol x-\begin{bmatrix}\nabla f_1(\boldsymbol x^{(k)})^\top\\\nabla f_2(\boldsymbol x^{(k)})^\top\\\vdots\\\nabla f_m(\boldsymbol x^{(k)})^\top\end{bmatrix}\boldsymbol x^{(k)}+\boldsymbol f^{(k)}\\ &\xlongequal{\rm def}\boldsymbol{A_kx-(A_kx^{(k)}-f^{(k)})}\\ &\xlongequal{\rm def}\boldsymbol{A_kx-b} \end{aligned}

又由于

F(x)ϕ(x)=i=1mφi(x)=φφF(\boldsymbol x)\approx\phi(\boldsymbol x)=\sum_{i=1}^m\varphi_i(\boldsymbol x)=\boldsymbol{\varphi^\top\varphi}

从而效仿线性最小二乘,有ϕ(x)=(Akxb)(Akxb)\phi(\boldsymbol x)=(\boldsymbol {A_kx-b})^\top(\boldsymbol {A_kx-b})

ϕ(x)=0\nabla\phi(\boldsymbol x)=\bf0AkAk(xx(k))=Akf(k)\boldsymbol{A_k^\top A_k(x-x^{(k)})=-A_k^\top f^{(k)}},若Ak\boldsymbol A_k 列满秩,那么AkAk\boldsymbol{A_k^\top A_k} 为对称正定矩阵,并且可逆。从而得到更新公式:

x(k+1)=x(k)(AkAk)1Akf(k)\boldsymbol x^{(k+1)}=\boldsymbol x^{(k)}-\boldsymbol{(A_k^\top A_k)^{-1}A_k^\top f^{(k)}}

Causs-Newton 方向

因为ϕ(x)=2AkAkx2Akb=2[AkAk(xx(k))+Akf(k)]\nabla\phi(\boldsymbol x)=2\boldsymbol{A_k^\top A_kx}-2\boldsymbol{A_k^\top b}=2\left[\boldsymbol{A_k^\top A_k(x-x^{(k)})+A_k^\top f^{(k)}}\right]

所以,2AkAk2\boldsymbol{A_k^\top A_k} 正好就是2ϕ(x)\nabla^2\phi(\boldsymbol x) 是目标函数的 Hesse 矩阵2F(x(k))\nabla^2 F(\boldsymbol x^{(k)})近似,记为HkH_k
此外,令x=x(k)\boldsymbol {x=x^{(k)}} 得到2Akf(k)2\boldsymbol{A_k^\top f^{(k)}} 正好是ϕ(x(k))= F(x(k))\nabla\phi(\boldsymbol x^{(k)})=\nabla\ F(\boldsymbol x^{(k)}) ,是目标函数在此处的梯度,记为gk\boldsymbol g_k

于是,上一节得到的更新公式还可以写为:

x(k+1)=x(k)Hk1gk\boldsymbol x^{(k+1)}=\boldsymbol x^{(k)}-H_k^{-1}\boldsymbol g_k

这正好就类似牛顿迭代法的公式!区别仅在于HkH_k 是近似的 Hesse 矩阵。也正因为如此,我们称方向d(k)=Hk1gk\boldsymbol d^{(k)}=-H_k^{-1}\boldsymbol g_kCauss-Newton 方向。但是,牛顿法的一些缺点它也继承了——HkH_k 非正定或奇异时,算法就失效了。

Causs-Newton 法的算法实现如下图所示:

Causs-Newton

注:在上述描述中出现的Ak,bA_k,b 是为了对照线性最小二乘问题的变量而命名的。
事实上,AkA_k 是向量函数φ(x)=(φ1(x),..,φm(x))\boldsymbol \varphi(\boldsymbol x)=(\varphi_1(\boldsymbol x),..,\varphi_m(\boldsymbol x))雅可比矩阵(Jacobian),记为JJ
因此许多文献是用JkJ_k 作为记号的。

Levenberg-Marquardt 算法

为了解决 Hesse 矩阵的近似AkAk\boldsymbol{A_k^\top A_k} 的奇异情形,Levenberg-Marquardt 修正方法与之前介绍的修正牛顿法一样,引入一个正定对角阵(通常是单位阵)对其进行修正,通过阻尼因子α\alpha 控制其影响程度。

最终确定方向:

d(k)=H~k1gk=(Hk+αkI)1gk=(AkAk+αkI)1Akf(k)  //忽略2倍系数=(JkJk+μkI)1Jkf(k)  //其他文献的记法\begin{aligned} \boldsymbol d^{(k)}&=-\tilde{H}_k^{-1}\boldsymbol g_k\\ &=-(H_k+\alpha_kI)^{-1}\boldsymbol g_k\\ &=-(\boldsymbol{A_k^\top A_k}+\alpha_kI)^{-1}\boldsymbol{A_k^\top f}^{(k)}\;\color{teal}{//\text{忽略2倍系数}}\\ &=-(\boldsymbol{J_k^\top J_k}+\mu_kI)^{-1}\boldsymbol{J_k^\top f}^{(k)}\;\color{teal}{//\text{其他文献的记法}} \end{aligned}

  • αk=0\alpha_k=0 时,d(k)\boldsymbol d^{(k)} 退化成了 Causs-Newton 方向;
  • αk\alpha_k\to\infty 时,d(k)\boldsymbol d^{(k)} 退化成了负梯度方向,即最速下降法的方向;

可见,Levenberg-Marquardt 算法结合了 Causs-Newton 法 和 最速下降法,并且通过阻尼因子加以控制。
然而当αk\alpha_k\to\infty 时,d(k)0\Vert\boldsymbol d^{(k)}\Vert\to0,这会导致x(k+1)=x(k)+d(k)x(k)\boldsymbol x^{(k+1)}=\boldsymbol x^{(k)}+\boldsymbol d^{(k)}\to\boldsymbol x^{(k)},从而收敛速度大大减小。因此,我们还需要引入增长因子β\beta动态调整αk\alpha_k.

  • FF 不下降,αβα\alpha\leftarrow\beta\alpha 使最速下降方向的贡献度提高,迫使函数值下降;
  • FF 能下降,αα/β\alpha\leftarrow\alpha/\beta 使 Causs-Newton 方向占主导,加快收敛速度;

整个LM算法的流程如下图所示:

LM算法

曲线拟合问题

假设已知nn 个数据样本点{(xi,yi)i=1,2,..,m}\{(x_i,y_i)\mid i=1,2,..,m\} ,样本点存在映射关系:xy:\boldsymbol{x\to y} 。现有一个含有参数(parameter)pRm\boldsymbol p\in\Bbb R^m 的函数y~(x;p)\tilde y(x;\boldsymbol p) 。曲线拟合的任务就是根据所有数据样本点将参数估计出来,使得函数y~(x;p)\tilde y(x;\boldsymbol p) 更接近原来的真实函数,即还原出映射规则。

该问题可以转化为最小化真实值yiy_i 和预测值y~(xi;p)\tilde y(x_i;\boldsymbol p) 之间的差距,进而找到最优的参数。更进一步地,我们可以将拟合问题视为非线性最小二乘问题:

min  F(p)=i=1nfi2(p)=i=1n[y~(xi;p)yi]2\min\; F(\boldsymbol p)=\sum_{i=1}^nf_i^2(\boldsymbol p)=\sum_{i=1}^n\left[\tilde y(x_i;\boldsymbol p)-y_i\right]^2

其中,残差向量函数f\boldsymbol f

f=[f1f2fn]\boldsymbol f=\begin{bmatrix}f_1\\f_2\\\vdots\\f_n\end{bmatrix}

实际上,如果再将目标函数乘上1/n1/n ,其结果就是 MSE 了。

C++实现

本章我们将测试两个待估计参数的函数,通过读取给定的样本数据文件,采用 Levenberg-Marquardt 算法 实现对函数参数的估计,最终达成拟合曲线的目的。

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <fstream> //用于读取数据
#include <vector>
#include "eigen3/Eigen/Dense" //矩阵运算
#include <cppad/cppad.hpp> // 自动微分计算Jacobian

using namespace Eigen;
using namespace std;

VectorXd data_x, data_y; // 全局变量

读取数据

样本数据的文件为 .txt 文本文件,内容格式如下所示:

1
2
3
4
5
0.000000,12.701052
0.001000,12.469602
0.002000,-3.209529
0.003000,-8.263387
0.004000,-3.169445

下面是实现数据读取的具体代码,其中将文件中的第一列和第二列分别存储到 data_xdata_y 中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

void read_txt_to_vector(const string& file_name, VectorXd& data_x, VectorXd& data_y) {

// 输入文件流对象
ifstream in_file(file_name);

// 判断文件是否打开成功
if (!in_file.is_open()) {
cout << "Error: cannot open file " << file_name << endl;
return;
}

string line;
vector<double> data1, data2;
int count = 0;

// 循环读取文件中的每一行,直到文件结束
while (getline(in_file, line)) {

stringstream ss(line);
string item;

// 循环读取每一行中的每个数据,以逗号为分隔符
while (getline(ss, item, ',')) {
if(count%2 == 0) data1.push_back(stod(item)); // str -> double
else data2.push_back(stod(item));
count++;
}
}

in_file.close();

// 将向量类型的数据转换为 Eigen::VectorXd 类型,并分别赋值给 data_x 和 data_y
// Eigen::Map 可以将内存块映射为 Eigen 类型对象
// 第一个参数是向量的数据指针,第二个参数是向量的大小
data_x = Map<VectorXd>(data1.data(), count / 2);
data_y = Map<VectorXd>(data2.data(), count / 2);
}

// 测试
int main(void) {
VectorXd data_x, data_y;
read_txt_to_vector("GaussCurveData.txt", data_x, data_y);
cout << "data_x = " << data_x.transpose() << endl;
cout << "data_y = " << data_y.transpose() << endl;
cout << data_x.size() << " " << data_y.size() << endl;
return 0;
}

基础运算实现

使用 C/C++ 实现时,我们需要考虑到 LM算法 涉及的矩阵运算、函数求导等复杂操作。

Eigen库

对于矩阵运算,具体到 LM算法 中最关键的是通过解线性方程组求出方向d\boldsymbol d,我们当然可以使用一/二维数组+有限次 for 循环,利用高斯消元等方法解决该问题。不过我们还可以通过引入 C++ 的开源库 Eigen 来进行矩阵运算。

Linux 下可直接通过命令安装:

1
sudo apt-get install libeigen3-dev

CppAD库

对于函数求导,具体到 LM 算法中 我们需要求出含参的待拟合函数ff 关于各参数的偏导数
在计算机中,我们可以通过:手动求导、有限差分法、自动微分法等方法实现导数计算。

下面我们将通过使用开源库 CppAD 来实现自动微分,以应对不同的函数。

Linux 下可直接通过命令安装:

1
sudo apt-get install cppad
效果测试

假如我们欲求多项式函数

f(x)=a0+a1x+a2x2+a3x3+a4x4f(x)=a_0+a_1x+a_2x^2+a_3x^3+a_4x^4

x=3x=3 处时的导数,其中假设ai=1,  (i=1,..,4)a_i=1,\;(i=1,..,4)

可以通过下列代码验证程序是否能准确计算出f(3)=142f'(3)=142 .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# include <iostream>        // standard input/output
# include <vector> // standard vector
# include <cppad/cppad.hpp> // the CppAD package

// begin the empty namespace
namespace {
// define the function Poly(a, x) = a[0] + a[1]*x[1] + ... + a[k-1]*x[k-1]
// where x[i] == x^i
// "x" and "y" are "Type" type, "a" is a CppAD vector
template <class Type>
Type Poly(const CPPAD_TESTVECTOR(double) &a, const Type &x){

size_t k = a.size();
Type y = 0.;
Type x_i = 1.;

for(size_t i = 0; i < k; i++){
y += a[i] * x_i; // y = y + a_i * x^i
x_i *= x; // x_i = x_i * x
}

return y;
}
}

int main(void){

using CppAD::AD; // use AD as abbreviation for CppAD::AD
using std::vector; // use vector as abbreviation for std::vector

// 多项式系数 vector of polynomial coefficients
size_t k = 5;
CPPAD_TESTVECTOR(double) a(k);

for(size_t i = 0; i < k; i++) a[i] = 1.;

// 自变量 domain space vector
size_t n = 1; // number of domain space variables
vector< AD<double> > ax(n); // vector of domain space variables
ax[0] = 3.; // value at which function is recorded

// 声明自变量
// declare independent variables and start recording operation sequence
CppAD::Independent(ax);

// 因变量 range space vector
size_t m = 1; // number of ranges space variables
vector< AD<double> > ay(m); // vector of ranges space variables
ay[0] = Poly(a, ax[0]); // record operations that compute ay[0]

// 定义 cppAD 的函数映射变量 f
// store operation sequence in f: X -> Y and stop recording
CppAD::ADFun<double> f(ax, ay);

// compute derivative using operation sequence stored in f
vector<double> jac(m * n); // Jacobian of f (m by n matrix)
vector<double> x(n); // domain space vector
x[0] = 3.; // argument value for computing derivative
jac = f.Jacobian(x); // Jacobian for operation sequence

// print the results
std::cout << "f'(3) computed by CppAD = " << jac[0] << std::endl;

// check if the derivative is correct
int error_code;
if( jac[0] == 142. )
error_code = 0;
else error_code = 1;

return error_code;
}

测试用拟合函数

Gaussian 函数

在本例中,需要拟合的高斯函数的表达式如下:

G(x)=A4ln2π  we4ln2w2(xx0)2G(x)=A\frac{\sqrt{4\ln2}}{\sqrt{\pi}\;w}e^{-\frac{4\ln2}{w^2}(x-x_0)^2}

其中输入自变量为xx,需要估计的参数有(A,x0,w)(A,x_0,w)

1
2
3
4
5
6
7
8
9
10
template <typename T>
T gaussCurve( T A, T x, T x0, T w){
T x2 = (x-x0)*(x-x0);
T w2 = w*w;
T G = A * (sqrt(4*log(2))/(sqrt(M_PI)*w))*CppAD::exp(-(4*log(2)/w2)*x2);
return G;
}

// 用 T 应对不同类型的输入和返回,主要是CppAD的类型的输入与返回,用于生成计算图
// 此外,此处必须使用 CppAD::exp() 而不是 std::exp() 以生成计算图

Pseudo-Voigt 函数

pseudo-Voigt 函数 是一种用于近似 Voigt 函数的函数,它是 Gaussian高斯函数 和 洛伦兹函数 的线性组合,表达式为:

pV(x)=ηG(x)+(1η)L(x)p_V(x) = \eta G(x) + (1 - \eta) L(x)

pseudo-Voigt 函数的优点是它比 Voigt 函数更容易计算,而且可以很好地拟合实际的光谱数据。而它的缺点是它并不是一个物理意义明确的函数,而且它的参数不一定与 Voigt 函数的参数一致。因此,pseudo-Voigt 函数只适用于一些简单的数据分析,而不适用于一些需要精确的物理模型的情况。

在本例中,我们更加细化其表达式为:

pV(x)=A[η2πw4(xx0)2+w2+(1η)4ln2π  wexp(4ln2w2(xx0)2)]p_V(x)=A\left[\eta\frac2\pi\frac{w}{4(x-x_0)^2+w^2}+(1-\eta)\frac{\sqrt{4\ln2}}{\sqrt{\pi}\;w}\exp{\left(-\frac{4\ln2}{w^2}(x-x_0)^2\right)}\right]

其中输入自变量为xx,需要估计的参数有(A,x0,w,η)(A,x_0,w,\eta)

1
2
3
4
5
6
7
8
9
10
11
template <typename T>
T pseudo_voigt(T A, T n, T x, T x0, T w){
T x2 = (x-x0)*(x-x0);
T w2 = w*w;
return A*(
n*(2.0/M_PI)*(w/(4*x2+w2)) +
(1-n)*(sqrt(4*log(2))/(sqrt(M_PI)*w))*CppAD::exp(
-(4*log(2)/w2)*x2
)
);
}

非线性最小二乘

对于拟合问题,我们希望拟合出来的函数y~\tilde y 与真实的yy 尽可能相近。这是通过优化函数的待定参数(如p=[A,η,..]\boldsymbol p=[A,\eta,..] )得来的。所以在计算图中需要把参数当成自变量求导。

对于输入的x=[x1,x2,..,xn]\boldsymbol x=[x_1,x_2,..,x_n]^\top 和参数p=[A,η,..]\boldsymbol p=[A,\eta,..],得到关于p\boldsymbol p 的残差:

f=[f1f2fn]\boldsymbol f=\begin{bmatrix}f_1\\f_2\\\vdots\\f_n\end{bmatrix}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
template <typename T>
void nls_func(const std::vector<T>& p, std::vector<T>& f, int func_id=0) {
// p 是参数向量, f 是残差向量, 包含每个观测点的残差
// 用 T 应对CppAD的类型的输入与返回,用于生成计算图
// 根据传入参数 func_id 确定拟合的函数
T A = p[0];
T x0 = p[1];
T w = p[2];

for (int i = 0; i < f.size(); i++) {
T xi = data_x[i];
T yi = data_y[i];
if(func_id){
T n = p[3];
f[i] = yi - pseudo_voigt(A, n, xi, x0, w);
}else{
f[i] = yi - gaussCurve(A, xi, x0, w);
}

}
}

void nls_func(const VectorXd& p, VectorXd& f, int func_id=0) {
// 运算符重载,用于实际计算

double A = p[0];
double x0 = p[1];
double w = p[2];

for (int i = 0; i < f.size(); i++) {
double xi = data_x[i];
double yi = data_y[i];
if(func_id){
double n = p[3];
f[i] = yi - pseudo_voigt(A, n, xi, x0, w);
}else{
f[i] = yi - gaussCurve(A, xi, x0, w);
}
}
}

Levmar算法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

VectorXd levenberg_marquardt(const VectorXd& p0,
int max_iter, int func_id,
double nu, double eps, double lambda=1e-3) {
/*
p0: 初始化参数; max_iter: 最大迭代数;
eps: 计算停止条件; lambda: 阻尼因子; mu: 增长因子.
*/

int iter = 0;
int m = p0.size(); // 参数大小
int n = data_y.size(); // 数据点个数
double objF, objF_prev; //目标函数

VectorXd p(m); //参数向量
VectorXd res(n); // 残差向量
VectorXd res_prev(n); // 新的残差向量
VectorXd d(m); // 更新步长
MatrixXd J(n, m); // Jacobian 矩阵
MatrixXd H(m, m); // H = J^T · J ==> 近似 Hesse矩阵
VectorXd g(m); // J^T·f ==> 方程组右端, 事实上也是梯度的近似

//构建计算图
CppAD::ADFun<double> f; // CppAD 映射对象

vector<CppAD::AD<double>> P(m); // CppAD 自变量
for (int i = 0; i < m; i++){
P[i] = p0[i]; // 初始化
p[i] = p0[i];
}
CppAD::Independent(P); // 声明自变量向量

vector<CppAD::AD<double>> F(n); // CppAD 因变量
nls_func(P, F,func_id); // 载入非线性最小二乘问题到计算图内
f.Dependent(P, F); // 声明因变量向量

// 开始迭代
while (iter < max_iter) {

nls_func(p, res, func_id);
objF = res.squaredNorm();

// 计算雅可比矩阵 (P.S. 太坑了Eigen::Map)
J = Map<MatrixXd>(f.Jacobian(p).data(), m, n).transpose();

// 计算 H 和 g
H = J.transpose() * J;
g = J.transpose() * res;

// 通过方程组 (H+lambda*I)d = -g 解出步长/方向 d

d = -(H + lambda * MatrixXd::Identity(m, m)).ldlt().solve(g);

// 计算新的残差向量和目标函数值
nls_func(p + d, res_prev, func_id);
objF_prev = res_prev.squaredNorm();

// 判断是否接受更新
if (objF_prev - objF < 1e-6) {
p = p + d; // 接受更新
objF = objF_prev;
lambda /= nu;
if(g.norm() < eps) break; //达到结束条件
} else {
lambda *= nu;// 拒绝更新,增大阻尼因子
}

// 输出当前的结果
printf("iter %d: ObjectFunc = %.3f, lambda = %.6f, ", iter, objF, lambda);
cout << "p = [" << p.transpose() << "];" << endl;

iter++;
}

return p; //返回最优参数
}

主函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main(void) {
// 设置参数
int max_iter = 1000;
double nu = 10;
double lambda = 1e-3;
double eps = 1e-7;
VectorXd p;

// Gaussian 函数
read_txt_to_vector("GaussCurveData.txt", data_x, data_y);
VectorXd p0(3);
p0 << 1, 1, 1;
p = levenberg_marquardt(p0, max_iter, 0, nu, eps, lambda);
std::cout << "Gaussian Optimal parameters: " << p.transpose() << std::endl<< std::endl;

// pseudo-Voigt 函数
read_txt_to_vector("pseudo_voigt_data.txt", data_x, data_y);
VectorXd p1(4);
p1 << 1, 1, 1, 1;
p = levenberg_marquardt(p1, max_iter, 1, nu, eps, lambda);
std::cout << "pseudo-Voigt Optimal parameters: " << p.transpose() << std::endl;

return 0;
}

结果分析

Python 中绘制拟合曲线,并计算残差平方和方差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import pandas as pd
import matplotlib.pyplot as plt
import math


def gaussCurve(A, x, x0, w):
x2 = (x - x0) ** 2
w2 = w ** 2
return A * (math.sqrt(4*math.log(2))/(math.sqrt(math.pi)*w)) * math.exp(-(4*math.log(2)/w2)*x2)

def pseudo_voigt(A, n, x, x0, w):
x2 = (x - x0) ** 2
w2 = w ** 2
return A * (n*(2.0/math.pi)*(w/(4*x2+w2)) + (1-n)*(math.sqrt(4*math.log(2))/(math.sqrt(math.pi)*w))*math.exp(-(4*math.log(2)/w2)*x2))


def plotCurve(x, y, y_pred, title):
plt.scatter(x, y, s=1)
plt.plot(x, y_pred, color="r")
plt.title(title)
plt.show()

if __name__ == "__main__":

# Gaussian 函数
A, x0, w = (100.303, 0.509871, 0.210608)
data = pd.read_csv("GaussCurveData.txt", header=None)
y_pred = pd.Series([gaussCurve(A,xi,x0,w) for xi in data[0]])
res = y_pred - data[1]
title = f"Parameter: $A={A},x_0={x0},\omega={w}$"
plotCurve(data[0],data[1],y_pred,title)
print("残差平方和",res.pow(2).sum())
print("方差",res.var())

# pseudo-Voigt 函数
A, x0, w, n = (122.06, 0.499887, 0.209804, 0.321467)
data = pd.read_csv("pseudo_voigt_data.txt", header=None)
y_pred = pd.Series([pseudo_voigt(A,n,xi,x0,w) for xi in data[0]])
res = y_pred - data[1]
title = f"Parameter: $A={A},\eta={n},x_0={x0},\omega={w}$"
plotCurve(data[0],data[1],y_pred,title)
print("残差平方和",res.pow(2).sum())
print("方差",res.var())

最终结果如下:

CaussCurve

CaussCurve

计算得残差平方和为116628.65116628.65,方差为116.698116.698MSE=116.629MSE=116.629.

Pseudo-Voigt Curve

Pseudo-Voigt Curve

计算得残差平方和为111362.95111362.95,方差为111.471111.471MSE=111.363MSE=111.363.