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…