Hackatona SNA 2019

Em fevereiro-março de 2019, foi realizado um concurso para classificar o feed da rede social Hackatona SNA 2019, em que nossa equipe ficou em primeiro lugar. No artigo falarei sobre a organização da competição, os métodos que testamos e as configurações do catboost para treinamento em big data.

Hackatona SNA 2019

Hackatona SNA

Esta é a terceira vez que um hackathon com este nome é realizado. É organizado pela rede social ok.ru, respectivamente, a tarefa e os dados estão diretamente relacionados a esta rede social.
SNA (análise de redes sociais), neste caso, é mais corretamente entendida não como uma análise de um gráfico social, mas sim como uma análise de uma rede social.

  • Em 2014, a tarefa era prever o número de curtidas que uma postagem receberia.
  • Em 2016 - a tarefa VVZ (talvez você conheça), mais próxima da análise do gráfico social.
  • Em 2019, classificar o feed do usuário com base na probabilidade de o usuário gostar da postagem.

Não posso falar de 2014, mas em 2016 e 2019, além da capacidade de análise de dados, também foram necessárias habilidades para trabalhar com big data. Acho que foi a combinação de aprendizado de máquina e problemas de processamento de big data que me atraiu para essas competições, e minha experiência nessas áreas me ajudou a vencer.

mlbootcamp

Em 2019, o concurso foi organizado na plataforma https://mlbootcamp.ru.

A competição começou online no dia 7 de fevereiro e consistiu em 3 tarefas. Qualquer pessoa poderia se cadastrar no site, baixar baseline e carregue seu carro por algumas horas. Ao final da etapa online do dia 15 de março, os 15 melhores de cada evento de salto foram convidados ao escritório do Mail.ru para a etapa offline, que aconteceu de 30 de março a 1º de abril.

Tarefa

Os dados de origem fornecem IDs de usuário (userId) e IDs de postagem (objectId). Se uma postagem foi mostrada ao usuário, os dados contêm uma linha contendo userId, objectId, reações do usuário a esta postagem (feedback) e um conjunto de vários recursos ou links para imagens e textos.

ID do usuário ID do objeto ID do proprietário retornos imagens
3555 22 5677 [gostei, cliquei] [hash1]
12842 55 32144 [não gostei] [hash2,hash3]
13145 35 5677 [clicado, compartilhado] [hash2]

O conjunto de dados de teste contém uma estrutura semelhante, mas falta o campo de feedback. A tarefa é prever a presença da reação “curtida” no campo de feedback.
O arquivo de submissão possui a seguinte estrutura:

ID do usuário Lista ordenada[objectId]
123 78,13,54,22
128 35,61,55
131 35,68,129,11

A métrica é o ROC AUC médio para usuários.

Uma descrição mais detalhada dos dados pode ser encontrada em site do conselho. Você também pode baixar dados lá, incluindo testes e fotos.

Palco on-line

Na etapa online, a tarefa foi dividida em 3 partes

  • Sistema colaborativo — inclui todos os recursos, exceto imagens e textos;
  • Imagem — inclui apenas informações sobre imagens;
  • Textos — inclui informações apenas sobre textos.

Estágio off-line

Na fase offline, os dados incluíam todos os recursos, enquanto os textos e imagens eram esparsos. Havia 1,5 vezes mais linhas no conjunto de dados, das quais já existiam muitas.

Resolução de problemas

Como faço CV no trabalho, comecei o meu percurso neste concurso com a tarefa “Imagens”. Os dados fornecidos foram userId, objectId, OwnerId (o grupo no qual a postagem foi publicada), carimbos de data e hora para criação e exibição da postagem e, claro, a imagem desta postagem.
Depois de gerar diversos recursos baseados em timestamps, a próxima ideia foi pegar a penúltima camada do neurônio pré-treinado no imagenet e enviar esses embeddings para boosting.

Hackatona SNA 2019

Os resultados não foram impressionantes. As incorporações do neurônio imagenet são irrelevantes, pensei, preciso fazer meu próprio codificador automático.

Hackatona SNA 2019

