基于深度学习⽅法的dota2游戏数据分析与胜率预测
(python3.6+keras框架实现)
这篇⽂章分为两⼤部分,第⼀部分为数据获取,第⼆部分为建模预测。
Part 1,数据获取
1.接⼝分析
①请求单场⽐赛
api.opendota/api/matches/{match_id}
调⽤该URL可以根据⽐赛ID来获得单场⽐赛的详细信息,包括游戏起始时间,游戏持续时间,游戏模式,⼤厅模式,天辉/夜魇剩余兵营数量,玩家信息等,甚⾄包括游戏内聊天信息都有记录。
上⾯就是⼀条聊天记录⽰例,在这局游戏的第7条聊天记录中,玩家“⾼⾼兴兴把家回”发送了消息:”1指1个⼩朋友”。
②随机查10场⽐赛
该URL会随机返回10场近期⽐赛的基本数据,包括游戏起始时间,对阵双⽅英雄ID,天辉是否胜利等数据。
该接⼝URL返回该英雄对应的基本信息,包括有英雄属性,近战/远程,英雄名字,英雄有⼏条腿等等。这⾥我们只对英雄名字这⼀条信息进⾏使⽤。
这个接⼝URL可以返回opendota数据库的表名称和其所包含的列名,在写sql语句时会有所帮助,⼀般与下⽅的数据浏览器接⼝配合使⽤。
⑤数据浏览器
该接⼝⽤来对⽹站的数据库进⾏访问,所输⼊参数为sql语句,可以对所需的数据进⾏筛选。如下图就是在matches表中寻ID=5080676255的⽐赛的调⽤⽅式。
但是在实际使⽤中发现,这个数据浏览器接⼝仅能够查询到正式⽐赛数据,像我们平时玩的游戏情况在matches数据表⾥是不存在的。
⑥公开⽐赛查
该接⼝URL可以查询到我们所需要的在线游戏对局数据,其输⼊参数less_than_match_id指的是某局
游戏的match_id,该接⼝会返回100条⼩于这个match_id的游戏对局数据,包括游戏时间,持续时间,游戏模式,⼤厅模式,对阵双⽅英雄,天辉是否获胜等信息。本次建模所需的数据都是通过这个接⼝来进⾏获取的。
2.通过爬⾍获取游戏对局数据
这次实验准备建⽴⼀个通过对阵双⽅的英雄选择情况来对胜率进⾏预测的模型,因此需要获得以下数据,[天辉⽅英雄列表]、[夜魇⽅英雄列表]、[哪⽅获胜]。
此外,为了保证所爬取的对局质量,规定如下限制条件:平均匹配等级>4000,游戏时间>15分钟(排除掉秒退局),天梯匹配⽐赛(避免普通⽐赛中乱选英雄的现象)。
⾸先,完成数据爬取函数:
1#coding:utf-8
2
3import json
4import requests
5import time
6
7 base_url = 'api.opendota/api/publicMatches?less_than_match_id='
8 session = requests.Session()
9 session.headers = {
10'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'
11 }
12
13def crawl(input_url):
14    time.sleep(1)  # 暂停⼀秒,防⽌请求过快导致⽹站封禁。
15    crawl_tag = 0
16while crawl_tag==0:
17try:
18            ("www.opendota/")  #获取了⽹站的cookie
19            content = (input_url)
20            crawl_tag = 1
21except:
22print(u"Poor internet connection. We'll have another try.")
23    json_content = json.)
24return json_content
这⾥我们使⽤request包来新建⼀个公共session,模拟成浏览器对服务器进⾏请求。接下来编辑爬取函数crawl(),其参数 input_url 代表opendota 所提供的API链接地址。由于没有充值会员,每秒钟只能向服务器发送⼀个请求,因此⽤sleep函数使程序暂停⼀秒,防⽌过快调⽤导致异常。由于API返回的
数据是json格式,我们这⾥使⽤json.loads()函数来对其进⾏解析。
接下来,完成数据的筛选和记录⼯作:
1 max_match_id = 5072713911    # 设置⼀个极⼤值作为match_id,可以查出最近的⽐赛(即match_id最⼤的⽐赛)。
2 target_match_num = 10000
3 lowest_mmr = 4000  # 匹配定位线,筛选该分数段以上的天梯⽐赛
4
5 match_list = []
6 recurrent_times = 0
7 write_tag = 0
8 with open('../data/matches_list_ranking.csv','w',encoding='utf-8') as fout:
9    fout.write('⽐赛ID, 时间, 天辉英雄, 夜魇英雄, 天辉是否胜利\n')
10while(len(match_list)<target_match_num):
11        json_content = crawl(base_url+str(max_match_id))
12for i in range(len(json_content)):
13            match_id = json_content[i]['match_id']
14            radiant_win = json_content[i]['radiant_win']
15            start_time = json_content[i]['start_time']
16            avg_mmr = json_content[i]['avg_mmr']
17if avg_mmr==None:
18                avg_mmr = 0
19            lobby_type = json_content[i]['lobby_type']
20            game_mode = json_content[i]['game_mode']
21            radiant_team = json_content[i]['radiant_team']
22            dire_team = json_content[i]['dire_team']
23            duration = json_content[i]['duration']  # ⽐赛持续时间
24if int(avg_mmr)<lowest_mmr:  # 匹配等级过低,忽略
25continue
26if int(duration)<900:  # ⽐赛时间过短,⼩于15min,视作有⼈掉线,忽略。
27continue
28if int(lobby_type)!=7 or (int(game_mode)!=3 and int(game_mode)!=22):
29continue
30            x = time.localtime(int(start_time))
31            game_time = time.strftime('%Y-%m-%d %H:%M:%S',x)
32            one_game = [game_time,radiant_team,dire_team,radiant_win,match_id]
33            match_list.append(one_game)
34        max_match_id = json_content[-1]['match_id']
35        recurrent_times += 1
36print(recurrent_times,len(match_list),max_match_id)
37if len(match_list)>target_match_num:
38            match_list = match_list[:target_match_num]
39if write_tag<len(match_list):  # 如果⼩于新的⽐赛列表长度,则将新⽐赛写⼊⽂件
40for i in range(len(match_list))[write_tag:]:
41                fout.write(str(match_list[i][4])+', '+match_list[i][0]+', '+match_list[i][1]+', '+\
42                    match_list[i][2]+', '+str(match_list[i][3])+'\n')
43            write_tag = len(match_list)
在上述代码中,⾸先定义⼀个 max_match_id ,即表明搜索在这场⽐赛之前的对局数据,另外两个变量target_match_num 和 lowest_mmr 分别代表所需记录的对局数据数量、最低的匹配分数。
外层while循环判断已经获取的⽐赛数据数量是否达到⽬标值,未达到则继续循环;在每次while循环中,⾸先通过crawl()函数获取服务器返回的数据,内层for循环对每⼀条json数据进⾏解析、筛选(其中lobby_type=7是天梯匹配,game_mode=3是随机征召模式,game_mode=22是天梯全英雄选择模式)。在for循环结束后,更新max_match_id的值(使其对应到当前爬取数据的最后⼀条数据,下⼀次爬取数据时则从该位置继续向下爬取),再将新爬取的数据写⼊csv数据⽂件。⼯作流程如下⽅图⽰,其中蓝框表⽰条件判断。
最终,我们通过该爬⾍爬取了10万条游戏对阵数据,其格式如下:
这10万条数据包含了10⽉16⽇2点到10⽉29⽇13点期间所有的⾼分段对局数据,每⼀条数据共有5个属性,分别是[⽐赛ID,开始时间,天辉英雄列表,夜魇英雄列表,天辉是否胜利] 下⾯开始⽤这些数据来进⾏建模。
Part 2,建模及预测
1.训练数据制作
⼀条训练(测试)样本分为输⼊、输出两个部分。
输⼊部分由⼀个⼆维矩阵组成,其形状为[2*129]其中2代表天辉、夜魇两个向量,每个向量有129位,当天辉(夜魇)中有某个英雄时,这个英雄id所
对应的位置置为1,其余位置为0。因此,⼀条样本的输⼊是由两个稀疏向量组成的⼆维矩阵。(英雄id的取值范围为1-129,但实际只有117个英雄,有些数值没有对应任何英雄,为了⽅便样本制作,将向量长度设为129)
输出部分则是⼀个整形的标量,代表天辉⽅是否胜利。我们将数据⽂件中的True使⽤1来代替,False使⽤0来代替。
因此,10万条样本最终的输⼊shape为[100000,2,129],输出shape为[100000,1]。
1# ===================TODO 读取对局数据  TODO========================
2 with open('../data/matches_list_ranking_all.csv','r',encoding='utf-8') as fo_1:
3    line_matches = adlines()
4    sample_in = []
5    sample_out = []
6for i in range(len(line_matches))[1:]:
7        split = line_matches[i].split(', ')
8        radiant = split[2]
9        dire = split[3]
10# print(split[4][:-1])
11if split[4][:-1]=='True':
12            win = 1.0
13else:
14            win = 0.0
15        radiant = list(map(int,radiant.split(',')))
16        dire = list(map(int,dire.split(',')))
17        radiant_vector = np.zeros(hero_id_max)
18        dire_vector = np.zeros(hero_id_max)
19for item in radiant:
20            radiant_vector[item-1] = 1
21for item in dire:
22            dire_vector[item-1] = 1
23        sample_in.append([radiant_vector,dire_vector])
24        sample_out.append(win)
之后,我们将样本进⾏分割,按照8:1:1的⽐例,80000条样本作为训练集,10000条样本作为测试集,
10000条样本作为验证集。其中验证集的作⽤是模型每在训练集上训练⼀个轮次以后,观测模型在验证集上的效果,如果模型在验证集上的预测精度没有提升,则停⽌训练,以防⽌模型对训练集过拟合。
1def make_samples():
2    train_x = []
3    train_y = []
4    test_x = []
5    test_y = []
6    validate_x = []
7    validate_y = []
8for i in range(len(sample_in)):
9if i%10==8:
10            test_x.append(sample_in[i])
11            test_y.append(sample_out[i])
12elif i%10==9:
13            validate_x.append(sample_in[i])
14            validate_y.append(sample_out[i])
15else:
16            train_x.append(sample_in[i])
17            train_y.append(sample_out[i])
18return train_x,train_y,test_x,test_y,validate_x,validate_y
2.搭建深度学习模型
考虑到⼀个样本的输⼊是由两个稀疏向量组成的⼆维矩阵,这⾥我们⼀共搭建了三种模型,CNN模型,
LSTM模型以及CNN+LSTM模型。那为什么⽤这三种模型呢,我其实也做不出什么特别合理的解释~~先试试嘛,效果不⾏丢垃圾,效果不错真⽜B。
①CNN模型
模型构建的⽰意图如下,使⽤维度为长度为3的⼀维卷积核对输⼊进⾏卷积操作,之后再经过池化和两次全链接操作,将维度变为[1*1],最后使⽤sigmoid激活函数将输出限定在[0,1]之间,即对应样本的获胜概率。下图展现了矩阵、向量的维度变化情况。
下⽅为模型代码,考虑到使⽤⼆维卷积时,会跨越向量,即把天辉和夜魇的英雄卷到⼀起,可能对预测结果没有实际帮助,这⾥使⽤Conv1D来对输⼊进⾏⼀维卷积。经过卷积操作后,得到维度为[2,64]的矩阵,再使⽤配套的MaxPooling1D()函数加⼊池化层。下⼀步使⽤Reshape()函数将其调整为⼀维向量,再加上两个Dropout和Dense层将输出转换成⼀个标量。
1 model = Sequential()
2 model.add(Conv1D(cnn_output_dim,kernel_size,padding='same',activation='relu',input_shape=(team_num,hero_id_max)))  #(none,team_num,129) 转换为 (none,team_num,32)
3 model.add(MaxPooling1D(pool_size=pool_size,data_format='channels_first'))  #(none,team_num,3
2)转换为 (none,team_num,16)
4 model.add(Reshape((int(team_num*cnn_output_dim/pool_size),), input_shape=(team_num,int(cnn_output_dim/pool_size))))
5 model.add(Dropout(0.2))
6 model.add(Dense((10),input_shape=(team_num,cnn_output_dim/pool_size)))
7 model.add(Dropout(0.2))
8 model.add(Dense(1))              # 全连接到⼀个元素
9 model.add(Activation('sigmoid'))
10 modelpile(loss='mse',optimizer='adam',metrics=['accuracy'])
在实际的调参过程中,卷积核长度,卷积输出向量维度,Dropout的⽐例等参数都不是固定不变的,可以根据模型训练效果灵活的进⾏调整。
②LSTM模型
模型构建的⽰意图如下,LSTM层直接以[2,129]的样本矩阵作为输⼊,⽣成⼀个长度为256的特征向量,该特征向量经过两次Dropout和全连接,成为⼀个标量,再使⽤sigmoid激活函数将输出限定在[0,1]之间。
下⽅为构建LSTM模型的代码,要注意hidden_size参数即为输出特征向量的长度,在进⾏调参时,也是⼀个可以调节的变量。
1 model = Sequential()
2 model.add(LSTM(hidden_size, input_shape=(team_num,hero_id_max), return_sequences=False))  # 输⼊(none,team_num,129)  输出向量 (hidden_size,)
3 model.add(Dropout(0.2))
4 model.add(Dense(10))
5 model.add(Dropout(0.2))
6 model.add(Dense(1))              # 全连接到⼀个元素
7 model.add(Activation('sigmoid'))
8 modelpile(loss='mse',optimizer='adam',metrics=['accuracy'])
③CNN+LSTM模型
模型构建的⽰意图如下,与CNN模型很像,唯⼀区别是将reshape操作由LSTM层进⾏替换,进⽽⽣成⼀个长度为256的特征向量。
CNN+LSTM模型的代码如下,与前两个模型⽅法类似,这⾥不再详细解说。
1 model = Sequential()
2 model.add(Conv1D(cnn_output_dim,kernel_size,padding='same',input_shape=(team_num,hero_id_max)))  #(none,team_num,9) 转换为 (none,team_num,32)
3 model.add(MaxPooling1D(pool_size=pool_size,data_format='channels_first'))  #(none,team_num,32)转换为 (none,team_num,16)
4 model.add(LSTM(hidden_size, input_shape=(team_num,(cnn_output_dim/pool_size)), return_sequences=False))  # 输⼊(none,team_num,129)  输出向量 (hidden_size,)
5 model.add(Dropout(0.2))
6 model.add(Dense(10))
7 model.add(Dropout(0.2))
8 model.add(Dense(1))              # 全连接到⼀个元素
9 model.add(Activation('sigmoid'))
10 modelpile(loss='mse',optimizer='adam',metrics=['accuracy'])
3.设置回调函数(callbacks)
回调函数是在每⼀轮训练之后,检查模型在验证集上的效果,如经过本轮训练,模型验证集上的预测效果⽐上⼀轮要差,则回调函数可以做出调整学习率或停⽌训练的操作。
1 callbacks = [keras.callbacks.EarlyStopping(monitor='val_loss', patience=2, verbose=0, mode='min'),\
2    keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=1, verbose=0, mode='min',\
3          epsilon=0.0001, cooldown=0, min_lr=0)]
4 hist = model.fit(tx,ty,batch_size=batch_size,epochs=epochs,shuffle=True,\
5    validation_data=(validate_x, validate_y),callbacks=callbacks)
6 model.save(model_saved_path+model_name+'.h5')
先设定回调函数callbacks,其中monitor=’val_loss’是指对验证集的损失进⾏监控,如果这个loss经过⼀轮训练没有继续变⼩,则进⾏回调;patience参数指的是等待轮次,在上述代码中,如果连续1轮训练’val_loss’没有变⼩,则调整学习率(ReduceLROnPlateau),如果连续2轮训
练’val_loss’没有变⼩,则终⽌训练(EarlyStopping)。
最后使⽤model.fit()函数开始训练,tx,ty是训练集的输⼊与输出,在validation_data参数中需要传⼊我们的验证集,⽽在callbacks参数中,需要传⼊我们设置好的回调函数。
4.预测效果
我们将训练后的模型来对测试集进⾏预测,经过这⼀天的反复调参,得到了多个预测效果不错的模型,
最⾼的模型预测准确度可以达到58%。即,对于“看阵容猜胜负”这个任务,模型可以达到58%的准确率。为了更全⾯的了解模型的预测效果,我们分别计算模型在测试集、训练集、验证集上的预测准确度,并计算模型在打分较⾼的情况下的预测精度。以下⾯这个模型为例:
可以看出,模型在训练集上的预测效果稍好,超过61%,⽽在训练集和验证集上的预测准确度在58%附近,没有出现特别明显的过拟合现象。
此外对于测试中的10000个样本,有4514个被模型判断为拥有60%以上的胜率,其中2844个预测正确,准确率达到63%;
有378场⽐赛被模型判断为拥有75%以上的胜率,其中281场预测正确,准确率74.3%;
有97场⽐赛被模型判断为拥有80%以上的胜率(阵容选出来就⼋⼆开),其中72场预测正确,准确率74.2%;
安卓在线解析json还有8场被模型认定为接近九⼀开的⽐赛,预测对了7场,准确率87.5%。
可以看出,模型给出的预测结果具有⼀定的参考价值。
为了对预测效果有些直观的感受,修改代码让模型对预测胜率⼤于0.85的⽐赛阵容进⾏展⽰。
这8场⽐赛,模型全部预测天辉胜率,胜率从85%~87%不等。如果让我来看阵容猜胜负的话,我是没有信⼼给到这么⾼的概率的。这8场⽐赛中,帕吉的出现次数很多,达到了5次,我在max+上查询了⼀下帕吉的克制指数:
从上图可以看出,在上⾯的8场⽐赛中,修补匠、⼈(⼯程师)、幽⿁、狙击⼿、魅惑魔⼥这些英雄确实出现在了帕吉的对⾯。这也说明我们模型的预测结果与统计层⾯上所展⽰出来的结论是较为⼀致的。
写在最后:
2.展望⼀下应⽤场景。
①选好阵容以后,⽤模型预测⼀下,阵容82开或91开的话,直接秒退吧,省的打完了不开⼼。╮( ̄﹏ ̄)╭
②对⽅已经选好阵容,我⽅还差⼀个英雄没选的情况下,使⽤模型对剩下来的英雄进⾏预测,选出胜率最⾼的英雄开战。实现起来较为困难,估计程序还没跑完,选英雄的时间就已经到了。
③参与电竞⽐赛,根据预测结果下注。这个嘛,鉴于天梯单排和职业战队⽐赛观感上完全不⼀样,估计模型不能做出较为准确的预测。
3.可能会有同学会问这次的10万条样本能不能包含所有的对阵可能性,结论是否定的。我也是在开展本次实验之前计算了⼀下,真是不算不知道,⼀算吓⼀跳。游戏⼀共有117个英雄,天辉选择5个,夜魇在剩余的112个⾥⾯选5个,⼀共能选出来
种不同的对阵,⼤概是2*1016!10万条样本完全只是九⽜⼀⽑⽽已。
4.最后吐槽⼀下V社的平衡性吧,这次爬取的10万条⽐赛记录,天辉胜利的有54814条,夜魇胜利的有45186条。说明当前版本地图的平衡性也太差了,天辉胜率⽐夜魇胜率⾼了9.6%。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。