写代码一天,debug一年?
说到这,小编得强调一下,代码层面的问题是不会有人教你怎么去解决的,因为这些问题太具体了,一切都得靠自己来。但是吧,看着几千甚至及万行的代码,在自己搓出来的shi山之中找一个非语法层面的错误,这谈何容易!这时候大家想必更绝望了,自己找又找不出错误,别人又帮不了,那岂不是凉了?
别急,今天小编就来谈谈,怎样快速定位问题,从而对症下药,解决问题。与其坐着绝望,不如自己动手丰衣足食!当然了,对于语法错误或者配置错误导致代码无法运行的,这些根本不算错误,不在本文的讨论之列哦。
一、大致定位问题
大家首先要明确一点,错误不会凭空产生,如果结果与预期不符合,那么在整个过程中肯定存在着大大小小的错误。第一步首先是要定位问题,相信写代码的同学,在开始之前肯定会有一套理论支撑,而代码无非就是把理论落实到了机器上面,最后发现运行结果与自己预期的不一致。好了,大家想想,为什么不一致?因为从理论上来说这个结果应该是A,而代码却给出了B。现在,大家记住这句话,因为我们下面的找错过程都是基于这句话进行的。
从理论推导到代码落地,出现问题的大范围一般有以下几个地方:
a. 输入错误
如果算法提示找不到可行解,那么是不是数据设计的有问题,需要检查一下。这个比较简单。
b. 理论上的错误
你的理论推导不正确,这是最根源的错误。没啥好说的,赶紧去修正理论。那么怎样判断是理论上的错误呢?很简单,你把代码中跑的数据,给手动模拟一遍,如果你坚信理论是正确的话,那么你手工模拟的结果应该也是在预期之内的。如果不是的话,那很有可能就是理论出现了问题。tip:手动模拟数据量不要太大,不然累死你。
c. 代码逻辑出现了错误
这是最常见的,也是最棘手的错误,毕竟咱们都是咱在巨人肩膀上做的研究(大部分理论前人已经给我们证明好了),没见过猪跑,总吃过猪肉,理论上出错的可能性还是比较小的。而并非每个人都是代码老司机,秋名山码神,大多数都是刚入行的新手,稍不留心写漏一个else导致逻辑错误也是大大有可能的。
这方面的原因判断也不麻烦,如果你做了上一步的手工模拟以后,发现手工模拟的结果和理论预期是一致的,那么很大概率是代码的锅。
二、找出问题所在
定位问题是解决问题的第一步,我见过很多人遇到问题,居然原因都不找就坐在屏幕前绝望的。作为科研人,这是非常不可取的,我们要有迎难而上的决心和courage,只要思想不滑坡,办法总比困难多嘛……
对于理论上的问题,基本上你手工模拟一遍以后就能发现那个地方不合理,那么修正一下模型或者推导就行了,比如哪里推导错了一个小数点等等。这个就是根据你做的主题以及个人一些科研经验来修复了,小编也没啥经验。
而对于代码逻辑上的问题,那就是咱们今天要讲的重点了。最有效也是最简单粗暴的做法就是debug!当然这个概念太广了,针对不同的方向有一套不同的法子,比如算法、开发等。小编就着这个运筹算法这个方向的两大类算法(启发式和精确解)给大家介绍一下一些debug的思路。
首先无论是对于什么算法,在初版完成以后,第一件要做的事情是写一个check的程序。这个程序有什么作用呢?第一,检查得出结果中是否所有约束都满足了。如果存在着某个约束没有得到满足,那么你就可以去找找相应的约束处理代码,看看到底是哪里没做到位。第二,根据得出来的解,重新把目标值从头到尾,从上到下,从左到右再算一遍,然后检查重新算的目标值和算法得出来的目标值是否相同。如果不同,那么可能是你目标值的计算出现了问题。
而目标值计算出错这种类型的错误在启发式算法中非常常见,为什么呢?大家可以看看我之前讲过了几期推文:
为了能够实现比较快速评估邻居解,我们很有必要在启发式中记录大量的Delta值,以减少冗余的计算,加快算法的求解速度。如果目标值过于复杂,记录的Delta值过多,难免会出错。最后导致目标值偏离了真实的目标值。因此最后重新验算目标值能够确保启发式算法的结果无误。
而且,对于一个启发式而言,得出的解满足约束,目标值又正确,那么就已经能算没有“错误”了,至于如何提高解的质量……请大家多关注公众号哈。
对于精确式算法而言,它的代码实现是和你的理论推导紧密联系的。不启发式那一套达到相同功能随便怎么来都行,要是你的数学推导搞错了一个小数点,那么就恭喜你,可能要debug上一年了。。。
其实像约束不满足这种错误精确式算法里面还是比较好解决的,而这类算法最麻烦的一个地方是,精确式跑出来的居然不是最优解(通过和CPLEX结果进行对比得知)!
这就比较绝望了,你说你出个bug,比如目标值没算对,或者是约束没做好,或许还能抢救下。你这下直接找不到最优解,茫茫shi山,错误要我从何找起?别急!还没到山穷水尽的地步。
首先,我们从精确算法的本质入手,它的本质是什么?穷举!对,就是穷举,只不过它在穷举的同时,会使用一些dominance rules把一些比较差的分支给干掉,这样在保证最优性的同时又能以比较快的速度找到最优解。那么,我们就顺着这个思路,相比大家的代码中肯定设置了一些dominance rules用来加速的。那么,先把dominance rules给注释掉,或者让他永远返回False,就是不会干掉任何一个解。就相当于full extension,全遍历了。这样是一定能找到最优解的,如果找到了,那么问题简单了,就是dominance rules出了问题。
如果还是没找到。。。那么恭喜你,问题又变复杂了。现在,让我们喝杯茶思考下人生,再想想还有什么办法。这个时候就很有必要上终极大法了--逆向调试法!没错,我们之前不是通过CPLEX得出了问题的最优解吗?(前提是你CPLEX的结果一定要正确,把模型构建出来并调正确,这个应该不是很难吧~)。我们通过观察CPLEX得出的最优解,然后逆向回来看算法的执行流程,仔细想想,为什么我们算法没有得出这个最优解呢?
然后你就回到算法中,看看是哪个部分和解的生成最接近,从那里开始一层一层往上调试。比如再branch and price求解vrp类问题中,CPLEX得出的一条路径中有0-1-2-3-4,而branch and price输出的最优解却没有这个路径。大家想想,在bp算法中路径是通过子问题的labeling algorithm加进去的,我们先在spprc中把每次迭代找到的路径全部输出来,看看是否包含了0-1-2-3-4。如果没有包含,那么就断定是spprc的问题,如果包含了,就想想是不是branch干掉了。。。
不信给大家看一下,为了找出我的labeling中为啥没有找出这条最优解的路径,我在扩展过程中写了如下调试代码:
可真是人肉调试了,不可谓不壮观。。。
三、小结
总之,debug是一门大学问,它的中心思想是定位问题的所在,才能对症下药。定位的过程也应当是先从大到小,再从内到外。所谓从大到小,先大概定位一下错误可能出现在了哪些地方,然后再从内部一层一层往外验证。
这个确实是需要经验,而且需要大量的时间。调试本身也是发现问题,解决问题,偶然的时候还能发现理论上的一些致命错误。因此是一门体力兼脑力活。每次有人问我,代码出现了逻辑上的错误该怎么办?我的回复往往只有一句话:System.out.println();因为这个真的很有用!!!
然后,建议大家不要去看代码,要去System.out.println();即不断的输出,通过输出来发现问题。因为你自己写的代码,写完以后你就给大脑灌输了我的代码一定是对的这种想法,无论你看10遍还是100遍,基本上都很难发现问题。而且代码量这么大,你从头到尾看得过来吗?
每次遇到问题的时候,我思考的不是怎么去解决这个问题,而是怎么去debug这个问题,即:我要输出什么信息,在哪里输出,才能定位到这个问题?想好以后便开始debug了。找出了问题,才能去修复它!所以呀,大家遇到问题一定要立刻马上right now动起来,手动起来,眼动起来,脑瓜子动起来。不然,坐着什么也不干,把他丢在一边,久而久之,这个问题就成了历史遗留问题。若干天后你就再也不想碰他了,这样就前功尽弃了。。。
不过,小编实在是太笨了,本来想把这些方法好好给写一写,后来写着写着就乱起来了,因此大家也就将就着看一看吧。毕竟这东西吧,只可意会不可言传。每个人都有自己的一些经验和法则,小编只是把自己的一些经验分享出来而已。
推荐阅读:
干货 | 学习算法,你需要掌握这些编程基础(包含JAVA和C++)