软件工程第一次结对编程
10/11-10/16日短短五天,我和隊友通過結對編程的方式完成了一個用來做“黃金點游戲”的小程序,項目地址:
https://github.com/ycWang9725/golden_point.git
黃金點游戲的基本規則
假設有M個玩家,P1,P2,…Pm
在 (0-100) 開區間內,所有玩家自由選擇兩個正有理數數字提交(可以相同或者不同)給服務器,
假設提交N11,N12,N21,N22,Nm1,Nm2等M2個數字后,服務器計算:(N11+N12+N21+N22+…+Nm1+Nm2)/(M2)*0.618 = Gnum,得到黃金點數字Gnum
查看所有玩家提交的數字與Gnum的算術差的絕對值,值最小者得M分,值最大者扣2分。其它玩家不得分
此回合結束,進行下一回合,多回合后,累計得分高者獲勝。
我們的bot收到的輸入是游戲當前輪和歷史所有輪次的Gnum和所有玩家的預測值,輸出兩個對下一輪黃金點的預測值。
PSP表
在任務要求下達的當天晚上,我與隊友兩人便開始了討論、設計與編碼。
| 計劃: 明確需求和其他因素, 估計以下的各個任務需要多少時間 | 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非常少,我們沒有撰寫測試報告。
我們的bot
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。
將Q-table的action改為0.3-3的對數差分編碼,使得Q-table在同一個state下獲得更大的橫向精度,也使model的輸出保持在上一輪黃金點的0.3-3倍范圍內,保證了輸出的穩定性。
事實上,除了上面的setting,我們還嘗試了不同的更新策略、編碼策略、reward策略,從中選擇了表現最好的作為QLearning model的最終版本。
UML
Design by Contract
契約式設計的好處在于:能夠將前提條件和后繼條件以及不變量分開處理,明確了調用方和被調用方的權利與義務,避免了雙方的權利或義務的重疊,有助于使得整體的代碼條理更清晰、功能劃分更明確、避免冗余的判斷。在結對編程中,假設需要我和隊友各自寫具有相互調用關系的類時,雙方都會在函數最開始用assert指出必須滿足的前置條件,并對后繼條件不符的情況進行異常處理,同時對于不變量進行檢查。
規范與異常處理
程序的代碼規范主要是依靠Pycharm的內置代碼規范,達成共識的方式非常簡單,我倆都有強迫癥,看著編譯器里面各種黃線白線就特別不爽,所以就不惜一切代價消除這些線,因此我們兩個都使用同一套代碼規范。
設計規范的話,根據《構建之法》書中提到的,一個函數只干一件事情,功能劃分到最小單元。這一點,我們嚴格執行,甚至其中一個要把兩個數構成的列表轉化成q-learning的獎勵的函數都要拆成兩個來寫,其中一個rank2score負責專門將一個輸入值轉化為一個獎勵輸出、另外一個my_rank2score在此上封裝,調用rank2score兩次,得到一個獎勵向量。這樣寫確實有好處,代碼想要重用時在任何功能層面上都能即拿即用,改的時候也能一改全改,非常方便。
本次編程中只對測試過程中出現的異常進行了處理,處理方式也特別簡單,就是pass,如果是需要輸出的地方就指定一個確定的值。從這個角度上來說,我們確實很欠缺。以后應該在編碼的過程中就想到異常處理這一步,形成習慣。
結對編程的過程
結對編程最開始接到任務時是很緊張的,因為不僅是要參加比賽,而且還要用到自己設計的AI,更重要的是給的時間非常短,還不到一周。聽到建議使用q-learning,更是比較懵逼,我們結對的兩個人從來沒有接觸過任何強化學習。因此,從任務剛剛布置下來就開始了工作。
2018/10/11 THU 晚上
兩個人找了一個休息室,架上電腦,先看了一下Q-learning 的論文,又看了一些大佬的博客,大概了解到了Q-learning是怎樣一回事,然后開始寫Q-learning的Python類,同時也看了一下老師給的Python接口,在公用的模擬復盤程序中試了一下能不能跑通。Q-learning類寫到一半,由于時間原因,回寢室休息,第一天工作結束。
放一張圖表明代碼進度(真的,只寫了這么幾行,太慢了。。。)
2018/10/12 FRI 晚上
既然已經熟悉了基礎知識,也熟悉了接口,接下來就是悶頭寫實現了。這次實現的任務量也不是很大,Q-learning類的代碼量擴充到了100行,同時又在這天晚上新寫了一個60多行的python下的模擬復盤環境,用以實時debug。至此,一個最基礎的q-table已經寫好了。目標不高,能跑就行。。
再上兩張圖表明代碼進度(把之前寫的函數折疊掉了,用于增量表現)
2018/10/13 SAT
和一起實習的同學去爬香山,自己長期不運動導致突然爬一次山就渾身酸痛。。。沒有任何理由在這一天寫任何代碼。
2018/10/14 SUN
11點多才起床。。但是該碼代碼還得碼代碼。下午兩點開始,兩個人會面,開始試驗q-learning。沒的說,先讓10個q-learning bots干一架再說!結果是這樣的:
就這樣對著屏幕分析了一個小時之后,覺得頭昏眼花。算了,寫個腳本畫個圖吧,于是就有了這個100多行的plot腳本(折疊了,要不看不到代碼總行數)
然后每個bot都得到了一張屬于他自己的計分圖,如下圖,是第0個bot的Hard reward走勢
對比不同bot總體得分圖:(這里橫坐標代表不同threshold參數的bot。顯然,最后一個bot被干掉了)
有了這些工具,媽媽再也不用擔心我們分析數據了。
就這樣,不停換和添加bot的參數,試驗到底具有怎樣個性的bot 容易贏,一直到10點,然后開始拿在前面的bot戰中獲勝的bot放到模擬復盤中跑,模擬它在參加那次夏令營的比賽。然而,情況不容樂觀。。。成功地拿到-300多分,穩居最后一位。
怎么辦呢?分析了一下bot提交的數發現,好像bot 根本沒學到大盤走勢啊!!以至于到盤末大盤都已經降到4.8左右了,我們的bot還在提交80.5這樣的數。。怎么辦?看看q-table吧
原來如此。。訓練集太小,根本更新不全q-table嘛。。另外,每個action的精度太低了,只有1的精確度,那不就是說假設黃金點是4,結果我們就算預測到在這附近,也只能提交量階中最接近的數4.5嘛?別人如果是基于統計的模型的話很輕松就能算出類似4.11這樣的數啊,所以我們輕松被超過啊。好,現在知道問題了,那就簡單了,解決問題不就得了!
第一:訓練集太小,那我就增大數據量啊!怎么在確定有限的回合中增大數據量呢?原先的q-learning是在不斷試錯中總結經驗,每次只更新上一次對應狀態和上一次對應動作那一個q-table 表項的元素。那為什么一定要等錯誤發生之后再總結經驗呢?為什么不能學習歷史,避免出錯呢?于是就有了我們的第一個trick:模擬歷史學習法,即根據歷史的狀態和歷史的黃金點結果,模擬自己采取每一個可能的action,得到reward,一次對q-table中的一整行進行更新。這樣數據量一下提升了100倍(100是我們每一個state的action數)
第二:action精度不夠,討論過后,我們的思路漸漸被引向了學過的差分編碼,即每個action是到上一次黃金點的差量,這樣基于黃金點不可能有特別大變動的假設,可以把action對應的值域縮小,在action總數不變的情況下,action的精度提高了。然而,我們不止于此,又想到:能不能讓精度對黃金點的位置有自適應呢?于是想到了“對數差分編碼”(這個名字我不知道有沒有,是我隨口說的)就是,每一次的action都對應于在上一次的黃金點基礎上乘以多少,這個乘的數就對應action,并且action的下標與乘的數的對數成正比。這樣,假設action對應3-0.3的數,則精度大概能達到(3/0.3)^(1/100)*gnum,其中,gnum是上一輪的黃金點。假設上一輪黃金點是4,那么精度可達0.093。如果黃金點更小,則精度更高。同理,state也可以使用對數編碼達到自適應精度。這就是我們的第二個trick:對數(差分)編碼
好,有了這兩個點子,那就沒問題了,安心睡覺了,明天再碼代碼
2018/10/15 MON 晚上
瘋狂碼代碼趕ddl,把昨天的所有想法都實現進去,為此,給bot添加了許多參數輸入,用于決定工作模式,到最后,q-learning bot的init函數變成了下面這樣,此時一個屏已經裝不下了,全是參數。。。總體代碼量600-700行吧
這一天工作到凌晨兩點,得到了不錯的成績:
但是發現在黃金點收斂到4.8左右的那次比賽的模擬復盤中,由于黃金點始終較高,所以導致我們的自適應精度較低,導致我們沒能拿第一。仔細一想,一共有400次提交,黃金點如果不單調遞減的話,那就很有可多收斂到一個較大的數上,或者在其上下波動。如果上下波動的話,一般的統計模型都難以處理,反而對于我們的q-learning比較有利。然而是如果黃金點精密地收斂到一個較大的數上對我們非常不利,因為我們的精度有限,而一般的統計模型可以輕松擬合這種收斂的序列,精度要多少有多少。于是,迫不得已,再加一個與q-learning互補的統計模型吧,沒剩多少時間了,那就寫一個最簡單的一階IIR濾波+噪聲吧。這就是我們的第三個trick。只好先睡覺了,明天就要比賽了,要保持精力。
2018/10/16 TUE 上午+下午
加了一個IIR,寫了互補控制策略,中途出了幾個bug,緊張死了。好在最后把bug都消除了,在兩次模擬復盤中都能拿第一了。5:00,提交最終版。
2018/10/16 TUE 晚上5:30-6:00
第一輪比賽。才拿了個第六名,感覺很失落,趕緊不吃飯了,分析問題原因。
2018/10/16 TUE 晚上6:00-7:00
復盤看了一下q-table,發現本次比賽的統計特性決定了q-table的值特別聚堆,沒區分度,于是增大了softmax的指數倍數(下圖中函數第三行的“vector * 150”),相當于normalize了。提交上去,聽天由命了。
2018/10/16 TUE 晚上7:00-7:30
第二輪比賽,穩拿第一。兩方面原因,一方面是我的改動,另外一方面有很多提交搗亂數字的隊伍都更規矩了,當然這也是大趨勢,如果很多人擾亂的話,會導致擾亂的效果下降,第二輪這種改變也符合我們的預期。
最后總評我們也是第一。很開心,沒白浪費這么多時間。
總結成功的原因
從計劃安排與實施過程方面,有三點原因:
1.很快確定核心算法,中途雖然遇到波折,但是始終都在以同一個算法為中心進行改動,因此在有限的時間內能夠把這一個算法做到極致
2.起步過程不貪心,沒有想要一口吃個胖子。
3.信心比較堅定,測試修改過程中都有一個明確的目標:至少要在模擬復盤中拿到第一。
在技術層面上,有三點原因:
1.在宏觀上,我們使用的策略是“唯快不破”的想法,即:以最快速度適應大盤的變化規律,這樣,不論對手采取什么手段,我們都能很快地學出來這種手段的適應辦法,從而超過使用這種手段的人的得分。
2.在微觀上,我們使用的模型參數量遠多于其他組的模型,因此有很強的適應能力,不僅如此,我們還設計了多種不同的tricks來提高模型性能,大幅提高學習效率。
3.搭建了比較完善的調試平臺,能夠實時地跟蹤各項參數的變化以及得分的變化,針對性地采取解決方案。這一點在比賽中場修改程序中起到了至關重要的作用。
結對編程的心得體會
在結對編程中,我對于python比較熟悉,隊友對于C#比較熟悉。因此,在算法上的python代碼主要由我來碼,隊友做領航員的角色。在復盤程序的調試中,主要由隊友操作C#程序,我也從中學到了一些C#的知識。結對編程的過程中,我和隊友互相實時復審代碼,十分高效,提早解決了很多可能出現的問題,算是學習到了。而且我們還從零開始學習實踐了不太好搞的強化學習,很有收獲!
列舉一下我認為本次結對編程中雙方的優缺點:
隊友:
優點:
1.看代碼非常仔細,不論我寫到那里,她都能在我的代碼中提出我可能忽略的地方。
2.在我專心實現算法時,她也會查找一些資料和庫的使用方法,輔助我快速實現。
3.理解代碼的能力以及與我溝通的能力很強,有幾次我寫代碼過程中忘記告訴她我的想法到底是什么了,但是她仍然能很快通過看代碼意會出我到底在干什么,甚至進一步提出改進方案。
4.學習能力比較突出,在結對最開始,她好像不太熟悉numpy等庫,但到結對后期,她已經能夠指出我使用這些庫的問題。
缺點:
可能是我很快就把解決方案都說到了吧,隊友對于算法改進方案的貢獻不是很多。
我:
優點:
1.對于python的常用庫都比較熟悉,因此搭建python的調試、測試環境等都很快。
2.綜合運用學過的知識,想到很多trick,提高了模型能力。
3.對于代碼架構有著自己的一套設計,功能劃分都很明確,因此編程到后期代碼量較大、功能較多時仍然能夠保持頭腦比較清醒,把更大的精力放在算法設計和實現功能上,而非重構和管理代碼上。
缺點:
比較粗心,經常被挑錯,而且有的時候寫代碼走神,寫著寫著就忘記了最開始提到的某些重點。
轉載于:https://www.cnblogs.com/RubikCube/p/9817804.html
總結
以上是生活随笔為你收集整理的软件工程第一次结对编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: TensorFlow 官方文档中文版发布
- 下一篇: Python的变量名、数据类型和简单运算