Py学习  »  机器学习算法

我用深度学习分析 LoL 小地图,自制数据集 DeepLeague 开源(下)

AI研习社 • 6 年前 • 911 次点击  

本文为雷锋字幕组编译的技术博客,原标题 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(https://github.com/remixz) 的开发者的帮助。这位开发者创建了接下来我将介绍的神秘的网络套接字数据集(我喜欢这样叫它)。

当你在 lolesports.com 上面观看英雄联盟比赛直播时,实际上有一个神秘的网络套接字,它不断地抛出关于这场比赛直播的数据。我称之为神秘的网络套接字因为有很少人知道这件事,而且它似乎是半隐藏的。套接字产生的数据包含这场比赛中的选手名字、他选择的英雄以及每一时刻的英雄位置和血量。它以这种形式存在是因为这样可以让直播软件在网页上实现统计功能。

你现在可能想知道我如何使用这个数据的了!

我创建了我自己的节点脚本(与 remixz 所创建的类似),每当神秘的网络套接字开启,脚本监听传入的数据,并将数据保存到一个 JSON 文件中。我在 AWS EC2 机器上托管了这个脚本,现在我正在自动保存 LCS(英雄联盟冠军联赛)中北美赛区和欧洲赛区的比赛数据!

如果你仍对数据感到好奇,这里是从 LCS 一场比赛中获取的小片段(http://t.cn/RE6IDVq ),可以让你更好地理解它是什么样子的。

这个 JSON 数据本身其实没什么用。但是请记住,我们这样做的目的是为了创建一个有标签的数据集。这个数据集是带标签的小地图图片,标签表示各个英雄在小地图上的位置。我并不关心 JSON 数据本身。

我应该补充说明的是,深度联盟只从比赛中识别了 55 个英雄(http://t.cn/RE6IsrE ),因为我只是用 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(http://suo.im/4Fhm2d ),其中也有 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 匹配。 为此,我创建了这个脚本(http://t.cn/RE6Mx8O )。

对于一个巨大的数据集,如果没有合适地处理它,使用起来是很麻烦的。我需要一种好方法去说 “这个帧有这些边界框 + 标签”。我可以只留下一些. 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 的作者(http://suo.im/2FLpOc )很了不起,他开放了所有源代码,向开发人员们敞开了大门。但他写 YOLO 用的是 C++ 语言。我不太爱用 C++,因为它大部分数据的代码完全可以用 Python 和 Node.js 来做。幸好有人决定创建 YAD2K(http://suo.im/InHKs ),这样大家就能用 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 实例,这样就不会有任何问题;但我又抠又穷。于是我给训练脚本添加了新功能(http://t.cn/RE6Mn5i ),以便批量训练。这样一来,一次就只加载一部分数据集,而不是一股脑儿全都加载到内存里。得救了!

我用改进后的 “批量训练” 来运行 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 中持续抓取数据!我们还可以试试能 “学着” 平衡数据集的焦点损失(https://arxiv.org/abs/1708.02002 ),不过我还没试过。

撇开对于某些冠军的错误分类不看,DeepLeague 仍然好得出奇。我真的很想看看这个项目是不是也能启发些别的点子。比如,我们可以在电游里进行动作检测吗?这样一来,当某些冠军在某些时候使用某些法术时,我们就可以识别出来!当 Ahri 把她的 Q 扔出去时,她会做出某些动作,而神经网络可以分析这些动作。试想一下,你可以对 Faker 是如何计算自己的能力、管理自己的法力、在地图上漫游的进行分析。这一切都可以通过计算机视觉实现:)。

非常感谢你阅读本文,我最喜欢你了。如果你有任何疑问,请随时在 Twitter 上给我留言。先再见啦!

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

更多文章,关注雷锋网,添加雷锋字幕组微信号(leiphonefansub)为好友

备注「我要加入」,To be a  AI  Volunteer !

NLP 工程师入门实践班:基于深度学习的自然语言处理

三大模块,五大应用,手把手快速入门 NLP

海外博士讲师,丰富项目经验

算法 + 实践,搭配典型行业应用

随到随学,专业社群,讲师在线答疑

▼▼▼



新人福利



关注 AI 研习社(okweiwu),回复  1  领取

【超过 1000G 神经网络 / AI / 大数据,教程,论文】



我用深度学习分析 LoL 小地图,自制数据集 DeepLeague 开源(上)


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/XvdCIndCFu
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/7944
 
911 次点击