Lolstats Documentación Técnica

De WikiGarcia

En este apartado se detalla el proceso de configuración, planeación y codificación del proyecto Lolstats.

Entorno y Herramientas

Dada su facilidad de uso, configuración y versatilidad para la IA, usaremos Python como lenguaje de programación principal.

La idea final del proyecto es poner el modelo disponible en una página web donde los usuarios puedan consultarlo, es por ello que usaremos Django como framework de desarrollo. También usaremos la facilidad que tiene para crear y administrar una base de datos.

Usaremos además el sistema operativo Linux para hacer todo el proyecto por su facilidad para trabajar en la Web. Las instrucciones siguientes aplican para cualquier distribución basada en Debian.

Configuración del entorno

Necesitaremos dos directorios principales:

  • Directorio de código: Aquí se colocará el proyecto Django y los cripts python necesarios. Ej: ~/lolstats/
  • Directorio de datos: Aquí se colocarán los datos extraídos de la API de Riot y del repositorio de recursos. Ej: ~/datos/

MariaDB (MySQL)

MySQL es un sistema de gestión de bases de datos basado en lenguaje SQL, es gratuito aunque es propiedad de Oracle, por eso mejor usaremos la versión de código libre llamada MariaDB la cuál tiene exactamente las mismas características.

Instalación

sudo apt update
sudo apt install mariadb-server
sudo mysql_secure_installation

A mi me gusta desactivar la validación de password porque es muy restrictiva.

mysql -u root -p
uninstall plugin validate_password;

Creación de Base de datos

mysql -u root -p
CREATE DATABASE lolstats;
CREATE USER 'usuario'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON `lolstats`.* TO 'usuario'@'localhost';

Herramientas extras (Opcinales)

  • Git: Para control de versiones.
  • Tmux: Para navegar entre ventanas de terminal.
  • Vim: Para editar archivos desde consola.

Django

Django es un framework de desarrollo web en Python basado en MVC, proporciona métodos convenientes para crear páginas web, servicios web y administrar bases de datos.

Instalación de Python

Las distribuciones Linux ya traen instalada una versión de Python. Asumiremos que con esa funciona.

Nótese que algunas veces el comando para usar python 3.x es python3 en lugar de python.

TODO: Agregar guía de instalación para la versión de Python usada.

Entorno virtual

Es recomendable trabajar en un entorno virtual para que futuras actualizaciones no afecten nuestro proyecto.

Instalamos el paquete.

pip install venv

Nos movemos al directorio de código y creamos el entorno virtual.

cd ~/lolstats/
python -m venv venv

Activamos el entorno:

source venv/bin/activate

A partir de ahora todo lo que hagamos en Python se hará con el entorno virtual. Recuerda siempre activar el entorno antes de ejecutar Python o pip.

Instalación Django

Para instalar el release oficial:

pip install django

Creación del proyecto

Solo cubriremos lo relacionado con el modelo de aprendizaje y no lo relativo a la página web.

Para crear el proyecto utilizamos el comando:

django-admin startproject lolstats

Para poder empezar a recopilar nuestros datos necesitamos configurar el acceso a base de datos. Me gusta colocar el usuario y contraseña en un archivo separado y usar el mecanismo nativo de MySQL para cargar las credenciales desde ahí.

Editar el archivo ~/lolstats/lolstats/settings.py

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'OPTIONS': {
            'read_default_file': '~/lolstats/.my.cnf',
        },
    }
}

Y generar el archivo en la ruta correspondiente ~/lolstats/.my.cnf:

[client]
user=lolstats
password=
database=lolstats

Keras

Keras es la librería de Inteligencia Artificial basada en Tensorflow que usaremos para entrenar los modelos de aprendizaje.

Instalar CUDA

Es una plataforma de desarrollo creada por Nvidia para realizar computo en paralelo, ya sea con CPU o con una GPU.

https://askubuntu.com/a/799185

Instalar CUDNN

Es una librería para crear Deep Neural Networks con la ayuda de GPUs.

https://developer.nvidia.com/cudnn https://docs.nvidia.com/deeplearning/cudnn/install-guide/index.html

Instalar Tensorflow

pip install tensorflow-gpu

Instalar Keras

pip install keras

Diseño de almacenamiento de datos

