Rekko Challenge 2019: comment c'était



Il n'y a pas si longtemps, un concours de systèmes de recommandation du cinéma en ligne Okko - Rekko Challenge 2019 a eu lieu sur la plateforme Boosters . Pour moi, c'était la première expérience de participer à une compétition avec un classement (auparavant, je n'essayais la force que dans un hackathon). La tâche est intéressante et familière de la pratique, il y a un fonds de prix, ce qui signifie qu'il était logique de participer. En conséquence, j'ai pris la 14e place, pour laquelle les organisateurs ont émis un T-shirt commémoratif. Nice Je vous remercie

Dans cet article, je vais vous plonger brièvement dans la tâche, parler des hypothèses avancées par moi, ainsi que comment faire glisser la concurrence dans les systèmes de recommandation et entrer dans le top 15 sans expérience d'empilement, ce qui sera particulièrement utile pour ceux qui ne participeront qu'à des concours.

Systèmes de recommandation


L'objectif principal des systèmes de recommandation est de donner à l'utilisateur ce qu'il veut acheter (malheureusement, une telle vue hypertrophiée nous est imposée par une application commerciale).
Il existe différentes déclarations de tâches (classement, recherche de tâches similaires, prédiction d'un élément spécifique) et, en conséquence, des moyens de les résoudre. Eh bien, nous aimons tous la variabilité des choix, qui est fournie par un ensemble de plusieurs solutions potentielles à chaque problème. Différentes approches sont bien décrites dans l'article Anatomie des systèmes de recommandation . Bien sûr, personne n'a annulé le théorème de la NFL , ce qui signifie que dans le problème concurrentiel, nous pouvons essayer différents algorithmes.

Énoncé du problème


En savoir plus sur la tâche et les données dans l' article des organisateurs. TL; DR je décrirai ici le minimum nécessaire pour comprendre le contexte.

L'ensemble de données contient un peu plus de dix mille films avec des attributs anonymisés. Les options suivantes sont disponibles sous forme de matrices d'interaction utilisateur-élément:

  • transactions - contient des informations sur les utilisateurs qui achètent du contenu / louent / consultent par abonnement;
  • évaluations - évaluations des films par les utilisateurs;
  • signets - l'événement d'ajout d'un film aux signets.

Toutes les informations sont prises sur une certaine période de temps, qui est présentée dans des unités arbitraires liées au réel.

ts=f(tsreal)


Le contenu avait l'ensemble d'attributs suivant:



Vous pouvez les lire en détail dans l'article des organisateurs, mais je veux faire immédiatement attention à ce qui a attiré mon attention: le paramètre «attributs». Il contenait un sac d'attributs catégoriques avec une cardinalité d'environ 36 000. Il y avait en moyenne 15 valeurs par film. À première vue, seuls les attributs les plus élémentaires qui décrivent le contenu sont chiffrés dans ces valeurs: acteurs, réalisateurs, pays, abonnements ou collections auxquels appartient le film.

Il est nécessaire de prévoir 20 films que les utilisateurs de test regarderont au cours des deux prochains mois. Les utilisateurs de test sont 50 000 des 500 000 utilisateurs. Au classement, ils sont divisés par deux: 25 000 chacun en public / privé.

Métrique


Les organisateurs ont choisi Mean Normalize Average Precision sur 20 éléments (MNAP @ 20) comme métrique. La principale différence avec le MAP habituel est que pour les utilisateurs qui n'ont pas regardé 20 films pendant la période de test, le rationnement ne se produit pas sur k, mais sur la valeur réelle des films regardés.



En savoir plus et voir le code en Cython ici.

Validation


Arriver à la solution du problème. Tout d'abord, il fallait décider ce qui était validé. Étant donné que nous devons prévoir les films à l'avenir, nous ne pouvons pas effectuer une ventilation simple par utilisateur. Étant donné que le temps est anonymisé, j'ai dû au moins commencer à le déchiffrer au moins approximativement. Pour ce faire, j'ai pris plusieurs utilisateurs, fait un planning des transactions et révélé une certaine saisonnalité. On a supposé que c'était quotidien, et connaissant la différence de temps entre les jours, nous pouvons calculer pour quelle période les données ont été téléchargées. Il s'est avéré qu'il s'agissait de transactions pendant 6 mois. Cela a été confirmé plus tard dans la chaîne de télégramme où le concours a été discuté.



Le graphique ci-dessus montre la fréquence horaire des transactions en utilisant des données d'un mois à titre d'exemple. Trois pics importants chaque semaine sont similaires aux vendredis, samedis et dimanches soirs.

Par conséquent, nous avons six mois de visionnage et des films doivent être prévus pour les deux prochains. Nous utiliserons le dernier tiers de la durée d'échantillonnage de la formation comme ensemble de données de validation.



Les quelques soumissions suivantes ont montré que la répartition était bien choisie et que la vitesse de validation locale était en excellente corrélation avec le classement.

Tentative de deanonymisation des données


Pour commencer, j'ai décidé d'essayer de désanonymiser tous les films, pour que:

  • générer un tas de signes par méta-informations du contenu. Au moins, les personnes suivantes me viennent à l'esprit: genres, distribution, entrée dans les abonnements, description textuelle, etc.;
  • jeter dans le four des interactions du côté pour réduire la parcimonie de la matrice. Oui, les règles de concurrence n'interdisent pas l'utilisation de données externes. Bien sûr, il n'y avait aucun espoir de correspondance avec des ensembles de données ouverts, mais personne n'a annulé l'analyse des portails russes.

Il semble que ce soit une motivation logique qui, selon mes attentes, allait devenir une solution phare.

Tout d'abord, j'ai décidé d'analyser le site Web Okko et de retirer tous les films avec leurs propriétés (classement, durée, restrictions d'âge et autres). Eh bien, comment analyser - tout s'est avéré être assez simple, dans ce cas, vous pouvez utiliser l'API:



Après être entré dans le catalogue et avoir choisi un genre ou un abonnement spécifique, il vous suffisait d'entrer dans l'un des éléments. En réponse à la demande ci-dessus, toute la gamme de films du genre / abonnement avec tous les attributs tombe. Très confortable :)

