fbpx

Categoria: Kaggle

Determinando a Relevância de Produtos num Sistema de Buscas Usando Machine Learning

Uma das tarefas mais importantes para um site de comércio eletrônico é garantir que o usuário encontre o produto que está buscando. Isto inclui um sistema de buscas que retorne produtos relevantes. Nesta competição a CrowdFlower, uma plataforma especializada na coleta de dados, disponibilizou dados sobre buscas por produtos, e a mediana da pontuação de relevância dada por três pessoas para cada um.

O objetivo era criar um modelo que pudesse calcular a relevância de um produto para uma determinada frase de busca.

Dados

Foram disponibilizados cerca de 10 mil registros para treino, e 20 mil para teste. Nestes registros havia a frase de busca, o título e a descrição do produto, a mediana da relevância e a variância da relevância atribuída pelos usuários.

Nos exemplos de teste havia uma parcela de registros “falsos”, que não eram utilizados para calcular a performance, mas foram colocados para evitar classificação humana.

Métrica

A métrica de avaliação foi o Quadratic Weighted Kappa. Ela mede a discordância entre dois “avaliadores”. Neste caso, media a discordância entre a pontuação dada pelo modelo e a pontuação dada por humanos. Ela é sensível não apenas à precisão, mas também à distribuição da pontuação. Em tese, uma atribuição de pontos aleatória resultaria num Kappa de 0, e uma concordância total, em Kappa igual a 1. Existem casos em que o Kappa se torna negativo.

Processamento dos Dados

Texto

A transformação básica do texto do título e da descrição é fazer uma bag of words. Em sua forma mais simples é uma matriz cujas linhas são os exemplos (documentos) e as colunas são palavras (termos). O valor de cada célula é o número de vezes que a palavra aparece naquele documento.

Uma transformação mais avançada é pegar esta matriz e fazer algumas operações utilizando a frequência dos termos em cada documento, e também no corpo de documentos. Esta é chamada de TF-IDF. Neste caso, os termos com maior presença dentro de um documento específico recebem um peso maior, mas que é descontado por um fator inversamente proporcional à presença dele em outros documentos.

Simplificando: palavras raras que aparecem bastante em um documento recebem maior peso, e palavras frequentes, que aparecem em todos os documentos, se tornam menos importantes.

Fora isso, é importante determinar um limite mínimo de vezes que a palavra deve aparecer no corpo de documentos, para diminuir o ruído.

Neste caso, a melhor representação que encontrei foi treinar uma matriz baseada apenas nos títulos, e outra apenas na descrição. Alguns participantes sugeriram treinar nos dois ao mesmo tempo, mas como se trata de um problema de busca, acredito que seja importante diferenciá-los.

SVD – Latent Semantic Analysis

Como você já deve imaginar, a matriz de documentos fica enorme, e é bastante esparsa. Isso dificulta o uso com alguns modelos não lineares. Neste caso existe a opção de reduzir a dimensionalidade usando o SVD. Este algoritmo de álgebra linear basicamente encontra os componentes principais da matriz de documentos, onde há maior variação. Isso reduz a dimensionalidade do problema, mas pode causar perda de informação.

Nestes casos de NLP eles acabam funcionando bem, porque filtram um pouco o ruído dos dados. Aplicar o SVD à matriz TF-IDF te dá uma representação conhecida como Latent Semantic Analysis. A interpretação é que cada componente obtido se refere a um “conceito” representado pelos documentos. Ele também é bastante usado para visualizar os documentos num espaço com menores dimensões.

Na competição utilizei SVD para reduzir a dimensionalidade da matriz TF-IDF antes de alimentá-la aos modelos.

Outros atributos

Além dos atributos acima, criei outros que pareciam fazer sentido, e alguns que encontrei em trabalhos acadêmicos relacionados. O mais importante deles foi a porcentagem de palavras da busca que estavam no título. Além disso, criei atributos baseados na similaridade média entre um produto e os outros da mesma frase de busca, e também a contagem de vezes que a frase de busca aparece nos dados.

Uma parte importante foi codificar cada frase de busca com one hot. Ou seja, para cada uma delas criei uma coluna que possuía o número 1 caso ela estivesse presente, e o número 0 caso contrário. Fiz isso para ajudar o modelo a capturar variações próprias de cada frase de busca.

Regressão ou Classificação?

Existiam duas maneiras de encarar essa tarefa: considerar cada nível da mediana como uma classe, sendo assim um problema de classificação, ou considerar como um problema de regressão e arredondar as previsões.

Num primeiro momento achei mais adequado tratar como regressão, mas como o Kaggle queria as previsões arredondadas, acabei criando vários modelos de classificação e não tive tempo de testar a outra alternativa.

Com a classificação temos duas opções: usar a classe prevista pelo modelo, ou utilizar as probabilidades de cada classe para fazer uma média ponderada para obter a pontuação. A primeira alternativa se mostrou melhor neste caso.

Modelos

SVM

Os melhores modelos foram obtidos usando SVM. Ele é um dos modelos mais utilizados para classificação de texto, então não é surpresa que tenha boa performance.

O melhor modelo individual, utilizando os componentes do SVD e os outros atributos, atingia um Kappa de 0,672 na validação cruzada, e 0,654 na LB.

Random Forest

Um dos meus modelos preferidos, mas normalmente não são a melhor opção para dados esparsos em altas dimensões. De qualquer maneira, após vários testes, consegui 0,6695 na validação cruzada, e 0,6508 na LB usando uma matriz TF-IDF junto com os outros atributos.

Uma razão que pode ajudar a explicar o fato dela se comportar melhor com a matriz TF-IDF do que com a matriz do SVD é que este é um método que reduz a variância, então acaba sendo robusto diante de muitos atributos, desde que uma parcela deles seja relevante para a previsão.

Gradient Boosted Trees

Apesar deste modelo também não ser muito recomendado para uso com dados esparsos em altas dimensões, decidi treinar vários modelos “fracos” e fazer o stacking usando as previsões deles e os outros atributos, que não envolviam a representação das palavras.

Os modelos do stacking tinham uma média de 0,60 Kappa na validação.

A pontuação deste modelo ficava por volta de 0,624 na LB. Mas a intenção real com ele era ter um modelo diferente do SVM para fazer o ensemble.

Pós Processamento

Em todas as soluções de Top 10 que eu li, havia um detalhe em comum: o pós processamento das previsões. As equipes usaram métodos especiais para fazer arredondamento, multiplicando por coeficientes, ou determinando pontos de corte diferentes para a previsão de cada “classe”.

Em um dos casos, o time ordenou as previsões e, de acordo com a proporção de cada classe nos dados de treino, arredondou as previsões. Kappa é uma métrica preocupada com a distribuição das previsões, então isso acabou ajudando.

O mais próximo que cheguei de um ajuste nas previsões foi utilizar a média geométrica em vez da média simples para o ensemble. Os modelos costumavam superestimar a relevância dos produtos, e ela puxava as previsões para a classes mais baixas.

Ensemble e Overfitting

No fim, fiz uma média geométrica com meus melhores modelos. O ensemble era formado por vários SVMs, uma Random Forest e um GBT.

Apesar de não ser uma prática recomendável, principalmente em casos com uma quantidade de dados pequena como esta competição, utilizei a LB para validar o ensemble. Isso acabou fazendo com que eu escolhesse o meu segundo melhor ensemble, mesmo assim acabei no Top 5% da competição.

Como Consegui o 6º lugar Entre 985 Participantes na Competição de Machine Learning do Facebook

