A Blog

or A notebook

0%

前言

​ 记录Eigen库的使用方法。

头文件

​ 如Eigen官方教程中所示,头文件分为以下几种:

Eigen

​ 其中,较为常用的分别为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 基础矩阵运算
#include <Eigen/Core> // 必须包含
#include <Eigen/Dense> // 稠密矩阵扩展(推荐)

// 线性系统求解
#include <Eigen/LU> // LU分解
#include <Eigen/Cholesky> // Cholesky分解
#include <Eigen/QR> // QR分解

// 稀疏矩阵
#include <Eigen/Sparse> // 稀疏矩阵核心
#include <Eigen/SparseLU> // 稀疏LU求解器

// 几何变换
#include <Eigen/Geometry> // 旋转、平移等

初始化

静态初始化

​ 创建已知维度的矩阵,比如Matrix3d、Vector4f。Matrix2Xd (行数固定) MatrixX2d (列数固定)

  1. 逗号初始化

    1
    2
    3
    4
    5
    6
    7
    Eigen::Matrix3d mat;
    mat << 1, 2, 3,
    4, 5, 6,
    7, 8, 9;

    Eigen::Vector4d vec;
    vec << 1, 2, 3, 4;
  2. 构造函数初始化

    1
    2
    3
    Eigen::Matrix2d mat(1.0, 2.0,  // 按行填充
    3.0, 4.0);
    Eigen::Vector3d vec(1.0, 2.0, 3.0);

动态初始化

​ 创建可调节维度的矩阵,比如MatrixXd、VectorXf。

  1. 指定大小后赋值

    1
    2
    3
    4
    5
    6
    Eigen::MatrixXd mat(2, 3);  // 2行3列,未初始化
    mat << 1, 2, 3,
    4, 5, 6;

    mat(0, 0) = 1.0; // 第1行第1列
    mat(1, 1) = 4.0; // 第2行第2列
  2. 使用函数进行特殊赋值

    1
    2
    3
    Eigen::MatrixXd mat = Eigen::MatrixXd::Zero(3, 3); // 全0矩阵
    Eigen::VectorXf vec = Eigen::VectorXf::Ones(5); // 全1向量
    Eigen::Matrix3f I = Eigen::Matrix3f::Identity(); // 单位矩阵

特殊矩阵赋值

  1. 常量矩阵

    1
    Eigen::Matrix4f mat = Eigen::Matrix4f::Constant(3.14); // 所有元素为3.14
  2. 随机矩阵

    1
    Eigen::MatrixXd rand_mat = Eigen::MatrixXd::Random(3, 3); // 范围[-1, 1]
  3. 线性序列矩阵

    1
    Eigen::VectorXd lin_vec = Eigen::VectorXd::LinSpaced(5, 0, 10); // 0到10的5等分

映射赋值

1
2
3
double data[] = {1, 2, 3, 4};
Eigen::Map<Eigen::VectorXd> vec(data, 4); // 直接映射数组
Eigen::Map<Eigen::MatrixXd> mat(data, 2, 2); // 2x2矩阵

块操作

提取块

  1. 取固定大小的块 matrix.block(startRow, startCol)

    1
    2
    3
    4
    5
    6
    7
    8
    Eigen::Matrix3d mat;
    mat << 1, 2, 3,
    4, 5, 6,
    7, 8, 9;

    // 提取左上角 2x2 子块
    Eigen::Block<Eigen::Matrix3d, 2, 2> block = mat.block<2, 2>(0, 0);
    cout << "2x2 Block:\n" << block << endl; // 输出 [1, 2; 4, 5]
  2. 提取动态大小的块 matrix.block(startRow, startCol, rows, cols)

    1
    2
    3
    // 提取右下角 2x2 子块(动态大小)
    Eigen::MatrixXd dynamic_block = mat.block(1, 1, 2, 2);
    cout << "Dynamic Block:\n" << dynamic_block << endl; // 输出 [5, 6; 8, 9]
  3. 特殊位置的块

