Natural Language Processing avec des petits morceaux de NodeJS dedans

A l’ère des voitures qui se conduisent toutes seules, des plantes qui peuvent réclamer à boire, des maisons intelligentes, les humains restent de grands incompris. Les pseudo intelligences artificielles présentes sur le marché à l’heure actuelle atteignent rapidement leurs limites et ne sont souvent basées que sur un système de réaction à certains mots-clés. Or il ne suffit pas de reconnaître un mot ou même un groupe de mots pour appréhender le sens d’un discours. Les modes d’expression humains au sens large (langues parlées ou écrites, mais aussi les langues signées, le mime, le dessin, voire même la musique) présentent une complexité jamais égalée par aucun système informatique.

Répondre à cette problématique est l’objectif du NLP ou Natural Language Processing, le traitement du langage naturel. Il regroupe deux domaines spécifiques : le NLU (Natural Language Understanding), qui vise à comprendre le langage naturel, et le NLG (Natural Language Generation), qui permet à une machine de s’exprimer de manière naturelle pour un être humain. Ces concepts sont à la base de toutes les intelligences artificielles conversationnelles, comme Siri, Ok Google ou encore Cortana pour ne donner que ces trois exemples. C’est ce qui donne à un robot la capacité de mener une vraie conversation.

Les concepts de base

Les nombreuses études déjà réalisées, tant en NLP qu’en linguistique pure, s’accordent toutes plus ou moins sur un schéma relativement similaire. Il est ainsi possible de modéliser une méthode, applicable aussi bien au NLU qu’au NLG, constituée d’étapes successives.

La tokenisation

Cette analyse a pour but de découper le texte en plusieurs tokens. Les tokens sont les éléments porteurs de sens les plus simples. Cette étape présente évidemment son lot de difficultés. Il serait en effet tentant d’utiliser un simple découpage en mots graphiques, c’est-à-dire de séparer les mots en fonction des espaces présents entre eux. Mais qu’en est-t-il alors des mots séparés par des apostrophes ou des tirets ? La phrase « va-t’en » comprend par exemple trois tokens distincts, mais aucun espace. À l’inverse, le mot « aujourd’hui » même s’il contient une apostrophe, ne constitue qu’un unique token. De la même manière, « fruit de la passion » doit être considéré comme un seul élément porteur de sens même si il est constitué de plusieurs mots graphiques ; en considérant les mots séparément, on perdrait l’information sémantique.

Siri

Cette image donne un bon exemple de mauvaise interprétation des unités de sens. Ici l’élément porteur de sens n’est pas « la femme » mais « la femme de ménage », et cette simple erreur conduit à une compréhension totalement erronée de la demande initiale (si si !).

Le but de la tokenisation est aussi d’apposer des étiquettes à chaque token, en déterminant la catégorie grammaticale à laquelle ils appartiennent. « La » est un article, « licorne » un nom, « danse » un verbe. Cela est compliqué par la forte ambiguïté qui règne dans beaucoup de langues. Ainsi, en français, on estime que 25% du lexique présente une forme ambiguë. Par exemple, « danse » peut être aussi bien utilisé comme nom commun que comme verbe. On peut pallier en partie ce problème en utilisant les probabilités (fréquence d’usage du mot dans chaque catégorie grammaticale possible).

On peut déduire dès ce stade un grand nombre d’informations. Une technique fréquemment utilisée est la lemmatisation. Chaque mot est considéré comme une composition de morphèmes (ou unités minimales de sens) : un ou des mots racines, ou lemmes, et des sous-mots, ou flexions (typiquement des préfixes ou suffixes apposés au lemme). Le mot « invisible », par exemple, est composé de trois morphèmes : in- vis- ible, « voir » étant le lemme du mot. On peut alors déterminer comment le lemme a été altéré et pourquoi. Ici par exemple, le préfixe « in » apporte une connotation négative au verbe voir tandis que le suffixe « ible » exprime la capacité à faire quelque chose. On peut également déterminer le temps et la personne d’un verbe conjugué, le genre et le nombre d’un adjectif ou d’un nom, etc.

