Publié le

Similarités dans la Réserve faunique de Mastigouche (Photos par David Lesieur)

En 2014, lorsque j’ai migré mon blogue personnel sur Pelican (un générateur de site statique), j’en ai profité pour ajouter, sous chaque article, une liste de recommandations d’articles traitant des mêmes sujets. Cette liste, générée automatiquement par l’extension Related Posts pour Pelican, montrait les autres articles ayant le plus grand nombre de marqueurs (tags) en commun avec l’article courant. En d’autres mots, le score de similarité entre deux articles dépendait du nombre de marqueurs qu’ils avaient en commun.

Il importe que cette liste de recommandations soit générée automatiquement, car je ne souhaite aucunement devoir, à chaque nouvel article, revisiter tous les articles antérieurs pour décider de les mettre en relation ou non avec le nouveau texte. Par contre le score de similarité, tel qu’il était calculé, ne donnait pas de très bons résultats. Voici deux exemples de cas problématiques découlant de ce calcul simpliste :

  • Le nombre de marqueurs par article est variable. Si l’article courant est associé uniquement au marqueur #Python, n’importe quel article ayant ce marqueur parmi d’autres peut être retenu pour recommandation. Pourtant, si un autre article compte, lui aussi, uniquement le marqueur #Python, il est probablement plus pertinent qu’un article qui a une foule de marqueurs en plus de #Python, puisqu’un marqueur unique signale sans doute le cœur du propos.
  • Si l’article courant est associé à #Pelican et à quelques autres marqueurs, l’algorithme favorise les articles qui ont plusieurs marqueurs en commun, même si #Pelican n’est pas du nombre. Pourtant, si le blogue n’a qu’un seul autre article traitant de #Pelican, c’est bien dommage de rater l’occasion de le recommander!

Ce genre de lacune faisait de moi un cordonnier bien mal chaussé : travailler dans les systèmes d’organisation et de recherche d’information et employer un si piètre système de recommandation? La honte! Car des approches ont depuis longtemps été développées pour pallier à ces difficultés, dont le fameux modèle vectoriel.

Le modèle vectoriel

Le modèle vectoriel tire son nom de ce qu’il s’appuie sur une représentation des textes dans un espace vectoriel qui compte autant de dimensions qu’il existe de termes possibles dans tout le corpus de textes. Un document textuel est représenté dans cet espace par un vecteur dont les composantes correspondent aux poids de chacun des termes pour ce document. Le poids d’un terme est en quelque sorte une estimation de la pertinence du terme en association avec le document, ou de la capacité de ce terme à distinguer le document des autres.

Salton et Buckley (1988) ont décrit trois facteurs à considérer dans le calcul de ces poids :

  1. Le nombre d’occurrences du terme dans le document. Plus un terme est fréquent dans un document, plus on considère que le terme est pertinent par rapport à ce document. C’est le facteur tf (term frequency).
  2. Le nombre de documents dans lequel apparaît le terme. On estime que la pertinence d’un terme décroît si sa fréquence dans l’ensemble du corpus augmente. En effet, un terme occurrant dans presque toute la collection de documents ne sera pas d’une grande utilité pour distinguer un document en particulier. C’est le facteur idf (inverse document frequency).
  3. La longueur des documents. On désire éviter que les documents plus longs ne soient favorisés simplement parce qu’ils comptent plus de termes que les documents plus courts. C’est le facteur normalisation.

Ces trois facteurs peuvent être pris en compte par la méthode du tf*idf. Différentes formules existent, mais en général elles fournissent pour chaque terme un poids dont la valeur normalisée est située entre 0 et 1; une valeur près de 0 signale un terme peu pertinent, tandis qu’une valeur près de 1 dénote un terme très pertinent. Le poids d’un terme pour un document donné sera élevé si le terme apparaît fréquemment dans un petit nombre de documents de la collection ou, au contraire, faible quand le terme est peu fréquent dans ce document ou quand il apparaît dans un grand nombre de documents.

Pour donner un exemple, si mon blogue utilisait cinq marqueurs différents, il se pourrait que l’un des articles, appelons-le a, soit représenté par le vecteur suivant :

a = [0,844 0,183 0,000 0,000 0,505]

On constate que l’article a est associé à trois marqueurs. Le poids élevé du premier signifie qu’il n’est pas souvent utilisé dans le blogue, tandis que le second est, au contraire, très fréquent, donc beaucoup moins discriminant. Les deux marqueurs suivants sont absents de l’article, tandis que le dernier est présent dans l’article et moins fréquent dans le blogue que le second, quoique plus usité que le premier.

Le calcul de la similarité

Pour déterminer la similarité de deux documents, on peut comparer les orientations de leurs vecteurs en calculant le cosinus de l’angle qui les sépare. Ainsi, deux vecteurs ayant la même orientation auront une similarité cosinus de 1, alors que des vecteurs orthogonaux auront une valeur de 0. Plus la valeur est près de 0, plus on considère que les documents sont divergents.

