# pingpong_ML > F14071075 @2021 上一次的 Arkanoid game 筆記: [Arkanoid game](/551W3kEET0CBQm1YS3RLtg) - 原始遊戲Github網址: https://github.com/LanKuDot/MLGame/tree/beta7.1.3/games/pingpong#readme 環境: --- - Ubuntu 20.04 - git - python 3.8.5 - pygame == 2.0.1 - sklearn == 0.22.2.post1 簡介: --- - 遊戲介紹: - 上方為2P, 下方為1P, 各自控制水平移動的板子,將球擊回去。 - 球落地算輸 - 球速會逐漸加速 - 本次實驗流程: - 首先,寫出一個透過 Rule 判定與控制遊戲的程式碼,讓程式可以自己與自己對打。 - (也就是透過程式直接計算落點、控制板子移動) - 1P、2P 都要由自己的程式控制。 - 這步驟很重要,成果會直接影響訓練結果 - 而後,使用 該程式 遊玩遊戲,將遊戲歷程紀錄成 .pickle 檔案保存 - 再透過 遊戲歷程.pickle 訓練 ML - 最後用 ML 預測/控制遊戲水平移動的板子 - (透過另一個 "ml_play.py" 檔案,讀取剛剛訓練的 ML model 和 回傳指令給遊戲) 下載遊戲: --- - 在terminal開啟想要儲存的資料夾後,使用git複製資料庫: `git clone https://github.com/LanKuDot/MLGame.git` - 手動遊玩: - 進入到MLGame資料夾後, `python MLGame.py -m pingpong HARD 3` - 控制: ![](https://i.imgur.com/eiYStGr.png) - 參數設定: - `-m` 手動模式 - `-f 300` fps 300 模式 - `-r` 將遊戲記錄成`.pickle`檔案存放到 `MLGame/games/pingpong/log`資料夾內 使用程式操控遊戲: --- - 在 MLGame/games/pingpong/ml 資料夾中新增 test01.py 檔案: - 編輯`test01.py` - 程式控制遊戲遊玩: - `python3 MLGame.py -r -i test01.py -f 500 pingpong NORMAL 3` - `python MLGame.py -i ml_play_template.py pingpong EASY 3` Rule_Base 控制遊戲進行: --- - 背景: - 可取得之遊戲 Info - ball_x - ball_y - platform_1P_x - platform_1P_y - platform_2P_x - platform_2P_y - frame - blocker - 可操控之遊戲 Cmd - SERVE_TO_LEFT - SERVE_TO_RIGHT - MOVE_LEFT - MOVE_RIGHT - NONE - RESET - 遊戲長寬: - 左到右: 0 ~ 200 - 上到下方平台: 0 ~ 400 - ball: 5 - platform: 40 - **結論**: - **ball_x: 0 ~ 195** - **ball_y: 0 ~ 395** - **platform 實際涵蓋: platform ~ platform+40** - 遊戲分析: - ball 上升過程不重要 - ball 下降時與牆壁碰撞所構成的 x 軸運動為 簡諧運動SHM - 半週期為 195 - 完整週期為 390 - ![](https://i.imgur.com/Ssu9BBj.png) > ## 注意!在實驗過程中,我發現牛頓在哭 > - 首先,這個模型理應是建立在 無摩擦力的完全碰撞 環境。 > - 所以我透過簡諧運動去預測球體落點。這本身應該沒有任何問題,但是卻很常發生“預測落點”和"實際落點"並不相符合的狀況。 > ![](https://i.imgur.com/jXA0aB4.gif) > - 花了很多時間檢查程式碼,卻沒有發現問題。正當我一籌莫展時,突然發現一個關鍵問題: > ![](https://i.imgur.com/Lm0Yj2X.png) > - 為什麼球以 +10 速度,從 190 衝撞牆壁(牆壁位於195位置)的時候,卻在 195 瞬間停止?而且從 195 反彈回來時,又重新獲得 10 的速度?! 理論上從 190 +10後 下一個點應該是撞到牆(195)反彈回 190 吧? > - 而後我明白了。這是個"伸縮"牆壁。也就是 **每一次簡諧運動,牆壁的實際邊界都是不同的!!!**。而再修這這項謎團後,就可以真正預測出準確的落點了。(for側面牆壁: real_wall = 195 + ball_speed_x - ( 195 % ball_speed_x )) > ![](https://i.imgur.com/DaSYHPS.gif) > 而後我發現,同理,下方的 1P 球拍(板子) 也會發生該問題。 > ![](https://i.imgur.com/0nGaAql.png) > - 拜託如果要把牛頓當空氣,先跟我說一下,我找這個Bug找了很久OAO - 計算出落點: - P1 ![](https://i.imgur.com/j1nD6pp.png) - P2 ![](https://i.imgur.com/uzsxnwg.png) - 但僅僅是依靠"球下降時計算出落點"這件事,不足以闖過 "HARD" 難度的遊戲。 - 因為在 HARD 難度時,中間會多出一個移動的漂浮台("blocker"),當球速隨時間上升後,若碰撞到漂浮台很可能會來不及移動球拍(板子)到球的落點 - 所以我們要多考慮三種狀況: - 當球朝向遠離板子的方向行進時: - 直接移動到 假設會撞到漂浮台並反彈回來的落點 - 透過 "伸縮"簡諧運動可以很簡單的計算出來若是撞到漂浮台反彈後的落點 - P1 ![](https://i.imgur.com/AddI88E.png) - P2 ![](https://i.imgur.com/pRlNGQ3.png) - 若對方的球飛過來的過程中撞到漂浮台的側邊,導致行進路線與預測之落點不同: - (我沒算這部分。祝好運^^) - 並且,因為最後的模型**要和別人的模型對戰**,因此我想將之複雜化:**切球** - 我在板子和球距離小於12的時候,控制板子向某個特定方向 - 要朝向哪個方向切球,是依據 (blocker_x < platform_x)&&(ball_speed > 0)決定 - 若結果為真,則向右切球: - 用(blocker_x < platform_x)&&(ball_speed > 0)的原因是為了盡量減少打出去的球碰到漂浮版彈回來的次數 ![](https://i.imgur.com/o5dgrVW.png) - Rule_base 結果呈現: - 注意:**因為會切球,所以有時球的x軸速度會與y軸速度不同。** ![](https://i.imgur.com/sbg1GqV.gif) > Rule Base 的遊戲控制寫法,將會極大的影響到後來使用 ML 訓練模組時的結果 遊戲紀錄檔(.pickle)處理: --- - 遊戲執行時加上`-r`就會自動產生遊戲紀錄檔,而記錄檔會在`MLGame/games/pingpong/log`資料夾內 因為名稱都很亂,為了方便等等使用: **批量編號改名(Linux):** - 在terminal中,進入到`log`資料夾後,用指令 `i=1; for x in *; do mv $x $i.pickle; let i=i+1; done` 將所有檔案重新編號為 `1.pickle` `2.pickle` `3.pickle`... - 改變之前: ![](https://i.imgur.com/WkoHf7T.png =400x) - 改變之後: ![](https://i.imgur.com/7YrHlGh.png =400x) 上傳遊戲.pickle至google colab --- - 將剛剛被重新編號過的`.pickle`檔案上傳至 Google Colab - ![](https://i.imgur.com/sPg0pdB.png =300x) 訓練Model === KNN --- - **KNN 01:** (with ml_knn.py) https://drive.google.com/file/d/1FHY1Qn__bXtTRn9kX37xSZfg4zp5p4Dc/view?usp=sharing - 修改自上次遊戲之KNN - Model 訓練架構: - Input: `[目前ball_x, 目前ball_y, 目前platform_x, 195 - ball_x, ball_x - 195]` - Output: `[0, 1, 2]` # 分別代表(NONE, MOVE_LEFT, MOVE_RIGHT) - 訓練資料前處理: - Train_X: 所有的 `[目前ball_x, 目前ball_y, 目前platform_x, 195 - ball_x, ball_x - 195]` 直接作為 input - Train_Y: 讀取 data['ml_1P'/'ml_2P']['command']並且轉換為 0, 1, 2 - 總共使用 181 個 Rule_Base 遊玩 NORMAL 3 產生的 .pickle 檔案訓練 - Result: - 儘管f1分數看起來很高,但事實上幾乎無法正常玩遊戲。 ![](https://i.imgur.com/c4S1T4Z.png) - 結果不盡理想。歸咎原因很可能是因為: - 因為rule_base有使用到切球機制,導致不容易訓練(在球接近板子時會發生錯亂,朝向不正確的方向前進) - 分數僅僅是代表"指令"是否吻合。無法完全代表實際的情況(板子位置是否相同) ![](https://i.imgur.com/BH66ZHk.gif) - **KNN 02:** (with ml_knn_04.py) https://drive.google.com/file/d/19yEu5vz8ipz-qwQJ-fX00LW8U4ILEGvq/view?usp=sharing - 修改自**KNN 01** - feature - 增加 "對手platform_x" 以幫助 overfitting (...) - Result: - 儘管f1分數有所提升,但仍然無法到正常玩遊戲。 ![](https://i.imgur.com/6rUT6L7.png) - 結果不盡理想。歸咎原因仍然與KNN 01相似。: ![](https://i.imgur.com/csU7sVG.gif) ![](https://i.imgur.com/i3FeV0c.png) Random_Forest Classifier --- - **RFC 01:** (with ml_knn.py) https://drive.google.com/file/d/1FHY1Qn__bXtTRn9kX37xSZfg4zp5p4Dc/view?usp=sharing - 修改自上次遊戲之KNN - Model 訓練架構: - Input: `[目前ball_x, 目前ball_y, 目前platform_x, 195 - ball_x, ball_x - 195]` - Output: `[0, 1, 2]` # 分別代表(NONE, MOVE_LEFT, MOVE_RIGHT) - 訓練資料前處理: - Train_X: 所有的 `[目前ball_x, 目前ball_y, 目前platform_x, 195 - ball_x, ball_x - 195]` 直接作為 input - Train_Y: 讀取 data['ml_1P'/'ml_2P']['command']並且轉換為 0, 1, 2 - 總共使用 **39** 個 Rule_Base 遊玩 NORMAL 3 產生的 .pickle 檔案訓練 - Result: - 儘管f1分數看起來不會到太糟,但事實上連發球都接不到。 ![](https://i.imgur.com/dp8qvWe.png) - 結果不呈現了。 Gradient_Boosting Classifier --- - **GBC 01** - 在此奉勸大家,寫完程式要再檢查一下,不要直接衝結果,會浪費很多時間 - 先看個結果 - Result: ![](https://i.imgur.com/5oboKrR.png) - 有點糟,但好像也沒那麼糟、對吧? - 結果是完全連一顆球都打不到。不巧這其實是我第一個實驗的模型,讓我以為我的整個架構是有問題的,但卻暫時想不到有什麼其他辦法來訓練模型。 - 結果是...手誤把 1P的資料打成2P的資料。訓練出來當然會有問題啊!!! - ![](https://i.imgur.com/RfRVyo5.png) - **GBC 02~03:** (with ml_knn.py) https://drive.google.com/file/d/18yyBVkOxf8GCoCbcCrtZ6CjDbxCTvQ0w/view?usp=sharing - 修改自上次遊戲之 GBC - Model 訓練架構: - Input: `[目前ball_x, 目前ball_y, 目前platform_x, 195 - ball_x, ball_x - 195]` - Output: `[0, 1, 2]` # 分別代表(NONE, MOVE_LEFT, MOVE_RIGHT) - 訓練資料前處理: - Train_X: 所有的 `[目前ball_x, 目前ball_y, 目前platform_x, 195 - ball_x, ball_x - 195]` 直接作為 input - Train_Y: 讀取 data['ml_1P'/'ml_2P']['command']並且轉換為 0, 1, 2 - 總共使用 **181** 個 Rule_Base 遊玩 NORMAL 3 產生的 .pickle 檔案訓練 - Result: - f1分數達到 98%。 ![](https://i.imgur.com/WhXxYO2.png) - 目標是直接overfitting 到 100%,所以並沒有在此停留。 - 直接前往下一個模型前進! - **GBC 04** https://drive.google.com/file/d/1cFcfWR4pSmsXSVRR87JTyTby9jyePV2c/view?usp=sharing - 修改自 GBC 02~03 - 增加 feature : - ball_speed_x - ball_speed_y - 對手的 platform_x - (球速大於1) ? 1 : 0 - Result: - 達到99%與100%的準確率 ![](https://i.imgur.com/TLgCx2d.png) - 目標是直接overfitting 到完全 100%,所以並沒有在此停留。 - 直接前往下一個模型前進! ### 實驗最終模型: GBC05 - **Gradient_Boosting 05:** (with ml_knn_07.py) https://drive.google.com/file/d/1SbXZoIxopzVoGupDYXWJ_blGZ4iqh_5G/view?usp=sharing - 由GBC 04 增加 feature "blocker_x" 而成 - Model 訓練架構: - Input: `[目前ball_x, 目前ball_y, 目前platform_x, ball_x - platform_x, platform_y - ball_y, ball_speed_x, ball_speed_y, platform_x_對手, ball_speed_y>0 ? 1 : 0, blocker_x]` - Output: `[0, 1, 2]` # 分別代表(NONE, MOVE_LEFT, MOVE_RIGHT) - 訓練資料前處理: - Train_X: 所有的 `[目前ball_x, 目前ball_y, 目前platform_x, ball_x - platform_x, platform_y - ball_y, ball_speed_x, ball_speed_y, platform_x_對手, ball_speed_y>0 ? 1 : 0, blocker_x]` 直接作為 input - Train_Y: 讀取 data['ml_1P'/'ml_2P']['command']並且轉換為 0, 1, 2 - 總共使用 181 個 Rule_Base 遊玩 HARD 產生的 .pickle 檔案訓練 - Feature 分析: - 我認為是真的有用的feature: - 目前ball_x - 目前ball_y - 目前platform_x - ball_x - platform_x - platform_y - ball_y - ball_speed_x - ball_speed_y - ball_speed_y>0 ? 1 : 0 - 我認為只是來幫助我overfitting(拿及格成績的feature)(及格要求:五次遊戲中一半的結束速度達|15,15|以上): - platform_x_對手 - blocker_x - grid.best_params: - learning_rate: 1.0 - max_depth: 13 ### 能夠做到切球!!! - 從速度上可以看到,X 速度時常和 Y 不同,代表該球有成功切球。 - 但也因為切球,所以可以發現有時球速會下降。(切球方向與球原動量方向相反) - 大部分的結束速度都有不錯的水平 ![](https://i.imgur.com/kQSafoK.png =150x) - Result: ![](https://i.imgur.com/nVEyQYI.gif) > 補充: ML訓練過程小插曲: > 1. 資料前處理惹禍 > 不知道為什麼,在不同的 ML 演算法訓練出來的model,在開始遊戲時的第一步,都不約而同的向右跑? > 結果是因為資料前處理中,我是修改自上次的程式,沒有更改到第一筆資料的前處理,導致第一筆資料都相同。 > 2. 準確率很高,可是卻完全接不到球? > 在訓練的過程中發生有好幾次的結果怪怪的,儘管準確率不差(90%以上),但是實際拿到遊戲遊玩的時候,卻甚至連一顆球都打不到? > 結果是因為,程式是更改自github內的說明程式,但它移動的指令是[-1, 0, 1] 去區分, 而我卻是以 [0, 1, 2] 去區分。這樣當然遊戲結果會很慘淡,因為控制對照全錯了。 > 3. 這次的遊戲歷史資料(.pickle)不只有"NONE",竟然還會出現"None"這種東西?! > 4. 如果你的 ml_play 會跑完一次就當機,代表你是使用上次的 ml_play 去修改的。注意一下 def update 裡面的scene_info["status"]這部分有更改: > ![](https://i.imgur.com/IKwNdiz.png) 補充: 儲存Model到.pickle格式: --- - Official: https://scikit-learn.org/stable/modules/model_persistence.html ![](https://i.imgur.com/7ev5lNb.png) - import: `import pickle` - store model.pickle file: ``` f = open('model_name.pickle', 'wb') pickle.dump(model, f) f.close() ``` - read model.pickle file: ``` f = open('model_name.pickle', 'rb') model = pickle.load(f) f.close() ``` 補充: 部分程式碼 --- rule.py: - 1P ![](https://i.imgur.com/lqf209L.png) - 2P ![](https://i.imgur.com/liECqAs.png) 為什麼要 Overfitting? 不是應該要避免這件事情發生嗎? --- - 我上次沒辦法接受,為什麼要把模型train到100% - 但這次我找了合理解釋: - 不要跟自己的成績過不去 (x - 可以用 Q-learning 中的 Q-table 概念來解釋 (O - Q-Learning - 透過更新(訓練) Q-Table 的方式,達到程式自己玩遊戲的效果。 - 而 Q-Table 就是,把所有狀況所有排列組合 列成表格(或State),然後透過遊戲過程不斷更新參數,將每個狀況(state)應該要做的最佳行為填入。 - Deep-Q Learning - 用Nerual Network 取代 Q-Table - 我就姑且理解它為 "ML-Q Learning" 吧! - 透過 ML overfitting 訓練集的方式,取代 Q-Learning 中的 Q-Table - 這樣對於 "Overfitting" 就可以說,我目的是做出Q-Table的model - 這樣可以說服你合理化 overfitting 了嗎? - 我覺得不行 - 這樣訓練的結果,導致其實訓練出來的遊戲並不是真的很像會自己玩遊戲 - 比較像是"重現遊戲紀錄" - 因為若拿兩個人的模型來對決, 我發現基本上"**發球的那方會贏**",也就是模型只是在重現自己的遊戲紀錄而已,根本不知道如何判斷如何操控。 結論 === 在此實驗中,若以目前的實驗結果來看,rule_base絕對是最重要的一個環節。 若沒有良好的紀錄檔用來訓練,要超過及格線很難。 我的程式因為有加入切球的機制,且每次切球的方向都會隨狀態變得不太一樣,因此不容易進行overfitting的訓練。從在KNN訓練中的結果都很慘淡,就可以窺知一二。但在GradientBoostingClassifier的強力bagging下,還是做到了"切球"這件事情,真的很神奇! 心得 === 這次也花了很多時間做不同的嘗試,包括 rule_base 與 ML 模型的建構, ![](https://i.imgur.com/TPjZ6dj.png) - 特別是 rule_base 的部分,因為要用 rule_base 訓練出一個可以打出速度超過(20,20)的程式,本身就需要照顧到不少小細節,而且還要去發掘並思考牛頓被當空氣的問題,導致這次實驗其實一半以上的時間都是花在這部分,而實際思考 ML 的時間卻並不多。 而對於評分機制,我認為可以有不同的設計方法。 - `"自己與自己對打結束時,速度超過(20, 20)“`這件事, 是否代表他訓練出來的模型就是比較好的模型,我認為有待商榷 - 而且就`"結束時的速度"`作為依據這件事,如我的模型中,因為有切球的因素,會導致球速一度超過20, 但最後又低於 20, 那我是否要選擇當球速超過20時,故意漏接球,使得結果超過(20, 20)? - 或許可以改為,`"訓練出來的model將會與助教的程式做對打"`,而分數就依照對打的結果去換算,且助教的程式完全不公開也不能事先測試。我想這樣會更吸引人、更讓樂於嘗試不同的實驗方法。