Rekko Challenge 2019: como fue



No hace mucho tiempo, se llevó a cabo un concurso de sistemas de recomendación del cine en línea Okko: Rekko Challenge 2019 en la plataforma Boosters . Para mí fue la primera experiencia de participar en una competencia con una tabla de clasificación (anteriormente probé la fuerza solo en un hackathon). La tarea es interesante y familiar para mí desde la práctica, hay un fondo de premios, lo que significa que tenía sentido participar. Como resultado, ocupé el decimocuarto lugar, por lo que los organizadores emitieron una camiseta conmemorativa. Agradable Gracias

En este artículo, lo sumergiré brevemente en la tarea, hablaré sobre las hipótesis presentadas por mí, así como sobre cómo arrastrar a la competencia en los sistemas de recomendación y llegar al top 15 sin acumular experiencia, lo que será especialmente útil para aquellos que solo van a participar en concursos.

Sistemas de recomendación


El objetivo principal de los sistemas de recomendación es dar al usuario lo que quiere comprar (desafortunadamente, una aplicación comercial nos impone una visión tan hipertrofiada).
Existen diferentes declaraciones de tareas (clasificación, búsqueda de tareas similares, predicción de un elemento específico) y, en consecuencia, formas de resolverlas. Bueno, a todos nos encanta la variabilidad en la elección, que es proporcionada por un conjunto de varias soluciones potenciales para cada problema. Varios enfoques están bien descritos en el artículo Anatomía de los sistemas de recomendación . Por supuesto, nadie canceló el teorema de la NFL , lo que significa que en el problema competitivo podemos probar diferentes algoritmos.

Declaración del problema.


Lea más sobre la tarea y los datos en el artículo de los organizadores. TL; DR aquí describiré el mínimo necesario para comprender el contexto.

El conjunto de datos contiene poco más de diez mil películas con atributos anónimos. Las siguientes opciones están disponibles como matrices de interacción usuario-elemento:

  • transacciones: contiene datos de usuarios que compran contenido / alquilan / visualizan por suscripción;
  • calificaciones: calificaciones de películas de los usuarios;
  • marcadores: el evento de agregar una película a los marcadores.

Toda la información se toma durante un cierto período de tiempo, que se presenta en unidades arbitrarias que están vinculadas a real.

ts=f(tsreal)


El contenido tenía el siguiente conjunto de atributos:



Puede leer sobre ellos en detalle en el artículo de los organizadores, pero quiero prestar atención de inmediato a lo que me llamó la atención: el parámetro "atributos". Contenía una bolsa de atributos categóricos con una cardinalidad de ~ 36 mil. Hubo un promedio de 15 valores por película. A primera vista, solo los atributos más básicos que describen el contenido están encriptados en estos valores: actores, directores, país, suscripciones o colecciones a las que pertenece la película.

Es necesario predecir 20 películas que los usuarios de prueba verán en los próximos dos meses. Los usuarios de prueba son 50 mil de los 500 mil usuarios. En la tabla de clasificación, se dividen a la mitad: 25 mil cada uno en público / privado.

Métrica


Los organizadores eligieron la Media Normalizar Precisión Media en 20 elementos (MNAP @ 20) como una métrica. La principal diferencia con el MAP habitual es que para los usuarios que no han visto 20 películas en el período de prueba, el racionamiento no ocurre en k, sino en el valor real de las películas vistas.



Lea más y vea el código en Cython aquí.

Validación


Llegando a la solución del problema. En primer lugar, era necesario decidir qué fue validado. Como necesitamos predecir películas en el futuro, no podemos hacer un desglose simple por usuarios. Debido al hecho de que el tiempo está anónimo, tuve que al menos comenzar a descifrarlo al menos aproximadamente. Para hacer esto, tomé varios usuarios, hice un cronograma para las transacciones y revelé una cierta estacionalidad. Se supuso que es diario, y conociendo la diferencia horaria entre los días, podemos calcular para qué período se cargaron los datos. Resultó que se trataba de transacciones durante 6 meses. Esto se confirmó más tarde en el canal de telegramas donde se discutió el concurso.



