fbpx

Vídeo: Machine Learning na sua empresa: casos de uso

Tutorial: Aumentando o Poder Preditivo de Seus Modelos de Machine Learning com Stacking Ensembles

Quem acompanha competições sabe que uma das coisas mais importantes é saber juntar vários modelos para criar uma solução poderosa. Várias pessoas já me perguntaram, por e-mail ou nas apresentações que fiz, sobre ensembles. Este é um assunto importante não apenas para competições, mas também para casos reais onde se quer extrair o máximo possível de performance dos modelos.

Ensembles são conjuntos de modelos que oferecem uma performance melhor do que cada modelo que o compõe.

Então neste artigo quero exemplificar a melhor maneira que conheço de criar ensembles: stacking. Este é um método que usei em todas as competições que tive bons resultados.

Antes de começar, uma dica: é importante pensar em Machine Learning como “processos”. Aqui não vamos apenas testar modelos em um banco de dados, mas vamos testar processos, pipelines de métodos aplicadas aos dados para saber quais são os resultados.

Este material é parte da apresentação que fiz no dia 30/11 no PyData Meetup São Paulo.

O Jupyter Notebook original está diponível neste link.

In [1]:
import pandas as pd
import numpy as np

Carregando os dados

Os dados utilizados são transações comerciais de imóveis na cidade de Ames, Iowa. Nosso objetivo é prever o preço de venda de uma casa alimentando o modelo com as características. Estes dados também são tema de uma competição no Kaggle. Os dados de treino e teste podem ser encontrados em: https://www.kaggle.com/c/house-prices-advanced-regression-techniques/

Algo muito importante é explorar os dados em busca de ideias de features e métodos para validar os modelos. Como o foco deste material é stacking, vou pular esta parte. Ainda assim, acho importante esclarecer que temos dois tipos básicos de variáveis nestes dados: categóricas e numéricas.

As variáveis categóricas possuem níveis que não podem ser ordenados. Em alguns casos é possível pensar em maneiras de ordená-los, como uma variável que descreve se uma rua é asfaltada ou não. Neste caso pode-se pensar que a rua asfaltada vai valorizar o imóvel.

As variáveis numéricas podem receber qualquer valor contínuo. Um exemplo seria o tamanho do terreno.

Nesta célula carregamos os dados e criamos um DataFrame com as features (X) e uma Série com o preço da venda (SalePrice, y) que é nosso alvo.

No total temos 79 colunas, mas limitei a 5 aqui por causa da formatação do site.

In [2]:
train = pd.read_csv('train.csv', index_col='Id')
X, y = train.drop('SalePrice', axis=1), train.SalePrice.copy()
train.head().iloc[:, :5]
Out[2]:
MSSubClass MSZoning LotFrontage LotArea Street
Id
1 60 RL 65.0 8450 Pave
2 20 RL 80.0 9600 Pave
3 60 RL 68.0 11250 Pave
4 70 RL 60.0 9550 Pave
5 60 RL 84.0 14260 Pave

Conjuntos de Variáveis (Features)

Uma das maneiras de obter modelos diferentes é variar a representação dos dados utilizada para treiná-los. Por isso nosso primeiro passo será construir estas representações.

Funções auxiliares

A métrica sugerida pelo Kaggle para avaliar os modelos é o RMSLE, este erro leva em conta a diferença entre o logaritmo das previsões e do alvo. É possível pensar neste erro como uma aproximação do erro percentual do modelo, mas com propriedades mais interessantes do ponto de vista matemático.

Criei três funções para cálculo do erro. Nós vamos fazer transformações da variável y, então para facilitar nosso trabalho, decidi criar funções personalizadas para cada transformação, assim evita alguma confusão na hora de transformá-las para computar o erro numa função só.

Para usarmos algumas funções do Scikit-learn que vão ajudar a manter nosso código mais limpo e mais eficiente, precisamos criar as funções de erro da maneira requerida pelo módulo. Neste caso, a função pecisa receber um modelo treinado, as features e o alvo. Ela computará as previsões e deverá retornar um número, que é o valor da métrica de erro.

A última linha da célula cria um objeto da validação cruzada do scikit-learn. Ele vai cuidar da divisão dos dados para nós. Este assunto merece uma série de artigos, mas basicamente é uma maneira de dividir os dados em N partes, e usar repetidamente N-1 partes como dados de treino, e a parte restante como dados de teste. Por exemplo, se temos as partes [1, 2, 3, 4, 5], numa das iterações podemos treinar usando as partes [1, 2, 3, 4] e validar na parte 5.

Apesar desta ser uma série temporal, o Kaggle decidiu tratar como dados aleatórios independentes, então por isso faremos esta validação cruzada simples.

In [3]:
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import cross_val_score, cross_val_predict, KFold
from sklearn.ensemble import RandomForestRegressor

def rmsle(estimator, X, y):
    p = estimator.predict(X)
    return np.sqrt(mean_squared_error(np.log1p(y), np.log1p(p)))

def rmsle_log_y(estimator, X, y):
    p = estimator.predict(X)
    return np.sqrt(mean_squared_error(y, p))

def rmsle_sqrt_y(estimator, X, y):
    p = estimator.predict(X)
    y = np.power(y, 2)
    p = np.power(p, 2)
    return np.sqrt(mean_squared_error(np.log1p(y), np.log1p(p)))

kf = KFold(n_splits=5, shuffle=True, random_state=1)

Feature set 1: variáveis “numéricas”

O primeiro conjunto de features que teremos será uma seleção simples das variáveis originalmente numéricas dos dados. Para isto basta selecionar todas as colunas que possuem variáveis com números inteiros ou de ponto flutuante. Além disso, para substituir os valores nulos (o scikit-learn exige a substituição), decidi colocar o valor -1. Como vamos utilizar apenas modelos baseados em árvores com estes dados originais, não há necessidade de se preocupar muito com isso para nossos propósitos.

Após armazenar estes dados na variável X1, vemos que são 36 colunas. Já nesta parte quero treinar um modelo de Random Forest dentro da validação cruzada, para sabermos como ele se sairia sozinho nos dados originais. Para isso utilizei a função cross_val_score. Ela é uma função que facilita bastante na hora de fazer a validação cruzada com scikit-learn. Basta colocar um modelo e os dados. Neste caso optei por especificar um esquema de validação e uma métrica de erro personalizada.

Esta função retorna uma lista com os erros de cada iteração da validação cruzada, então fiz a média para sabermos o erro médio das partes.

In [4]:
X1 = X.select_dtypes(include=[np.number]).fillna(-1)
print('Dims', X1.shape)
model = RandomForestRegressor(n_estimators=1000, random_state=0)
error = cross_val_score(model, X1, y, cv=kf, scoring=rmsle).mean()
print('RMSLE:', error)
Dims (1460, 36)
RMSLE: 0.14582352618

Feature set 2: Ordinal Encoding Categóricas

Agora vamos criar um outro conjunto de features, desta vez adicionando as variáveis categóricas. Existem várias maneiras de codificar este tipo de variável para os modelos, uma delas é usando um formato ordinal. Isso simplesmente significa substituir cada valor original por números sequenciais. Em alguns modelos isso pode ser problemático, pois eles tentarão capturar alguma relação de ordem em valores que podem não ter. No nosso caso, com modelos baseados em árvores de decisão, este problema é quase inexistente.