Demorou muito e o resultado não melhorou.

Geração de recursos

Trabalhar com imagens leva muito tempo, então resolvi fazer algo mais simples.
Como você pode ver imediatamente, existem vários recursos categóricos no conjunto de dados e, para não me preocupar muito, apenas usei o catboost. A solução foi excelente, sem nenhuma configuração cheguei imediatamente à primeira linha da tabela de classificação.

São muitos dados e estão dispostos em formato parquet, então, sem pensar duas vezes, peguei o scala e comecei a escrever tudo no spark.

Os recursos mais simples que deram mais crescimento do que incorporações de imagens:

  • quantas vezes objectId, userId e OwnerId apareceram nos dados (devem estar correlacionados com a popularidade);
  • quantas postagens o userId viu do proprietário (deve estar correlacionado com o interesse do usuário no grupo);
  • quantos userIds únicos visualizaram postagens de OwnerId (reflete o tamanho do público do grupo).

A partir dos timestamps foi possível obter o horário do dia em que o usuário assistiu ao feed (manhã/tarde/noite/noite). Ao combinar essas categorias, você pode continuar a gerar recursos:

  • quantas vezes o userId fez login à noite;
  • em que horário esta postagem é mostrada com mais frequência (objectId) e assim por diante.

Tudo isso melhorou gradativamente as métricas. Mas o tamanho do conjunto de dados de treinamento é de cerca de 20 milhões de registros, portanto, a adição de recursos atrasou bastante o treinamento.

Repensei minha abordagem ao uso de dados. Embora os dados dependam do tempo, não vi nenhum vazamento de informação óbvio “no futuro”, no entanto, por precaução, dividi-os assim:

Hackatona SNA 2019

O conjunto de treinamento que nos foi fornecido (fevereiro e 2 semanas de março) foi dividido em 2 partes.
O modelo foi treinado com dados dos últimos N dias. As agregações descritas acima foram construídas com base em todos os dados, incluindo o teste. Ao mesmo tempo, surgiram dados nos quais é possível construir várias codificações da variável alvo. A abordagem mais simples é reutilizar o código que já está criando novos recursos e simplesmente alimentá-lo com dados nos quais ele não será treinado e alvo = 1.

Assim, obtivemos recursos semelhantes:

  • Quantas vezes o userId viu uma postagem no grupo OwnerId;
  • Quantas vezes userId gostou da postagem no grupo OwnerId;
  • A porcentagem de postagens que o userId gostou do OwnerId.

Ou seja, descobriu-se significa codificação de destino em parte do conjunto de dados para várias combinações de características categóricas. Em princípio, o catboost também constrói codificação de destino e desse ponto de vista não há benefício, mas, por exemplo, tornou-se possível contar o número de usuários únicos que curtiram as postagens deste grupo. Ao mesmo tempo, o objetivo principal foi alcançado - meu conjunto de dados foi reduzido várias vezes e foi possível continuar gerando recursos.

Embora o catboost possa construir codificação apenas com base na reação curtida, o feedback tem outras reações: compartilhado de novo, não curtido, não gostei, clicado, ignorado, codificações para as quais podem ser feitas manualmente. Recalculei todos os tipos de agregados e eliminei recursos de baixa importância para não inflar o conjunto de dados.

Naquela época eu estava em primeiro lugar por uma ampla margem. A única coisa confusa foi que os embeddings de imagens quase não mostraram crescimento. A ideia surgiu de dar tudo para o catboost. Agrupamos imagens Kmeans e obtemos um novo recurso categórico imageCat.

Aqui estão algumas classes após filtragem manual e fusão de clusters obtidos do KMeans.

Hackatona SNA 2019

Com base no imageCat, geramos:

  • Novos recursos categóricos:
    • Qual imageCat foi visualizado com mais frequência por userId;
    • Qual imageCat mostra o proprietárioId com mais frequência;
    • Qual imageCat foi apreciado com mais frequência pelo userId;
  • Vários contadores:
    • Quantos imageCat exclusivos analisaram userId;
    • Cerca de 15 recursos semelhantes mais codificação de destino conforme descrito acima.