El gráfico anterior muestra la frecuencia horaria de las transacciones utilizando datos de un mes como ejemplo. Tres picos prominentes cada semana son similares a los viernes, sábados y domingos por la noche.

En consecuencia, tenemos seis meses de visualización y es necesario predecir las películas para los próximos dos. Utilizaremos el último tercio del tiempo de muestra de entrenamiento como un conjunto de datos de validación.



Las siguientes presentaciones mostraron que la división se eligió bien y la velocidad de validación local se correlacionó excelentemente con la tabla de clasificación.

Intentar desanonimizar datos


Para empezar, decidí tratar de desanonimizar todas las películas, de modo que:

  • generar un montón de signos por metainformación del contenido. Al menos, me vienen a la mente las siguientes personas: géneros, elenco, entrada en suscripciones, descripción de texto, etc.
  • lanzar en el horno de interacciones desde el lado para reducir la escasez de la matriz. Sí, las reglas de competencia no prohibieron el uso de datos externos. Por supuesto, no había esperanza de una coincidencia con los conjuntos de datos abiertos, pero nadie canceló el análisis de los portales rusos.

Parece ser una motivación lógica, que, según mis expectativas, se convertiría en una solución destacada.

En primer lugar, decidí analizar el sitio web de Okko y sacar todas las películas junto con sus propiedades (calificación, duración, restricciones de edad y otras). Bueno, cómo analizar: todo resultó ser bastante simple, en este caso, podría usar la API:



Una vez que ingresó al catálogo y eligió un género o suscripción específicos, solo tenía que ingresar a cualquiera de los elementos. En respuesta a la solicitud anterior, se cae todo el conjunto de películas en el género / suscripción con todos los atributos. Muy comodo :)

Entonces los atributos de un elemento en la estructura se veían
"element": { "id": "c2f98ef4-2eb5-4bfd-b765-b96589d4c470", "type": "SERIAL", "name": " ", "originalName": " ", "covers": {...}, "basicCovers": {...}, "description": "      ,    ...", "title": null, "worldReleaseDate": 1558731600000, "ageAccessType": "16", "ageAccessDescription": "16+    16 ", "duration": null, "trailers": {...}, "kinopoiskRating": 6, "okkoRating": 4, "imdbRating": null, "alias": "staraja-gvardija", "has3d": false, "hasHd": true, "hasFullHd": true, "hasUltraHd": false, "hasDolby": false, "hasSound51": false, "hasMultiAudio": false, "hasSubtitles": false, "inSubscription": true, "inNovelty": true, "earlyWindow": false, "releaseType": "RELEASE", "playbackStartDate": null, "playbackTimeMark": null, "products": { "items": [ { "type": "PURCHASE", "consumptionMode": "SUBSCRIPTION", "fromConsumptionMode": null, "qualities": [ "Q_FULL_HD" ], "fromQuality": null, "price": { "value": 0, "currencyCode": "RUB" }, "priceCategory": "679", "startDate": 1554670800000, "endDate": null, "description": null, "subscription": { "element": { "id": "bc682dc6-c0f7-498e-9064-7d6cafd8ca66", "type": "SUBSCRIPTION", "alias": "119228" } }, "offer": null, "originalPrice": null }, ... ], "emptyReason": null }, "licenses": null, "assets": {...}, "genres": { "items": [ { "element": { "id": "Detective", "type": "GENRE", "name": "", "alias": "Detective" } }, ... ], "totalSize": 2 }, "countries": { "items": [ { "element": { "id": "3b9706f4-a681-47fb-918e-182ea9dfef0b", "type": "COUNTRY", "name": "", "alias": "russia" } } ], "totalSize": 1 }, "subscriptions": { "items": [ { "element": { "id": "bc682dc6-c0f7-498e-9064-7d6cafd8ca66", "type": "SUBSCRIPTION", "name": "   ", "alias": "119228" } }, ... ], "totalSize": 7 }, "promoText": null, "innerColor": null, "updateRateDescription": null, "contentCountDescription": null, "copyright": null, "subscriptionStartDate": null, "subscriptionEndDate": null, "subscriptionActivateDate": null, "stickerText": null, "fullSeasonPriceText": null, "purchaseDate": null, "expireDate": null, "lastWatchedChildId": null, "bookmarkDate": null, "userRating": null, "consumeDate": null, "lastStartingDate": null, "watchDate": null, "startingDate": null, "earlyWatchDate": null } 


