Back-End

13 jun, 2018

O que é Machine Learning e como funciona

Publicidade

Dirigir, investir, detectar fraudes, classificar doenças e criar medicamentos. Há pouco tempo essas atividades eram feitas somente por seres humanos. Hoje, com o desenvolvimento de técnicas de aprendizado de máquina (machine learning), podemos ensinar sistemas a executá-las com precisão igual – ou superior – a nossa.

O machine learning usa algoritmos para entender o modelo (a lógica, o padrão) que dá origem a um conjunto de dados para conseguir prever ou classificar novos valores.

A programação tradicional baseia-se em definir cada etapa que o programa deve executar para obter um resultado. Com machine learning, a ideia é fazer com que ele aprenda os passos necessários para isto.

A grande vantagem ocorre quando trabalhamos com problemas complexos, onde o algoritmo não é bem definido, como identificar pessoas em uma foto. É muito difícil escrever um programa que faça isso bem feito, pois a variedade de cenários possíveis é muito grande. Existe uma infinidade de poses, locais, ângulos, iluminação e cores em uma foto.

Preparar um algoritmo para trabalhar com todos estes cenários é muito trabalhoso. Com machine learning, podemos ensinar o computador a fazer essa tarefa sem programar o passo a passo, apenas ensinando com base em exemplos.

Podemos separar os algoritmos de machine learning conforme seu tipo de aprendizagem, ou seja, a forma como aprendem.

Tipos de aprendizagem

Existem três principais categorias: aprendizagem supervisionada, não supervisionada e por reforço.

Aprendizagem supervisionada

Imagine um programa que usa machine learning para identificar fotos de cachorros. Vou usar dois dados de entrada para treiná-lo:

  • Imagem: fotos variadas, algumas de cachorros, e outras não.
  • Boolean: um boolean que indica se a foto é ou não de um cachorro.

O treinamento ocorre quando o programa “vê” uma imagem junto com sua resposta, ou seja, esta imagem é de um cachorro? Imagine isto se repetindo para milhares de imagens diferentes. Chega um momento em que o programa identifica as características que fazem uma imagem ser a imagem de um cachorro.

Esta é a aprendizagem supervisionada: dizemos ao computador o que é cada entrada (qual o label) e ele aprende quais características daquelas entradas fazem elas serem o que são.

De acordo com o tipo de resultado do algoritmo, podemos classificá-lo entre algoritmo de classificação ou algoritmo de regressão.

O exemplo anterior é um exemplo de classificação. Estamos classificando uma entrada entre dois tipos possíveis: cachorro ou não-cachorro.

Já a regressão ocorre quando o resultado é numérico. Por exemplo, um programa que calcula o valor que uma casa deveria ter com base em características como número de quartos, localização e ano de construção. Com base em exemplos de casas similares, o computador aprende a precificar novas casas.

Aprendizagem não supervisionada

Nos três tipos de aprendizagem, existe a semelhança do computador aprender a inferir algo com base em suas experiências passadas. A diferença da aprendizagem não supervisionada para a supervisionada é que aqui a aprendizagem ocorre com dados não rotulados, ou seja, não dizemos ao computador o que é aquela entrada.

Por exemplo, vamos imaginar uma distribuidora que quer classificar seus clientes em categorias. Ela cria um programa que usa aprendizagem não supervisionada para fazer isso. Ou seja, ela ensina a separar os dados em grupos semelhantes, sem dizer o que são estes grupos. Um possível resultado é um grupo de clientes que compram produtos frescos, e outro com clientes que compram produtos industrializados, por exemplo.

Da mesma forma com a classificação de imagens. Temos um modelo que aprendeu a classificar imagens entre dois grupos distintos. Ao receber uma nova imagem, com base em seus atributos, ele identifica a qual grupo ela pertence, sem necessariamente saber o que é aquele grupo.

Aprendizagem por reforço

A terceira classificação é a aprendizagem por reforço. Imagine criar um programa responsável por dirigir um veículo autônomo. Ele deve aprender a dirigir pelas ruas e transportar seus passageiros. Existem diversas formas de otimizar esta tarefa. Por exemplo, chegar ao destino no menor tempo possível e não causar nenhum acidente.

Queremos que ele saiba o que fazer conforme o que ocorre à sua volta, e preferimos que ele demore um pouco mais do que causar um acidente, por exemplo. A aprendizagem por reforço é uma forma de ensinar ao computador qual ação priorizar dada uma determinada situação.

