Deep learning, BFGS, GSL, OpenMP et CUDA
lundi 20 mars 2017 à 23:13Il m'arrive par moment de renouer avec les travaux de recherche de ma jeunesse : les réseaux de neurones. J'ai d'ailleurs écrit ici même quelques billets sur le sujet, dans une série non terminée.
Je tombe régulièrement sur des articles consacrés au deep learning, nouvelle terminologie à la mode remettant en scène les outils de ma jeunesse. Alors je creuse un peu plus, rebondis de publication en publication, jusqu'à retrousser les manches et ressortir mes vieux rêves.
Bien sur, le temps est passé, et de nombreuses avancées ont eu lieu. Mais si j'ai appris une chose de mes années de jeune chercheur, c'est que tout est possible pour qui s'en donne la peine. Je n'ai donc aucune honte à remettre mes habits d'étudiant et à lire toute une bibliographie sur ces sujets.
J'ai recommencé il y a quelques semaines. Une heure par ci, deux heures par là, prises sur mes soirées et mes week-ends, entre deux occupations plus sérieuses. J'ai commencé à apprendre le langage Python, surtout pour sa simplicité. Je suis loin d'en avoir fait le tour et nous nous apprivoisons doucement.
Il faut dire que j'ai enseigné pendant dix ans le langage C... Et que j'aime beaucoup son côté "proche de la machine". Je passe donc souvent de Python au langage C, et depuis quinze jours, j'écris et je réécris un ensemble de programmes de simulation de réseaux de neurones et d'optimisation.
Il est vrai que j'ai découvert sur internet beaucoup d'outils extraordinaires, comme par exemple la bibliothèque mathématique GSL avec laquelle je joue beaucoup, en particulier avec la fonction d'optimisation multidimensionnelle gsl_multimin_fdfminimizer_vector_bfgs2 qui implémente l'un des algorithmes d'optimisation avec lequel j'ai le plus travaillé dans ma jeunesse : BFGS.
Mais rien ne vaut l'écriture soi-même d'un tel algorithme d'optimisation. Cela permet d'en comprendre les subtilités, surtout que sa mathématique reste encore à ma portée, et de l'adapter à son problème précis, le tout piloté par une classique recherche linéaire basée sur les conditions de Wolfe et Powell (attention allergiques aux maths s'abstenir ;-). Comme je n'ai pas de problème précis à régler, je joue avec un problème classique de classification de chiffres manuscrits issus de la base de donnée MNIST.
Je suis encore très loin des performances des meilleurs algorithmes, mais au moins, cela me permet de tester quelques idées.
J'ai donc délaissé provisoirement le langage Python pour écrire un programme en langage C et m'amuser avec des tableaux de pointeurs, des allocations de mémoire et du calcul de matrices de grandes tailles.
En effet, l'apprentissage supervisé d'un réseau de neurones consiste à trouver le meilleur jeu de coefficients permettant de minimiser une fonction d'erreurs. Dans le problème qui m'occupe (la reconnaissance de caractères manuscrits), les entrées sont des images 28x28 en 255 niveaux de gris. Cela fait quand même 784 entrées, plus l'entrée constante qui permet de passer d'un espace vectoriel à un espace affine, soit 785 neurones d'entrée.
Ces 785 entrée injectent les pixels dans un réseau de neurones complètement connectés (je n'aime pas les réseaux à couche cachés, j'ai toujours préféré sa généralisation complètement connectée). Le réseau possède une sortie unique, si l'on code la réponse de 0 à 9, ou dix sorties si l'on préfère un codage hypercube (par exemple chaque chiffre sera codé par 9 zéros et un 1 sur sa sortie correspondante : 7=0000000100) qui semble être la représentation privilégiée.
Un réseau typique dans mon cas sera constitué de 785 entrées, N neurones cachés et 10 neurones de sortie. Si N vaut par exemple 25, cela donne 28 025 coefficients à calculer... C'est-à-dire un vecteur gradient à 28 025 composantes et une matrice "approximation de l'inverse du Hessien" de 28 025 x 28 025 termes, soit plus de 785 millions de nombres réels double précision... Il s'agit de ne pas se tromper dans les "malloc" pour éviter les "segmentation faults" !
Je suis en train de tester une version modifiée par mes soins de l'algorithme BFGS où cette grande matrice est remplacée par N matrices plus petites.
Mes programmes sont désespéramment longs dans leurs calculs sur mon pauvre PC perso, un "vieux" i7 à 8 cœurs. Constatant qu'un seul cœur était mis à contribution, je me suis tourné avec un peu d'appréhension vers le calcul parallèle. Et j'ai découvert (ne riez pas) l'interface de programmation OpenMP : quelques lignes de directives bien placées, et hop, le programme utilise les 8 cœurs de ma machine. C'est magique !
Je commence enfin à avoir des résultats corrects avec l'apprentissage de mon réseau de 25 neurones sur ce fichu problème de reconnaissance de chiffres manuscrits.
Les semaines passent, le temps me glisse entre les doigts. J'aimerais bosser un peu la question de l'utilisation de mon GPU à travers la bibliothèque CUDA, surtout que je peux accéder au boulot à une carte NVidia Tesla (pendant quelques minutes, histoire de voir si j'arrive à programmer une multiplication matricielle). Si j'arrive à maîtriser CUDA, alors il me faudra négocier avec Mme Zythom l'achat d'une carte NVidia supportant cette technologie et accessible financièrement (parce que la NVidia Tesla K80 à 7000 euros, ça va pas être possible...)
Encore de longues soirées en perspective, à regarder évoluer les coefficients de mes petits réseaux de neurones...
Ensuite, dès que j'en aurai le courage, je réattaque TensorFlow que j'ai lâchement abandonné en attendant des tutos plus détaillés.
Si mes neurones réels ne flanchent pas d'ici là ;-)
Je tombe régulièrement sur des articles consacrés au deep learning, nouvelle terminologie à la mode remettant en scène les outils de ma jeunesse. Alors je creuse un peu plus, rebondis de publication en publication, jusqu'à retrousser les manches et ressortir mes vieux rêves.
Bien sur, le temps est passé, et de nombreuses avancées ont eu lieu. Mais si j'ai appris une chose de mes années de jeune chercheur, c'est que tout est possible pour qui s'en donne la peine. Je n'ai donc aucune honte à remettre mes habits d'étudiant et à lire toute une bibliographie sur ces sujets.
J'ai recommencé il y a quelques semaines. Une heure par ci, deux heures par là, prises sur mes soirées et mes week-ends, entre deux occupations plus sérieuses. J'ai commencé à apprendre le langage Python, surtout pour sa simplicité. Je suis loin d'en avoir fait le tour et nous nous apprivoisons doucement.
Il faut dire que j'ai enseigné pendant dix ans le langage C... Et que j'aime beaucoup son côté "proche de la machine". Je passe donc souvent de Python au langage C, et depuis quinze jours, j'écris et je réécris un ensemble de programmes de simulation de réseaux de neurones et d'optimisation.
Il est vrai que j'ai découvert sur internet beaucoup d'outils extraordinaires, comme par exemple la bibliothèque mathématique GSL avec laquelle je joue beaucoup, en particulier avec la fonction d'optimisation multidimensionnelle gsl_multimin_fdfminimizer_vector_bfgs2 qui implémente l'un des algorithmes d'optimisation avec lequel j'ai le plus travaillé dans ma jeunesse : BFGS.
Mais rien ne vaut l'écriture soi-même d'un tel algorithme d'optimisation. Cela permet d'en comprendre les subtilités, surtout que sa mathématique reste encore à ma portée, et de l'adapter à son problème précis, le tout piloté par une classique recherche linéaire basée sur les conditions de Wolfe et Powell (attention allergiques aux maths s'abstenir ;-). Comme je n'ai pas de problème précis à régler, je joue avec un problème classique de classification de chiffres manuscrits issus de la base de donnée MNIST.
Je suis encore très loin des performances des meilleurs algorithmes, mais au moins, cela me permet de tester quelques idées.
J'ai donc délaissé provisoirement le langage Python pour écrire un programme en langage C et m'amuser avec des tableaux de pointeurs, des allocations de mémoire et du calcul de matrices de grandes tailles.
En effet, l'apprentissage supervisé d'un réseau de neurones consiste à trouver le meilleur jeu de coefficients permettant de minimiser une fonction d'erreurs. Dans le problème qui m'occupe (la reconnaissance de caractères manuscrits), les entrées sont des images 28x28 en 255 niveaux de gris. Cela fait quand même 784 entrées, plus l'entrée constante qui permet de passer d'un espace vectoriel à un espace affine, soit 785 neurones d'entrée.
Ces 785 entrée injectent les pixels dans un réseau de neurones complètement connectés (je n'aime pas les réseaux à couche cachés, j'ai toujours préféré sa généralisation complètement connectée). Le réseau possède une sortie unique, si l'on code la réponse de 0 à 9, ou dix sorties si l'on préfère un codage hypercube (par exemple chaque chiffre sera codé par 9 zéros et un 1 sur sa sortie correspondante : 7=0000000100) qui semble être la représentation privilégiée.
Un réseau typique dans mon cas sera constitué de 785 entrées, N neurones cachés et 10 neurones de sortie. Si N vaut par exemple 25, cela donne 28 025 coefficients à calculer... C'est-à-dire un vecteur gradient à 28 025 composantes et une matrice "approximation de l'inverse du Hessien" de 28 025 x 28 025 termes, soit plus de 785 millions de nombres réels double précision... Il s'agit de ne pas se tromper dans les "malloc" pour éviter les "segmentation faults" !
Je suis en train de tester une version modifiée par mes soins de l'algorithme BFGS où cette grande matrice est remplacée par N matrices plus petites.
Mes programmes sont désespéramment longs dans leurs calculs sur mon pauvre PC perso, un "vieux" i7 à 8 cœurs. Constatant qu'un seul cœur était mis à contribution, je me suis tourné avec un peu d'appréhension vers le calcul parallèle. Et j'ai découvert (ne riez pas) l'interface de programmation OpenMP : quelques lignes de directives bien placées, et hop, le programme utilise les 8 cœurs de ma machine. C'est magique !
Je commence enfin à avoir des résultats corrects avec l'apprentissage de mon réseau de 25 neurones sur ce fichu problème de reconnaissance de chiffres manuscrits.
Les semaines passent, le temps me glisse entre les doigts. J'aimerais bosser un peu la question de l'utilisation de mon GPU à travers la bibliothèque CUDA, surtout que je peux accéder au boulot à une carte NVidia Tesla (pendant quelques minutes, histoire de voir si j'arrive à programmer une multiplication matricielle). Si j'arrive à maîtriser CUDA, alors il me faudra négocier avec Mme Zythom l'achat d'une carte NVidia supportant cette technologie et accessible financièrement (parce que la NVidia Tesla K80 à 7000 euros, ça va pas être possible...)
Encore de longues soirées en perspective, à regarder évoluer les coefficients de mes petits réseaux de neurones...
Ensuite, dès que j'en aurai le courage, je réattaque TensorFlow que j'ai lâchement abandonné en attendant des tutos plus détaillés.
Si mes neurones réels ne flanchent pas d'ici là ;-)