Classification Bayésienne Naïve avec Oracle

Les récentes campagnes concernant le maintien de la Grande Bretagne dans l’UE ou celle des primaires aux Etats-Unis ont mis sur le devant de la scène de nouvelles méthodes de détermination des tendances de votes. Ces dernières se basent en grand partie sur les informations échangées via les réseaux sociaux – Twitter notamment – et sont connues sous le nom d’Analyse de Sentiment ou Opinion Mining.

Leur principe consiste à exploiter une information non-structurée – du texte généralement – pour déterminer des nuances (pour/contre, triste/neutre/joyeux etc…).

On imagine aisément la complexité de ce type d’analyse dans la mesure où il y a une infinité de manière d’exprimer un avis dans un texte.

Une des techniques employées pour réaliser ce genre d’analyse – la Classification Naïve Bayésienne – se base, comme son nom l’indique, sur l’inférence Bayésienne. Cette technique est dite naïve car elle repose sur une simplification majeure – à savoir, l’indépendance des mots au sein du texte. C’est bien entendu faux mais en pratique, cela marche quand même! On trouve sur internet de nombreuses discussions & études expliquant pourquoi ce n’est pas vraiment problématique.

Toujours est-t ‘il que cette simplification autorise la représentation du texte sous la forme d’un sac de mots. Sous cette forme, seules les fréquences relatives d’apparition des termes sont importantes pour le classifieur. Il existe aussi des variantes basées sur des n-grams (groupes de n mots).

A titre d’exemple, dans ce billet, je me suis intéressé à la détermination du genre littéraire d’un livre à partir du résumé généralement présenté en quatrième de couverture. Pour cela, j’ai réalisé un peu de web-scrapping à partir du site de l’excellent éditeur Decitre.

Cela m’a permis de collecter environ 21000 résumés de livres dans trois genre différents: Littérature Sentimentale, Polar et Philosophie-Sociologie.

On peut comprendre le fonctionnement du classifieur à partir d’un exemple simple de faible dimension. Imaginons par exemple que l’on essaie de déterminer le genre d’un livre dont l’extrait contient les 3 termes suivants: « meurtre », « romance », « enquête ».

La formule du théorème de Bayes nous dit P(A|B)=P(B|A)P(A)/P(B)

On calcule d’abord les probabilités « à priori » de chaque classe P(A) dans notre jeu d’apprentissage. Ici, considérons les valeurs suivantes:

  • P(sentimental)=0.33
  • P(polar)=0.5
  • P(philo-socio)=0.17

Puis on calcule, à partir du jeu d’apprentissage, la probabilité des mots (unigrammes) dans chaque classe:

  • P(meutre|sentimental)=0.1
  • P(meutre|polar)=0.9
  • P(meutre|philo-socio)=0.05
  • P(romance|sentimental)=0.8
  • P(romance|polar)=0.3
  • P(romance|philo-socio)=0.6
  • P(enquête|sentimental)=0.05
  • P(enquête|polar)=0.9
  • P(enquête|philo-socio)=0.4

Ce calcul est souvent accompagné d’une correction de Laplace pour éviter l’effet de l’absence d’un terme dans une catégorie.

On peut ensuite calculer la vraisemblance P(B|A) pour chaque classe. Celle-ci se simplifie en un produit des probabilités des unigrammes en raison de l’hypothèse d’indépendance des termes (aspect « Naïf » de la méthode):

  • P(meurtre, romance, enquête|sentimental) = P(meutre|sentimental)*P(romance|sentimental)*P(enquête|sentimental)=0.1*0.8*0.05=0.004
  • P(meurtre, romance, enquête|polar)=P(meutre|polar)*P(romance|polar)*P(enquête|polar)=0.9*0.3*0.9=0.243
  • P(meurtre, romance, enquête|philo-socio)=P(meutre|philo-socio)*P(romance|philo-socio)*P(enquête|philo-socio)=0.05*0.6*0.4=0.012