Queda por recorrer todos los géneros, analizar JSON y luego permitir duplicados, ya que una película puede pertenecer a varios géneros / suscripciones.

Sí, aquí tuve suerte y ahorré mucho tiempo. Si este no es su caso, y necesita analizar el contenido html, entonces hay artículos en el centro que pueden ayudar con esto, por ejemplo, aquí .

"La cosa es el sombrero", pensé, "solo podemos contenerlo". "El punto es el sombrero", me di cuenta al día siguiente: los datos no coincidían absolutamente. Sobre esto a continuación.

En primer lugar, el tamaño del catálogo fue significativamente diferente: en el conjunto de datos, 10.200, recopilados del sitio, 8870. Esto se deduce de la historicidad del conjunto de datos: solo se descargó lo que estaba en el sitio y los datos de la competencia para 2018. Algunas de las películas no están disponibles. Ups

En segundo lugar, uno de los atributos potenciales para el partido fue la intuición persistente solo sobre lo siguiente:

característica5 - límite de edad. Fue bastante fácil de entender. La cardinalidad del atributo es 5 valores flotantes únicos y "-1". Entre los datos recopilados, el atributo "ageAccessType" se encontró con solo una cardinalidad de 5. La asignación se veía así:

 catalogue.age_rating = catalogue.age_rating.map({0: 0, 0.4496666915: 6 0.5927161087: 12 0.6547073468: 16 0.6804096966000001: 18}) 

feature2 : clasificación de películas convertidas de la búsqueda de películas. Inicialmente, en la etapa EDA, la idea de que estamos tratando con una calificación fue presentada por la correlación del parámetro con el número total de vistas. Posteriormente, la creencia de que esta calificación era de una búsqueda de películas confirmó la presencia del parámetro "kinopoiskRating" en los datos del sitio.

¡Un paso más cerca del partido! Ahora queda por encontrar una manera de revertir la conversión para el parámetro feature2 presentado en forma anónima.

Así es como se ve la distribución de valores en feature2 :



Y así, la distribución de los valores de los parámetros kinopoiskRating :



Cuando le mostré estas imágenes a mi colega Sasha, inmediatamente vio que esto era un grado de tres. Tres matemáticos no son respetados, pero el número Pi es muy parejo. Como resultado, resultó así:



Parece ser todo, pero no del todo. Vemos distribuciones idénticas, pero los valores nominales y la cantidad aún no convergen. Si supiéramos un cierto número de ejemplos de comparación, solo quedaría aproximar una función lineal para encontrar el factor. Pero no los teníamos.

Aproximarse, por cierto, no es la palabra más adecuada. Necesitamos una solución con un error casi igual a cero. La precisión en los datos recopilados es de 2 caracteres después del separador. Si considera que hay muchas películas con una calificación de 6.xx y hay películas con la misma calificación, entonces debe luchar por la precisión aquí.

¿Qué más puedes probar? Puede confiar en los valores mínimos y máximos y utilizar MinMaxScaler, pero la falta de fiabilidad de este método genera dudas de inmediato. Permítame recordarle que la cantidad de películas no coincidió inicialmente, y nuestro conjunto de datos es histórico y el estado actual del sitio. Es decir no hay garantías de que las películas con las calificaciones mínimas y máximas en ambos grupos sean idénticas (resultó que tenían un límite de edad diferente, y la duración no convergió de la palabra "completamente"), y tampoco se comprende con qué frecuencia OKKO se actualiza en API cambia diariamente la clasificación de búsqueda de películas.