Textos

Os resultados no concurso de imagens me agradaram e decidi experimentar textos. Não trabalhei muito com textos antes e, tolamente, matei o dia no tf-idf e no svd. Então vi a linha de base com doc2vec, que faz exatamente o que preciso. Depois de ajustar ligeiramente os parâmetros do doc2vec, obtive incorporações de texto.

E então simplesmente reutilizei o código das imagens, substituindo os embeddings de imagens por embeddings de texto. Como resultado, fiquei em 2º lugar no concurso de texto.

Sistema colaborativo

Restava uma competição que ainda não tinha “cutucado” com um bastão e, a julgar pela AUC na tabela de classificação, os resultados desta competição em particular deveriam ter tido o maior impacto na fase offline.
Peguei todos os recursos que estavam nos dados de origem, selecionei os categóricos e calculei os mesmos agregados das imagens, exceto os recursos baseados nas próprias imagens. Apenas colocar isso no catboost me levou ao 2º lugar.

Primeiros passos da otimização catboost

Um primeiro e dois segundos lugares me agradaram, mas houve o entendimento de que não havia feito nada de especial, o que significa que poderia esperar perda de posições.

A tarefa da competição é classificar as postagens dentro do usuário, e todo esse tempo eu estava resolvendo o problema de classificação, ou seja, otimizando a métrica errada.

Deixe-me dar um exemplo simples:

ID do usuário ID do objeto predição verdade fundamental
1 10 0.9 1
1 11 0.8 1
1 12 0.7 1
1 13 0.6 1
1 14 0.5 0
2 15 0.4 0
2 16 0.3 1

Vamos fazer um pequeno rearranjo

ID do usuário ID do objeto predição verdade fundamental
1 10 0.9 1
1 11 0.8 1
1 12 0.7 1
1 13 0.6 0
2 16 0.5 1
2 15 0.4 0
1 14 0.3 1

Obtemos os seguintes resultados:

modelo AUC Usuário1 AUC Usuário2 AUC significa AUC
opção 1 0,8 1,0 0,0 0,5
opção 2 0,7 0,75 1,0 0,875

Como você pode ver, melhorar a métrica geral de AUC não significa melhorar a métrica média de AUC de um usuário.

Impulso de gato sabe como otimizar métricas de classificação da Caixa. Eu li sobre métricas de classificação, histórias de sucesso ao usar catboost e definir YetiRankPairwise para treinar durante a noite. O resultado não foi impressionante. Decidindo que não estava treinado, mudei a função de erro para QueryRMSE, que, a julgar pela documentação do catboost, converge mais rápido. No final consegui os mesmos resultados do treino para classificação, mas os conjuntos desses dois modelos deram uma boa subida, o que me levou ao primeiro lugar nas três competições.

5 minutos antes do encerramento da etapa online da competição “Sistemas Colaborativos”, Sergey Shalnov me levou para o segundo lugar. Seguimos juntos o próximo caminho.

Preparando-se para o estágio off-line

Tínhamos a vitória garantida na etapa online com uma placa de vídeo RTX 2080 TI, mas o prêmio principal de 300 rublos e, muito provavelmente, até o primeiro lugar final nos obrigaram a trabalhar nessas 000 semanas.

Acontece que Sergey também usou catboost. Trocamos ideias e recursos e aprendi sobre relatório de Anna Veronica Dorogush que continha respostas para muitas das minhas perguntas, e mesmo aquelas que eu ainda não tinha respondido naquela época.

A visualização do relatório me levou à ideia de que precisamos retornar todos os parâmetros ao valor padrão, e fazer as configurações com muito cuidado e somente após corrigir um conjunto de recursos. Já um treinamento durou cerca de 15 horas, mas um modelo conseguiu obter uma velocidade melhor que a obtida no conjunto com classificação.

Geração de recursos