Após codificar desta maneira, rodamos novamente a validação cruzada, agora nestes novos dados.

In [5]:
from sklearn.preprocessing import LabelEncoder

X2 = X.copy()
for col in X2.columns:
    if X2[col].dtype == object:
        enc = LabelEncoder()
        X2[col] = enc.fit_transform(X[col].fillna('Missing'))

print('Dims', X2.shape)
X2.fillna(-1, inplace=True)
model = RandomForestRegressor(n_estimators=1000, random_state=0)
error = cross_val_score(model, X2, y, cv=kf, scoring=rmsle).mean()
print('RMSLE:', error)
Dims (1460, 79)
RMSLE: 0.143837364859

Bônus: OneHot Encoding Categóricas

A maneira mais popular de codificar variáveis categóricas é o One Hot Encoding. Basicamente consiste em transformar cada valor da variável em uma coluna cujo novo valor será 1 caso a variável tenha aquele valor num determinado exemplo, ou 0 em caso negativo. Existem indicativos que árvores de decisão não processem tão bem este tipo de representação, mas em alguns casos práticos já vi isto funcionar melhor que o ordinal, então veja esta como mais uma ferramenta.

Este método cria mais de 200 novas colunas, o que deixa o processo de treino mais devagar, então decidi deixar a linha da validação cruzada comentada. Caso queira ver o resultado, basta rodá-la sem o jogo da velha.

In [6]:
#from sklearn.preprocessing import OneHotEncoder
X3 = X.copy()
cats = []
for col in X3.columns:
    if X3[col].dtype == object:
        X3 = X3.join(pd.get_dummies(X3[col], prefix=col), how='left')
        X3.drop(col, axis=1, inplace=True)
    

print('Dims', X3.shape)
X3.fillna(-1, inplace=True)
model = RandomForestRegressor(n_estimators=1000, random_state=0)
#cross_val_score(model, X3, y, cv=kf, scoring=rmsle).mean()
Dims (1460, 288)

Transformações do Target

Uma maneira interessante de criar diversidade, e às vezes até obter uma melhor performance, num caso de regressão, é transformar a variável que estamos tentando prever. Neste caso testaremos duas transformações: logaritmo e raiz quadrada.

Log

É possível ver que tentar prever o logaritmo do preço nos dá um resultado melhor. Isto acontece não só pelo fato do modelo capturar padrões diferentes, mas também porque usamos uma métrica baseada na diferença de logaritmos.

