Module 4 — Filtrage par contenu et similarité
Les deux modules précédents ne connaissent rien des cours eux-mêmes : ils ne voient que des identifiants et une matrice d'interactions. Cette myopie a un prix : un cours qui vient d'être publié n'a aucun voisin, aucun facteur latent, et reste invisible tant que quelqu'un ne l'a pas noté. Le filtrage par contenu apporte la réponse complémentaire — recommander en s'appuyant sur ce que le cours est, pas seulement sur ce que les autres en ont fait.
Représenter un cours par son contenu
Un cours de la plateforme est décrit par plusieurs signaux textuels : un titre, un résumé, des mots-clés, la liste des modules, parfois une transcription des vidéos. La question est : comment transformer ce texte en un vecteur numérique utilisable pour la similarité ?
L'approche historique repose sur TF-IDF : chaque cours devient un vecteur creux dans l'espace du vocabulaire, où la coordonnée d'un mot vaut sa fréquence dans le cours pondérée par sa rareté dans le catalogue. C'est rapide, interprétable, et suffisant pour un catalogue de quelques centaines de cours. Le défaut est bien connu : deux cours qui parlent de la même chose avec des mots différents (« régression logistique » et « classification binaire linéaire ») auront une similarité nulle.
L'approche moderne s'appuie sur les plongements appris par un modèle de langage — vus au cours 13 sur le NLP. Chaque cours est encodé par sentence-transformers, text-embedding-3 ou un modèle équivalent, en un vecteur dense de 384 à 1 536 dimensions qui capture le sens. Les cours « Introduction à la régression logistique » et « Classification binaire pour débutants » ont alors une similarité cosinus supérieure à 0,8, même sans mot commun.
from sentence_transformers import SentenceTransformer
import numpy as np
encodeur = SentenceTransformer("intfloat/multilingual-e5-base")
descriptions = [
"Introduction à la régression logistique en Python.",
"Classification binaire pour débutants avec scikit-learn.",
"Le langage Rust pour l'ingénierie système.",
]
plongements = encodeur.encode(descriptions, normalize_embeddings=True)
# La similarité cosinus se réduit à un produit scalaire sur des vecteurs normés.
S = plongements @ plongements.T
print(S)
La normalisation est un détail qui change tout : lorsque tous les vecteurs sont de norme unité, le produit scalaire est la similarité cosinus, et l'on peut utiliser une recherche approchée par produit scalaire (module 6) au lieu d'une recherche exacte.
Profil utilisateur par agrégation
Un apprenant n'a pas de description textuelle, mais il a un historique. Le profil utilisateur dans l'espace de contenu est l'agrégat des cours qu'il a suivis, pondéré par sa préférence :
où est le plongement du cours et le poids (une note explicite normalisée, ou une durée relative de visionnage). Le profil vit dans le même espace que les cours, et l'on peut donc calculer pour tout cours du catalogue, y compris ceux publiés hier.
Cette formulation gère naturellement les intérêts multiples. Un apprenant qui suit du Python et du management aura un profil « au milieu » des deux, et les cours à la frontière (par exemple « Python pour l'analyse financière ») auront une similarité maximale, ce qui est souvent exactement ce qu'il faut recommander. On peut aussi maintenir plusieurs profils par apprenant, un par groupe thématique de son historique — c'est ce que fait YouTube depuis 2016 pour éviter que la moyenne de vidéos très diverses ne produise un vecteur au milieu de nulle part.
Similarité cosinus et recherche des voisins
Une fois profil et plongements en main, la recommandation est un tri :
def top_k_contenu(profil_u, plongements, deja_suivis, k=10):
scores = plongements @ profil_u # produit scalaire = cosinus si normés
scores[list(deja_suivis)] = -np.inf # exclure ce que u a déjà vu
return np.argsort(-scores)[:k]
Pour 500 cours et 30 000 apprenants, ce calcul brut se fait en une seconde. Au-delà de 100 000 cours, il faut passer à une recherche approchée (FAISS, ScaNN, HNSW) qui pré-indexe l'espace et répond en une milliseconde par apprenant — nous y reviendrons au module 6.
Un raffinement utile consiste à combiner deux similarités : celle du contenu et celle apprise par le collaboratif du module 3. Sur un cours nouveau, seule la première est disponible ; sur un cours mature, la seconde apporte la nuance que le texte ne capte pas (la qualité pédagogique perçue, par exemple, qu'aucune description ne trahit). C'est le fil directeur du module 5 sur les approches hybrides.
La bulle de filtre
Le filtrage par contenu, quand il est utilisé seul, produit un effet documenté depuis Pariser en 2011 : la bulle de filtre. Un apprenant qui a suivi trois cours de machine learning ne reçoit que des cours de machine learning — de plus en plus pointus, de plus en plus proches de son historique — et n'est jamais exposé à un domaine adjacent qu'il aurait pourtant apprécié. Le modèle est très bon localement, catastrophique globalement.
Le mécanisme est mécaniste, pas idéologique : la similarité cosinus favorise ce qui ressemble à ce qui existe déjà. Un cours de leadership pour ingénieurs peut être très pertinent pour un développeur senior, mais il est loin de tous ses cours de code. Sa similarité est faible ; il n'apparaît jamais dans le top-10 ; l'apprenant ne le découvre pas ; le modèle n'apprend pas qu'il l'aurait apprécié. La rétroaction est en boucle fermée.
Trois contre-mesures existent et cohabitent dans les systèmes matures :
- Injecter de la diversité. Après avoir sélectionné les vingt cours les plus proches, en garder dix par un algorithme comme MMR (Maximal Marginal Relevance) qui pénalise la redondance intra-liste. Le module 8 formalisera cette diversité.
- Injecter de l'exploration. Une fraction fixe de la liste (5 à 10 %) est tirée aléatoirement pondérée par la popularité, indépendamment de la similarité. C'est la version simple du dilemme exploration-exploitation qui reviendra au module 7.
- Combiner avec le collaboratif. Le collaboratif casse la bulle parce qu'il repose sur des utilisateurs qui, eux, ont fait des sauts thématiques. Un ingénieur ayant suivi du leadership finit par apparaître comme voisin d'autres ingénieurs, et le leadership remonte pour le groupe.
Un moteur uniquement fondé sur le contenu obtient d'excellentes valeurs de rappel@k et de précision@k hors ligne, parce que ces métriques évaluent contre l'historique observé — qui reflète exactement la bulle. Ce n'est qu'en couverture (part du catalogue effectivement recommandée) et en A/B test qu'on voit le problème. Le module 8 revient précisément sur cette limite des métriques hors ligne.
En résumé
- Représenter un cours par un plongement de sa description permet de calculer la similarité même entre cours sans évaluateur commun.
- Le profil utilisateur est l'agrégat pondéré des plongements de son historique ; on peut en maintenir plusieurs pour des intérêts multiples.
- La similarité cosinus dans cet espace résout le démarrage à froid des cours ; la recherche approchée (module 6) la rend rapide à grande échelle.
- La bulle de filtre est le défaut structurel du contenu seul ; elle se corrige par diversité, exploration, et surtout hybridation avec le collaboratif du module 5.
Module suivant : combiner intelligemment collaboratif et contenu.