操作 语法 示例
前n行 matrix.topRows(n) mat.topRows(2) → 前2行
后n行 matrix.bottomRows(n) mat.bottomRows(1) → 最后1行
左n行 matrix.leftCols(n) mat.leftCols(2) → 左2列
右n行 matrix.rightCols(n) mat.rightCols(1) → 右1列
任意行 matrix.row(i) mat.row(0) → 第1行
任意列 matrix.col(j) mat.col(1) → 第2列
主对角线 matrix.diagonal() mat.diagonal() → [1, 5, 9]

特殊函数

转置

1
2
3
MatrixXcf a = MatrixXcf::Random(2,2);
cout << "Here is the matrix a^T\n" << a.transpose() << endl;
// a = a.transpose(); // 禁止

共轭

1
2
MatrixXcf a = MatrixXcf::Random(2,2);
cout << "Here is the conjugate of a\n" << a.conjugate() << endl;

共轭转置

1
2
MatrixXcf a = MatrixXcf::Random(2,2);
cout << "Here is the matrix a^*\n" << a.adjoint() << endl;

点积

两向量维度必须相同,输出标量

1
double dot_product = a.dot(b);

差积

叉积结果是一个新向量 c,其方向垂直于原始 a 和 b 所在的平面。仅三维向量

1
double angle = acos(a.dot(b) / (a.norm() * b.norm())); // 弧度制

归约操作

  1. 所有元素求和

    1
    double sum = mat.sum();
  2. 所有元素求积

    1
    double prod = mat.prod();
  3. 所有元素的平均值

    1
    double mean = mat.mean();
  4. 最小元素值

    1
    2
    3
    int row, col;
    double min_val = mat.minCoeff(&row, &col); // 获取最小值及其位置
    cout << "Min: " << min_val << " at (" << row << "," << col << ")" << endl;
  5. 最大元素值

    1
    2
    3
    int row, col;
    double max_val = mat.maxCoeff(&row, &col); // 获取最大值及其位置
    cout << "Max: " << max_val << " at (" << row << "," << col << ")" << endl;
  6. 矩阵的迹(主对角线元素之和)

    1
    double trace = mat.trace();
  7. 范数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Eigen::MatrixXd m(2, 2);
    m << 1, 2,
    3, 4;
    // Frobenius 范数(类似于向量的 L2 范数)
    double frob_norm = m.norm(); // sqrt(1² + 2² + 3² + 4²) = sqrt(30) ≈ 5.47723
    // 平方范数
    double frob_norm = squaredNorm(); // 等于上面的平方

    // L1 范数(最大绝对列和)
    double l1_norm = m.colwise().lpNorm<1>().maxCoeff(); // max(1+3, 2+4) = 6

    // 无穷范数(最大绝对行和)
    double inf_norm = m.rowwise().lpNorm<1>().maxCoeff(); // max(1+2, 3+4) = 7

    // 谱范数(最大奇异值,即 L2 范数)
    double l2_norm = m.lpNorm<2>();

前言

课程的第一个作业是输入起始点和终止点,通过求解优化问题来生成路径平滑。与常规的搜索算法,采样算法的原理完全不同,却也能得到一条比较好的路径。


主节点

​ 主要用来初始化节点和接收话题消息。当存储 ‘/move_base_simple/goal’ 消息的容器满2个时,则进入处理:

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
if (startGoal.size() == 2)
{
// 设置点的数量
const int N = (startGoal.back() - startGoal.front()).norm() / config.pieceLength;
// 初始化存储内点的矩阵,不包括起点和终点
Eigen::Matrix2Xd innerPoints(2, N - 1);
// 根据线性插值填充内点
for (int i = 0; i < N - 1; ++i)
{
innerPoints.col(i) = (startGoal.back() - startGoal.front()) * (i + 1.0) / N + startGoal.front();
}
// 创建路径平滑器
path_smoother::PathSmoother pathSmoother;
// 配置路径平滑器,输入起点、终点、内点数量、障碍物和惩罚权重
pathSmoother.setup(startGoal.front(), startGoal.back(), N, config.circleObs, config.penaltyWeight);
// 创建曲线
CubicCurve curve;
// 使用路径平滑器优化曲线
if (std::isinf(pathSmoother.optimize(curve, innerPoints, config.relCostTol)))
{
return;
}

if (curve.getPieceNum() > 0)
{
visualizer.visualize(curve);
}
}