Entonces quedó claro que necesitaba más candidatos de atributos para la coincidencia.

¿Qué más es interesante?

feature1 es algún tipo de fecha. En teoría, para las fechas, los organizadores prometieron preservar la separación de los estados, lo que implicaba una función lineal. En general, la transformación debería haber sido idéntica al atributo ts para las matrices de interacción. Si nos fijamos en la distribución de películas por largometraje_1 ...



... entonces la hipótesis de la fecha de lanzamiento de la película es barrida de inmediato. La intuición sugiere que el número de películas producidas por la industria debería aumentar con el tiempo. Esto se confirma a continuación.

Hay 14 atributos en los datos que recibimos del sitio. De hecho, desafortunadamente, los valores estaban contenidos solo para lo siguiente:







Ninguno de los ejemplos anteriores es similar a feature_1 . Bueno, no había más ideas para comparar, y parece que todo el alboroto con esta tarea fue en vano. Por supuesto, escuché sobre concursos, donde los chicos marcaron los datos manualmente, pero no cuando se trata de cientos y miles de copias.

Solución


1. Modelos simples

Al darme cuenta de que no todo es tan simple, comencé a trabajar humildemente con lo que es. Para empezar, quería comenzar con una simple. Probé varias soluciones de uso frecuente en la industria, a saber: filtrado colaborativo (en nuestro caso basado en elementos) y factorización de matrices .

La comunidad hace un uso extensivo de un par de bibliotecas de Python adecuadas para estas tareas: implícita y LightFM . El primero es capaz de factorizar en base a ALS, así como al Filtrado Colaborativo del Vecino más cercano con varias opciones para preprocesar la matriz ítem-ítem. El segundo tiene dos factores distintivos:

  • La factorización se basa en SGD, lo que hace posible el uso de funciones de pérdida basadas en muestreo, incluido WARP .
  • Utiliza un enfoque híbrido, que combina información sobre los atributos y elementos del usuario en el modelo de tal manera que el vector latente del usuario es la suma de los vectores latentes de sus atributos. Y de manera similar para los artículos. Este enfoque se vuelve extremadamente conveniente cuando hay un problema de arranque en frío para el usuario / elemento.

No teníamos ningún atributo de usuario (aparte de la capacidad de mostrarlos en función de la interacción con las películas), por lo que solo utilicé los atributos de los elementos.

En total, 6 configuraciones fueron para enumerar los parámetros. Se usó una combinación de tres matrices como matriz de interacción, donde las calificaciones se convirtieron a binarias. Resultados comparativos con los mejores hiperparámetros para cada configuración en la tabla a continuación.
ModeloPrueba MNAP @ 20
ALS implícita0,02646
Coseno implícito kNN CF0,03170
TFIDF implícito kNN CF0,03113
LightFM (sin características del artículo), pérdida de BPR0,02567
LightFM (sin características del artículo), pérdida de WARP0,02632
LightFM con características del artículo, pérdida de WARP0,02635

Como puede ver, el filtrado colaborativo clásico ha demostrado ser mucho mejor que otros modelos. No es perfecto, pero no se requiere mucho de la línea base. Enviar con esta configuración dio 0.03048 en la tabla de clasificación pública. No recuerdo la posición en ese momento, pero en el momento del cierre de la competencia, esta presentación definitivamente habría llegado al top 80 y me habría otorgado una medalla de bronce.

2. Hola Impulso

¿Qué podría ser mejor que un modelo? Correcto: varios modelos.

Por lo tanto, la siguiente opción era el conjunto o, en el contexto de las recomendaciones, un modelo de clasificación del segundo nivel. Como enfoque, tomé este artículo de los chicos de Avito. Parece que se cocina estrictamente de acuerdo con la receta, revuelve y sazona periódicamente con los atributos de las películas. La única desviación fue el número de candidatos: tomé el top 200 de LightFM, porque con 500,000 usuarios, más simplemente no cabía en la memoria.