On arrive enfin à l’application du théorème de Bayes pour le calcul des probabilités à posteriori P(A|B):

  • P(sentimental|meurtre, romance, enquête) = P(meurtre, romance, enquête|sentimental)*P(sentimental)/P(meurtre, romance, enquête)=0.00132/P(meurtre, romance, enquête)
  • P(polar|meurtre, romance, enquête)=P(meurtre, romance, enquête|polar)*P(polar)/P(meurtre, romance, enquête)=0.1215/P(meurtre, romance, enquête)
  • P(philo-socio|meurtre, romance, enquête)=P(meurtre, romance, enquête|philo-socio)*P(philo-socio)/P(meurtre, romance, enquête)=0.00204/P(meurtre, romance, enquête)

Le dénominateur étant le même entre les 3 expressions, leur comparaison est possible sur la base du numérateur. On voit ainsi clairement que la probabilité que l’extrait provienne d’un polar est la plus importante.

C’est sur ce principe que la classification est réalisée.

A noter que lors d’une implémentation informatique, on passera par une transformation logarithmique pour changer les multiplications en additions et ainsi éviter le problème d’underflow qui survient lors de la multiplication d’un grand nombre de faibles probabilités.

Voilà, pour le principe général…

Passons maintenant à la mise en pratique à grande échelle!

Préparation des données

Les données sont accessibles ici au format Table Externe DataPump: DECITRE_EXTTAB

SQL> CREATE OR REPLACE DIRECTORY D1 AS 'C:\RTI\Tmp';

Directory created.

SQL>
SQL> DROP TABLE decitre_exttab_dp;

Table dropped.

SQL>
SQL> CREATE TABLE decitre_exttab_dp
  2  (
  3     categorie   VARCHAR2 (50),
  4     extrait     VARCHAR2 (4000)
  5  )
  6  ORGANIZATION EXTERNAL
  7     (TYPE oracle_datapump
  8           DEFAULT DIRECTORY D1
  9           LOCATION ('decitre_exttab.dp'));

Table created.

SQL>
SQL>   SELECT categorie, COUNT (*)
  2      FROM decitre_exttab_dp
  3  GROUP BY categorie;

CATEGORIE                                            COUNT(*)
-------------------------------------------------- ----------
litterature-sentimentale                                 7251
polar                                                    8140
philo-socio                                              6141

SQL>
SQL>

L’ensemble est ensuite divisé en un jeu d’apprentissage (75%) et un jeu de test (25%). A noter l’emploi de la clause SAMPLE qui permet un échantillonnage aléatoire des données ainsi que l’ajout d’une colonne IDENTITY qui permet de valoriser simplement une clé primaire:

SQL> DROP TABLE decitre_books;

Table dropped.

SQL>
SQL> CREATE TABLE decitre_books
  2  AS
  3     SELECT * FROM decitre_exttab_dp;

Table created.

SQL>
SQL> ALTER TABLE decitre_books
  2     ADD book# NUMBER GENERATED AS IDENTITY;

Table altered.