Nesta competição de recrutamento, o Facebook disponibilizou dados anônimos sobre usuários de um site de leilões. A tarefa era criar um modelo que determinasse qual usuário é humano e qual é robô. Os usuários humanos de um site de leilões não querem ter que competir com máquinas. Um site com muitos robôs pode sofrer um êxodo de usuários humanos, então é importante ter um sistema robusto de detecção de usuários suspeitos.

Segundo os administradores, estes dados não vinham do Facebook, mas acredito que o interesse era criar um ambiente parecido com o sistema de leilões de publicidade deles.

Esta competição tornou-se bastante especial para mim, já que foi meu primeiro Top 10 no Kaggle, e acabei me tornando Kaggle Master. Este era meu primeiro objetivo, mas também tem o bônus de ser considerado para uma entrevista para a vaga de Data Scientist no Facebook.

Dados

Uma das partes mais interessantes desta competição, e que a tornava bastante próxima de um caso real de ciência de dados, era o fato de não termos um arquivo bruto com variáveis relacionadas ao objetivo, que pudesse ser utilizado diretamente com algum modelo de machine learning.

Foram disponibilizados dois arquivos: o primeiro continha cerca de 2000 linhas com informações anônimas dos usuários, como ID e endereço. O segundo continha mais de 7 milhões de lances dados no site.

As variáveis do segundo arquivo incluíam: ID do lance, ID do usuário, ID do leilão, categoria do leilão, dispositivo do usuário, tempo, país, IP e URL. O tempo estava codificado, de maneira que não era possível descobrir a data exata dos lances.

Validação

Ter um ambiente de validação robusto é essencial para qualquer tarefa de machine learning. Neste caso havia o desafio de termos apenas 2000 exemplos para treinar e validar. Após testar várias combinações, decidi utilizar uma validação cruzada estratificada, com 5 divisões, e repetir essa validação por 5 vezes.

A repetição acaba fazendo com que os exemplos sejam colocados, aleatoriamente, em divisões diferentes, o que ajuda a estabilizar a estimativa de erro, já que um ou outro exemplo particular terá menor influência na média.

Criação de variáveis

A parte mais importante de qualquer trabalho de ciência de dados é garantir que você tenha os dados corretos para fazer a previsão. Um erro muito comum é achar que é só colocar os dados no algoritmo, e ele vai aprender o que você quiser. Esta foi, de longe, a parte mais importante da minha solução.

Contagens

O primeiro passo foi criar variáveis com a contagem de quantos lances cada usuário tinha feito. Estendendo esta ideia, criei variáveis para a contagem de quantos dispositivos, IPs, categorias, países e URLs diferentes um mesmo usuário tinha utilizado. Com estas variáveis simples, uma Random Forest, sem tuning, atingia 0,86 AUC na validação cruzada com 5 divisões.

No caso dos países, além de integrar a variável própria para contar o número de países diferentes, fiz variáveis individuais para cada um deles, contando quantos lances o usuário tinha dado vindos de cada país. Isto ajuda o modelo a identificar “países de risco”, onde pode ser mais fácil hospedar robôs para dar os lances.

Estatísticas simples

Depois de ver que as contagens possuíam um grande valor informativo, decidi testar estatísticas simples, como médias e desvios. Exemplos de variáveis importantes:

– Média e desvio padrão de lances por leilão;
– Número máximo de lances dados num mesmo leilão, com a mesma timestamp;
– Média de tempo entre lances.

Temporais

A variável que indicava o tempo nos dados originais estava codificada de maneira que não dava para saber a data exata em que os lances ocorreram. Mas, após analisar os dados, percebi que mesmo não sabendo a data exata, era possível identificar o que seriam dias, horas e minutos.

Isto me permitiu criar variáveis relativas a médias, desvios, proporções e contagens baseadas em unidades de tempo. Algumas variáveis importantes:

– Desvio padrão de lances por dia;
– Média de lances por dia;
– Média de lances na mesma timestamp;
– Contagem de lances por hora do dia;
– Média de leilões que o usuário participou no mesmo dia.

Parece que todos os participantes que se posicionaram no topo da tabela conseguiram identificar estes padrões temporais.

Proporções

Algumas proporções baseadas nas contagens e no tempo também foram importantes. Alguns exemplos são:

– Proporção máxima do total de lances em um dia atribuídos ao usuário;
– Densidade de lances por hora do dia

Modelos

Quando você tem variáveis que contêm bastante informação sobre o evento, a parte de seleção e ajuste de parâmetros do modelo se torna mais simples. Neste caso, apesar de testar brevemente modelos lineares, acabei optando por explorar melhor modelos baseados em ensembles de decision trees.

Random Forest

O primeiro modelo que testei, e o que se provou mais útil neste caso.

Acredito que sua performance superior se deu por ser um modelo mais voltado para a diminuição da variância. Por ter poucos dados para treinar, era importante que o modelo conseguisse lidar bem com valores extremos, e estabilizasse as previsões.

Uma Random Forest com 500 árvores e parâmetros ajustados através da validação cruzada apresentava 0,9112 AUC na medição local, e 0,9203 na LB.

Gradient Boosted Trees

Este é um dos modelos mais utilizados em competições de ciência de dados porque reconhecidamente oferece uma boa performance em diversos tipos de tarefas. Neste caso não foi assim. Os meus melhores modelos ficaram por volta dos 0,90-0,91 AUC na LB apesar de alcançarem a 0,94 na validação.

Isso levanta a seguinte questão: temos um modelo baseado em decision trees que nos dá uma boa performance (Random Forest) e, em tese, boosting melhoraria a pontuação, por que este não é o caso aqui?

A minha resposta é: Random Forest aumenta o bias, ou seja, torna o modelo mais rígido, com menor variância, enquanto o GBT aumenta a variância, sendo mais sensível ao ruído presente nos dados. Neste caso temos menos de 2000 exemplos, e um forte desequilíbrio entre as classes. Estas duas razões combinadas geram dados com uma boa quantidade de variância, aumentá-la ainda mais vai causar overfitting.

A solução seria fazer o tuning e regularizar mais o GBT, mas com poucos dados para validar fica difícil confiar nos resultados. Por isso, decidi confiar nas razões teóricas e continuar trabalhando com a Random Forest para ter um modelo mais estável.

Ensembles

O toque final de boa parte das melhores soluções de competições é fazer um ensemble com os melhores modelos.

Ensemble Aleatório

Devido ao pequeno número de exemplos, era difícil confiar nos parâmetros encontrados através da validação. Por isso, decidi criar um ensemble com Random Forests “aleatórias” nos parâmetros. Cada uma delas tinha uma seed e alguns parâmetros diferentes. Este ensemble atingiu uma AUC de 0,92 na estimativa out-of-bag e na LB.

AdaBoost Random Forest

Apesar da instabilidade do GBT, decidi aplicar boosting à Random Forest. Talvez aplicando este método em modelos mais estáveis que as decision trees pudesse ajudar, mesmo com a pequena quantidade de exemplos. E foi isso mesmo que aconteceu.

Na validação obtive 0,925 de AUC, e 0,928 na LB.

Ainda assim havia uma diferença entre a pontuação da validação e da LB quando eu adicionava novas variáveis.

Estabilizando as seeds

Para estabilizar as previsões decidi rodar o modelo de Boosted Random Forest usando várias seeds diferentes, e fazer a média. Isso não deu um bom resultado na LB, mas apresentava estabilidade na validação cruzada, o que me fez confiar no modelo.

