SNA Hackathon 2019

En febreiro-marzo de 2019, realizouse un concurso para clasificar o feed de redes sociais SNA Hackathon 2019, na que o noso equipo ocupou o primeiro posto. No artigo falarei da organización da competición, dos métodos que probamos e da configuración de catboost para adestrar en big data.

SNA Hackathon 2019

Hackathon SNA

Esta é a terceira vez que se celebra un hackathon con este nome. Está organizado pola rede social ok.ru, respectivamente, a tarefa e os datos están directamente relacionados con esta rede social.
SNA (análise de redes sociais) neste caso enténdese máis correctamente non como unha análise dun gráfico social, senón como unha análise dunha rede social.

  • En 2014, a tarefa consistía en predecir o número de Gústame que recibiría unha publicación.
  • En 2016 - a tarefa VVZ (quizais estea familiarizado), máis preto da análise do gráfico social.
  • En 2019, clasificación do feed do usuario en función da probabilidade de que lle guste a publicación.

Non podo dicir sobre 2014, pero en 2016 e 2019, ademais das capacidades de análise de datos, tamén se requirían habilidades para traballar con big data. Creo que foi a combinación de aprendizaxe automática e problemas de procesamento de grandes datos o que me atraeu a estas competicións, e a miña experiencia nestas áreas axudoume a gañar.

mlbootcamp

En 2019, o concurso organizouse na plataforma https://mlbootcamp.ru.

O concurso comezou en liña o 7 de febreiro e constaba de 3 tarefas. Calquera pode rexistrarse no sitio, descargar baseline e carga o teu coche durante unhas horas. Ao final da etapa en liña o 15 de marzo, os 15 mellores de cada evento de salto de obstáculos foron invitados á oficina de Mail.ru para a etapa fóra de liña, que tivo lugar do 30 de marzo ao 1 de abril.

Tarefa

Os datos de orixe proporcionan ID de usuario (userId) e ID de publicación (objectId). Se ao usuario se lle mostrou unha publicación, os datos conteñen unha liña que contén userId, objectId, reaccións dos usuarios a esta publicación (retroalimentación) e un conxunto de varias funcións ou ligazóns a imaxes e textos.

ID do usuario objectId ID propietario realimentación imaxes
3555 22 5677 [Gústame, clicou] [hash1]
12842 55 32144 [non gustou] [hash2,hash3]
13145 35 5677 [clic, compartido] [hash2]

O conxunto de datos de proba contén unha estrutura similar, pero falta o campo de comentarios. A tarefa é predicir a presenza da reacción "gústame" no campo de feedback.
O ficheiro de presentación ten a seguinte estrutura:

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

A métrica é a media ROC AUC para os usuarios.

Pódese atopar unha descrición máis detallada dos datos en web do concello. Tamén pode descargar datos alí, incluíndo probas e imaxes.

Fase en liña

Na fase en liña, a tarefa dividiuse en 3 partes

Etapa fóra de liña

Na fase sen conexión, os datos incluían todas as funcións, mentres que os textos e as imaxes eran escasos. Había 1,5 veces máis filas no conxunto de datos, das que xa había moitas.

A solución do problema

Xa que fago CV no traballo, comecei a miña andaina neste concurso coa tarefa "Imaxes". Os datos que se proporcionaron foron userId, objectId, ownerId (o grupo no que se publicou a publicación), marcas de tempo para crear e mostrar a publicación e, por suposto, a imaxe desta publicación.
Despois de xerar varias funcións baseadas en marcas de tempo, a seguinte idea foi tomar a penúltima capa da neurona adestrada previamente en imagenet e enviar estas incorporacións a boost.

SNA Hackathon 2019

Os resultados non foron impresionantes. As incorporacións da neurona imagenet son irrelevantes, pensei, teño que facer o meu propio codificador automático.

SNA Hackathon 2019

Levou moito tempo e o resultado non mellorou.

Xeración de características