Posso vincular recompensas e punições aos possíveis resultados e, ponderando-as da forma certa, ensinar o nível de prioridade de cada meta. Neste exemplo, posso atribuir uma punição muito maior em caso de um acidente em comparação a se atrasar poucos minutos. Desta forma, se ensina quais ações o computador deve priorizar.

Exemplo

Darei um exemplo simples de um programa que utiliza aprendizagem supervisionada para estimar o valor de uma casa conforme suas características (retorno numérico, ou seja, regressão).

Podemos aplicar machine learning com qualquer tecnologia. Neste exemplo, usarei Python, uma das linguagens mais populares neste meio, devido à grande quantidade de bibliotecas de machine learning disponíveis para ela.

Tecnologias

  • Python 2.7: linguagem de programação.
  • NumPy: biblioteca com funções estatísticas que vai nos ajudar a compreender o conjunto de dados (dataset).
  • Pandas: biblioteca para facilitar o trabalho com arquivos CSV.
  • SciKit Learn: biblioteca que contém os algoritmos de machine learning.

Fluxo de trabalho

Existem muitos detalhes a se considerar quando se escreve um programa que usa machine learning para aprender. Durante o desenvolvimento deste exemplo, comentarei sobre alguns deles.

No geral, um típico fluxo de trabalho é:

  • Importar os dados do arquivo CSV para treinar o modelo.
  • Extrair estatísticas destes dados, para entender sua forma, distribuição e qualidade.
  • Pré-processar os atributos do dataset. Tratar os tipos e remover outliers.
  • Definir a métrica de desempenho.
  • Separar os dados em conjunto de treinamento e teste.
  • Treinar o modelo.
  • Fazer inferências.

1 – Importar os dados

Aqui irei importar as bibliotecas necessárias, ler o arquivo CSV e imprimir algumas informações sobre ele:

# -*- coding: utf-8 -*-

import numpy as np
import pandas as pd
from sklearn.cross_validation import ShuffleSplit
from sklearn.cross_validation import train_test_split
from sklearn.metrics import r2_score
from sklearn.metrics import make_scorer
from sklearn.grid_search import GridSearchCV
from sklearn.tree import DecisionTreeRegressor

# Lendo conjunto de dados de imóveis
data = pd.read_csv('housing.csv')
prices = data['MEDV']
features = data.drop('MEDV', axis = 1)

print "\n\nO conjunto de dados de imóveis tem {} pontos " \
	  "com {} variáveis em cada.\n\n".format(*data.shape)

Resultado:

O conjunto de dados de imóveis tem 489 pontos com 4 variáveis em cada.

Os pontos são os imóveis (cada linha do arquivo representa um imóvel). As variáveis são as características deles, ou seja, neste dataset há 489 imóveis, cada um com quatro características. Um detalhe importante: uma dessas características é o valor do imóvel. É o que quero inferir, a variável alvo. E é o label que será utilizado no treinamento (lembre -se: na aprendizagem supervisionada treinamos o computador informando o valor associado às características aprendidas).

Aqui estão as quatro características:

  • RM: número médio de quartos entre os imóveis na vizinhança.
  • LSTAT: porcentagem de proprietários na vizinhança considerados de “baixa renda”.
  • PTRATIO: razão de estudantes para professores nas escolas de ensino fundamental e médio na vizinhança.
  • MEDV: valor do imóvel.

E um exemplo dos dados:

2 – Extrair estatísticas

# Extraindo estatísticas
minimum_price = np.amin(prices)
maximum_price = np.amax(prices)
mean_price = np.average(prices)
median_price = np.median(prices)
std_price = np.std(prices)

print "Estatísticas para os dados dos imóveis:\n"
print "Preço mínimo: ${:,.2f}".format(minimum_price)
print "Preço máximo: ${:,.2f}".format(maximum_price)
print "Preço médio: ${:,.2f}".format(mean_price)
print "Preço mediano: ${:,.2f}".format(median_price)
print "Desvio padrão dos preços: ${:,.2f}".format(std_price)

Resultado:

Estatísticas para os dados dos imóveis:

Preço mínimo: $105,000.00
Preço máximo: $1,024,800.00
Preço médio: $454,342.94
Preço mediano: $438,900.00
Desvio padrão dos preços: $165,171.13