Conclusão

No fim, utilizei um modelo de Boosted Random Forest com um conjunto de atributos que apresentava AUC 0,94 na validação cruzada, e foi basicamente minha pontuação no test set final da competição, que me garantiu o 6º lugar.

Machine Learning Para Previsão de Vendas Usando Dados Meteorológicos

O WalMart é uma rede com milhares de lojas em 27 países. É possível encontrar vários artigos sobre os mecanismos tecnológicos utilizados para gerenciar a logística e distribuição dos produtos. É a segunda vez que eles oferecem uma competição no Kaggle com a intenção de encontrar candidatos para entrevistas para vagas de cientistas de dados.

Uma grande vantagem deste tipo de competição é termos acesso a dados de grandes companhias, e entender quais são os problemas que eles estão tentando resolver com modelos probabilísticos.

O objetivo da competição era criar um modelo que pudesse prever a quantidade de venda de alguns produtos, em lojas específicas, nos dias antes e depois de nevascas e tempestades. O exemplo dado por eles na descrição da tarefa foi a venda de guarda-chuvas, que intuitivamente deve ver um aumento antes de uma grande tempestade.

Dados

Para treinar o modelo foram oferecidos dois arquivos: um deles continha informações sobre a identificação das lojas, produtos, e as estações meteorológicas mais próximas. O outro continha dados meteorológicos de cada estação.

No total foram disponibilizados dados de 111 produtos cujas vendas podem ser afetadas pelas condições climáticas, 45 lojas, e 20 estações meteorológicas. O objetivo era prever a quantidade de cada produto, em cada loja, que seria vendida 3 dias antes, 3 dias depois, e no dia do evento climático.

Um evento climático era considerado caso tivesse sido registrada mais que uma polegada de chuva, ou mais que duas polegadas de neve naquele dia.

A combinação entre lojas e produtos nos dava cerca de 4,6 milhões de exemplos para treino, e cerca de 500 mil para teste. Cada exemplo se referia a um dia, loja e produto.

Métrica

A métrica utilizada foi o Root Mean Square Log Error. É basicamente o RMSE aplicado à transformação log(Y + 1) das previsões. Isso significa que erros em previsões que deveriam ser próximas de 0 seriam punidos mais severamente do que erros em previsões com números mais altos. Por exemplo, prever 5 itens quando deveria ser 0, tem uma punição maior do que prever 105 quando deveria ser 100.

Transformação dos dados

Como eu só tinha 10 dias para trabalhar nesta competição, decidi verificar até onde era possível chegar com um modelo baseado apenas nas variáveis originais e, quem sabe, fazer um ensemble.

Uma diferença desta para outras competições é que, apesar dos dados virem organizados, você era responsável por unir as variáveis climáticas com os dados identificados de produtos e lojas. Faz todo o sentido, pois o Walmart não iria querer um data scientist que não saiba manipular dados.

Para isto utilizei Pandas. Esta biblioteca Python para manipulação de dados é uma das mais utilizadas, e lembra bastante as estruturas de dados disponíveis em R.

Num primeiro momento usei todas as variáveis como numéricas, e treinei um XGBoost com leve tuning, excluindo as variáveis que codificavam alertas especiais, e usando 10% do dataset para determinar o número de árvores. Como já era de se esperar, o resultado foi ruim, cerca de 0,1643 na LB.

Binarizando as variáveis

Depois de testar o primeiro modelo, codifiquei as variáveis categóricas com one-hot encoding. Ou seja, para cada nível foi criada uma coluna com o indicador 0 ou 1, caso a variável estivesse presente naquele exemplo. Normalmente o número de colunas deve ser o número de níveis menos um, para não ter problemas com colinearidade. Como eu planejava usar modelos que não eram sensíveis a esse problema, eu não me preocupei em excluir uma das colunas.

Após fazer tuning usando um subset com 10% dos dados, e obter o RMSLE de 0,1216 na validação, enviei a solução, e obtive o valor de 0,1204 na LB, uma boa melhora sobre o anterior.

Imputação

Muitos dados meteorológicos não estavam presentes, então decidi testar um método simples de imputação: substituir os NaNs pela média dos valores da coluna. Após fazer o tuning novamente, agora adequado a esses novos valores, obtive o RMSLE de 0,1140 nos 10% de validação e 0,1095 na LB.

Variáveis temporais

Não explorei muito a dimensão temporal dos dados, em uma das tentativas adicionei informações meteorológicas do dia anterior, o que reduziu o erro para 0,1083.

Subdivisões dos dados

Um método que funcionou muito bem na minha primeira competição, e que sempre acabo tentando, é dividir o training set em pequenos subsets relacionados a alguma variável e treinar um modelo específico para cada um. Neste caso, decidi criar 19 modelos, um para cada uma das 19 estações meteorológicas presentes no test set. O RMSLE na LB foi de 0,1101. Pior do que com um modelo que trata as estações como variáveis no dataset inteiro.

Um problema grave com esta abordagem é tentar usar o mesmo modelo, com os mesmos parâmetros, para datasets diferentes. Sabendo disso, decidi fazer um pequeno tuning dos parâmetros para cada dataset, o que reduziu o RMSLE da LB para 0,1069.

Apesar da pequena diferença, parece que a divisão em modelos individuais para cada estação capturava algumas informações que não estavam presentes no modelo que considerava todas juntas.

Modelos

Dos modelos que testei, dois se destacaram: Gradient Boosted Trees (XGBoost) e Random Forests.

Random Forest

Eu tinha usado Random Forest para regressão apenas uma vez, em um trabalho, mas nunca com uma quantidade grande de dados. Após fazer o tuning dos parâmetros, aplicando o modelo nos dados imputados, ela resultou num RMSLE de 0,1116 na LB.

XGBoost

O XGBoost apresentou o melhor erro de um modelo individual. Além de ajustar parâmetros, como a profundidade das árvores, usando um subset dos dados, era necessário ajustar a quantidade de árvores e a learning rate. Normalmente uma learning rate pequena e uma grande quantidade de árvores é a receita segura para melhorar a performance, em troca de mais tempo para treinar o modelo.

Como o XGBoost é sensível à seed do RNG, decidi fazer um ensemble de XGBs mudando apenas este valor. Este método melhorou marginalmente a minha pontuação em outras competições, mas nesta o impacto dele foi maior pelo seguinte fato: o XGBoost possui uma função que permite deixar dados separados para que ele determine o número de árvores que minimiza o erro. Neste caso decidi usar a função, e deixei 5% dos dados separados. Além de variar a seed para o próprio XGB, eu variei a seed para fazer a divisão dos dados, isso fez com que os modelos ficassem mais diversos, o que é essencial para um bom ensemble.

O máximo que cheguei a treinar foram 10 XGBoosts neste esquema. Apesar de ser um modelo bastante estável, o RMSLE do ensemble foi de 0,1041, apresentando uma redução comparado a 0,1095 do modelo individual.

Solução Final

No fim, juntei todas as soluções que eu havia enviado, e acabei obtendo um RMSLE de 0,1028, garantindo uma posição entre as 20% melhores.

Possíveis Melhorias

Um dia após o término da competição eu revisei as variáveis do meu XGBoost, e descobri que as variáveis que identificavam os produtos (item_nbr) não estavam em formato binário, e eram consideradas por ele as mais importantes. Com a codificação correta acredito que seria possível diminuir mais o erro, e alcançar uma posição final melhor.