Traballar con imaxes leva moito tempo, así que decidín facer algo máis sinxelo.
Como podes ver inmediatamente, hai varias características categóricas no conxunto de datos e, para non molestar demasiado, só tomei catboost. A solución foi excelente, sen ningunha configuración cheguei inmediatamente á primeira liña da táboa de clasificación.

Hai bastantes datos e están dispostos en formato parqué, así que, sen pensalo dúas veces, collín scala e empecei a escribir todo en chispa.

As características máis sinxelas que deron máis crecemento que as incrustacións de imaxes:

  • cantas veces apareceron objectId, userId e ownerId nos datos (deberían correlacionarse coa popularidade);
  • cantas publicacións viu userId desde ownerId (debería correlacionarse co interese do usuario no grupo);
  • cantos ID de usuario únicos viron publicacións de ownerId (reflicte o tamaño da audiencia do grupo).

A partir das marcas de tempo foi posible obter a hora do día na que o usuario vía a fonte (mañá/tarde/noite/noite). Ao combinar estas categorías, podes seguir xerando funcións:

  • cantas veces userId iniciou sesión á noite;
  • en que momento se mostra con máis frecuencia esta publicación (objectId) e así por diante.

Todo isto foi mellorando aos poucos as métricas. Pero o tamaño do conxunto de datos de adestramento é duns 20 millóns de rexistros, polo que engadir funcións retardou moito o adestramento.

Repensei o meu enfoque para usar os datos. Aínda que os datos dependen do tempo, non vin ningunha filtración de información obvia "no futuro", con todo, por se acaso, desglosámolo así:

SNA Hackathon 2019

O conxunto de formación que nos facilitou (febreiro e 2 semanas de marzo) estaba dividido en 2 partes.
O modelo foi adestrado cos datos dos últimos N días. As agregacións descritas anteriormente foron construídas sobre todos os datos, incluída a proba. Ao mesmo tempo, apareceron datos sobre os que é posible construír varias codificacións da variable de destino. O enfoque máis sinxelo é reutilizar o código que xa está a crear novas funcións e simplemente alimentalo con datos sobre os que non se adestrará e apuntará a = 1.

Así, temos características similares:

  • Cantas veces userId viu unha publicación no grupo ownerId;
  • Cantas veces a userId lle gustou a publicación no grupo ownerId;
  • A porcentaxe de publicacións ás que userId gustou de ownerId.

É dicir, resultou codificación de destino media en parte do conxunto de datos para varias combinacións de características categóricas. En principio, catboost tamén constrúe codificación de destino e desde este punto de vista non hai ningún beneficio, pero, por exemplo, fíxose posible contar o número de usuarios únicos aos que lles gustaban as publicacións deste grupo. Ao mesmo tempo, conseguiuse o obxectivo principal: o meu conxunto de datos reduciuse varias veces e foi posible seguir xerando funcións.

Aínda que catboost só pode construír a codificación en función da reacción que lle gusta, os comentarios teñen outras reaccións: compartida, non me gustou, non me gustou, no que se fixo clic, ignorada, codificacións para as que se poden facer manualmente. Recalculei todo tipo de agregados e eliminei funcións con pouca importancia para non inflar o conxunto de datos.

Nese momento estaba no primeiro lugar por unha ampla marxe. O único que resultaba confuso era que as incrustacións de imaxes non mostraban case ningún crecemento. A idea xurdiu para dar todo a catboost. Agrupamos imaxes de Kmeans e obtemos unha nova característica categórica imageCat.

Aquí tes algunhas clases despois do filtrado manual e a fusión dos clústeres obtidos de KMeans.

SNA Hackathon 2019

En base a imageCat xeramos:

  • Novas características categóricas:
    • Cal imageCat foi vista con máis frecuencia por userId;
    • Que imageCat mostra con máis frecuencia ownerId;
    • Que imageCat gustou máis a miúdo por userId;
  • Contadores varios:
    • Cantos únicos imageCat mirou userId;
    • Preto de 15 funcións similares máis codificación de destino como se describe anteriormente.

