mnemlaghi commited on
Commit
b3f3132
1 Parent(s): 684d8ce

last files for commit

Browse files
Files changed (3) hide show
  1. app.py +82 -0
  2. ranker.py +122 -0
  3. requirements.txt +4 -0
app.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from ranker import SparseTfIdfRanker, BertRanker, SparseDenseMoviesRanker
3
+ import pandas as pd
4
+ import torch
5
+ from utils import get_image_from_url
6
+
7
+ st.set_page_config(layout="wide")
8
+
9
+ @st.cache(allow_output_mutation = True)
10
+ def get_ranker(modele):
11
+ df = pd.read_pickle('films.pkl')
12
+
13
+ if modele=="sparse-dense":
14
+ modelpath = "sentence-transformers/multi-qa-MiniLM-L6-cos-v1"
15
+ bert_index = torch.load('multi-qa-MiniLM-L6-cos-v1_embeddings.pth')
16
+ sparse_index = torch.load('sparsefake.pth')
17
+ ranker = SparseDenseMoviesRanker(df, modelpath=modelpath, bert_index = bert_index, sparse_index = sparse_index, vectorizer_path='my_v.pkl')
18
+ elif modele=="sparse":
19
+ sparse_index = torch.load('sparsefake.pth')
20
+ ranker = SparseTfIdfRanker(df, vectorizer_path = 'my_v.pkl', index_matrix = sparse_index)
21
+ elif modele=="dense":
22
+ modelpath = "sentence-transformers/multi-qa-MiniLM-L6-cos-v1"
23
+ bert_index = torch.load('multi-qa-MiniLM-L6-cos-v1_embeddings.pth')
24
+ ranker = BertRanker(df, index_matrix = bert_index, modelpath=modelpath)
25
+ else:
26
+ return NotImplementedError
27
+ return ranker
28
+
29
+
30
+ def main():
31
+
32
+ st.title("Canap' _is all you need_ 🎥 ")
33
+ st.header("Prototype simple de recommandation de contenus basée sur des description de films")
34
+ st.write("Jeunes parents, je m'adresse en particulier à vous. Pendant que vos jeunes chérubins 👶 tentent de trouver le sommeil, la plupart de vos soirées se terminent dans les tréfonds d'un canapé, emportés par algorithme de recommandation de contenu qui se base sur moult critères. D'un autre côté, les choix proposés par les sites francophones vous font rentrer dans un vortex sans fin qui allégera vos espoirs d'une soirée cinéma réussie.")
35
+ st.write("Je vous propose, avec cette maquette, de comprendre un tout petit peu ce qui se passe derrière le capot.")
36
+ st.write("⚠️ Il s'agit bien évidemment d'un prototype; je rejette toute responsabilité quant au potentiel ratage d'un dîner pour cause de promesse cinématographique non tenue ⚠️ . Après tout, je ne suis qu'un ingénieur. Si j'étais critique ciné, je ne connaîtrais probablement pas [Hugging Face](https://huggingface.co/) 🤗, ce qui serait bien dommage - et je connais déjà Martin Scorsese, ce qui suffit amplement à me pavaner en soirée (même si, en tant que jeune parent, je n'ai plus de soirée, souvenez-vous).")
37
+ st.subheader("Principe et objectifs")
38
+ st.write("On propose ici de comparer trois modèles de recommandation sur une base de films.")
39
+ st.write("Plus sérieusement, l'objectif est ici triple")
40
+ st.write("* Montrer que l'on peut rapidement prototyper un modèle de recommandation sur base de contenus")
41
+ st.write("* Prouver qu'il est également possible de le faire en langue française. Et, plus généralement, encourager les avancées remarquables du NLP francophone")
42
+ st.write("* Avec la mise à disposition des modèles pré-entraînés, la période actuelle de l'intelligence artificielle est similaire à l'avènement de l'open source. Il s'agit juste de dépasser rapidement la phase de prototypage !")
43
+ st.write("Pour les curieux, je suis en train d'élaborer [un post explicatif, plus technique](https://mnemlaghi.github.io/simsearch)")
44
+
45
+ st.subheader("La donnée")
46
+ st.write("La donnée indexée est issue de la collecte de descriptifs d'environ 8000 films. Les métadonnées telles que l'image du film sont récupérées en temps réel lors de la requête")
47
+
48
+ st.subheader("Expérimentons !")
49
+ modele = st.selectbox("Choisissez votre modèle de recommandation", ['sparse-dense', 'sparse', 'dense'])
50
+ ranker = get_ranker(modele)
51
+ query = st.text_input("Quelle histoire voulez-vous regarder ce soir ?", "Une histoire de pirates à la recherche d'un trésor")
52
+ topn = st.number_input("Combien de films souhaitez-vous afficher ?", min_value = 1, max_value = 20, value = 5)
53
+
54
+
55
+ with st.spinner("Canap' vous recherche des films appropriés sur une base de plus de 8000 films..."):
56
+ if modele=='sparse-dense':
57
+ user_firstranking = st.number_input("Filtre isssu du premier ranking (vous pouvez le laisser par défaut à 100) ?", min_value = 10, max_value = 1000, value=1000)
58
+ df = ranker.run_query(query, topn, first_ranking=user_firstranking)
59
+ else:
60
+ df = ranker.run_query(query, topn)
61
+
62
+ score_key = 'tfidf-score' if modele=='sparse' else 'bert-score'
63
+ if st.button("Qu'est-ce qu'on regarde, ce soir ?"):
64
+ for i,v in df.iterrows():
65
+ url, desc, score, title = v['url'], v['desc'], v[score_key], v['title']
66
+ st.header(title)
67
+ col1, col2, col3 = st.columns([3,5,2])
68
+ with col1:
69
+ st.image(get_image_from_url(url))
70
+ st.write(f"[Lien]({url})")
71
+
72
+ with col2:
73
+ desc_low = desc[:50]
74
+ with st.expander(desc_low+'...'):
75
+ st.write(desc)
76
+
77
+ with col3:
78
+ percent = str(round(score*100, 1)) +"%"
79
+ st.metric('similarité', percent)
80
+
81
+ if __name__=='__main__':
82
+ main()
ranker.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import AutoModel, AutoTokenizer
2
+ import pandas as pd
3
+ import torch
4
+ from torch.utils.data import Dataset
5
+ import logging
6
+ from tqdm import tqdm
7
+ from torch.utils.data import DataLoader
8
+ import torch.nn.functional as F
9
+ import pickle
10
+ import string
11
+ from abc import abstractmethod
12
+ import json
13
+
14
+
15
+ class AbstractMoviesRanker:
16
+ """Abstract class for ranking items"""
17
+ def __init__(self, df, index_matrix, score_name = "score"):
18
+ self.df = df
19
+ self.ids = self.df.index.values
20
+ self.index_matrix = index_matrix
21
+ self.score_name = score_name
22
+
23
+ @abstractmethod
24
+ def encode_query(self, query):
25
+ pass
26
+
27
+ def get_scores(self, encoded_query):
28
+ return torch.mm(encoded_query, self.index_matrix.transpose(0,1))[0].tolist()
29
+
30
+ def get_top_ids(self, scores, topn=6):
31
+ ids_scores_pairs = list(zip(self.ids.tolist(), scores))
32
+ ids_scores_pairs = sorted(ids_scores_pairs, key = lambda x:x[1], reverse = True)
33
+ sorted_ids = [v[0] for v in ids_scores_pairs]
34
+ sorted_scores = [v[1] for v in ids_scores_pairs]
35
+ sorted_df = self.df.loc[sorted_ids[:topn], :]
36
+ sorted_df.loc[:,self.score_name] = sorted_scores[:topn]
37
+ return sorted_df
38
+
39
+ def run_query(self, query, topn=6):
40
+ encoded_query = self.encode_query(query)
41
+ scores = self.get_scores(encoded_query)
42
+ return self.get_top_ids(scores, topn)
43
+
44
+ depunctuate = staticmethod(lambda x: x.translate(str.maketrans('','',string.punctuation)))
45
+
46
+ class SparseTfIdfRanker(AbstractMoviesRanker):
47
+ """Sparse Ranking via TF iDF"""
48
+ def __init__(self, df, index_matrix, vectorizer_path):
49
+ super(SparseTfIdfRanker, self).__init__(df, index_matrix, score_name = 'tfidf-score')
50
+ self.vectorizer = pickle.load(open(vectorizer_path, 'rb'))
51
+ self.index_matrix = self.index_matrix.to_dense() ##For dot products
52
+
53
+ def encode_query(self, query):
54
+ encoded_query = torch.tensor(self.vectorizer.transform([self.depunctuate(query)]).todense(), dtype = torch.float32)
55
+ return F.normalize(encoded_query, p=2)
56
+
57
+
58
+ class BertRanker(AbstractMoviesRanker):
59
+ """Dense Ranking with embedding matrix"""
60
+ def __init__(self, df, index_matrix, modelpath):
61
+ super(BertRanker, self).__init__(df, index_matrix, score_name = "bert-score")
62
+ self.tokenizer = AutoTokenizer.from_pretrained(modelpath)
63
+ self.model = AutoModel.from_pretrained(modelpath)
64
+
65
+ def encode_query(self, query):
66
+ tok_q = self.tokenizer(query, return_tensors="pt", padding="max_length", max_length = 128, truncation=True)
67
+ o = self.model(**tok_q)
68
+ encoded_query = self.mean_pooling(o, tok_q['attention_mask'])
69
+ return F.normalize(encoded_query, p=2)
70
+
71
+ @staticmethod
72
+ def mean_pooling(model_output, attention_mask):
73
+ token_embeddings = model_output.last_hidden_state #First element of model_output contains all token embeddings
74
+ input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
75
+ return torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(input_mask_expanded.sum(1), min=1e-9)
76
+
77
+
78
+
79
+ class SparseDenseMoviesRanker():
80
+ """Sparse Ranking via TF iDF, filtering a first rank, then dense ranking on these items"""
81
+ def __init__(self, df, modelpath, bert_index, sparse_index, vectorizer_path):
82
+ self.df =df
83
+ self.ids = self.df.index.values
84
+ self.tfidf_engine = SparseTfIdfRanker(df, sparse_index, vectorizer_path)
85
+ self.modelpath = modelpath
86
+ self.bert_index = bert_index
87
+
88
+ def run_query(self, query, topn=6, first_ranking=1000):
89
+ tfidf_sorted_frame = self.tfidf_engine.run_query(query, topn=first_ranking)
90
+ firstranking_index = self.bert_index[tfidf_sorted_frame.index.values]
91
+ self.bert_engine = BertRanker(tfidf_sorted_frame, firstranking_index, self.modelpath)
92
+ bert_sorted_frame = self.bert_engine.run_query(query, topn=topn)
93
+ return bert_sorted_frame
94
+
95
+ @classmethod
96
+ def from_json_config(cls, jsonfile):
97
+ with open(jsonfile) as fp:
98
+ conf = json.loads(fp.read())
99
+
100
+ ##Load data for ranking
101
+ df = pd.read_pickle(conf['dataframe'])
102
+
103
+ ##Load indices, e.g. embeddings and encoding utilities
104
+ bert_index = torch.load(conf['bert_index'])
105
+ sparse_index = torch.load(conf['sparse_index'])
106
+ vectorizer_path = conf['vectorizer_path']
107
+ modelpath = conf['modelpath']
108
+
109
+ ##Conf for first ranking
110
+ firstranking = conf.get('firstranking', 100)
111
+ ranker = cls(df, modelpath, bert_index, sparse_index, vectorizer_path)
112
+ return ranker
113
+
114
+
115
+ if __name__=='__main__':
116
+
117
+ engine = SparseDenseMoviesRanker.from_json_config('conf.json')
118
+
119
+ for query in ["une histoire de pirates et de chasse au trésor", "une histoire de gangsters avec de l'argent"]:
120
+ print(query)
121
+ final_df = engine.run_query(query)
122
+ print(final_df.head())
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ torch
2
+ scikit-learn
3
+ pandas
4
+ transformers