Classificando 150 Mil Produtos em Categorias Diferentes Usando Machine Learning

O grupo Otto é uma das maiores empresas de comércio online do mundo.
Segundo eles, devido à diversidade da infraestrutura global da empresa, muitos produtos similares acabam classificados em categorias incorretas. Por isso eles disponibilizaram dados sobre aproximadamente 200 mil produtos, pertencentes a nove categorias. O objetivo era criar um modelo probabilístico que classificasse corretamente os produtos dentro de cada categoria.

Dados

Para treino foram disponibilizados cerca de 60 mil produtos e, para teste, cerca de 150 mil.

Os 93 atributos não foram identificados, a única informação dada sobre eles é o fato de serem variáveis numéricas. Isso dificulta bastante para entendermos a melhor maneira de trabalhar com os dados, e também o trabalho de criação de novos atributos.

Além disso, a tarefa envolve múltiplas classes, ou seja, temos que gerar probabilidades sobre 9 classes.

Métrica

A métrica escolhida para esta competição foi a log loss multiclasse.

Esta métrica pune probabilidades muito confiantes em previsões erradas, e é sensível ao desequilíbrio entre as classes. Algumas delas representavam 20% dos dados de treino, enquanto outras, 9%. Neste caso, tentar equilibrar as classes, seja com subsampling ou penalizando as classes menores, não vai ajudar.

Criação de Atributos

Mesmo com as variáveis anônimas, decidi explorar a possibilidade de integrar interações entre elas, que talvez não fossem capturadas pelos modelos.

Básico

Procurei criar novos atributos baseados em somas, subtrações, razões e multiplicações entre os originais. O único atributo que contribuiu significativamente foi a soma de todos os atributos para um determinado produto. A maioria dos atributos era bastante esparsa (tinha mais zeros do que outros valores), então talvez isso tenha contribuído para a falta de sucesso.

Usando um GBM/RF para Selecionar Atributos

Como nós estamos falando de um espaço gigantesco de possibilidades de combinações de atributos, decidi usar uma técnica usada por um participante de outra competição e publicada neste link: http://trevorstephens.com/post/98233123324/armchair-particle-physicist

Basicamente consiste em criar alguns datasets com as interações desejadas e treinar um modelo de Gradient Boosted Trees em cada um. Após isso, verifica-se quais são os atributos mais importantes em cada dataset. A lógica é: se um atributo for importante em vários datasets diferentes, ele deve ser importante no geral.

Eu fiz isso nestes dados, usando 2 divisões estratificadas, mas apesar deles concordarem na importância de algumas interações, elas não melhoraram meu modelo base. Como eu já havia passado dias me dedicando a essa parte, eu decidi que ia me focar mais em ajustar modelos para um ensemble.

Caso eu tivesse continuado, talvez selecionar as X melhores interações e reajustar os hiperparâmetros de algum modelo pudesse extrair valor destas variáveis.

Modelos

Gradient Boosted Trees (XGBoost)

O meu modelo individual, com a menor log loss, foi criado com o XGBoost, que é uma implementação rápida e paralela das poderosas Gradient Boosted Trees. Esse modelo já esteve em soluções vencedoras de boa parte das competições e normalmente apresenta uma performance superior.

Para encontrar o melhor grupo de atributos usei uma busca aleatória. Fixei a learning rate em 0,05 e variei atributos como a profundidade das árvores, mínimo de exemplos que deveriam compor um nó, e a proporção de exemplos e variáveis que o algoritmo deveria selecionar aleatoriamente para criar cada árvore.

Normalmente quanto menor a learning rate, melhor a precisão, mas são necessárias mais árvores, o que aumenta o tempo de treino. No fim, deixei este valor em 0,01 e treinei 2000 árvores.

Estes atributos servem para controlar o overfitting. Após encontrar os melhores atributos, a log loss da validação cruzada em 3 divisões era 0,4656, e na leaderboard, 0,4368.

Random Forests

Apesar de não ter muito sucesso inicial com as Random Forests nesta competição. Uma sugestão dada no fórum foi de calibrar as previsões. Recentemente a equipe desenvolvedora do scikit-learn disponibilizou uma nova ferramenta que nos permite ajustar os valores de saída de um modelo de maneira que eles se tornem mais próximos das probabilidades reais.

Uma Random Forest normalmente faz uma votação entre seus componentes, e a proporção de cada classe é dada como a probabilidade do exemplo pertencer àquela classe. Essas proporções não correspondem às probabilidades reais dos eventos, então só teremos probabilidades relevantes se fizermos o ajuste.

Como esta competição pedia para prevermos a probabilidade de um produto pertencer a uma categoria, era muito importante ter as probabilidades ajustadas corretamente. Neste caso, utilizei a nova ferramenta do scikit-learn com 5 divisões dos dados. Em 4 delas ele treinava a Random Forest e, na restante, uma regressão isotônica. Isso era repetido 5 vezes, e a média da probabilidade das 5 regressões era o resultado.

Isso melhorou bastante a previsão da Random Forest e, apesar de também usarem decision trees, como o GBM, elas acabaram contribuindo significativamente para um ensemble.

Redes Neurais

Tive a oportunidade de conhecer dois módulos ótimos para treinar redes neurais de maneira simples em Python. Eles são Lasagne e NoLearn. Com eles é simples criar e treinar redes neurais de última geração, usando inclusive GPUs para processamento.

Mas não se engane, apesar deles facilitarem a implementação, uma rede neural exige uma grande quantidade de decisões para se tornar útil. Temos que determinar desde a arquitetura, até os métodos de inicialização dos pesos e regularização. Alguns pesquisadores sugerem fazer uma busca aleatória por parâmetros iniciais e continuar a ajustar manualmente.

Neste caso, a arquitetura que me serviu bem foi a seguinte: 3 layers de neurônios com o seguinte número de unidades em cada uma 768-512-256. Eu treinei duas versões, uma delas com dropout de 0.5 entre os hidden layers. E outra com dropout de 0.5 apenas no primeiro hidden layer. As unidades dos hidden layers eram ReLu, e a unidade de saída, softmax.

Uma interpretação interessante, de um outro competidor que chegou a uma arquitetura parecida, é de que o primeiro hidden layer permite que a rede faça projeções aleatórias dos dados, e os layers menores, que vêm após este, buscam uma sintetização das informações. Redes neurais são naturalmente difíceis de interpretar, mas eu achei este um ponto de vista bem interessante.

Por fim, como a mínima de uma rede neural é local, e dependente dos pesos iniciais (e aí temos muitos pesos iniciais), decidi fazer a média de 5 redes neurais com pesos iniciais diferentes. Esta média me deu um score de 0.4390 na LB, comparável ao XGBoost.

Outros

Ainda tentei treinar: SVM, Nearest Neighbors, Regressão Logística, mas nenhum deles apresentava uma boa performance sozinho, ou contribuía significativamente para o ensemble.

Ajustando os Hiperparâmetros

Já que não tínhamos acesso ao conteúdo das variáveis, era vital ajustarmos os parâmetros para termos os melhores modelos. No início fiz uma busca aleatória com valores razoáveis, mas que permitissem aos modelos explorar um pouco o espaço.

Normalmente os hiperparâmetros de um modelo não são independentes, ou seja, ajustar um de cada vez não vai te levar à melhor combinação. Por isso é importante fazer uma exploração com combinações de parâmetros. Infelizmente na maioria das vezes é impossível testar todas as combinações possíveis, mas quando fazemos a busca aleatória podemos ter uma ideia do espaço de soluções, sem ter que explorá-lo exaustivamente.

