
Há pouco tempo, um concurso de sistemas de recomendação do cinema online Okko -
Rekko Challenge 2019 foi realizado na plataforma
Boosters . Para mim, essa foi a primeira experiência de participar de uma competição com uma tabela de líderes (antes eu tentava força apenas em uma hackathon). A tarefa é interessante e familiar para mim da prática; existe um fundo de prêmios, o que significa que fazia sentido participar. Como resultado, ocupei o 14º lugar, pelo qual os organizadores emitiram uma camiseta comemorativa. Nice Obrigada
Neste artigo, imergirei você brevemente na tarefa, falarei sobre as hipóteses apresentadas por mim, bem como como arrastar a concorrência nos sistemas de recomendação e entrar no top 15 sem acumular experiência, o que será especialmente útil para aqueles que apenas participarão de concursos.
Sistemas Recomendadores
O principal objetivo dos sistemas de recomendação é dar ao usuário o que ele
deseja comprar (infelizmente, essa visão hipertrofiada é imposta a nós por aplicativos comerciais).
Existem diferentes instruções de tarefas (classificação, busca por similares, previsão de um elemento específico) e, portanto, maneiras de resolvê-las. Bem, todos nós amamos a variabilidade na escolha, que é fornecida por um conjunto de várias soluções possíveis para cada problema. Várias abordagens são bem descritas no artigo
Anatomia dos sistemas recomendadores . É claro que ninguém cancelou o
teorema da
NFL , o que significa que no problema competitivo podemos tentar algoritmos diferentes.
Declaração do problema
Leia mais sobre a tarefa e os dados no
artigo pelos organizadores. TL; DR aqui descreverei o mínimo necessário para entender o contexto.
O conjunto de dados contém pouco mais de dez mil filmes com atributos anônimos. As seguintes opções estão disponíveis como matrizes de interação de itens do usuário:
- transações - contém fatos de usuários que compram conteúdo / alugam / visualizam por assinatura;
- classificações - classificações de filmes por usuários;
- favoritos - o evento de adicionar um filme aos favoritos.
Todas as informações são obtidas durante um certo período de tempo, apresentadas em unidades arbitrárias vinculadas ao real.
O conteúdo tinha o seguinte conjunto de atributos:

Você pode ler sobre eles em detalhes no artigo pelos organizadores, mas quero prestar atenção imediatamente ao que chamou minha atenção: o parâmetro "attribute". Continha uma sacola de atributos categóricos com uma cardinalidade de ~ 36 mil. Havia uma média de 15 valores por filme. À primeira vista, apenas os atributos mais básicos que descrevem o conteúdo são criptografados nesses valores: atores, diretores, país, assinaturas ou coleções às quais o filme pertence.
É necessário prever 20 filmes que os usuários de testes assistirão nos próximos dois meses. Os usuários de teste são 50 mil dos 500 mil usuários. Na tabela de classificação, eles são divididos ao meio: 25 mil cada, em público / privado.
Métrica
Os organizadores escolheram Média Normalizar Precisão Média em 20 elementos (MNAP @ 20) como métrica. A principal diferença do MAP habitual é que, para usuários que não assistiram a 20 filmes no período de teste, o racionamento não ocorre em k, mas no valor real dos filmes assistidos.

Leia mais e veja o código no Cython
aqui.Validação
Chegando à solução do problema. Antes de tudo, era necessário decidir o que era validado. Como precisamos prever filmes no futuro, não podemos fazer uma análise simples por usuários. Devido ao fato do tempo ser anonimizado, tive que pelo menos começar a decifrá-lo pelo menos aproximadamente. Para fazer isso, peguei vários usuários, fiz uma programação de transações e revelei uma certa sazonalidade. Supunha-se que fosse diário e, conhecendo a diferença horária entre os dias, podemos calcular por qual período os dados foram carregados. Descobriu-se que eram transações por 6 meses. Isso foi confirmado mais tarde no canal de telegrama onde o concurso foi discutido.

O gráfico acima mostra a frequência horária das transações usando dados de um mês como exemplo. Três picos importantes a cada semana são semelhantes às noites de sexta, sábado e domingo.
Consequentemente, temos seis meses de exibição, e os filmes precisam ser previstos para os próximos dois. Usaremos o último terço do tempo da amostra de treinamento como um conjunto de dados de validação.