路径平滑器

结构体

​ 这个结构体是实现优化过程的部分,通过设置代价函数,并实现优化,结构如下:

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
class PathSmoother
{
private:
cubic_spline::CubicSpline cubSpline;

int pieceN;
Eigen::Matrix3Xd diskObstacles;
double penaltyWeight;
Eigen::Vector2d headP;
Eigen::Vector2d tailP;
Eigen::Matrix2Xd points;
Eigen::Matrix2Xd gradByPoints;
// L-BFGS 求解器
lbfgs::lbfgs_parameter_t lbfgs_params;

private:
static inline double costFunction(void *ptr,
const Eigen::VectorXd &x,
Eigen::VectorXd &g)

public:
inline bool setup(const Eigen::Vector2d &initialP,
const Eigen::Vector2d &terminalP,
const int &pieceNum,
const Eigen::Matrix3Xd &diskObs,
const double penaWeight);

inline double optimize(CubicCurve &curve,
const Eigen::Matrix2Xd &iniInPs,
const double &relCostTol);
};

}

代价函数

​ 首先,作业完成的代价函数。

​ 能量代价将在三次样条插值部分介绍。在这里先介绍障碍物代价,工程中的障碍物只由圆形组成,在diskObstacles分别存储圆心x,圆心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
49
50
51
52
53
static inline double costFunction(void *ptr,
const Eigen::VectorXd &x,
Eigen::VectorXd &g)
{
//设置指针
auto smooth_ptr = reinterpret_cast< PathSmoother*>( ptr) ;

/* 初始化 */
int nums = smooth_ptr->pieceN-1;
double energy_cost = 0 , obstacle_cost = 0;
// 初始化总梯度、能量梯度、障碍梯度
Eigen::Matrix2Xd grad = Eigen::Matrix2Xd::Zero(2, nums);
Eigen::Matrix2Xd energy_grad = Eigen::Matrix2Xd::Zero(2, nums);
Eigen::Matrix2Xd obstacle_grad = Eigen::Matrix2Xd::Zero(2, nums);

// 将 VectorXd 的 x 转化为 Matrix2Xd
Eigen::Matrix2Xd inPs = Eigen::Matrix2Xd::Zero(2, nums);
inPs.row(0) = x.head(nums);
inPs.row(1) = x.tail(nums);

/* 能耗代价 */
// 输入内点
smooth_ptr->cubSpline.setInnerPoints(inPs);
// 获取能耗的代价和梯度
smooth_ptr->cubSpline.getStretchEnergy(energy_cost);
smooth_ptr->cubSpline.getGrad(energy_grad);


/* 障碍代价 */
for(int i = 0 ; i < nums ; ++i)
for(int j = 0 ; j < smooth_ptr->diskObstacles.cols() ;++j )
{
Eigen::Vector2d diff = inPs.col(i) - smooth_ptr->diskObstacles.col(j).head(2);
double dis = diff.norm();
double delta = smooth_ptr->diskObstacles(2,j) -dis ;
if(delta > 0)
{
obstacle_cost+= smooth_ptr->penaltyWeight * delta;
obstacle_grad.col(i) += - smooth_ptr->penaltyWeight * diff /dis ;
}
}

/* 总代价和梯度 */
double cost = obstacle_cost+energy_cost;
grad = obstacle_grad + energy_grad;

// 将 Matrix2Xd 的 x 转化为 VectorXd
g.setZero();
g.head(nums) = grad.row(0).transpose();
g.tail(nums) = grad.row(1).transpose();

return cost;
}

