SLAM编程:优化问题求解(1)_程序设计

  • 前言
  • 一、大量的问题都是优化问题
  • 二、如何以朴素理论手写优化问题的程序
    • 1.程序总体设计
    • 2.编写大循环:更新参数并输出信息
    • 3.编写小循环:前向传播,计算误差,反向传播=>计算H、b
  • 三、使用库来完成优化问题
    • 1.使用g2o库完成单一位姿优化问题
  • 总结

前言

很久没有写技术博客了,想想看还是要固定时间养成习惯写,中间探索了很多学习编程的方法,最终还是得自己编出来+技术博客输出的方法是最好的。不能纯粹的画思维脑图,PPT之类的画板也有限,而且大量的时间浪费在思考与绘制精致的图表结构上,很费力,效率也很低。Word文档笔记不是很方便,最终还是技术博客比较适合,像LateX那样不用过多的考虑格式的问题(英文期刊提供的LateX模板会让你爱上写作英文论文),这篇是回坑吹水的第一篇,选择优化问题来写,希望能感受到越来越稀有的专注力带来的快乐吧。


提示:以下是本篇文章正文内容,下面案例可供参考

一、大量的问题都是优化问题

优化问题随处可见,深度学习就是把优化问题干到了极致(把参数直接往目标上靠)。平常所谓的“估计”问题,看起来是计算问题(因为表面上有固定的模型),但是也是优化问题。毕竟模型的形式是抽象的、能够使用人类思维来解释的,而模型的参数取值是具体的、令人头大的,需要使用数学方法来解释的。一份优秀的科研成果,少不了优化。我们经常会看到以下的形式出现在论文中:

这就是一个典型的优化求参数问题。这一句话的含义是,找到使得后面那个平方求和式值最小的T。我们正常看到的是min,而这里前面的arg指的就是“argument”的缩写,表示“参数”。这里再多说一嘴,有的时候一些程序里面,找到数组最大/最小值标号,也是argmax或者argmin(不信你去看PyTorch)。这个里面,就是把整个数组看成是一个函数,数组的index看成是参数,那么要找下标的话,自然是argmax和argmin了。

二、如何以朴素理论手写优化问题的程序

如果我们涉及到优化问题,首先要分清楚是有监督的优化问题还是无监督的优化问题。无监督优化问题里叫“目标函数”,我们只需要去寻找他的数学性质,寻找其极值点即可。有监督的优化问题里叫“损失函数”,我们必须去减少我们模型的表现和某个真值之间的损失。一提到有监督的优化问题,啪的一下就是四个要素很快啊!
——————————
数据:数据必须是成对的,要有观测值,也就是因变量y,以及自变量X。
抽象模型:负责对自变量X进行前向传播;
损失函数:传播得到的预测值要和因变量y进行作差来计算损失;
优化算法:最后一个要点,采用何种算法来进行优化?

——————————
以深度学习图像识别为例,自变量X是输入图像,与之对应的观测值是标签,模型是我们预先定义好的卷积神经网络,优化算法是随机梯度下降法SGD;以SLAM位姿估计为例,自变量是物方点的XYZ坐标,与之对应的观测值是图像上的(u,v)像素坐标,模型是共线条件方程(或者叫PnP投影关系),优化算法是GuassNewton或者是LM方法。这一节讲的是“朴素理论”,也就是编写梯度下降的循环迭代方法,不包含图优化(在后文中会介绍到)等理论。

1.程序总体设计

涉及到这种机器学习的优化方法,都需要求取梯度,如果不使用一些具有自动求导的框架(TensorFlow、PyTorch、Paddle)或者是库(Ceres、g2o、Matlab等),就需要我们告诉程序求导的规则。通俗而言,求一阶导数构成的矩阵叫“Jacbian”矩阵,求二阶导数构成的矩阵叫“Hessien”矩阵(这样并不是很严密的说法,但可以理解了)。我们不希望求取二阶的Hessian矩阵,尽管它在增量方程中是需要的,对于Hessian矩阵的不同方法的近似构成了GuassNewton方法和LM等方法,前者只计算Jacbian矩阵,而后用(JTJ)的方式来近似Hessian矩阵,构成了我们熟知的“最小二乘解”,在程序中,对于大量的数据点,采用叠加的方法进行统计(按道理来说,要将数据点进行拍列后再相乘,但是变长的数据结构是不利于程序写作的,因此考虑到JTJ的特殊性,采用相加的方法,这样可以保证结果一致)。关于求导,我会再新开一个文章,专门讲如何求取Jacbian矩阵的问题。
——————————
划重点:一个手写的优化程序势必由两个循环组成:
大循环:要将所有数据输入多少次(在深度学习中,叫“Epoch”)
小循环:逐个数据点,或者逐批次数据点来计算误差、求导等处理。
——通常而言,对于小规模的参数求解问题,都是在小循环结束之后,汇总误差与梯度信息来计算参数更新量;但是在大规模参数求解问题中(例如那个1750亿参数的模型),就需要每一个批次(batch)来不断调整参数,以至于深度学习还会存在mini-batch这种说法。
——————————
为避免让读者觉得乏善可陈,下面我们以一个比“曲线拟合”问题稍微高级一点的高斯牛顿法位姿估计程序为例,分享一些心得。这是因为,在一些复杂的场景下,我们需要重新定义参数更新方法和思考方式,源码来自高翔博士的《视觉SLAM十四讲》,一并感谢高翔博士拯救我这个编程小白。