Dada la naturaleza de los datos y las fuentes de donde se obtienen, usaremos dos técnicas distintas para almacenarlos.

  • Base de datos: La idea es almacenar en tablas los datos generales de los jugadores y sus partidas, ej: IDs de jugadores o partidas, niveles, rangos, etc. El objetivo es poder utilizar consultas SQL para filtrar de forma eficiente y correcta los datos generales sin tener que parsear archivos.
  • Archivos JSON: Se usa JSON porque es el formato que usa el repositorio de recursos y la API. Se busca almacenar en texto plano el grueso de la información de los invocadores y las partidas, ya que es mucha información como para organizarla en una Base de datos (e innecesario). La idea es filtrar los datos con SQL y luego acceder a sus detalles buscando el archivo JSON correspondiente.

Diseño de Base de Datos

Las tablas que contendrá la base de datos se muestran en la siguiente figura.

Diagrama de Base de Datos

Estructura de Archivos

Se dividirán los archivos en dos directorios, dependiendo del tipo de archivo que se trate.

Data Dragon

  • Ubicación: /datos/data_dragon

Se trata de los documentos de dicho repositorio. Son documentos que contienen los datos del juego y solo se actualizan cada parche. Aquí se encuentran los catálogos del juego, como son datos de campeones, mapas, objetos, etc.

La estructura interna del directorio se mantiene igual a la original.

/datos/data_dragon
├── css
├── data
├── img
├── js
├── manifest.js
└── manifest.json

Documentos de información

  • Ubicación: /datos/lolstats

Les llamaremos así a los documentos que almacenan las respuestas de las peticiones a la API de Riot. Cuando se realiza una petición a la API (ej: Obtener los datos de una partida) se guarda la respuesta JSON en un archivo para su posterior procesamiento.

Los achivos se dividen por su tipo:

  • games: Información de los juegos.
  • summoners: Información de los invocadores.
  • tiers: Información de las colas clasificatorias.
/datos/lolstats/
├── games
├── summoners
└── tiers


Recopilación de datos

Data Dragon

La documentación completa sobre el repositorio puede encontrarse en la documentación de la API Riot para League of Legends.

El link directo de descarga se forma de la siguiente manera:

https://ddragon.leagueoflegends.com/cdn/dragontail-<version>.tgz

La información adicional que se encuentra disponible es:

Datos dinámicos

Entramos a donde el grueso de la información se concentra. La estrategia consiste en obtener la mayor cantidad de datos posible, empezando por los rangos más altos de clasificatorias.

Experimentalmente todos los datos obtenidos serán de la región de LAN.

Por la naturaleza y funcionamiento de la API de Riot, es necesario realizar una estrategia como la siguiente:

  1. Obtener los nombres e identificadores de los 1,000 primeros invocadores en el ranking (Challenger, Grandmaster, Master, etc.).
  2. Para cada invocador obtener su historial de las últimas 100 partidas clasificatorias. Esto para tener la referencia de otros invocadores en los mismos rangos.
  3. Para cada partida obtener los datos de los 9 invocadores restantes.
  4. Para cada uno de los invocadores restantes obtener los datos de las últimas 50 partidas clasificatorias (ir más allá traería datos irrelevantes).

Obtención top 1000 invocadores

Como trabajaremos con una API de pruebas con limitantes de cantidades de peticiones por minuto y por segundo, primero generaremos un algoritmo para delimitar cada petición.

global maxPerSecond = 20;
global maxPer2Minutes = 100;
global countPerSecond = 0;
global countPer2Minutes = 0;
global lastSecond = 0;
global lastMinute = 0;
function sendRequest:
    while true:
        if currentSecond = lastSecond:
            if countPerSecond < maxPerSecond:
                MaqueRequest()
                break;
            else:
                sleep 1
        else if abs(currentMinute - lastMinute) <= 2:
            if countPer2Minutes < maxPerMinute:
                MakeRequest()
                break;
            else:
                sleep 1
        else:
            MaqueRequest()
            break;

function MakeRequest():
    countPer2Minutes++;
    countPerSecond++;
    Send();
    if currentSecond != lastSecond:
        countPerSecond = 1;
        lastSecond = currentSecond;
        if not abs(currentMinute - lastMinute) <= 2:
            countPer2Minutes = 1;
            lastMinute = currentMinute;

