我用深度學習分析LoL小地圖,自製數據集DeepLeague開源(下)

.. 本文為雷鋒字幕組編譯的技術博客,原標題 DeepLeague: leveraging computer vision and deep learning on the League of Legends mini map + giving away a dataset of over 100,000 labeled images to further esports analytics research,作者Farza。

翻譯 | 於澤平 安妍     整理 |  凡江

嗨!請確定你已經閱讀過 我用深度學習分析LoL小地圖,自製數據集DeepLeague開源(上),否則你可能會對這一部分感到困惑。

創建數據集

第一部分:神秘的網絡套接字

可能許多人都想知道如何創建這個數據集,因為這個數據集實在是太大了。現在我來揭曉答案。我沒有手動標記100000張小地圖,那樣做太瘋狂了。每張圖片都有10個邊界框(假設所有10個英雄都存活),表示這些英雄在哪裡以及它們是什麼英雄。即使我手動標記每張圖片需要5秒鐘,也需要超過8000分鐘才能完成!

下面讓我來講講我是怎樣創建數據集的,這包含一些聰明的技巧以及Github上面另一位名為remixz的開發者的幫助。這位開發者創建了接下來我將介紹的神秘的網絡套接字數據集(我喜歡這樣叫它)。

當你在lolesports.com上面觀看英雄聯盟比賽直播時,實際上有一個神秘的網絡套接字,它不斷地拋出關於這場比賽直播的數據。我稱之為神秘的網絡套接字因為有很少人知道這件事,而且它似乎是半隱藏的。套接字產生的數據包含這場比賽中的選手名字、他選擇的英雄以及每一時刻的英雄位置和血量。它以這種形式存在是因為這樣可以讓直播軟件在網頁上實現統計功能。

你現在可能想知道我如何使用這個數據的了!

我創建了我自己的節點腳本(與remixz所創建的類似),每當神秘的網絡套接字開啟,腳本監聽傳入的數據,並將數據保存到一個JSON文件中。我在AWS EC2機器上託管了這個腳本,現在我正在自動保存LCS(英雄聯盟冠軍聯賽)中北美賽區和歐洲賽區的比賽數據!

如果你仍對數據感到好奇,這裡是從LCS一場比賽中獲取的小片段,可以讓你更好地理解它是什麼樣子的。

這個JSON數據本身其實沒什麼用。但是請記住,我們這樣做的目的是為了創建一個有標籤的數據集。這個數據集是帶標籤的小地圖圖片,標籤表示各個英雄在小地圖上的位置。我並不關心JSON數據本身。

我應該補充說明的是,深度聯盟只從比賽中識別了55個英雄,因為我只是用LCS的數據。LCS中的選手通常只使用一部分英雄。例如,中單通常會玩狐狸,但是幾乎沒有人玩提莫!這意味着我沒有辦法訓練一個可以識別提莫的模型。同樣,也意味着我有太多狐狸的數據。我需要在我的數據集中獲得英雄的平衡。你可以查看代碼,看看我如何使用check_champs函數平衡我的數據集的。

現在,我擁有的一切是這些JSON文件,它們對應某場比賽每一時刻的每一個英雄的位置。因此,我所需要做的就是下載JSON文件對應的比賽視頻,並將JSON數據與視頻匹配。起初我認為這很簡單。我以為只需要去YouTube上面找到並下載比賽視頻,寫一個腳本自動從視頻中提取幀,再將它與JSON數據匹配就可以了。

第二部分:理解問題

我犯了一個很大的錯誤。讓我來解釋一下。

我無法像處理我在家裡錄製的英雄聯盟視頻一樣處理LCS的視頻。舉個例子,如果我需要記錄我自己在家從頭到尾玩的一局英雄聯盟,我只需要運行這段代碼:

注意:當我說「幀」時,我的意思是假設遊戲中的每一秒與一個圖像「幀」相對應。一個60秒的視頻將一共有60幀,每一幀都對應一秒。因此,1FPS(每秒傳輸幀數)!

# first go through every single frame in the VOD.

in_game_timestamp = 0

for frame in vod:

    # go in the vod's json data. find the json data associated with 

    # that specific timestamp.

    frame.json_data = vod_json_data[in_game_timestamp]

    in_game_timestamp += 1

一幀的例子。我保存了完整的視頻,只是從中裁剪出了小地圖。

這段代碼可以在我家裡保存的遊戲視頻上完美運行。假設我在遊戲中0:00的時間戳開始錄製視頻,在22:34時間戳停止。那麼如果我想要遊戲計時器中每一秒的一幀數據,這是很容易的,因為:

我在家裡錄製的視頻的時間戳與遊戲中時間戳是直接對齊的。

哈哈,朋友們,我真希望LCS視頻也可以這樣簡單地處理。

獲得LCS比賽視頻的唯一方式是通過Twitch上的直播流。這些LCS比賽的Twitch直播流在比賽過程中有許多終端,例如即時回放,選手採訪以及暫停。視頻的JSON數據對應遊戲中的計時器。你明白這為什麼會成為一個問題了嗎?我的視頻計時器與遊戲計時器不是對齊的。

溜了溜了

假設發生了這種情況:

  • LCS比賽在遊戲計時器中時間戳為12:21

  • LCS比賽在遊戲計時器中時間戳為12:22

  • LCS比賽在遊戲計時器中時間戳為12:23

  • 流轉換為即時回放,並播放最近的一場團戰,共17秒。

  • LCS比賽在遊戲計時器中時間戳為12:24

天哪!這真是太可怕了。我完全失去了遊戲計時器的軌跡,因為這次中斷,遊戲時間戳和視頻時間戳變得不對齊了!我的程序應該怎樣了解如何從視頻中提取數據,並將每一幀與我從網絡套接字中獲得的JSON數據相關聯呢?

第三部分:取消谷歌雲服務

現在問題已經很清晰了。我用來從視頻中提取數據的腳本必須知道遊戲中的時間戳的實際情況。只有這樣我才能真正確定正在展示的是比賽而不是一些類似即時回放或是其他中斷的內容。同樣,知道遊戲中的時間戳是非常重要的,因為神秘的網絡套接字數據集給我們的是實際遊戲中的每一秒對應的幀的數據。即使有類似即時重放的事情出現,神秘的網絡套接字仍然在向我們發送數據。因此,為了匹配JSON數據中的幀,我們需要知道遊戲中的時間戳。

我做的第一個嘗試是使用基本的OCR(光學字符識別)識別時間戳。我嘗試了很流行的庫,但都獲得了很糟糕的結果。我猜測的是,奇怪的字體以及總在變化的背景使其變得十分困難。

裁剪出的遊戲計時器的樣例

最後,我發現了谷歌的Cloud Vision API,其中也有OCR功能。這個API效果很好,幾乎不犯錯誤。但有一個問題。

每使用API處理1000張圖片需要花費1.5美元。我首先想到的是將所有時間戳放到同一張圖片中,並將它們作為一張圖片進行處理。但由於某種原因,我得到了很糟糕的結果。API一直在給我錯誤的答案。這意味着我有一個選擇,我將需要每次發送給API一個小時間戳。我有超過100000幀,這代表我需要付150美元。這其實也不算太差,只是我沒有那麼多錢,我還在上大學,我還只是個孩子…

但是!我很幸運地找到了這個:

創建一個賬戶就可以獲得免費使用的300美元。無疑,我現在還沒有創建3個賬戶,因此我可以用免費的900美元處理我的視頻,並在GCP上做隨機測試和腳本。這將違反服務條款,也不尊重公司。 

無論如何,憑藉這筆免費資金,我編寫了一個腳本,可以逐個使用Google Vision API處理視頻。這個腳本輸出了一個名為time_stamp_data_clean.json的JSON文件,它從遊戲中提取了各個幀,並根據每一幀對應的時間從遊戲計時器中讀取並標記。

time_stamp_data_clean.json的數據,告訴我們遊戲計時器根據特定幀讀取的內容。

太厲害了! 這種方法是有效的!

在這一點上,一切都接近完美,數據集幾乎準備就緒。 現在是最後一步了。 我們只需要將來自此JSON的數據與來自神秘網絡套接字的JSON匹配。 為此,我創建了這個腳本

對於一個巨大的數據集,如果沒有合適地處理它,使用起來是很麻煩的。我需要一種好方法去說「這個幀有這些邊界框+標籤」。我可以只留下一些.jpg文件和一個包含所有標籤和坐標信息的.csv文件。它看起來就像這樣:

frame_1.jpg, Ahri [120, 145], Shen [11, 678], ...
frame_2.jpg, Ahri [122, 147], Shen [15, 650], ...
frame_3.jpg, Ahri [115, 133], Shen [10, 700], ...

但這是不好的。因為CSV很煩人,JPG更煩人。另外,這意味着我將不得不對所有圖片文件重命名,以便使它們與CSV對應。這樣肯定不行。必須要有一個更好的辦法。確實有。

