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和全部玩家的預測值,輸出兩個對下一輪黃金點的預測值。數據結構
在任務要求下達的當天晚上,我與隊友兩人便開始了討論、設計與編碼。app
PSP各個階段 | 預估時間 | 實際記錄 |
計劃: 明確需求和其餘因素,估計如下的各個任務須要多少時間dom
|
30 | 20 |
開發(包括下面 8 項子任務)
|
885 | 875 |
|
120 | 60 |
|
10 | 20 |
|
10 | 10 |
|
5 | 5 |
|
20 | 20 |
|
600 | 600 |
|
- | - |
|
120 | 180 |
報告 | 150 | 150 |
|
- | - |
|
30 | 30 |
|
120 | 120 |
總共花費時間 | 1065 | 1045 |
因爲採用結對編程的工做方式,咱們沒有計劃也沒有經歷代碼複審階段。需求分析階段的預估時間和實際時間較長,是由於我與隊友兩人對強化學習(Q-learning方法)並不熟悉,所以將查找參考資料和學習的時間算了進去。咱們的具體編碼階段並不是徹底用於編碼,事實上,其中有大約2/3的時間在修改和驗證算法的策略,咱們將這些時間均算做此項。因爲程序中bug大多在編碼和驗證過程當中發現並修改,在測試階段檢測並修改的bug很是少,咱們沒有撰寫測試報告。
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。
事實上,除了上面的setting,咱們還嘗試了不一樣的更新策略、編碼策略、reward策略,從中選擇了表現最好的做爲QLearning model的最終版本。
另外,在測試中咱們發現,雖然爲精度作了改進,但固定state和action的QLearning model仍是沒法很好地handle隨着遊戲進行Gnum收斂到波動範圍在1%如下的狀況,所以爲了保證此類狀況下bot的適應性,咱們添加了一個IIR模型。IIR模型輸出歷史Gnum的滑動平均±最近十個Gnum的標準差區間內的均勻分佈的隨機數。咱們在get_numbers.get_result()中設置了一個選擇閾值,根據根據兩個模型的上一輪預測是否進入前三名來更新閾值,並做爲選擇QLearning model/IIR model的依據。
契約式設計的好處在於:可以將前提條件和後繼條件以及不變量分開處理,明確了調用方和被調用方的權利與義務,避免了雙方的權利或義務的重疊,有助於使得總體的代碼條理更清晰、功能劃分更明確、避免冗餘的判斷。在結對編程中,假設須要我和隊友各自寫具備相互調用關係的類時,雙方都會在函數最開始用assert指出必須知足的前置條件,並對後繼條件不符的狀況進行異常處理,同時對於不變量進行檢查。
程序的代碼規範主要是依靠Pycharm的內置代碼規範。在結對編程活動的一開始,咱們肯定了使用python語言以後,就發現咱們都很是喜歡Pycharm的代碼規範,所以馬上肯定了下來。
關於設計規範,咱們儘可能作到最小單元設計,即每一個函數僅完成一項工做。這樣寫在咱們後期調試模型的模式、添加/刪改trick的時候顯示了巨大的優點,讓咱們在修改時可以專心於算法的策略而非一邊想算法一邊重構代碼。
因爲本次結對編程任務比較單一,可能發生的異常狀況也比較少,所以咱們的異常處理主要是針對咱們程序輸出的結果加了保護使其在合規範圍內,還有在測試過程當中的異常處理,這個咱們並無花不少心思,只是pass掉了。
本次結對編程任務主要是腳本輸入輸出,咱們並無爲其適配UI,所以此部分略去。
下午接到任務,兩人都感到棘手,因而一塊兒在吃飯的時候討論了一下,當晚就開始了工做。
因爲咱們對強化學習並不熟悉,因而先一塊兒學習了一下Q-learning的思想和算法。而後咱們規劃了一下接下來的工做方向,決定先實現最基礎的Q-learning,state和action的都只有0-100均勻分佈的100個,而後讓咱們的bot之間比賽,選擇其中表現最好的。當時我和隊友兩人針對在實際比勝過程中是否須要實時train bot意見稍有不一樣,因而咱們擱置分歧,決定先實現基本算法再說。這個問題在結對編程進行時天然而然地解決了,由於比賽時地Gnum走勢與本身訓練時不可能徹底相同,因此放開讓算法本身學習纔是最好的。
而後咱們開始設計實現Q-learning的僞代碼,並將其轉化成QLearn class的函數名(。。。)。這以後,咱們試圖將這些函數寫滿,實現最基本的Q-learning算法,可是時間有限只寫了一部分就結束了。中間咱們還嘗試使用了模擬覆盤程序。
咱們在這天晚上埋頭工做四小時,實現了最基本的Q-learning算法。在使用模擬覆盤程序查看效果的過程當中,咱們發現結果不好,但用C#編寫的模擬程序調用咱們的腳本,debug十分不方便,因而用python編寫了test.py和view_npy.py來debug和查看Q-table。
然而,當咱們作完這些後,仍然發現咱們的bot僅僅停留在能夠運行的狀態,在模擬覆盤程序中的表現十分辣眼。因爲時間太晚,咱們決定回去休息,週日再戰。
與聯培班的其餘同窗奮鬥在北京城郊另外一戰場。
Nothing Done.
Pass
午餐後開始工做,因爲個人公司的電腦斷網,而我在週五晚上沒有將代碼傳到雲端或共享給隊友,致使咱們足不出戶調算法的夢想幾乎破滅,趕忙去公司共享給隊友,再回去調。吸收此次教訓,我決定之後每次工做完成都將代碼共享給隊友(然而並無用,由於個人電腦老是斷網,事實上後來的工做都是在隊友的電腦上完成的)
依照最初的計劃,咱們先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,即便咱們猜對了精度也只在小數點之前。
既然如此,優化算法勢在必行。通過討論,咱們決定在兩個方向優化算法:
有了優化方向,咱們心情輕鬆下來,也感到了疲憊,因而決定明天再繼續。
這天咱們感到很是緊張,晚上八點鐘左右開始工做,一直作到凌晨兩點(這裏向被吵到的我和隊友的室友表示歉意)。咱們將以前想到過的全部想法都實現並嘗試了,以致於QLearn class的初始化須要11個參數,若是咱們當時還理智的話,就把他們包起來了,但當時咱們兩人都很感到很急,因此沒有心思作這一點。仍是須要改進,應該從init函數須要兩個參數的時候就開始打包。
這晚咱們的bot終於可以在模擬覆盤程序中得分了,但咱們分析提供的兩輪數據發現,第一輪數據的Gnum收斂到波動<1%,而第二輪數據的波動能夠超過100%,咱們的bot在第二輪數據中表現不錯,第一輪數據中的表現就差強人意。分析緣由,雖然咱們在Q-table上使用了對數編碼,可是在黃金點收斂得數值較大且波動很是小時,精度仍然不夠。此時已經是凌晨一點多,咱們決定臨時加一個簡單的滑動平均模型用來與Q-learning互補。
當天咱們發現了好幾個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的表現像咱們的預期同樣開始走高,最終拿到第一名。我和隊友都很開心。
總結此次結對編程,我感到收穫很是大:
若是說結對編程有什麼缺點,那就是在咱們嘗試是個bot比賽時跑一次須要20分鐘,那時咱們兩人要一塊兒乾等,有點浪費時間。
講一講我認爲的咱們兩人各自的優缺點:
我:
隊友:
[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 深刻理解強化學習)