像设计CPU一样设计算法
之前两篇文章中,我们介绍了深度学习算法用于无人机的检测以及可用于无人机视觉与控制算法研究的仿真平台。今天我们要来看,无人机算法的开发与部署的过程中,会遇到的问题与解决方案。
在进入主题之前,首先问大家一个问题。
1. 修理收音机容易还是修理电脑容易?
没有接触过收音机的同学可能会说,收音机比电脑简单多了,肯定是收音机好修理啊。其实不然,收音机内部都是电阻、三极管、电路板等,甚至一些收音机内都是贴片元件。若是没有足够的专业知识以及合适的修理工具,一般人什么都做不了,更不用谈及修理。
相反的,电脑的复杂程度远高于收音机,但是一般性电脑的修理却是比收音机要容易。若是内存卡出了问题,只要更换内存卡就可以;若是硬盘出了问题,只要更换硬盘就可以。一句话来总结,哪里坏了换哪里,只需要简单的插拔即可。
2. 高内聚、低耦合
在软件工程里面,上述电脑的这种“易插拔”称作“高内聚、低耦合”。同时,我们还要指出,像电脑内的CPU这类产品,所有电脑都在使用,但是却不用知道其内部实现细节,在主板上只要简单的“插上”就能使用。这是因为,每个主板都会预留与CPU针脚的插槽,无论是何种CPU都遵循这个统一的接口来设计。所以CPU的设计是基于“接口”的设计,而不是针对“实现”的设计。

这个思想也同样适用与软件设计与算法设计。先举一个反面例子,通常习惯用“面向过程”编程的同学,在开发中常遇到这种情况,修改一处的代码会导致其他地方的代码也出错,像下面的动图所示。所以在程序员也常被戏称,一直在“写Bug”。其实,这就是“收音机”式的编程。各个模块相互依赖,难以维护。而我们想要的是“CPU”式的设计,编写的算法能够像CPU一样,只需要简单的“插拔”就能实现算法的修改、部署、升级,而不影响系统其他模块。

那什么是“CPU”式的编程或者算法开发呢?这就需要用到面向对象编程与设计模式。接下来,我们将给出一种算法开发的场景,来展示设计模式在算法开发与部署中的妙用。
3. 场景:多种算法,框架类似,但部分实现细节有差异
现在我们需要在无人机上实现不同的控制算法,为简单起见,假定算法需求如下:
l 不同的算法所需的输入不同,例如有的需要获取无人机位置,有的需要获取速度,加速度等
l 不同的算法,控制方式不同,如PID控制法,最优控制法,模型预测法
l 控制输出都需要经过速度限幅,最大速度为5m/s
设计方案A
UML图如下所示

实例代码如下所示,为简化代码,省略了内存管理,且将不同文件的代码整合在一处并省略了源文件的代码。
class ControlAlgorithmA { public: void ObtainControlInput(); // Obtain velocity void Control(); // PID control void VelocityLimit(); // v <= 5 m/s }; class ControlAlgorithmB { public: void ObtainControlInput(); // obtain position void Control(); // optimizing control void VelocityLimit(); // v <= 5 m/s }; class ControlAlgorithmC { public: void ObtainControlInput(); // obtain position and velocity void Control(); // model predictive control void VelocityLimit(); // v <= 5 m/s }; int main(int argc, char* argv[]) { ControlAlgorithmA* algorithm_a = new ControlAlgorithmA(); ControlAlgorithmB* algorithm_b = new ControlAlgorithmB(); ControlAlgorithmC* algorithm_c = new ControlAlgorithmC(); // run algorithm A algorithm_a->ObtainControlInput(); algorithm_a->Control(); algorithm_a->VelocityLimit(); // run algorithm B algorithm_b->ObtainControlInput(); algorithm_b->Control(); algorithm_b->VelocityLimit(); // run algorithm C algorithm_c->ObtainControlInput(); algorithm_c->Control(); algorithm_c->VelocityLimit(); return 0; }
三种算法具有相同的结构,用户在使用三种算法时不得不从头到尾一步步调用算法的各个步骤,并且当速度限幅发生改变,比如降低到3m/s,三种算法都要改动,且容易发生错误。
设计方案B——使用模板模式(Template Pattern)
UML图如下所示

示例代码如下
class ControlAlgorithm { public: void Template() { ObtainControlInput(); Control(); VelocityLimit(); } protected: virtual void ObtainControlInput() = 0; virtual void Control() = 0; void VelocityLimit(); // v <= 5 m/s }; class ControlAlgorithmA : public ControlAlgorithm { public: void ObtainControlInput() override; // Obtain velocity void Control() override; // PID control }; class ControlAlgorithmB : public ControlAlgorithm { public: void ObtainControlInput() override; // obtain position void Control() override; // optimizing control }; class ControlAlgorithmC : public ControlAlgorithm { public: void ObtainControlInput() override; // obtain position and velocity void Control() override; // model predictive control }; int main(int argc, char* argv[]) { // run algorithm A ControlAlgorithm* algorithm = new ControlAlgorithmA(); algorithm->Template(); // run algorithm B algorithm = new ControlAlgorithmB(); algorithm->Template(); // run algorithm C algorithm = new ControlAlgorithmC(); algorithm->Template(); return 0; }
ControlAlogrithm类在这里的作用就好像是提供了前文提到的“卡槽”,A,B,C三种算法就好像不同型号的CPU,虽然型号不同,内部实现不同,但是算法的“接口”是相同的。同时,ControlAlogrithm类在Template方法中定义了算法的骨架,算法的具体步骤则推迟到子类中(除了VelocityLimit,是所有子类都相同的)。模板模式使得子类可以不改变一个算法的结构的前提下对算法进行某些特定步骤的修改。
甚至,我们可以使用“工厂模式”或是“策略模式”(不在本篇文章中详述)将三种算法进一步“隐藏”,客户端甚至只需要“知道”ControlAlogrithm类,而不需要“知道”任意的算法,使得客户端不依赖于任何算法,如下图所示意。“Less Code, Less Bug”,客户端“知道”的越少,“犯错”的概率也就越小。
int main(int argc, char* argv[]) { // run algorithm A Factory* factory; ControlAlgorithm* algorithm = factory->CreateAlgorithm("A"); algorithm->Template(); // run algorithm B algorithm = factory->CreateAlgorithm("B"); algorithm->Template(); // run algorithm C algorithm = factory->CreateAlgorithm("C"); algorithm->Template(); return 0; }
4. 总结的话
通过使用“面向对象”和“设计模式”,我们可以开发并部署”CPU”式的算法/程序,通过基于“接口”的设计实现“易插拔”即“高内聚耦合”的特性,使得我们的算法可以非常复杂但易于维护、复用和拓展。关于更多的设计模式的使用,可以在参考资料中查询。
本期是关于算法的设计。实际上在开发过程中,算法肯定需要不断的修改,如何能快速知道修改完的代码是否能正常运行,算法运算各阶段结果是否准确?谨请关注下一期——算法的测试开发。
参考资料
[1] 程杰. 大话设计模式. 清华大学出版社, 2007.
[2] Gamma, Erich, et al. Elements of reusable object-oriented software. Vol. 99. Reading, Massachusetts: Addison-Wesley, 1995.

本文由西湖大学智能无人系统实验室工程师陈华奔安原创
申请文章授权请联系后台相关运营人员
▌微信公众号:空中机器人前沿
▌知乎:空中机器人前沿(本文链接:https://zhuanlan.zhihu.com/p/453593782?)
▌Youtube:Aerial robotics @ Westlake University
▌实验室网站:https://shiyuzhao.westlake.edu.cn/
