ML .NET: IA para ajudar o Detetive Pikachu com Descobertas sobre Pokémon
Olá pessoa!
Que tal ajudarmos o detetive Pikachu em uma investigação usando IA para descobertas sobre Pokémon?
Este post faz parte de uma série sobre o ML.NET! Para visualizar a série inteira clique aqui
Nessa publicação vamos usar inteligência artificial para ajudar o detetive Pikachu a investigar uma base sobre pokémon de diferentes regiões.
A base de dados que usaremos com as características dos pokémon foi encontrada no Github do usuário armgilles. Essa base contém os atributos base: HP, Attack, Sp. Attack, Defense, Sp. Defense e Speed. Além disso, contém outras informações como: nome, número, tipos, região e identificação de lendários.
Essa base contém as mega evoluções e todos os pokémon até a região de Kalos (dos jogos Pokémon X & Y), você pode verificar o formato dos dados na imagem abaixo:
Agora vamos criar nosso projeto F# para importar esses dados usando Type Providers!
Crie um projeto do tipo Console em F# normalmente e como quase sempre fazemos por aqui, instale o pacote FSharp.Data:
PM> Install-Package FSharp.Data
Se você nunca utilizou o CsvProvider sugiro que dê uma olhadinha nesse post aqui) e depois volte pra cá!
Assim como fizemos no post anterior de ML.NET, vamos fazer o download do arquivo CSV e incorporá-lo no projeto, conforme imagem a seguir:
Lembre-se também de alterar a propriedade Copy to Output Directory para Copy if newer.
Feito isso, vamos carregar nosso CSV:
[<Literal>]
let url = "Dataset/pokemon.csv"
type PokemonCSV = CsvProvider<url>
[<EntryPoint>]
let main argv =
let allPokemon = PokemonCSV.Load url
Simples assim!
Agora vamos criar o nosso próprio tipo para Pokémon, obtendo apenas as propriedades que forem necessárias para nossa exploração.
Para isso, vamos criar um novo arquivo chamado Types e incluir o nosso tipo lá:
module Types
type Pokemon = {
Number : single
Name : string
Type1: string
Type2 : string
Total: single
HP : single
Attack : single
Defense : single
SpAttack : single
SpDefense : single
Speed : single
}
Note que estamos alterando o tipo de todas as propriedades básicas para single, isso porque nos cálculos que vamos usar poderemos precisar de casas decimais.
Agora vamos voltar para a função main
e transformar os dados carregados do CSV para valores do nosso domínio. Para isso, basta utilizarmos um map
, conforme código:
let listOfPokemon = allPokemon.Rows
|> Seq.map (fun pokemon -> {
Number = (single) pokemon.Number
Name = pokemon.Name
Type1 = pokemon.``Type 1``
Type2 = pokemon.``Type 2``
HP = (single) pokemon.HP
Attack = (single) pokemon.Attack
SpAttack = (single) pokemon.``Sp. Atk``
Defense = (single) pokemon.Defense
SpDefense = (single) pokemon.``Sp. Def``
Speed = (single) pokemon.Speed
Total = (single) pokemon.Total
})
Agora como estamos utilizando o ML.NET, precisamos criar nosso contexto de machine learning, sem ele não conseguimos nem carregar os nossos dados para treinamento:
let mlContext = new MLContext();
let data = listOfPokemon
|> mlContext.Data.LoadFromEnumerable
Neste ponto já preparamos nossos dados, agora é hora de entender o que vamos fazer e o que estamos tentando descobrir nessa base!
Clusterização
Diferente da classificação de comentários feito anteriormente, não temos nenhuma classificação previamente feita em nossa base de dados. Isso implica que não temos a resposta para treinar o modelo.
O que fazer então?
Vamos utilizar o que chamamos de aprendizado não supervisionado. Isso significa que executaremos um algoritmo que precisará encontrar a relação entre os dados por si só. Para fazer isso precisamos informar quantos grupos (clusters) queremos classificar a base e quais são os parâmetros para localizar a similaridade entre os grupos.
De forma bastante simplista, o algoritmo irá separar os dados na quantidade de clusters que informarmos. Para fazer isso será gerado um centróide com uma média de todas as características determinadas.
Depois disso, a distância de cada registro para cada centróide é calculada. No fim desse cálculo, o registro fará parte do cluster que possui o centróide mais próximo.
Para fazer essa clusterização, usaremos o algoritmo K-Means.
K-Means no ML.NET
Como já dito, precisamos escolher as informações para calcular os centróides dos clusters. Para nosso problema, vamos usar os tipos e atributos base.
Só temos um pequeno problema, os tipos são do tipo string
e o restante dos valores são single
. Precisamos transformar os tipos em um valor single
também, dessa forma todas as informações (features) utilizadas serão do mesmo tipo.
Vamos começar criando uma nova propriedade no tipo Pokemon:
type Pokemon = {
//...
Type1: string
Type2 : string
ConvertedType1:single
ConvertedType2:single
//...
}
Agora vamos criar um método para converter o tipo de string
para single
. Basta fazermos um pattern matching para isso:
let typeToSingle pkmnType =
match pkmnType with
|"Bug" -> 0
|"Dark"-> 1
|"Dragon"-> 2
|"Electric"-> 3
|"Fairy"-> 4
|"Fighting"-> 5
|"Fire"-> 6
|"Flying"-> 7
|"Ghost"-> 8
|"Grass"-> 9
|"Ground"-> 10
|"Ice"-> 11
|"Normal"-> 12
|"Poison"-> 13
|"Psychic"-> 14
|"Rock"-> 15
|"Stell"-> 16
|"Water"-> 17
| _ -> -1
|> (single)
Pronto! Agora já podemos preencher esse valor no momento que carregamos o CSV:
//...
let listOfPokemon = allPokemon.Rows
|> Seq.map (fun pokemon -> {
//...
Type1 = pokemon.``Type 1``
ConvertedType1 = (typeToSingle pokemon.``Type 1``)
Type2 = pokemon.``Type 2``
ConvertedType2 = (typeToSingle pokemon.``Type 2``)
//...
})
Feito!
Agora já podemos voltar ao processo de treinamento do algoritmo e informar todos as propriedades que serão utilizadas para fazer a clusterização.
Faremos isso criando um EstimatorChain
que deve criar uma coluna chamada “Features” com todas as propriedades, veja:
let pipeline = EstimatorChain().Append(
mlContext.Transforms.Concatenate( "Features",
"ConvertedType1","ConvertedType2",
"HP","Attack","SpAttack",
"Defense", "SpDefense", "Speed","Total" ))
Essa é a hora de criarmos o algoritmo que treinará o modelo, para isso, precisamos indicar o número de clusters e o campo com as features que acabamos de criar:
let options = Trainers.KMeansTrainer.Options()
options.NumberOfClusters <- 3
options.FeatureColumnName <- "Features"
let trainer = mlContext.Clustering.Trainers.KMeans options
Neste ponto, para criar o modelo basta unirmos a pipeline criada anteriormente com o nosso algoritmo de treino:
let pipelineTraining = pipeline.Append trainer
let model = pipelineTraining.Fit data
Agora já podemos utilizar nosso modelo? -Quase.
Precisamos criar um PredictionEngine
. Ele pode ser gerado através de nosso modelo, no entanto, para criarmos um motor de predição precisamos informar o tipo usado como entrada e o tipo de saída.
Vamos voltar no nosso arquivo Types.fs
e criar o tipo para a saída do algoritmo, ele deve conter o cluster e a distância para todos os centróides:
open Microsoft.ML.Data
//...
[<CLIMutable>]
type ClusterPrediction = {
[<ColumnName("PredictedLabel")>]
PredictedClusterId : uint32
[<ColumnName("Score")>]
Distances : single array
}
Veja que estamos utilizando as anotações do ML.NET para indicar os valores resultantes.
Voltando para a função main
, já podemos criar nosso motor de predição:
let predictiveModel =
mlContext.Model.CreatePredictionEngine<Pokemon, ClusterPrediction>(model)
Agora que temos nosso motor de predição, vamos percorrer nossa base e verificar o resultado para cada registro:
let clusterizedList =
listOfPokemon
|> Seq.map(fun pokemon -> pokemon,(predictiveModel.Predict pokemon))
Note que criamos uma sequence agrupando o pokémon e seu resultado em uma tupla. Isso facilitará nosso trabalho tendo uma coleção unificando com os dois valores.
Vamos simplesmente exibir o resultado no console, mas vamos fazer isso em um formato de CSV:
printfn "Number; Name; Total; Type1; Type2; Cluster"
clusterizedList
|> Seq.iter(fun (pkmn, result) -> printfn "%i;%s;%i;%s;%s;%i"
((int)pkmn.Number)
pkmn.Name
((int)pkmn.Total)
pkmn.Type1
pkmn.Type2
result.PredictedClusterId
Simples, não?
Ao executar já podemos conferir o resultado, veja:
Podemos exportar o resultado do console para um arquivo CSV, dessa forma fica mais fácil de visualizarmos os dados. Faremos isso da forma mais simples e sem precisar alterar nosso código, vamos utilizar o prompt de comandos do Windows.
Primeiro navegue até o diretório do projeto e execute o projeto redirecionando o console para o arquivo:
DIRETORIO DO PROJETO> dotnet run PokeClustering >pkmn.csv
Feito isso temos um arquivo CSV pronto para visualizarmos. Podemos abrir no Excel, realizar filtros e fazer qualquer tipo de operação:
Ao realizar filtros por cluster fica bastante claro o tipo a classificação, veja os primeiros registros do Cluster 1:
Não precisamos de muito tempo para verificar que todos os pokémon do cluster 1 não são necessariamente poderosos. Talvez os mais poderosos que tenha aparecido nesses registros sejam mesmo o Pidgeotto e o grande Pikachu.
Note que eu digo talvez, mesmo com o status base deles sendo claramente maior. Isso porque nossos dados são uma versão resumida dos monstrinhos, não estamos levando em consideração a lista de movimentos nem o nível necessário para evolução, por exemplo.
A média do total dos atributos do cluster 1 ficou em: 303.90, sendo a menor média de todos os clusters. Vamos chamar este cluster de “Pokémon mais fracos”.
Vamos primeiro para o terceiro cluster, afinal ele é o cluster intermediário.
Note que podemos ver as evoluções dos pokémon iniciais e até uma mega evolução (Mega Beedrill). A média deste cluster ficou em: 472.97.
Por fim, temos o cluster dos pokémon mais poderosos! Esse cluster possui uma média de 622.57. Ele é formado praticamente por lendários, pseudo-lendários (como o Dragonite) e Mega evoluções:
Note que existem alguns outliers, como o Arcanine, por exemplo. Ele possui uma média relativamente menor do que resto, mas acaba estando mais próximo deste cluster do que do anterior.
Já conseguimos visualizar como o agrupamento funcionou nesta planilha, mas que tal gerarmos um gráfico completo?
Visualização dos dados
Não vamos conseguir gerar um gráfico usando todas as features utilizadas para a clusterização, isso porque o gráfico teria dimensões demais para ser visualizado. Mas podemos ter um resultado muito semelhante em um gráfico de 3 dimensões: onde X e Y representam o tipo do pokémon e Z representa o Total dos atributos.
Para fazer esse gráfico vamos instalar o pacote nuget: Xplot.Plotly.
PM> Install-Package Xplot.Plotly
Agora precisamos criar os dados para nosso gráfico, podemos fazer isso através de uma list comprehension, onde iremos iterar por todos os cluster, gerando a série de dados:
let chartData = [
for cluster in 1..options.NumberOfClusters do
let pkmn = clusterizedList
|> Seq.filter( fun (pkmn, result) -> result.PredictedClusterId = (uint32) cluster)
yield //...
]
Ok, estamos percorrendo os clusters e filtrando os pokémon de cada um, mas o que precisamos retornar no yield
?
Precisamos de um valor do tipo Scatter3d
, nele podemos definir quais valores serão usados em quais eixos, qual valor deve ser utilizado para o label e até o tamanho do ponto no gráfico.
let chartData = [
for cluster in 1..options.NumberOfClusters do
let pkmn = clusterizedList
|> Seq.filter( fun (pkmn, result) -> result.PredictedClusterId = (uint32) cluster)
yield Scatter3d(
x = (pkmn |> Seq.map( fun (pkmn, result) -> pkmn.ConvertedType1)),
y = (pkmn |> Seq.map( fun (pkmn, result) -> pkmn.ConvertedType2)),
z = (pkmn |> Seq.map( fun (pkmn, result) -> pkmn.Total)),
text = (pkmn |> Seq.map( fun (pkmn, result) -> pkmn.Name)),
mode = "markers",
marker =
Marker(
size = 12.,
opacity = 0.8
)
)
]
Pronto, agora é só configurarmos algumas coisinhas e realizarmos a plotagem:
let chartOptions = Options ( title = "Pokémon Cluster")
let chart =
chartData
|> Chart.Plot
|> Chart.WithOptions chartOptions
|> Chart.WithHeight 600
|> Chart.WithWidth 800
|> Chart.WithLabels ["Pokémon mais fracos"; "Pokémon muito poderosos";"Pokémon"]
Note que nos labels estou nomeando os clusters para facilitar a visualização. Agora com o mesmo snippet que sempre utilizamos podemos fazer o gráfico ser gerado e visualizado no Chrome:
let html = chart.GetHtml()
File.AppendAllLines ("metrics.html",[html])
Process.Start (@"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
"file:\\" + Directory.GetCurrentDirectory() + "\\metrics.html")
|> ignore
Você pode conferir o resultado do gráfico gerado (apenas para os pokémon da primeira geração) abaixo:
Atenção
Optei por colocar apenas os pokémon da primeira região para facilitar a visualização, caso esteja interessado, você pode visualizar o gráfico com todos eles aqui.
Com isso utilizamos K-means com ML.NET para realizar a clusterização e análise de uma base Pokémon!
Atenção
Você pode verificar o código fonte completo aqui e os arquivos resultantes (modelo e CSV) aqui.
Bom, o post de hoje termina por aqui aqui.
Espero que tenham gostado, qualquer dúvida, correção ou sugestão, deixem nos comentários!
Até mais.
Sempre vale lembrar que as informações e textos aqui no blog representam minha opinião pessoal, o que pode não ser igual à sua ou de qualquer outra pessoa, incluindo a empresa para qual eu trabalho. Portanto as publicações inseridas aqui estão relacionadas somente a mim.