我將所有數據保存到一個.npz文件中用來代替JPG和CSV。這個.npz文件使用numpy的矩陣保存。Numpy是機器學習的一種語言,所以這很完美。每個圖片被保存到numpy數組中,標籤也隨之保存,就像這樣:

[
[[image_as_numpy_array],
[[Ahri, 120, 145, 130, 150],
[Shen, 122, 147, 170, 160],
...
],[[image_as_numpy_array],
[[Ahri, 125, 175, 180, 190],
[Shen, 172, 177, 190, 180],
...
]
...]

現在我們不再需要處理煩人的文件名或者CSV了。所有數據都被保存在同一個文件的許多數組中,並且可以通過索引輕鬆訪問。

最後就是困難的深度學習部分了,獲取+處理數據,已經完成了!

選擇一個神經網絡框架

假如不用模型來訓練數據,那數據有什麼用?

從最開始我就想用一個專門用於檢測物體的現有框架,因為這隻不過是個概念驗證罷了。我不想花上幾周時間來搭一個適合電游的框架。這事兒我還是留給未來的博士生們去做吧:)。

我在上文提到過,之所以選用YOLO框架,是因為它運行速度很快,而且在一度算得上先進。另外,YOLO的作者很了不起,他開放了所有源代碼,向開發人員們敞開了大門。但他寫YOLO用的是C++語言。我不太愛用C++,因為它大部分數據的代碼完全可以用Python和Node.js來做。幸好有人決定創建YAD2K,這樣大家就能用Python和Keras來使用YOLO了。說實在的,我選擇YOLO還有一個重要原因在於我讀懂了它的論文。我深知只有真的讀懂了這篇論文,才能弄清楚框架背後的核心代碼。其他熱門框架的論文我沒能讀得這麼透。YOLO只需看一眼圖像就能得出結論,這項能力比R-CNN用的上千個區域提案還要人性化。最重要的是,YOLO的代碼對照着論文就很好懂。

至於YOLO是如何運行的,我在此就不贅述了。有很多其他資源對此做出了解釋,比如這個(此處有超鏈接),就比我解釋得清楚多了!

對YOLO進行再訓練

注意:這部分的技術性較強,如果你有什麼不懂的地方,儘管在推特上問我!

YOLO是個特別有深度的神經網絡。也有可能它其實沒什麼深度,是我太容易被打動了。不管怎樣,框架是這樣的:


我用的是一台2012年的MacBook Pro。我沒法用它來訓練YOLO這麼個龐大的模型,那估計得花上好幾年時間。於是我買了個AWS EC2 GPU實例,因為我想在21世紀結束前完成模型訓練。

以下是再訓練腳本的運行方式:

  • 我沒有完全從頭開始訓練YOLO。 

  • YAD2K首先得經得住訓練前的重量,凍結主體的所有層次,然後運行5次迭代。

  • 接着,YAD2K和整個模型在未凍結的狀態下運行30次迭代。

  • 然後,當驗證損失開始增加時,YAD2K會儘早停止並退出,這樣模型就不會被過度訓練了。

所以,起初我還天真地從5個LCS遊戲中提取了大約7,500幀的數據,用YOLO運行了一遍,結果數據在2次迭代內過度擬合然後湮滅了。這倒也說得通。這個模型有很多參數,而我沒有使用任何形式的數據增強,註定行不通。

說到數據增強,我其實沒用在這個項目上。通常來說,在針對現實中的物體訓練模型時,數據增強對模型的幫助非常大。例如,一個杯子可以在圖像中顯示出數千種不同的尺寸。我們無法得到包含每個尺寸的數據集,因此使用數據增強。但就這個迷你地圖的例子而言,除了冠軍圖標和其他一些東西(比如病房)的位置,一切都是恆定不變的。由於我只用了1080p的VOD,迷你地圖的大小也是恆定的。如果我想為某位冠軍提供更多數據,數據增強會非常有用。所以我可以把迷你地圖的框架翻轉過來,然後從一個框架中得到兩個框架。但與此同時,冠軍圖標也會被翻轉,導致模型混淆。關於這一點我還沒測試過,但說不定能行呢!

經歷了第一次失敗后,我想,「好吧,算了,我用整個數據集試試」。但我的訓練腳本在大約20分鐘內一直在崩潰,因為我的系統「內存不足」(和RAM一個道理)。

再一次 溜了溜了

這也講得通,因為我的數據集十分龐大,而訓練腳本是把整個數據集都加載到RAM內存里,相當於在電腦上一下子打開十萬張圖。我本可以租一個內存更大的AWS實例,這樣就不會有任何問題;但我又摳又窮。於是我給訓練腳本添加了新功能,以便批量訓練。這樣一來,一次就只加載一部分數據集,而不是一股腦兒全都加載到內存里。得救了!

我用改進后的「批量訓練」來運行YOLO,事情總算出現了轉機。我前前後後試了有10次。在這個過程中,我把模型運行了好幾個小時,才意識到代碼有一個巨大的錯誤,於是我終止訓練,從頭再來一遍。最後,我修復了錯誤,損失終於開始降低,而模型這次也沒有過度擬合!我花了大約兩天時間,讓模型運行了完整的訓練時長。可惜這下我的錢包癟了不少,好在最終我還是得到了最後的重量。損失呈現很好的收斂,驗證損失也收斂得很好,而且正好處於訓練損失之上。

我真想向大家展示一下我那美麗的TensorFlow圖形。怪我太蠢,我把訓練后的重量保存到筆記本電腦之後,不小心刪除了我實例里的所有內容。KMS :(。我可以再花2天時間來訓練模型、分析訓練,只要先給我買GPU的錢就行:)。

差不多就是這樣了!忙了這麼久,我終於能取得些可靠的成果來用於我的任務了。寫到這兒,我本可以就此收尾,大談DeepLeague是個多麼完美的工具,我是多麼了不起。但那不是實話。特此澄清,DeepLeague還遠稱不上完美,不過我的確很了不起。

儘管DeepLeague大多數情況下都做得很好,但它仍存在一些主要問題。下面我們來看看其中一個。

在上面的輸出中,DeepLeague錯誤地把Cho'Gath標記成了Karma,還給了它1.0的置信度。這很糟。神經網絡似乎百分百確定Cho'Gath就是Karma。我在其他地方提到過,我得對數據集加以平衡,否則我就會在掌握某個冠軍很多數據的同時,只掌握另一個冠軍很少的數據。例如,我有Karma的很多數據,是因為她已經壞了,而大家都在LCS遊戲里扮演過她。但是,並沒有多少人扮演Cho'Gath!這代表我所掌握的Cho'Gath的數據要比Karma的少得多。其實,還存在一個更深層次的問題使得平衡數據集如此困難。

比方說,在Karma所在的遊戲里,我有50,000個幀,整個數據集有100,000幀。對於單獨一位冠軍而言,這麼多數據已經相當多了;而如果在很多Karma身上訓練我的網絡,可能會使得神經網絡對其他冠軍的了解變得困難很多。此外,還有可能導致嚴重的本土化問題。

我知道你們在想什麼:「扔掉點兒Karma的數據唄!」我不能為了平衡數據集而扔掉含有Karma的幀,因為這些幀同時也包含了其他9位冠軍的數據。也就是說,如果我扔掉那些含有Karma的幀,就會同時減少其他那9位冠軍的數據!我試着儘可能去平衡數據集,但由於網絡只在地圖裡的少數幾個地方見到過兩三次Cho'Gath,而在地圖上到處都能見到好多Karma,因此Cho'Gath很有可能被識別成Karma。正如很多深度學習中的問題一樣,針對這個問題最簡單的解決方案是:更多的數據。這是非常可行的,我們可以從web socket中持續抓取數據!我們還可以試試能「學着」平衡數據集的焦點損失,不過我還沒試過。

撇開對於某些冠軍的錯誤分類不看,DeepLeague仍然好得出奇。我真的很想看看這個項目是不是也能啟發些別的點子。比如,我們可以在電游里進行動作檢測嗎?這樣一來,當某些冠軍在某些時候使用某些法術時,我們就可以識別出來!當Ahri把她的Q扔出去時,她會做出某些動作,而神經網絡可以分析這些動作。試想一下,你可以對Faker是如何計算自己的能力、管理自己的法力、在地圖上漫遊的進行分析。這一切都可以通過計算機視覺實現:)。

非常感謝你閱讀本文,我最喜歡你了。如果你有任何疑問,請隨時在Twitter上給我留言。先再見啦!

博客原址 https://medium.com/@farzatv/deepleague-part-2-the-technical-details-374439e7e09a

更多文章,關注雷鋒網,添加雷鋒字幕組微信號(leiphonefansub)為好友

備註「我要加入」,To be a  AI  Volunteer !雷鋒網雷鋒網雷鋒網


想在手機閱讀更多iPhone 遊戲資訊?下載【香港矽谷】Android應用
分享到Facebook
技術平台: Nasthon Systems