Depois de feita esta exploração, é bom variar manualmente alguns deles, e ver se é possível melhorar a performance em alguma combinação vizinha.

Resultado

No fim minha solução era formada por uma média ponderada dos seguintes modelos:

– Gradient Boosted Trees (XGBoost) sobre as variáveis originais e a soma das colunas.
– Random Forest sobre as variáveis originais, log(X+1) delas, e soma das colunas.
– Rede neural com 3 layers e dropout de 0.5 em cada um, sobre as variáveis originais, log(X+1) delas, e soma das colunas.
– Rede neural com 3 layers com dropout de 0.5 no primeiro hidden layer, sem dropout no segundo, sobre as variáveis originais, log(X+1) delas, e soma das colunas.

Além desses modelos, adicionei um bias, simplesmente prevendo a proporção de cada classe como probabilidade de um exemplo pertencer a elas.

Esta solução atingiu a log loss de 0,4080, e me garantiu a posição 35 entre 3514 times (Top 1%).

Soluções vencedoras

A solução do time vencedor consistia de um ensemble de três camadas:

Na primeira camada foram treinados 33 modelos diferentes, variando tanto o tipo do modelo, quanto as variáveis utilizadas para treinar cada um. Alguns deles eram múltiplos modelos treinados com bagging.

Na segunda camada, as previsões destes modelos eram utilizadas para alimentar um XGBoost, uma rede neural, e um ExtraTrees com Adaboost.

Na terceira, e última, camada, foi feito o bagging dos três modelos da segunda camada (totalizando 1100 modelos) e depois uma média ponderada entre os três.

Além disso, foram criadas novas variáveis, baseadas na distância do exemplo para os exemplos mais próximos de cada classe, e também outras baseadas em transformações TF-IDF e T-SNE do dataset original.

Usando Machine Learning Para Identificar Motoristas Através de Dados GPS

Nos últimos anos a indústria de seguros tem buscado maneiras de aprimorar seus modelos usando Machine Learning. Uma delas é utilizar dados que vão além de um formulário preenchido pelo segurado para determinar o risco de acidentes.

Um dos métodos utilizados é usar dados comportamentais do motorista, obtidos através de rastreamento via GPS. Desta maneira acredita-se ser possível capturar informações e padrões de perfil que vão além dos métodos tradicionais.

O que torna um motorista diferente do outro? Identificar o motorista que está conduzindo um veículo durante uma viagem é o primeiro passo para conseguirmos um bom modelo.

Descrição da Competição

A seguradora AXA decidiu disponibilizar viagens anônimas de aproximadamente 2700 motoristas. Para cada motorista são 200 viagens, sendo que algumas delas não são do motorista em questão, e a tarefa era criar um modelo que identificasse quais eram as viagens atribuídas a este motorista incorretamente.

Para evitar o vazamento de informações, os dados foram centralizados e rotacionados de maneira aleatória, além de ter alguns trechos das viagens removidos do início e do fim.

Dados

No total foram disponibilizadas 200 viagens de 2736 motoristas. Dentro da pasta de cada motorista havia uma quantidade aleatória de viagens falsamente atribuídas ao motorista em questão. Uma informação importante é que tínhamos a garantia que a maioria das viagens pertencia verdadeiramente ao motorista.

Cada viagem era descrita em um arquivo CSV com um valor numérico para a posição x e y, que seria a distância da origem, em metros, e cada linha correspondia a 1 segundo de viagem.

Métrica

A métrica utilizada para avaliação foi a área sob a curva ROC, abreviada por AUC.

Atributos

Os atributos que utilizei se baseiam principalmente nas três primeiras derivadas do deslocamento: velocidade, aceleração e arrancada. Outras derivadas acabavam por prejudicar a performance.

Frequências – FFT

Procurei extrair frequências, tanto dos dados originais brutos, quanto das derivadas. Fiz isto com a função FFT (Fast Fourier Transform) disponível no Numpy. Dividi o intervalo de frequências por 20, e o valor dos atributos era a soma das frequências do respectivo intervalo.

Neste caso eu extraí as frequências isoladas para cada componente (x e y). Talvez obter as frequências usando a velocidade e aceleração, considerando os dois componentes juntos, seja melhor, mas não testei esta hipótese.

O AUC máximo que consegui, usando frequências das três derivadas, foi 0,7058, com uma regressão logística sem otimização de parâmetros. Apesar de não ter testado, acredito que otimizando os parâmetros do modelo para cada motorista este número poderia ser aumentado até um máximo de 0,73.

Histogramas

Esta é a transformação que encontrei em todos os trabalhos acadêmicos que li sobre o assunto. Consiste em fazer um histograma dos atributos. Neste caso, ganhamos um novo parâmetro, o tamanho dos intervalos.

A melhor maneira que encontrei foi dividir a magnitude da velocidade em 50 intervalos, e a aceleração, em 10.

Dois detalhes que melhoraram a performance: utilizar o atributo “density” da função do Numpy, que calcula a função densidade da probabilidade, e normalizar os resultados para que sua soma fosse 1.

Valores em Porcentagens

Outra sugestão bastante frequente era utilizar o valor dos atributos em determinadas porcentagens. Isso ajuda a descobrir quão extremos são os valores dos mesmos.

Um exemplo pode facilitar o entendimento: no caso da velocidade, para obter o valor presente no percentil 5%, ordenei os valores de maneira crescente e localizei o ponto da lista que representava a fronteira dos primeiros 5% dos dados ordenados.

Se tivermos uma lista com 100 valores de velocidade ordenados, com um índice que vai de 1 a 100, o valor presente na posição 5 seria o desejado. O valor presente na posição 50, por exemplo, seria a mediana.

Isso é importante porque diferencia motoristas que possuem um perfil mais rápido ou mais lento.

Eu utilizei os percentis 5% a 95%, em intervalos de 5%.

Viagens Repetidas

Uma abordagem que foi importante para que os melhores colocados subissem na tabela foi criar um algoritmo para encontrar viagens similares. Era notável que alguns trechos, apesar de possuírem um ângulo diferente com relação à origem, se tratavam do mesmo caminho.

Eu fiz um algoritmo simples, baseado na velocidade do motorista durante a viagem mas, apesar de ter encontrado algumas viagens similares, acabava prejudicando a pontuação.

Os algoritmos que tiveram sucesso eram mais complexos, e em alguns casos envolviam a transformação das variáveis, redução de dimensões, e limites para determinar a similaridade. Dentre todos, dois métodos pareceram essenciais:

Ramer–Douglas–Peucker: como cada viagem tinha uma duração diferente, este método foi utilizado para reduzir, e igualar, o número de pontos.

Dynamic Time Warping: este método foi utilizado para comparar os trechos e determinar a similaridade entre eles.

Amostragem

Como não era possível saber realmente quais eram as viagens de cada motorista. Ou seja, existiam viagens marcadas como positivas, quando na realidade eram negativas, havia a oportunidade de se decidir a melhor maneira de fazer a amostragem dos dados.

Aleatório

Num primeiro momento, com métodos não supervisionados, usei apenas as 200 viagens de cada motorista. Uma sugestão no fórum da competição foi usar as 200 viagens de um motorista como exemplos positivos e usar viagens de outros motoristas como exemplos negativos.

Em geral esta foi uma abordagem bastante positiva. Dentre as maneiras de fazê-la estavam:

– Escolher, aleatoriamente, uma quantidade N de motoristas e tratar todas as viagens deles como negativas.