2.编写大循环:更新参数并输出信息

两重循环嵌套的时候,一定不能照着程序一行一行的敲。人只能一次干一件事,先完成大循环,就当是一次小循环已经结束了、输出了我想要的东西,然后只需要对这些东西进行一些处理就可以了,这样在编写程序的时候,能够保证“线性思路进入程序”,而不是陷进下一个循环里面。简单的来说,大循环以自己指定的迭代次数(Epoch)为目的,是最终完成优化的那个循环,也得具备终止条件。
——————————
在小循环之前要做这么几件事:
(1)梯度要清零,因为所有数据已经遍历过一次了;
(2)记录当前大循环的Cost也要清零,理由同上;
(3)用于计算增量dx的H和b要清零(梯度包含在其中,回顾GN方法)
——————————
假设小循环已经结束,得到了我们需要的H和b:
则可以立即求出更新量dx,但是在更新之前,有3个条件得考虑:
(1)求解出来的dx不是数字(分母为零/NaN),就得让程序报错停止了;
(2)记录每一次大循环得到的cost,因为大循环的cost记录的是当前参数在所有数据上的表现,如果说这一次的cost比上一次循环的cost还大,就说明更新是不合适的,应当退出;
(3)如果dx本身的范数很小,也说明收敛了,不需要更新迭代了;代码如下(示例):

int iteration = 10;
typedef Eigen::Matrix<double,6,1> Vector6d;
double lastCost = 0;
double cost = 0;
Sophus::SE3d pose;
for(int iter=0;iter<iteration;iter++){Eigen::Matrix<double,6,6> H = Eigen::Matrix<double,6,6>::Zero();Vector6d b = Vector6d::Zero();cost = 0;//此处写小循环//.....//假设我们已经得到了H和bVector6d dx = H.ldlt().solve(b);if(isnan(dx[0])){cout<<"The result isnan!"<<endl;break;}if(dx.norm()<1e-6){cout<<"Covergence!"<<endl;break;}if(iter > 0 && cost>=lastCost){cout<<"The cost:"<<cost<<">=lastCost"<<lastCost<<endl;break;}//然后才是更新pose = Sophus::SE3d::exp(dx)*pose;lastCost = cost;cout<<"Iterattion:"<<iter<<"Cost:"<<cost<<endl;
}

3.编写小循环:前向传播,计算误差,反向传播=>计算H、b

小循环是针对“每一对数据”的,这就不是我们所指定的了,要根据数据点来算。三维坐标数据点的格式是vector<Eigen::Vector3d> points_3d; 二维像素点的数据格式仿照上面,区别在于2d,传入到函数当中。代码如下(示例):

for(int i=0;i<points_3d.size();i++){//前向传播,计算重投影:KTPEigen::Vector3d pc = pose * points_3d[i];Eigen::Vector2d proj(fx * pc[0]/pc[2]+cx,fy * pc[1]/pc[2]+cy,);Eigen::Vector2d e = points_2d[i]-proj;cost += e.squaredNrm();//在此输入2×6的Jacbian矩阵,由pc[i],fx,fy,cx,cy构成Eigen::Matrix<double,2,6> J;J << ....;//计算H和b,采用累加法H += J.transpose()*J;b += -J.transpose()*e;
}

至此,一个GuassNewton法进行位姿估计的小程序就写完了。实际上对于以最小二乘的优化问题,还有ceres库和g2o库。ceres库比较契合这种写法,本帖会不断的更新,先放出以图优化理论的g2o库的相关理解。这个库的典型特点是不好使用和理解,适合看别人的程序。

三、使用库来完成优化问题