优化调用

​ 我注意到这个函数是static 函数,这个函数的用法很奇怪,等会和optimize() 函数一起说,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
inline double optimize(CubicCurve &curve,
const Eigen::Matrix2Xd &iniInPs,
const double &relCostTol)
{
double minCost = 0;
// lbfgs_optimize 需要输入 VectorXd 的数据,先创建一个2 * (n-1) 的vector
Eigen::VectorXd x = Eigen::VectorXd::Map(iniInPs.data(),2*(pieceN-1));
// L-GFGS优化的函数接口
int status = lbfgs_optimize(iniInPs, minCost, & costFunction,nullptr,this, lbfgs_params);

if(status >= 0 )
{
cubSpline.getCurve();
}
else
std::cout << "Generation failed!" << std::endl;
return minCost;
}

​ lbfgs_optimize 函数在optimize中,传入的是this,是PathSmoother 的类型的。在传入lbfgs_optimize函数中,变为了void ;在lbfgs_optimize函数中创建回调结构体,结构体中设置代价函数的地址,随后在调用proc_evaluate时,又传入instance的地址,即PathSmoother 的this指针。在costFunction函数中又将的void ptr还原成PathSmoother*。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
inline int lbfgs_optimize(Eigen::VectorXd &x,
double &f,
lbfgs_evaluate_t proc_evaluate,
lbfgs_progress_t proc_progress,
void *instance,
const lbfgs_parameter_t &param)
{
...
/* Construct a callback data. */
callback_data_t cd;
cd.instance = instance;
cd.proc_evaluate = proc_evaluate;
cd.proc_progress = proc_progress;

/* Evaluate the function value and its gradient. */
fx = cd.proc_evaluate(cd.instance, x, g);
...
}

// 定义函数指针类型
typedef double (*lbfgs_evaluate_t)(void *instance,
const Eigen::VectorXd &x,
Eigen::VectorXd &g);

​ 实现void 和 PathSmoother 的来回转换本人还不是特别理解。

三次样条插值

特性

​ 三次样条由于n+1点构成(y0,y1, … , yn)。第i段的曲线表达为:

​ 在段与段的交接处,除了曲线连续外,要保证一阶二阶导数也连续,所以有

​ 整合起来,得到最终的系统为:

​ 其中D0 = Dn =0对于曲线的能量函数,表达为

​ 对函数进行求导后平方,再积分得到最终的能量代价为

​ 一阶梯度则为

​ 一阶梯度需要分别求d(di)/dx 和 d(ci)/dx,为:

​ 梯度的第一项为

​ 后面Di的导数可以从上面最终的系统的方程倒推。令过程变量为:

​ 那么D = A^(-1) B,那么d(Di)/dx = A^(-1) d(B)/dx。

​ 最终回带,就求得了能量函数的一阶梯度。具体可参照:

1
https://mathworld.wolfram.com/CubicSpline.html

结构体

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
 class CubicSpline
{
public:
CubicSpline() = default;
~CubicSpline() { A.destroy(); }

private:
int N;
Eigen::Vector2d headP;
Eigen::Vector2d tailP;
//带状矩阵
BandedSystem A;
Eigen::MatrixX2d b;
// 存储系数矩阵(a,b,c,d)
Eigen::Matrix2Xd coeffs;
// 存储过程梯度
Eigen::MatrixXd partial_c,partial_d;

public:
// 初始化系统
inline void setConditions(const Eigen::Vector2d &headPos,
const Eigen::Vector2d &tailPos,
const int &pieceNum);
// 输入内点,获取系数矩阵
inline void setInnerPoints(const Eigen::Ref<const Eigen::Matrix2Xd> &inPs);
// 根据系数矩阵得到曲线
inline void getCurve(CubicCurve &curve) const;
// 能量代价
inline void getStretchEnergy(double &energy) const;
// 获取系数矩阵
inline const Eigen::MatrixX2d &getCoeffs(void) const;
// 梯度
inline void getGrad(Eigen::Ref<Eigen::Matrix2Xd> gradByPoints) const;
// 计算梯度的过程梯度
inline void getPartial();
};

