ANN/ORE #1 – Reconnaissance de caractères du dataset MNIST

Dans la continuité des précédents posts, afin de tester des ANN plus conséquents, je vais maintenant utiliser le dataset MNIST. Il s’agit d’un célèbre dataset de reconnaissance OCR popularisé par les travaux de Yann Le Cun.

Le dataset disponible ici contient des images en noir et blanc de chiffres (0 à 9) manuscrits. Il est subdivisé en un échantillon d’apprentissage de 60000 images et d’un jeu de test de 10000 images. Chaque image est constitué de 784 pixels (28×28).

Le format sous lequel les données sont mises à disposition n’est pas directement utilisable sous R (ou du moins pas simplement). Je vais donc passer par une première étape de chargement de ces données sous la forme de tables au sein d’une base Oracle. J’y accéderai ensuite via ORE ou ROracle en fonction du besoin…

Le format des fichiers est détaillé en bas de la page http://yann.lecun.com/exdb/mnist/.

Ici, on transfère les fichiers sur le serveur de base de données afin d’utiliser UTL_FILE pour les lire byte par byte:

[rtiran@psu888 ~]$ ll /tmp/*ubyte
-rw-r----- 1 rtiran dba  7840016 Jun  8 11:31 /tmp/t10k-images-idx3-ubyte
-rw-r----- 1 rtiran dba    10008 Jun  8 11:31 /tmp/t10k-labels-idx1-ubyte
-rw-r--r-- 1 rtiran dba 47040016 Jun  8 11:31 /tmp/train-images-idx3-ubyte
-rw-r--r-- 1 rtiran dba    60008 Jun  8 11:31 /tmp/train-labels-idx1-ubyte
[rtiran@psu888 ~]$

Le fichier train-images-idx3-ubyte démarre par 4 series de 4 bytes (32 bits) inutiles pour la suite de l’analyse. Ensuite, chaque byte correspond à un pixel et chaque image contient 784 pixels (28*28).

Dans un premier temps, on stocke la valeur de chaque pixel dans la table IMGS_FLAT. Chaque ligne correspondant à un pixel (l’ID de l’image et la position du pixel sont aussi stockés):

SQL> CREATE TABLE imgs_flat
  2  (
  3      img_id NUMBER,
  4      pix_id NUMBER,
  5      pix_val NUMBER
  6  );

Table created.

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

Directory created.

SQL>

La routine PL/SQL suivante procède au balayage du fichier et a l’extraction de la valeur de chaque pixel:

SQL> SET TIMING on
SQL> DECLARE
  2      f_imgs          UTL_FILE.file_type;
  3      l_buffer        RAW (32);
  4      l_cnt           NUMBER := 0;
  5      l_pix_id        NUMBER := 0;
  6      l_img_id        NUMBER := 0;
  7      l_eof           BOOLEAN := FALSE;
  8
  9      TYPE t_imgs_flat IS TABLE OF imgs_flat%ROWTYPE
 10          INDEX BY BINARY_INTEGER;
 11
 12      arr_imgs_flat   t_imgs_flat;
 13  BEGIN
 14      f_imgs := UTL_FILE.fopen ('D1', 'train-images-idx3-ubyte', 'rb');
 15
 16      FOR i IN 1 .. 4
 17      LOOP
 18          UTL_FILE.get_raw (f_imgs, l_buffer, 4);
 19          DBMS_OUTPUT.put_line (
 20              UTL_RAW.cast_to_binary_integer (l_buffer,
 21                                              endianess   => UTL_RAW.big_endian));
 22      END LOOP;
 23
 24      LOOP
 25          l_cnt := l_cnt + 1;
 26          l_pix_id := MOD (l_cnt, 28 * 28);
 27
 28          IF l_pix_id = 0
 29          THEN
 30              l_pix_id := 784;
 31          END IF;
 32
 33          l_img_id := CEIL (l_cnt / (28 * 28));
 34
 35          BEGIN
 36              UTL_FILE.get_raw (f_imgs, l_buffer, 1);
 37              arr_imgs_flat (l_cnt).img_id := l_img_id;
 38              arr_imgs_flat (l_cnt).pix_id := l_pix_id;
 39              arr_imgs_flat (l_cnt).pix_val :=
 40                  UTL_RAW.cast_to_binary_integer (
 41                      l_buffer,
 42                      endianess   => UTL_RAW.big_endian);
 43          EXCEPTION
 44              WHEN NO_DATA_FOUND
 45              THEN
 46                  l_eof := TRUE;
 47          END;
 48
 49          IF MOD (l_cnt, 1e6) = 0 OR l_eof
 50          THEN
 51              FORALL i IN arr_imgs_flat.FIRST .. arr_imgs_flat.LAST
 52                  INSERT INTO imgs_flat
 53                  VALUES arr_imgs_flat (i);
 54
 55              arr_imgs_flat.delete;
 56
 57              COMMIT;
 58          END IF;
 59
 60          IF l_eof
 61          THEN
 62              EXIT;
 63          END IF;
 64      END LOOP;
 65
 66      COMMIT;
 67  END;
 68  /

PL/SQL procedure successfully completed.

Elapsed: 00:11:31.96
SQL>

Ce découpage dure une dizaine de minutes.

On obtient alors une table de 47040000 enregistrements (60000 images * 784 pixels):

SQL> SET TIMING off
SQL> SELECT COUNT (*) FROM imgs_flat;

  COUNT(*)
----------
  47040000

SQL> 

L’étape suivante consiste a pivoter ces enregistrements de manière à avoir une structure tabulaire constituée de 784 champs (1 champ par pixel) et 60000 enregistrements (1 par image).

Pour cela, on crée la table cible IMGS. Elle contient un champ IMG_ID et 784 champs P1, P2, … P784 contenant la valeur du pixel associé:

SQL> CREATE TABLE imgs
  2  (
  3      img_id    NUMBER PRIMARY KEY
  4  );

Table created.

SQL> SET TIMING on
SQL> BEGIN
  2      FOR i IN 1 .. 28 * 28
  3      LOOP
  4          EXECUTE IMMEDIATE 'alter table IMGS add (p' || i || ' number)';
  5      END LOOP;
  6  END;
  7  /

PL/SQL procedure successfully completed.

Elapsed: 00:00:20.90
SQL> SET TIMING off
SQL>      SELECT column_name
  2         FROM user_tab_columns
  3        WHERE table_name = 'IMGS'
  4     ORDER BY column_id
  5  FETCH FIRST 10 ROWS ONLY;

COLUMN_NAME
--------------------------------------------------------------------------------
IMG_ID
P1
P2
P3
P4
P5
P6
P7
P8
P9

10 rows selected.

SQL> 

On utilise ensuite la clause PIVOT pour récupérer sous forme linéaire toutes les valeurs de pixels pour chaque image. C’est ce qu’on insère dans IMGS.

L’ordre SQL final étant gigantesque, on le construit dynamiquement. On en profite au passage pour appliquer une normalisation MIN/MAX aux valeurs des pixels:

SQL> SET TIMING on
SQL> DECLARE
  2      l_pivot_clause   VARCHAR2 (32000);
  3  BEGIN
  4      FOR i IN 1 .. 28 * 28
  5      LOOP
  6          l_pivot_clause := l_pivot_clause || i || ' as P' || i || ',';
  7      END LOOP;
  8
  9      l_pivot_clause := RTRIM (l_pivot_clause, ',');
 10
 11      EXECUTE IMMEDIATE 'INSERT INTO IMGS
 12      SELECT *
 13        FROM (
 14          SELECT a.IMG_ID,
 15                 a.PIX_ID,
 16                 a.pix_val / 255 pix_val
 17            FROM imgs_flat a
 18        )
 19             PIVOT
 20                 (MAX (pix_val) FOR pix_id IN (' || l_pivot_clause || '))';
 21
 22      COMMIT;
 23  END;
 24  /

PL/SQL procedure successfully completed.

Elapsed: 00:01:38.92
SQL>

A ce stade, on dispose d’une table dont chaque ligne contient la valeur des pixels d’une image.

On va ensuite charger dans une autre table (IMGS_VAL) les labels des images:

SQL> CREATE TABLE imgs_val
  2  (
  3      img_id    NUMBER PRIMARY KEY,
  4      img_val   NUMBER
  5  );

Table created.

SQL>

La encore on va utiliser une lecture byte par byte du fichier train-labels-idx1-ubyte. On va ignorer les 64 premiers bits (8 bytes) du fichier. Chaque byte suivant correspond au label de l’image correspondante:

SQL> SET TIMING on
SQL> DECLARE
  2      f_imgs         UTL_FILE.file_type;
  3      l_buffer       RAW (32);
  4      l_cnt          NUMBER := 0;
  5      l_val          NUMBER := 0;
  6
  7      TYPE t_imgs_val IS TABLE OF imgs_val%ROWTYPE
  8          INDEX BY BINARY_INTEGER;
  9
 10      arr_imgs_val   t_imgs_val;
 11  BEGIN
 12      f_imgs := UTL_FILE.fopen ('D1', 'train-labels-idx1-ubyte', 'rb');
 13
 14      FOR i IN 1 .. 2
 15      LOOP
 16          UTL_FILE.get_raw (f_imgs, l_buffer, 4);
 17          DBMS_OUTPUT.put_line (
 18              UTL_RAW.cast_to_binary_integer (l_buffer,
 19                                              endianess   => UTL_RAW.big_endian));
 20      END LOOP;
 21
 22      LOOP
 23          l_cnt := l_cnt + 1;
 24
 25          BEGIN
 26              UTL_FILE.get_raw (f_imgs, l_buffer, 1);
 27              arr_imgs_val (l_cnt).img_id := l_cnt;
 28              arr_imgs_val (l_cnt).img_val :=
 29                  UTL_RAW.cast_to_binary_integer (
 30                      l_buffer,
 31                      endianess   => UTL_RAW.big_endian);
 32          EXCEPTION
 33              WHEN NO_DATA_FOUND
 34              THEN
 35                  EXIT;
 36          END;
 37      END LOOP;
 38
 39      FORALL i IN arr_imgs_val.FIRST .. arr_imgs_val.LAST
 40          INSERT INTO imgs_val
 41          VALUES arr_imgs_val (i);
 42
 43      COMMIT;
 44  END;
 45  /

PL/SQL procedure successfully completed.

Elapsed: 00:00:01.23
SQL>

A ce stade, la table IMGS_VAL contient 60000 enregistrements – 1 par image – et le champ IMG_VAL contient le label de chaque image de l’échantillon d’apprentissage:

SQL> SELECT COUNT (*) FROM imgs_val;

  COUNT(*)
----------
     60000

SQL>
SQL>      SELECT *
  2         FROM imgs_val
  3     ORDER BY img_id
  4  FETCH FIRST 5 ROWS ONLY;

    IMG_ID    IMG_VAL
---------- ----------
         1          5
         2          0
         3          4
         4          1
         5          9

SQL>

On peut alors utiliser une vue pour grouper les données de IMGS et IMGS_VAL. On convertit le champ IMG_VAL au format texte afin qu’il soit ensuite considéré comme un facteur par R:

SQL> CREATE OR REPLACE VIEW mnist_training_set
  2  AS
  3      SELECT to_char(img_val) img_lbl,
  4             b.*
  5        FROM imgs_val a, imgs b
  6       WHERE a.img_id = b.img_id;

View created.

SQL>

A noter qu’on ajoute une contrainte d’intégrité déclarative sur la vue afin de donner à R un critère d’ordonnancement des données:

SQL> ALTER VIEW mnist_training_set ADD CONSTRAINT training_set_pk PRIMARY KEY (img_id)
  2                               DISABLE NOVALIDATE;

View altered.

SQL>

Sans cette contrainte, on obtiendrai le message suivant lors d’une récupération des données au sein d’un dataframe :

> ts <- ore.pull(MNIST_TRAINING_SET)
Warning message:
ORE object has no unique key - using random order 
>

On procède de manière identique pour le chargement des données de validation. Les tables intermédiaires se nomment IMGS_FLAT_TEST, IMGS_TEST, IMGS_TEST_VAL et la vue de regroupement se nomme MNIST_TEST_SET:

SQL> CREATE TABLE imgs_flat_test
  2  (
  3      img_id NUMBER,
  4      pix_id NUMBER,
  5      pix_val NUMBER
  6  );

Table created.

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

Directory created.

SQL>
SQL> DECLARE
  2      f_imgs               UTL_FILE.file_type;
  3      l_buffer             RAW (32);
  4      l_cnt                NUMBER := 0;
  5      l_pix_id             NUMBER := 0;
  6      l_img_id             NUMBER := 0;
  7      l_eof                BOOLEAN := FALSE;
  8
  9      TYPE t_imgs_flat_test IS TABLE OF imgs_flat_test%ROWTYPE
 10          INDEX BY BINARY_INTEGER;
 11
 12      arr_imgs_flat_test   t_imgs_flat_test;
 13  BEGIN
 14      f_imgs := UTL_FILE.fopen ('D1', 't10k-images-idx3-ubyte', 'rb');
 15
 16      FOR i IN 1 .. 4
 17      LOOP
 18          UTL_FILE.get_raw (f_imgs, l_buffer, 4);
 19          DBMS_OUTPUT.put_line (
 20              UTL_RAW.cast_to_binary_integer (l_buffer,
 21                                              endianess   => UTL_RAW.big_endian));
 22      END LOOP;
 23
 24      LOOP
 25          l_cnt := l_cnt + 1;
 26          l_pix_id := MOD (l_cnt, 28 * 28);
 27
 28          IF l_pix_id = 0
 29          THEN
 30              l_pix_id := 784;
 31          END IF;
 32
 33          l_img_id := CEIL (l_cnt / (28 * 28));
 34
 35          BEGIN
 36              UTL_FILE.get_raw (f_imgs, l_buffer, 1);
 37              arr_imgs_flat_test (l_cnt).img_id := l_img_id;
 38              arr_imgs_flat_test (l_cnt).pix_id := l_pix_id;
 39              arr_imgs_flat_test (l_cnt).pix_val :=
 40                  UTL_RAW.cast_to_binary_integer (
 41                      l_buffer,
 42                      endianess   => UTL_RAW.big_endian);
 43          EXCEPTION
 44              WHEN NO_DATA_FOUND
 45              THEN
 46                  l_eof := TRUE;
 47          END;
 48
 49          IF MOD (l_cnt, 1e6) = 0 OR l_eof
 50          THEN
 51              FORALL i IN arr_imgs_flat_test.FIRST .. arr_imgs_flat_test.LAST
 52                  INSERT INTO imgs_flat_test
 53                  VALUES arr_imgs_flat_test (i);
 54
 55              arr_imgs_flat_test.delete;
 56
 57              COMMIT;
 58          END IF;
 59
 60          IF l_eof
 61          THEN
 62              EXIT;
 63          END IF;
 64      END LOOP;
 65
 66      COMMIT;
 67  END;
 68  /

PL/SQL procedure successfully completed.

SQL> CREATE TABLE imgs_test
  2  (
  3      img_id    NUMBER PRIMARY KEY
  4  );

Table created.

SQL>
SQL> BEGIN
  2      FOR i IN 1 .. 28 * 28
  3      LOOP
  4          EXECUTE IMMEDIATE 'alter table IMGS_TEST add (p' || i || ' number)';
  5      END LOOP;
  6  END;
  7  /

PL/SQL procedure successfully completed.

SQL> DECLARE
  2      l_pivot_clause   VARCHAR2 (32000);
  3  BEGIN
  4      FOR i IN 1 .. 28 * 28
  5      LOOP
  6          l_pivot_clause := l_pivot_clause || i || ' as P' || i || ',';
  7      END LOOP;
  8
  9      l_pivot_clause := RTRIM (l_pivot_clause, ',');
 10
 11      EXECUTE IMMEDIATE 'INSERT INTO IMGS_TEST
 12      SELECT *
 13        FROM (
 14          SELECT a.IMG_ID,
 15                 a.PIX_ID,
 16                 a.pix_val / 255 pix_val
 17            FROM imgs_flat_test a
 18        )
 19             PIVOT
 20                 (MAX (pix_val) FOR pix_id IN (' || l_pivot_clause || '))';
 21
 22      COMMIT;
 23  END;
 24  /

PL/SQL procedure successfully completed.

SQL>
SQL> CREATE TABLE imgs_test_val
  2  (
  3      img_id    NUMBER PRIMARY KEY,
  4      img_val NUMBER
  5  );

Table created.

SQL>
SQL> DECLARE
  2      f_imgs              UTL_FILE.file_type;
  3      l_buffer            RAW (32);
  4      l_cnt               NUMBER := 0;
  5      l_val               NUMBER := 0;
  6
  7      TYPE t_imgs_test_val IS TABLE OF imgs_test_val%ROWTYPE
  8          INDEX BY BINARY_INTEGER;
  9
 10      arr_imgs_test_val   t_imgs_test_val;
 11  BEGIN
 12      f_imgs := UTL_FILE.fopen ('D1', 't10k-labels-idx1-ubyte', 'rb');
 13
 14      FOR i IN 1 .. 2
 15      LOOP
 16          UTL_FILE.get_raw (f_imgs, l_buffer, 4);
 17          DBMS_OUTPUT.put_line (
 18              UTL_RAW.cast_to_binary_integer (l_buffer,
 19                                              endianess   => UTL_RAW.big_endian));
 20      END LOOP;
 21
 22      LOOP
 23          l_cnt := l_cnt + 1;
 24
 25          BEGIN
 26              UTL_FILE.get_raw (f_imgs, l_buffer, 1);
 27              arr_imgs_test_val (l_cnt).img_id := l_cnt;
 28              arr_imgs_test_val (l_cnt).img_val :=
 29                  UTL_RAW.cast_to_binary_integer (
 30                      l_buffer,
 31                      endianess   => UTL_RAW.big_endian);
 32          EXCEPTION
 33              WHEN NO_DATA_FOUND
 34              THEN
 35                  EXIT;
 36          END;
 37      END LOOP;
 38
 39      FORALL i IN arr_imgs_test_val.FIRST .. arr_imgs_test_val.LAST
 40          INSERT INTO imgs_test_val
 41          VALUES arr_imgs_test_val (i);
 42
 43      COMMIT;
 44  END;
 45  /

PL/SQL procedure successfully completed.

SQL> CREATE OR REPLACE VIEW mnist_test_set
  2  AS
  3      SELECT to_char(img_val) img_lbl,
  4             b.*
  5        FROM imgs_test_val a, imgs_test b
  6       WHERE a.img_id = b.img_id;

View created.

SQL> ALTER VIEW mnist_test_set ADD CONSTRAINT test_set_pk PRIMARY KEY (img_id)
  2                               DISABLE NOVALIDATE;

View altered.

SQL>

On dispose maintenant de deux vues MNIST_TRAINING_SET et MNIST_TEST_SET présentant les données du dataset dans un format exploitable pour les fonctions R de construction de modèles de réseau de neurones.

Les scripts exécutés ci-dessus sont accessibles ici: load_mnist_training_set et load_mnist_test_set.

A suivre…

Laisser un commentaire

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