A continuación podemos comenzar a hacer la recopilación de los datos. El proceso para obtener la lista de 1000 invocadores con sus partidas se describe en el código mostrado a continuación, donde se consultan los siguientes métodos de la API:

  • League-exp-v4 (getSummonersFromAPI): Devuelve la lista de los invocadores en una cola clasificatoria. La cola en la que se busca es RANKED_SOLO_5x5 en la división CHALLENGER I, GRANDMASTER I, MATER I y así sucesivamente hasta completar los 1,000.

https://la1.api.riotgames.com/lol/league-exp/v4/entries/RANKED_SOLO_5x5/CHALLENGER/I?page=1

  • Summoner-v4 (getSummonerById): Devuelve la información del invocador identificado con el ID de Invocador encriptado. Este dato es el que se obtiene de League-exp-v4.

https://la1.api.riotgames.com/lol/summoner/v4/summoners/{encryptedSummonerId}


  • Match-v5 (getSummonerGames): Devuelve el historial de partidas de un invocador identificado con su PUUID. Este dato se obtiene de Summoner-v4. Solo obtenemos las partidas de la cola clasificatoria.

https://la1.api.riotgames.com/lol/match/v5/matches/by-puuid/{puuid}/ids?queue=420&start=0&count=100

  • Match-v5 (getGameData): De este método también se consultan los detalles de cada partida identificada con su ID de partida.

https://la1.api.riotgames.com/lol/match/v5/matches/{matchid}

def getSummoners():
    summoners = getSummonersFromAPI();
    for sid in summoners:
        if not summonerInDB():
            summoner = getSummonerById();
            summoner.save();
        games = getSummonerGames(100)
        for game in games:
            if not gameInDB():
                game_data = getGameData(game)
                game_data.save()

Los tres elementos principales que se almacenan son:

  • Datos del invocador: Se almacena en base de datos los datos generales, mientras que se guarda un archivo json (identificado con el ID del registro) en /datos/lolstats/summoners con los detalles.
  • Datos generales de la partida: Se almacena en base de datos el identificador de Riot de la partida. Y se guarda un archivo json (identificado con el ID del registro) en /datos/lolstats/games con los detalles de la partida.
  • Relación de Invocador/Partida: En una tabla de base de datos se almacena la relación de cuáles jugadores participaron en las partidas y a qué equipo pertenecieron.

Las matemáticas indicarían que al finalizar el proceso tendríamos 100,000 partidas guardadas, sin embargo, muchas partidas se repiten ya que más de uno de los jugadores del top 100 participan en ellas.

Obtención de los invocadores restantes

A continuación se deben buscar los datos de los invocadores restantes, así que para cada partida guardada buscamos si ya se encuentra el invocador o si no lo buscamos.

Se consultan los siguientes métodos de la API:

  • Summoner-v4 (getSummoner): Devuelve la información del invocador identificado con el PUUID. Este dato es el que se obtiene del JSON de la partida.

https://la1.api.riotgames.com/lol/summoner/v4/summoners/by-puuid/{encryptedPUUID}

function fillSummonerss():
    matches = getSavedMatches()
    for match in matches:
        participants = match.getParticipants()
        for participant in participants:
            if not participantInDB():
                summoner = getSummoner()
                summoner.save()

Obtención de las partidas restantes

El último paso consiste en que para cada invocador se obtienen sus últimas 50 partidas.

  • Match-v5 (getSummonerGames): Devuelve el historial de partidas de un invocador identificado con su PUUID. Solo obtenemos las partidas de la cola clasificatoria.

https://la1.api.riotgames.com/lol/match/v5/matches/by-puuid/{puuid}/ids?queue=420&start=0&count=100

  • Match-v5 (getGameData): De este método también se consultan los detalles de cada partida identificada con su ID de partida.

https://la1.api.riotgames.com/lol/match/v5/matches/{matchid}

function fillMatches():
    summoners = getSavedSummoners()
    for summoner in summoners:
        matches = getSummonerGames(50)
        for game in games:
            if not gameInDB():
                game_data = getGameData(game)
                game_data.save()

Modelos de predicción

A continuación se enumeran los modelos y pruebas que se hicieron para obtener un modelo que pudiera predecir lo que queremos.

Redes Neuronales

La primera aproximación fue hacer una red neuronal simple (perceptrón multi-capa). La idea es crear una red con tres capas:

  1. Capa de entrada: 80 neuronas
  2. Capa intermedia: 160 neuronas
  3. Capa de salida: 2 neuronas

