Classification Textuelle avec Oracle Text #1

Dans un article précédent, Oracle Text a été utilisé pour réaliser un clustering. Ici, c’est une classification supervisée reposant sur l’algorithme de SVM que nous allons mettre en oeuvre. La encore, l’avantage de l’utilisation d’Oracle Text est que cela ne nécessite pas de disposer de la licence Oracle Advanced Analytics.

Pour cela, je vais utiliser un dataset issu de kaggle relatif à un catalogue de revues de dégustation de vins.

Celui-ci est au format csv et j’utilise un table externe pour accéder aux données:

 
SQL> CREATE OR REPLACE DIRECTORY d1 AS '/tmp';

Directory created.

SQL>
SQL> CREATE TABLE wine_dataset_ext
  2  (
  3      id NUMBER,
  4      country VARCHAR2 (50),
  5      description VARCHAR2 (1000),
  6      designation VARCHAR2 (100),
  7      points VARCHAR2 (50),
  8      price VARCHAR2 (50),
  9      province VARCHAR2 (50),
 10      region_1 VARCHAR2 (50),
 11      region_2 VARCHAR2 (50),
 12      taster_name VARCHAR2 (50),
 13      taster_twitter_handle VARCHAR2 (50),
 14      title VARCHAR2 (200),
 15      variety VARCHAR2 (50),
 16      winery VARCHAR2 (100)
 17  )
 18  ORGANIZATION EXTERNAL
 19      (TYPE oracle_loader
 20       DEFAULT DIRECTORY d1
 21       ACCESS PARAMETERS (
 22           RECORDS DELIMITED BY NEWLINE
 23           SKIP 1
 24           FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LRTRIM MISSING FIELD VALUES ARE NULL
 25       )
 26       LOCATION (d1:'winemag-data-130k-v2.csv'))
 27      REJECT LIMIT 10;

Table created.

SQL>

Les enregistrements sont filtrés pour ne conserver que ceux relatifs aux 4 catégories de vin les plus représentées:

 
SQL> CREATE TABLE wine_dataset
  2  AS
  3      SELECT variety, description
  4        FROM wine_dataset_ext
  5       WHERE variety IN (     SELECT variety
  6                                FROM wine_dataset_ext
  7                            GROUP BY variety
  8                            ORDER BY COUNT (*) DESC
  9                         FETCH FIRST 4 ROWS ONLY);

Table created.

SQL>
SQL> ALTER TABLE wine_dataset
  2      ADD wine# NUMBER GENERATED AS IDENTITY;

Table altered.

