现代软件工程 结对编程 黄金点游戏

10/11-10/16日短短五天,我和队友经过结对编程的方式完成了一个用来作“黄金点游戏”的小程序,项目地址:python

 https://github.com/ycWang9725/golden_point.gitgit

黄金点游戏的基本规则

假设有M个玩家,P1,P2,…Pmgithub

在 (0-100) 开区间内,全部玩家自由选择两个正有理数数字提交(能够相同或者不一样)给服务器,算法

假设提交N11,N12,N21,N22,Nm1,Nm2等M*2个数字后,服务器计算:(N11+N12+N21+N22+…+Nm1+Nm2)/(M*2)*0.618 = Gnum,获得黄金点数字Gnum编程

查看全部玩家提交的数字与Gnum的算术差的绝对值,值最小者得M分,值最大者扣2分。其它玩家不得分小程序

此回合结束,进行下一回合,多回合后,累计得分高者获胜。服务器

咱们的bot收到的输入是游戏当前轮和历史全部轮次的Gnum和全部玩家的预测值,输出两个对下一轮黄金点的预测值。数据结构

PSP表

在任务要求下达的当天晚上,我与队友两人便开始了讨论、设计与编码。app

PSP各个阶段 预估时间 实际记录

计划: 明确需求和其余因素,估计如下的各个任务须要多少时间dom

 

 30  20

开发(包括下面 8 项子任务)

 

 885  875
  •  需求分析
 120  60
  •  生成设计文档
 10 20 
  • 设计复审 
10  10 
  • 代码规范 
 5
  • 具体设计 
20 20 
  • 具体编码 
600 600
  • 代码复审 
 -
  • 测试 
 120 180
 报告  150 150 
  •  测试报告
 -
  • 计算工做量 
 30 30 
  • 过后总结 
 120 120 
总共花费时间   1065  1045

因为采用结对编程的工做方式,咱们没有计划也没有经历代码复审阶段。需求分析阶段的预估时间和实际时间较长,是由于我与队友两人对强化学习(Q-learning方法)并不熟悉,所以将查找参考资料和学习的时间算了进去。咱们的具体编码阶段并不是彻底用于编码,事实上,其中有大约2/3的时间在修改和验证算法的策略,咱们将这些时间均算做此项。因为程序中bug大多在编码和验证过程当中发现并修改,在测试阶段检测并修改的bug很是少,咱们没有撰写测试报告。

咱们的bot

  1. get_numbers.py做为程序的输入输出接口,将程序外输入的历史数据传给Q-learning和IIR两个模型,获取两个模型的预测并根据两个模型的历史预测成功率选择其一输出。
  2. Q-learning模型(QLearning.py)。此模块维护一个Q-table,每步根据上一步的Gnum和Q-table获得几率最大的下一个Gnum,用softmax方法引入噪声后做为此模型对下一轮的预测,并根据当前轮Gnum更新Q-table。
  3. IIR滤波器(iir.py)。此模块根据游戏的历史黄金点对Gnum曲线进行滤波,获得平滑后的结果引入均匀分布的噪声后做为此模型对下一轮的预测。

 get_numbers.py内部未定义类,包含的函数及功能:

函数 功能
LineToNums(line, type=float) 处理输入的历史数据行,获得能够被get_result()使用的数据结构
get_result(history) 将history数据输入到两个model之中,分别接收两个model的输出,用随机数的方式选择一个交给main()函数,并根据两个模型预测值的排名更新选择模型的阈值
main() main()函数调用前两个函数,做为与外界的输入输出接口

 

QLearning主要含一个class QLearn,包含的函数及功能(较为重要的用红色标出):

