Dota es un juego del género "MOBA" (Multiplayer Online Battle Arena) y es uno de los videojuegos online mas populares de los últimos años, con peaks de 700 mil usuarios jugando simultáneamente.
El juego consiste en partidas donde dos equipos de cinco jugadores cada uno se enfrentan para destruir la base enemiga. Al comienzo de una partida cada jugador elige un "héroe", que es el personaje que usará durante toda la partida. Todos los héroes tienen habilidades distintas, que los distinguen del resto. Actualmente se puede escoger de un rango de 115 héroes diferentes.
Dota posee además una escena competitiva bastante fuerte, donde durante todo el año se juegan torneos. El torneo más importante del año es el International, donde se reparten mas de 25 millones de dolares entre los 16 equipos participantes.
En principio hay dos problemas que nos gustaría resolver. El primero consiste en hacer una predicción de los resultados de las partidas basándonos principalmente en los héroes que se escogieron al principio de ésta. Esto se pretende realizar usando un sistema de redes neuronales, entrenada con un subconjunto de los mismos datos.
El otro problema tentativo a resolver consiste en establecer una correlación entre el uso del chat y las victorias/derrotas de los equipos. Para esto se observaría cuánto hablan en el chat los jugadores y qué tipo de lenguaje utilizan para luego ver si esto afecta las chances de ganar de sus equipos respectivos.
Desde el segundo hito en adelante, se opta por intentar resolver el primer problema, planteándonos como hipótesis que es posible predecir el 70% de los resultados de las partidas de dota 2 basándonos sólamente en qué héroe elige cada equipo.
El dataset que tenemos consta de 16 tablas, de las cuales la mayoría contiene datos de distintos aspectos de cada partida y se relacionan mediante el ID de cada match. Las tablas más relevantes para nuestras hipótesis son:
Para procesar los datos requeridos para resolver el problema, se hizo un script en python que lee la columna hero_id de la tabla players y guarda un 1 en un vector de tamaño 226 (113*2), en la posición hero_id si el héroe es escogido por el equipo 1 en la posición (113+hero_id) si el héroe es escogido por el equipo 2.
Además de esto, se guardaron en otro archivo los resultados de cada partida, indicando con un 1 si ganó el equipo 1 y con un 0 si ganó el equipo 2. A continuación se presenta el script que se usó:
#@title
#Este codigo carga el dataset e imagenes desde google drive
!pip install -U -q PyDrive
import os
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials
# 1. Authenticate and create the PyDrive client.
auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)
# choose a local (colab) directory to store the data.
local_download_path = os.path.expanduser('')
try:
os.makedirs(local_download_path)
except: pass
# 2. Auto-iterate using the query syntax
# https://developers.google.com/drive/v2/web/search-parameters
file_list = drive.ListFile(
{'q': "'1MDYJWd_gu8VkGxoP_2FrKBbzbw64PDnx' in parents"}).GetList()
for f in file_list:
# 3. Create & download by id.
print('title: %s, id: %s' % (f['title'], f['id']))
fname = os.path.join(local_download_path, f['title'])
print('downloading to {}'.format(fname))
f_ = drive.CreateFile({'id': f['id']})
f_.GetContentFile(fname)
##1YtobvZB2SdRMvk48diLMipF4toSxXh1F
#funciones e imports
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score
from sklearn.metrics import accuracy_score
from sklearn.metrics import classification_report
def npy_append(x1,x2):
h=x1.shape[0]
w1=x1.shape[1]
w=w1+x2.shape[1]
my_array=np.ndarray([h, w])
my_array[:,:w1]=x1
my_array[:,w1:w]=x2
return my_array
def train_and_print(red, X_train, X_test, y_train, y_test):
red.fit(X_train, y_train)
y_pred = red.predict(X_test)
print("Accuracy = ", accuracy_score(y_test, y_pred))
print(classification_report(y_test, y_pred))
f1 = f1_score(y_test, y_pred, average="macro")
print("f1 = ", f1)
def train_and_get_f1(red, X_train, X_test, y_train, y_test):
red.fit(X_train, y_train)
y_pred = red.predict(X_test)
f1 = f1_score(y_test, y_pred, average="macro")
print("f1 = ", f1)
return f1
import numpy as np
players=open("players.csv","r")
players.readline()
match=open("match.csv","r")
match.readline()
x=np.ndarray([50000,226])
y=np.ndarray([50000])
for i in range(50000):
for j in range(10):
hero = int(players.readline().split(",")[2])
if j<5:
x[i, hero] = 1
else:
x[i, hero+113] = 1
if match.readline().split(",")[9]=="TRUE":
y[i]=1
else:
y[i]=0
from IPython.display import Image, display
display(Image('oneHot.png',width=600))
Luego de procesar los datos, éstos quedan en un formato ideal para ser entregados a los clasificadores de la librería scikit.learn. El primer experimento realizado consistió en entrenar y probar una red neuronal de múltiples capas de perceptrones y analizar los resultados. Luego de esto se probó el mismo experimento, pero en vez de eso se intentó con un clasificador de Support Vector Machine optimizado con SGD. Finalmente, se intenta implementar un claisificador Naive Bayes, correspondiente al experimento 3.
Cabe destacar que, para esta serie de experimentos, se utiliza un parseo de los datos diferente al empleado en el hito 3. La diferencia radica en que, se utiliza un vector de 113 coordenadas, en las que se indican con 1 las posiciones correspondientes a héroes escogidos por el equipo 1, y -1 a los elegidos por el segundo equipo.
A continuación se muestran partes del código utilizado para realizar la clasificación.
from sklearn.model_selection import train_test_split
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import classification_report, accuracy_score
from sklearn.svm import SVC
import numpy as np
X = np.load('picks.npy')
y = np.load('winners.npy')
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .2, random_state=15, stratify = y)
#esta función entrena un clasificador y entrega un reporte para los datos.
def train_and_print(red, X_train, X_test, y_train, y_test):
red.fit(X_train, y_train)
y_pred = red.predict(X_test)
print("Accuracy = ", accuracy_score(y_test, y_pred))
print(classification_report(y_test, y_pred))
Se probaron varias configuraciones de MLPClassifier y la que mejores resultado arrojó se muestra a continuación. Como se puede ver, éste clasificador usa optimización SGD, función de activación de tangente hiperbólica y tiene tres capas ocultas de 60, 30 y 15 perceptrones, respectivamente.
redsgd6 = MLPClassifier(max_iter=500,solver='sgd', alpha=1e-5, hidden_layer_sizes=(60,30,15),
random_state=1, activation= "tanh")
train_and_print(redsgd6, X_train, X_test, y_train, y_test)
Cómo se ve en el reporte, el clasificador tiene una accuracy de 0.6062, que aún está a un 10% de diferencia de lo que se quiere alcanzar. En este caso accuracy es un buen indicador, ya que ambas clases están balanceadas.
from sklearn.linear_model import SGDClassifier
from sklearn import linear_model
import numpy as np
X = np.load('picks2.npy')
y = np.load('winners.npy')
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, recall_score, precision_score, accuracy_score, classification_report
#separación test train
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .3, random_state=15, stratify = y)
#se crea el clasificador
clf = linear_model.SGDClassifier(loss="hinge", penalty="l2", l1_ratio=0.15, max_iter=1000, shuffle=True, verbose=0, epsilon=0.1, n_jobs=1, random_state=30, learning_rate="optimal", eta0=0.0, power_t=0.5, class_weight=None, warm_start=False, average=False)
clf.fit(X_train, y_train)
El código anterior implementa un clasificador SVM, utilizando Stochastic Gradient Descent como función de optimización. A continuación se presentan los resultados obtenidos con este clasificador.
y_pred = clf.predict(X_test)
print("Accuracy = ", accuracy_score(y_test, y_pred))
print(classification_report(y_test, y_pred))
Las métricas obtenidas para este clasificador son de orden similar a las de la red neuronal. En particular, se observa un desempeño asimétricos para las clases 1 y -1, es decir, para las victorias del equipo 1 y 2.
#NAIVE BAYES
import numpy as np
from sklearn.naive_bayes import MultinomialNB
clfNB = MultinomialNB()
X = np.load('picks2.npy')
y = np.load('winners.npy')
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, recall_score, precision_score, accuracy_score, classification_report
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .3, random_state=15, stratify = y)
train_and_print(clfNB, X_train, X_test, y_train, y_test)
Se observa un desempeño para este clasificador idéntico a los resultados obtenidos en los experimentos anteriores. Nuevamente, se observan métricas asimétricas para las clases 1 y -1, clasificando de mucha mejor manera las victorias del equipo 1. Ésta disparidad en los modelos obtenidos motiva a parsear los datos de la manera en que se expuso previamente, que puede permitir a los clasificadores utilizados obtener resultados más balanceados para las dos clases. En los experimentos posteriores, se utiliza el parseo que solamente utiliza 0's y 1's.
Para este conjunto de experimentos se usó información acerca de los primeros minutos de la partida para predecir el resultado de ésta, en vez de utilizar los héroes elegidos.
A continuación se detallan los experimentos realizados.
display(Image('MLPVSINFO.png',width=600))
Como es de esperar, en el gráfico se observa una tendencia creciente, mientras más minutos de información de la partida se incluyen, mejor es el resultado arrojado por el clasificador. Cabe destacar que este resultado es bastante razonable, puesto que mientras más minutos de partida transcurren, más cerca está de terminar, por lo que se le entrega información que es más representativa del outcome de la partida.
Sin embargo, puesto que el modelo de clasificación tiene como objetivo predecir el resultado de una partida en curso, se utilizarán como atributos para las experiencias futuras, la información hasta máximo el minuto 10.
X = np.load('stats_5.npy')
y = np.load('winners01.npy')
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, recall_score, precision_score, accuracy_score, classification_report
#separación test train
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .1, random_state=15, stratify = y)
#se crea el clasificador
clfSGD1 = linear_model.SGDClassifier(loss="log", penalty="l2", l1_ratio=0.15, max_iter=1000, shuffle=True, verbose=0, epsilon=0.1, n_jobs=1, random_state=30, learning_rate="optimal", eta0=0.0, power_t=0.5, class_weight=None, warm_start=False, average=False)
train_and_print(clfSGD1, X_train, X_test, y_train, y_test)
X = np.load('stats_10.npy')
y = np.load('winners01.npy')
#separación test train
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .1, random_state=15, stratify = y)
#se crea el clasificador
clfSGD2 = linear_model.SGDClassifier(loss="log", penalty="l2", l1_ratio=0.15, max_iter=1000, shuffle=True, verbose=0, epsilon=0.1, n_jobs=1, random_state=30, learning_rate="optimal", eta0=0.0, power_t=0.5, class_weight=None, warm_start=False, average=False)
train_and_print(clfSGD2, X_train, X_test, y_train, y_test)
Los resultados observados al probar con una Support Vector Machine se encuentran dentro de niveles similares a los obtenidos al utilizar un MLP, del orden de 60% para los primeros 5 minutos de partida, y cercano a 65% para los primeros 10 de ésta.
Se realiza éste mismo experimento con un modelo de clasificación Naive Bayes, sin mucho éxito. Las métricas obtenidas con éste método se encuentran muy por debajo de lo obtenido con otros clasificadores, por lo que se omiten del documento.
Como en el experimento anterior se observaron mejores métricas, se repetirán estos experimentos usando los datos de elección de personajes y los de los primeros minutos de la partida juntos. Se espera que, la información proveída por ambos sets de atributos permite entregar una visión más completa de la partida.
X = npy_append(np.load('picks2.npy'),np.load('stats_5.npy'))
y = np.load('winners01.npy')
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .2, stratify = y)
first = 70
second = 50
third = 20
alpha = 1e-3
activation = "relu" #relu, logisitc, tanh
max_iter = 100
solver = 'sgd' #adam, sgd, lbfgs
red1 = MLPClassifier(max_iter=max_iter, solver=solver, alpha=alpha,
hidden_layer_sizes=(first,second,third), activation= activation)
f1=train_and_print(red1, X_train, X_test, y_train, y_test)
X = npy_append(np.load('picks2.npy'),np.load('stats_10.npy'))
y = np.load('winners01.npy')
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .2, stratify = y)
red2 = MLPClassifier(max_iter=max_iter, solver=solver, alpha=alpha,
hidden_layer_sizes=(first,second,third), activation= activation)
f1=train_and_print(red2, X_train, X_test, y_train, y_test)
clfNB = MultinomialNB()
X = npy_append(np.load('picks2.npy'),np.load('stats_5.npy'))
y = np.load('winners01.npy')
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .3, random_state=15, stratify = y)
train_and_print(clfNB, X_train, X_test, y_train, y_test)
clfNB2 = MultinomialNB()
X = npy_append(np.load('picks2.npy'),np.load('stats_10.npy'))
y = np.load('winners01.npy')
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .3, random_state=15, stratify = y)
train_and_print(clfNB2, X_train, X_test, y_train, y_test)
import numpy as np
X = npy_append(np.load('picks2.npy'),np.load('stats_5.npy'))
y = np.load('winners01.npy')
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, recall_score, precision_score, accuracy_score, classification_report
from sklearn.linear_model import SGDClassifier
from sklearn import linear_model
#separación test train
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .1, random_state=15, stratify = y)
#se crea el clasificador
clfSGD1 = linear_model.SGDClassifier(loss="log", penalty="l2", l1_ratio=0.15, max_iter=1000, shuffle=True, verbose=0, epsilon=0.1, n_jobs=1, random_state=30, learning_rate="optimal", eta0=0.0, power_t=0.5, class_weight=None, warm_start=False, average=False)
train_and_print(clfSGD1, X_train, X_test, y_train, y_test)
X = npy_append(np.load('picks2.npy'),np.load('stats_10.npy'))
y = np.load('winners01.npy')
#separación test train
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = .1, random_state=15, stratify = y)
#se crea el clasificador
clfSGD2 = linear_model.SGDClassifier(loss="log", penalty="l2", l1_ratio=0.15, max_iter=1000, shuffle=True, verbose=0, epsilon=0.1, n_jobs=1, random_state=30, learning_rate="optimal", eta0=0.0, power_t=0.5, class_weight=None, warm_start=False, average=False)
train_and_print(clfSGD2, X_train, X_test, y_train, y_test)
A continuación se incluyen gráficos que sintetizan los resultados obtenidos para cada clasificador:
display(Image('MLP.png',width=600))
display(Image('NB.png',width=600))
Observamos que para Naive Bayes y Multilayer Perceptron (red neuronal), el mejor desempeño se observa al entregar como atributos las elecciones de personajes, e información de los primeros 10 minutos de la partida. Se alcanzan f1-scores del orden de 62% y 64% respectivamente.
display(Image('SVM.png',width=600))
Para sorpresa del equipo, el clasificador que entrega los mejores desempeños corresponde a una SVM, utilizando como atributos de entrada la elección de personajes, y datos de los primeros 10 minutos de partida. Ésta combinación permite alcanzar la meta planteada al comienzo del proyecto, obteniéndose métricas (accuracy y f1-score) del orden de 70%.
Con los resultados obtenidos hasta el hito 2, no fue posible verificar la hipótesis. Esto se debe probablemente a que el resultado de una partida está definido por muchos mas factores que sólo quien elige a qué héroe.
Sin embargo, al agregar información sobre los primeros minutos de la partida se lograron conseguir clasificadores con puntuaciones más cercanas a lo que se había propuesto.
Otros factores que pueden afectar el resultado de una partida pueden incluir el nivel de habilidad de cada uno de los jugadores, su estado emocional, si la dinámica del equipo es buena, etc., pero estos datos no se encuentran disponibles, por lo que no se pueden utilizar.
Otra opción para mejorar el clasificador es hacer que el clasificador usado entregue probabilidades de victoria en vez de resultados categóricos, utilizando alguna herramienta para hacer regresión.
Además de lo anterior, al comparar los diferentes clasificadores, se observó que el clasificador que entrega los mejores resultados es el SVC. Esto va contra lo que se intuía inicialmente, ya que se creía que lo que mejor funcionaría sería el clasificador de redes neuronales, ya que es el más complejo.