SQL>
SQL> ALTER TABLE wine_dataset
  2      ADD CONSTRAINT pk_wine_dataset PRIMARY KEY (wine#);

Table altered.

SQL>

Les quatre catégories sont approximativement distribuées de manière équitable (entre 20 et 30%):

 
SQL>   SELECT variety,
  2           ROUND (100 * ratio_to_report (cnt) OVER (PARTITION BY NULL)) pct
  3      FROM (  SELECT variety, COUNT (*) cnt
  4                FROM wine_dataset
  5            GROUP BY variety)
  6  ORDER BY 2 DESC;

VARIETY                                                   PCT
-------------------------------------------------- ----------
Pinot Noir                                                 31
Chardonnay                                                 27
Cabernet Sauvignon                                         22
Red Blend                                                  21

SQL>

On crée une table de lookup pour ces catégories:

 
SQL> CREATE TABLE wine_variety
  2  AS
  3      SELECT ROWNUM cat#, variety
  4        FROM (SELECT DISTINCT variety
  5                FROM wine_dataset);

Table created.

SQL>
SQL> ALTER TABLE wine_variety
  2      ADD CONSTRAINT pk_wine_variety PRIMARY KEY (cat#);

Table altered.

SQL>

On divise (80/20) le dataset en un ensemble d’apprentissage et un ensemble de test :

 
SQL> CREATE TABLE train_set_wine#
  2  AS
  3      SELECT wine#
  4        FROM wine_dataset SAMPLE (80);

Table created.

SQL>
SQL> CREATE TABLE train_set_wines
  2  AS
  3      SELECT a.wine#, description
  4        FROM wine_dataset a, train_set_wine# b
  5       WHERE a.wine# = b.wine#;

Table created.

SQL>
SQL> CREATE TABLE train_set_variety
  2  AS
  3      SELECT a.wine#, cat#
  4        FROM wine_dataset a, train_set_wine# b, wine_variety c
  5       WHERE c.variety = a.variety AND a.wine# = b.wine#;

Table created.

SQL>
SQL> CREATE TABLE test_set_wines
  2  AS
  3      SELECT wine#,
  4             (SELECT cat#
  5                FROM wine_variety
  6               WHERE variety = a.variety)
  7                 cat#,
  8             description
  9        FROM wine_dataset a
 10       WHERE wine# NOT IN (SELECT wine# FROM train_set_wine#);

Table created.

SQL>

Une stoplist contenant les mots vides (de la langue anglaise) ainsi que le nom des catégories de vin est ensuite créée:

 
SQL> BEGIN
  2      ctx_ddl.create_stoplist (stoplist_name   => 'WINE_STOPLIST',
  3                               stoplist_type   => 'BASIC_STOPLIST');
  4
  5      FOR rec
  6          IN (SELECT spw_word mot
  7                FROM ctx_stopwords
  8               WHERE spw_stoplist = 'DEFAULT_STOPLIST')
  9      LOOP
 10          ctx_ddl.add_stopword (stoplist_name   => 'WINE_STOPLIST',
 11                                stopword        => rec.mot);
 12      END LOOP;
 13
 14  ctx_ddl.add_stopword (stoplist_name => 'WINE_STOPLIST', stopword => 'Cabernet');
 15  ctx_ddl.add_stopword (stoplist_name => 'WINE_STOPLIST', stopword => 'Sauvignon');
 16  ctx_ddl.add_stopword (stoplist_name => 'WINE_STOPLIST', stopword => 'Chardonnay');
 17  ctx_ddl.add_stopword (stoplist_name => 'WINE_STOPLIST', stopword => 'Pinot');
 18  ctx_ddl.add_stopword (stoplist_name => 'WINE_STOPLIST', stopword => 'Noir');
 19  ctx_ddl.add_stopword (stoplist_name => 'WINE_STOPLIST', stopword => 'Red');
 20  ctx_ddl.add_stopword (stoplist_name => 'WINE_STOPLIST', stopword => 'Blend');
 21
 22  END;
 23  /

PL/SQL procedure successfully completed.

SQL>

On crée ensuite un index textuel exploitant la stoplist précédemment créée et utilisant la tokenisation par défaut:

 
SQL> CREATE INDEX train_set_wines_ctxidx
  2      ON train_set_wines (description)
  3      INDEXTYPE IS ctxsys.context
  4          PARAMETERS ('
  5          stoplist        WINE_STOPLIST
  6        ')
  7  /

Index created.

SQL>

On spécifie l’algorithme de classification – ici, Support Vector Machine – et la table de stockage du paramétrage du classifieur :

 
SQL> EXEC ctx_ddl.create_preference('WINE_CLASSIFIER','SVM_CLASSIFIER');

PL/SQL procedure successfully completed.

SQL> CREATE TABLE restab
  2  (
  3      cat_id  NUMBER,
  4      TYPE    NUMBER (3) NOT NULL,
  5      rule    BLOB
  6  );

Table created.

SQL>

La classification est alors déclenchée via CTX_CLS.TRAIN:

 
SQL> BEGIN
  2      ctx_cls.train (index_name   => 'train_set_wines_ctxidx',
  3                     docid        => 'wine#',
  4                     cattab       => 'train_set_variety',
  5                     catdocid     => 'wine#',
  6                     catid        => 'cat#',
  7                     restab       => 'restab',
  8                     pref_name    => 'wine_classifier');
  9  END;
 10  /

PL/SQL procedure successfully completed.

SQL>

On peut alors créer un index de « routage » (CTXRULE) exploitant les règles produites par la classification:

 
SQL> CREATE INDEX restabx
  2      ON restab (rule)
  3      INDEXTYPE IS ctxsys.ctxrule
  4          PARAMETERS ('filter ctxsys.null_filter classifier wine_classifier');

Index created.

SQL>

La fonction MATCH_SCORE de l’opérateur MATCHES peut alors être utilisé pour quantifier le degré de correspondance du texte descriptif vis à vis des règles produites par le classifieur.

Pour le vin 6 par exemple, c’est la catégorie 4 qui présente le score le plus élevé (31):

 
SQL>   SELECT b.wine#,
  2           a.cat_id      pred_cat#,
  3           match_score (1) scr
  4      FROM restab a, test_set_wines b
  5     WHERE matches (rule, description, 1) > 0 AND b.wine# = 6
  6  ORDER BY 3 DESC;

     WINE#  PRED_CAT#        SCR
---------- ---------- ----------
         6          4         31
         6          2         24
         6          3         17
         6          1         15

SQL>

On stocke le résultat du scoring pour le dataset de test dans une table « res ». On peut alors comparer la catégorie réelle avec celle prédite:

 
SQL> CREATE TABLE res
  2  AS
  3      SELECT *
  4        FROM (SELECT wine#,
  5                     pred_cat#,
  6                     cat#,
  7                     ROW_NUMBER () OVER (PARTITION BY wine# ORDER BY scr DESC)
  8                         rn
  9                FROM (SELECT b.wine#,
 10                             a.cat_id        pred_cat#,
 11                             b.cat#,
 12                             match_score (1) scr
 13                        FROM restab a, test_set_wines b
 14                       WHERE matches (rule, description, 1) > 0))
 15       WHERE rn = 1;

Table created.

SQL> WITH
  2      badpred
  3      AS
  4          (SELECT COUNT (*) c1
  5             FROM res
  6            WHERE pred_cat# != cat#),
  7      totpop AS (SELECT COUNT (*) c2 FROM res)
  8  SELECT ROUND (100 * (c2 - c1) / c2, 2) pct_good
  9    FROM badpred, totpop;

  PCT_GOOD
----------
     84.82

SQL>

Le taux de prédictions correcte est de l’ordre de 85% – ce qui est très correct quand on considère que l’on n’a pas particulièrement fait d’effort de préparation des données ni de tuning du modèle.

Laisser un commentaire

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

× 5 = thirty five