Sulci ===== Workshop @fabelier, 15 février 2012 .fx: frontpage .. image:: logo-fabelier.png .. image:: logo-liberation.png ---- Qui sommes-je? ============== - aujourd'hui responsable de l'informatique éditoriale (web et papier) - rentré à Libération comme correcteur, en janvier 2004 - spécialiste ni de python, ni de la langue française, ni de TAL, mais amoureux des trois - y.boniface (at) liberation.fr - https://github.com/yohanboniface / https://github.com/liberation - https://bitbucket.org/yohanboniface / https://bitbucket.org/liberation ---- Un exemple pour commencer ========================= .. sourcecode:: pycon >>> from sulci.textmining import SemanticalTagger >>> text = u"""«La Russie et la Chine finiront par regretter leur décision qui ... les a vues s’aligner sur un dictateur en fin de vie et qui les a mises en ... porte-à-faux avec le peuple syrien.»""" >>> s = SemanticalTagger(text) >>> s.descriptors [(, 100.0), (, 100.0), (, 14.798308089447328), (, 10.337552742616033)] En d'autres termes, en entrée, on fournit un texte, en sortie on reçoit des descripteurs, i.e. des mots-clés ou expressions censés décrire le sens du texte. ---- Contexte ======== **Pourquoi ce projet** - version propriétaire et fermée (Windows) achetée en 2007 et jamais jugée utilisable - outil demandé depuis lors par le service qui gère nos archives - documentation: - catégoriser les archives - retrouver les articles des archives - construire des dossiers thématiques pour les journalistes - L'intérêt d'un tel outil pour eux est d'avoir automatiquement les descripteurs les plus évidents, pour se concentrer sur les plus délicats - le projet est né un peu par hasard, comme exercice pédagogique - l'objectif est que l'outil soit utilisé aussi par les journalistes cette année ---- Les grandes lignes ================== - développé en python - s'appuie sur le framework Django (utilisé à Libération) - les principaux algorithmes demandent un apprentissage - l'apprentissage sémantique est dépendant du corpus utilisé, il faut donc choisir le corpus d'apprentissage en fonction du futur corpus d'utilisation - cet apprentissage prend la forme de données SQL ; une db est donc nécessaire pour pouvoir utiliser sulci - le code est actuellement utilisé en production à Libération, mais il faut le considérer comme un «prototype opérationnel» **Comment ça se prononce** soule-tchi **Pourquoi ça s'appelle comme ça** Ça vient de la région du Sulcis, au sud de la Sardaigne (Italie). Mon grand-père y était mineur. Et il y a laissé la peau. ---- Les algos et traitements ======================== ---- Prétraitement du texte brut --------------------------- C'est une bête fonction python (`sulci.textutils.normalize_text`), qui: - transforme les entités (é => é) - supprime les balises - normalise certains caractères (les apostrophes, les guillemets par exemple) - transforme par exemple les "dit-il" en "dit - il" .. sourcecode:: pycon >>> from sulci.textutils import normalize_text >>> text = """

«C’est le petit monde politico-médiatique qui m’a prise en ... grippe, pas l’opinion publique.»

Eva Joly ... candidate Europe Ecologie-les Verts à l’Elysée dans l’hebdomadaire ... Politis paru hier