Como resultado, la velocidad obtenida por mí en la validación fue peor que en un modelo.

Después de varios días de experimentación, se dio cuenta de que nada funcionaba y que nada funcionaría por sí solo. O no sé cómo cocinarlo (spoiler: la segunda respuesta correcta). ¿Qué estoy haciendo mal? Se me ocurrieron dos razones:

  • Por un lado, tomar los primeros 200 del modelo de primer nivel es razonable desde el punto de vista de generar muestras "negativas", es decir aquellas películas que también son relevantes para el usuario, pero no vistas por él. Por otro lado, algunas de estas películas se pueden ver durante el período de prueba, y presentamos estos ejemplos como negativos. Luego, decidí reducir los riesgos de este hecho, volviendo a verificar la hipótesis con el siguiente experimento: tomé todos los ejemplos positivos + aleatorios para la muestra de entrenamiento. La velocidad en la muestra de prueba no mejoró. Aquí es necesario aclarar que en el muestreo en la prueba también hubo predicciones superiores del modelo de primer nivel, porque en la tabla de clasificación nadie me dirá todos los ejemplos positivos.
  • De las 10.200 películas disponibles en el catálogo, solo 8.296 películas hicieron alguna interacción. Casi 2.000 películas se vieron privadas de la atención del usuario, en parte porque no estaban disponibles para compra / alquiler / como parte de una suscripción. Los chicos en el chat preguntaron si las películas inaccesibles podrían estar disponibles en el período de prueba. La respuesta fue si. Definitivamente es imposible tirarlos. Por lo tanto, sugerí que casi 2,000 películas más estarán disponibles en los próximos 2 meses. De lo contrario, ¿por qué tirarlos al conjunto de datos?

3. Neuronas

Del párrafo anterior se hizo la pregunta: ¿cómo podemos trabajar con películas para las que no hay interacciones? Sí, recordamos las características del elemento en LightFM, pero como recordamos, no entraron. Que mas Neuronas!

El arsenal de código abierto tiene un par de bibliotecas de alto nivel bastante populares para trabajar con sistemas de recomendación: Spotlight de Maciej Kula (autor de LightFM) y TensorRec . El primero debajo del capó PyTorch, el segundo - Tensorflow.

Spotlight puede factorizar en conjuntos de datos implícitos / explícitos con neuronas y secuencias modelo. Al mismo tiempo, en la factorización "lista para usar" no hay forma de agregar funciones de usuario / elemento, así que suéltelo.

TensorRec, en cambio, solo sabe factorizar y es un marco interesante:

  • gráfico de representación: un método de transformación (se puede establecer de manera diferente para el usuario / elemento) de los datos de entrada en la incrustación, según los cálculos que se realizarán en el gráfico de predicción. La elección consiste en capas con diferentes opciones de activación. También es posible utilizar una clase abstracta y pegarse en una transformación personalizada, que consiste en una secuencia de capas de keras.
  • El gráfico de predicción le permite seleccionar la operación al final: su producto de punto favorito, distancia euclidiana y coseno.
  • pérdida: también hay mucho para elegir. Estamos satisfechos con la implementación de WMRB (esencialmente el mismo WARP, solo sabe cómo aprender por lotes y distribuido)

Lo más importante es que TensorRec puede trabajar con características contextuales y, de hecho, el autor admite que se inspiró originalmente en la idea de LightFM. Bueno, veamos. Tomamos interacciones (solo transacciones) y características del artículo.

Enviamos a la búsqueda de varias configuraciones y esperamos. En comparación con LightFM, la capacitación y la validación llevan mucho tiempo.

Además, hubo un par de inconvenientes que tuvieron que ser encontrados:

  1. Al cambiar el indicador detallado, el método de ajuste no ha cambiado nada y no se han proporcionado devoluciones de llamada. Tuve que escribir una función que entrenó internamente una era usando el método fit_partial , y luego ejecuté la validación para entrenar y probar (en ambos casos, se usaron muestras para acelerar el proceso).
  2. En general, el autor del marco es un gran compañero y utiliza tf.SparseTensor en todas partes. Sin embargo, vale la pena entender que como predicción, incluso para la validación, obtienes el resultado en denso como un vector con la longitud n_items para cada usuario. De esto se derivan dos consejos: haga un ciclo para generar predicciones con lotes (el método de la biblioteca no tiene dicho parámetro) con el filtro top-k y prepare las tiras con RAM.