函数 功能
__init__(self, load, load_path, test, bot_num, n_steps, epsilon_th, mode, rwd_fun, state_map, coding, multi_rwd) 初始化一个QLearing对象,根据输入的参数在npy文件中load或初始化16个变量,它们被用来保存q-table, model预测的随机程度, model计算reward和rank的模式等
update_epsilon(self)  每次调用将会更新epsilon,若在预测中使用epsilon,则减少model输出随机数的可能性
next_action(self, all_last_actions, curr_state)  输入上一轮的Gnum和全部玩家的预测,调用uptate_q_table, update_epsilon, prob2action等函数,更新model的参数并输出model的预测值
prob2action(self, prob)  根据model的不一样模式,输出由q_table和上一轮Gnum决定,加或不加softmax噪声的Q-table的action
action2outputs(self, last_action, last_gnum) 将Q-table的action译码成输出值 
update_q_table(self, all_last_actions, curr_state)  在不一样模式下调用calculate_reward或calculate_multi_reward计算模型的reward,并借助Q-learning的更新公式更新q-table
calculate_reward(self, all_last_actions, curr_state)  根据上一轮全部玩家的输出和Gnum,计算本身的排名,调用my_rank2score产生得分做为reward
rank2score(self, rank, n_bots)  输入一个rank,根据模式的不一样生成soft/norm-soft/soft-hard-avg类型的score
my_rank2score(self, my_rank, n_bots)  调用rank2score,返回输入的rank list产生的score list
gnum2state(self, gnum)  根据模式的不一样,将输入的Gnum用不一样的方式编码成Q-table的state
calculate_multi_reward(self, all_last_actions, curr_state)  根据上一轮全部玩家的输出和Gnum,计算Q-table一个state的全部action的rank,因为action较多,采用了与calculate_reward中不一样的计算排名的方法,调用my_rank2score产生得分做为reward
random_softmax(self, vector)  用softmax的方法对输入的Q-table行进行加噪声的取Max,返回Q-table的action编号

 IIR主要含一个class IIR,包含的函数及功能:

函数 功能
__init__(self, alpha, noise_rate) 初始化一个QLearing对象,在npy文件中load或初始化self.mem变量,它被用来保存Gnum的历史平滑值
get_next(self, last_gnum) 根据输入的上一轮Gnum和self.mem,计算出Gnum的滑动平均值并加均匀噪声输出

 

每次调用get_numbers.get_result()都将建立一个QLearing对象和一个IIR对象。而后分别调用QLearn.next_action()和IIR.get_next()获得两个输出,再根据历史表现二选一输出。

除此以外,咱们还编写了几个用于测试的模块:test.py, plot_all.py, view_npy.py

咱们的算法主要依赖于Q-table的学习,所以咱们为它加了几个trick。

  1. 每次更新Q-table时,更新同一个state的全部action的几率。
  2. 将Q-table的state改成0.01-50的对数编码,所以在黄金点游戏极可能收敛到小值的前提下,固定state数目的Q-table能够得到更大的纵向精度
  3. 将Q-table的action改成0.3-3的对数差分编码,使得Q-table在同一个state下得到更大的横向精度,也使model的输出保持在上一轮黄金点的0.3-3倍范围内,保证了输出的稳定性。
  4. 不一样于黄金点游戏的“赢者通吃”的得分规则,咱们为Q-learning设计的reward是soft-reward,便是说让玩家的排名与获得的reward成线性关系,第一名得M分,最后一名得-2分,其他名次也能够获得(-2, M)的不等的分数。

事实上,除了上面的setting,咱们还尝试了不一样的更新策略、编码策略、reward策略,从中选择了表现最好的做为QLearning model的最终版本。

另外,在测试中咱们发现,虽然为精度作了改进,但固定state和action的QLearning model仍是没法很好地handle随着游戏进行Gnum收敛到波动范围在1%如下的状况,所以为了保证此类状况下bot的适应性,咱们添加了一个IIR模型。IIR模型输出历史Gnum的滑动平均±最近十个Gnum的标准差区间内的均匀分布的随机数。咱们在get_numbers.get_result()中设置了一个选择阈值,根据根据两个模型的上一轮预测是否进入前三名来更新阈值,并做为选择QLearning model/IIR model的依据。

UML

 

 

Code Contract

契约式设计的好处在于:可以将前提条件和后继条件以及不变量分开处理,明确了调用方和被调用方的权利与义务,避免了双方的权利或义务的重叠,有助于使得总体的代码条理更清晰、功能划分更明确、避免冗余的判断。在结对编程中,假设须要我和队友各自写具备相互调用关系的类时,双方都会在函数最开始用assert指出必须知足的前置条件,并对后继条件不符的状况进行异常处理,同时对于不变量进行检查。