Na competição de Sistemas Colaborativos, um grande número de recursos são avaliados como importantes para o modelo. Por exemplo, auditweights_spark_svd - o sinal mais importante, mas não há informações sobre o que significa. Achei que valeria a pena contar os vários agregados com base em características importantes. Por exemplo, auditweights_spark_svd médio por usuário, por grupo, por objeto. O mesmo pode ser calculado utilizando dados sobre os quais nenhum treinamento é realizado e meta = 1, ou seja, média auditweights_spark_svd por usuário por objetos que ele gostou. Sinais importantes além auditweights_spark_svd, foram vários. Aqui estão alguns deles:

  • auditweightsCtrGender
  • pesos auditadosCtrHigh
  • userOwnerCounterCreateLikes

Por exemplo, a média auditweightsCtrGender de acordo com o userId, acabou sendo um recurso importante, assim como o valor médio userOwnerCounterCreateLikes por userId+ownerId. Isso já deve fazer você pensar que precisa entender o significado dos campos.

Também foram características importantes auditweightsLikesCount и auditweightsShowsCount. Dividindo um pelo outro, obteve-se uma característica ainda mais importante.

Vazamentos de dados

A modelagem de competição e produção são tarefas muito diferentes. Ao preparar os dados, é muito difícil levar em conta todos os detalhes e não transmitir algumas informações não triviais sobre a variável alvo no teste. Se estivermos criando uma solução de produção, tentaremos evitar vazamento de dados ao treinar o modelo. Mas se quisermos vencer a competição, então os vazamentos de dados são os melhores recursos.

Depois de estudar os dados, você pode ver que de acordo com os valores objectId auditweightsLikesCount и auditweightsShowsCount mudança, o que significa que a proporção dos valores máximos desses recursos refletirá a pós-conversão muito melhor do que a proporção no momento da exibição.

O primeiro vazamento que encontramos é auditweightsLikesCountMax/auditweightsShowsCountMax.
Mas e se olharmos os dados mais de perto? Vamos classificar por data do show e obter:

ID do objeto ID do usuário auditweightsShowsCount auditweightsLikesCount alvo (é apreciado)
1 1 12 3 provavelmente não
1 2 15 3 talvez sim
1 3 16 4

Foi surpreendente quando encontrei o primeiro exemplo desse tipo e descobri que minha previsão não se concretizou. Mas, tendo em conta que os valores máximos destas características dentro do objeto aumentaram, não tivemos preguiça e decidimos encontrar auditweightsShowsCountNext и auditweightsLikesCountPróximo, ou seja, os valores no próximo momento. Adicionando um recurso
(auditweightsShowsCountNext-auditweightsShowsCount)/(auditweightsLikesCount-auditweightsLikesCountNext) demos um salto brusco rapidamente.
Vazamentos semelhantes poderiam ser usados ​​encontrando os seguintes valores para userOwnerCounterCreateLikes dentro de userId+ownerId e, por exemplo, auditweightsCtrGender dentro de objectId+userGender. Encontramos 6 campos semelhantes com vazamentos e extraímos deles o máximo de informações possível.

Naquela época, havíamos extraído o máximo de informações possível dos recursos colaborativos, mas não voltamos às competições de imagens e textos. Tive uma ótima ideia de verificar: quanto rendem recursos baseados diretamente em imagens ou textos em competições relevantes?

Não houve vazamentos nas competições de imagem e texto, mas naquela época eu já havia retornado os parâmetros padrão do catboost, limpo o código e adicionado alguns recursos. O total foi:

Solução breve
Máximo com imagens 0.6411
Máximo sem imagens 0.6297
Resultado do segundo lugar 0.6295

Solução breve
Máximo com textos 0.666
Máximo sem textos 0.660
Resultado do segundo lugar 0.656

Solução breve
Máximo em colaboração 0.745
Resultado do segundo lugar 0.723

Tornou-se óbvio que dificilmente conseguiríamos extrair muito dos textos e imagens e, depois de tentar algumas das ideias mais interessantes, paramos de trabalhar com elas.