– Escolher, aleatoriamente, uma quantidade V de viagens de N motoristas diferentes, para usar como negativas.

A abordagem que me serviu melhor foi baseada na segunda opção: selecionar um pequeno número de viagens de vários motoristas diferentes.

Amostras similares/diferentes

Outra alternativa que testei foi selecionar amostras negativas de motoristas que eram mais similares ou diferentes das amostras positivas, de acordo com uma medida como distância euclidiana.

Criei atributos como a média de velocidade, distância percorrida, e duração das viagens para cada motorista e determinei quais eram os mais parecidos, e quais eram os mais diferentes do motorista em questão.

Apesar da abordagem utilizando os motoristas “mais diferentes” mostrar uma melhora durante a validação cruzada, ela não melhorou a pontuação no test set. A razão que atribuo a isto é o fato dos dados da validação conterem viagens que são falsas, mas estão marcadas como verdadeiras, enquanto que no test set temos a realidade absoluta.

Reforçando as previsões

Procurei reforçar as previsões do modelo treinando mais de uma vez, mas agora usando as previsões do modelo anterior no lugar da variável dependente original, da seguinte maneira:

No primeiro passe fiz o procedimento normal, usando as viagens do motorista como positivas, e outras aleatoriamente selecionadas de outros motoristas como negativas para obter previsões.

Nos próximos passes eu utilizei as previsões do modelo anterior como variável dependente. Isso melhorou bastante na validação cruzada, mas apresentou uma melhora muito pequena nos dados de validação do Kaggle.

Modelos e Métodos

Clustering

Num primeiro momento decidi agrupar as viagens de acordo com características similares. Essa ideia foi baseada na suposição que, como a maioria das viagens pertencia ao motorista, seria possível separar as viagens em dois clusters, e aquele que tivesse a minoria das viagens seria o “negativo”.

Outra ideia era aumentar o número de clusters, já que existiam diferenças das viagens entre si, mesmo pertencentes ao mesmo motorista.

De qualquer maneira, estas ideias se mostraram inúteis, com AUC próximo de 0,5.

Outra maneira não supervisionada que tentei foi calcular a similaridade de cosseno entre a média dos atributos das viagens e cada uma delas. Neste caso a “probabilidade” de uma viagem pertencer ao motorista seria um menos este valor. O que também se mostrou inútil.

Regressão Logística

O meu modelo com maior sucesso foi a regressão logística. Isso me surpreendeu, já que o sucesso descrito por vários participantes no fórum envolvia boosted trees.

Utilizei cerca de 1000 exemplos, 200 positivos e 800 negativos. Um detalhe importante foi ativar a opção “class_weight=auto”. Esta opção atribui uma penalidade diferente aos exemplos, de acordo com a distribuição das classes. Neste caso, um erro ou acerto na classe positiva valia mais do que o mesmo na classe negativa. Desta maneira, apesar de termos mais exemplos positivos, a penalização por erros era equilibrada.

Treinei uma regressão para cada motorista, personalizando a penalidade de regularização (L1 ou L2) e o coeficiente da mesma. Cada modelo fazia a validação cruzada de valores para este coeficiente utilizando os dois tipos de regularização, e no fim selecionava o tipo de regularização e o coeficiente que apresentasse a melhor AUC.

O melhor modelo individual atingiu a AUC de 0,86.

Random Forests/Boosted Trees

De acordo com os fóruns, o modelo mais utilizado foi Gradient Boosted Trees. Basicamente ele consiste em treinar uma sequência de decision trees pouco complexas, atribuindo um peso maior a exemplos que foram classificados incorretamente pelas árvores anteriores. A probabilidade de um exemplo específico é uma combinação linear das previsões de cada árvore.

No meu caso as tentativas de utilizar este modelo não foram bem sucedidas. Dois motivos podem ter contribuído:

– Por desconfiança da validação, eu acabei não dando muita atenção à otimização dos parâmetros dele para conseguir uma boa AUC.
– O processo de validação cruzada para encontrar bons parâmetros ia demorar muito, e testes preliminares não haviam apresentado bons resultados.

De qualquer maneira treinei um modelo de Random Forests, com os parâmetros padrões, para utilizar num ensemble. Ele sozinho possuía AUC de 0,847.

Ensemble

Próximo do fim da competição decidi partir para a forma mais “fácil” de se melhorar a pontuação: ensembles.

Para uma parte do ensemble treinei os seguintes modelos:

– Regressão Logística apenas com regularização L1
– Regressão Logística apenas com regularização L2
– SVM RBF
– SVM Linear
– Random Forests

Apesar do SVM não oferecer probabilidades, acreditei que ele seria uma boa contribuição para o ensemble, o que se mostrou verdadeiro.

A média simples da previsão destes modelos produziu uma AUC de 0,8825.

Por fim, juntei estes com todos os outros que já havia produzido desde o início da competição, para uma AUC final de 0,8915. Isto me garantiu uma colocação entre as 15% melhores soluções da competição.

Métodos das soluções vencedoras

As soluções vencedoras usaram 400 exemplos para cada motorista, selecionando viagens aleatórias de outros motoristas como exemplos negativos. O modelo mais comum foi Gradient Boosted Trees. Além disso, algoritmos complexos para encontrar viagens similares e atribui-las diretamente ao motorista conquistaram os pontos finais necessários para se posicionar entre as 10 melhores soluções.

Calculando a Probabilidade de Cliques em 4 Milhões de Anúncios Usando Métodos para Big Data

A Avazu é uma plataforma de publicidade que oferece anúncios em sites e aplicativos móveis. Um dos maiores desafios do ramo é determinar qual o anúncio com a maior relevância para o usuário. Se o anúncio estiver de acordo com os interesses do usuário, a chance dele clicar é maior, e isso aumenta os lucros tanto da plataforma quanto do anunciante, que terá um cliente em potencial visitando seu site.

Machine Learning têm sido usado por várias empresas que oferecem anúncios no formato pay-per-click. Ou seja, que são pagas pelo anunciante a cada clique no anúncio oferecido. Dentre as aplicações, a criação de modelos inteligente para calcular a probabilidade de um usuário clicar no anúncio, baseando-se em:

Informações de perfil: localização, dispositivo utilizado;
Informações do anunciante: categoria, tamanho e cor do anúncio;

E também dados sobre o site ou aplicativo no qual o anúncio é exibido.

Neste repositório disponibilizo o código original para a implementação do FTRL Proximal pelo usuário tinrtgu do Kaggle, baseado no paper desenvolvido pelo Google, e também o código modificado para obter meu melhor modelo individual.

Descrição dos dados

Nesta competição a Avazu ofereceu um dataset com 10 dias de informações, tanto sobre os usuários, quanto sobre os anunciantes e as plataformas de exibição. O objetivo era prever a probabilidade de um visitante, em um aplicativo ou site, clicar no anúncio exibido. Como teste, foi reservado um dia, 31/10/2014, para avaliar o modelo.

O training set possuía cerca de 40 milhões de linhas, e o teste cerca de 4 milhões. Além disso, é um problema de alta dimensionalidade, com mais de 1 milhão de variáveis independentes.

A métrica escolhida para avaliação foi a LogLoss, que pune previsões incorretas que são muito confiantes. Um detalhe é que existia um desequilíbrio entre a proporção de exemplos positivos e negativos, e esta métrica favorece acertos na classe com maior representação (no caso, os negativos).

Modelo base