代码规范

程序的代码规范主要是依靠Pycharm的内置代码规范。在结对编程活动的一开始,咱们肯定了使用python语言以后,就发现咱们都很是喜欢Pycharm的代码规范,所以马上肯定了下来。

关于设计规范,咱们尽可能作到最小单元设计,即每一个函数仅完成一项工做。这样写在咱们后期调试模型的模式、添加/删改trick的时候显示了巨大的优点,让咱们在修改时可以专心于算法的策略而非一边想算法一边重构代码。

因为本次结对编程任务比较单一,可能发生的异常状况也比较少,所以咱们的异常处理主要是针对咱们程序输出的结果加了保护使其在合规范围内,还有在测试过程当中的异常处理,这个咱们并无花不少心思,只是pass掉了。

界面部分

本次结对编程任务主要是脚本输入输出,咱们并无为其适配UI,所以此部分略去。

结对的过程

Day 1

下午接到任务,两人都感到棘手,因而一块儿在吃饭的时候讨论了一下,当晚就开始了工做。

因为咱们对强化学习并不熟悉,因而先一块儿学习了一下Q-learning的思想和算法。而后咱们规划了一下接下来的工做方向,决定先实现最基础的Q-learning,state和action的都只有0-100均匀分布的100个,而后让咱们的bot之间比赛,选择其中表现最好的。当时我和队友两人针对在实际比胜过程中是否须要实时train bot意见稍有不一样,因而咱们搁置分歧,决定先实现基本算法再说。这个问题在结对编程进行时天然而然地解决了,由于比赛时地Gnum走势与本身训练时不可能彻底相同,因此放开让算法本身学习才是最好的。

而后咱们开始设计实现Q-learning的伪代码,并将其转化成QLearn class的函数名(。。。)。这以后,咱们试图将这些函数写满,实现最基本的Q-learning算法,可是时间有限只写了一部分就结束了。中间咱们还尝试使用了模拟复盘程序。

Day 2

咱们在这天晚上埋头工做四小时,实现了最基本的Q-learning算法。在使用模拟复盘程序查看效果的过程当中,咱们发现结果不好,但用C#编写的模拟程序调用咱们的脚本,debug十分不方便,因而用python编写了test.py和view_npy.py来debug和查看Q-table。

然而,当咱们作完这些后,仍然发现咱们的bot仅仅停留在能够运行的状态,在模拟复盘程序中的表现十分辣眼。因为时间太晚,咱们决定回去休息,周日再战。

Day 3

与联培班的其余同窗奋斗在北京城郊另外一战场。

Nothing Done.

Pass

Day -2

午餐后开始工做,因为个人公司的电脑断网,而我在周五晚上没有将代码传到云端或共享给队友,致使咱们足不出户调算法的梦想几乎破灭,赶忙去公司共享给队友,再回去调。吸收此次教训,我决定之后每次工做完成都将代码共享给队友(然而并无用,由于个人电脑老是断网,事实上后来的工做都是在队友的电脑上完成的)

依照最初的计划,咱们先build了是个QLearn bot,让它们比赛。输出结果后发现txt形式的log肉眼是观察不出太多规律的,因而咱们编写了plot_all.py脚原本观察每一个bot的表现。

今后咱们就开始了漫长的调QLearn模式的过程。

咱们让本身的bot比了好多场,终于选出了一个效果最好的,决定让它到模拟复盘中跑一下。随着程序上得分的飞速跳跃,咱们的心情很是复杂。

 

 。。。

此时已经很晚了,咱们赶忙debug查看Q-table,发现咱们的Q-table更新过的值太少、太离散了,致使在比胜过程中咱们的bot基本在瞎猜,很不靠谱,并且因为均匀分布在0-50的100个state和均匀分布在0-100的100个action,即便咱们猜对了精度也只在小数点之前。

 

既然如此,优化算法势在必行。通过讨论,咱们决定在两个方向优化算法:

  • 模拟历史学习,增大Q-table更新的范围
  • 对数编码Q-table,增大精度