Textos

Os resultados do concurso de imaxes adecuáronme e decidín probarme cos textos. Non traballei moito cos textos antes e, por tonterías, matei o día en tf-idf e svd. Entón vin baseline con doc2vec, que fai exactamente o que necesito. Despois de axustar lixeiramente os parámetros doc2vec, conseguín incorporacións de texto.

E entón simplemente reutilicei o código para as imaxes, nas que substituín as insercións de imaxes por insercións de texto. Como resultado, obtiven o 2o posto no concurso de textos.

Sistema colaborativo

Quedaba unha competición que eu aínda non tiña "picada" cun pau, e a xulgar pola AUC na táboa de clasificación, os resultados desta competición en particular deberían ter o maior impacto na fase sen conexión.
Tomei todas as características que estaban nos datos fonte, seleccionei as categóricas e calculei os mesmos agregados que para as imaxes, excepto as características baseadas nas propias imaxes. Só poñer isto en catboost levoume ao segundo lugar.

Primeiros pasos da optimización de catboost

Un primeiro e dous segundos agradáronme, pero entendín que non fixera nada especial, o que significa que podía esperar unha perda de posición.

A tarefa do concurso é clasificar as publicacións dentro do usuario, e todo este tempo estiven resolvendo o problema de clasificación, é dicir, optimizando a métrica incorrecta.

Permíteme darche un exemplo sinxelo:

ID do usuario objectId predicción verdade do terreo
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

Imos facer unha pequena reorganización

ID do usuario objectId predicción verdade do terreo
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 Usuario 1 AUC Usuario 2 AUC media AUC
Opción 1 0,8 1,0 0,0 0,5
Opción 2 0,7 0,75 1,0 0,875

Como podes ver, mellorar a métrica AUC global non significa mellorar a métrica AUC media dentro dun usuario.

Catboost sabe como optimizar as métricas de clasificación da caixa. Lin sobre as métricas de clasificación, historias de éxito ao usar catboost e configurar YetiRankPairwise para adestrar durante a noite. O resultado non foi impresionante. Decidindo que estaba pouco adestrado, cambiei a función de erro a QueryRMSE, que, a xulgar pola documentación de catboost, converxe máis rápido. Ao final conseguín os mesmos resultados que no adestramento para a clasificación, pero os conxuntos destes dous modelos deron un bo incremento, o que me levou ao primeiro posto das tres competicións.

5 minutos antes do peche da fase en liña do concurso "Sistemas colaborativos", Sergey Shalnov pasoume ao segundo lugar. Percorremos xuntos o camiño máis adiante.

Preparándose para a fase sen conexión

Garantíanos a vitoria na fase en liña cunha tarxeta de vídeo RTX 2080 TI, pero o premio principal de 300 rublos e, moi probablemente, ata o primeiro posto final obrigáronnos a traballar durante estas 000 semanas.

Como se viu, Sergey tamén usou catboost. Intercambiamos ideas e características, e aprendín reportaxe de Anna Veronica Dorogush que contiña respostas a moitas das miñas preguntas, e mesmo ás que aínda non tiña nese momento.

Ver o informe levoume á idea de que necesitamos devolver todos os parámetros ao valor predeterminado e facer a configuración con moito coidado e só despois de arranxar un conxunto de funcións. Agora un adestramento levaba unhas 15 horas, pero un modelo logrou obter unha velocidade mellor que a obtida no conxunto con clasificación.

Xeración de características

No concurso de Sistemas Colaborativos, un gran número de funcións avalíase como importantes para o modelo. Por exemplo, auditweights_spark_svd - o sinal máis importante, pero non hai información sobre o que significa. Pensei que pagaría a pena contar os distintos agregados en función de características importantes. Por exemplo, auditorweights_spark_svd medio por usuario, por grupo, por obxecto. O mesmo pódese calcular utilizando datos sobre os que non se realiza ningún adestramento e obxectivo = 1, é dicir, media auditweights_spark_svd polo usuario por obxectos que lle gustaban. Ademais, sinais importantes auditweights_spark_svd, foron varios. Aquí están algúns deles:

  • auditorweightsCtrGender
  • auditorweightsCtrHigh
  • userOwnerCounterCreateLikes