1.使用g2o库完成单一位姿优化问题

要使用g2o库,首先要把优化问题建模为图优化问题。简而言之,就是定义好图的各个要素
——————————
1、定义图的顶点——待优化的变量。
待优化的变量通常叫“参数块”(和Ceres类似),一个参数块包含着从自变量X到因变量y这个模型里面的所有待优化参数,因此与不是某一个单一变量就可以成为一个顶点的(除非模型里只有这一个参数)。定义顶点有三个要点:
(1)定义顶点的模板实例;
(2)定义顶点的归零函数;
(3)定义顶点的更新函数。
首先说模板实例,所有的顶点继承自g2o的BaseVertex类。类模板参数为<参数更新量的维度,待优化变量的数据类型>。这两者有可能不一样。一般来说更新值和待优化变量的数据类型应当一致,这样可以使用简单的向量空间加法进行更新(例如深度学习采用的梯度下降法将所有参数看成是一个向量直接求解),然而对于位姿优化问题,具有明确的李群和李代数更新方式——左右扰动,i.e.,用一个6元素的一维向量对一个4×4位姿变换矩阵T进行更新,这样的话就不一样了;而后再说归零函数,就是顶点如何初始化,换言之就是待优化变量如何初始化;第三点就是更新函数,这里就是我们要定义的更新方法。代码如下(示例):

class VertexPose:public g2o::BaseVertex<6,Sophus::SE3d>{//更新维度是6,参数块类型是4×4李群
public://防止在内存分配上出现问题EIGEN_MAKE_ALIGNED_OPERATOR_NEW;//顶点归零函数,这里_estimate是内置变量virtual void setToOriginImpl() override {_estimate = Sophus::SE3d();}//顶点更新函数,对_estimate进行更新virtual void OplusImpl(const double *update)  override {//指针类型的可以采用下标进行访问Eigen::Matrix<double,6,1> update_eigen;update_eigen << update[0],update[1],update[2],update[3],update[4],update[5];_estimate = Sophus::SE3d::exp(update_eigen)*_estimate;}virtual bood read(istream &in) override {}virtual bood write(otream &out) override {}
}

——————————
2、定义边,也就是和模型相关的部分。
(1)定义边的类型(指定模板参数)
(2)定义构造函数并在最后指定私有成员;
(2)定义误差计算函数(computeError)
(3)定义导数函数(linearizeOplus)
边的类型分为一元(Unary)边和多元边,对于一个简单的曲线拟合或者是单一位姿估问题,都是一元边,对于大型的SLAM问题,往往是采用多元边。边的模板参数很有意思,<观测值维度,观测值数据类型,顶点类型>。对于单一位姿估计(光束法平差)问题,观测值就是像素坐标,维度为2,观测值数据类型就是Eigen::Vector2d;数据类型就是我们之前定义好的VertexPose。定义构造函数的写法一般是简写,这在函数里面会有,刻画的是投影关系,自然要传入的是自变量X(物方空间坐标点)+相机内参矩阵;误差计算函数完成前向传播后的loss计算功能,而导数函数+顶点更新函数则完成反向传播功能。特别要注意的是,每一对自变量X和因变量y之间都会构造出一条边。代码如下(示例):

class EdgeProjection:g2o::BaseUnaryEdge<2,Eigen::Vector2d,VertexPose>{public://防止在内存分配上出现问题EIGEN_MAKE_ALIGNED_OPERATOR_NEW;//构造函数EdgeProjection(const Eigen::Vector3d &pos_3d,const Eigen::Matrix3d &K):_pos3d(pos_3d),_K(K) {} //等价于{_pos3d=pos_3d;_K=K}//误差计算函数,该函数不接受参数,因此需要构造函数virtual void computeError() override {//取出第0号顶点,也就是参数块顶点,估计得到当前的参数const VertexPose *v = static_cast<VertexPose *>(_vertics[0]);Sophus::SE3d T = v->estimate();//根据模型进行前向传播Eigen::Vector3d pos_pixel = _K * T * (_pos3d[i]);pos_pixel /= pos_pixel[2]; //(u,v,1)//计算误差,_measurement和_error是内置变量_error = _measurement - pos_pixel.head<2>();}//导数函数,该函数不接受参数,因此需要构造函数virtual void linearizeOplus() override {//取出第0号顶点,也就是参数块顶点,估计得到当前的参数const VertexPose *v = static_cast<VertexPose *>(_vertics[0]);Sophus::SE3d T = v->estimate();//由于导数的形式是针对图像坐标系来的,因此要再计算一次参数//导数中的形式是[X',Y',Z']形式过来的Eigen::Vector3d pos_cam = T * _pos3d[i];double fx = _K(0,0);double fy = _K(1,1);double cx = _K(0,2);double cy = _K(1,2);double X = pos_cam[0];double Y = pos_cam[1];double Z = pos_cam[2];_jacbianOpluXi<<.....;//2×6的偏导数}virtual bood read(istream &in) override {}virtual bood write(otream &out) override {}//这里是要和构造函数在一块写的
private:Eigen::Vector3d _pos3d;Eigen::Matrix3d _K;
}