A geração adicional de recursos em sistemas colaborativos não aumentou e começamos a classificação. Na fase online, o conjunto de classificação e classificação deu-me um pequeno aumento, porque descobri que treinei mal a classificação. Nenhuma das funções de erro, incluindo YetiRanlPairwise, produziu algo próximo do resultado que LogLoss produziu (0,745 vs. 0,725). Ainda havia esperança para o QueryCrossEntropy, que não pôde ser lançado.

Estágio off-line

Na fase offline, a estrutura de dados permaneceu a mesma, mas houve pequenas alterações:

  • os identificadores userId, objectId, ownerId foram rerandomizados;
  • vários sinais foram removidos e vários foram renomeados;
  • os dados aumentaram aproximadamente 1,5 vezes.

Além das dificuldades listadas, houve uma grande vantagem: a equipe recebeu um grande servidor com RTX 2080TI. Eu gosto do htop há muito tempo.
Hackatona SNA 2019

Havia apenas uma ideia: simplesmente reproduzir o que já existe. Depois de passar algumas horas configurando o ambiente no servidor, gradualmente começamos a verificar se os resultados eram reproduzíveis. O principal problema que enfrentamos é o aumento do volume de dados. Decidimos reduzir um pouco a carga e definir o parâmetro catboost ctr_complexity=1. Isso diminui um pouco a velocidade, mas meu modelo começou a funcionar, o resultado foi bom - 0,733. Sergey, ao contrário de mim, não dividiu os dados em 2 partes e treinou com todos os dados, embora isso tenha dado o melhor resultado na fase online, na fase offline houve muitas dificuldades. Se pegássemos todos os recursos que geramos e tentássemos colocá-los no catboost, nada funcionaria no estágio online. Sergey fez otimização de tipo, por exemplo, convertendo os tipos float64 em float32. Neste artigo, Você pode encontrar informações sobre otimização de memória em pandas. Como resultado, Sergey treinou na CPU usando todos os dados e obteve cerca de 0,735.

Estes resultados foram suficientes para vencer, mas escondemos a nossa verdadeira velocidade e não podíamos ter a certeza de que outras equipas não estivessem a fazer o mesmo.

Lute até o fim

Ajuste de catboost

Nossa solução foi totalmente reproduzida, adicionamos os recursos de dados de texto e imagens, então só faltou ajustar os parâmetros do catboost. Sergey treinou na CPU com um pequeno número de iterações, e eu treinei naquela com ctr_complexity=1. Faltava um dia, e se você apenas adicionasse iterações ou aumentasse ctr_complexity, pela manhã você poderia obter uma velocidade ainda melhor e caminhar o dia todo.

No estágio offline, as velocidades podem ser facilmente ocultadas simplesmente escolhendo não a melhor solução no site. Esperávamos mudanças drásticas na tabela de classificação nos últimos minutos antes do encerramento das finalizações e decidimos não parar.

No vídeo da Anna aprendi que para melhorar a qualidade do modelo o melhor é selecionar os seguintes parâmetros:

  • taxa de Aprendizagem — O valor padrão é calculado com base no tamanho do conjunto de dados. Aumentar a taxa de aprendizagem requer aumentar o número de iterações.
  • l2_leaf_reg — Coeficiente de regularização, valor padrão 3, de preferência escolha entre 2 e 30. Diminuir o valor leva a um aumento no overfit.
  • temperatura_de_ensaque — adiciona randomização aos pesos dos objetos na amostra. O valor padrão é 1, onde os pesos são extraídos de uma distribuição exponencial. Diminuir o valor leva a um aumento no overfit.
  • força_aleatória — Afeta a escolha de divisões em uma iteração específica. Quanto maior for random_strength, maior será a chance de uma divisão de baixa importância ser selecionada. A cada iteração subsequente, a aleatoriedade diminui. Diminuir o valor leva a um aumento no overfit.