Por exemplo, a media auditorweightsCtrGender segundo userId resultou ser unha característica importante, igual que o valor medio userOwnerCounterCreateLikes por userId+ownerId. Isto xa debería facerche pensar que cómpre comprender o significado dos campos.

Tamén foron características importantes auditorweightsLikesCount и auditorweightsShowsCount. Dividindo uns por outros, obtívose unha característica aínda máis importante.

Fugas de datos

A competencia e a modelización da produción son tarefas moi diferentes. Ao preparar os datos, é moi difícil ter en conta todos os detalles e non transmitir algunha información non trivial sobre a variable obxectivo na proba. Se estamos a crear unha solución de produción, tentaremos evitar o uso de fugas de datos ao adestrar o modelo. Pero se queremos gañar a competición, as filtracións de datos son as mellores características.

Despois de estudar os datos, podes ver que segundo os valores objectId auditorweightsLikesCount и auditorweightsShowsCount cambio, o que significa que a relación dos valores máximos destas funcións reflectirá a conversión posterior moito mellor que a relación no momento da visualización.

A primeira fuga que atopamos é auditweightsLikesCountMax/auditweightsShowsCountMax.
Pero e se observamos os datos máis detidamente? Clasifiquemos por data do concerto e obteña:

objectId ID do usuario auditorweightsShowsCount auditorweightsLikesCount obxectivo (gústame)
1 1 12 3 probablemente non
1 2 15 3 quizais si
1 3 16 4

Foi sorprendente cando atopei o primeiro exemplo deste tipo e resultou que a miña predición non se fixo realidade. Pero, tendo en conta o feito de que os valores máximos destas características dentro do obxecto aumentaron, non fomos preguiceiros e decidimos atopar auditweightsShowsCountNext и auditweightsLikesCountNext, é dicir, os valores no momento seguinte. Engadindo unha función
(auditweightsShowsCountNext-auditweightsShowsCount)/(auditweightsLikesCount-auditweightsLikesCountNext) demos un forte salto rapidamente.
Poderíanse usar filtracións similares atopando os seguintes valores para userOwnerCounterCreateLikes dentro userId+ownerId e, por exemplo, auditorweightsCtrGender dentro objectId+userGender. Atopamos 6 campos similares con filtracións e extraemos deles a maior cantidade de información posible.

Nese momento, tiñamos exprimido toda a información posible das funcións colaborativas, pero non volvemos aos concursos de imaxes e textos. Tiven unha gran idea para comprobar: canto aportan as funcións baseadas directamente en imaxes ou textos nos concursos relevantes?

Non houbo filtracións nos concursos de imaxes e textos, pero nese momento devolvín os parámetros de catboost predeterminados, limpei o código e engadira algunhas funcións. O total foi:

decisión en breve
Máximo con imaxes 0.6411
Máximo sen imaxes 0.6297
Resultado segundo lugar 0.6295

decisión en breve
Máximo con textos 0.666
Máximo sen textos 0.660
Resultado segundo lugar 0.656

decisión en breve
Máximo en colaboración 0.745
Resultado segundo lugar 0.723

Fíxose obvio que é improbable que puidésemos sacar moito de textos e imaxes, e despois de probar un par das ideas máis interesantes, deixamos de traballar con elas.

A xeración posterior de funcións en sistemas colaborativos non deu un aumento, e comezamos a clasificar. Na fase en liña, o conxunto de clasificación e clasificación deume un pequeno aumento, xa que resultou porque non entrenei a clasificación. Ningunha das funcións de erro, incluíndo YetiRanlPairwise, produciu un resultado próximo ao que fixo LogLoss (0,745 vs. 0,725). Aínda había esperanza para QueryCrossEntropy, que non se puido lanzar.