——————————
3、配置g2o
我们必须让g2o适配于我们当前的问题,采用的是形象的make_unique 指定2个类型
(1)BlockSolver类型,也就是我们要求解的参数块的性质;
(2)LinearSolver类型,也就是我们要采用什么方法来求解当前问题;
对于(1),通过指定BlockSolverTraits的模板参数来设置。这一点看了网上的回答也没有看懂。有一个大佬的回复是https://www.zhihu.com/question/337617035/answer/767355216,这两个模板参数分别代表一条边对应的两个顶点的维度。SLAM十四讲里面,说第二个参数指的是优化变量维度。在几种特定的情境下验证一下对不对吧:首先是曲线拟合,当时给出的是模板参数是<3,1>,表示一头连着优化变量abc,三维,一头连着因变量y,1维;但是到这里来的话明显有问题,因为这个里面误差变量明显是因变量y,也就是像素坐标(u,v),,这个是在_error私有成员里面限定好的,按照这个理论,这里的写法应该是<6,2>,但是实例程序给出的注释是<6,3>,位姿优化变量是6维,3表示的是路标点的[X,Y,Z]。但是XYZ和pose同时处在模型的一边啊,也就是T*pos_3d[i];
所以我的理解倾向于(当然不一定对,只能说符合这两种情况的条件),这里的第一个模板参数表示优化变量维度,第二个模板参数表示的是自变量的维度。这里的自变量是[X,Y,Z]。
对于曲线拟合,自变量维度就是1,因为只有一个x。代码如下(示例):

typedef g2o::BlockSolver<g2o::BlockSolverTraits<6,3>> BlockSolverType;
typedef g2o::LinearSolverDense<BlockSolverType::PoseMatrixType> LinearSolverType;
auto solver = new g2o::OptimizationAlgorithmGuassNewton(g2o::make_unique<BlockSolverType>(g2o::make_unique<LinearSolverType>()));
g2o::SparseOptimizer optimizer; //generate the graph
optimizer.setAlgorithm(solver);
optimizer.setVerbose(true);//打开控制台输出

——————————
4、将顶点和边加入到图模型中
添加顶点的时候:
(1)设置编号;(2)设置估计类型;(3)添加顶点

VertexPose *v_pose = new VertexPose();
v_pose->setId(0);
v_pose->setEstimate(Sophus::SE3d);
optimizer.addVertex(v_pose);

添加边的时候,要逐对数据点(X,y)添加:每条边要设置好:
(1)Id;
(2)和哪些顶点相连?
(3)观测值是什么?(y)
(4)信息矩阵——和观测值的维度平方一致

int index = 1;
for(int i=0;i<pts_3d.size();i++){auto p2d = pts_2d_eigen[i];auto p3d = pts_3d_eigen[i];//construct the edge;EdgeProjection *edge = new EdgeProjection(p3d,K_eigen);edge->setId(index);edge->setVertex(0,v_pose);edge->setMeasurement(p2d);edge->setInformation(Eigen::Matrix2d::Identity());optimizer.addEdge(edge);
}

——————————
5、指定迭代次数等参数,进行求解
(1)初始化优化器;
(2)进行优化

optimizer.setVerbose(true);
optimizer.initializeOptimization();
optimizer.optimize(10);
pose = v_pose->estimate();
cout<<"The pose estimated by g2o is\n"<<pose.matrix()<<endl;

总结

本文会不断的进行更新,后期会引入更多的BA问题,欢迎大家与我探讨,争取坚持每周一更吧,也算是对自己的一个激励,顺便也让自己不要那么沉迷于手机。