计算系数矩阵

​ 在初始化系统后,输入内点,就可以计算系数了。计算的思路是利用LU分解计算 A*D=b,计算过程变量D后,回带进ai,bi,ci,di的公式中,就可以得到系数矩阵。

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
inline void setInnerPoints(const Eigen::Ref<const Eigen::Matrix2Xd> &inPs)
{
b.setZero();
/* 先计算 D = A^-1 * b ,使用LU分解计算 A*D=b D为变量 */
// 填充b矩阵,inPs 内 设置的是除了起点终点的中间点,所以b的开头结尾需要单独处理
b.row(0) = 3 * (inPs.col(1).transpose()- headP.transpose() );
b.row(N-2) = 3 * (tailP.transpose()-inPs.col(N-3).transpose() );
for(int i = 1; i < N-2 ;++i)
{
b.row(i) = 3 * (inPs.col(i+1).transpose()-inPs.col(i-1).transpose() );
}

// 填充A矩阵
A.reset();
A(0,0)=4,A(0,1)=1;
A(N-2,N-2)=4,A(N-2,N-3)=1;
for(int i = 1; i < N-2 ;++i)
{
A(i,i) = 4, A(i,i-1) = A(i,i+1) = 1;
}

// LU分解后求解,存入b
A.factorizeLU();
A.solve(b);

// 初始化coeffs
coeffs.resize(2,4*N);
coeffs.setZero();

coeffs.col(0) = headP;
coeffs.col(1) = Eigen::Vector2d::Zero();
coeffs.col(2) = 3*(inPs.col(0) - headP) -b.row(0).transpose();
coeffs.col(3) = 2*(headP - inPs.col(0)) +b.row(0).transpose();

coeffs.col(4*N-4) = inPs.col(N-2);
coeffs.col(4*N-3) = b.row(N-2).transpose();
coeffs.col(4*N-2) = 3*(tailP - inPs.col(N-2)) -2 * b.row(N-2).transpose();
coeffs.col(4*N-1) = 2*(inPs.col(N-2) - tailP) +b.row(N-2).transpose();

for(int i = 1 ; i<N-1;++i)
{
coeffs.col(4*i) = inPs.col(i-1);
coeffs.col(4*i+1) = b.row(i-1).transpose();
coeffs.col(4*i+2) = 3*(inPs.col(i) - inPs.col(i-1)) -2 * b.row(i-1).transpose()-b.row(i).transpose() ;
coeffs.col(4*i+3) = 2*(inPs.col(i-1) - inPs.col(i)) +b.row(i-1).transpose()+ b.row(i).transpose() ;
}

// 将系数转置后存入b
b.resize(4*N,2);
b = coeffs.transpose();

return;
}

计算梯度