Finalmente, en la mejor opción de configuración, logré exprimir 0.02869 en mi muestra de prueba. Había algo similar a LB.

Bueno, ¿qué esperaba? ¿Que agregar no linealidad a las características del elemento dará un doble aumento en la métrica? Es ingenuo.

4. Haz esa tarea

Entonces espera un momento. Parece que nuevamente me encontré con malabares neuronas. ¿Qué hipótesis quería probar cuando comencé este negocio? La hipótesis era: "En los próximos 2 meses de la selección retrasada, se verán casi 2.000 películas nuevas en la clasificación. Algunos de ellos tendrán una gran cantidad de puntos de vista ".

Para que pueda verificarlo en 2 pasos:

  1. Sería bueno ver cuántas películas agregamos en el período de prueba honestamente dividido por nosotros con respecto al tren. Si solo tomamos las vistas, entonces las películas "nuevas" son solo 240 (!). La hipótesis se sacudió de inmediato. Parece que la compra de contenido nuevo no puede diferir en esa cantidad de un período a otro.
  2. Terminamos Tenemos la oportunidad de entrenar al modelo para usar solo la presentación basada en las características del elemento (en LightFM, por ejemplo, esto se hace de manera predeterminada, si no rellenamos previamente la matriz de atributos con la matriz de identidad). Además, por infrecuencia solo podemos enviarnos a este modelo (!) Nuestras películas inaccesibles y nunca vistas. A partir de estos resultados, enviamos y obtenemos 0.0000136.

Bingo! Esto significa que puede dejar de exprimir la semántica de los atributos de la película. Por cierto, más tarde, en DataFest, los chicos de OKKO dijeron que la mayor parte del contenido inaccesible era solo algunas películas antiguas.

Necesitas probar algo nuevo y nuevo, viejo y olvidado. En nuestro caso, no está completamente olvidado, pero sucedió hace un par de días. ¿Filtrado colaborativo?

5. Sintonice la línea base

¿Cómo puedo ayudar a la línea de base de la FQ?

Idea número 1

En Internet, encontré una presentación sobre el uso de la prueba de razón de probabilidad para filtrar películas menores.

A continuación, dejaré mi código de Python para calcular la puntuación de LLR, que tuve que escribir en mi rodilla para probar esta idea.

Cálculo LLR
 import numpy as np from scipy.sparse import csr_matrix from tqdm import tqdm class LLR: def __init__(self, interaction_matrix, interaction_matrix_2=None): interactions, lack_of_interactions = self.make_two_tables(interaction_matrix) if interaction_matrix_2 is not None: interactions_2, lack_of_interactions_2 = self.make_two_tables(interaction_matrix_2) else: interactions_2, lack_of_interactions_2 = interactions, lack_of_interactions self.num_items = interaction_matrix.shape[1] self.llr_matrix = np.zeros((self.num_items, self.num_items)) # k11 - item-item co-occurrence self.k_11 = np.dot(interactions, interactions_2.T) # k12 - how many times row elements was bought without column elements self.k_12 = np.dot(interactions, lack_of_interactions_2.T) # k21 - how many times column elements was bought without row elements self.k_21 = np.dot(lack_of_interactions, interactions_2.T) # k22 - how many times elements was not bought together self.k_22 = np.dot(lack_of_interactions, lack_of_interactions_2.T) def make_two_tables(self, interaction_matrix): interactions = interaction_matrix if type(interactions) == csr_matrix: interactions = interactions.todense() interactions = np.array(interactions.astype(bool).T) lack_of_interactions = ~interactions interactions = np.array(interactions, dtype=np.float32) lack_of_interactions = np.array(lack_of_interactions, dtype=np.float32) return interactions, lack_of_interactions def entropy(self, k): N = np.sum(k) return np.nansum((k / N + (k == 0)) * np.log(k / N)) def get_LLR(self, item_1, item_2): k = np.array([[self.k_11[item_1, item_2], self.k_12[item_1, item_2]], [self.k_21[item_1, item_2], self.k_22[item_1, item_2]]]) LLR = 2 * np.sum(k) * (self.entropy(k) - self.entropy(np.sum(k, axis=0)) - self.entropy(np.sum(k, axis=1))) return LLR def compute_llr_matrix(self): for item_1 in range(self.num_items): for item_2 in range(item_1, self.num_items): self.llr_matrix[item_1, item_2] = self.get_LLR(item_1, item_2) if item_1 != item_2: self.llr_matrix[item_2, item_1] = self.llr_matrix[item_1, item_2] def get_top_n(self, n=100, mask=False): filtered_matrix = self.llr_matrix.copy() for i in tqdm(range(filtered_matrix.shape[0])): ind = np.argpartition(filtered_matrix[i], -n)[-n:] filtered_matrix[i][[x for x in range(filtered_matrix.shape[0]) if x not in ind]] = 0 if mask: return filtered_matrix != 0 else: return filtered_matrix 


