从初赛(3月30日)结束后到今天也过了近三分之一个四月天了,现在区域复赛正在如火如荼的进行中…
“还有2天就复赛了,好紧张!!”
“你初赛就被淘汰了,复赛和你有什么关系?”
“(ಥ_ಥ)”
是的,这篇博文是菜鸟失败后的自我总结(安慰),初赛在京津冀东北赛区排到了40 ,距离进复赛差了8名,所以晋级的大佬可以点击屏幕上方的“x”键退出这个页面了。
“失败是成功之继母,总结是成功之亲爹”
这场比赛虽然结果不尽人意,但是依旧收获了许多,接下来,文章会通过回顾我们小组在比赛过程中的历程和思路,总结一下得失。
团队协作的竞赛,最好的协作方法自然是使用git版本控制系统了,事实上我们也是这么做的。
Github在被微软收购后,可以免费创建私有代码库了,免费代码库只能拥有最多三名协作者,这对于我们这个三人小队来讲却足够了。
在仔细看过比赛的特别声明(大赛组委会对未入围的作品没有著作权)后,我终于可以放心的将代码仓库设为public了。
源码仓库的地址戳这里 -> secretCode repo
仓库的文件结构是这样的:
mainfile
├─SDK_python
├─CodeCraft-2019
├─config
├─logs
└─src
├─CodeCraft-2019.py
├─Test.py
├─base_class.py
├─testdijkstra.py
└─tools.py
├─CodeCraft_tar.sh
└─build_and_run.sh
1-map-exam-1.zip
1-map-exam-2.zip
1-map-training-1.zip
1-map-training-2.zip
HuaweiAnswer.cpp
.
.
.
secretCode相关文件安排.md
mainfile 里的文件是比赛的主要代码,和mainfile同级的目录里存放着一些与比赛相关的文件、比如说:
mainfile目录下的文件是按照比赛SDK的要求放置的,因为用的是python语言,所以根目录是SDK_python,src里的文件就是我们编写的主要程序文件了,其中:
问:为什么用python?
答:因为我不想秃头
赛题的介绍请看官网的介绍,因为初赛的任务书只有参赛后才能下载,所以不知道放在网上是否侵权,所以就不放在仓库了,大体的情况和上面那个连接差不多。
简单的分析一下赛题:
#(道路id,道路长度,最高限速,车道数目,起始点id,终点id,是否双向)
#(结点id,道路id,道路id,道路id,道路id)
#(id,始发地,目的地,最高速度,计划出发时间)
#(carId,StartTime,RoadId...)
其实不难得出、我们的程序主要有两个部分的难题:
在解决那两个部分的难题之前,我们需要先考虑一下整个程序的结构。
经验告诉我,磨刀不误砍柴工,虽然这个刀磨得也不锋利
首先程序至少有四个实体类——road、cross、car、answer,这几个实体类需要放在一个模块里以供调用,所以我们设置了base_class.py。
因为要输入和输出文本文件,所以至少要有一个工具类专门处理这些事情,所以我们设置了tools.py。
因为要寻路所以需要有一个地图类去处理每一辆车的寻路问题,所以我们设置了基础类-RoadCrossMap,详细的代码请看base_class.py,这个RoadCrossMap会处理所有和地图构建相关事情,拥有的方法也都是和寻路和构建地图相关的。
这是我们最初的想法,也就这样草草的分配了需要的类和函数,根目录的secretCode相关文件安排.md记录了这些想法。
然后就是解决核心的两个问题了
首先是寻路算法的选择、这部分我们查了一些资料、问过一些大佬、也商量了一段日子,我们考虑过这几种寻路算法:
在最短路算法是相对容易实现的、我们可以将车在道路上的理论最快行驶时间当做图中边的权值。
蚁群调度算法和一笔画问题计算路径的时间复杂度会远高于最短路算法,性能会怎样是未知的。
这个时候我打算遵循构建机器学习项目的关键思想:
不拘泥于细节、先用最简单的方法实现、实现后如果发现这是问题导致了性能瓶颈、并且改善它能提高很多的性能(性价比高),那么再花时间想办法改善它。
这个构建机器学习项目的关键思想不是我说的,是吴恩达老师在他的深度学习教学视频中明确提到的,如果感兴趣可以去Coursera的deep-learning页面申请助学金,我很推荐这个课程,它让我获益良多。
吴恩达老师在该专项课程中的课程3-Structuring Machine Learning Projects中有明确提到这个思想。
这种思想对于学术研究来讲也是通用的,这是我有幸听到西湖大学的校长施一公先生的演讲后得到的宝贵经验。
于是寻路算法的选择便转化成了怎么挑选最短路算法的问题了,在重新审视了这几个算法的时间复杂度、和稳定性以后、我最终选择了基于最小堆优化过后的Dijkstra算法。
理论上来说基于最小堆优化过后的Dijkstra算法在图点数为V、边数为E的图中时间复杂度为V * O(logE) ~ V * O(logV)
接下来是这个最短路的实现代码,你可以在tools.py中找到它:
#dijkstra算法,获取单源点到多终点的最短路,并返回最短路字典
def dijkstra_raw(self,edges, from_node, to_node_list):
"""
:param edges: 边集 ——(路头,路尾,权值)
:param from_node:源点
:param to_node_list:终点列表
:return: shortest_path_dict:最短路的字典 {key = to_node : value = (cost,path)}
"""
shortest_path_dict = {
}
g = defaultdict(list) # 定义一个空字典、此字典若没找到key,则返回空列表[]
for l, r, c in edges: # l:源点、r:终点 c:距离
g[l].append((c, r)) # 建立一个字典,元素是源点为key、(距离,终点)为value
q, seen = [(0, from_node, ())], set() # 初始化优先队列、初始化已找到的最短路径集合——————元组(源点到点A的距离,某个点A,源点到点A的路径)
while q: # 优先队列为空时,循环结束
(cost, v1, path) = heappop(q) # 从优先队列中找出与源点距离最小的点
if v1 not in seen: # 若点v1以确定最短路径则无需计算
seen.add(v1)
path = (v1, path)
if v1 in to_node_list: # 如果找到了某一个终点
shortest_path_dict[v1] = (cost, path)
if len(shortest_path_dict) == len(to_node_list): # 如果所有的tonode都找到了
return shortest_path_dict
for c, v2 in g.get(v1, ()): # 更新优先队列,加入与当前点相连的所有点的信息
if v2 not in seen:
heappush(q, (cost + c, v2, path))
return {
"ERROR": (float("inf"), [])}
你希望我一行一行的给你解释?这不可能,因为我自己都快忘了是怎么写的了…
你可能注意到了:
这是最关键的核心问题了,许多的队伍都是因为不知道如何设置车辆的starttime而频繁的发生死锁问题,这在官方论坛中很常见。
我们在这个算法方案上迭代了5次、每次迭代都会复杂一点。
如下图:
这应该是最简单、效率也最低的方案了,每辆车的出发时间是前面所有车行驶时间的综合、行驶时间是最短路 / 理论最高速。
这其实不算方案,算是我们测试程序的其他模块是否正确的黑盒测试,让我们确保了除了调度算法以外,其他的模块都运行正常。
比方案一进步了一点点、我们一个路口、一个路口的跑…
有如下规则:
如果你仔细阅读了源码,你会发现,在tools.py中的classify_of_diff_ori_car方法返回的字典中,路口的顺序是怎样的,那么路口优先级的顺序就是怎样的,这让本方案的路口优先级似乎又有规律可循,不过当时没有仔细的思考这个问题,因为我们从不认为这是我们的最终方案。
说出来你可能不信,除了增加了一些细节以外,这个方案就是我们初赛的最终方案。
这不是后面的方案不行,而是因为一些其他的问题,这在最后总结中会给出详细的说明。
方案三又比方案二进步了一点点、因为我们手动为每一个路口设定优先级了。
除了路口优先级的设定、前两个规则和方案二没有区别。
路口优先级是这样设定的:
首先、我们默认由road、cross文件生成的地图都是方形、这是由官网的介绍中的路口数据文件描述推理得到的、但是有可能在方形地图的中间会有一些缺失的路口。
那么地图的数据就可以转换成一个二维矩阵了、不管实际的地图是长方形还是正方形。
优先级规则:
解释:
上图被红色框框和绿色框框夹住的路口(下面简称层级一)优先级最高、绿色框框和黄色框框夹住的路口(下边简称层级二)优先级次之。
在层级一中、红色标注1路口和红色标注2路口是一同发车的。
过了一个同层级延迟以后、红色标注3的路口和红色标注4的路口再一同发车。
以此推类、层级一中的绿色标注7和绿色标注8也是一同发车。
当第一层级的路口全都发完车以后、过了一个层间延迟以后、第二层级的车就可以发车了、例如蓝色标注11和蓝色标注12是层级二中最早一同发车的。
可以注意到、有两个参数需要调试:
1. 同层级延迟
2. 层间延迟
经过调试后、这个方案比方案二并没有快多少,这有可能是两个路口一起跑还是大大的增加了死锁的概率,经过我们的分析后得到了方案四。
你应该注意到了,方案三那个图中、红色标注3的路口(后面简称路口3)离红色标注1的路口(后面简称路口1)很近、但是路口3只比路口1晚发车了一个同层级延迟
同样的,图中id为7的路口(后面简称路口11)、和蓝色标注11(后面简称路口11)很近、但是路口11只比路口7晚发车了一个层间延迟
同样这种情况在地图中的很多相邻路口之间都发生着
有着上述情况的相邻路口、他们之间的车道会变得相当拥挤、发生死锁的概率大大的提高了!
所以方案四很大程度上避免了这种情况的发生。
优先级规则的前两条和方案三一致,只是同一层级的优先级设置规则改变了
上图用红色标注、标注了12个路口、他们就是按照这个顺序发车的。
下面说的路口x,指的是标记为x的路口
当路口1的有优先级确定以后、然后确定优先级的是路口2、路口3、路口4、他们之间各自推迟了一个同层级延迟。
随后确定的是路口5的优先级、然后依次是路口6、路口7、路口8
…以此推类…
基本上是左上角、右下角、右上角、左下角的顺序(先这么概括吧、想不到如何准确描述了)
这样的优先级设置能够延缓方案三带来的高概率死锁问题。
实际上、方案四比方案三快了三分之一。
我们用方案四在当时的京津冀赛区排到了第15名。
然而,我们在路口优先级设置的相关的上已经黔驴技穷了
终于我们想到了研究地图数据(结合road数据、cross数据和car数据)、根据地图的规律来改进我们的方案。
这本应该在最开始就做的步骤却被我们拖到了现在、这对学过机器学习方法和深度学习方法的我们是很不应该的。
研究训练赛的数据以后,我们得到了如下几条规律
先放图:
以中间那条红色的中轴线分分割,左边的路口发的车,目的地总是在右边。
具体的规律是:
这样来讲、左边几乎所有的路口发出的车、都必定有车到路口64;右边几乎所有的路口发出的车、都必定有车到路口1。
综合以上所有规律、我们发现之前的方案设置的优先级方案很蠢。
所以我们打算从目的地路口入手计算每个路口的优先级。
因为时间问题、我们只写了一个简单版本的、因为过于简单我就不解释了、你可以去tools.py中的方案五的注释下查看源码。
其实是更改后效率没有提升太多、我不想写了
因为时间的关系、我们最后没有选择继续优化路口优先级算法、而是选择了优化寻路算法
“一笔画”寻路算法的可行性讨论:
一笔画算法从理论上来讲是不会发生死锁的
因为一笔画算法在地图上画了一个环、这个环经过了所有的路口。
所以我们只要定下一个固定的方向(顺时针/逆时针)、那么所有的车都在这个环上按照一个方向行驶、
同一个地点的所有车、他们的方向一致、这就避免了死锁问题。
但是有如下弊端:
所以,这个算法的瓶颈在于图中有多条单车道的道路
如果去掉所有单车道的道路、那么就不一定能画出一个能够经过所有路口的环了、这也是我们放弃了一笔画算法的原因。
有没有可能再次优化最短路算法呢?
答案是肯定的,研究了Dijkstra生成的最短路后,我发现一个情况,见下图:
前提:
从路口44到路口53有两种方式:
1. 44 -> 52 -> 53 (红色路线)
2. 44 -> 45 -> 53 (绿色路线)
Dijkstra算法得到的是红色的路线、可以看到他经过了5087道路、这是一个车道数为1的道路。
而绿色的路线经过了5073道路,这是一个车道数为4的道路。
经过计算、我们发现绿色路线比红色路线长不了很多、但是因为车道数较多,所以更不容易发生拥堵、也就更容易预防死锁问题。
所以我们基于这个规律再次修改了最短路上的权值、增加了车道数的因素、车道数越多的道路权值比同样长的车道数少的道路要低一些。
具体低多少根据参数决定
这就很大程度的缓解了因为车道数少而引发的拥堵问题。
至此,我说完了我们的所有解决方案 + 优化
感谢你看到这里!因为太长,我自己都看不下去了
正式初赛的那一天、在见到初赛数据的那一瞬间,我们傻眼了。
因为路口文件中的路口ID是随机的!!!!
这意味着我们生成路口位置矩阵的方法失效了,因为训练赛的数据都是按照一定规律排列的。
如果没有路口位置矩阵,那么我们只能使用效率很低的方案二了,然而这不能够确保我们晋级。
事实上,我们根据现有情况紧急写了新的生成路口位置矩阵函数(见tools.py的compute_cross_location_matrix函数)
但是,到最后这个函数也没有调试完成,只完成了函数的主体。
我保证,如果完成它,我们还是有希望晋级的。
到现在我也没去完善它,没有必要了