In [7]:
model = RandomForestRegressor(n_estimators=1000, random_state=0)
error = cross_val_score(model, X1, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()
print('RF, X1, log-target RMSLE:', error)

model = RandomForestRegressor(n_estimators=1000, random_state=0)
error = cross_val_score(model, X2, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()
print('RF, X2, log-target RMSLE:', error)
RF, X1, log-target RMSLE: 0.14518580749
RF, X2, log-target RMSLE: 0.14207134495

Raiz Quadrada

Esta transformação também nos dá um resultado melhor do que usar a variável em seu estado original. Uma das sugestões da razão pela qual vemos este efeito é que estas transformações fazem com que a variável y tenha uma distribuição mais próxima da normal, o que facilita o trabalho do modelo.

In [8]:
model = RandomForestRegressor(n_estimators=1000, random_state=0)
error = cross_val_score(model, X1, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()
print('RF, X1, sqrt-target RMSLE:', error)

model = RandomForestRegressor(n_estimators=1000, random_state=0)
error = cross_val_score(model, X2, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()
print('RF, X2, sqrt-target RMSLE:', error)
RF, X1, sqrt-target RMSLE: 0.145652934484
RF, X2, sqrt-target RMSLE: 0.143004600132

Gerando modelos com modelos/algoritmos diferentes

Outra maneira de gerar diversidade para o ensemble é gerar modelos diferentes. Neste caso vou usar meu modelo preferido, o GBM. Este também é baseado em árvores de decisão, mas basicamente treina cada árvore sequencialmente focando nos erros cometidos pelas anteriores.

Nas células abaixo é possível ver a performance deste modelo nos feature sets e transformações que usamos com a Random Forest. Vemos que ele traz uma melhora significativa, capturando melhor os padrões da relação entre as variáveis e o preço de venda dos imóveis.

In [9]:
from sklearn.ensemble import GradientBoostingRegressor
                
model = GradientBoostingRegressor(random_state=0)
error = cross_val_score(model, X1, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()
print('GBM, X1, log-target RMSLE:', error)

model = GradientBoostingRegressor(random_state=0)
error = cross_val_score(model, X2, np.log1p(y), cv=kf, scoring=rmsle_log_y).mean()
print('GBM, X2, log-target RMSLE:', error)
GBM, X1, log-target RMSLE: 0.133492454914
GBM, X2, log-target RMSLE: 0.129806890482
In [10]:
from sklearn.ensemble import GradientBoostingRegressor
                
model = GradientBoostingRegressor(random_state=0)
error = cross_val_score(model, X1, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()
print('GBM, X1, sqrt-target RMSLE:', error)

model = GradientBoostingRegressor(random_state=0)
error = cross_val_score(model, X2, np.sqrt(y), cv=kf, scoring=rmsle_sqrt_y).mean()
print('GBM, X2, sqrt-target RMSLE:', error)
GBM, X1, sqrt-target RMSLE: 0.134258972813
GBM, X2, sqrt-target RMSLE: 0.130919235682

Ajustando hiperparâmetros

Para simplificar o exemplo e focar na parte do ensemble, não vou fazer o ajuste dos hiperparâmetros do modelo. Hiperparâmetros são os atributos do modelo (como a profundidade das árvores de decisão) que precisam ser ajustados usando dados separados de validação ou um ciclo de validação cruzada.

É bom saber que nem sempre os melhores modelos formam os melhores ensembles. É importante ter modelos poderosos ao fazer stacking, mas também devemos lembrar que é importante ter diversidade. Às vezes alguns modelos que possuem um erro mais alto podem capturar padrões diferentes dos melhores modelos e por isso contribuem com o ensemble.

Caso você decida fazer o ajuste dos hiperparâmetros, é importante colocá-lo dentro do ciclo de validação cruzada que veremos no próximo passo.

Stacking

Tudo o que fizemos acima é para que possamos criar o nosso ensemble. Esta é a hora juntarmos os métodos usados para melhorarmos o poder preditivo dos nossos modelos.

O Stacking é uma maneira de fazer o ensemble na qual usamos modelos para fazer previsões, e depois usamos estas previsões como features em novos modelos, no que pode ser chamado de “segundo nível”. Você pode fazer este processo várias vezes, mas a cada nível o retorno em performance com relação à computação necessária é menor.

Nesta fase precisamos de dois ciclos de validação cruzada: externo e interno. No interno, nós treinaremos os modelos nos dados originais e faremos as previsões. No externo, treinaremos o modelo usando as previsões do primeiro passo como features.

A cada passo da validação cruzada interna, vamos salvar as previsões para a parte dos dados que for usada como validação. Desta maneira teremos previsões para todos os exemplos de nossos dados de treino. Além disso treinaremos um modelo nos dados originais de treino para podermos fazer previsões para os dados de teste.

No ciclo externo treinaremos um modelo nas previsões geradas pelo ciclo interno, e as features dos dados de validação serão as previsões dos modelos de primeiro nível nos dados de teste.

No nosso caso específico, criaremos previsões usando todas as combinações de modelos (RF e GBM), transformações do target (log e raiz quadrada) e feature sets (X1 e X2). Não usaremos o X3 porque levaria muito tempo para treinar, e para os fins de demonstração do método estes dois serão o bastante.

No fim teremos previsões de 8 modelos no primeiro nível. No segundo nível usei uma regressão linear regularizada (Ridge). Tendo estas previsões podemos computar o erro nos dados fora das nossas amostras de treino e validação internas, o que nos dará uma estimativa confiável do erro do ensemble.

In [13]:
from itertools import product
from sklearn.linear_model import Ridge

kf_out = KFold(n_splits=5, shuffle=True, random_state=1)
kf_in = KFold(n_splits=5, shuffle=True, random_state=2)

cv_mean = []
for fold, (tr, ts) in enumerate(kf_out.split(X, y)):
    X1_train, X1_test = X1.iloc[tr], X1.iloc[ts]
    X2_train, X2_test = X2.iloc[tr], X2.iloc[ts]
    y_train, y_test = y.iloc[tr], y.iloc[ts]
    
    modelos = [GradientBoostingRegressor(random_state=0), RandomForestRegressor(random_state=0)]
    targets = [np.log1p, np.sqrt]
    feature_sets = [(X1_train, X1_test), (X2_train, X2_test)]
    
    
    predictions_cv = []
    predictions_test = []
    for model, target, feature_set in product(modelos, targets, feature_sets):
        predictions_cv.append(cross_val_predict(model, feature_set[0], target(y_train), cv=kf_in).reshape(-1,1))
        model.fit(feature_set[0], target(y_train))
        ptest = model.predict(feature_set[1])
        predictions_test.append(ptest.reshape(-1,1))
    
    predictions_cv = np.concatenate(predictions_cv, axis=1)
    predictions_test = np.concatenate(predictions_test, axis=1)
    
    stacker = Ridge()
    stacker.fit(predictions_cv, np.log1p(y_train))
    error = rmsle_log_y(stacker, predictions_test, np.log1p(y_test))
    cv_mean.append(error)
    print('RMSLE Fold %d - RMSLE %.4f' % (fold, error))
    
print('RMSLE CV5 %.4f' % np.mean(cv_mean))
    
RMSLE Fold 0 - RMSLE 0.1248
RMSLE Fold 1 - RMSLE 0.1449
RMSLE Fold 2 - RMSLE 0.1257
RMSLE Fold 3 - RMSLE 0.1409
RMSLE Fold 4 - RMSLE 0.1087
RMSLE CV5 0.1290

Como podemos ver, nosso melhor modelo de primeiro nível é o GBM treinado com a transformação log nos dados X2, que atinge o erro de 0,1298. Nosso ensemble atinge o valor de 0,1290. Uma melhora de 0,62%.

O objetivo deste artigo era demonstrar o método, sem se preocupar muito com a performance no fim. Um ensemble feito visando melhora de performance pode apresentar um resultado mais significativo.

Em alguns casos, como em fundos de investimento ou aplicações de saúde, uma melhora pequena pode ter um resultado bastante significativo no mundo real, que justifique a criação de uma solução mais completa usando stacking.

Como sempre na aplicação de Machine Learning, nada é garantia de sucesso, mas este método é um dos mais consistentes em oferecer uma melhora.

Detectando Anúncios Duplicados com Machine Learning

Em sites de classificados online é comum ver pessoas postando anúncios quase idênticos, mudando apenas uma palavra, ou as fotos, para tentar fazer com que mais usuários vejam o anúncio e respondam.

Isso acaba sendo um problema para quem quer encontrar o melhor negócio, já que precisa tolerar vários anúncios irrelevantes até encontrar o que deseja. Uma das soluções é ter uma equipe de revisores para avaliar os anúncios, mas dá pra perceber que os custos se tornariam altos ao recebermos milhões de anúncios por dia.

Esta é uma tarefa na qual machine learning pode ajudar.

A Avito é uma empresa de classificados online bastante popular no Leste Europeu. Não é a primeira vez que eles oferecem competições no Kaggle, e desta vez querem um sistema que possa detectar anúncios duplicados automaticamente.

O time do qual fiz parte terminou no terceiro lugar, e este artigo trata de um modelo que seria o bastante para terminar em 15º lugar.

Quais eram os dados disponíveis

Os arquivos principais eram aqueles que descreviam os pares de anúncios. Continham o ID dos dois anúncios, um indicador sobre a duplicidade do par, e o método utilizado para gerar aquele indicador. Esta terceira variável só estava disponível nos dados de treino.

Três métodos foram usados para gerar os dados: em um deles uma pessoa avaliava um par de anúncios e determinava se era duplicado, em outro um sistema automaticamente determinava se os anúncios eram duplicados, e no terceiro, uma pessoa avaliava manualmente os anúncios, mas apenas de um usuário.

No geral, pouco mais de 40% dos pares eram anúncios duplicados.

Como dados auxiliares tínhamos: informações textuais como título, descrição, atributos do anúncio (marca do carro, em anúncios de automóveis, por exemplo). Informações visuais, que eram as imagens dos anúncios. E informações gerais, como a categoria do anúncio, a latitude e longitude de onde foi postado, um indicador da região geográfica, e a estação de metrô mais próxima.

Os pares totalizavam mais de 4 milhões de linhas.

Como modelar esta tarefa

Uma das habilidades mais importantes do cientista de dados é saber traduzir uma tarefa para o mundo do machine learning. Neste caso, como estamos tratando de detectar duplicações, mais importante do que o conteúdo dos anúncios, é a similaridade entre as características do par.

Similaridade entre imagens

Mais importante que o próprio texto dos anúncios eram as imagens. Dá pra raciocinarmos o seguinte: mudar uma ou outra palavra do texto para não dar a impressão de anúncio duplicado é fácil, mas poucas pessoas vão ter o trabalho de tirar fotos diferentes, então a tendência é que as fotos de anúncios duplicados sejam as mesmas, ou muito parecidas.

Tive a oportunidade de aprender uma nova maneira de calcular a similaridade entre imagens, que é simples e tem um resultado muito bom. Basicamente consiste em reduzir a imagem, computar uma função sobre os pixels da imagem redimensionada e determinar bits para representar o valor da função computada.

Mais informações sobre estes métodos podem ser encontradas neste link: http://www.hackerfactor.com/blog/?/archives/529-Kind-of-Like-That.html

Similaridade entre o texto dos anúncios

Apesar de importantes, estas variáveis não são tão boas quanto a similaridade das imagens, já que é fácil modificar apenas algumas palavras e ter anúncios diferentes.

Nós não tínhamos informações temporais, como o momento em que o anúncio foi feito, então haviam anúncios que tinham o mesmo texto e não eram duplicados, e imagino que seja o caso de anúncios que expiraram e foram colocados novamente.

Um passo importante foi fazer a limpeza dos dados: transformar o texto para caixa baixa, remover a pontuação e fazer stemming.

Algumas métricas de similaridade funcionaram bem, como a Jaccard e a Cosine.

Similaridade entre outros campos

Também criei variáveis para indicar se outros campos, como o da localização, possuíam o mesmo valor. Algumas destas acabaram ajudando, mas foram minoria.

Como tínhamos latitude e longitude, calculei a distância geográfica entre os anúncios, que acabou contribuindo com o modelo.

Além disso, diferenças entre o preço dos dois produtos foram importantes. Não apenas comparar se o preço era exatamente igual, mas qual a magnitude da diferença.

Outras variáveis

Variáveis como o preço de um produto e a região onde ele se encontra também foram importantes. Uma das razões pode ser que em cidades maiores, ou bairros com maior população, exista uma probabilidade maior de duplicação.

O modelo utilizado

Apesar da solução final do time contar com vários modelos, eu foquei em criar apenas um que fosse capaz de capturar os padrões através das variáveis sobre os dados originais.

Este modelo foi um Gradient Boosted Trees, com a implementação do XGBoost. Um detalhe é que usei uma quantidade baixa de árvores, mas com uma profundidade grande em cada árvore, o que ajudou a capturar os padrões. Acredito que uma quantidade maior de árvores, e mais superficiais, teria atingido uma performance melhor.

Este modelo, por si só, terminaria em 15º lugar.

Como Maximizar Suas Habilidades de Machine Learning – Vídeo

Neste artigo estão o vídeo e os links para os recursos apresentados na palestra do evento de lançamento dos nanodegrees de Machine Learning da Udacity, no Campus São Paulo do Google, em 29 de agosto de 2016.

 

Geral:

Duolingo (para aprender ou reforçar o Inglês): http://www.duolingo.com

Livros/Blogs:

“The Elements of Statistical Learning”
http://statweb.stanford.edu/~tibs/ElemStatLearn/

“Introduction to Statistical Learning”
http://www-bcf.usc.edu/~gareth/ISL/

“Deep Learning”
http://www.deeplearningbook.org

Blog do Kaggle
http://blog.kaggle.com

Ferramentas Python

Anaconda: https://www.continuum.io/downloads
Pandas: http://pandas.pydata.org
Numpy: http://www.numpy.org
Scipy: http://www.scipy.org
Scikit-learn: http://scikit-learn.github.io/stable
Matplotlib: http://matplotlib.org
Jupyter Notebook: http://jupyter.org

Ferramentas Avançadas

Keras: http://www.keras.io
Theano: http://deeplearning.net/software/theano/
XGBoost: http://xgboost.readthedocs.io/en/latest/

Melhorando o Sistema de Busca de E-commerce da Home Depot com Machine Learning

Uma das tarefas mais importantes para um site de e-commerce é apresentar produtos relevantes para o usuário. Existem várias maneiras de se criar um sistema de busca de produtos, inclusive usando machine learning em alguma parte do processo. Uma delas é usar um modelo para determinar a relevância de um produto para um termo de busca.

A Home Depot é uma rede de lojas que comercializa itens para casa, como móveis, eletrodomésticos e equipamentos de manutenção. Além das lojas físicas, existe uma loja virtual onde os consumidores podem navegar pelos produtos e fazer compras.

Nesta competição a tarefa a ser solucionada era determinar qual a relevância dos resultados do mecanismo de busca do site desta empresa. Foram disponibilizados dados sobre a frase de busca e informações dos produtos retornados, além de uma pontuação, indicando a relevância daquele resultado.

A solução apresentada aqui é parte da que usei para garantir o 13º lugar, entre mais de 2000 times, na competição.

Quais Eram as Informações Disponíveis?

Foram disponibilizados pares de termos de busca e produtos retornados. Para a maioria dos produtos havia o título e a descrição, além de alguns atributos específicos como medidas, cor, material e textura.

Por fim, uma coluna determinava a relevância, que ia de 1 a 3, sendo 1 para produtos irrelevantes para a busca, e 3 para produtos bastante relevantes. Estas notas foram dadas por vários avaliadores humanos

Durante a criação dos dados, os avaliadores tinham fotografias dos produtos para ajudar a determinar a relevância, mas estas não foram disponibilizadas durante a competição.

As Diferenças Entre Dados de Treino e Teste

O treino e o teste continham termos de busca diferentes. Apenas 30% dos termos de busca do teste apareciam no treino. No fim da competição foi revelado que alguns exemplos dos dados de teste não eram utilizados para avaliar o resultado do modelo, mas apenas para evitar que competidores determinassem a relevância manualmente.

Alguns produtos também só apareciam em um dos dois conjuntos dos dados.

Num caso real esta divisão faz sentido, já que não queremos que o modelo seja bom apenas nas buscas que já conhecemos, mas também em novas buscas e produtos.

Como Isso Influenciou a Validação

Por causa da diferença citada acima, era importante criar um ambiente de validação que não tivesse dados sobre o mesmo termo de busca no treino e no teste.

Em vez de fazer uma validação aleatória comum, sorteei termos de busca, de maneira que fossem criadas 5 divisões, cada uma contendo todas as linhas para os termos de busca atribuídos a ela.

Isso foi fundamental para obter um bom resultado na competição, já que os resultados locais estavam bastante próximos dos resultados na validação do Kaggle (Public Leaderboard), o que me dava mais segurança sobre a capacidade de meu modelo generalizar para os dados privados.

Como as Soluções Eram Avaliadas

A métrica escolhida pelos patrocinadores foi o RMSE (raiz quadrada da média dos erros elevados ao quadrado). É uma métrica bem popular e basicamente aplica uma penalidade proporcional à diferença entre a previsão e a realidade.

Como Modelar Esta Tarefa

Num primeiro momento vi muita gente tentando criar modelos baseados em representações de “bag-of-words” dos dados. Esta representação consiste em criar uma matriz cujas linhas são os exemplos e as colunas são as palavras contidas nos exemplos. Em sua forma mais simples, se a palavra está no documento, o valor para a coluna, naquela linha é 1, caso contrário 0.

Mas esta tarefa requer um pensamento diferente. Na verdade o conteúdo até tinha poder preditivo, mas o mais importante não eram as palavras, e sim a relação entre o produto e termo de busca. E isso dificilmente é capturado da melhor maneira pela abordagem acima.

Pensando nisso, decidi me focar em criar variáveis que representassem a similaridade entre os produtos e os termos de busca correspondentes.

Limpando e Preparando o Texto Original

Um dos passos para o processamento de dados textuais é descobrir se a limpeza de algumas partes do texto pode ajudar o modelo a encontrar uma solução melhor.

Uma das sugestões dadas no fórum era substituir “medidas” expressas de maneiras diferentes por um termo comum. Um exemplo seria “m2” se tornar “metro quadrado”. Isso evita que algumas métricas de similaridade determinem que estes são conceitos diferentes, apesar de se referirem à mesma coisa.

Além disso, remover toda a pontuação e excluir palavras muito comuns, com pouco valor preditivo (como artigos), também ajudou a tornar o modelo melhor.

Stemming

Uma técnica bastante útil nestes casos, stemming significa retirar partes que flexionam uma palavra. Por exemplo, transformar a palavra “training” em “train”. Isso ajuda em algumas tarefas nas quais estas diferenças entre as palavras não influenciam, ou até acabam atrapalhando o modelo.

Neste caso, fazer o stemming sobre o texto ajudou as medidas de similaridade a capturar com maior precisão os valores corretos entre termos e produtos.

Correção Ortográfica

Como estamos tratando de texto gerado por usuários, podem acontecer erros de digitação, ou até mesmo diferenças de escrita em nomes de marcas mais complicadas de produtos.

Por isso, foi importante encontrar uma maneira de corrigir os possíveis erros ortográficos. Utilizei uma tabela disponibilizada nos fóruns, na qual um usuário usou o corretor ortográfico do Google para arrumar as frases de busca. Este foi um dos passos que mais ajudou a reduzir o erro do modelo.

N-gramas

N-gramas na análise de texto normalmente podem significar trechos de caracteres ou de palavras.

Nesta competição usei os dois tipos. Quando a unidade mínima é uma palavra, um unigrama pode ser “tinta” ou “azul”. Mas também é possível utilizar bigramas, trigramas, etc. Um bigrama seria “escada branca”, simplesmente uma string com duas palavras.

Quando a unidade mínima do n-grama é um caractere, o N se refere ao número de letras do trecho, sendo “abc” um trigrama.

Apesar de existirem similaridades que podem ser aplicadas diretamente a caracteres, como a Levenshtein, usar as similaridades descritas aqui com n-gramas de caracteres ajudou o modelo.

Similaridades Entre Termos de Busca e Produto

Uma das maneiras mais simples de se retornar resultados para um termo de busca é retornar os produtos com títulos e descrições mais similares às palavras buscadas. Normalmente este método já dá resultados bons, mas claro que existem detalhes que não são capturados por ele., e itens que possuem as mesmas palavras, mas são irrelevantes.

Além disso é preciso definir como será feita a comparação entre as palavras dos termos de busca e a descrição do produto. Para fazer isso existem algumas medidas bastante populares. Neste caso específico, utilizei o valor das medidas para um determinado par de busca e produto como variável no modelo.

Coeficiente de correspondência simples

Uma das variáveis mais poderosas, a correspondência simples é apenas a proporção de palavras de um campo que estão presentes no outro campo. Neste caso, um exemplo é o número de palavras do termo de busca que estão presentes no título do produto divido pelo número total de palavras do primeiro.

Como um exemplo simples: para um termo de busca “tinta azul para exterior” e um produto com título “galão de tinta para exterior – azul”, teríamos as 4 palavras do termo presentes no título, o que daria um coeficiente 4/4 = 1. Já um produto com título “galão de tinta para interior – amarela”, teria um coeficiente de 2/4 = 0,5.

Este coeficiente foi computado nas duas direções, verificando quais palavras do termo de busca estavam no título, e também qual a proporção de palavras do título no termo de busca.

Similaridade Cosseno

Esta similaridade também é bastante pupular e parece funcionar muito bem na prática. Primeiro é necessário transformar o texto original em uma matriz. A forma mais popular de fazer isso é considerar cada documento em uma linha da matriz, criando uma coluna para cada palavra e colocando um valor na célula correspondente à presença ou ausência daquela palavra no documento da linha.

Este valor pode ser um simples indicador binário da presença da palavra, ou também o número de vezes que a palavra aparece no documento. Mas uma maneira mais avançada, e a que utilizei nesta competição, é computar o coeficiente TF-IDF e usar como valor. Este coeficiente leva em conta a frequência da palavra no documento atual e também com relação aos outros documentos.

Após transformar os campos de texto originais em matrizes, normalizei os vetores para que cada um tivesse tamanho igual a 1, e computei o produto escalar entre eles. Como exemplo, computei o produto escalar entre o termo de busca e o título do produto para cada linha.

Similaridade Jaccard

Outra métrica de similaridade bastante interessante é a Jaccard. Originalmente ela é uma medida para conjuntos. No caso, para usá-la com texto, consideramos cada frase como um conjunto de palavras (ou n-gramas).

Para computar a métrica, basta dividir o número de elementos da intersecção entre os dois grupos pelo número de elementos da união dos mesmos. Por exemplo, se considerarmos o termo de busca “tinta azul para exterior” e o título “galão de tinta para exterior – azul” teremos uma intersecção de tamanho 4 e uma união de tamanho 6, então a similaridade Jaccard é igual a 0,667.

Existe uma versão bastante similar à Jaccard, chamada Similaridade Tanimoto, que pode ser computada diretamente sobre vetores de palavras, mas aqui usei a versão tradicional.

O Modelo Utilizado

O modelo que utilizei, sem surpresas, foi Gradient Boosted Trees, através da implementação do XGBoost.

Para tentar extrair valor do texto “bruto” tentei usar um SVM linear e Random Forests, mas nenhum deles foi capaz de me dar uma performance muito boa. Isso já era esperado, já que o conteúdo, apesar de importante, não é o foco da tarefa, e sim a similaridade entre campos diferentes.

Ideias Que Não Funcionaram

Em todo projeto de ciência de dados existem vários métodos a serem testados, mas boa parte deles acaba não sendo útil na tarefa específica. Apesar de machine learning ser uma área acadêmica de exatas, aplicar os modelos a dados reais envolve bastante tentativa e erro guiada, claro, por experiências anteriores. Por isso acho importante relatar algumas coisas que tentei mas não funcionaram.

Olhando os dados parecia que a correspondência entre as palavras finais de um termo de busca e título do produto era bastante preditiva quanto à relevância. Tentei criar várias variáveis baseadas nessa ideia, mas elas não funcionaram. Entendo que qualquer padrão relacionado a isso já era capturado pelas outras variáveis.

Outro método bastante comum em competições, o ensemble de vários modelos para melhorar a pontuação, não funcionou bem desta vez. Outros times também relataram que era mais produtivo juntar as variáveis de todos os integrantes e treinar um modelo, do que tentar criar um ensemble com os modelos de cada um.

Apesar do pequeno ganho, os times com as melhores colocações usaram ensembles com uma quantidade considerável de modelos.

Como Venci a Competição de Machine Learning da Maior Empresa de Telecomunicações da Austrália em Apenas 19 Dias

A Telstra, maior empresa de telecomunicações e mídia da Austrália, patrocinou uma competição no Kaggle buscando cientistas de dados que se destacassem para participar de um processo de recrutamento para integrar seu time de Big Data.

Foram disponibilizados dados de logs de serviço de sua rede, e a tarefa era criar um modelo que pudesse prever a gravidade de uma falha num determinado local e horário, através dos dados destes relatórios.

No total, 974 competidores do mundo todo participaram, e esta vitória acabou me colocando no 12º lugar (de 500.000+ usuários) no ranking global do Kaggle.

Dados

Eram cerca de 7400 exemplos para treino e 11200 para teste. Uma quantidade pequena, o que requer uma atenção especial durante a criação e validação do modelo.

Cada exemplo possuía um ID, que representava um par entre local e horário do evento. Além disso, foram disponibilizados outros arquivos que continham, para cada exemplo: informações sobre sinais obtidos pelo sistema atual ao processar os logs, o tipo de evento, e a gravidade de mensagens enviadas pelo sistema naquele momento.

A gravidade do problema tinha três categorias: 0, significando que não havia problemas; 1, significando que alguns problemas foram detectados na rede; e 2, representando muitos problemas reportados. Portanto, trata-se de classificação com múltiplas classes.

Validação

Apesar de haver uma dependência temporal e espacial entre os dados, já que são de locais diferentes, em momentos de tempo diferentes, os dados foram particionados de maneira aleatória. Era possível saber a localização, mas não o momento em que o registro foi obtido.

Com isso, uma validação cruzada simples foi o melhor ambiente que encontrei para testar meu modelo. A validação cruzada é simplesmente o procedimento de dividir seus dados em K partes, e fazer um ciclo de treino, usando K – 1 para treinar e avaliando na divisão não utilizada para teste.

Neste caso usei 5 divisões, que forneceram uma estimativa bastante confiável da performance nos dados de teste revelados após o fim da competição.

Variáveis

No total eu tinha 242 variáveis. Vou citar aqui as informações que acredito serem mais importantes para um modelo que fosse implementado em produção.

Variável “mágica”

Havia um padrão nos dados, que mais tarde seria chamado de “variável mágica” nos fóruns, que permitia explorar o que parecia ser um erro durante o preparo dos dados, e conseguir uma boa melhora na precisão do modelo.

Apesar dos arquivos de treino e teste possuírem IDs aleatórios, o ordenamento dos registros dos outros arquivos possuía valor preditivo. Com isso, você poderia usar o próprio ID destes arquivos, ou criar variáveis relacionadas a ele, para explorar a falha.

Algumas pessoas criticaram o fato dos melhores colocados usarem este erro para conseguir uma pontuação melhor na competição, mas eu acredito que seja importante conseguir encontrar este tipo de erro em dados, pois a única coisa que mudaria fora de uma competição é que ele deveria ser corrigido em vez de explorado.

Por isso, é sempre importante buscar erros nos dados. Existem diversos tipos de vazamentos de informação, alguns muito sutis, que podem fazer seu modelo parecer melhor do que realmente é.

Localização

Havia uma variável indicando o local daquele registro. Esta, em tese, é uma variável categórica, ou seja, não tem ordem natural, mas mesmo assim utilizei-a como ordinal, em vez de criar uma coluna para cada valor, por dois motivos: árvores conseguem aproximar bem os padrões de variáveis categóricas mesmo usando este formato (em alguns casos é melhor usar one hot), e poderia haver algum padrão de proximidade entre os locais.

Por exemplo, pode ser que o local 201 e 202 estivessem sob as mesmas condições climáticas, o que poderia influenciar a ocorrência de problemas nestas duas estações, então talvez houvesse um agrupamento implícito ali.

Variáveis do Log

Os registros dos logs de serviço possuíam o que parecia ser uma linha para cada sinal de aviso, e um valor indicando quantas vezes o aviso foi disparado naquela ocasião. Os organizadores não deixaram claro se era este o significado, mas é a explicação mais plausível.

Com isso, criei uma coluna para cada um destes sinais, e o valor para cada exemplo era o número de avisos.

Estas colunas eram bastante esparsas (mais zeros do que outros valores), então a maioria delas acabava sendo inútil, mas algumas com bastante frequência possuíam valor preditivo.

Solução Final

Após ficar satisfeito com o desempenho de meu melhor modelo individual, decidi partir para o ensemble. Ou seja, criar vários modelos que capturem padrões diferentes nos dados e se complementem.

Dentre os 15 modelos que compuseram a solução final, dois se destacaram: Gradient Boosted Trees (na implementação do XGBoost) e Redes Neurais (usando Keras).

Gradient Boosted Trees – XGBoost

Quem está acostumado com o Kaggle sabe que boa parte das soluções de competições envolvem Boosted Trees. Este modelo é bastante poderoso por si só, mas a implementação paralela no pacote XGBoost tem se mostrado muito boa para resolver tarefas de machine learning em geral.

Neste caso, meu melhor modelo “individual” foi um destes, e finalizou em 18º lugar. Ainda tinha bastante coisa que poderia ser melhorada nele, mas como eu tinha apenas uma semana para o fim da competição, e sabia que sem um ensemble ficaria bem mais difícil vencer, decidi encerrar a otimização neste ponto.

Redes Neurais

Um modelo que encontra boas soluções, mas usando uma estratégia diferente das árvores de decisão é uma rede neural. Apesar dela não ter demonstrado uma performance comparável à do XGBoost, foi um ótimo complemento a ele no ensemble.

Usei a biblioteca Keras, que é fantástica para criar redes neurais em Python.

Testei normalizar os dados com todos os transformadores do scikit-learn, mas o melhor foi o padrão (StandardScaler), que subtrai a média e divide pelo desvio padrão. Além disso usei duas camadas de neurônios e o otimizador Adam. Para encontrar uma boa arquitetura, coeficiente de dropout e parâmetros em geral, fiz uma busca aleatória.

Redes neurais parecem bem sensíveis a “tudo”, então acredito que existam combinações diferentes de arquitetura, normalização de dados e codificação de variáveis que dariam resultados iguais ou melhores.

Um dos motivos pelos quais ela também não ficou tão boa foi porque eu não criei um conjunto de variáveis específico para a rede neural. Usei o mesmo conjunto de variáveis das árvores. Se este fosse meu modelo principal, eu teria feito diferente, mas como era apenas um componente do ensemble, esta maneira já resolvia.

Conclusão

Este foi um breve resumo de 3 semanas de trabalho nesta competição.

Meu plano inicial era criar apenas um modelo e ver até onde dava pra chegar, mas ao ver que o modelo tinha ficado bom, decidi tentar alcançar a minha primeira vitória individual.

Um detalhe interessante é que enviei a solução vencedora meia hora antes do fim da competição, após testar algumas últimas ideias.

Usando Machine Learning Para Prever o Primeiro Destino de 60 mil Usuários da AirBnB

A AirBnB é uma empresa de tecnologia que oferece um ambiente virtual onde os usuários podem reservar locais para se hospedar em diversos lugares do mundo, e também anunciar seus imóveis para a comunidade de viajantes.

Visando encontrar candidatos para compor seu time de cientistas de dados, eles decidiram patrocinar uma competição na qual o objetivo era prever qual o primeiro país em que um novo usuário fará sua reserva de hospedagem.

A parte mais interessante, para mim, desta competição, é que foi possível chegar a uma boa posição (Top 7%) com apenas um modelo, que poderia ser utilizado em produção.

Informações dos Dados

Foram disponibilizados dados anônimos sobre o perfil do usuário, como idade, gênero, data de criação da conta, idioma, e o canal utilizado para chegar ao site. Estes dados vinham desde 2010.

Além disso, dados sobre as sessões dos usuários, identificadas pelo id, que descreviam qual a ação executada (clique, mensagem para o dono do imóvel, visualização de resultados de busca, etc) e quantos segundos se passaram entre aquela ação e a anterior. Estes dados existiam apenas a partir de janeiro de 2014.

Existiam outros dois arquivos, com dados sobre as características dos países, mas não encontrei utilidade para eles.

A métrica utilizada era o NDCG. Basicamente vai de 0 a 1 e mede a relevância dos resultados do modelo ordenados pelo ranking. Mais informações neste link (em inglês).

Um usuário poderia escolher entre 12 países, mas quase 90% deles ia para os Estados Unidos ou não marcava viagem.

Logo ficou claro, devido à métrica utilizada, que valeria mais a pena se concentrar em modelar o fato do usuário marcar ou não a viagem, ficando o destino em segundo plano.

Validação

Os dados disponibilizados para treino e validação se referiam a usuários que criaram suas contas antes de 1 de julho de 2014. E os dados de teste, na leaderboard, eram os três meses após esta data.

Vários participantes utilizaram validação cruzada, sorteando aleatoriamente os exemplos, mas por uma questão de tempo para rodar o modelo durante o desenvolvimento, e também porque as características dos dados dependiam do tempo, decidi usar dados de maio e junho de 2014 como validação.

Algumas características dos dados, como a proporção de usuários que possuíam dados sobre sessões mudavam com o tempo, então decidi usar um período bastante próximo do teste para validar. E este período se mostrou bastante robusto.

Variáveis

Perfil do Usuário

Utilizei as variáveis padrões do perfil do usuário já descritas acima. Como eu planejava usar um modelo baseado em árvores de decisão, transformei cada variável categórica em ordinal, pois estes modelos conseguem aproximar bem os padrões mesmo que estas variáveis não tenham um ordenamento real.

Além disso, procurei computar o tempo que um usuário gastava entre eventos das sessões, e quantos tipos diferentes de ações ele executou no site.

Datas

Extraí informações básicas das datas, criando colunas individuais para o dia, mês, ano e dia da semana. Isto ajuda o modelo a capturar efeitos sazonais da série temporal.

Comportamento do Usuário

Esta é uma parte essencial na maior parte dos modelos para e-commerce. Neste caso específico, criei uma coluna com o tempo que o usuário passou em cada ação executada por ele no site. Além disso, fiz o mesmo procedimento calculando quantas vezes o usuário executou uma determinada ação.

Modelo

Utilizei uma Random Forest com 300 árvores que foi boa o bastante para conquistar um lugar no Top 7% (91 de 1463).

No total eu tinha cerca de 1100 colunas, e a maior parte delas bastante esparsa (a maioria dos valores eram zero). Algumas pessoas dizem que estes modelos baseados em árvores de decisão não se dão bem com este formato de dados, mas a minha experiência sugere que é uma questão de ajuste de parâmetros.

Infelizmente não tive tempo de treinar um bom modelo de Gradient Boosted Trees, que certamente teria uma performance melhor, e pela diferença pequena de scores entre a Random Forest e o topo da tabela, é quase certo que seria bom o bastante para pegar um lugar no Top 10.

Classificando Visitas de Clientes ao Walmart em 37 Categorias Usando Machine Learning

Definir qual é a intenção de um cliente ao visitar uma loja, seja ela real ou virtual, pode ajudar uma empresa a oferecer uma experiência personalizada. Por isso é importante utilizar ferramentas que possam ajudar a categorizar e identificar estas visitas.

O Walmart disponibilizou dados anônimos sobre viagens de seus clientes a algumas de suas lojas. Dentre os dados estavam os itens comprados e suas respectivas quantidades, além de informações descritivas sobre os produtos e o dia da semana. A tarefa era utilizar estas informações para categorizar a intenção da visita daquele cliente.

Apesar de não nos dar uma tabela de referência para cada código, os organizadores informaram que alguns exemplos de tipos de visitas são: compra semanal de alimentos, presentes para uma data comemorativa, ou compra de roupas para a nova estação.

Dados

Existiam cerca de 95 mil viagens para treino, e 95 mil para teste. O arquivo disponibilizado continha basicamente uma linha para cada produto comprado durante uma visita, e era responsabilidade do competidor transformá-lo adequadamente para unir os dados de cada viagem.

Para definir os produtos existiam: o código UPC, com mais de 100 mil tipos diferentes; o código Fineline, que é uma categoria refinada criada pelo Walmart, com cerca de 5 mil tipos; e o Departamento, que continha cerca de 60 tipos diferentes.

Além disso, a variável ScanCount definia quantos produtos daquele tipo foram comprados, sendo que, se fosse um número negativo, o cliente estava devolvendo os produtos.

Transformando as Variáveis

Para conseguir treinar um modelo era necessário, no mínimo, fazer o agrupamento das informações básicas por viagem. Vários relatos no fórum falavam de transformações resultando em milhares de variáveis.

Como eu tive apenas 7 dias para trabalhar nesta competição, meu objetivo foi criar um modelo simples e compacto, com o menor número de variáveis possível, mas com um bom desempenho para me posicionar entre os 10% melhores.

Variáveis Básicas

Dentre as variáveis básicas, baseadas em estatísticas de cada visita, alguns exemplos são: a média da quantidade de cada produto comprado na visita, indicação se houve devolução de produto, soma da quantidade de todos os produtos comprados e o departamento com maior quantidade de itens comprados.

Contagens e Proporções

Decidi agregar a quantidade de produtos comprados por departamento, e usar tanto a contagem de cada departamento, quanto a proporção que este departamento ocupava nesta compra. Isso criou cerca de 120 variáveis.

SVD + TF-IDF

Para tentar utilizar o Fineline e o UPC sem aumentar muito o número de variáveis, decidi somar a quantidade de produtos em cada um deles e fazer duas transformações.

Primeiro a TF-IDF, que substitui as quantidade por pesos relativos à proporção de um item naquela viagem, e quão frequente é a presença deste item em outras viagens.

Depois, apliquei o SVD, que tenta encontrar as direções que possuem maior variação nos dados.

Estas transformações normalmente são usada com texto, aplicadas à contagem de palavras em cada documento, e conhecidas como Latent Semantic Analysis.

Além de reduzir a dimensão dos dados, espera-se que ela descarte boa parte do ruído, e encontre categorias estruturais às quais as viagens pertencem.

Na prática isso ajudou bastante com o Fineline, mas não ajudou muito com o UPC.

Regressão Logística L1 e L2

Uma outra maneira de reduzir a dimensão é treinar um modelo mais simples e usar as previsões do mesmo como variável no modelo principal.

Para isso, treinei duas regressões logísticas: uma com penalidade L2 e outra com penalidade L1. Isso gerou cerca de 70 variáveis, 37 para cada regressão, com a probabilidade de um exemplo pertencer a cada classe.

Modelos

Gradient Boosted Trees – XGBoost

A maior parte do meu tempo foi focada em construir um bom modelo com o XGBoost. Este modelo já era bom o bastante para ficar nos 10% melhores.

Redes Neurais

Para complementar o XGBoost, e tentar uma posição melhor, decidi treinar uma rede neural nas mesmas variáveis. Ela possuía 2 camadas ocultas e dropout.

Outros competidores reportaram bons resultados com redes neurais, mas não passei muito tempo mexendo com elas. A intenção era apenas conseguir uma leve melhora sobre o resultado do XGBoost.

Resultado e Melhorias Possíveis

A solução final foi um ensemble simples entre uma rede neural e o XGBoost, que foi o bastante para garantir uma posição entre as 7% melhores. Ela foi obtida em cerca de 8 horas de trabalho.

Utilizando apenas o modelo GBT, sem ensemble, era possível ficar no Top 9%. Este modelo tinha apenas 200 variáveis. A maioria dos modelos melhores tinham mais de 5000 variáveis. Certamente aumentando o número de variáveis este modelo poderia ficar acima do Top 5%, mas ia demorar muito para treinar.

Outras possíveis melhorias seriam: aumentar o ensemble, criar um conjunto de variáveis otimizado para a rede neural, utilizar diretamente as variáveis com contagens do Fineline, tunar melhor os parâmetros das regressões logísticas e das transformações.

Como Conquistamos o 1º Lugar Entre 1300 Times Na Competição de Machine Learning da Caterpillar

A Caterpillar é uma empresa que fabrica equipamentos industriais, como tratores e motores. Para manter suas operações, eles precisam comprar tubos com diversas especificações, de vários fornecedores diferentes, para usar em sua linha de produção. Cada fornecedor e produto possui um modelo de precificação diferente.

A tarefa nesta competição era criar um modelo que fosse capaz de precificar os tubos utilizando dados históricos de fornecedores e características dos produtos.

Tive o prazer de competir no time que venceu esta competição, ultrapassando mais de 1300 times de cientistas de dados do mundo todo. Neste artigo quero descrever as partes mais importantes sobre a criação da solução que garantiu esta vitória.

Por ter uma familiaridade melhor com a minha parte da solução, boa parte deste artigo se refere a ela, mas quero deixar claro que a solução vencedora foi criada utilizando modelos diferentes desenvolvidos individualmente por cada integrante. Nenhum de nós teria conseguido vencer sozinho, e todos foram essenciais para que o time alcançasse o primeiro lugar.

Dados

Foram disponibilizados vários arquivos com características dos tubos e dos fornecedores. Existem duas categorias básicas de precificação: preço fixo e preço variável de acordo com a quantidade.

Havia cerca de 30 mil linhas para treino, e 30 mil linhas para teste. Algo importante a se levar em consideração é o fato que várias linhas se referiam aos mesmos tubos, mudando apenas a quantidade mínima de compra para obter aquele preço.

Dentre as variáveis disponíveis estavam: quantidade, ID do tubo, data da compra, previsão de uso anual, diâmetro e comprimento.

Validação

Esta é uma das partes mais importantes de qualquer tarefa de mineração de dados. É essencial criar um ambiente de validação que tenha as mesmas características, ou se aproxime bastante, do ambiente de produção. No nosso caso, dos dados de teste.

Isso significa que não adiantaria simplesmente embaralhar as linhas e distribuir em divisões como na validação cruzada “padrão”. Neste caso, decidi sortear divisões baseadas nos tubos. Na validação cruzada, em vez de distribuir linhas nas divisões dos dados, cada divisão continha todas as linhas de um determinado tubo.

Apesar de ser uma série temporal, os dados de treino e teste não estavam divididos entre passado e futuro, então levar a data em consideração para definir os dados de validação não trazia benefícios.

Este esquema de validação se mostrou bastante robusto e próximo tanto da leaderboard pública quanto da avaliação nos dados de teste (leaderboard privada).

Extração de Variáveis

Depois de um ambiente de validação robusto, a parte mais importante é encontrar características dos dados que possam ser transformadas em variáveis para alimentar o modelo.

Vou apresentar transformações que, em geral, podem ser utilizadas com outros dados, ou seja, não são específicas desta tarefa.

Representação Ordinal das Variáveis Categóricas

As implementações mais populares dos modelos baseados em decision trees disponíveis em Python tratam todas as variáveis como numéricas. Neste caso existem algumas alternativas, como fazer one-hot encoding, para que o modelo possa encontrar características únicas dos níveis das variáveis categóricas.

Mas, pela própria natureza do modelo, fazer a codificação das variáveis de maneira ordinal permite às decision trees aproximar a captura de valor de cada categoria como se estivesse codificada da maneira correta.

Neste caso temos uma variável categórica em que cada nível é representado por um número.

Contagem de Registros

O número de vezes que um registro aparece nos dados também pode ter valor preditivo.

Uma transformação comum, e bastante útil, é substituir os valores categóricos pela contagem de registros que pertencem àquela categoria. Neste caso houve uma melhora significativa no erro.

Quantidade Mínima e Máxima Disponível

Utilizando o fato que cerca de 90% dos tubos tinham seu preço variando com a quantidade comprada, percebi que os tubos mais caros tinham uma quantidade máxima de compra menor do que tubos mais baratos.

Boa parte dos tubos mais caros tinham seu preço mínimo atingido ao comprar 5 unidades, enquanto os tubos mais baratos, chegavam a 250 unidades.

Este atributo foi muito importante, e demonstra o valor que existe em procurar entender os dados e a tarefa em questão.

Vazamento de Informação nos IDs

Esta é a situação em que temos informações nos dados de treino que permitem descobrir padrões na variável dependente, mas que não deveriam existir na realidade.

Neste caso, cada tubo era identificado por um número, e ao fazer o ordenamento dos dados usando esta variável, percebi que existiam pequenos clusters de tubos com preços e IDs parecidos. Isso significa que o ordenamento dos tubos possuía valor preditivo.

Para usar esta informação, simplesmente criei uma variável com a parte numérica dos IDs.

Tanto em competições, quanto na prática, é importante procurar estes padrões. No primeiro caso, isso pode ajudar você a conseguir um resultado melhor. No segundo caso, deve-se remover todo tipo de vazamento de informações para não comprometer a performance do modelo em um ambiente de produção.

Peso dos Componentes

Cada tubo possuía componentes diferentes que seriam acoplados a ele. As características de cada componente variavam, mas uma delas se destacava, o peso.

Com acesso à quantidade e ao tipo de componente utilizado em cada tubo, calculei a soma do peso dos componentes de cada um, e esta foi uma variável com uma correlação significativa com o preço, e pouco correlacionada com as outras.

Modelos

Gradient Boosted Trees

Famoso por seu bom desempenho em competições de áreas diversas, este foi o meu melhor modelo. Utilizei a implementação do XGBoost, que é a melhor versão open source que conheço deste modelo.

O XGBoost tem a opção de otimizar o RMSE, mas como nesta competição estamos otimizando o RMSLE, ou seja o erro logarítmico, fiz a transformação da variável dependente com a função log(y + 1). Existiam sugestões de utilizar a raiz da variável, em vez do log, e meu melhor modelo acabou sendo treinado na variável transformada com a 15ª raiz.

Este modelo, sozinho, era o bastante para conseguir o 10º lugar.

Regularized Greedy Forests

Esta foi a novidade desta competição. Apesar de ter testado este modelo na competição do Facebook, eu nunca havia dedicado tempo a fazê-lo funcionar.

O funcionamento é basicamente o seguinte: ele otimiza uma função loss igual ao GBT, mas com um termo de regularização. Além disso, periodicamente reajusta os pesos de cada nó terminal das árvores do ensemble.

Em tese é um aprimoramento sobre o GBT, mas nesta competição não apresentou uma performance melhor. Acredito que com mais tempo para ajustar os parâmetros e treinar seja possível conseguir um resultado melhor.

Outros Modelos

Dois outros modelos foram utilizados por mim, ou outros integrantes, para compor o ensemble, mas que sozinhos não tinham uma boa performance:

– Redes Neurais
– Factorization Machines

Ensemble (Stacking)

O ensemble foi feito em três níveis, usando modelos de todos os integrantes do time.

No primeiro nível, os modelos recebiam os dados originais e faziam suas previsões individuais em dados fora da amostra de treino, no mesmo esquema de validação cruzada.

No segundo nível, quatro modelos utilizavam as previsões criadas no nível anterior como dados de treino, e mais uma vez faziam previsões usando um esquema de validação cruzada para evitar overfitting.

No terceiro nível, a média das previsões dos modelos do segundo nível era utilizada como previsão final.

Conclusão

No caso desta competição, era extremamente improvável vencer sem formar um bom time. O uso de soluções criadas por membros diferentes, e que acabavam melhorando as previsões no ensemble, foi essencial.

Além disso um dos fatores mais importantes para se vencer uma competição do Kaggle é o tempo. Quanto mais tempo você tiver para testar ideias, maiores as chances de sucesso.

Teste