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)