Entender o dataset (dataset) com o qual se está trabalhando é muito importante. A qualidade do modelo treinado depende da qualidade dos dados utilizados para treiná-lo. Um exemplo de característica que se percebe ao olhar as estatísticas são os outliers, valores atípicos, inconsistentes com o resto. É importante tratá-los utilizando alguma abordagem, como remover o elemento do conjunto, ou utilizar um valor médio em seu lugar.

3 – Pré-processar os atributos

Para não estender muito o exemplo, escolhi um dataset pronto para utilizar. Isso significa que não será necessário tratar valores nulos, remover outliers e converter tipos de variáveis. No mundo real isso é muito difícil de acontecer. Técnicas de análise de dados devem ser utilizadas para tratar o dataset antes de utilizá-lo.

4 – Definir a métrica de desempenho

É preciso saber o quão bem o computador aprendeu. Algumas métricas nos ajudam a avaliar isso, para que seja possível comparar o desempenho entre modelos de aprendizado diferentes, parâmetros diferentes e conjuntos de treinamento diferentes.

Existem diversas métricas de avaliação, cada uma adequada a um tipo de problema. Por exemplo, accuracy, precision e recall para problemas de classificação, e erro médio absoluto, erro quadrático médio, coeficiente de determinação (R²) ou percentual de variância para problemas de regressão.

Usarei o coeficiente de determinação, R². Ele varia entre 0 e 1 e descreve a porcentagem da correlação ao quadrado entre a estimativa e o valor atual da variável alvo. Ou seja, um modelo com coeficiente de determinação 0 nunca acerta suas estimativas. Já um com coeficiente de determinação 1 estima perfeitamente.

# Definindo função de avaliação utilizando métrica Rˆ2. Melhor score = 1.0
def performance_metric(y_true, y_predict):
	return r2_score(y_true, y_predict)

5 – Dividir o dataset em subconjuntos de treinamento e de teste

Temos 489 imóveis no dataset e queremos treinar nosso modelo utilizando estes dados. Porém, não adianta apenas treinar sem saber como ele irá performar ao inferir sobre dados nunca antes vistos. Para ter certeza de que o treinamento funcionou, é preciso ter uma forma controlada de saber o quanto ele acerta em suas estimativas. Para isso, podemos dividir o dataset em dois: uma parte utilizada para treinar o modelo, e a outra para aferir sua assertividade.

O benefício de separar o dataset em subconjuntos de treinamento e de teste, é que poderemos testar o modelo contra dados nunca antes vistos durante a fase de treinamento, e assim prevenir erros de variância, que ocorrem quando o modelo é excessivamente sensível aos dados com que foi treinado.

Variância é um problema que ocorre quando o modelo performa muito bem nos dados de treinamento, e muito mal nos dados de teste. Ou seja, ele se prendeu muito à estrutura dos dados com os quais foi treinado. Isso significa que sua capacidade de generalização não é boa. Ou seja, para novos dados diferentes daqueles do conjunto de treinamento, o modelo não conseguirá realizar estimativas precisas.

Este é um dos problemas que o conjunto de teste resolve. Se a performance não for boa nele, sabemos que o modelo não está bom.

# Misturando e separando os dados em conjuntos de treinamento e teste
X_train, X_test, y_train, y_test = train_test_split(features, prices, test_size = 0.2, random_state = 123)
print "\nSeparação entre treinamento e teste feita com êxito.\n"

Usei a função train_test_split do sklearn para misturar os dados e separar 20% para o conjunto de teste.

6 – Treinar o modelo

Existem diversos algoritmos de machine learning possíveis para treinar este modelo. A escolha do mais adequado depende de vários fatores. Cada um é mais adequado a um determinado tipo de problema:

Posso escolher três ou quatro algoritmos diferentes e comparar a performance para escolher o melhor. Para simplificar o exemplo e a explicação, usarei o algoritmo de árvore de decisão (decision tree).

Uma árvore de decisão divide um conjunto de dados em subconjuntos menores (chamados nós). Cada vez que um subconjunto é criado, mais precisão temos nas estimativas do modelo, pois características em comum são gradualmente separadas em subconjuntos diferentes. Para estimar, percorre-se a árvore do topo até o último nó, de acordo com as características da entrada.

Assim como os outros algoritmos de machine learning, a árvore de decisão possui parâmetros alteráveis (hyper-parameters) para melhor atender os diversos cenários. Para ela, o principal é a profundidade da árvore (max_depth). Quanto mais profunda, mais níveis de decisão existem, o que torna-a mais complexa e mais ajustada aos dados de treinamento.