A l’issue de cette analyse, on obtient donc des tokens et leurs méta-données. De la phrase « La licorne danse. », on peut ainsi déduire les trois tokens suivants :

La licorne danse
catégorie : article
genre: féminin
nombre : singulier
catégorie : nom
genre : féminin
nombre : singulier
lemme: licorne
catégorie : verbe
mode : indicatif
temps : présent
personne : 3ème du singulier
lemme : danser

L’analyse syntaxique

Cette étape permet de dégager une représentation de la structure d’un texte, de manière à mettre en lumière les relations syntaxiques entre les mots. Cette étape se base sur un dictionnaire (le vocabulaire) et sur un ensemble de règles syntaxiques (la grammaire), pour déterminer les syntagmes, ou constituants, présents dans la phrase et les organiser selon leur hiérarchie dans la phrase. Prenons un exemple de phrase simple : « Le stagiaire a cassé l’Internet. ». L’analyse syntaxique permet de déterminer l’arbre suivant :

Hiérarchie syntaxique

Les abréviations utilisées sont les suivantes :

  • S : phrase (sentence) ;
  • NP : syntagme nominal (noun phrase) ;
  • VP : syntagme verbal (verbal phrase) ;
  • Det : déterminant.

Sont ainsi modélisés chacun des syntagmes qui composent la phrase, ainsi que leurs relations entre eux. On va dès lors pouvoir valider la syntaxe de la phrase, en définissant au préalable les différentes structures possibles. Avec une phrase de ce type, cela est relativement simple, mais il en va tout autrement pour des phrases plus complexes, composées de propositions multiples, ou comportant des ambiguïtés. Dans la phrase « J’ai rencontré une professeure de boxe française. », il est impossible de savoir si l’adjectif française se rapporte à la boxe ou à la professeure.

Il existe de très nombreuses manières différentes de procéder à cette analyse, qu’il serait trop long de détailler ici. Sachez cependant qu’on peut regrouper ces méthodes en deux catégories : les analyses avec contexte ou hors contexte, ces dernières étant plus simples à mettre en place mais bien moins précises.

L’analyse sémantique

Son rôle est double. Elle se compose en effet de deux concepts distincts : la sémantique grammaticale et la sémantique lexicale.

La sémantique grammaticale consiste à associer un rôle grammatical à chacun des syntagmes définis lors de l’analyse syntaxique. Il s’agit par exemple des fameux COD et COI que l’on a tous appris dans notre enfance (et oubliés depuis). Si nous reprenons notre exemple précédent, légèrement modifié, nous obtenons le découpage suivant :

  • Le stagiaire => sujet ;
  • a cassé => verbe ;
  • l’Internet => COD (complément d’objet direct) ;
  • la semaine dernière => CC (complément circonstanciel).

Il s’agit ici d’une phrase très simple, sujet-verbe-complément, mais qui peut très rapidement se complexifier (Le stagiaire de troisième a négligemment cassé l’Internet il y a 3 jours en cherchant Google sur Google.)

On va ensuite associer aux syntagmes une représentation de leur sens au sein de la phrase. Notre exemple précédent pourra par exemple être analysé ainsi :

  • Le stagiaire => agent ;
  • a cassé => action ;
  • l’Internet => objet ;
  • la semaine dernière => moment.

Quant à la sémantique lexicale, elle s’attache au sens des mots eux-mêmes. Il faut donc revenir à nos tokens, tout en tenant compte de tous les résultats obtenus par les analyses qui ont suivi. Même si cela paraît évident au premier abord, il suffit de considérer la phrase suivante pour comprendre la complexité du problème : « Ma religion m’interdit de manger ou embaucher un avocat. », ou encore de se demander à quoi correspond le mot « il » dans les deux phrases « Le chat se passe la patte derrière l’oreille, il se lave. » et « Le chat se passe la patte derrière l’oreille, il va pleuvoir. ».