Atribuir uma probabilidade de 50% a todos os exemplos oferecia uma log loss de 0,6932.

O primeiro modelo, mais simples, que testei foi uma regressão logística com Vowpal Wabbit, sem regularização e com apenas 1 passe sobre os dados. Ele oferecia a log loss de 0,3993.

Limpeza dos dados

Devido ao grande número de variáveis independentes, algumas delas acabam aparecendo pouco no dataset, e podem atrapalhar a classificação. Como neste caso o modelo utilizado foi a regressão logística, e ela é sensível a outliers, a remoção deles poderia melhorar a performance.

Desta maneira, fiz dois testes: remover os outliers, ou agrupá-los sob uma variável “rara”.

A escolha que melhorou significativamente a log loss foi agrupar as variáveis que apareciam menos de 5 vezes sob uma variável comum. A regressão logística, sem otimização, aplicada ao dataset “limpo” alcançou uma log loss de 0,3954.

Problemas com validação

Nesta competição um dos grandes problemas era encontrar uma maneira adequada de fazer validação. Estes são dados que mudam rapidamente, novas variáveis entram, e antigas deixam de ser utilizadas em questão de dias ou semanas, então é natural que haja esta dificuldade para chegar a um consenso sobre a performance exata do modelo.

Algumas formas foram discutidas no fórum da competição, e duas delas foram bastante utilizadas:

Holdout

Nesta modalidade, a cada N exemplos utilizados para treinar o modelo, é calculada a log loss no exemplo N+1. Após passar pelo dataset inteiro, é calculada a média. Nas minhas tentativas este se mostrou um método bastante otimista quanto à realidade.

Validação nos últimos dias

Neste caso, por se tratar de dados com dependência temporal, o modelo era treinado excluindo-se o último, ou os dois últimos dias do dataset, que eram utilizados para validação. Esta alternativa parecia ser mais precisa do que a outra, mas não era robusta à escolha de interações entre as variáveis, então era necessário ter cuidado com o overfitting nestes casos.

Criação de variáveis

Todas as variáveis desta competição eram binárias, e algumas delas eram anônimas, então eu não tinha informações sobre o que elas representavam. Desta maneira, testei duas alternativas para criar novas variáveis que pudessem colaborar na classificação.

Frequência

Uma das alternativas foi contar quantas vezes cada variável aparecia no dataset e usar este valor na classificação. Esta tentativa não deu certo, e piorou o desempenho. Uma alternativa que apresentou melhora, mas nada significativo, foi criar uma variável indicando se um determinado exemplo possuía mais ou menos de 100 ocorrências.

Modelos utilizados

Regressão Logística – Vowpal Wabbit

Aproveitei esta oportunidade para conhecer uma nova ferramenta: Vowpal Wabbit. Uma implementação rápida de algoritmos com funções convexas de Machine Learning tanto para regressão, quanto para classificação. Só para lembrarmos, a regressão logística “pura”, sem limpeza dos dados, apresentava uma log loss de 0,3993. Após a limpeza este número caiu para 0,3954.

SVM calibrado – Vowpal Wabbit

Como era necessário enviar uma lista de probabilidades para o Kaggle, tentei utilizar a distância dos dados para o hiperplano do SVM como inputs, tanto para uma regressão logística (inspirado em Platt’s scaling), quanto para uma regressão isotônica, disponível no scikit-learn. São dois modos populares de calibrar um SVM para que ele nos dê probabilidades. As duas não apresentaram uma boa melhora na pontuação.

Follow the Regularized Leader Proximal (FTRL Proximal)

Paper Google – Ad Click Prediction (em inglês)

Este foi o algoritmo que me ajudou a melhorar bastante a log loss. Ele foi implementado por um dos competidores e disponibilizado no fórum da competição. Desenvolvido pelo Google para a mesma tarefa de calcular a probabilidade de clique num anúncio, baseado em informações do usuário, ele cria uma representação mais esparsa dos dados, e acaba sendo mais robusto contra outliers. Neste paper o autor descreve a implementação e as características dele comparado a outros algoritmos utilizados para a mesma tarefa.

Pode-se dizer que ele é basicamente uma regressão logística com ajustes para não precisar utilizar tanta memória para armazenar os pesos, e também uma forma de otimização que força os pesos menos significativos a tomarem o valor de zero absoluto. Ou seja, num problema com milhões de variáveis como este, desconsidera aquelas que não são realmente importantes. Faz a seleção de atributos automaticamente.

Com os dados limpos, uma leve otimização, e 3 passes sobre os dados, este modelo atingiu log loss de 0,3925.

Redes Neurais

Fiz uma tentativa de classificação com redes neurais tanto no Vowpal Wabbit quanto com uma implementação própria. No Vowpal Wabbit a rede neural possui apenas um hidden layer com ativação sigmóide. Não houve melhora.

Criei, em Python, uma rede neural com unidades de ativação ReLu. Este tipo de ativação tem sido bastante utilizado na área de Deep Learning, devido a não ter o problema de gradientes explodindo ou desaparecendo, além de favorecerem uma representação de dados esparsa. Em alguns casos o resultado é equivalente, ou melhor, do que redes previamente ativadas com treinos não supervisionados.

Utilizei apenas um hidden layer, e neste caso, houve uma melhora na validação dos dados, mas ela não se traduziu em melhora na pontuação oficial da competição. Talvez utilizar mais de um hidden layer, e começar a entrar na área de Deep Learning tivesse ajudado, mas eu não tive tempo para testar.

O fato de só termos variáveis binárias também dificulta o trabalho da rede neural em encontrar uma representação adequada, e aprender interações que possam ser relevantes para a classificação. Usar variáveis numéricas, derivadas das originais, poderia facilitar o trabalho

A melhor rede neural, nos dados originais limpos, com 30 neurônios no hidden layer, e ativação ReLu, atingiu a log loss de 0,3937.

Interação entre variáveis

Outra alternativa era criar interações entre duas ou mais variáveis. Isso era feito simplesmente criando uma variável que indicasse a presença de combinações entre variáveis. Num primeiro momento tentei a interação entre todas as duplas de variáveis, o que piorou o desempenho.

Fiz uma nova tentativa, desta vez criando manualmente combinações que me pareciam relevantes (representando o acesso de um determinado usuário a um aplicativo, por exemplo). E elas acabaram por melhorar o modelo, reduzindo a log loss para 0,3897.

Estas tentativas foram feitas utilizando o código FTRL Proximal.

Hashing trick

Num problema com muitas variáveis independentes como este surge a dificuldade de armazenar pesos para todas elas na memória. Apesar deste ser um dataset pequeno quando falamos de big data, já requer o uso do hashing trick. Esta técnica consiste em fazer o hashing das variáveis e atribuir pesos aos buckets, em vez de diretamente a cada variável. Neste caso, por termos apenas variáveis binárias, é bastante fácil utilizar.

Após feito o hash, utiliza-se uma lista com os índices dos buckets que devem ter seus pesos atualizados naquela iteração. Existe o risco de colisão, e quanto maior o número de buckets, menor este risco. Mesmo assim, na prática não há uma perda significativa de performance por esta razão, por isso esta se tornou uma técnica amplamente utilizada em problemas que envolvem alta dimensionalidade dos dados.

Tanto o Vowpal Wabbit, quanto o FTRL Proximal utilizam esta técnica.

Ensemble