​ 最终的梯度比较难计算的是d(ci)/dx和d(di)/dx,其中的过程梯度比较多,所以先用一个求解过程梯度函数进行预处理。

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
inline void getPartial()
{
/* 首先求解 partial_D */
// 填充A矩阵
Eigen::MatrixXd A_tmp=Eigen::MatrixXd::Zero(N-1,N-1);
A_tmp(0,0)=4,A_tmp(0,1)=1;
A_tmp(N-2,N-2)=4,A_tmp(N-2,N-3)=1;
for(int i = 1; i < N-2 ;++i)
{
A_tmp(i,i) = 4, A_tmp(i,i-1) = A_tmp(i,i+1) = 1;
}
//填充partial_b矩阵
Eigen::MatrixXd partial_b = Eigen::MatrixXd::Zero(N-1,N-1);
partial_b(0,0)=0,partial_b(0,1)=3;
partial_b(N-2,N-2)=0,partial_b(N-2,N-3)=-3;
for(int i = 1; i < N-2 ;++i)
{
partial_b(i,i) = 0, partial_b(i,i-1) = -3, partial_b(i,i+1) = 3;
}
// partial_D = A^-1 * partial_b
// 个人觉得也能够用代价函数里LU分解的方式求partial_D
Eigen::MatrixXd partial_D = A_tmp.inverse() * partial_b;

/* 其次求解partial_delta*/
Eigen::MatrixXd partial_delta = Eigen::MatrixXd::Zero(N,N-1);
//填充partial_delta矩阵
partial_delta(0,0) = -1;
partial_delta(N-1,N-2) = 1;
for(int i = 1; i < N-1 ;++i)
{
partial_delta(i,i) =-1, partial_delta(i,i-1) = 1;
}

/*求解最终的partial_c,partial_d*/
partial_c.resize(N,N-1);
partial_c.setZero();
partial_d.resize(N,N-1);
partial_d.setZero();
// 填写最终的矩阵系数
Eigen::MatrixXd Q_c = Eigen::MatrixXd::Zero(N,N-1);
Eigen::MatrixXd Q_d = Eigen::MatrixXd::Zero(N,N-1);
//因为D0=Dn = 0 。而 partial_D 是从D1~Dn-1的
Q_c(0,0) = -1,Q_c(N-1,N-2)=-2;
Q_d(0,0) = 1,Q_d(N-1,N-2) = 1;
for(int i = 1 ;i< N-1;++i)
{
Q_c(i,i) = -1,Q_c(i,i-1)=-2;
Q_d(i,i) = 1,Q_d(i,i-1)=1;
}
partial_c = -3 * partial_delta + Q_c * partial_D;
partial_d = 2 * partial_delta + Q_d * partial_D;
}

​ 最终得到梯度:

1
2
3
4
5
6
7
8
9
10
11
12
inline void getGrad(Eigen::Ref<Eigen::Matrix2Xd> gradByPoints) const
{
gradByPoints.setZero();
for(int i = 0;i<N;++i)
{
Eigen::Vector2d ci = coeffs.col(4*i+2);
Eigen::Vector2d di = coeffs.col(4*i+3);
gradByPoints += (24*di + 12*ci)*partial_d.row(i)+
(12*di + 8*ci)*partial_c.row(i);
}
}

L-BFGS

线搜索

​ 通过weak wolfe原则和二分法结合,寻找步长。唯一还是不太理解的是二分法的选择,不满足充分下降条件时stp 缩小,而不满足曲线条件时stp 增大。

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
inline int line_search_lewisoverton(Eigen::VectorXd &x,
double &f,
Eigen::VectorXd &g,
double &stp,
const Eigen::VectorXd &s,
const Eigen::VectorXd &xp,
const Eigen::VectorXd &gp,
const double stpmin,
const double stpmax,
const callback_data_t &cd,
const lbfgs_parameter_t &param)
{
int iter = 0 ;
double upper = stpmax, low = stpmin;
double fx,fxp=f;
while(true)
{
iter++;
if(iter>param.max_linesearch)
break;

// 更新x,f(x)
x = xp + stp * s;
fx = cd.proc_evaluate(cd.instance,x,g);

/* weak wolfe条件 */
// 如果 fx 下降不够(即 fxp - fx 太小,不满足不等式),说明 stp 太大
if( fxp - fx < - stp * param.f_dec_coeff * gp.dot(s))
{
upper = stp;
}
// 如果新梯度 g.dot(s) 不够平缓(不满足不等式),说明 stp 太小
else if(g.dot(s) < param.s_curv_coeff * gp.dot(s))
{
low = stp;
}
// 满足 Wolfe 条件
else
{
f = fx;
return iter;
}

if (upper < stpmax)
{
stp = 0.5 * (low + upper);
}
else
{
stp = 2 * low;
}

}
return LBFGSERR_MAXIMUMLINESEARCH;
}

优化