有了优化方向,咱们心情轻松下来,也感到了疲惫,因而决定明天再继续。

Day -1

这天咱们感到很是紧张,晚上八点钟左右开始工做,一直作到凌晨两点(这里向被吵到的我和队友的室友表示歉意)。咱们将以前想到过的全部想法都实现并尝试了,以致于QLearn class的初始化须要11个参数,若是咱们当时还理智的话,就把他们包起来了,但当时咱们两人都很感到很急,因此没有心思作这一点。仍是须要改进,应该从init函数须要两个参数的时候就开始打包。

这晚咱们的bot终于可以在模拟复盘程序中得分了,但咱们分析提供的两轮数据发现,第一轮数据的Gnum收敛到波动<1%,而第二轮数据的波动能够超过100%,咱们的bot在第二轮数据中表现不错,第一轮数据中的表现就差强人意。分析缘由,虽然咱们在Q-table上使用了对数编码,可是在黄金点收敛得数值较大且波动很是小时,精度仍然不够。此时已经是凌晨一点多,咱们决定临时加一个简单的滑动平均模型用来与Q-learning互补。

Day DDL!

当天咱们发现了好几个bug,差一点就致使比赛的时候咱们输出随机数了。。

下午调整好程序,保存了稳定版,其实又冒出了一些新想法,不过简单尝试后效果很差,也没时间优化了,就提交了稳点版。

比赛!

上半场比赛中咱们的bot表现差强人意,中盘时咱们观察别人的输出和黄金点的走势,老是不得要领,后来队友打开咱们的Q-table发现,因为比赛数据与模拟数据的区别,咱们的Q-table不能区分度很小,因而咱们在softmax时增大了指数倍数(代码中*150的部分),使得输出的结果更接近于Q-table最大值所在的action。

 

def random_softmax(self, vector): mean = np.mean(vector) vector = vector - mean vector = np.exp(vector * 150) sum_exp = np.sum(vector) vector = vector/sum_exp accumulate_vector = 0 random = np.random.rand() for i in range(np.size(vector)): accumulate_vector += vector[i] if random < accumulate_vector: return i return np.size(vector) - 1

 

下半场开始几十轮后咱们的bot的表现像咱们的预期同样开始走高,最终拿到第一名。我和队友都很开心。

 

总结此次结对编程,我感到收获很是大:

  • 首先,个人队友很是优秀,我在与他合做过程当中学习到了不少东西,例如在算法中使用数学思惟,例如一些python工具的使用,例如代码设计,还有一款好用的文件管理app和一家味道不错的外卖店。
  • 其次,此次的编程任务让我学习到了之前没有接触过的强化学习,而且实践了Q-learning算法,开阔了解决问题的思路。
  • 并且,虽然此次结对编程过程当中,我更多的时候是担任“领航员”的角色,代码主要是由队友敲出来的,可是我也深入地体会到告终对编程实时复审代码的高效,和两人合做解决问题的奇妙之处

若是说结对编程有什么缺点,那就是在咱们尝试是个bot比赛时跑一次须要20分钟,那时咱们两人要一块儿干等,有点浪费时间。

讲一讲我认为的咱们两人各自的优缺点:

我:

  • 比较擅长查找参考资料
  • 对代码的检查比较仔细
  • 能够很快地想到现有模型的缺点
  • 缺点:python代码量不够,numpy等包不是很熟,实际编码速度极慢
  • 缺点:存在懒惰心理

队友:

  • 代码、算法能力强
  • 数学底子好,有将数学应用到算法中的思路
  • 有把事情作到最好的习惯,不知足于中庸的结果
  • 缺点:彷佛不太愿意为编码的问题上网搜索,基本的数学函数没有用过会想要本身实现

 

 

 

 

学习Q-Learning过程当中的参考资料

[1] CJCH Watkins, P Dayan, Q-learning, Machine learning, 1992 - Springe

[2] https://baijiahao.baidu.com/s?id=1597978859962737001&wfr=spider&for=pc(机器之心:经过 Q-learning 深刻理解强化学习)

相关文章
相关标签/搜索