Outros parâmetros têm um efeito muito menor no resultado final, por isso não tentei selecioná-los. Uma iteração de treinamento em meu conjunto de dados de GPU com ctr_complexity=1 levou 20 minutos, e os parâmetros selecionados no conjunto de dados reduzido foram ligeiramente diferentes dos ideais no conjunto de dados completo. No final, fiz cerca de 30 iterações em 10% dos dados e, em seguida, mais 10 iterações em todos os dados. Aconteceu algo assim:

  • taxa de Aprendizagem Aumentei 40% do padrão;
  • l2_leaf_reg deixou tudo igual;
  • temperatura_de_ensaque и força_aleatória reduzido para 0,8.

Podemos concluir que o modelo foi subtreinado com os parâmetros padrão.

Fiquei muito surpreso quando vi o resultado na tabela de classificação:

modelo Modelo 1 Modelo 2 Modelo 3 conjunto
Sem ajuste 0.7403 0.7404 0.7404 0.7407
Com afinação 0.7406 0.7405 0.7406 0.7408

Concluí por mim mesmo que se não for necessária a aplicação rápida do modelo, então é melhor substituir a seleção de parâmetros por um conjunto de vários modelos usando parâmetros não otimizados.

Sergey estava otimizando o tamanho do conjunto de dados para executá-lo na GPU. A opção mais simples é cortar parte dos dados, mas isso pode ser feito de várias maneiras:

  • remover gradualmente os dados mais antigos (início de fevereiro) até que o conjunto de dados comece a caber na memória;
  • remova recursos de menor importância;
  • remova userIds para os quais há apenas uma entrada;
  • deixe apenas os userIds que estão no teste.

E, finalmente, faça um conjunto com todas as opções.

O último conjunto

No final da noite do último dia, havíamos apresentado um conjunto de nossos modelos que rendeu 0,742. Durante a noite lancei meu modelo com ctr_complexity=2 e em vez de 30 minutos ele treinou por 5 horas. Só às 4 da manhã foi contabilizado, e fiz o último conjunto, que deu 0,7433 na tabela de classificação pública.

Devido às diferentes abordagens para resolver o problema, nossas previsões não foram fortemente correlacionadas, o que deu um bom aumento no conjunto. Para obter um bom conjunto, é melhor usar as previsões do modelo bruto predizer(prediction_type='RawFormulaVal') e definir scale_pos_weight=neg_count/pos_count.

Hackatona SNA 2019

No site você pode ver resultados finais na tabela de classificação privada.

Outras soluções

Muitas equipes seguiram os cânones dos algoritmos do sistema de recomendação. Não sendo especialista na área, não posso avaliá-los, mas lembro-me de 2 soluções interessantes.

  • A solução de Nikolay Anokhin. Nikolay, sendo funcionário do Mail.ru, não se candidatou a prêmios, portanto seu objetivo não era atingir a velocidade máxima, mas sim obter uma solução facilmente escalonável.
  • Decisão da equipe vencedora do Prêmio do Júri com base em este artigo do Facebook, permitiu um agrupamento de imagens muito bom sem trabalho manual.

Conclusão

O que mais ficou na minha memória:

  • Se houver recursos categóricos nos dados e você souber como fazer a codificação de destino corretamente, ainda é melhor tentar o catboost.
  • Se você estiver participando de uma competição, não perca tempo selecionando outros parâmetros além de learning_rate e iterações. Uma solução mais rápida é fazer um conjunto de vários modelos.
  • Boostings podem aprender na GPU. Catboost pode aprender muito rapidamente na GPU, mas consome muita memória.
  • Durante o desenvolvimento e teste de ideias, é melhor definir um pequeno rsm~=0.2 (somente CPU) e ctr_complexity=1.
  • Ao contrário de outras equipes, o conjunto dos nossos modelos deu um grande aumento. Apenas trocamos ideias e escrevemos em idiomas diferentes. Tínhamos uma abordagem diferente para dividir os dados e, creio, cada um tinha seus próprios bugs.
  • Não está claro por que a otimização de classificação teve um desempenho pior do que a otimização de classificação.
  • Ganhei alguma experiência trabalhando com textos e uma compreensão de como os sistemas de recomendação são feitos.

Hackatona SNA 2019

Obrigado aos organizadores pelas emoções, conhecimento e prêmios recebidos.

Fonte: habr.com

Adicionar um comentário