Ainsi, les attributs d'un élément de la structure semblaient
"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 } 


Il reste à parcourir tous les genres, à analyser JSON, puis à autoriser les doublons, puisqu'un film peut appartenir à plusieurs genres / abonnements.

Oui, ici j'ai eu de la chance et j'ai gagné beaucoup de temps. Si ce n'est pas votre cas et que vous devez analyser le contenu html, il existe des articles sur le hub qui peuvent vous aider, par exemple, ici .

«Le truc, c'est le chapeau, pensai-je, nous ne pouvons que le retenir.» «Le fait est le chapeau», ai-je réalisé le lendemain: les données n'étaient pas absolument identiques. À ce sujet ci-dessous.

Premièrement, la taille du catalogue était significativement différente: dans l'ensemble de données - 10 200, collectées sur le site - 8870. Cela découle de l'historicité de l'ensemble de données: il n'a été téléchargé que ce qui se trouve sur le site maintenant, et les données de concurrence pour 2018. Certains films sont devenus indisponibles. Oups

Deuxièmement, parmi les attributs potentiels de la correspondance, l'intuition persistante concernait uniquement les éléments suivants:

feature5 - limite d'âge. C'était assez facile à comprendre. La cardinalité de l'attribut est de 5 valeurs flottantes uniques et «-1». Parmi les données collectées, l'attribut «ageAccessType» a été trouvé avec juste une cardinalité de 5. Le mappage ressemblait à ceci:

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

feature2 - classement de film converti à partir de la recherche de film. Initialement, au stade de l'EDA, l'idée que nous avons affaire à une notation a été présentée par la corrélation du paramètre avec le nombre total de vues. Par la suite, la croyance que cette évaluation provenait d'une recherche de films a confirmé la présence du paramètre «kinopoiskRating» dans les données du site.