L’analyse pragmatique

En dernier lieu, l’analyse pragmatique permet d’interpréter le discours à son niveau le plus élevé. Cette interprétation peut dépendre du contexte immédiat ou d’une connaissance plus globale.

Au boulot

Fort de ces connaissances, nous allons donc tenter de réaliser un programme concret. Pour cet exemple, je vous propose de créer une simple interface capable d’analyser les tweets qui contiennent certains mots-clés.

Pour ne pas tenter de réinventer la roue, j’ai recensé un certain nombre de librairies capables d’effectuer les analyses présentées plus haut. Une des plus abouties à l’heure actuelle est celle de l’université de Stanford : CoreNLP. Très puissante, elle permet d’obtenir rapidement des résultats assez impressionnants.

Interface en ligne de Stanford CoreNLP

Elle est développée en Java, mais il existe des wrappers pour l’utiliser avec pratiquement n’importe quel langage. Vous pouvez l’essayer en ligne ici. Pour simplifier les choses, nous allons nous intéresser seulement au NLP en anglais. Même si la librairie permet d’analyser le français, elle est bien plus efficace avec la langue de Shakespeare.

La première chose à faire est de télécharger et d’installer la librairie CoreNLP. Elle est disponible à cette adresse. Le fichier contient des jars avec le code de la librairie elle-même et les différents modèles utilisés pour le parsing de l’anglais. A noter que ce sont ces derniers qu’il faudra changer pour analyser du texte écrit en français. S’y trouvent également la documentation et le code source du projet. Une fois le fichier dé-zippé, il suffit de lancer le jar principal depuis le dossier de la librairie avec la commande java -mx4g -cp "*" edu.stanford.nlp.pipeline.StanfordCoreNLPServer.

Et voilà ! Vous pouvez d’ores et déjà tester la librairie dans votre navigateur à l’adresse http://localhost:9000/. Vous pouvez vous amuser à essayer différents annotateurs de la liste pour voir les résultats obtenus.

Nous utiliserons Node.js avec Express et Jade afin d’avoir rapidement une application simple. Commençons par installer les dépendances nécessaires.

npm i express body-parser twitter-node-client

Express est un micro-framework pour Node.js qui permet entre autres de gérer les urls et d’utiliser des templates. BodyParser est un simple outil qui permet comme son nom l’indique de parser le corps de la requête. Jade est un moteur de templates performant et rapide à prendre en main. Enfin, nous utiliserons Twitter node client pour faire appel à l’API de Twitter.

Il nous faut ensuite créer le module serveur, un simple fichier server.js à la racine du projet.

"use strict";

var express = require('express');
var bodyParser = require('body-parser');

const PORT = 8080;

var app = express();
app.set('views', './views');
app.set('view engine', 'jade');
app.use(bodyParser.urlencoded({ extended: false}));
var Twitter = require('twitter-node-client').Twitter;

app.all('/', (req, res) => {

  if (req.method == 'POST') {

    let keywords = req.body.keywords;
    let nb = req.body.nbTweets;
    
    var twitterClient = new Twitter({
      consumerKey: 'CONSUMER_KEY',
      consumerSecret: 'CONSUMER_SECRET',
      accessToken: 'ACCESS_TOKEN',
      accessTokenSecret: 'ACCESS_TOKEN_SECRET'
    });

    // We build the twitter query
    const query = {
      q: keywords + " -filter:retweets -filter:links -filter:images",
      count: nb,
      lang: 'en'
    };

    var tweets = twitterClient.getSearch(query, (err, response, body) => {
      throw err;
    }, (data) => {
      let tweets = JSON.parse(data).statuses.map((tweet) => {
        return tweet.text;
      });
      res.render('home',
      {
        results: tweets,
        keywords: keywords,
        nbTweets: nb,
      });
    });
  } else {
    res.render('home',
    {
      keywords: '',
      nbTweets: 15,
    });
  }
});