SLAM编程:优化问题求解(1)_程序设计相关推荐

  1. SLAM Cartographer(18)后端优化问题求解器

    SLAM Cartographer(18)后端优化问题求解器 1. 全局优化 2. 提供数据 3. 后端优化器 4. 求解过程 4.1. 定义优化问题 4.1.1. 子图全局位姿 4.1.2. 节点全 ...

  2. 计算机算法对程序设计的作用,算法计算机论文,关于数学算法对计算机编程优化相关参考文献资料-免费论文范文...

    导读:本论文主要论述了算法计算机论文范文相关的参考文献,对您的论文写作有参考作用. (重庆人文科技学院 理工学院数学系,重庆 401572) 摘 要:数学算法是一种将很多问题进行归纳总结,然后采用统一 ...

  3. SLAM图优化g2o

    SLAM图优化g2o 图优化g2o框架 图优化的英文是 graph optimization 或者 graph-based optimization, "图"其实是数据结构中的gr ...

  4. SLAM编程:坐标变换

    SLAM编程:坐标变换 前言 一.SLAM中涉及的坐标系有哪些? 二.如何理解坐标变换? 三.代码中的实现 1. 典型SLAM工程当中的坐标变换理解 2. 求解两个坐标之间的距离 总结 前言 要写一个 ...

  5. 激光SLAM后端优化总结之卡尔曼滤波

    激光SLAM后端优化总结之卡尔曼滤波 一.贝叶斯滤波 1.1 状态估计模型 1.2 公式推导 二.卡尔曼滤波(KF) 三.扩展卡尔曼滤波(EKF) 一.贝叶斯滤波 1.1 状态估计模型 1.2 公式推 ...

  6. 切割优化模型可以用c语言编程不,数学算法对计算机编程优化研究

    数学算法是一种以数学模型为基础的理论知识,能够对计算机编程中的问题进行归纳总结和统一计算,以提高逻辑应用的效率,它是计算机编程的基础.计算机编程是从数学模型开始的,首先要根据具体问题进行抽象,以建立一 ...

  7. 交通物流模型 | Python建模实现动态交通分配优化问题求解

    文章目录 效果一览 文章概述 研究内容 程序设计 参考资料 效果一览 文章概述 交通物流模型 | Pyomo建模框架实现动态交通分配优化问题求解,DTA 交通分配问题通常需要考虑许多因素,例如道路容量 ...

  8. SLAM后端优化之-核函数

    1.核函数作用:保证每条边的误差不会大的没边,掩盖掉其他的边 在SLAM后端优化中,BA优化了所有的相机姿态和所有路标点,使用的最小化误差项作的二范数平方和作为目标函数:当我们的误差来源特别大的时候: ...

  9. seo优化时网站_选择关键词的方法

    网站优化中关键词的选择和布局很关键,直接影响网站的流量.选好关键词是第一步,接下来怎么布局也很重要,今天小编带大家分享一些技巧. 1.根据内容选择关键词 在选择关键词之前,我们应该首先确定网站的内容并 ...

最新文章

  1. Scala和范畴论 -- 对Monad的一点认识
  2. 功能点分析:商品类目表
  3. AI时代的高科技读心术:算法解码脑中图像
  4. CentOS6.5下安装JDK
  5. Ubuntu“无法解析或打开软件包的列表或是状态文件”的解决办法。
  6. Unity3d Material(材质) 无缝拼接
  7. JBoss - 调整JVM内存 -Xms512m -Xmx1024m
  8. mysql5.7.28升级到5.7.29_MySQL升级5.7.29
  9. finereport 格式化金额函数_帆软报表常用函数总结
  10. healthkit简介
  11. python爬取有声小说_python写的有声小说爬虫
  12. 程序员增加收入的实用之道
  13. postgresql 数据库中 like 、ilike、~~、~~*、~、~*的含义
  14. js赋值改变后,原数据也发生改变
  15. linux绝育玩客云_玩客云实用指南(真·无痛绝育),附玩物下载对比
  16. strcpy 和strncpy 的代码和区别
  17. NDK开发入门终极教程
  18. 你要的项目复盘都在这里,赶紧学习
  19. Linux:《gzip》《bzip2》压缩解压
  20. j2ee常用工作流比较(shart、osworkflow、jbpm)

热门文章

  1. 紧张 + 刺激,源自一次 OOM 历险
  2. 1086 就不告诉你 (15分) PAT乙级真题
  3. HTML img 标签 二维码显示
  4. 【机器学习系列博客】1. 维度的诅咒
  5. vue项目中实现浏览器全屏 - screenfull
  6. ITK学习笔记(十二) SimpleITK获取二值图像bbox
  7. 灵飞经4·西城八部 第十六章 风流云散 5
  8. 3D开发-全景技术方案
  9. phpstudy连接数据库;启动 MySQL 后立即停止;数据库连接失败; http://localhost:8080/
  10. oracle rank 语法_Oracle-- (RANK) 排名函数