{"id":1167,"date":"2017-12-29T14:28:03","date_gmt":"2017-12-29T13:28:03","guid":{"rendered":"http:\/\/130.61.50.57\/?p=1167"},"modified":"2017-12-27T10:54:14","modified_gmt":"2017-12-27T09:54:14","slug":"clustering-textuel-avec-oracle-text","status":"publish","type":"post","link":"https:\/\/blog.tiran.stream\/?p=1167","title":{"rendered":"Clustering textuel avec Oracle Text"},"content":{"rendered":"<p style=\"text-align: justify;\">La <a href=\"https:\/\/fr.wikipedia.org\/wiki\/Fouille_de_textes\" target=\"_blank\" rel=\"noopener\">fouille de textes<\/a>\u00a0ou <a href=\"https:\/\/en.wikipedia.org\/wiki\/Text_mining\" target=\"_blank\" rel=\"noopener\">TDM<\/a> (Text Data Mining) est un sujet qui revient r\u00e9guli\u00e8rement dans les projets d&rsquo;extraction d\u2019information \u00e0 partir de sources non structur\u00e9s.<\/p>\n<p style=\"text-align: justify;\"><a href=\"http:\/\/www.oracle.com\/technetwork\/database\/12coracletexttwp-1961244.pdf\" target=\"_blank\" rel=\"noopener\">Oracle Text<\/a> est un moteur d&rsquo;indexation textuelle disponible dans toutes les \u00e9ditions du SGBD Oracle. Outre ses capacit\u00e9s d&rsquo;analyse, ce moteur offre justement quelques fonctionnalit\u00e9s de datamining qui ont l&rsquo;avantage \u00e9norme de ne pas d\u00e9pendre de l&rsquo;option Advanced Analytics. A ce titre, elles sont accessibles sur toutes les \u00e9ditions du produit (SE, SE2, EE) sans licence additionnelle!\u00a0Ce point apparaissait tr\u00e8s clairement dans la <a href=\"https:\/\/docs.oracle.com\/cd\/E11882_01\/datamine.112\/e16808\/text.htm#DMCON418\" target=\"_blank\" rel=\"noopener\">documentation du produit jusqu&rsquo;\u00e0 la version 11.2<\/a>:<\/p>\n<p><em>Oracle Data Mining is an option of the Enterprise Edition of Oracle Database. To use Oracle Data Mining, you must have a license for the Data Mining option. <strong>To use Oracle Text and its data mining capabilities, you do not need to license the Data Mining option.<\/strong><\/em><\/p>\n<p style=\"text-align: justify;\">Mais la mention a \u00e9t\u00e9 supprim\u00e9e dans la documentation officielle 12c&#8230;<\/p>\n<p style=\"text-align: justify;\">Cela m&rsquo;a conduit \u00e0 poster <a href=\"https:\/\/community.oracle.com\/thread\/4084345\" target=\"_blank\" rel=\"noopener\">une question sur ODC<\/a>\u00a0et il m&rsquo;a \u00e9t\u00e9 confirm\u00e9 par un Product Manager que la r\u00e8gle de licensing n&rsquo;avait pas \u00e9t\u00e9 chang\u00e9e!<\/p>\n<p style=\"text-align: justify;\">Pour f\u00eater \u00e7a, tentons un petit clustering k-means \u00e0 partir de donn\u00e9es textuelles!!! &#8230; et tant qu&rsquo;\u00e0 faire, utilisons une instance ex\u00e9cutant un moteur SE2!<\/p>\n<p style=\"text-align: justify;\"><span style=\"font-size: 14pt;\">\ud83d\ude42<\/span><\/p>\n<pre class=\"brush: sql; ruler: true;\">\u00a0\r\nSQL&gt; SELECT version, edition FROM v$instance;\r\n\r\nVERSION         EDITION\r\n--------------- -------\r\n12.1.0.2.0      SE\r\n\r\nSQL&gt;\r\nSQL&gt; SELECT banner\r\n  2    FROM v$version\r\n  3   WHERE banner LIKE &#039;Oracle%&#039;;\r\n\r\nBANNER\r\n--------------------------------------------------------------------------------\r\nOracle Database 12c Standard Edition Release 12.1.0.2.0 - 64bit Production\r\n\r\nSQL&gt;\r\nSQL&gt; SELECT comp_name, version, status\r\n  2    FROM dba_registry\r\n  3   WHERE comp_id = &#039;CONTEXT&#039;;\r\n\r\nCOMP_NAME            VERSION         STATUS\r\n-------------------- --------------- ----------\r\nOracle Text          12.1.0.2.0      VALID\r\n\r\nSQL&gt;\r\n<\/pre>\n<p style=\"text-align: justify;\">Ici, je vais utiliser une s\u00e9rie de recettes de cuisine et mon id\u00e9e est de parvenir \u00e0 diviser le jeu de donn\u00e9es sur la caract\u00e9ristique sal\u00e9\/sucr\u00e9. Pour cela, j&rsquo;ai fait un peu de webscrapping \u00e0 partir du site <a href=\"https:\/\/www.lebruitdufrigo.fr\/\" target=\"_blank\" rel=\"noopener\">www.lebruitdufrigo.fr<\/a>. Les recettes y sont d\u00e9j\u00e0 class\u00e9es par famille de plats et j&rsquo;ai extrait les listes d\u2019ingr\u00e9dients des entr\u00e9es, plats, desserts, tartes sal\u00e9es et p\u00e2tisseries.<\/p>\n<p style=\"text-align: justify;\">Globalement, les cat\u00e9gories desserts et p\u00e2tisseries sont plut\u00f4t sucr\u00e9es. Les autres recettes sont plut\u00f4t sal\u00e9es. Un clustering bas\u00e9 sur les ingr\u00e9dients devrait pouvoir faire ressortir cette diff\u00e9rence.<\/p>\n<p style=\"text-align: justify;\">Les donn\u00e9es sont accessibles ici au format Table Externe DataPump:\u00a0<a href=\"https:\/\/blog.tiran.stream\/wp-content\/uploads\/2017\/12\/recettes_ext.zip\">recettes_ext<\/a><\/p>\n<p style=\"text-align: justify;\">On peut les recharger simplement:<\/p>\n<pre class=\"brush: sql; ruler: true;\">SQL&gt; CREATE OR REPLACE DIRECTORY extdir AS &#039;\/tmp&#039;;\r\n\r\nDirectory created.\r\n\r\nSQL&gt;\r\nSQL&gt; CREATE TABLE recettes_ext\r\n  2  (\r\n  3      recette_id NUMBER,\r\n  4      TYPE_PLAT VARCHAR2 (30),\r\n  5      RECETTE VARCHAR2 (100),\r\n  6      INGREDIENTS VARCHAR2 (1000)\r\n  7  )\r\n  8  ORGANIZATION EXTERNAL\r\n  9      (TYPE oracle_datapump\r\n 10       DEFAULT DIRECTORY extdir\r\n 11       LOCATION (&#039;recettes_ext.dp&#039;));\r\n\r\nTable created.\r\n\r\nSQL&gt;\r\n<\/pre>\n<p style=\"text-align: justify;\">La premi\u00e8re \u00e9tape consiste \u00e0 \u00ab nettoyer \u00bb le texte. Oracle Text va prendre en charge la plupart des op\u00e9rations de nettoyage (suppression des \u00e9l\u00e9ments de ponctuation et des mots vides etc\u2026). En revanche, la pr\u00e9sence de chiffres indiquant des quantit\u00e9s (ex. 100g, 30cl&#8230;) va devoir \u00eatre trait\u00e9e manuellement. En effet, dans notre contexte, ces derniers n&rsquo;ont pas vraiment de valeur ajout\u00e9e. On va les filtrer \u00e0 l&rsquo;aide d&rsquo;une<a href=\"https:\/\/docs.oracle.com\/database\/121\/SQLRF\/functions163.htm#SQLRF06302\" target=\"_blank\" rel=\"noopener\"> expression r\u00e9guli\u00e8re<\/a> :<\/p>\n<pre class=\"brush: sql; ruler: true;\">SQL&gt; CREATE TABLE recettes_clean\r\n  2  AS\r\n  3      SELECT recette_id,\r\n  4             CASE\r\n  5                 WHEN type_plat IN (&#039;Patisserie&#039;, &#039;Dessert&#039;) THEN &#039;Sucr\u00e9&#039;\r\n  6                 ELSE &#039;Sal\u00e9&#039;\r\n  7             END\r\n  8                 categorie_plat,\r\n  9             REGEXP_REPLACE (ingredients, &#039;[[:digit:]]&#039;, &#039;&#039;)\r\n 10                 ingredients\r\n 11        FROM recettes_ext;\r\n\r\nTable created.\r\n\r\nSQL&gt;\r\nSQL&gt; ALTER TABLE recettes_clean\r\n  2      ADD CONSTRAINT pk_recettes_clean PRIMARY KEY (recette_id);\r\n\r\nTable altered.\r\n\r\nSQL&gt;\r\n<\/pre>\n<p style=\"text-align: justify;\">Les \u00ab mots vides \u00bb sont des mots tr\u00e8s commun (comme des articles par exemple) qu\u2019il convient de supprimer de l\u2019analyse. Oracle propose une liste pr\u00e9d\u00e9finie mais celle-ci a \u00e9t\u00e9 enrichie \u00e0 partir de sources trouv\u00e9es sur internet: <a href=\"http:\/\/www.ranks.nl\/stopwords\/french\" target=\"_blank\" rel=\"noopener\">http:\/\/www.ranks.nl\/stopwords\/french<\/a><\/p>\n<p style=\"text-align: justify;\">Les donn\u00e9es sont accessibles ici (au format Table Externe DataPump): <a href=\"https:\/\/blog.tiran.stream\/wp-content\/uploads\/2016\/06\/MOTSVIDES_EXTTAB.zip\">MOTSVIDES_EXTTAB<\/a><\/p>\n<p style=\"text-align: justify;\">On peut alors cr\u00e9e une <a href=\"https:\/\/docs.oracle.com\/database\/121\/CCAPP\/GUID-71087894-0C0B-42CF-B37E-064401C54545.htm#CCAPP9094\" target=\"_blank\" rel=\"noopener\">STOPLIST<\/a> \u00e0 partir de ces mots:<\/p>\n<pre class=\"brush: sql; ruler: true;\">SQL&gt; CREATE TABLE motsvides_exttab_dp\r\n  2  (\r\n  3      MOT VARCHAR2 (100)\r\n  4  )\r\n  5  ORGANIZATION EXTERNAL\r\n  6      (TYPE oracle_datapump\r\n  7       DEFAULT DIRECTORY extdir\r\n  8       LOCATION (&#039;motsvides_exttab.dp&#039;));\r\n\r\nTable created.\r\n\r\nSQL&gt;\r\nSQL&gt; BEGIN\r\n  2      ctx_ddl.create_stoplist (stoplist_name   =&gt; &#039;RECETTE_STOPLIST&#039;,\r\n  3                               stoplist_type   =&gt; &#039;BASIC_STOPLIST&#039;);\r\n  4\r\n  5      FOR rec IN (SELECT mot FROM motsvides_exttab_dp)\r\n  6      LOOP\r\n  7          ctx_ddl.add_stopword (stoplist_name   =&gt; &#039;RECETTE_STOPLIST&#039;,\r\n  8                                stopword        =&gt; rec.mot);\r\n  9      END LOOP;\r\n 10  END;\r\n 11  \/\r\n\r\nPL\/SQL procedure successfully completed.\r\n\r\nSQL&gt;\r\n<\/pre>\n<p style=\"text-align: justify;\">On va y adjoindre une s\u00e9rie d\u2019abr\u00e9viations des unit\u00e9s de mesure (g, kg, l etc\u2026) et de termes de cuisine sp\u00e9cifiques (pinc\u00e9e, morceau, boite etc&#8230;):\u00a0<a href=\"https:\/\/blog.tiran.stream\/wp-content\/uploads\/2017\/12\/cuisine_stoplist_exttab.zip\">cuisine_stoplist_exttab<\/a><\/p>\n<pre class=\"brush: sql; ruler: true;\">SQL&gt; CREATE TABLE cuisine_stoplist_exttab\r\n  2  (\r\n  3      MOT VARCHAR2 (100)\r\n  4  )\r\n  5  ORGANIZATION EXTERNAL\r\n  6      (TYPE oracle_datapump\r\n  7       DEFAULT DIRECTORY extdir\r\n  8       LOCATION (&#039;cuisine_stoplist_exttab.dp&#039;));\r\n\r\nTable created.\r\n\r\nSQL&gt;\r\nSQL&gt; BEGIN\r\n  2      FOR rec IN (SELECT mot FROM cuisine_stoplist_exttab)\r\n  3      LOOP\r\n  4          ctx_ddl.add_stopword (stoplist_name   =&gt; &#039;RECETTE_STOPLIST&#039;,\r\n  5                                stopword        =&gt; rec.mot);\r\n  6      END LOOP;\r\n  7  END;\r\n  8  \/\r\n\r\nPL\/SQL procedure successfully completed.\r\n\r\nSQL&gt;\r\nSQL&gt; COMMIT;\r\n\r\nCommit complete.\r\n\r\nSQL&gt;\r\n<\/pre>\n<p style=\"text-align: justify;\">Pour l\u2019\u00e9tape de tokenization, le d\u00e9coupage du texte utilisera les espaces comme s\u00e9parateurs. C\u2019est la m\u00e9thode par d\u00e9faut qui est employ\u00e9 par le <a href=\"https:\/\/docs.oracle.com\/database\/121\/CCREF\/cdatadic.htm#CCREF0218\" target=\"_blank\" rel=\"noopener\">BASIC_LEXER<\/a>.<\/p>\n<p style=\"text-align: justify;\">D&rsquo;autre part, on va activer:<\/p>\n<ul>\n<li style=\"text-align: justify;\">le param\u00e8tre <a href=\"https:\/\/docs.oracle.com\/database\/121\/CCREF\/cspell.htm#CCREF2210\" target=\"_blank\" rel=\"noopener\">BASE_LETTER<\/a> qui permet de convertir les caract\u00e8res accentu\u00e9s dans la lettre sous-jacente (\u00e9 -&gt; e).<\/li>\n<li>la \u00ab\u00a0<a href=\"https:\/\/fr.wikipedia.org\/wiki\/Racinisation\" target=\"_blank\" rel=\"noopener\">racinisation<\/a>\u00a0\u00bb de mani\u00e8re \u00e0 associer les d\u00e9clinaisons d&rsquo;un m\u00eame terme (hach\u00e9, hach\u00e9s, hach\u00e9e, hacher etc&#8230;): <a href=\"https:\/\/docs.oracle.com\/database\/121\/CCREF\/cdatadic.htm#GUID-B5DF63F9-FAAF-47B1-8711-04B1818499E8__GUID-4C671C96-C12A-48E9-8CA1-9F71BC77EE7C\" target=\"_blank\" rel=\"noopener\">INDEX_STEMS<\/a><\/li>\n<\/ul>\n<pre class=\"brush: sql; ruler: true;\">SQL&gt; BEGIN\r\n  2      ctx_ddl.create_preference (&#039;RECETTE_LEXER&#039;, &#039;BASIC_LEXER&#039;);\r\n  3      ctx_ddl.set_attribute ( &#039;RECETTE_LEXER&#039;, &#039;INDEX_STEMS&#039;, &#039;FRENCH&#039;);\r\n  4      ctx_ddl.set_attribute (&#039;RECETTE_LEXER&#039;, &#039;BASE_LETTER&#039;, &#039;YES&#039;);\r\n  5  END;\r\n  6  \/\r\n\r\nPL\/SQL procedure successfully completed.\r\n\r\nSQL&gt;\r\n<\/pre>\n<p style=\"text-align: justify;\">Un <a href=\"https:\/\/docs.oracle.com\/database\/122\/CCAPP\/indexing-with-oracle-text.htm#CCAPP9023\" target=\"_blank\" rel=\"noopener\">index textuel (type CTXSYS.CONTEXT)<\/a> est alors cr\u00e9\u00e9 sur la liste des ingr\u00e9dients des recettes :<\/p>\n<pre class=\"brush: sql; ruler: true;\">SQL&gt; CREATE INDEX recettes_ctxidx\r\n  2      ON recettes_clean (ingredients)\r\n  3      INDEXTYPE IS ctxsys.context\r\n  4          PARAMETERS (&#039;\r\n  5      lexer           RECETTE_LEXER\r\n  6      stoplist        RECETTE_STOPLIST\r\n  7    &#039;)\r\n  8  \/\r\n\r\nIndex created.\r\n\r\nSQL&gt;\r\n<\/pre>\n<p style=\"text-align: justify;\">Ce dernier va servir de support \u00e0 l\u2019algorithme k-means lors de la clusterisation. Celle-ci va \u00eatre r\u00e9alis\u00e9e \u00e0 l\u2019aide de la proc\u00e9dure <a href=\"https:\/\/docs.oracle.com\/database\/121\/CCREF\/cclspkg.htm#GUID-E3D9A1E3-CDC8-4532-ABD3-87C0FF87FBE3\" target=\"_blank\" rel=\"noopener\">CTX_CLS.CLUSTERING<\/a>. On pr\u00e9cise le nombre de clusters souhait\u00e9s (ici, 2) ainsi que le recours aux racines des termes (STEM_ON) \u00e0 l\u2019aide d\u2019une pr\u00e9f\u00e9rence cr\u00e9\u00e9e pr\u00e9alablement \u00e0 l\u2019aide du package <a href=\"https:\/\/docs.oracle.com\/database\/121\/CCREF\/cddlpkg.htm#GUID-56C4580F-6A43-43F4-BB83-DFAEB72A8DC3\" target=\"_blank\" rel=\"noopener\">CTX_DDL<\/a> :<\/p>\n<pre class=\"brush: sql; ruler: true;\">SQL&gt; BEGIN\r\n  2      ctx_ddl.create_preference (&#039;TXT_CLUSTERING&#039;, &#039;KMEAN_CLUSTERING&#039;);\r\n  3      ctx_ddl.set_attribute (&#039;TXT_CLUSTERING&#039;, &#039;CLUSTER_NUM&#039;, &#039;2&#039;);\r\n  4      ctx_ddl.set_attribute (&#039;TXT_CLUSTERING&#039;, &#039;STEM_ON&#039;, &#039;TRUE&#039;);\r\n  5      ctx_cls.clustering (index_name    =&gt; &#039;RECETTES_CTXIDX&#039;,\r\n  6                          docid         =&gt; &#039;RECETTE_ID&#039;,\r\n  7                          doctab_name   =&gt; &#039;KMEANS_CLUST_RES&#039;,\r\n  8                          clstab_name   =&gt; &#039;KMEANS_CLUST_DETAILS&#039;,\r\n  9                          pref_name     =&gt; &#039;TXT_CLUSTERING&#039;);\r\n 10  END;\r\n 11  \/\r\n\r\nPL\/SQL procedure successfully completed.\r\n\r\nSQL&gt;\r\n<\/pre>\n<p style=\"text-align: justify;\">La proc\u00e9dure de clustering produit deux tables (param\u00e8tres DOCTAB_NAME et CLSTAB_NAME). La premi\u00e8re contient l\u2019ID du cluster auquel chaque enregistrement est assign\u00e9.<\/p>\n<p style=\"text-align: justify;\">On peut alors produire un tableau de contingence (\u00e0 l\u2019aide de l\u2019op\u00e9rateur SQL PIVOT) :<\/p>\n<pre class=\"brush: sql; ruler: true;\">SQL&gt; SELECT *\r\n  2    FROM (SELECT categorie_plat, clusterid\r\n  3            FROM recettes_clean a, KMEANS_CLUST_RES b\r\n  4           WHERE a.recette_id = b.docid)\r\n  5         PIVOT\r\n  6             (COUNT (*) FOR clusterid IN (3 Groupe_1, 2 Groupe_2));\r\n\r\nCATEGORIE_PLAT    GROUPE_1   GROUPE_2\r\n--------------- ---------- ----------\r\nSucr\u00e9                  181          7\r\nSal\u00e9                    12        344\r\n\r\nSQL&gt;\r\n<\/pre>\n<p style=\"text-align: justify;\">La dichotomie est claire. Le cluster GROUPE_1 correspond manifestement aux recettes plut\u00f4t sucr\u00e9es et le cluster GROUPE_2 correspond aux recettes sal\u00e9es.\u00a0Seule 19 recettes (sur 544 \u2013 <strong>soit 3.5%<\/strong>) ne sont pas associ\u00e9es au groupe auquel on pourrait s\u2019attendre!<\/p>\n<p style=\"text-align: justify;\">\n","protected":false},"excerpt":{"rendered":"<p>La fouille de textes\u00a0ou TDM (Text Data Mining) est un sujet qui revient r\u00e9guli\u00e8rement dans les projets d&rsquo;extraction d\u2019information \u00e0<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"colormag_page_container_layout":"default_layout","colormag_page_sidebar_layout":"default_layout","footnotes":""},"categories":[4,6,10,19],"tags":[],"class_list":["post-1167","post","type-post","status-publish","format-standard","hentry","category-clustering","category-oracle","category-preparation-des-donnees","category-donnees-non-structurees"],"_links":{"self":[{"href":"https:\/\/blog.tiran.stream\/index.php?rest_route=\/wp\/v2\/posts\/1167","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.tiran.stream\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.tiran.stream\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.tiran.stream\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.tiran.stream\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1167"}],"version-history":[{"count":39,"href":"https:\/\/blog.tiran.stream\/index.php?rest_route=\/wp\/v2\/posts\/1167\/revisions"}],"predecessor-version":[{"id":1233,"href":"https:\/\/blog.tiran.stream\/index.php?rest_route=\/wp\/v2\/posts\/1167\/revisions\/1233"}],"wp:attachment":[{"href":"https:\/\/blog.tiran.stream\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1167"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.tiran.stream\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1167"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.tiran.stream\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1167"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}