En guise d’exemples amusants à interpréter, voici les scores de similarité approximatifs qui pourraient être obtenus en comparant certains articles avec l’article a donné précédemment :

Similarité(a, [0,815 0,177 0,000 0,260 0,487]) = 0,966
Similarité(a, [0,645 0,140 0,645 0,000 0,386]) = 0,764
Similarité(a, [0,000 0,341 0,000 0,000 0,940]) = 0,536
Similarité(a, [1,000 0,000 0,000 0,000 0,000]) = 0,844

D’après les scores, les deux premiers articles sont assez similaires à l’article a, mais le second a le score le plus faible des deux, notamment parce qu’un des marqueurs qui le distingue le plus est absent de a. Le troisième n’a pas un score très élevé, car même s’il a deux marqueurs en commun avec a, ce ne sont pas les marqueurs les plus pertinents. En revanche, le dernier article obtient un score élevé même s’il a un seul marqueur en commun avec a, car ce marqueur est hautement discriminant dans les deux documents comparés. En ce qui me concerne, le problème des recommandations automatiques peu pertinentes paraît résolu!

Un aspect très attrayant de cette méthode de mesure tient à la possibilité d’ordonner les documents en fonction de leur score de similarité. On pourra facilement limiter la liste des recommandations à un petit nombre de documents, ou encore n’afficher que ceux dont le score dépasse un certain seuil.

Il est à noter que dans le cas typique d’un blogue, un marqueur n’est jamais répété plus d’une fois pour un même article, ce qui signifie que son tf est toujours 1 s’il est associé à l’article. Dans ce contexte, les seuls facteurs modifiant le poids d’un marqueur présent dans un article sont l’idf et la normalisation. Le tf prendrait toute son importance si on appliquait la méthode au texte intégral des articles. Avec le texte intégral, les vecteurs compteraient aussi beaucoup plus de dimensions, en fonction du nombre de formes lexicales extraites des textes.

La mise en œuvre avec Python

Puisque mon blogue est construit avec Pelican, lui-même bâti avec le langage Python, la solution naturelle pour opérationnaliser cette méthode de recommandation d’articles passe par la création d’une nouvelle extension pour Pelican. Cette extension sera responsable de fournir, pour chaque article, la liste des articles recommandés.

Pour les calculs, inutile de réinventer la roue : le tf*idf est un modèle bien établi et l’environnement Python est riche en bibliothèques logicielles capables de l’appliquer. À cette fin, j’ai choisi Gensim, qui pourra traiter des collections de documents bien plus vastes que la mienne et même bâtir la matrice des scores de similarité pour toutes les paires de documents.

C’est ainsi que j’ai créé l’extension Similar Posts pour Pelican. Si les détails d’implémentation vous intéressent, le code source tient sur quelques lignes.

En ce qui a trait au choix de la formule de tf*idf, j’ai adopté celle de Lucene après quelques expérimentations informelles avec les petits échantillons que sont mon blogue et la suite de tests unitaires que j’ai créée pour Similar Posts. Les recommandations obtenues me paraissaient plus pertinentes qu’avec la formule par défaut de Gensim, qui en outre a l’inconvénient de produire un poids de 0 pour un terme présent dans tous les documents, ce qui signifie que le terme est disqualifié, comme s’il n’existait pas (même s’il s’agit d’un cas de figure peu probable, ce comportement est peu désirable à mon avis quand on appuie le calcul sur les marqueurs plutôt que sur le plein texte, puisque chaque marqueur est significatif, au contraire des mots d’un texte).

Quelques améliorations possibles

Quelques améliorations à considérer pour l’extension Similar Posts :

  • Ajouter la possibilité de traiter le titre et le texte intégral des articles plutôt que seulement les marqueurs. Ceci rendrait l’extension utile aux blogues qui n’utilisent pas les marqueurs. En ce qui concerne mon blogue, j’aime l’idée du choix éditorial représenté par les marqueurs et les relations explicites qu’ils créent entre les articles, mais il est possible qu’avec une combinaison judicieuse des marqueurs et du texte intégral, la pertinence des recommandations soit améliorée. Une certaine attention devra être portée au traitement du texte (segmentation, filtrage, transformation), qui n’a pas été nécessaire avec les marqueurs puisqu’ils sont peu nombreux et soigneusement contrôlés.
  • Expérimenter différentes formules de tf*idf avec de plus gros corpus et une évaluation plus formelle en terme de rappel et précision.
  • Je n’ai pas voulu faire d’optimisations prématurées, surtout que les sites basés sur Pelican sont rarement gigantesques, mais il serait souhaitable d’éviter de stocker toutes les données en mémoire pendant le traitement, surtout si on traite le texte intégral. Avec un tout petit peu d’efforts, il serait possible d’exploiter la capacité de Gensim à travailler en streaming, document par document.

Référence

Salton, G. et Buckley, C. (1988). Term-weighting approaches in automatic text retrieval. Information Processing & Management, 24(5), 513‑523. doi:10.1016/0306-4573(88)90021-0

Commentaires