SQL>
SQL> ALTER TABLE decitre_books
  2     ADD CONSTRAINT pk_decitre_books PRIMARY KEY (book#);

Table altered.

SQL>
SQL> DROP TABLE train_set_book# PURGE;

Table dropped.

SQL>
SQL> CREATE TABLE train_set_book#
  2  AS
  3     SELECT book#
  4       FROM decitre_books SAMPLE (75);

Table created.

SQL>
SQL>
SQL> CREATE OR REPLACE VIEW train_set_books
  2  AS
  3     SELECT categorie, extrait
  4       FROM decitre_books NATURAL JOIN train_set_book#;

View created.

SQL>
SQL> CREATE OR REPLACE VIEW test_set_books
  2  AS
  3     SELECT categorie, extrait
  4       FROM decitre_books
  5      WHERE book# NOT IN (SELECT book#
  6                            FROM train_set_book#);

View created.

SQL>
SQL>

Le classifieur Bayésien requiert la connaissance de probabilités « à priori » – c’est à dire, une idée de la fréquence des divers groupes dans la population générale.

Ici, on estime ces probabilités en mesurant la fréquence de chaque groupe au sein de notre échantillon d’apprentissage:

SQL>   SELECT categorie,
  2           COUNT (*),
  3           ratio_to_report (COUNT (*)) OVER (PARTITION BY NULL) pct
  4      FROM train_set_books
  5  GROUP BY categorie;

CATEGORIE                                            COUNT(*)        PCT
-------------------------------------------------- ---------- ----------
polar                                                    6107 .379222553
litterature-sentimentale                                 5419 .336500248
philo-socio                                              4578 .284277198

SQL>

Ces résultats vont être stockés dans une table (au format imposé) qui sera utilisée ultérieurement par Oracle Data Miner:

SQL> CREATE TABLE BOOKS_NB_PRIORS
  2  (
  3     target_value        VARCHAR2 (100),
  4     prior_probability   NUMBER
  5  );

Table created.

SQL>
SQL> INSERT INTO BOOKS_NB_PRIORS (target_value, prior_probability)
  2       SELECT categorie, ratio_to_report (COUNT (*)) OVER (PARTITION BY NULL) pct
  3         FROM train_set_books
  4     GROUP BY categorie;

3 rows created.

SQL>

Analyse lexicale

L’étape suivante consiste à constituer le sac de mots à partir des extraits d’apprentissage. Oracle Data Miner s’appuie pour cela sur l’option Oracle Text qui offre des possibilités de découpage (tokenization) et de racinisation (stemming) linguistique:

SQL> column comp_name format a20
SQL> column status format a15
SQL> column version format a15
SQL> 
SQL> SELECT comp_name, version, status
  2    FROM dba_registry
  3   WHERE comp_id = 'CONTEXT';

COMP_NAME            VERSION         STATUS
-------------------- --------------- ---------------
Oracle Text          12.1.0.2.0      VALID

SQL>

On va donc définir – en créant une POLICY – les propriétés de l’analyse lexicale que l’on souhaite appliquer aux données:

  • Suppression des mots vides

Les « mots vides » sont des mots très commun (comme des articles par exemple) qu’il convient de supprimer de l’analyse. Oracle propose une liste prédéfinie mais celle-ci a été enrichie à partir de sources trouvées sur internet: http://www.ranks.nl/stopwords/french

Les données sont accessibles ici (au format Table Externe DataPump): MOTSVIDES_EXTTAB

On peut alors crée une STOPLIST à partir de ces mots:

SQL> CREATE TABLE motsvides_exttab_dp
  2  (
  3     mot   VARCHAR2 (30)
  4  )
  5  ORGANIZATION EXTERNAL
  6     (TYPE oracle_datapump
  7           DEFAULT DIRECTORY D1
  8           LOCATION ('motsvides_exttab.dp'));

Table created.

SQL>
SQL>   SELECT mot
  2      FROM motsvides_exttab_dp 
  3  FETCH FIRST 10 ROWS ONLY;

MOT
------------------------------
duquel
etc
peu
tienne
eu
avoir
t
est
faites
sans

10 rows selected.

SQL>
SQL> BEGIN
  2     ctx_ddl.create_stoplist (stoplist_name   => 'BOOKS_STOPLIST',
  3                              stoplist_type   => 'BASIC_STOPLIST');
  4
  5     FOR rec IN (SELECT mot
  6                   FROM motsvides_exttab_dp)
  7     LOOP
  8        ctx_ddl.add_stopword (stoplist_name   => 'BOOKS_STOPLIST',
  9                              stopword        => rec.mot);
 10     END LOOP;
 11  END;
 12  /

PL/SQL procedure successfully completed.

SQL>
SQL> COMMIT;

Commit complete.

SQL>
  • Mécanique de tokenization

On va découper le texte en mot en se servant des espaces comme séparateurs. C’est la méthode par défaut qui est employé par le BASIC_LEXER.

  • Racinisation

On va procéder à une racinisation des mots de manière associer les différentes dérivations d’un même terme. Par exemple, les mots « suis », « étais », « seras » seront associés au verbe « être ».

Cette racinisation sera basée sur la langue Française.

Elle est contrôlée par le paramètre STEMMER de l’attribut WORDLIST.

Ces divers paramétrages sont fédérés au sein d’une POLICY créée à l’aide de la procédure ctx_ddl.create_policy.

SQL> BEGIN
  2     ctx_ddl.create_preference ('BOOKS_NB_LEXER', 'BASIC_LEXER');
  3     ctx_ddl.create_preference ('BOOKS_NB_WORDLIST', 'BASIC_WORDLIST');
  4     ctx_ddl.set_attribute ('BOOKS_NB_WORDLIST', 'STEMMER', 'FRENCH');
  5
  6     ctx_ddl.create_policy (policy_name   => 'BOOKS_NB_POLICY',
  7                            lexer         => 'BOOKS_NB_LEXER',
  8                            stoplist      => 'BOOKS_STOPLIST',
  9                            wordlist      => 'BOOKS_NB_WORDLIST');
 10  END;
 11  /

PL/SQL procedure successfully completed.

SQL>

Création du modèle

On passe ensuite à la création du modèle. Pour cela, on créée une table de paramétrage qui contient les éléments de configuration:

SQL> CREATE TABLE books_nb_settings
  2  (
  3     setting_name    VARCHAR2 (30),
  4     setting_value   VARCHAR2 (4000)
  5  );

Table created.

SQL>
SQL> BEGIN
  2     INSERT INTO books_nb_settings
  3             VALUES (
  4                       DBMS_DATA_MINING.algo_name,
  5                       DBMS_DATA_MINING.algo_naive_bayes);
  6
  7     INSERT INTO books_nb_settings
  8          VALUES (DBMS_DATA_MINING.prep_auto, DBMS_DATA_MINING.prep_auto_on);
  9
 10     INSERT INTO books_nb_settings
 11          VALUES (DBMS_DATA_MINING.odms_text_policy_name, 'BOOKS_NB_POLICY');
 12
 13     INSERT INTO books_nb_settings
 14          VALUES (DBMS_DATA_MINING.CLAS_PRIORS_TABLE_NAME, 'BOOKS_NB_PRIORS');
 15
 16     COMMIT;
 17  END;
 18  /

PL/SQL procedure successfully completed.

SQL> COMMIT;

Commit complete.

SQL>

On y spécifie l’algorithme utilisé (Bayes Naïf), la table des probabilités à priori (BOOKS_NB_PRIORS), le nom de la policy Oracle Text (BOOK_NB_POLICY) et on active la préparation automatique des données (point important pour l’application de la POLICY).

Le modèle est ensuite généré en précisant le recours à une transformation afin que le champ EXTRAIT subissent une transformation de type TEXTE. Au sein de cette transformation, on indique le nombre de features à utiliser dans le modèle. On pourrait aussi y indiquer le type de tokenization et le nom de la policy si cela n’avait pas été fait en amont (‘TEXT(POLICY_NAME:BOOKS_NB_POLICY)(TOKEN_TYPE:NORMAL)(MAX_FEATURES:1000)’):

SQL> DECLARE
  2     xformlist   DBMS_DATA_MINING_TRANSFORM.transform_list;
  3  BEGIN
  4     DBMS_DATA_MINING_TRANSFORM.set_transform (
  5        xform_list           => xformlist,
  6        attribute_name       => 'EXTRAIT',
  7        attribute_subname    => NULL,
  8        expression           => 'EXTRAIT',
  9        reverse_expression   => NULL,
 10        attribute_spec       => 'TEXT(MAX_FEATURES:1000)');
 11     DBMS_DATA_MINING.create_model (
 12        model_name            => 'BOOKS_NB',
 13        mining_function       => DBMS_DATA_MINING.classification,
 14        data_table_name       => 'TRAIN_SET_BOOKS',
 15        case_id_column_name   => NULL,
 16        target_column_name    => 'CATEGORIE',
 17        settings_table_name   => 'BOOKS_NB_SETTINGS',
 18        xform_list            => xformlist);
 19  END;
 20  /

PL/SQL procedure successfully completed.

SQL>
SQL>

Scoring

On peut alors réaliser le scoring du modèle sur l’échantillon de test. Les résultats sont présentés dans la matrice de confusion ci-après:

SQL> column cat_reelle format a25
SQL>   SELECT *
  2      FROM (SELECT categorie AS cat_reelle,
  3                   PREDICTION (BOOKS_NB USING *) AS cat_predict
  4              FROM test_set_books)
  5           PIVOT
  6              (COUNT (*)
  7              FOR cat_predict
  8              IN ('litterature-sentimentale' litterature_sentimentale,
  9                 'philo-socio' philo_socio,
 10                 'polar' polar))
 11  ORDER BY 1;

CAT_REELLE                LITTERATURE_SENTIMENTALE PHILO_SOCIO      POLAR
------------------------- ------------------------ ----------- ----------
litterature-sentimentale                      1538           6        288
philo-socio                                      4        1504         55
polar                                          155          25       1853

SQL>

Le taux de réussite de prédiction du modèle est de 90% [(1538+1504+1853)/5428] – ce qui est très bon.

On peut encore l’améliorer en augmentant le nombre de features. Ici on passe de 1000 à 3000 features:

SQL> BEGIN
  2     DBMS_DATA_MINING.drop_model ('BOOKS_NB');
  3  END;
  4  /

PL/SQL procedure successfully completed.

SQL>
SQL>
SQL> DECLARE
  2     xformlist   DBMS_DATA_MINING_TRANSFORM.transform_list;
  3  BEGIN
  4     DBMS_DATA_MINING_TRANSFORM.set_transform (
  5        xform_list           => xformlist,
  6        attribute_name       => 'EXTRAIT',
  7        attribute_subname    => NULL,
  8        expression           => 'EXTRAIT',
  9        reverse_expression   => NULL,
 10        attribute_spec       => 'TEXT(MAX_FEATURES:3000)');
 11     DBMS_DATA_MINING.create_model (
 12        model_name            => 'BOOKS_NB',
 13        mining_function       => DBMS_DATA_MINING.classification,
 14        data_table_name       => 'TRAIN_SET_BOOKS',
 15        case_id_column_name   => NULL,
 16        target_column_name    => 'CATEGORIE',
 17        settings_table_name   => 'BOOKS_NB_SETTINGS',
 18        xform_list            => xformlist);
 19  END;
 20  /

PL/SQL procedure successfully completed.

SQL>   SELECT *
  2      FROM (SELECT categorie AS cat_reelle,
  3                   PREDICTION (BOOKS_NB USING *) AS cat_predict
  4              FROM test_set_books)
  5           PIVOT
  6              (COUNT (*)
  7              FOR cat_predict
  8              IN ('litterature-sentimentale' litterature_sentimentale,
  9                 'philo-socio' philo_socio,
 10                 'polar' polar))
 11  ORDER BY 1;

CAT_REELLE                LITTERATURE_SENTIMENTALE PHILO_SOCIO      POLAR
------------------------- ------------------------ ----------- ----------
litterature-sentimentale                      1609           2        221
philo-socio                                      3        1512         48
polar                                          134          10       1889

SQL>

Le taux de réussite de prédiction du modèle augmente à 92.3% [(1609+1512+1889)/5428].

L’ensemble des paramètres utilisés par le modèles sont accessibles via les vues:

  • dba_mining_models/dba_mining_model_settings/dba_mining_model_tables/dba_mining_model_attributes pour la partie ODM
  • ctx_user_indexes/ctx_user_index_objects/ctx_user_index_values/ctx_user_preferences/ctx_user_preference_values/ctx_user_stoplists/ctx_user_stopwords pour la partie d’analyse textuelle (vues Oracle Text)

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

÷ one = four