Como resultado, la matriz resultante se puede usar como una máscara para dejar solo las interacciones más significativas y hay dos opciones: usar umbral o dejar elementos top-k con el valor más alto. De la misma manera, la combinación de varias influencias en una compra en una velocidad se usa para clasificar artículos, en otras palabras, la prueba muestra cuán importante es, por ejemplo, agregar a favoritos con respecto a la posible conversión a una compra. Parece prometedor, pero el uso ha demostrado que filtrar usando el puntaje LLR da un aumento muy pequeño, y combinar varios puntajes solo empeora el resultado. Aparentemente, el método no es para estos datos. De las ventajas, solo puedo notar que, mientras descubría cómo implementar esta idea, tuve que profundizar en lo implícito bajo el capó.

Un ejemplo de la aplicación implícita de esta lógica personalizada se dejará debajo del gato.

Modificación de la matriz en implícito
 #   LLR scores.     n_items * n_items. llr = LLR(train_csr, train_csr) llr.compute_llr_matrix() #     id, ,      llr_based_mask = llr.get_top_n(n=500, mask=True) #     ,  - CosineRecommender. model = CosineRecommender(K=10200) model.fit(train_csr.T) # model.similarity -   co-occurrence (  Cosine - ).        . masked_matrix = np.array(model.similarity.todense()) * llr_based_mask #  fit()  scorer     model.similarity. #     . model.scorer = NearestNeighboursScorer(csr_matrix(masked_matrix)) #      :   recommend   . test_predict = {} for id_ in tqdm(np.unique(test_csr.nonzero()[0])): test_predict[id_] = model.recommend(id_, train_csr, filter_already_liked_items=True, N=20) #   tuples (item_id, score),   id  . test_predict_ids = {k: [x[0] for x in v] for k, v in test_predict.items()} #       / ,     ,   Cython. 


Idea número 2

Surgió otra idea que podría mejorar las predicciones sobre el filtrado colaborativo simple. La industria a menudo usa algún tipo de función de amortiguación para estimaciones antiguas, pero tenemos un caso no tan simple. Probablemente, debe tener en cuenta de alguna manera 2 casos posibles:

  1. Los usuarios que miran el servicio son en su mayoría nuevos
  2. "Examinadores". Es decir, aquellos que vinieron relativamente recientemente y pueden ver películas anteriormente populares.

Por lo tanto, es posible dividir el componente colaborativo en dos grupos diferentes automáticamente. Para hacer esto, inventé una función que en lugar de valores implícitos establecidos en "1" o "0" en la intersección del usuario y la película, un valor que muestra la importancia de esta película en el historial de visualización del usuario.

confianza(película)=αStartTime+βΔWatchTime