""" >>> print normalize_text(text) «C'est le petit monde politico-médiatique qui m'a prise en grippe, pas l'opinion publique.» Eva Joly candidate Europe Ecologie - les Verts à l'Elysée dans l'hebdomadaire Politis paru hier Pour aller voir le code: https://github.com/yohanboniface/sulci/blob/master/sulci/textutils.py#L30 ---- Tokenisation ------------ - le texte brut est découpé et "pythonnisé" - deux niveaux de découpages: la phrase et les éléments de la phrase ("mots", ponctuation...) - chacun de ces éléments devient un objet python: une phrase est une instance de `Sample`; un mot, une instance de `Token` .. sourcecode:: pycon >>> text = u"Les insultes sont la seule raison des pauvres d'esprit" >>> st = StemmedText(text) >>> sample = st.samples[0] # Première phrase >>> token = sample[0] >>> token.original u'Les' >>> token.lemme u'le' .. note:: ---- Catégorisation syntaxique ------------------------- On détermine le "rôle" d'un Token dans la phrase: nom commun, adjectif, ponctuation, etc. Pourquoi ? - distinguer les homonymes ("les poules du couvent couvent") - pouvoir pondérer / filtrer selon le type: par exemple donner plus de poids aux noms et adjectifs et aucun aux déterminants .. sourcecode:: pycon >>> for t in sample: ... print t.original, t.tag Les DTN:pl insultes SBC:pl sont ECJ:pl la DTN:sg seule ADJ:sg raison SBC:sg des DTC:pl pauvres SBC:pl d' PREP esprit SBC:sg ---- Lemmatisation ------------- On détermine la version "neutre" d'un mot, pour essayer de gommer les aspérités de son utilisation dans le contexte: - l'infinitif pour un verbe - le singulier pour un nom - le masculin singulier pour un adjectif - etc. Par exemple, la phrase:: Les insultes sont la seule raison des pauvres d'esprit devient:: Le insulte être le seul raison de un pauvre de esprit ---- Lemmatisation ............. .. sourcecode:: pycon >>> from sulci.textmining import StemmedText >>> text = u"Les insultes sont la seule raison des pauvres d'esprit" >>> st = StemmedText(text) >>> print st.samples[0] # la première phrase le insulte être le seul raison des pauvre de esprit ---- Lemmatisation ............. **Et la stemmatisation ?** Pros: - ne demande pas d'apprentissage Cons: - algorithme strictement morphologique (ne prend pas en compte contexte, POS) ; donc par exemple ne sait pas distinguer "couvent" de "couvent" dans le classique: «Les poules du couvent couvent.» - l'algorithme le plus connu (Porter, implémenté notamment par le projet Snowball) est réputé pas très adapté au français Pour tester quand même (package `pystemmer` à installer): .. sourcecode:: pycon >>> from Stemmer import Stemmer >>> stemmer = Stemmer("french") >>> stemmer.stemWord("chevaux") # résultat attendu 'cheval' >>> stemmer.stemWord("genoux") # résultat considéré comme une erreur 'genoux' ---- Extraction des entités clés --------------------------- Extraire les éléments du discours qui représentent le sens du texte. Une entité clé, c'est quoi ? - collocations (~ expression dont le sens vaut plus que la somme des sens qui la composent), n-grams (~ expression dont les composantes avaient statistiquement plus de chances d'apparaître ensemble que séparemment) - noms propres - mots dont la fréquence est supérieure à la fréquence moyenne des mots dans le texte - le maire de Paris Bertrand Delanoë ---- Extraction des entités clés ........................... **Pointwise mutual information** Compare la fréquence d'apparition d'une expression avec celle des éléments de cette expression .. sourcecode:: python ngram_possible = len(self.text) - len(self) + 1 members_probability = product([1.0 * s.count/len(self.text) for s in self]) s_m_i = math.log(1.0 * self.count / ngram_possible / members_probability) **Pondération des entités** :: statistical_mutual_information * nrelative_frequency * POS score En d'autres termes: * le score de l'expression en tant que collocation * multiplié par la fréquence relative à la taille de l'expression * multiplié par un score issu de la catégorie lexicales des éléments de l'expression ---- Extraction des entités clés ........................... .. sourcecode:: pycon >>> from sulci.textmining import SemanticalTagger >>> text = u"""A l’occasion d’un attroupement lors de sa visite à Fessenheim, ... hier, Nicolas Sarkozy nous convie à faire quelques pas avec lui devant les ... caméras. Il tient manifestement à s’adresser directement aux lecteurs de ... Libération. Le président-candidat : «[François Hollande] se coupe des ... ouvriers en faisant cela [en fermant Fessenheim]. C’est une erreur. François ... Mitterrand ne l’aurait jamais fait. Les gens ont bien compris que cette ... décision était purement électoraliste. Ils ne sont pas bêtes.» ... Le journaliste de Libération : «Mais cela va coûter beaucoup d’argent de ... remettre à niveau la centrale.» ... Le président-candidat : «Le rapport de la Cour des comptes a été très clair. ... Ça coûterait encore plus cher d’investir dans le photovoltaïque.» On entre ... dans un nouveau bâtiment. Encore le président-candidat : «Sans moi, vous ... allez vous embêter. Qu’est-ce que Libération va devenir ? Combien de pages ... vous faites sur moi par jour ?»""" >>> s = SemanticalTagger(text) >>> s.deduplicate_keyentities() >>> for ke in s.keyentities: ... print ke François Hollande Nicolas Sarkozy François Mitterrand aller président-candidat Libération cela Fessenheim Qu'est Cour ---- Catégorisation sémantique ------------------------- - les catégories sémantiques sont appelées "descripteurs" - sulci est un moteur d'inférence (il va trouver qu'un texte parle du Parti socialiste même si la chaîne "Parti socialiste" n'apparaît jamais dans le texte, mais par exemple seulement "François Hollande", "Martine Aubry"...) - MAIS il ne connaît que les descripteurs qu'on lui a fait apprendre - un peu plus de 7000 descripteurs dans l'apprentissage Libération - les descripteurs sont déduits des entités clés ("déclencheurs") extraites lors de l'étape précédente via les relations déclencheurs=>descripteurs créées lors de l'apprentissage .. sourcecode:: pycon >>> for d in s.descriptors[:10]: ... print d (, 111.5773664718986) (, 99.607961983161587) (, 70.391150257770533) (, 65.768314429804633) (, 64.654728257089261) (, 58.484979899632719) (, 43.289428857194572) (, 42.946056049664115) (, 39.57865973263668) (, 36.031019330252548) ---- L'apprentissage =============== ---- Généralités ----------- - chaque apprentissage s'appuie sur un corpus - qu'est-ce qu'un corpus ? Un ensemble de textes sur lesquels sont déjà posés les descripteurs qu'on souhaite faire apprendre ---- Catégorisation syntaxique ------------------------- - cet apprentissage est propre à une langue - c'est un algorithme de Brill à peine revisité - error driven => il apprend de ses erreurs **A quoi ressemble le corpus** La/DTN:sg liberté/SBC:sg de/PREP la/DTN:sg presse/SBC:sg n'/ADV aura/ACJ:sg donc/ADV vécu/PAR:sg que/SUB vingt/CAR et/COO un/CAR ans/SBC:pl en/PREP Hongrie/SBP:sg ./. - actuellement, le corpus pour la catégorisation syntaxique contient 30666 mots ---- L'algo de Brill --------------- - en entrée, on fournit: - un corpus tagué proprement - un lexique des mots les plus courants, déjà tagués - des templates de règles - le corpus est dupliqué dans une version sans les tags - un premier tag issu de règles basiques est appliqué à chaque mot du corpus - les deux corpus sont alors comparés - dès qu'une erreur est trouvée, l'algorithme infère des règles pouvant expliquer cette erreur en utilisant les templates fournis - chacune de ces règles est testée sur tout le corpus de travail; est retenue celle qui a le meilleur bilan erreurs corrigées / erreurs créées - on passe à l'erreur suivante, et ainsi de suite tant qu'il reste des erreurs ---- L'algo de Brill ............... - Toute cette séquence est répétée deux fois: - une fois pour les règles lexicales (s'appuyant sur la forme des mots) - une fois pour les règles contextuelles (s'appuyant sur les mots et tags voisins) ---- L'algo de Brill ............... **Exemples de règles** Règles lexicales: > SBC:sg des fgoodright SBC:pl > SBC:sg ait fhassuf 3 VCJ:sg > SBC:sg les fgoodright SBC:pl > SBC:sg ces fhassuf 3 SBC:pl > SBC:sg aient fhassuf 5 VCJ:pl > SBC:sg rer fhassuf 3 VNCFF Règles contextuelles: > DTN:sg PRV:sg WDNEXTTAG leur VNCFF > SBC:sg PAR:sg PREVBIGRAM a été > DTN:pl PRO:pl WDAND2TAGBFR PRV:pl tous ---- Lemmatisation ------------- - c'est un dérivé de l'algorithme de Brill - sortaient / portaient, galère galère **A quoi ressemble le corpus** > La/DTN:sg/le liberté/SBC:sg de/PREP la/DTN:sg/le presse/SBC:sg n'/ADV/ne > aura/ACJ:sg/avoir donc/ADV vécu/PAR:sg/vivre que/SUB vingt/CAR et/COO un/CAR > ans/SBC:pl/an en/PREP Hongrie/SBP:sg ./. - extension du format utilisé pour la catégorisation syntaxique - la lemmatisation a besoin de la catégorie syntaxique ---- **Exemples de règles** > COO MAKELOWER 0.678643 > ECJ:sg FORCELEMME être 0.660030 > PAR:sg CHANGESUFFIX "enu" "enir" 0.471148 > VNCNT CHANGESUFFIX "ssant" "sser" 0.270237 > ADJ:pl CHANGESUFFIX "aines" "ain" 0.422129 > ADJ:pl CHANGESUFFIX "ales" "al" 0.465632 > ADJ:sg CHANGESUFFIX "uelle" "uel" 0.422129 ---- Catégorisation sémantique ------------------------- - c'est le plus lourd - il est totalement modelé par le corpus utilisé - 42000 textes dans le corpus utilisé pour l'apprentissage Libération - les textes du corpus sont donc **déjà** catégorisés - plus de 36 heures de traitement - avec zeromq, on peut paralléliser le traitement ---- Catégorisation sémantique ......................... - chaque texte est processé, jusqu'à en extraire les entités clés - par hypothèse, chaque entité clé est liée à chaque descripteur du texte - ce lien est surpondéré à chaque apparition de cette relation - en fin de traitement, les relations très peu pondérées sont nettoyées pour alléger les temps de traitement ---- Catégorisation sémantique ......................... Dans l'apprentissage Libération actuel: - un peu plus de 7000 descripteurs entraînés - 196000 déclencheurs (entités clés) - 359034 relations déclencheur=>descripteurs après nettoyage ---- Catégorisation sémantique ......................... .. sourcecode:: pycon >>> trigger = Trigger.objects.get(original="Nicolas Sarkozy") >>> for relation in trigger: ... print relation, relation.pondered_weight Nicolas Sarkozy =[952.000000]=> Nicolas Sarkozy 1.0 Nicolas Sarkozy =[530.000000]=> chef de l'Etat 0.556722689076 Nicolas Sarkozy =[346.000000]=> UMP 0.207169853114 Nicolas Sarkozy =[306.000000]=> élection présidentielle 0.292729591837 Nicolas Sarkozy =[269.000000]=> gouvernement 0.128829582681 Nicolas Sarkozy =[263.000000]=> réforme 0.205825814745 Nicolas Sarkozy =[260.000000]=> déclaration 0.244856563315 Nicolas Sarkozy =[249.000000]=> 2012 0.23511588751 Nicolas Sarkozy =[234.000000]=> polémique 0.156721544204 Nicolas Sarkozy =[226.000000]=> France 0.060967341482 Nicolas Sarkozy =[212.000000]=> Parti socialiste 0.0744638549426 >>> descriptor = Descriptor.objects.get(name="Parti socialiste") >>> for relation in descriptor.triggertodescriptor_set.all()[:10]: ... print relation, relation.pondered_weight PS =[634.000000]=> Parti socialiste 1.0 Martine Aubry =[374.000000]=> Parti socialiste 0.589905362776 France =[255.000000]=> Parti socialiste 0.116548967594 UMP =[219.000000]=> Parti socialiste 0.124626466201 Nicolas Sarkozy =[212.000000]=> Parti socialiste 0.0744638549426 Parti =[212.000000]=> Parti socialiste 0.322225408661 Ségolène Royal =[211.000000]=> Parti socialiste 0.332807570978 Français =[193.000000]=> Parti socialiste 0.169804525811 François Hollande =[177.000000]=> Parti socialiste 0.279179810726 Dominique Strauss-Kahn =[172.000000]=> Parti socialiste 0.192820084991 ---- Catégorisation sémantique ......................... **pondered_weight** - La relation entre un déclencheur et un descripteur est pondérée de la sorte: > weight de la relation courante > / weight de la relation max du déclencheur > / weight de la relation max du descripteur ---- Idées d'utilisation future ========================== - News catégorisée, transparent et open source - processer wikipedia - serveur centralisé avec webservice pour utilisation facilitée ---- Pour aider ========== - accélérer l'apprentissage sémantique en utilisant un base en RAM (voire tester d'autres modèles de données) - inverse document frequency - toute review de code, tous retours d'utilisation sont bons à prendre :) - tests unitaires - étendre le corpus pour l'apprentissage de la catégorisation sémantique et de la lemmatisation ---- En savoir plus ============== - \#sulci sur freenode - https://github.com/yohanboniface/sulci/ ---- Remerciements ============= Libé, bien sûr, laboratoire permanent Jérôme Petazzoni, optimisateur garanti sans gaz à effet de serre ---- Liens ===== - algo Porter dans le projet snowball - algo Porter par Fabien Poulard - pointwise mutual information - adaptation du catégoriseur de Brill au français et modification de l'approche