Esta sería un prueba simple y rápida para probar si una aproximación tan simple podría entregar algún resultado

Creación de Data Set

Con los datos crudos que se tienen, es necesario crear el set de entrenamiento y pruebas. Para este modelo, la intención inicial es incluir datos estadísticos de cada jugador, para ello se analiza su historial y se generan los vectores de pruebas.

Para cada invocador tenemos su historial de al menos 50 partidas, sin embargo, no podemos extraer los datos estadísticos de las 50 partidas para hacer nuestro dataset, debemos conservar algunas partidas para el entrenamiento y las pruebas. Por lo que se hace una distinción:

  • Partidas del dataset: Para cada invocador se considerarán sus últimas 10 partidas para ser analizadas, es decir, de esas 10 partidas se extraeran las estadísticas de cada jugador y formarán parte del dataset de entrenamiento y pruebas.
  • Partidas de estadísticas: Para cada una de las partidas del dataset se considerarán sus 40 partidas anteriores para extraer las estadísticas del jugador.

Para cada partida del dataset, los datos que debemos llenar son (Todos los valores son normalizados con base en su máximo):

  • Estadísticas del jugador (Para cada jugador):
    • Level: Máximo permitido 1200.
    • WinRate: Porcentaje de victoria en esa cola. 40 partidas anteriores.
    • WinStreak: Número de victorias seguidas al momento. 40 partidas anteriores.
    • MeanKDA: Promedio de KDA (Asesinatos, Muertes, Asistencias) en las 40 partidas anteriores.
    • ChampionMastery: Puntos de maestría con el campeón. Máximo permitido 6,000,000.
    • ChampionWinRate: Porcentaje de victorias con ese campeón. Solo se buscará en las 40 partidas anteriores.
    • ChampionMeanKDA: Promedio de KDA con el campeon.
    • ChampionGames: Partidas jugadas con el campeón. 40 partidas anteriores.

Una vez que se obtienen los datos de cada jugador se tienen que unir en un arreglo que será nuestro vector de entrada, dicho arreglo se conformará de la concatenación de los vectores de cada uno de los 10 jugadores. Es importante destacar que siempre se colocarán primero las estadísticas del equipo 1 y luego los del equipo 2, sin revolver los vectores de los equipos.

  • Estadísticas jugador 1 equipo 1
  • Estadísticas jugador 2 equipo 1
  • Estadísticas jugador 3 equipo 1
  • Estadísticas jugador 4 equipo 1
  • Estadísticas jugador 5 equipo 1
  • Estadísticas jugador 1 equipo 2
  • Estadísticas jugador 2 equipo 2
  • Estadísticas jugador 3 equipo 2
  • Estadísticas jugador 4 equipo 2
  • Estadísticas jugador 5 equipo 2

Ejemplo

  1. Para el invocador Invocador_00001 se buscan sus últimas 50 partidas: LAN_000001, ..., LAN_000050
  2. Se toman sus últimas 10 partidas para el dataset: LAN_000041, ..., LAN_000050
  3. Para cada una de las 10 partidas se sacan las estadísticas del jugador utilizando sus 40 partidas anteriores, ejemplo:
    1. Partida LAN_000041 se sacan estadísticas de partidas LAN_000039, ..., LAN_00001
    2. Partida LAN_000042 se sacan estadísticas de partidas LAN_000040, ..., LAN_00002
    3. Partida LAN_000043 se sacan estadísticas de partidas LAN_000041, ..., LAN_00003
    4. Y así sucesibamente

Entrenamiento

Se obtuvo un dataset con <N> partidas, de las cuales se usará el 80% para entrenamiento y 20% para probar el modelo.

Se utilizó la configuración más básica de Keras para entrenamiento:

El modelo a utilizar fue el siguiente:


Nota: Como no se obtuvo ningún resultado interesante se probaron algunas otras configuraciones de capas, pero ninguna hizo la diferencia (como era de esperar pues no es tan simple).

Resultados

Se pudo observar que el modelo rápidamente se sobreajustó a los datos de entrenamiento, ya que se obtenían altos valores de exactitud pero al utilizar los datos de pruebas la predicción no superó el 53% de exactitud, es decir, el modelo es inutil.

Discusión

Era de esperar que una aproximación tan aleatoria no diera ningún tipo de resultados, es por eso que ahora procederemos a implementar modelos y metodologías que han sido utilizados para situaciones similares.