É preciso tomar cuidado para o modelo não ficar muito sensível às nuances dos dados de treinamento, o que pode ocorrer com uma árvore muito profunda. Isto daria ao modelo uma baixa capacidade de generalização. Da mesma forma, não podemos ter uma árvore muito rasa, pois ela não será capaz de compreender a complexidade do cenário que estamos tentando modelar. Para escolher o melhor parâmetro nesta situação, é possível usar a técnica de busca em matriz (grid search).

Grid search consiste em analisar múltiplas combinações de parâmetros. A cada combinação, é feita uma análise da performance do algoritmo. No final, escolhe-se o parâmetro que deu o melhor resultado.

def fit_model(X, y):
	# Gera conjuntos de validação-cruzada para o treinamento de dados
	cv_sets = ShuffleSplit(X.shape[0]          # qt total elementos
	                     , n_iter = 10         # qt vezes embaralhar e dividir
	                     , test_size = 0.2
	                     , random_state = 123)
	
	grid = GridSearchCV(DecisionTreeRegressor()
	                  , dict(max_depth = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
			  , make_scorer(performance_metric)
		          , cv = cv_sets)
	
	# Encontrando os melhores parâmetros do estimador
	grid = grid.fit(X, y)
	
	return grid.best_estimator_

Criei a função fit_model que recebe os atributos do dataset de treinamento (X) e seus labels (preço – y). Os dados são embaralhados e divididos em conjuntos utilizando a função ShuffleSplit. O resultado disso é alimentado no objeto GridSearchCV, que fará a busca em matriz para encontrar o melhor parâmetro.

Repare que passei como parâmetro para ele qual algoritmo será utilizado para gerar o modelo (DecisionTreeRegressor()) e também qual parâmetro quero encontrar o melhor valor (max_depth = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]). Ou seja, encontrar a melhor profundidade para a árvore, entre 1 até 10.

O parâmetro make_scorer recebe a função performance_metric que criei anteriormente, e é o resultado dela que será utilizado para saber qual a performance obtida com o algoritmo de árvore de decisão com cada uma das opções de profundidade.

Por fim, o método fit do GridSearchCV encontra e treina o melhor algoritmo de árvore de decisão, e retorna uma instância dele, já treinada com base nos dados de treinamento.

7 – Fazer previsões

Finalmente! Usando as funções definidas anteriormente, treino a árvore de decisão chamando a função fit_model. Depois, crio alguns imóveis de teste e mando o algoritmo treinado fazer a previsão de seus valores. Com base nos atributos deles, e no conhecimento da relação entre estes atributos e o valor (conhecimento que o programa obteve ao ser treinado utilizando a árvore de decisão), ele estima qual deveria ser o valor destes três novos imóveis que criei para testar.

# Cria um regressor (DecisionTree) com o parâmetro 'max_depth
# otimizado para os dados de treinamento
regressor = fit_model(X_train, y_train)

print "O parâmetro 'max_depth' otimizado " \
      "para o modelo é {}.\n".format(regressor.get_params()['max_depth'])

client_data = [[5, 17, 15], # Imóvel 1
               [4, 32, 22], # Imóvel 2
               [8, 3, 12]]  # Imóvel 3

# Mostra as estimativas
for i, price in enumerate(regressor.predict(client_data)):
	print "Preço estimado para o imóvel {}: ${:,.2f}".format(i+1, price)

Resultado:

O parâmetro 'max_depth' otimizado para o modelo é 4.

Preço estimado para o imóvel 1: $408,870.00
Preço estimado para o imóvel 2: $232,662.50
Preço estimado para o imóvel 3: $892,850.00

Finalizando

Acesse o meu Github para baixar o código completo deste exemplo, juntamente com o dataset.

Existem muitos detalhes que não foram cobertos por este artigo para não estendê-lo demais. A ideia é dar uma introdução ao assunto, para que possamos entender um pouco do que é machine learning e como é implementar programas que utilizam este recurso.

Grande parte do sucesso de um modelo vem da qualidade dos dados, por isso é importante dar a devida atenção a esta parte do processo. No mundo real, os dados provavelmente precisarão ser tratados antes de treinar um algoritmo de machine learning, e existe toda uma ciência específica para fazer isso.