Etapa fóra de liña

Na fase sen conexión, a estrutura de datos permaneceu a mesma, pero houbo cambios menores:

  • os identificadores userId, objectId, ownerId foron realeatorizados;
  • quitáronse varios letreiros e renomeáronse varios;
  • os datos aumentaron aproximadamente 1,5 veces.

Ademais das dificultades enumeradas, había unha gran vantaxe: o equipo foi asignado a un gran servidor cun RTX 2080TI. Disfruto de htop durante moito tempo.
SNA Hackathon 2019

Só había unha idea: simplemente reproducir o que xa existe. Despois de pasar un par de horas configurando o ambiente no servidor, pouco a pouco comezamos a comprobar que os resultados eran reproducibles. O principal problema ao que nos enfrontamos é o aumento do volume de datos. Decidimos reducir un pouco a carga e establecer o parámetro catboost ctr_complexity=1. Isto reduce un pouco a velocidade, pero o meu modelo comezou a funcionar, o resultado foi bo: 0,733. Sergey, a diferenza de min, non dividiu os datos en 2 partes e adestrouse en todos os datos, aínda que isto deu o mellor resultado na fase en liña, na fase fóra de liña houbo moitas dificultades. Se tomamos todas as funcións que xeramos e intentamos incorporalas a catboost, nada funcionaría na fase en liña. Sergey fixo unha optimización de tipos, por exemplo, convertendo os tipos float64 en float32. Neste artigo, Podes atopar información sobre a optimización da memoria en pandas. Como resultado, Sergey adestrouse na CPU usando todos os datos e obtivo uns 0,735.

Estes resultados foron suficientes para gañar, pero ocultamos a nosa verdadeira velocidade e non podíamos estar seguros de que outros equipos non estivesen facendo o mesmo.

Loita ata o final

Afinación Catboost

A nosa solución foi totalmente reproducida, engadimos as características de datos de texto e imaxes, polo que só quedaba axustar os parámetros de catboost. Sergey adestrouse na CPU cun pequeno número de iteracións, e eu adestrouse na CPU con ctr_complexity=1. Quedaba un día, e se só engadías iteracións ou aumentabas a ctr_complexity, pola mañá poderías conseguir unha velocidade aínda mellor e camiñar todo o día.

Na fase sen conexión, as velocidades poderían ocultarse facilmente sen escoller a mellor solución no sitio. Agardabamos cambios drásticos na táboa de clasificación nos últimos minutos antes de que se pechasen os envíos e decidimos non parar.

A partir do vídeo de Anna, aprendín que para mellorar a calidade do modelo, é mellor seleccionar os seguintes parámetros:

  • taxa_aprendizaxe — O valor predeterminado calcúlase en función do tamaño do conxunto de datos. Aumentar learning_rate require aumentar o número de iteracións.
  • l2_leaf_reg — Coeficiente de regularización, valor por defecto 3, preferiblemente escoller entre 2 e 30. A diminución do valor leva a un aumento do sobreajuste.
  • temperatura_ensaco — engade aleatorización aos pesos dos obxectos da mostra. O valor predeterminado é 1, onde os pesos se extraen dunha distribución exponencial. A diminución do valor leva a un aumento do sobreajuste.
  • forza_aleatoria — Afecta á elección de divisións nunha iteración específica. Canto maior sexa random_strength, maior será a posibilidade de seleccionar unha división de baixa importancia. En cada iteración posterior, a aleatoriedade diminúe. A diminución do valor leva a un aumento do sobreajuste.

Outros parámetros teñen un efecto moito menor no resultado final, polo que non tentei seleccionalos. Unha iteración de adestramento no meu conxunto de datos GPU con ctr_complexity=1 levou 20 minutos e os parámetros seleccionados no conxunto de datos reducido foron lixeiramente diferentes dos óptimos do conxunto de datos completo. Ao final, fixen unhas 30 iteracións sobre o 10% dos datos, e despois unhas 10 iteracións máis en todos os datos. Resultou algo así:

  • taxa_aprendizaxe Aumentei un 40% desde o valor predeterminado;
  • l2_leaf_reg deixouno igual;
  • temperatura_ensaco и forza_aleatoria reducido a 0,8.

