Python algo de recommandation

Python algo de recommandation

Sept 1, 1019

Algorithmes de recommandation

Lien vers le notebook en ligne : https://mybinder.org/v2/gh/tix06/notebooks_classif/master

Choisir alors le fichier : algorithme de recommandation.ipynb

Il s’agit d’un cas très classique d’algorithme utilisé dans le web marketing. Un client choisit et met dans ses favoris un article, ou dans son panier. Le site lui propose des articles compatibles, ou similaires. La recommandation peut être basée sur la description de ces articles. L’algorithme va alors chercher les articles qui ont le plus de points communs dans leur description; c’est à dire le plus de mots communs dans leur description.

articlesCompatibles.png

La première étape pour calculer les similarités consiste à découper les descriptions en listes de mots (c’est la tokenisation) puis à prendre les racines des mots, les stems.

Extraire les mots d’un texte

Prenons un exemple avec le texte suivant :

texte = """
Mardi 20 février, à la médiathèque des Mureaux (Yvelines), le chef de l’Etat a accompagné
la locataire de la rue de Valois pour la remise officielle du rapport
sur les bibliothèques, rédigé par leur ami commun, l’académicien
Erik Orsenna, avec le concours de Noël Corbin, inspecteur général
des affaires culturelles. L’occasion de présenter les premières
mesures en faveur d’un « plan bibliothèques ».
"""

On utilise alors une expression regulière, ; |, |\' |\n |\s+,pour découper le texte, et conserver les stems dans une liste. C’est ce que réalisera ici la fonction decoupe(texte) :

import re
def decoupe(texte):
    """
    utilise une expression regulière pour découper le texte (en paramètre) : 
    - le découpage se fait en fonction de ; ou , ou ' ou \n ou un (ou +) espaces
    - on conserve enfin les stems qui font 4 caractères ou plus
    la fonction retourne une liste de stems
    un stem peut être présent plusieurs fois dans la liste retournée
    """
    texte_decoupe = list(re.split('; |, |\' |\n |\s+',texte))
    texte_mini=[]
    for t in texte_decoupe:
        match = re.match('[a-z]{4,}',t)
        if match!=None:
            texte_mini.append(match.group(0))
    return texte_mini

texte_mini = decoupe(texte)
texte_mini
['chef',
 'accompagn',
 'locataire',
 'pour',
 'remise',
 'officielle',
 'rapport',
 'biblioth',
 'leur',
 'commun',
 'avec',
 'concours',
 'inspecteur',
 'affaires',
 'culturelles',
 'premi',
 'mesures',
 'faveur',
 'plan',
 'biblioth']

Recommandation d’un article à partir de sa description

traitement du fichier de données en csv

import pandas as pd
df = pd.read_csv("datas/descriptif.csv", sep=";")
df
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}

# on créé une liste contenant les articles : 
col = list(df.keys())
# on créé une liste (descriptif) contenant, pour chaque article, une liste avec TOUS les mots retenus dans la description
descriptif = []
for cle in col:
    desc = df[cle][0]
    descriptif.append(decoupe(desc))

descriptif
[['bande',
  'dessinee',
  'ouvrage',
  'illustre',
  'racontant',
  'histoire',
  'images',
  'aventure',
  'fantaisie',
  'lire'],
 ['roman',
  'ouvrage',
  'avec',
  'texte',
  'raconte',
  'histoire',
  'biogaphie',
  'aventure',
  'documentaire',
  'lire'],
 ['ordinateur',
  'dispositif',
  'pour',
  'produire',
  'consulter',
  'objets',
  'numeriques',
  'communiquer',
  'calculer',
  'jouer',
  'lire',
  'videos'],
 ['console',
  'dispositif',
  'pour',
  'jouer',
  'jeux',
  'videos',
  'lire',
  'videos',
  'aventure',
  'fantaisie']]

Vecteurs

L’étape suivante consiste à stocker chacune des descriptions traitées sous forme de vecteurs en base de données.

Chaque ligne est un vecteur qui correspond à une description, chaque colonne correspond à un stem.