As próximas submissões mostraram que a divisão foi bem escolhida e a velocidade de validação local se correlacionou de maneira excelente com a tabela de classificação.
Tentativa de descriptografar dados
Para começar, decidi tentar anonimizar todos os filmes, para que:
- gerar um monte de sinais por meta-informação do conteúdo. Pelo menos, as seguintes pessoas vêm à mente: gêneros, elenco, entrada em assinaturas, descrição de texto, etc;
- jogue no forno de interações laterais para reduzir a escassez da matriz. Sim, as regras da concorrência não proibiram o uso de dados externos. Obviamente, não havia esperança de uma correspondência com conjuntos de dados abertos, mas ninguém cancelou a análise de portais russos.
Parece ser uma motivação lógica, que, de acordo com as minhas expectativas, se tornaria uma solução completa.
Em primeiro lugar, decidi analisar o site da Okko e retirar todos os filmes, juntamente com suas propriedades (classificação, duração, restrições de idade e outros). Bem, como analisar - tudo acabou sendo bastante simples; nesse caso, você pode usar a API:

Depois de entrar no catálogo e escolher um gênero ou assinatura específica, você só precisava entrar em qualquer um dos elementos. Em resposta à solicitação acima, todo o conjunto de filmes do gênero / assinatura com todos os atributos cai. Very comfortable :)
Portanto, os atributos de um elemento na estrutura pareciam"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 }
Resta analisar todos os gêneros, analisar JSON e permitir duplicatas, pois um filme pode pertencer a vários gêneros / assinaturas.
Sim, aqui tive sorte e economizei muito tempo. Se esse não for o seu caso, e você precisar analisar o conteúdo html, há artigos no hub que podem ajudar com isso, por exemplo,
aqui .
"O negócio é o chapéu", pensei, "só podemos segurá-lo." "O ponto é o chapéu", percebi no dia seguinte: os dados não eram absolutamente correspondentes. Sobre isso abaixo.
Em primeiro lugar, o tamanho do catálogo era significativamente diferente: no conjunto de dados - 10.200, coletado no site - 8870. Isso decorre da historicidade do conjunto de dados: foi baixado apenas o que está no site agora e os dados da competição para 2018. Alguns dos filmes ficaram indisponíveis. Opa
Em segundo lugar, um dos atributos em potencial da correspondência era a intuição persistente apenas sobre o seguinte:
feature5 - limite de idade. Foi fácil o suficiente para entender. A cardinalidade do atributo é de 5 valores flutuantes exclusivos e "-1". Entre os dados coletados, o atributo "ageAccessType" foi encontrado com apenas uma cardinalidade de 5. O mapeamento ficou assim:
catalogue.age_rating = catalogue.age_rating.map({0: 0, 0.4496666915: 6 0.5927161087: 12 0.6547073468: 16 0.6804096966000001: 18})
feature2 - classificação de filme convertido a partir da pesquisa de filmes. Inicialmente, no estágio da EDA, a ideia de que estamos lidando com uma classificação foi submetida pela correlação do parâmetro com o número total de visualizações. Posteriormente, a crença de que essa classificação era de uma pesquisa de filme confirmou a presença do parâmetro "kinopoiskRating" nos dados do site.
Um passo mais perto da partida! Agora resta encontrar uma maneira de reverter a conversão para o parâmetro
feature2 apresentado de forma anônima.
Aqui está a aparência da distribuição de valores no
feature2 :

E assim a distribuição dos valores dos parâmetros
kinopoiskRating :

Quando mostrei essas imagens ao meu colega Sasha, ele imediatamente viu que isso era um grau de três. Três matemáticos não são respeitados, mas o número Pi é muito par. Como resultado, ficou assim:

Parece ser tudo, mas não exatamente. Vemos distribuições idênticas, mas os valores nominais e a quantidade ainda não convergem. Se conhecêssemos um certo número de exemplos de comparação, restaria apenas aproximar uma função linear para encontrar o fator. Mas nós não os tínhamos.
Aproximar, a propósito, não é a palavra mais adequada. Precisamos de uma solução com um erro quase igual a zero. A precisão nos dados coletados é de 2 caracteres após o separador. Se você considera que há muitos filmes com uma classificação de 6.xx e há filmes com a mesma classificação, lute pela precisão aqui.
O que mais você pode tentar? Você pode confiar nos valores mínimos e máximos e usar o MinMaxScaler, mas a falta de confiabilidade desse método levanta imediatamente dúvidas. Deixe-me lembrá-lo de que o número de filmes não coincidiu inicialmente, e nosso conjunto de dados é histórico e o estado atual no site. I.e. não há garantias de que os filmes com classificações mínima e máxima em ambos os grupos sejam idênticos (verificou-se que eles tinham uma restrição de idade diferente e a duração não convergiu da palavra "completamente"), assim como não há entendimento de quantas vezes a OKKO é atualizada API que muda diariamente a classificação da pesquisa de filmes.
Então ficou claro que eu precisava de mais candidatos a atributos para correspondência.
O que mais é interessante?feature1 é algum tipo de data. Em tese, por datas, os organizadores prometeram a preservação da separação de estados, o que implicava uma função linear. Em geral, a transformação deveria ter sido idêntica ao atributo
ts para as matrizes de interação. Se você olhar para a distribuição de filmes por
feature_1 ...