​ 这一部分是项目自带的。

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
输入:
- 初始点 x0
- 目标函数 f(x)
- 梯度计算函数 grad_f(x)
- 参数:
- 内存大小 m (存储的向量对数量)
- 最大迭代次数 max_iter
- 梯度容差 g_epsilon
- 线搜索参数 (Wolfe 条件系数)

输出:
- 最优解 x
- 最优值 f(x)

算法步骤:
1. 初始化:
- x = x0
- g = grad_f(x)
- d = -g # 初始搜索方向 (负梯度)
- k = 0 # 迭代计数
- 初始化历史队列 S = [], Y = [] # 存储 s_k = x_{k+1} - x_k, y_k = g_{k+1} - g_k

2. 检查初始点是否为驻点:
if ||g||_inf < g_epsilon:
return x, f(x)

3. 主循环 (while k < max_iter):
a. 线搜索 (满足 Wolfe 条件):
- α = line_search(x, f, g, d) # 找到步长 α
- x_new = x + α * d
- g_new = grad_f(x_new)

b. 检查收敛:
if ||g_new||_inf < g_epsilon:
return x_new, f(x_new)

c. 更新历史向量对:
- s_k = x_new - x
- y_k = g_new - g
- 将 (s_k, y_k) 加入队列 S, Y
- if len(S) > m: # 限制内存
移除 S, Y 中最旧的向量对

d. 计算新的搜索方向 d (L-BFGS 双循环递归):
- q = -g_new
- for i = len(S)-1 downto 0:
ρ_i = 1 / (y_i^T s_i)
α_i = ρ_i * s_i^T q
q = q - α_i * y_i
- r = q * (s_{last}^T y_{last} / y_{last}^T y_{last}) # 缩放
- for i = 0 to len(S)-1:
β_i = ρ_i * y_i^T r
r = r + (α_i - β_i) * s_i
- d = r # 新的搜索方向

e. 更新变量:
- x = x_new
- g = g_new
- k = k + 1

4. 返回结果:
return x, f(x)

效果

Peek-2025-06-26-19-58

Peek-2025-06-26-20-02

Peek-2025-06-26-20-03

​ 尽管确实能生成路径,但是容易生成弯绕,曲折的路径。

前言

最近在学习深蓝学院的汪博讲的《机器人学中的数值优化》,本文用来记录与总结。在课程第一章汪博对凸函数 的性质花了很长时间介绍,本文略过。 水平有限,如有错误之处,敬请指正!


第一章 无约束优化问题

​ 无约束优化问题的定义为:

​ 其中,x 属于 n 维实数空间的变量。关于无约束的优化问题,课程中主要介绍了几种常见数值优化方法。本文对一些求导概念性属于进行介绍。

名称 符号 维度 X 维度 Y 维度
导数 f’(x) 1 1 1
梯度 ▽f(x) n × 1 n 1
海森 Hessian n × n n 1
雅可比 Jacobian m × n n m

​ 其中,梯度和海森分别是多输入单输出函数的一阶和二阶导数。

梯度下降法

​ 梯度下降法顾名思义,利用的是梯度信息。具体的迭代表达式为:

​ 其中,α 为步长。

​ 为什么需要步长这一概念?-▽f(x)是搜索的方向,如果走多了,会出现“曲折、振荡”的现象。线搜索 专门用于计算步长 α ,避免手动设定固定步长导致的收敛失败。

​ 常见的确定步长方法有 :

  1. α = c ,为常数,这就特备容易引发上面说的 振荡。
  2. α = c / k ,步长随着迭代次数增加逐渐降低
  3. 精确线搜索
  4. 非精确线搜索

​ 下面对几个线搜索相关的方法具体介绍 。


【补充】 线搜索

精确线搜索

​ 假设第 k 步的最优步长为 $ α_k $,那么则构建出子优化问题:

​ 但是此方法复杂度太高。

Armijo准则

​ 一般 c 取(0,1)。公式咋得来的?请看:

​ 总的来说就是 实际下降量 ≥ 预期下降的缩小值保证每次迭代的函数值下降是充分的,而非随意或微小的。所以,满足Armijo准则的条件又叫做充分下降条件

弱Wolfe准则

​ 弱Wolfe准则满足2个条件,分别为上述的Armijo条件和曲率条件。曲率条件表达式为:

​ 如下图,公式的左半边是Φ(α)函数的导数,物理意义是说,曲率条件的物理意义是Φ(α)函数在α的斜率大于等于c2倍的Φ(0)的斜率。只限制方向导数的下界 ,允许步长较大。

weak_wolfe

强Wolfe准则

​ 强Wolfe准则和弱Wolfe准则的区别在于前者曲率条件多了个绝对值,也就是

strong_wolfe

  • 为什么弱Wolfe准则强Wolfe准则 的曲率条件一个是 而一个是

    弱Wolfe 允许斜率过正, 而 强Wolfe准则 避免斜率过度正或过度负,从而防止步长过大(如跳过极小点)或过小(收敛缓慢)。


牛顿法

​ 梯度下降法是使用一阶梯度信息进行更新的,牛顿法则用到了二阶的Hessian矩阵。怎么得来的呢?首先,将f(x)进泰勒展开到二次项。

​ 求极值,即求f’(x) = 0 (因为默认是光滑凸函数),对上面公式求导得到

​ 这就得到了递推公式! ,一般称x_k后面的为牛顿步。 牛顿法相对于梯度下降法有更快的收敛速度,但是稳定性较差。因为需要求解Hessian的逆,所以呀需要 Hessian 非奇异且正定(非奇异—->求逆 , 正定—->牛顿步为下降方向),而且这一步很耗时。所以出现了一些解决方案:

  1. 使用修正矩阵M 去近似 Hessian

  2. 引入线性方程组,求解d代替求逆

​ 1)若 Hessian 矩阵半正定 , 使用Cholesky 分解

​ 其中 L 是下三角矩阵。

​ 2)若Hessian 矩阵不定,使用Bunch-Kaufman 分解

​ 其中 D 是 块对角矩阵 (稀疏矩阵)。可以很容易地去修改这些小块矩阵的特征值,把他们改成正数。

拟牛顿法

BFGS

​ 总的来说,牛顿法还是存在不小的问题的,其一,不保证一定是下降方向;其二,二阶Hessian求解还是比较难的,所以出现了拟牛顿法,核心思想是使用一阶梯度信息去近似二阶信息。同样是求解B矩阵近似Hessian 。

​ 对梯度▽f(x) 泰勒展开得到

​ 然后,由于x_k + p =x_k+1,带入得到

​ 整理得到约束条件,一般叫做 牛顿条件

MB互为逆矩阵,一般用第二个B 矩阵的公式。公式里变量具体含义为:

​ 那么接下来去求B。 核心思想是:尽量保留已有信息,同时满足牛顿条件

​ 通过一系列复杂的计算(没看懂),最终得到BFGS算法的迭代公式,3 2 1 上公式!

BFGS

​ 这是一个迭代更新的过程,每一轮迭代优化步都需要对B矩阵进行更新,从而用历史的一阶梯度信息,对二阶信息进行迭代估计。

​ 但这个方法的缺点越比较明显:

  • 存储和计算开销较大,去近似的Hessian矩阵,需要n × n 的空间,当变量维数很大,内存和计算代价高
  • 无法保证B^k+1 正定,不能保证目标函数下降,所以一般与线搜索连用

L-BFGS

​ 核心思路:使用有限个的历史梯度变化,近似Hessian ,直接上伪代码!

L-BFGS

共轭梯度法

​ 共轭梯度法是专门用来求解线性方程的。共轭梯度法在每步迭代中选择一组 与A共轭的方向。主啵累了,放公式:

Newton-CG

参考

1
2
https://zhuanlan.zhihu.com/p/670856639
https://www.shenlanxueyuan.com/course/745

Quick Start

Create a new post

1
$ hexo new "My New Post"