donde StartTime es la fecha de lanzamiento de la película, y ΔWatchTime es la diferencia entre la fecha de lanzamiento y la fecha que el usuario vio, y α y β son hiperparámetros.

Aquí, el primer término es responsable de aumentar la velocidad de las películas que se han lanzado recientemente, y el segundo es tener en cuenta la adicción de los usuarios a las películas antiguas. Si una película se lanzó hace mucho tiempo, y el usuario la compró de inmediato, entonces hoy, este hecho no debe tenerse en cuenta mucho. Si se trata de algún tipo de novedad, debemos recomendarlo a un mayor número de usuarios que también ven nuevos artículos. Si la película es bastante antigua, y el usuario la vio solo ahora, entonces esto también puede ser importante para aquellos como él.

Queda un poco, para ordenar los coeficientes α y β . Sal por la noche y el trabajo está hecho. No hubo suposiciones sobre el rango en absoluto, por lo que inicialmente era grande, y a continuación se muestran los resultados de una búsqueda óptima local.



Usando esta idea, un modelo simple en una matriz dio una velocidad de 0.03627 en validación local y 0.03685 en LB público, que inmediatamente parece un buen impulso en comparación con los resultados anteriores. En ese momento, me llevó al top 20.

Idea No. 3 La

idea final era que las películas antiguas que el usuario había visto durante mucho tiempo, pueden ignorarse en absoluto. Este método de detección de la FQ a menudo se llama poda. Repasemos varios valores y analicemos la hipótesis:



resulta que para los usuarios con una larga historia tiene sentido dejar solo las últimas 25 interacciones.

En total, al trabajar con datos, logramos una velocidad de 0.040312 en la muestra de prueba local, y la presentación con estos parámetros dio el resultado en 0.03870 y 0.03990 en las partes públicas / privadas, respectivamente, y me proporcionó el 14to lugar y una camiseta.

Segmentos reconocidos


Ejecutar proyectos en jupyter notebook es un trabajo ingrato. Usted se pierde constantemente en su código, que se encuentra disperso en varios cuadernos. Y rastrear resultados solo en celdas de salida es completamente peligroso en términos de reproducibilidad. Por lo tanto, los expertos en datos pueden hacer frente tanto como pueden. Mis colegas y yo creamos nuestro marco de ciencia de datos para cortar galletas : Ocean. Esta es una herramienta para crear estructura y gestión de proyectos. Puedes leer sobre sus ventajas en nuestro artículo . En gran parte debido al buen enfoque para la separación de experimentos, no perdí la cabeza y no me confundí al probar hipótesis.

El secreto del éxito (no el mío)


Como se supo inmediatamente después de la apertura de la parte privada de la tabla de clasificación, casi todos los muchachos con las mejores soluciones usaron modelos simples en el primer nivel y aumentaron con características adicionales en el segundo. Como entendí más tarde, mi pinchazo en este experimento fue principalmente una forma de dividir la muestra al entrenar el modelo de segundo nivel. Intenté esto: al



darme cuenta de que esto era estúpido, comencé a buscar métodos bastante sofisticados, como este:



Bueno, encontré el camino correcto en la diapositiva de la presentación de Evgeny Smirnov, que ocupó el segundo lugar. Fue una división por usuarios en la parte de prueba del modelo de primer nivel. Parece trivial, pero esta idea no se me ocurrió.



Conclusión


Los concursos son concursos. Los modelos pesados ​​realmente dan más precisión, especialmente si puedes cocinarlos. ¿Esta experiencia será útil? En lugar de una respuesta, diré que después del final de la competencia, los organizadores arrojaron un spoiler, un gráfico de importancia de las características del modelo del producto, según el cual es obvio que usan el mismo enfoque "exitoso" en la producción. Resulta que también están fluyendo en la producción.

Resulta que para mí la experiencia de participación fue realmente útil profesionalmente. Gracias por leer el artículo; Preguntas, sugerencias, comentarios son bienvenidos.

Source: https://habr.com/ru/post/461055/


All Articles