... a hipótese da data de lançamento do filme é imediatamente eliminada. A intuição sugere que o número de filmes produzidos pela indústria deve aumentar ao longo do tempo. Isto é confirmado abaixo.
Existem 14 atributos nos dados que recebemos do site. De fato, infelizmente, os valores estavam contidos apenas para o seguinte:



Nenhum dos exemplos acima é semelhante ao
feature_1 . Bem, não havia mais idéias para comparação, e parece que todo o barulho com essa tarefa foi em vão. É claro que ouvi falar de concursos, onde os caras marcaram os dados manualmente, mas não quando se trata de centenas e milhares de cópias.
Solução
1. Modelos simplesPercebendo que nem tudo é tão simples, comecei a trabalhar humildemente com o que é. Para começar, eu queria começar com um simples. Tentei várias soluções frequentemente usadas na indústria, a saber:
filtragem colaborativa (no nosso caso, baseada em itens) e
fatoração de matrizes .
A comunidade faz uso extensivo de algumas bibliotecas python adequadas para essas tarefas:
implícita e
LightFM . O primeiro é capaz de fatorar com base no ALS, bem como na filtragem colaborativa de vizinhos mais próximos, com várias opções para pré-processar a matriz item-item. O segundo possui dois fatores distintos:
- A fatoração é baseada no SGD, o que possibilita o uso de funções de perda baseadas em amostragem, incluindo WARP .
- Ele usa uma abordagem híbrida, combinando informações sobre atributos e itens do usuário no modelo de forma que o vetor latente do usuário seja a soma dos vetores latentes de seus atributos. E da mesma forma para itens. Essa abordagem se torna extremamente conveniente quando há um problema de partida a frio para o usuário / item.
Como não possuímos atributos de usuário (além da capacidade de exibi-los com base na interação com os filmes), usei apenas os atributos dos itens.
No total, 6 configurações foram para enumerar os parâmetros. Uma combinação de três matrizes foi usada como matriz de interação, onde as classificações foram convertidas em binárias. Resultados comparativos com os melhores hiperparâmetros para cada configuração na tabela abaixo.
Como você pode ver, a filtragem colaborativa clássica provou ser muito melhor do que outros modelos. Não é perfeito, mas não é necessário muito da linha de base. Enviar com esta configuração forneceu 0,03048 no ranking público. Não me lembro da posição naquele momento, mas no momento do encerramento da competição, essa finalização definitivamente alcançaria o top 80 e proporcionou uma medalha de bronze.
2. Olá BoostingO que poderia ser melhor que um modelo? Correto: vários modelos.
Portanto, a próxima opção foi o conjunto ou, no contexto das recomendações, um modelo de classificação do segundo nível. Como abordagem, peguei
este artigo dos caras da Avito. Parece estar cozinhando estritamente de acordo com a receita, mexendo e temperando periodicamente com os atributos dos filmes. O único desvio foi o número de candidatos: peguei os 200 melhores no LightFM, porque com 500.000 usuários, mais simplesmente não coube na memória.
Como resultado, a velocidade obtida por mim na validação foi pior do que em um modelo.
Após vários dias de experimentação, percebeu-se que nada estava funcionando e que nada funcionaria por si só. Ou não sei como cozinhar (spoiler: a segunda resposta correta). O que estou fazendo de errado? Duas razões vieram à mente:
- Por um lado, tirar as 200 primeiras do modelo de primeiro nível é sensato do ponto de vista da geração de amostras "negativas", ou seja, aqueles filmes que também são relevantes para o usuário, mas não são assistidos por ele. Por outro lado, alguns desses filmes podem ser assistidos durante o período de teste, e apresentamos esses exemplos como negativos. Em seguida, decidi reduzir os riscos desse fato, reconsiderando a hipótese com o seguinte experimento: tomei todos os exemplos positivos + aleatórios para a amostra de treinamento. A velocidade na amostra de teste não melhorou. Aqui é necessário esclarecer que, na amostra do teste, também havia previsões de topo do modelo de primeiro nível, porque na tabela de líderes ninguém me contou todos os exemplos positivos.
- Dos 10.200 filmes disponíveis no catálogo, apenas 8.296 filmes fizeram interações. Quase 2.000 filmes foram privados da atenção do usuário, em parte porque não estavam disponíveis para compra / aluguel / como parte de uma assinatura. Os caras do chat perguntaram se filmes inacessíveis poderiam ficar disponíveis no período de teste. A resposta foi sim. É definitivamente impossível jogá-los fora. Assim, sugeri que quase 2.000 filmes adicionais estarão disponíveis nos próximos 2 meses. Caso contrário, por que jogá-los no conjunto de dados?
3. NeurôniosNo parágrafo anterior, a pergunta foi feita: como podemos trabalhar com filmes para os quais não há interações? Sim, recordamos os recursos do item no LightFM, mas, como lembramos, eles não entraram. O que mais? Neurônios!
O arsenal de código aberto possui algumas bibliotecas de alto nível bastante populares para trabalhar com sistemas de recomendação:
Spotlight de Maciej Kula (autor do LightFM) e
TensorRec . O primeiro sob o capô PyTorch, o segundo - Tensorflow.
O Spotlight pode levar em consideração conjuntos de dados implícitos / explícitos com neurônios e sequências de modelos. Ao mesmo tempo, na fatoração “pronta para uso”, não há como adicionar recursos de usuário / item;
O TensorRec, por outro lado, sabe apenas como fatorar e é uma estrutura interessante:
- gráfico de representação - um método de transformação (pode ser definido de forma diferente para o usuário / item) dos dados de entrada na incorporação, com base nos quais os cálculos no gráfico de previsão ocorrerão. A escolha consiste em camadas com diferentes opções de ativação. Também é possível usar uma classe abstrata e manter uma transformação personalizada, consistindo em uma sequência de camadas keras.
- O gráfico de previsão permite selecionar a operação no final: seu produto de ponto favorito, distância euclidiana e cosseno.
- perda - também há muito por onde escolher. Ficamos satisfeitos com a implementação do WMRB (essencialmente o mesmo WARP, só sabe aprender em lote e distribuído)
Mais importante ainda, o TensorRec é capaz de trabalhar com recursos contextuais e, de fato, o autor admite que ele foi originalmente inspirado pela idéia do LightFM. Bem, vamos ver. Tomamos interações (somente transações) e recursos de itens.
Enviamos para pesquisa de várias configurações e aguardamos. Comparado ao LightFM, o treinamento e a validação levam muito tempo.
Além disso, havia alguns inconvenientes que precisavam ser encontrados:
- Ao alterar o sinalizador detalhado, o método de ajuste não mudou nada e nenhum retorno de chamada foi fornecido para você. Eu tive que escrever uma função que treinou internamente uma época usando o método fit_partial e, em seguida, executei a validação para treinamento e teste (em ambos os casos, as amostras foram usadas para acelerar o processo).
- Em geral, o autor do framework é um ótimo companheiro e utiliza o tf.SparseTensor em qualquer lugar. No entanto, vale a pena entender que, como previsão, inclusive para validação, o resultado é denso como um vetor com o comprimento n_items para cada usuário. Duas dicas seguem a seguir: faça um ciclo para gerar previsões com lotes (o método da biblioteca não possui esse parâmetro) com filtragem top-k e prepare as tiras com RAM.
Por fim, na melhor opção de configuração, consegui extrair 0,02869 na minha amostra de teste. Havia algo semelhante ao LB.
Bem, o que eu esperava? Que a adição de não linearidade aos recursos do item dará um duplo aumento na métrica? É ingênuo.
4. Beck essa tarefaEntão espere um momento. Parece que eu novamente encontrei neurônios de malabarismo. Que hipótese eu queria testar quando assumi esse negócio? A hipótese foi: “Nos próximos 2 meses da seleção atrasada, quase 2.000 novos filmes serão vistos na tabela de classificação. Alguns deles terão uma grande quantidade de opiniões. ”
Então você pode verificá-lo em duas etapas:
- Seria bom ver quantos filmes nós adicionamos no período de teste honestamente dividido por nós em relação ao trem. Se considerarmos apenas as opiniões, os filmes “novos” são apenas 240 (!). A hipótese imediatamente tremeu. Parece que a compra de novo conteúdo não pode diferir nessa quantidade de período para período.
- Nós terminamos. Temos a oportunidade de treinar o modelo para usar apenas a apresentação com base nos recursos do item (no LightFM, por exemplo, isso é feito por padrão, se não preenchermos previamente a matriz de atributos com a matriz de identidade). Além disso, por infidelidade, podemos nos submeter apenas a este modelo (!) Nossos filmes inacessíveis e nunca vistos. A partir desses resultados, enviamos e obtemos 0,0000136.
Bingo! Isso significa que você pode parar de extrair a semântica dos atributos do filme. A propósito, mais tarde, no DataFest, os caras da OKKO disseram que a maior parte do conteúdo inacessível eram apenas alguns filmes antigos.
Você precisa tentar algo novo e novo - velho e esquecido. No nosso caso, não completamente esquecido, mas o que aconteceu alguns dias atrás. Filtragem colaborativa?
5. Ajuste a linha de baseComo posso ajudar a linha de base do CF?
Idéia número 1Na Internet, encontrei uma apresentação sobre o uso do teste da razão de verossimilhança para filtrar filmes menores.
Abaixo deixarei meu código python para calcular a pontuação do LLR, que tive que escrever de joelhos para testar essa ideia.
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))
Como resultado, a matriz resultante pode ser usada como uma máscara para deixar apenas as interações mais significativas e existem duas opções: use threshold ou deixe os elementos top-k com o valor mais alto. Da mesma forma, a combinação de várias influências em uma compra em uma velocidade é usada para classificar itens; em outras palavras, o teste mostra o quanto é importante, por exemplo, adicionar aos favoritos a possibilidade de conversão em uma compra. Parece promissor, mas o uso mostrou que a filtragem usando a pontuação LLR gera um aumento muito pequeno, e a combinação de várias pontuações apenas piora o resultado. Aparentemente, o método não é para esses dados. Das vantagens, só posso notar que, ao descobrir como implementar essa idéia, tive que cavar o implícito sob o capô.
Um exemplo de aplicação implícita dessa lógica personalizada será deixado sob o gato.
Modificação da matriz em implícita Idéia número 2Outra idéia surgiu que poderia melhorar as previsões sobre a filtragem colaborativa simples. O setor costuma usar algum tipo de função de amortecimento para estimativas antigas, mas temos um caso não tão simples. Provavelmente, você precisa levar em consideração 2 casos possíveis:
- Os usuários que assistem ao serviço são principalmente novos
- "Examinadores". Ou seja, aqueles que vieram relativamente recentemente e podem assistir a filmes populares anteriormente.
Assim, é possível dividir o componente colaborativo em dois grupos diferentes automaticamente. Para fazer isso, inventei uma função que, em vez de valores implícitos, definidos como "1" ou "0" na interseção do usuário e do filme, um valor que mostra a importância desse filme no histórico de exibição do usuário.
onde
StartTime é a data de lançamento do filme e
ΔWatchTime é a diferença entre a data de lançamento e a data em que o usuário assistiu, e
α e
β são hiperparâmetros.
Aqui, o primeiro termo é responsável por aumentar a velocidade dos filmes lançados recentemente, e o segundo é levar em consideração o vício dos usuários em filmes antigos. Se um filme foi lançado há muito tempo e o usuário o comprou imediatamente, hoje, esse fato não deve ser levado em consideração muito. Se esse é um tipo de novidade, recomendamos que seja recomendado a um número maior de usuários que também assistem a novos itens. Se o filme é bastante antigo e o usuário o assistiu apenas agora, isso também pode ser importante para pessoas como ele.
Ainda existe um pouco - para separar os coeficientes
α e
β . Sair para a noite, e o trabalho está feito. Como não havia nenhuma suposição sobre o intervalo, ele foi inicialmente grande e abaixo estão os resultados de uma pesquisa ideal local.

Usando essa idéia, um modelo simples em uma matriz deu uma velocidade de 0,03627 na validação local e 0,03685 no LB público, o que imediatamente parece um bom impulso em comparação com os resultados anteriores. Naquele momento, ele me levou ao top 20.
№3, , , . CF . :

, 25 .
, , 0.040312 , 0.03870 0.03990 public/private 14- .
Acknowledgments
jupyter notebook — . , . output . .
cookiecutter-data-science — Ocean. .
. .
( )
private , . , . :

, , , :

, 2 . . , .

Conclusão
— . , . ? , — feature importance , , «» . , .
, . ; , , .