La fonction mots retourne une liste contenant tous les mots de descriptif, de manière unique. (les mots qui apparaissent plusieurs fois dans descriptif ne sont renvoyés qu’une seule fois dans la liste mot

La fonction tab créé un tableau où les lignes correspondent au nombre d’occurences de ce mot dans la description de l’objet. La ligne de rang 1 correspond aux occurences pour le premier objet de la description, la ligne de rang 2 au 2e objet de la description,…

Pour avoir des valeurs normalisées, on transforme ces occurences en une frequence : $$f(mot) = \tfrac{nb\quad occurences}{nb\quad mots}$$

def mots(descriptif):
    """
    retourne la liste de tous les mots de descriptif, de manière unique, dans une seule liste
    """
    liste_de_mots = []
    for desc in descriptif:
        for i in desc : 
            if not(i in liste_de_mots):
                liste_de_mots.append(i)
    return liste_de_mots

mots(descriptif)
['bande',
 'dessinee',
 'ouvrage',
 'illustre',
 'racontant',
 'histoire',
 'images',
 'aventure',
 'fantaisie',
 'lire',
 'roman',
 'avec',
 'texte',
 'raconte',
 'biogaphie',
 'documentaire',
 'ordinateur',
 'dispositif',
 'pour',
 'produire',
 'consulter',
 'objets',
 'numeriques',
 'communiquer',
 'calculer',
 'jouer',
 'videos',
 'console',
 'jeux']
def tab(descriptif):
    """
    retourne la liste tableau avec les occurences pour chaque article (chaque rang) 
    des mots retenus (mots)
    une colonne par mot
    """
    tableau = []
    mots_elements = mots(descriptif)
    for desc in descriptif:
        ligne = []
        nombre_de_mots = len(desc)
        ligne = [desc.count(mot)/nombre_de_mots for mot in mots_elements]
        tableau.append(ligne)
    return tableau
tableau = tab(descriptif)
print(tableau)
[[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.1, 0.0, 0.0, 0.1, 0.0, 0.1, 0.0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.08333333333333333, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.08333333333333333, 0.08333333333333333, 0.08333333333333333, 0.08333333333333333, 0.08333333333333333, 0.08333333333333333, 0.08333333333333333, 0.08333333333333333, 0.08333333333333333, 0.08333333333333333, 0.08333333333333333, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.1, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.2, 0.1, 0.1]]
# visualisation
df2=pd.DataFrame(tableau,index=col,columns=mots(descriptif))
df2
bande dessinee ouvrage illustre racontant histoire images aventure fantaisie lire produire consulter objets numeriques communiquer calculer jouer videos console jeux
bande dessinee 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 0.0
roman 0.0 0.0 0.1 0.0 0.0 0.1 0.0 0.1 0.0 0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.0 0.0
ordinateur 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.083333 0.083333 0.083333 0.083333 0.083333 0.083333 0.083333 0.083333 0.083333 0.0 0.0
console de jeux 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.1 0.1 0.100000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.100000 0.200000 0.1 0.1

Calcul des similarités

La dernière étape est celle du calcul des similarités. Cela consiste à appliquer la similarité cosinus 2 à 2 pour tous les produits de la base de données : $$s_{ij}=\tfrac{u_i\cdot u_j}{||ui||\cdot||uj||}$$

On cherche alors une règle de similitude, du type : A=>B (si on aime A alors on aurait tendance à aussi apprécier B)

import numpy as np
def cosij(ui,uj):
    """
    retourne le resultat du calcul de s_ij pour 2 valeurs ui(i) et uj(j)
    paramètres :
    les vecteurs ui et uj de modules mod(ui) et mod(uj)
    """
    s=0
    for i in range(len(ui)):
        s += ui[i]*uj[i]/(mod(ui)*mod(uj))
    return s

def mod(u):
    """
    calcule le module du vecteur mis en paramètre
    """
    s=0
    for i in range(len(u)):
        s += u[i]*u[i]
    s = s**0.5
    return s

def matrice_cos(A,B):
    """
    retourne la matrice de similitude entre 2 listes 
    de dimension 2 (liste de liste), A et B mises en paramètre
    """
    C = np.zeros(shape=(len(A),len(B)))
    for i in range(len(A)):
        for j in range(len(B)):
            C[i,j]=cosij(A[i],B[j])
    return C

mat=matrice_cos(tableau,tableau)
mat
array([[1.        , 0.4       , 0.09128709, 0.27386128],
       [0.4       , 1.        , 0.09128709, 0.18257419],
       [0.09128709, 0.09128709, 1.        , 0.5       ],
       [0.27386128, 0.18257419, 0.5       , 1.        ]])
# visualisation du tableau grace à son DataFrame
df3=pd.DataFrame(mat,index=col,columns=col)
df3
bande dessinee roman ordinateur console de jeux
bande dessinee 1.000000 0.400000 0.091287 0.273861
roman 0.400000 1.000000 0.091287 0.182574
ordinateur 0.091287 0.091287 1.000000 0.500000
console de jeux 0.273861 0.182574 0.500000 1.000000
# visualisation à l'aide d'un outil en couleur
import seaborn as sns
import matplotlib.pyplot as plt
col = list(df3.keys())
sns.heatmap(mat, square=True, annot=True, cbar=False
            , xticklabels=list(col)
            , yticklabels=list(col))
<matplotlib.axes._subplots.AxesSubplot at 0x1a17767250>

png

# visualisation avec matplotlib (un peu moins lisible)
plt.matshow(mat, cmap='rainbow');

png

Interpretation

On voit assez facilement que :

  • la bande dessinée est proche dans sa description avec le roman, avec un score de 0.4, et un un peu moins proche de la console de jeux (score de 0.27)
  • l'ordinateur est compatible avec la console de jeux, avec un score de 0.5
  • La description de la bande dessinnée et du roman ne contient presque aucun point commun avec l'ordinateur
def articles(tab):
    tab=tab.sort_values(ascending=False)
    articles = list(tab.index.values)
    recom = 'l\'article \033[1m {0:15} \033[0;0m est similaire à \033[1m{1:15}\033[0;0m voire peut être un peu à \033[1m{2:15}\033[0;0m'.format(articles[0],articles[1],articles[2])
    return recom

def recommandation(dataf):
    texte=[]
    col = list(dataf.keys()) 
    for c in col:
        tab_red=[]
        tab_red = dataf[c]
        texte.append(articles(tab_red))
    return texte

texte = recommandation(df3)
for t in texte:
    print(t)
l'article  bande dessinee   est similaire à roman           voire peut être un peu à console de jeux
l'article  roman            est similaire à bande dessinee  voire peut être un peu à console de jeux
l'article  ordinateur       est similaire à console de jeux voire peut être un peu à roman          
l'article  console de jeux  est similaire à ordinateur      voire peut être un peu à bande dessinee 

definir des classes à l’aide d’une représentation en graphe

import matplotlib.pyplot as plt
import networkx as nx

mat=matrice_cos(tableau,tableau)
col = list(df3.keys())

G = nx.Graph()

def makeG(mat,col):
    """
    creation du graphe G à partir : 
    - du paramètre col : la liste des lés (colonnes) du DataFrame fg3 (equiv à la matrice de similitude)
    - du paramètre mat : la matrice de similitude déjà calculée
    Les noeuds du graphe sont constitués à partir des articles
    Les arêtes sont pondérés d'un poids weight qui est la valeur lue dans la matrice de similitude mat
    """
    for i in range(len(mat[:,0])):
        for j in range(len(mat[0,:])):
            G.add_edge(col[i], col[j], weight=mat[i,j])
    return G


    

#G.add_edge('a', 'b', weight=0.6)


def drawG(G,seuil):
    """
    segmentation de arêtes en fonction du paramètre seuil
    - les arêtes de weight >= seuil sont représentées en trait plein sur le graphe
    - celle de weight < seuil sont représentées en trait pointillé
    """
    elarge = [(u, v) for (u, v, d) in G.edges(data=True) if d['weight'] >= seuil]
    esmall = [(u, v) for (u, v, d) in G.edges(data=True) if d['weight'] < seuil]
    pos = nx.spring_layout(G)  # positions for all nodes
    # nodes
    nx.draw_networkx_nodes(G, pos, node_size=700)
    # edges
    nx.draw_networkx_edges(G, pos, edgelist=elarge,width=6)
    nx.draw_networkx_edges(G, pos, edgelist=esmall,width=2, alpha=0.5, edge_color='b', style='dashed')
    # labels
    nx.draw_networkx_labels(G, pos, font_size=20, font_family='sans-serif')
    plt.axis('off')
    plt.show()
    
G = makeG(mat,col)
drawG(G,0.3)

png

Interprétation

Il apparait ici clairement 2 classes en choisissant seuil = 0.3 :

  • les articles du genre littérature
  • les articles du genre électronique

Classification

L’arbre de classification peut être utile dans le cas L’apprentissage par arbre de décision désigne une méthode basée sur l’utilisation d’un arbre de décision comme modèle prédictif. Dans ces structures d’arbre, les feuilles représentent les valeurs de la variable-cible (les classes) et les embranchements correspondent à des combinaisons de variables d’entrée qui mènent à ces valeurs. En analyse de décision, un arbre de décision peut être utilisé pour représenter de manière explicite les décisions réalisées et les processus qui les amènent.

Une des variables d’entrée est sélectionnée à chaque nœud intérieur (ou interne, nœud qui n’est pas terminal) de l’arbre selon une méthode qui dépend de l’algorithme.

L’arbre est en général construit en séparant l’ensemble des données en sous-ensembles en fonction de la valeur d’une caractéristique d’entrée. Il est construit de manière récursive. C’est un algorithme glouton.

Plus de détail : https://fr.wikipedia.org/wiki/Arbre_de_décision_(apprentissage)

Il existe ainsi les :

  • arbres de classification (feuille = classe)
  • arbres de regression, qui permettent de prédire une quantité réelle

Les algorithmes pour construire les arbres de décision sont construits en divisant l’arbre du sommet vers les feuilles en choisissant à chaque étape une variable d’entrée qui réalise le meilleur partage de l’ensemble d’objets, comme décrit précédemment. Pour choisir la variable de séparation sur un nœud, les algorithmes testent les différentes variables d’entrée possibles et sélectionnent celle qui maximise un critère donné.

suite : voir la page sur les arbres de décision : graphes_mini_prog.md

Liens