Un pas de plus vers le match! Il reste maintenant à trouver un moyen d'inverser la conversion du paramètre feature2 présenté sous une forme anonyme.

Voici à quoi ressemble la distribution des valeurs dans feature2 :



Et donc la distribution des valeurs des paramètres kinopoiskRating :



Lorsque j'ai montré ces images à mon collègue Sasha, il a immédiatement vu qu'il s'agissait d'un degré de trois. Trois mathématiciens ne sont pas respectés, mais le nombre Pi est très pair. En conséquence, cela s'est avéré comme ceci:



Il semble que ce soit tout, mais pas tout à fait. Nous voyons des distributions identiques, mais les valeurs nominales et la quantité ne convergent toujours pas. Si nous connaissions un certain nombre d'exemples de comparaison, il ne resterait plus qu'à approximer une fonction linéaire pour trouver le facteur. Mais nous ne les avions pas.

Soit dit en passant, n'est pas le mot le plus approprié. Nous avons besoin d'une solution avec une erreur presque égale à zéro. La précision des données collectées est de 2 caractères après le séparateur. Si vous considérez qu'il y a beaucoup de films avec une note de 6.xx et qu'il y a des films avec la même note, alors vous devriez vous battre pour la précision ici.

Que pouvez-vous essayer d'autre? Vous pouvez vous fier aux valeurs minimales et maximales et utiliser MinMaxScaler, mais le manque de fiabilité de cette méthode soulève immédiatement des doutes. Permettez-moi de vous rappeler que le nombre de films n'a pas coïncidé au départ, et notre jeu de données est historique, et l'état actuel sur le site. C'est-à-dire il n'y a aucune garantie que les films avec les cotes minimales et maximales dans les deux groupes sont identiques (il s'est avéré qu'ils avaient une limite d'âge différente, et la durée ne correspondait pas au mot «complètement»), ainsi que la fréquence de mise à jour OKKO dans API de classement de recherche de films changeant quotidiennement

Il est donc devenu clair que j'avais besoin de plus d'attributs pour l'appariement.

Quoi d'autre est intéressant?

feature1 est une sorte de date. En théorie, pour les dates, les organisateurs ont promis la préservation de la séparation des états, ce qui impliquait une fonction linéaire. En général, la transformation aurait dû être identique à l'attribut ts pour les matrices d'interaction. Si vous regardez la distribution des films par fonction_1 ...



... puis l'hypothèse de la date de sortie du film est immédiatement balayée. L'intuition suggère que le nombre de films produits par l'industrie devrait augmenter avec le temps. Ceci est confirmé ci-dessous.

Il y a 14 attributs dans les données que nous avons reçues du site. En fait, malheureusement, les valeurs n'étaient contenues que pour les éléments suivants:







Aucun des exemples ci-dessus n'est similaire à feature_1 . Eh bien, il n'y avait plus d'idées de comparaison, et il semble que toute cette agitation avec cette tâche ait été vaine. Bien sûr, j'ai entendu parler de concours, où les gars ont annoté les données manuellement, mais pas quand il s'agit de centaines et de milliers de copies.

Solution


1. Modèles simples

Réalisant que tout n'est pas si simple, j'ai commencé à travailler humblement avec ce qui est. Pour commencer, je voulais commencer par un simple. J'ai essayé plusieurs solutions souvent utilisées dans l'industrie, à savoir: le filtrage collaboratif (dans notre cas basé sur les items) et la factorisation des matrices .

La communauté utilise largement quelques bibliothèques python adaptées à ces tâches: implicite et LightFM . Le premier est capable de factoriser basé sur ALS, ainsi que sur le filtrage collaboratif du plus proche voisin avec plusieurs options pour prétraiter la matrice article-article. Le second a deux facteurs distinctifs:

  • La factorisation est basée sur SGD, ce qui permet d'utiliser des fonctions de perte basées sur l'échantillonnage, y compris WARP .
  • Il utilise une approche hybride, combinant des informations sur les attributs utilisateur et les éléments du modèle de telle sorte que le vecteur latent de l'utilisateur soit la somme des vecteurs latents de ses attributs. Et de même pour les articles. Cette approche devient extrêmement pratique en cas de problème de démarrage à froid pour l'utilisateur / l'article.

