Reconnaissance d écriture de chiffres et accélération à l aide d un kd-arbre Ensimag 1A - Préparation au Projet C Année scolaire 2011 2012 1 Présentation L objet de ce projet est de réaliser un petit programme de reconnaissance d écriture de chiffres. Le programme que vous allez écrire prendra une ou plusieurs images en entrée, représentant un chiffre dessiné par l utilisateur. En sortie, votre programme affichera le chiffre reconnu dans chaque image donnée, après en avoir effectué l analyse. (a) (b) FIGURE 1 (a) Image en niveau de gris de 28 28 pixels, représentant le chiffre 3. (b) Quelques exemples d images de chiffres issus de la base MNIST. Comme illustré en figure 1(a), l image d un chiffre fourni à votre programme est composée d un rectangle de pixels, chacun représenté par une valeur entre 0 et 255 encodant son niveau de gris. Cette image n est donc pas trivialement interprétable par le programme, pour lequel il est un tableau de valeurs brutes. Pour trouver le chiffre représenté, il faut faire appel à un algorithme de reconnaissance de motif. Il existe de nombreux algorithmes de reconnaissance ; une majorité d entre eux utilise une base d apprentissage, c est à dire une liste d images exemples qui ont préalablement été étiquetées par un ou plusieurs experts (ici humain). La figure 1(b) illustre quelques éléments de la base de chiffres MNIST 1, obtenue en faisant écrire les 10 chiffres à plusieurs milliers de personnes. L expert étiquette chaque image, c est à dire donne, pour chaque image de la base, le chiffre correspondant à son interprétation de l image. Le programme de reconnaissance fournit alors pour toute nouvelle image test (ne figurant pas dans la base), une étiquette en extrapolant à partir des exemples de la base. Parmi les nombreux algorithmes de reconnaissance de motifs existant, il en existe un très simple, qui fait l objet de ce projet. Il s agit de l algorithme de recherche du plus proche voisin (Nearest Neighbor 1. http://yann.lecun.com/exdb/mnist/ 1
search). Celui-ci consiste, pour une image test donnée, à trouver la plus proche dans la base d exemples étiquetés, et à renvoyer l étiquette correspondante associée dans la base. Cette recherche du plus proche voisin peut être naïve (exhaustive), auquel cas l image test est comparée à toutes les images de la base d apprentissage. Mais ceci n est pas très efficace, particulièrement si l on souhaite reconnaître un grand nombre de chiffres. C est pourquoi on précalcule en général une structure intermédiaire facilitant la recherche et permettant d éviter le parcours exhaustif. Une des structures les plus utilisées pour accélérer cette recherche est le kd-arbre[2]. Vous allez implémenter les structures et algorithmes nécessaires à cette recherche, détaillés ci-dessous. 2 Distance entre images Pour trouver l image de la base la plus «proche» d une image test donnée, il faut définir une distance entre deux images. Pour cela on considère que chaque image d un chiffre est centrée correctement sur le chiffre, et d une résolution carrée d de taille raisonnable. C est le cas des images de la base MNIST, qui font 28 28 pixels. Une fois centrées, comparer deux images se ramène à comparer deux à deux leurs pixels. On peut alors traiter chaque image de taille d d comme un grand vecteur de valeurs en niveau de gris, de dimension d 2. En notant une image I, une distance d(i 1, I 2 ) peut alors être définie sur la base des normes L p classiques sur les vecteurs : D p (I 1, I 2 ) = x {1,,d 2 } (I 1 (x) I 2 (x)) p 1 p, (1) où I i (x) désigne le niveau de gris du pixel x. En particulier, par exemple, la norme euclidienne entre deux images est très utilisée (p = 2) : D 2 (I 1, I 2 ) = x {1,,d 2 } (I 1 (x) I 2 (x)) 2 (2) Le coût de calcul de cette distances est en O(d 2 ), c est pourquoi, avant de traiter les images, on en réduit en général la dimension, en faisant une mise à l échelle de l image à une dimension plus petite : typiquement d = 7 ou 8. Une fois la distance choisie, et la base d apprentissage chargée, l algorithme du plus proche voisin dans sa version naïve/exhaustive est très simple à écrire. Reconnaître une image test consiste en une simple boucle calculant sa distance à chaque image exemple en gardant l image la plus proche. Implémentez l algorithme du plus proche voisin version naïve, en vous appuyant sur les structures de données et fonctions fournies pour le chargement d images (cf. Annexe). 3 Accélération avec un kd-arbre La complexité de la reconnaissance en version naïve est en O(Nd 2 ) avec N le nombre d éléments dans la base d apprentissage, et O(P Nd 2 ) si l on veut reconnaître P images tests. Le kd-arbre [2, 1] est un arbre binaire permettant de réduire cette complexité. Son but est d appliquer une stratégie de type «diviser-pour-régner» à la recherche, en stockant les images de la base de manière judicieuse. Le principe est une généralisation de la structure d arbre binaire de recherche, pour des données à plusieurs dimensions. Chaque nœud interne de l arbre partitionne l ensemble des images en deux sous-ensembles de taille équivalente selon la valeur d un pixel choisi. Ainsi, lors d une recherche du plus proche voisin d une image, on pourra progressivement descendre l arbre en fouillant récursivement l un ou l autre de ces sous-ensembles, et obtenir un comportement logarithmique dans les meilleurs cas. 2
3.1 Propriété du kd-arbre Chaque nœud interne de l arbre, stocke une image séparatrice I s et une coordonnée de séparation x s (l indice d un pixel), qui partitionne les images en deux sous-ensembles, accessibles via le fils gauche et le fils droit de l arbre. Toute image I du sous-ensemble gauche est telle que I(x s ) < I s (x s ), c est à dire que le niveau de gris du pixel séparateur de I est inférieur à celui du pixel séparateur de l image séparatrice. Réciproquement toute image I du sous-ensemble droit est telle que I(x s ) I s (x s ). Un arbre est illustré en figure 2. 230 10 3 67 47 243 35 23 65 223 23 113 28 243 35 45 240 12 23 57 49 197 120 98 228 56 12 77 FIGURE 2 Un exemple de kd-arbre (deux premiers niveaux seulement). Pour simplifier l exemple, l arbre est représenté avec des images de taille 2 2 (4 valeurs de pixels), représentées ici en chaque noeud. Le carré de couleur représente le pixel séparateur à un nœud donné. L ensemble des images stockées à gauche ont un pixel de valeur inférieure, celles stockées à droite un pixel de valeur supérieure. En résumé, chaque nœud interne de l arbre stocke donc les informations suivantes (voir structure imposée en Annexe) : L indice du pixel séparateur x s. Un pointeur sur l image séparatrice I s stockée dans le nœud. Les pointeurs sur les racines des sous arbres gauches et droits. 3.2 Construction d un kd-arbre La construction d un kd-arbre est récursive. La fonction de construction d un arbre prend en entrée l ensemble des images à structurer. Une bonne construction doit scinder cet ensemble d images en deux sous-ensembles de taille équivalente pour obtenir un arbre équilibré. Les deux sous-arbres gauche et droit sont ensuite construits par récursion sur ces sous-ensembles. Sélection du pixel séparateur. Un critère naïf facile à implémenter dans un premier temps, est de sélectionner un pixel séparateur aléatoirement. Mais il est peu efficace pour la recherche. Un critère très efficace est de choisir un pixel avec la plus large plage de valeurs sur l ensemble des images à partitionner : cela permet de sélectionner un pixel qui sera discriminant pour la recherche. Pour le mettre en oeuvre, on pourra calculer la différence entre max et min des valeurs de chaque pixel sur l ensemble des images, pour conserver celui aux valeurs les plus étendues. Sélection de l image séparatrice. L algorithme de construction sélectionne l image permettant d obtenir deux sous-ensembles de taille équivalente. Pour cela, il trie l ensemble des images selon la valeur (niveau de gris) du pixel séparateur, pour enfin stocker l image médiane de l ensemble dans le nœud. Retour à l algorithme naïf pour les feuilles. L algorithme de construction ci-dessus pourrait être poursuivi jusqu à obtenir des ensembles d images (et donc des feuilles) vides. Pour gagner en stockage 3
et en rapidité de traitement récursif, on choisit d arrêter la récursion lorsque le nombre d images restant passe en dessous d un certain seuil (un paramètre de votre méthode, par exemple 10). Les feuilles stockeront donc un ensemble d images non partitionnées, dans un ordre quelconque. On impose pour cela une structure de données basée sur une union, permettant de stocker les deux types de nœuds de l arbre, interne et feuilles (cf. Annexe). 3.3 Recherche du plus proche voisin dans un kd-arbre La recherche dans un kd-arbre repose sur le principe de la séparation et évaluation (branch and bound en anglais). C est à dire que les données sont séparées et hierarchisées, pour permettre une sélection du plus proche voisin la plus économe possible, en élagant au plus tôt toute branche dont l exploration est inutile. Ici, les données ont été séparées spatialement lors de la construction de l arbre. Chaque couple (pixel,image) séparateur dans un nœud interne créée un hyperplan séparateur dans l espace des images, le scindant en deux sous-volumes contenant les sous-ensembles gauche et droit d images. La figure 3 illustre ce phénomène dans le cas d un espace de recherche à deux dimensions (correspondant au cas simplifié où les images n auraient que 2 pixels, pour permettre la visualisation). [5,8] (a) (b) (c) FIGURE 3 (a) Exemple de kd-arbre pour 4 images à 2 pixels, deux nœuds internes et deux feuilles. (b) Visualisation des hyperplans séparateurs correspondants dans l espace des images à deux pixels (abscisse : niveau de gris du premier pixel, ordonnée : celui du deuxième pixel). Il faut imaginer un espace plus général et volumique pour des dimensions supérieures à 2, mais le principe est identique. (c) Principe d élagage des feuilles. Une requête recherchant l image la plus proche de l image test [5,8] n a pas besoin d examiner la feuille contenant [6,3], ni de la comparer à [2,5], dès lors que [8,9] est examinée en premier. En effet, ni le plan séparateur passant par [2,5], ni le sous-espace de la feuille [6,3] n intersecte la sphère maximale donnée par la distance au meilleur candidat [8,9]. En revanche l hyperplan passant par [3,8] intersecte cette sphère et [3,8] devra donc être examinée. Ce qui permettra de trouver que [3,8] est le plus proche voisin de [5,8]. Tiré de [3]. La phase d évaluation des nœuds se fait récursivement, en conservant à tout instant du parcours l image candidate actuellement la plus proche de l image test ainsi que la distance correspondante. Cette distance est une borne maximale et définit une sphère maximale dans laquelle doit se trouver tout meilleur candidat. Ainsi tout sous-volume de l arbre n intersectant pas cette sphère n a pas besoin d être examiné (figure 3(c)). Cette propriété est vraie pour toute distance de forme exprimée en (1). En pratique pour évaluer cette intersection il suffira de faire un test d intervalle sur les valeurs du pixel séparateur du nœud : un sous arbre est exploré ssi son intervalle intersecte l intervalle couvert par la sphère maxi- 4
male de recherche. La borne maximale est initialisée à l infini au début du parcours (racine de l arbre). L algorithme d examen d un arbre aura donc les étapes suivantes : Si le nœud examiné est une feuille, comparer l image test à toute image du nœud pour conserver le plus proche et mettre à jour la borne max en conséquence. Pour les nœuds internes, réaliser les 3 étapes suivantes : 1. Examiner le sous-arbre correspondant au volume incluant l image test. Cet examen met à jour la borne et le candidat. 2. Si le plan séparateur passant par l image stockée dans le nœud intersecte la sphère maximale, alors évaluer la distance de cette image distance à l image test. Si inférieure à la borne maximale, mettre à jour celle-ci : l image de ce nœud devient le meilleur candidat. 3. Si le volume correspondant au sous arbre opposé (ne contenant pas l image test) intersecte la sphère maximale, alors examiner ce sous arbre. Cet examen met à jour la borne et le candidat. L ordre est important ici : on examine les candidats du plus proche au plus éloigné selon la valeur du pixel séparateur : tout d abord le sous-arbre ayant le plus de chances de contenir le plus proche voisin (1) puis le nœud lui-même (2), et enfin le sous espace opposé (3). Cela permet de trouver rapidement la borne la plus restrictive possible. Lorsque tous les nœuds pertinents ont été examinés, renvoyer le meilleur candidat, qui est le plus proche voisin dans la base. 4 Travail demandé Pour le mini-projet, il est demandé de réaliser en langage C le module de construction du kd-arbre ainsi que deux programmes décrits ci-dessous. 4.1 Module kd-arbre Le mini-projet devra fournir un module de recherche du plus proche voisin basé sur un kd-arbre. Les fonctionnalités suivantes devront y figurer : types et fonctions pour les distances entre images construction et allocation d un kd-arbre recherche dans un kd-arbre destruction d un kd-arbre le module devra être paramètrable par une fonction d évaluation de distance image (type pointeur de fonction, donné en Annexe) 4.2 Progamme attendu Nous souhaitons trouver dans l archive un programme recognize, qui prend en entrée une seule image de type pgm et renvoie sur la sortie standard uniquement le chiffre reconnu. Les arguments seront fournis via ligne de commande. Les options en ligne de commande devront permettre de spécifier si l on applique l algorithme naïf ou le kd-arbre, ainsi que la dimension d à laquelle les images seront réduites pour la reconnaissance. 4.3 Recommandations d implémentation Représentation des ensembles d images. Pour éviter toute duplication des données images lors de la manipulation d ensembles d images, il est préconisé d utiliser un tableau de pointeurs d images pour les représenter. La manipulation de l ensemble d images ne nécessite alors aucune copie d image ; utiliser quicksort (librairie standard C) pour effectuer le tri à la construction de l arbre. On pourra utiliser un tableau temporaire de pointeurs sur les valeurs d un même pixel pour un ensemble d images, pour faciliter l appel à quicksort. 5
Tests recommandés Vous écrirez et fournirez tout test que vous jugerez pertinent permettant de vérifier la correction du programme. Le programme devra être exempt de fuite mémoire et être débogué. Pour contrôler le bon fonctionnement du module, il est conseillé de : comparer l exécution à celle de l algorithme naïf. contrôler le temps d éxécution du programme avec la commande UNIX time compter le nombre de nœuds visités et le nombre de distances image calculées dans le cadre des recherches dans l arbre 5 Extensions facultatives En extension, vous pouvez implémenter un programme perf qui test les images de la base de test MNIST (voir annexe) et renvoie sur la sortie standard le taux de reconnaissance (nombre d images correctement reconnues / nombre d image tests total). Les options en ligne de commande devront permettre de spécifier si l on applique l algorithme naïf ou le kd-arbre, la dimension d, mais aussi le nombre d images testées de la base MNIST (on évitera de tester systématiquement l ensemble complet des 10 000 images tests, ce qui prend plusieurs minutes). Préférer travailler avec un sous ensemble de 10, 100 ou 1 000 images tests pour le débogage. On utilisera pour cela la fonction resize_array du module array (cf. Annexe). 6 Annexe 6.1 Structure de données imposée Une structure de données vous est imposée pour le kd-arbre, permettant la représentation des types de nœuds sous forme d une union node de deux structures (interne : kdbranch, feuille : kdleaf). La caractérisation du nœud repose sur un type enuméré kdnode_type, et l accès à l une ou l autre des interprétations du nœud se fera en fonction, avec le champs leaf et branch de l union. Chaque image est supposée stockée comme un tableau de d 2 pixels stockés, indexable de 0 à d 2 1 (une image est donc de type uint8_t*). s t r u c t kdnode ; / noeud i n t e r n e / s t r u c t kdbranch { u i n t 8 _ t split_image ; / p o i n t e u r sur l image s e p a r a t r i c e / u i n t 3 2 _ t index ; / i n d i c e du p i x e l s e p a r a t e u r / s t r u c t kdnode l e f t, r i g h t ; } ; / f e u i l l e / s t r u c t kdleaf { u i n t 3 2 _ t s i z e ; u i n t 8 _ t images ; / images de l a f e u i l l e ( t a b. de p o i n t e u r s ) / } ; typedef enum { BRANCH, / c a r a c t e r i s a t i o n noeud i n t e r n e / LEAF / c a r a c t e r i s a t i o n f e u i l l e / } kdnode_type ; / s t r u c t u r e de noeud du kd a r b r e / s t r u c t kdnode { kdnode_type type ; 6
} ; union { } node ; s t r u c t kdleaf l e a f ; s t r u c t kdbranch branch ; / s t r u c t u r e e n c a p s u l a n t un kd a r b r e / s t r u c t kdtree { const s t r u c t array d a t a s e t ; / p o i n t e u r sur images de l a b a s e / s t r u c t kdnode root ; / r a c i n e du kd t r e e / image_distance d i s t _ f u n c ; / p o i n t e u r de f o n c t i o n de d i s t a n c e / u i n t 3 2 _ t nb_pixel ; / nb de p i x e l s d une image / u i n t 3 2 _ t l i n e a r _ c u t o f f ; / t a i l l e max d e s f e u i l l e s / } ; 6.2 Modules et données fournis Nous vous fournissons deux modules, image et array, permettant de manipuler des images. Module array. Le module array définit un allocateur et accesseur de tableau d images, avec la propriété que toutes les images d un même tableau ont des dimensions communes données par get_width et get_height. / c r e a t e s a 3 e n t r y array, i n d e x e d by image, x and y ; i n d i c a t e p i x e l s i z e in b y t e s / s t r u c t array c r e a t e _ a r r a y ( u i n t 3 2 _ t size, u i n t 3 2 _ t width, u i n t 3 2 _ t height, u i n t 3 2 _ t p i x e l _ s i z e ) ; / r e s i z e number o f images / void r e s i z e _ a r r a y ( s t r u c t array array, u i n t 3 2 _ t new_size ) ; / r e c l a i m a r r a y memory / void f r e e _ a r r a y ( s t r u c t array array ) ; / image a c c e s s o r : g i v e s p o i n t e r t o image as l i n e a r a r r a y o f p i x e l s images can be a d d r e s s e d as ( u i n t 8 _ t ) g e t _ e l e m ( a, elem ) [ y g e t _ w i d t h + x ] / void get_elem ( const s t r u c t array a, u i n t 3 2 _ t elem ) ; / number o f images in a r r a y a / u i n t 3 2 _ t g e t _ s i z e ( const s t r u c t array a ) ; / g e t s i z e o f s c a l a r v a l u e in b y t e s, as d e f i n e d a t a r r a y c r e a t i o n / u i n t 3 2 _ t g e t _ s c a l a r _ s i z e ( const s t r u c t array a ) ; / g e t width o f image a r r a y / u i n t 3 2 _ t get_width ( const s t r u c t array a ) ; / g e t h e i g h t o f image a r r a y / u i n t 3 2 _ t get_height ( const s t r u c t array a ) ; / g e t i n d e x o f image from i t s d a t a p o i n t e r o b t a i n e d from g e t _ e l e m / u i n t 3 2 _ t get_index ( const s t r u c t array a, void data ) ; Module image. Le module image fournit des fonctions utilitaires permettant de charger les données de la base MNIST - images et étiquettes (labels). Il permet également le chargement et la sauvegarde 7
de fichiers image au format pgm, utiles pour le débogage et pour écrire les programmes d application. Enfin il fournit une fonction utilitaire permettant de réduire les dimensions des images d un tableau d images (image_downscale), pour passer des dimensions MNIST 28 28 à une taille plus réduite, 7 7 par exemple. Les fonctions de chargement load_* crééent toutes un tableau d images ou d étiquettes (appel create_array en interne). Il faudra donc pour chacune bien appeler free_array pour libérer l espace correspondant. Pour le cas des étiquettes il s agit d images de taille 1 1, avec un seul entier de type uint8_t représentant l étiquette (valeur du chiffre). Pour la fonction load_pgm_image chargeant une image pgm, un tableau d une seule image est créé avec l image chargée. / d o w n s c a l e e v e r y image in t h e a r r a y a t o new ( s m a l l e r ) s i z e width x h e i g h t / s t r u c t array image_downscale ( const s t r u c t array a, u i n t 3 2 _ t width, u i n t 3 2 _ t height ) ; / l o a d image a r r a y in MNIST f o r m a t : s e e h t t p : / / yann. l e c u n. com / exdb / mnist / / s t r u c t array load_image_array ( char filename ) ; / l o a d l a b e l a r r a y in MNIST f o r m a t : s e e h t t p : / / yann. l e c u n. com / exdb / mnist / / s t r u c t array l o a d _ l a b e l _ a r r a y ( char filename ) ; / l o a d a pgm as s i n g l e image a r r a y / s t r u c t array load_pgm_image ( char filename ) ; / s a v e a r r a y e l e m e n t ( elem ) as a pgm as s i n g l e image a r r a y / void save_pgm_image ( char filename, const s t r u c t array a, u i n t 3 2 _ t elem ) ; / d e f i n e f u n c t i o n p o i n t e r t y p e f o r d i s t a n c e s between two images / typedef f l o a t ( image_distance ) ( const u i n t 8 _ t imga, const u i n t 8 _ t imgb, u i n t 3 2 _ t width, u i n t 3 2 _ t height ) ; / e u c l i d e a n d i s t a n c e ( with i m a g e _ d i s t a n c e p r o t o t y p e ) / f l o a t image_euclidean_distance ( const u i n t 8 _ t imga, const u i n t 8 _ t imgb, u i n t 3 2 _ t width, u i n t 3 2 _ t height ) ; Données fournies. Nos fournissons les fichiers de la base MNIST contenant 10 000 images test (t10k-images-idx3-ubyte) et 60 000 images de la base d apprentissage (train-images-idx3-ubyte). Toutes deux sont étiquetées, les fichiers d étiquette sont réciproquement t10k-labels-idx1-ubyte et train-labels-idx1-ubyte. La base d apprentissage est étiquetée par définition, celle des tests l est aussi pour vérification des performances de l algorithme. On peut pour cela calculer des statistiques sur le taux de succès/erreur de la reconnaissance, connaissant la vraie réponse pour les images de la base de test. Références [1] Jon Louis Bentley. Multidimensional divide-and-conquer. Commun. ACM, 23(4) :214 229, April 1980. [2] Jerome H. Friedman, Jon Louis Bentley, and Raphael Ari Finkel. An algorithm for finding best matches in logarithmic expected time. ACM Trans. Math. Softw., 3(3) :209 226, September 1977. [3] A. Moore. An introductory tutorial on kd-trees. Technical Report Technical Report No. 209, Computer Laboratory, University of Cambridge, Pittsburgh, PA, 1991. 8