Podemos concluír que o modelo foi pouco adestrado cos parámetros predeterminados.

Sorprendeume moito cando vin o resultado na táboa de clasificación:

Modelo Modelo 1 Modelo 2 Modelo 3 conxunto
Sen afinar 0.7403 0.7404 0.7404 0.7407
Con afinación 0.7406 0.7405 0.7406 0.7408

Concluín por min mesmo que, se non se precisa unha aplicación rápida do modelo, é mellor substituír a selección de parámetros por un conxunto de varios modelos utilizando parámetros non optimizados.

Sergey estaba optimizando o tamaño do conxunto de datos para executalo na GPU. A opción máis sinxela é cortar parte dos datos, pero pódese facer de varias maneiras:

  • eliminar gradualmente os datos máis antigos (a principios de febreiro) ata que o conxunto de datos comece a caber na memoria;
  • eliminar funcións de menor importancia;
  • eliminar userIds para os que só hai unha entrada;
  • deixar só os UserIds que están na proba.

E, finalmente, fai un conxunto de todas as opcións.

O último conxunto

A última hora da noite do último día, tiñamos presentado un conxunto dos nosos modelos que producía 0,742. Durante a noite lancei o meu modelo con ctr_complexity=2 e en lugar de 30 minutos adestrouse durante 5 horas. Só ás 4 da mañá se contou, e fixen o último conxunto, que deu 0,7433 na clasificación pública.

Debido a diferentes enfoques para resolver o problema, as nosas predicións non estaban fortemente correlacionadas, o que deu un bo aumento do conxunto. Para obter un bo conxunto, é mellor usar o modelo bruto predict predict(prediction_type='RawFormulaVal') e establecer scale_pos_weight=neg_count/pos_count.

SNA Hackathon 2019

Na web podes ver resultados finais na táboa de clasificación privada.

Outras solucións

Moitos equipos seguiron os canons dos algoritmos do sistema de recomendación. Eu, ao non ser un experto neste campo, non podo avalialos, pero lembro 2 solucións interesantes.

  • A solución de Nikolay Anokhin. Nikolay, sendo un empregado de Mail.ru, non solicitou premios, polo que o seu obxectivo non era acadar a máxima velocidade, senón obter unha solución facilmente escalable.
  • A decisión do equipo gañador do premio do xurado baseada este artigo de facebook, permitiu unha moi boa agrupación de imaxes sen traballo manual.

Conclusión

O que máis me quedou na memoria:

  • Se hai características categóricas nos datos e sabes como facer a codificación de destino correctamente, aínda é mellor probar catboost.
  • Se estás a participar nunha competición, non debes perder o tempo seleccionando parámetros que non sexan learning_rate e iteracións. Unha solución máis rápida é facer un conxunto de varios modelos.
  • Os aumentos poden aprender na GPU. Catboost pode aprender moi rapidamente na GPU, pero consume moita memoria.
  • Durante o desenvolvemento e a proba de ideas, é mellor establecer un pequeno rsm~=0.2 (só CPU) e ctr_complexity=1.
  • A diferenza doutros equipos, o conxunto dos nosos modelos deu un gran aumento. Só intercambiamos ideas e escribimos en diferentes idiomas. Tiñamos un enfoque diferente para dividir os datos e, creo, cada un tiña os seus propios erros.
  • Non está claro por que a optimización da clasificación funcionou peor que a optimización da clasificación.
  • Adquirí algo de experiencia traballando con textos e entendín como se fan os sistemas de recomendación.

SNA Hackathon 2019

Grazas aos organizadores polas emocións, coñecementos e premios recibidos.

Fonte: www.habr.com

Engadir un comentario