app.listen(PORT);

Rien de bien compliqué ici. L’application est initialisée puis, dans le cas d’une requête POST, les données du formulaire sont récupérées. Nous créons alors un simple client Twitter en passant en paramètre les différentes clés nécessaires (que vous pouvez récupérer ici), puis nous appelons l’api de Twitter avec une query simple. Les filtres ajoutés permettent d’ignorer les tweets contenant des images ou des urls, sans intérêt pour nous, ainsi que les retweets qui feraient doublon. Le template est ensuite rendu.

C’est ce template qu’il va nous falloir créer ensuite, dans un sous-dossier views. Il va afficher deux champs de texte, un pour les mots-clés à rechercher et un autre pour le nombre de tweets que l’on veut récupérer, ainsi qu’une liste avec les résultats (pour l’instant juste le contenu des tweets), J’ai ajouté Bootstrap via un CDN pour avoir quelque chose d’un peu plus joli. Rien de bien compliqué non plus donc.

mixin showResult(result)
  li.list-group-item
    p= result

html
  head
    link(href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css', rel='stylesheet')
  body
    div.container
      h1 Twitter NLP
      form(method='post', action='/')
        div.form-group
          label.form-label Entrez les mots-clés à rechercher
          input.form-control(
            required=true,
            name='keywords',
            value="#{keywords}"
          )
        div.form-group
          label.form-label Combien de tweets voulez-vous analyser ?
          input.form-control(
            type="number",
            required=true
            name='nbTweets',
            value="#{nbTweets}"
          )
        button.btn(type='submit') Analyser
      h2 Résultats
      ul.list-group
        each result in results
          +showResult(result)

Nous pouvons déjà faire un premier test. En ouvrant le navigateur sur http://localhost:8080/ (ou le port que vous avez choisi), nous obtenons bien une liste de tweets contenant les mots-clés spécifiés. Passons aux choses sérieuses.

Créons un deuxième fichier, nlp.js qui va nous permettre d’effectuer l’analyse.

"use strict";

const execSync = require('child_process').execSync;

module.exports = {
    parse: (text, port, annotators, format) => {
      let output = execSync('output=$(wget --post-data ' + '"' + text + '" ' + '"localhost:' + port + '/?properties={"annotators": ' + '"' + annotators + '", "outputFormat": "' + format + '"' + '}" -qO -) && echo $output',{ encoding: 'utf8' });
      output = output.replace(/(\r\n|\n|\r)/gm, ''); // Need to fix JSON response
      return output;
    },
};

Le fichier contient une seule méthode simple. Une commande en asynchrone va interroger notre serveur CoreNLP et lui demander d’analyser le texte fourni, ici le contenu du tweet. C’est donc la ligne suivante qui nous intéresse :

let output = execSync('output=$(wget --post-data '
+ '"' + text + '" ' + '"localhost:' + port
+ '/?properties={"annotators": ' + '"' + annotators + '", "outputFormat": "'
+ format + '"' + "}' -qO -) && echo $output', { encoding: 'utf8' });

Et plus particulièrement le paramètre « properties ». Via ce dernier, nous spécifions le format de sortie à utiliser, mais surtout les annotateurs. Mais nous y reviendrons plus tard.

Nous pouvons maintenant modifier notre server.js pour faire appel à notre nouveau module, en adaptant le premier callback de la recherche de la manière suivante :

(data) => {
  let tweets = JSON.parse(data).statuses.map((tweet) => {
    let text = tweet.text;
    const output = nlp.parse(text, 9000, "ssplit,parse", "json");
    return {
      text: text,
      output: output
    };
  });
  res.render('home',
  {
    results: tweets,
     keywords: keywords,
     nbTweets: nb,
   });
 }

Et la mixin showResult du template, adaptée pour afficher le résultat :

mixin showResult(result)
  li.list-group-item
    p= result.text
    p= result.output

Il est possible que le JSON récupéré lors de l’appel au serveur soit mal formé, c’est un bug connu de la version 3.6.0 de CoreNLP qui ajoute des sauts de ligne invalides à la réponse. Un simple output = output.replace(/(\r\n|\n|\r)/gm, ''); avant le return réglera le problème.

À mon premier essai, CoreNLP a crashé avec le message suivant AVERTISSEMENT: Untokenizable: ? (U+D83D, decimal: 55357). Un des tweets n’a pas pu être parsé à cause de la présence d’un caractère non tokenizable. Ce caractère, vous le connaissez bien, c’est l’emoji 😄. Nous allons donc nous débarrasser des emojis et autres caractères non gérés en utilisant une regex simple avant l’analyse : text = text.replace(/[^a-zA-Z0-9\s\p{P}]/g, '');. De cette manière, seuls le texte, les chiffres et la ponctuation sont conservés. Profitons-en pour éliminer les usernames twitter dans les messages avec une autre regex : text = text.replace(/(^|[^@\w])@(\w{1,15})\b/g, '');

Finalement, un premier résultat est obtenu :

Résultat

Un peu illisible à première vue, mais nous allons voir ce qu’on peut en tirer.

Reprenons le résultat brut :

{
    "sentences": [
        {
            "index": 0,
            "parse": "(ROOT (S (CC And) (NP (PRP you)) (ADVP (RB just)) (VP (VBP need) (S (VP (TO to) (VP (VB let) (S (NP (DT that) (NN pony)) (VP (VB go)))))))))",
            "basic-dependencies": [
                {
                    "dep": "ROOT",
                    "governor": 0,
                    "governorGloss": "ROOT",
                    "dependent": 4,
                    "dependentGloss": "need"
                },
                {
                    "dep": "cc",
                    "governor": 4,
                    "governorGloss": "need",
                    "dependent": 1,
                    "dependentGloss": "And"
                },
                ...
            ],
            "collapsed-dependencies": [
                {
                    "dep": "ROOT",
                    "governor": 0,
                    "governorGloss": "ROOT",
                    "dependent": 4,
                    "dependentGloss": "need"
                },
                {
                    "dep": "cc",
                    "governor": 4,
                    "governorGloss": "need",
                    "dependent": 1,
                    "dependentGloss": "And"
                },
                ...
            ],
            "collapsed-ccprocessed-dependencies": [
                {
                    "dep": "ROOT",
                    "governor": 0,
                    "governorGloss": "ROOT",
                    "dependent": 4,
                    "dependentGloss": "need"
                },
                {
                    "dep": "cc",
                    "governor": 4,
                    "governorGloss": "need",
                    "dependent": 1,
                    "dependentGloss": "And"
                },
                ...
            ],
            "tokens": [
                {
                    "index": 1,
                    "word": "And",
                    "originalText": "And",
                    "lemma": "and",
                    "characterOffsetBegin": 0,
                    "characterOffsetEnd": 3,
                    "pos": "CC",
                    "before": "",
                    "after": " "
                },
                {
                    "index": 2,
                    "word": "you",
                    "originalText": "you",
                    "lemma": "you",
                    "characterOffsetBegin": 4,
                    "characterOffsetEnd": 7,
                    "pos": "PRP",
                    "before": " ",
                    "after": " "
                },
                {
                    "index": 3,
                    "word": "just",
                    "originalText": "just",
                    "lemma": "just",
                    "characterOffsetBegin": 8,
                    "characterOffsetEnd": 12,
                    "pos": "RB",
                    "before": " ",
                    "after": " "
                },
                {
                    "index": 4,
                    "word": "need",
                    "originalText": "need",
                    "lemma": "need",
                    "characterOffsetBegin": 13,
                    "characterOffsetEnd": 17,
                    "pos": "VBP",
                    "before": " ",
                    "after": " "
                },
                {
                    "index": 5,
                    "word": "to",
                    "originalText": "to",
                    "lemma": "to",
                    "characterOffsetBegin": 18,
                    "characterOffsetEnd": 20,
                    "pos": "TO",
                    "before": " ",
                    "after": " "
                },
                {
                    "index": 6,
                    "word": "let",
                    "originalText": "let",
                    "lemma": "let",
                    "characterOffsetBegin": 21,
                    "characterOffsetEnd": 24,
                    "pos": "VB",
                    "before": " ",
                    "after": " "
                },
                {
                    "index": 7,
                    "word": "that",
                    "originalText": "that",
                    "lemma": "that",
                    "characterOffsetBegin": 25,
                    "characterOffsetEnd": 29,
                    "pos": "DT",
                    "before": " ",
                    "after": " "
                },
                {
                    "index": 8,
                    "word": "pony",
                    "originalText": "pony",
                    "lemma": "pony",
                    "characterOffsetBegin": 30,
                    "characterOffsetEnd": 34,
                    "pos": "NN",
                    "before": " ",
                    "after": " "
                },
                {
                    "index": 9,
                    "word": "go",
                    "originalText": "go",
                    "lemma": "go",
                    "characterOffsetBegin": 35,
                    "characterOffsetEnd": 37,
                    "pos": "VB",
                    "before": " ",
                    "after": ""
                }
            ]
        }
    ]
}

Lors de l’appel à l’analyse, nous avions les annotateurs à utiliser : ssplit et parse. ssplit divise un ensemble de tokens en phrases, c’est ce qui nous permet de récupérer un tableau « sentences » dans le résultat. parse est l’annotateur qui va déterminer les relations entre les mots, ou dépendances, et permettre d’analyser la structure de la phrase.

Le JSON récupéré nous donne plusieurs informations. Les différentes phrases sont traitées une à une dans un tableau. Pour chaque phrase, nous obtenons :

  • Son index dans le texte ;
  • Le résultat du parse ;
  • Les dépendances qui représentent les relations grammaticales entre les mots sous trois formes différentes en fonction de la manière dont sont traitées les dépendances impliquant des prépositions ou des conjonctions ;
  • Les tokens.

Un token donne plusieurs informations :

{
  "index": 8,
  "word": "pony",
  "originalText": "pony",
  "lemma": "to",
  "characterOffsetBegin": 30,
  "characterOffsetEnd": 34,
  "pos": "NN",
  "before": " ",
  "after": " "
}

Nous connaissons ainsi pour chaque token son index et sa position dans la phrase ainsi que les caractères qui l’entourent. L’information la plus intéressante ici est donnée par la clé « pos » (pour part-of-speech). Il s’agit de la fonction du token. Les tags utilisés sont des standards définis dans le Penn Treebank Project. Parmi les plus fréquemment rencontrés en anglais, on trouve NN (nom singulier), VB (verbe sous sa forme de base), JJ (adjectif)… Une liste exhaustive est disponible ici.

La clé « parse », elle, donne une représentation visuelle du découpage de la phrase, en utilisant les mêmes tags, ce qui permet de comprendre sa structure.

(ROOT (S (CC And) (NP (PRP you)) (ADVP (RB just)) (VP (VBP need) (S (VP (TO to) (VP (VB let) (S (NP (DT that) (NN pony)) (VP (VB go)))))))))

Enfin les différentes dépendances permettent de construire un arbre des liens entre les mots. Par exemple la dépendance suivante permet de faire le lien entre « let » et « go », indissociables dans le sens mais pourtant séparés dans la phrase (le tag « ccomp » signifie « clausal complement »). La définition de ces dépendances utilisent également des conventions qui sont décrites ici.

{
  "dep":"ccomp",
  "governor":6,
  "governorGloss":"let",
  "dependent":9,
  "dependentGloss":"go"
}

Bonus : l’analyse de sentiments

Pour donner un vrai sens à notre application, nous allons rajouter l’analyse des sentiments exprimés dans les tweets. Nous pourrons ainsi générer des statistiques pour déterminer ce que certains sujets inspirent aux utilisateurs du réseau social.

La version que nous avons téléchargé précédemment de CoreNLP server contient un bug qui empêche le fonctionnement de l’annotateur sentiment. Nous allons donc procéder différemment. Téléchargez la source du projet directement depuis leur Github. Une fois le fichier dézippé, depuis le répertoire du projet, faites :

ant
cd classes
jar -cf ../stanford-corenlp.jar edu

Il faut ensuite télécharger les modèles les plus récents puis les placer à la racine de la librairie. Le serveur se lance depuis le répertoire courant avec la commande :

java -mx4g -cp "stanford-corenlp.jar:stanford-english-corenlp-models-current.jar:lib/*" edu.stanford.nlp.pipeline.StanfordCoreNLPServer

Et voilà, il ne nous reste plus qu’à ajouter l’annotateur « sentiment » dans l’appel au serveur NLP.

const output = nlp.parse(text, 9000, "ssplit,parse,sentiment", "json");

Lors de la recherche des mots-clés, deux nouvelles valeurs apparaissent dans les résultats de l’analyse : « sentimentValue » et « sentiment ». La première est un score entre 0 et 4, 0 étant le sentiment le plus négatif et 4 le plus positif, tandis que le deuxième est la représentation textuelle de ce chiffre (Negative, Neutral, Positive).

Faisons quelques modifications afin de récupérer pour chaque tweet le score de sentiment et l’afficher joliment dans le template :

let tweets = JSON.parse(data).statuses.map((tweet) => {
  let text = tweet.text;
  const output = nlp.parse(text, 9000, "ssplit,parse,sentiment", "json");
  const sentiments = output.sentences.map((sentence) => {
    return sentence.sentimentValue;
  });
  const score = sentiments.reduce((sum, a) => { return sum + a },0)/(sentiments.length||1);
  let color = '#d9edf7';
  if (score > 2) {
    color = '#dff0d8';
  } else if (score 
mixin showResult(result)
  li.list-group-item(style="background-color:#{result.color};")
    p= result.text

Et le résultat :

Résultat

Les résultats semblent assez aléatoires, mais il est possible d’entraîner l’annotateur avec vos propres données, pour un domaine spécifique, par exemple des avis sur des restaurants. L’entraînement de base est fait sur des critiques de film et sera donc plus précis sur ce sujet.

Le code complet est disponible ici.

Aller plus loin

Dans cet article, nous n’avons évoqué la complexité du NLP que selon des critères purement « factuels » comme la syntaxe ou la sémantique, mais derrière le langage humain et la capacité à le comprendre se cachent de nombreuses autres difficultés. Un discours, même le plus simple, n’a de sens que s’il est associé à son contexte culturel, relationnel, émotionnel, et j’en passe. Pour que notre programme soit vraiment intelligent, il faudrait qu’il puisse comprendre quand une question qui lui est posée se rapporte à une référence culturelle par exemple. Cette première difficulté peut en théorie être résolue en procurant à l’IA un accès à une base de données suffisamment exhaustive de références historiques, culturelles, artistiques…

Se pose alors un autre problème, encore plus complexe, la capacité à comprendre et à exprimer des émotions, ou à faire de l’humour, deux choses considérées encore à l’heure actuelle comme l’apanage de l’espèce humaine, ce qui nous différencie des autres animaux. Notre algorithme est dans l’incapacité totale de reconnaître lorsqu’on lui pose une question troll. Il ne peut pas non plus adapter sa réponse à nos attentes émotionnelles. Plusieurs éditeurs d’IA tentent déjà de répondre à ces problématiques. C’est par exemple ce que fait Google en faisant ingurgiter à ses programmes d’énormes quantités de romans à l’eau de rose, pour tenter de leur inculquer le sens des émotions, de la nuance, et ainsi améliorer les interactions en les rendant plus « humaines ». De manière plus générale, les solutions avancées consistent à nourrir l’IA avec des contenus non plus factuels mais que l’on appellera des contenus plus humains à défaut d’un meilleur terme. On peut ainsi imaginer d’injecter dans ses bases de données l’ensemble des tweets de ces dernières années.

Même ainsi, notre IA sera loin d’être parfaite. Considérons par exemple une IA que l’on aurait éduquée en lui fournissant toute la base de données de Reddit, il y a alors fort à parier que notre IA deviendrait… nazie. Une analyse statistique a en effet révélée que 80% des threads Reddit regroupant plus de 1000 commentaires contiennent une référence à Hitler. Bien sûr il s’agit majoritairement de références humoristiques (du moins nous l’espérons !). Cependant, notre IA, bien que connaissant l’histoire de la 2è guerre mondiale dans toute son horreur, sera influencée par ses connaissances de telle sorte qu’elle en viendra naturellement à sembler adhérer elle-même aux idées nazies. C’est exactement ce qui est arrivé à Tay, l’IA de Microsoft, lancée sur Twitter comme une adolescente candide et remise au placard moins de 24 heures plus tard car elle était devenue raciste, misanthrope et xénophobe.

Tay, l'IA devenue fan d'Hitler

Pourtant, nous sommes nous-même soumis quotidiennement à toutes ces références (pour peu d’utiliser Internet et de ne pas vivre dans une cave) sans qu’elles impactent pour autant notre idéologie et nos convictions profondes. Que manque-t-il alors à notre IA pour devenir imperméable à ces influences ? Cette intégrité requiert une alchimie extrêmement subtile entre nos connaissances factuelles (culture, contexte, Histoire) et humaines (humour, sentiments), avec quelques ingrédients en plus. Il s’agit entre autres de valeurs morales, qui nous sont inculquées tout au long de notre vie par notre environnement familial, religieux, scolaire. Il faut donc fournir à l’IA un socle de valeurs, qui doivent rester inébranlables et primer sur toutes celles acquises par la suite. On est ici très proche des trois lois de la robotique énoncées dans les années 1930 par l’auteur de science-fiction Isaac Asimov.

En itérant de cette manière, on se rend compte que la création d’une vraie intelligence nécessite un ensemble de composants interdépendants et en nombre quasi infini. Nous pourrions encore parler de la conscience de soi, de la capacité à adapter nos valeurs, capacité indispensable à la tolérance, de la notion d’empathie, qui va plus loin que la simple compréhension des sentiments… Finalement, les meilleurs programmes de NLP sont à des années lumières de constituer une vraie intelligence.

Et ensuite ?

Vous l’aurez compris, nous sommes encore très loin d’avoir appréhendé tous les aspects de ce qui constitue sans doute un des défis technologiques majeurs de notre époque. Même un récapitulatif exhaustif de toutes les fondations nécessaires à la création d’une IA ne ferait que soulever d’autres difficultés. Les problématiques techniques, bien qu’extrêmement complexes, ne représentent qu’une infime partie du problème.

Il y aurait encore beaucoup à dire sur le sujet, en commençant par évoquer plus en détail l’état de l’art. Si cela vous intéresse, je vous renvoie à cet article de Wikipédia, plutôt exhaustif. Je n’ai volontairement pas évoqué le machine learning, bien qu’il soit absolument indissociable du NLP, car ce domaine est suffisamment fascinant (et surtout complexe) pour mériter son propre article. Si ce sujet en particulier vous intéresse, je vous invite à regarder la librairie TensorFlow, récemment rendue open-source par Google, qui me semble extrêmement prometteuse (bien que je n’ai pas encore eu l’occasion de l’utiliser). En attendant, vous pouvez toujours demander à Siri de vous rappeler les trois lois de la robotique.

Quelques sources en vrac

blog comments powered by Disqus