Nous n'avions aucun attribut utilisateur (à part la possibilité de les afficher en fonction de l'interaction avec les films), j'ai donc utilisé uniquement les attributs des éléments.

Au total, 6 réglages sont allés énumérer les paramètres. Une combinaison de trois matrices a été utilisée comme matrice d'interaction, où les évaluations ont été converties en binaire. Résultats comparatifs avec les meilleurs hyperparamètres pour chaque paramètre dans le tableau ci-dessous.
ModèleTest MNAP @ 20
SLA implicite0,02646
Cosinus implicite kNN CF0,03170
TFIDF kNN CF implicite0,03113
LightFM (sans caractéristiques de l'article), perte de BPR0,02567
LightFM (sans caractéristiques de l'article), perte WARP0,02632
LightFM avec caractéristiques de l'objet, perte WARP0,02635

Comme vous pouvez le voir, le filtrage collaboratif classique s'est révélé bien meilleur que les autres modèles. Pas parfait, mais beaucoup n'est pas exigé de la ligne de base. Soumettre avec cette configuration a donné 0,03048 sur le classement public. Je ne me souviens pas de la position à ce moment-là, mais au moment de la clôture de la compétition, cette soumission aurait certainement atteint le top 80 et fourni une médaille de bronze.

2. Bonjour Boosting

Quoi de mieux qu'un modèle? Correct: plusieurs modèles.

Par conséquent, l'option suivante était l'ensemble ou, dans le contexte des recommandations, un modèle de classement du deuxième niveau. En guise d'approche, j'ai pris cet article des gars d'Avito. Il semble cuire strictement selon la recette, en remuant et en assaisonnant périodiquement avec les attributs des films. La seule déviation était le nombre de candidats: j'ai pris le top 200 de LightFM, car avec 500 000 utilisateurs, plus simplement ne correspondait pas à la mémoire.

En conséquence, la vitesse que j'ai obtenue lors de la validation était pire que sur un modèle.

Après plusieurs jours d'expérimentation, la réalisation est venue que rien ne fonctionnait et que rien ne fonctionnerait tout seul. Ou je ne sais pas comment le faire cuire (spoiler: la bonne deuxième réponse). Qu'est-ce que je fais mal? Deux raisons me sont venues à l'esprit:

  • D'une part, prendre les 200 premiers du modèle de premier niveau est judicieux du point de vue de la génération d'échantillons «durs négatifs», c'est-à-dire ces films qui sont également pertinents pour l'utilisateur, mais pas regardés par lui. En revanche, certains de ces films peuvent être visionnés pendant la période de test, et nous présentons ces exemples comme négatifs. Ensuite, j'ai décidé de réduire les risques de ce fait, en revérifiant l'hypothèse avec l'expérience suivante: j'ai pris tous les exemples positifs + aléatoires pour l'échantillon d'apprentissage. La vitesse sur l'échantillon d'essai ne s'est pas améliorée. Ici, il est nécessaire de préciser que dans l'échantillonnage du test, il y avait également des prédictions supérieures du modèle de premier niveau, car sur le classement, personne ne me dira tous les exemples positifs.
  • Sur les 10 200 films disponibles dans le catalogue, seuls 8 296 films ont fait des interactions. Près de 2 000 films ont été privés de l'attention des utilisateurs, en partie parce qu'ils n'étaient pas disponibles à l'achat / à la location / dans le cadre d'un abonnement. Les gars du chat ont demandé si des films inaccessibles pourraient devenir disponibles pendant la période de test. La réponse était oui. Il est définitivement impossible de les jeter. Ainsi, j'ai suggéré que près de 2 000 films supplémentaires seront disponibles dans les 2 prochains mois. Sinon, pourquoi les jeter dans l'ensemble de données?

3. Neurones

Dans le paragraphe précédent, la question a été posée: comment travailler avec des films pour lesquels il n'y a aucune interaction? Oui, nous rappelons les caractéristiques des objets dans LightFM, mais comme nous nous en souvenons, elles ne sont pas entrées. Quoi d'autre? Neurones!

L'arsenal open source possède quelques bibliothèques de haut niveau assez populaires pour travailler avec des systèmes de recommandation: Spotlight de Maciej Kula (auteur de LightFM) et TensorRec . Le premier sous le capot PyTorch, le second - Tensorflow.

Spotlight peut prendre en compte des ensembles de données implicites / explicites avec des neurones et des séquences de modèles. Dans le même temps, dans la factorisation «prête à l'emploi», il n'y a aucun moyen d'ajouter des fonctionnalités utilisateur / article, alors laissez-la tomber.

TensorRec, en revanche, ne sait que factoriser et est un cadre intéressant:

  • graphique de représentation - une méthode de transformation (elle peut être définie différente pour l'utilisateur / l'élément) des données d'entrée dans l'incorporation, en fonction des calculs dans le graphique de prédiction. Le choix se compose de couches avec différentes options d'activation. Il est également possible d'utiliser une classe abstraite et de coller dans une transformation personnalisée, consistant en une séquence de couches de kéros.
  • Le graphique de prédiction vous permet de sélectionner l'opération à la fin: votre produit scalaire préféré, la distance euclidienne et le cosinus.
  • perte - il y a aussi beaucoup de choix. Nous avons été satisfaits de l'implémentation de WMRB (essentiellement le même WARP, ne sait apprendre que le batch et le distribué)

Plus important encore, TensorRec est capable de travailler avec des fonctionnalités contextuelles, et en effet l'auteur admet qu'il a été à l'origine inspiré par l'idée de LightFM. Voyons voir. Nous prenons les interactions (uniquement les transactions) et les fonctionnalités des articles.

Nous envoyons à la recherche de différentes configurations et attendons. Par rapport à LightFM, la formation et la validation prennent beaucoup de temps.

De plus, deux inconvénients ont dû être rencontrés:

  1. Depuis la modification du drapeau détaillé, la méthode fit n'a rien changé et aucun rappel n'a été fourni pour vous. J'ai dû écrire une fonction qui a été formée en interne à une époque en utilisant la méthode fit_partial , puis j'ai exécuté la validation pour le train et le test (dans les deux cas, des échantillons ont été utilisés pour accélérer le processus).
  2. En général, l'auteur du framework est un grand gars et utilise tf.SparseTensor partout. Cependant, il vaut la peine de comprendre qu'en tant que prédiction, y compris pour la validation, vous obtenez le résultat en tant que vecteur dense avec la longueur n_items pour chaque utilisateur. Deux conseils en découlent: faire un cycle pour générer des prédictions avec des lots (la méthode de la bibliothèque n'a pas un tel paramètre) avec un filtrage top-k et préparer les bandes avec de la RAM.

En fin de compte, sur la meilleure option de configuration, j'ai réussi à presser 0,02869 sur mon échantillon de test. Il y avait quelque chose de similaire à LB.

Eh bien, qu'est-ce que j'espérais? Que l'ajout de la non-linéarité aux fonctionnalités de l'élément donnera une double augmentation de la métrique? C'est naïf.

4. Beck cette tâche

Alors attendez un instant. Il semble que j'ai de nouveau jonglé avec des neurones. Quelle hypothèse avais-je envie de tester lors de mon entrée en activité? L'hypothèse était la suivante: «Au cours des 2 prochains mois de la sélection différée, près de 2 000 nouveaux films seront vus au classement. Certains d'entre eux auront une large part de vues. »

Vous pouvez donc le vérifier en 2 étapes:

  1. Ce serait bien de voir combien de films nous avons ajoutés dans la période de test honnêtement divisée par nous concernant le train. Si nous ne prenons que les vues, alors les «nouveaux» films ne sont que 240 (!). L'hypothèse a immédiatement tremblé. Il semble que l'achat de nouveau contenu ne puisse pas différer de ce montant d'une période à l'autre.
  2. Nous finissons. Nous avons la possibilité de former le modèle à utiliser uniquement la présentation basée sur les caractéristiques des éléments (dans LightFM, par exemple, cela se fait par défaut, si nous n'avons pas prérempli la matrice d'attributs avec la matrice d'identité). De plus, pour infreness on ne peut se soumettre qu'à ce modèle (!) Nos films inaccessibles et jamais vus. De ces résultats, nous soumettons et obtenons 0,0000136.

Bingo! Cela signifie que vous pouvez arrêter d'extraire la sémantique des attributs de film. Soit dit en passant, plus tard, au DataFest, des gars d'OKKO ont déclaré que la plupart du contenu inaccessible n'était que de vieux films.

Vous devez essayer quelque chose de nouveau et de nouveau - un ancien bien oublié. Dans notre cas, pas complètement oublié, mais ce qui s'est passé il y a quelques jours. Filtrage collaboratif?

5. Réglez la ligne de base

Comment puis-je aider la base de référence des FC?

Idée numéro 1

Sur Internet, j'ai trouvé une présentation sur l'utilisation du test du rapport de vraisemblance pour filtrer les films mineurs.

Ci-dessous, je vais laisser mon code python pour calculer le score LLR, que j'ai dû écrire sur mon genou pour tester cette idée.

Calcul 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 


Par conséquent, la matrice résultante peut être utilisée comme masque pour ne laisser que les interactions les plus importantes et il y a deux options: utiliser le seuil ou laisser les éléments top-k avec la valeur la plus élevée. De la même manière, la combinaison de plusieurs influences sur un achat en une seule vitesse est utilisée pour classer les articles, en d'autres termes, le test montre à quel point il est important, par exemple, d'ajouter des favoris concernant une éventuelle conversion en achat. Cela semble prometteur, mais l'utilisation a montré que le filtrage utilisant le score LLR donne une augmentation très miniature, et la combinaison de plusieurs scores ne fait qu'aggraver le résultat. Apparemment, la méthode n'est pas pour ces données. Parmi les avantages, je peux seulement noter que, tout en trouvant comment mettre en œuvre cette idée, j'ai dû creuser dans l'implicite sous le capot.

Un exemple d'application implicite de cette logique personnalisée sera laissé sous le chat.

Modification de la matrice en implicite
 #   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. 


Idée numéro 2

Une autre idée est venue qui pourrait améliorer les prévisions sur le filtrage collaboratif simple. L'industrie utilise souvent une sorte de fonction d'amortissement pour les anciennes estimations, mais nous avons un cas pas si simple. Probablement, vous devez en quelque sorte prendre en compte 2 cas possibles:

  1. Les utilisateurs qui regardent le service sont pour la plupart nouveaux
  2. "Examinateurs." Autrement dit, ceux qui sont venus relativement récemment et peuvent regarder des films auparavant populaires.

Ainsi, il est possible de diviser automatiquement le composant collaboratif en deux groupes différents. Pour ce faire, j'ai inventé une fonction qui, au lieu de valeurs implicites définies sur «1» ou «0» à l'intersection de l'utilisateur et du film, une valeur montrant à quel point ce film est important dans l'historique de visualisation de l'utilisateur.

confiance(film)=αStartTime+βΔWatchTime


StartTime est la date de sortie du film et ΔWatchTime est la différence entre la date de sortie et la date que l'utilisateur a regardée, et α et β sont des hyperparamètres.

Ici, le premier terme est chargé d'augmenter la vitesse des films récemment sortis, et le second est de prendre en compte la dépendance des utilisateurs aux vieux films. Si un film est sorti il ​​y a longtemps et que l'utilisateur l'a immédiatement acheté, alors aujourd'hui, ce fait ne devrait pas être beaucoup pris en compte. S'il s'agit d'une nouveauté, nous devons la recommander à un plus grand nombre d'utilisateurs qui regardent également de nouveaux articles. Si le film est assez ancien et que l'utilisateur ne l'a regardé que maintenant, cela peut également être important pour ceux comme lui.

Il reste un peu - pour trier les coefficients α et β . Partez pour la nuit et le travail est fait. Il n'y avait aucune hypothèse sur la plage du tout, elle était donc initialement grande et voici les résultats d'une recherche optimale locale.



En utilisant cette idée, un modèle simple sur une matrice a donné une vitesse de 0,03627 sur la validation locale et de 0,03685 sur le LB public, ce qui ressemble immédiatement à un bon coup de pouce par rapport aux résultats précédents. À cette époque, cela m'a amené au top 20.

Idée numéro 3

L'idée finale était que les vieux films que l'utilisateur avait regardés pendant longtemps pouvaient être ignorés du tout. Cette méthode de dépistage de la mucoviscidose est souvent appelée élagage. Passons en revue plusieurs valeurs et testons l'hypothèse:



Il s'avère que pour les utilisateurs ayant une longue histoire, il est logique de ne laisser que les 25 dernières interactions.

Au total, en travaillant avec les données, nous avons atteint une vitesse de 0,040312 sur l'échantillon de test local, et la soumission avec ces paramètres a donné le résultat en 0,03870 et 0,03990 sur les parties publiques / privées, respectivement, et m'a fourni la 14e place et un t-shirt.

Segments reconnus


L'exécution de projets dans le cahier jupyter est un travail ingrat. Vous êtes constamment perdu dans votre code, qui est dispersé sur plusieurs cahiers. Et le suivi des résultats uniquement dans les cellules de sortie est complètement dangereux en termes de reproductibilité. Par conséquent, les data -entistes s'en sortent autant qu'ils le peuvent. Mes collègues et moi avons créé notre cadre de science des données de découpe - Océan. Il s'agit d'un outil de création de structure et de gestion de projet. Vous pouvez lire ses avantages dans notre article . En grande partie grâce à la bonne approche de la séparation des expériences, je n'ai pas perdu la tête et je ne me suis pas trompé lors des tests d'hypothèses.

Le secret du succès (pas le mien)


Comme il est devenu connu immédiatement après l'ouverture de la partie privée du classement, presque tous les gars avec des solutions de pointe ont utilisé des modèles simples au premier niveau et des fonctionnalités supplémentaires au second. Comme je l'ai compris plus tard, ma perforation dans cette expérience était principalement un moyen de diviser l'échantillon lors de la formation du modèle de deuxième niveau. J'ai essayé ceci:



Réalisant que c'était stupide, j'ai commencé à chercher des méthodes assez sophistiquées, comme celle-ci:



Eh bien, j'ai trouvé le bon chemin sur la diapositive de la présentation d'Evgeny Smirnov, qui a pris la 2e place. Il s'agissait d'une scission des utilisateurs dans la partie test du modèle de premier niveau. Cela semble banal, mais cette idée ne m'est pas venue à l'esprit.



Conclusion


Les concours sont des concours. Les modèles lourds donnent vraiment plus de précision, surtout si vous pouvez les faire cuire. Cette expérience sera-t-elle utile? Au lieu d'une réponse, je dirai qu'après la fin du concours, les organisateurs ont jeté un spoiler - un graphique d'importance des fonctionnalités du modèle de produit, selon lequel il est évident qu'ils utilisent la même approche «réussie» en production. Il s'avère qu'ils coulent également dans la prod.

Il s'avère que pour moi l'expérience de participation a été vraiment utile professionnellement. Merci d'avoir lu l'article; Les questions, suggestions et commentaires sont les bienvenus.

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


All Articles