É quase impossível pontuar bem numa competição do Kaggle sem juntar modelos num ensemble. Eu não conheço uma competição que os participantes nas melhores posições não tenham usado esta técnica. Basicamente, se você tiver modelos que tenham uma precisão parecida, mas erros diferentes, há uma boa probabilidade que, se juntar as previsões deles, você terá uma previsão melhor.

Visite este artigo para saber mais sobre maneiras de usar ensemble learning para melhorar seus modelos de machine learning.

Modelos em subdivisões dos dados baseados nas variáveis independentes

A primeira tentativa de ensemble foi criar modelos individuais para determinadas subdivisões do dataset. Por exemplo, criar 24 datasets, cada um contendo apenas os exemplos gerados numa determinada hora, ou criar um dataset para cada ID de aplicativo, e treinar modelos individuais neles. Depois, subdividir da mesma maneira o test set e obter previsões destes modelos individuais.

Em alguns casos, se as subdivisões forem feitas em grupos que realmente apresentem diferença entre eles, a performance pode melhorar bastante. No nosso caso, por exemplo, utilizando apenas os modelos divididos por hora, a log loss da regressão logística, antes da limpeza dos dados, caiu para 0,3976.

Após criar vários modelos baseados em subgrupos que pareciam ser diferentes entre eles, o ensemble final, fazendo a média simples de 8 modelos, atingiu uma log loss de 0,3935.

Modelos baseados em interações aleatórias das variáveis

Outra alternativa para criar ensembles é utilizar variáveis diferentes para cada modelo. Após verificar que algumas interações melhoravam a pontuação utilizando o FTRL Proximal, decidi criar um script que testava o efeito que uma interação teria na pontuação do ensemble. Apesar de interessante, deve-se tomar bastante cuidado com o overfitting.

Cada modelo selecionava uma interação e testava se o desempenho de um ensemble entre ele, e alguns com outras interações, melhorava. Esta tentativa gerou 5 modelos lineares com diferentes combinações de variáveis, que juntos, fazendo a média simples, atingiram uma log loss de 0,3888.

Juntando os melhores os modelos

No fim, juntei vários modelos que apresentavam uma boa pontuação, e cheguei à log loss de 0,3878 que garantiu a 42ª posição, entre as 3% melhores soluções. A diferença entre a log loss desta solução e da vencedora foi de 0,0087.

Outras ideias e solução vencedora

Após o término da competição é comum que os participantes com as melhores colocações divulguem seus modelos e ideias que contribuíram para alcançar uma boa performance.

Neste caso, além dos métodos descritos aqui, duas aproximações se destacaram: atributos que levavam em conta fatores temporais, como o número de visitas de um usuário a um site ou aplicativo na última hora; quantidade de visitas no mesmo dia; e utilizar atributos relacionados à contagem de vezes que a variável, ou interações entre variáveis, apareciam.

Saber utilizar as características temporais dos dados parece ter favorecido bastante os melhores colocados.

Usando Machine Learning para Descobrir Anúncios Ilegais Em Russo, Sem Saber Falar Russo

O site Avito.Ru, fundado em 2007, é um dos 5 maiores sites russos de classificados. Ele é utilizado tanto por pessoas físicas quanto empresas para negociar produtos novos e usados. Um dos maiores desafios é manter a qualidade do conteúdo diante do crescimento e do volume de novos anúncios. Para resolver isso, o site decidiu buscar uma solução baseada nos dados disponíveis.

Esta foi uma competição disponibilizada na plataforma Kaggle.

Para os leitores com conhecimento técnico, o código da solução final está disponível neste link (github).

Dados disponíveis

No banco de dados disponibilizado havia o conteúdo dos anúncios, atributos sobre os produtos anunciados, categoria e subcategoria, título e descrição. A variável que precisava ser prevista era binária, se o anúncio seria bloqueado ou não pela moderação. Cerca de 4 milhões de anúncios com as previsões dadas pela moderação humana foram disponibilizados para treino, e 1,5 milhão para teste.

Além disso havia uma variável indicando se aquele anúncio havia sido bloqueado por um moderador experiente, que poderia fazer a diferença no fator de erro humano.

Métrica de Avaliação

A métrica utilizada para avaliar a melhor solução era AvgPrecision@K (Top at K). Após atribuir uma pontuação que indicava a probabilidade de um determinado anúncio estar em desacordo com as regras, eles deveriam ser ordenados, de maneira que os mais prováveis estivessem mais acima no ranking. Feito isso, o sistema considerava os K primeiros anúncios e comparava com os valores reais para decidir qual a porcentagem de acerto do modelo.

Transformação dos dados

Minha solução envolveu apenas o título e a descrição dos anúncios. Primeiro o básico, colocar todas as palavras em caixa baixa, remover as stopwords, e fazer o stemming. Um detalhe é que os anúncios estavam em russo, então eu não sei exatamente se existem diferenças de caixa alta e baixa no idioma, ou quais stopwords são relevantes para outras palavras (no caso do inglês, “can” é uma stopword, mas pode ser relevante por também ser usada como “lata”).

De qualquer maneira, esses procedimentos melhoravam a performance do modelo.

Além disso, transformei os documentos numa matriz em que cada linha era um anúncio, e cada coluna, uma palavra. Nos valores testei três variações:

– Binária: na qual a presença da palavra no anúncio era indicada com o número 1;
– Contagem: cada valor era o número de vezes que a palavra aparecia no anúncio;
– Tf-Idf: cada valor era baseado em uma fórmula que leva em conta a frequência da palavra no anúncio, e também em relação aos outros anúncios. Ela atribui um valor maior a palavras raras no contexto geral, mas que tenham uma alta frequência dentro do anúncio específico.

Dentre estas alternativas, a que demonstrou melhor performance foi a Tf-Idf. Esta é uma técnica bastante usada em classificação de texto, e normalmente apresenta uma melhora na precisão da classificação.

Apesar de fazer algumas tentativas de limpeza dos dados, tirando as palavras mais raras, a performance era afetada negativamente. O que ajudou um pouco foi remover números.

Solução

Meu foco era criar uma solução simples, então meu primeiro teste foi com a Logistic Regression em todos os dados. Um dos fatores a se levar em conta era a distribuição de exemplos positivos e negativos, que não era 50%. Com esta solução a precisão era de 91,8%.

Após testar algumas opções disponíveis no scikit-learn (biblioteca de Machine Learning em Python que utilizei), descobri que usando a função loss “modified_huber” apresentava uma maior precisão. Esta é uma função mais robusta do que a log loss, pois aplica uma penalidade quadrática para pequenos erros, e linear para grandes.

Outra ideia que ajudou muito foi separar os anúncios por categoria. Cada categoria possuía uma proporção diferente de exemplos positivos e negativos (Algumas com menos de 1% e outras com mais de 30%). Aplicando o algoritmo acima nos dados transformados desta maneira obtive 97,3% de precisão. Uma melhora de 10,6%.

Para a solução final, treinei também uma modelo Naive Bayes que, apesar de assumir que as variáveis são independentes, possui uma boa performance para classificação de texto. Ao fazer a média da solução dos dois algoritmos, consegui a precisão final de 97,9%.

Diferenças para a solução vencedora

Comparando a minha solução com a solução da dupla vencedora, há uma pequena diferença de 0,8% de precisão. Mas quando olhamos a complexidade, a história é outra. A solução vencedora utilizou mais de 30 modelos, entre transformações e classificação, para cada exemplo do training set. Na prática, assim como acontece normalmente nestas competições, não valeria a pena implementar a solução vencedora. Mas isso não tira o crédito dos vencedores, e também a oportunidade de aprendizado.

Teste