La carte graphique, CUDA et l’expert

Je me suis lancé passionnément dans le calcul parallèle (cf billet précédent). Pour cela, je me suis offert une carte graphique à un prix abordable et que je destine au calcul intensif.

Imaginez mon impatience en attendant la livraison… Le jour J arrive et me voilà en train de démonter ma machine pour y ajouter ce magnifique objet technologique.

A ce stade du récit, il me faut faire un petit rappel : je suis adepte de GNU/Linux depuis de nombreuses années. J’aime tester de nouvelles distributions, j’aime l’univers GNU/Linux, ou FreeBSD. J’ai fait ma thèse sous Domain/OS et HP-UX. J’ai bien connu la distribution Yggdrasil qui était la première sur cédérom, c’est dire si je suis vieux. A titre professionnel, j’ai d’abord choisi Yggdrasil, puis Slackware, puis Red Hat, et enfin depuis de nombreuses années, Debian. A titre privé, j’ai beaucoup aimé Gentoo, puis Ubuntu, pour maintenant utiliser Mint.

La plupart des analyses techniques que je réalise pour mes expertises judiciaires sont faites sous GNU/Linux en utilisant des logiciels en ligne de commande.

Pour autant, j’apprécie la facilité d’installation des distributions GNU/Linux actuelles : il suffit de graver un DVD ou d’utiliser une clef USB et hop, les différents éléments de votre machine sont fonctionnels (en général ;-).

J’aime beaucoup la facilité…

Les plus barbu(e)s d’entre vous se souviennent des heures passées à configurer leur serveur X pour arriver à brancher deux malheureux écrans sur la même carte graphique. C’est un peu là où je vais en (re)venir.

La semaine dernière, lorsque j’ai reçu ma nouvelle carte graphique, j’étais tout excité à l’idée de transformer mon bête ordinateur en bête de calculs parallèles. Après m’être assuré (quand même) que toutes mes données étaient correctement sauvegardées, j’ai démonté ma tour pour y insérer cette nouvelle carte graphique surpuissante.

Premier constat : la carte graphique nécessite une alimentation dédiée, avec un connecteur 8 broches… Affolé, je regarde l’intérieur de ma machine éventrée, et je constate avec joie qu’un connecteur sort de mon boitier d’alimentation avec 6 broches d’un côté et deux de l’autre qui ressemblent fort au truc nécessaire pour faire fonctionner ma carte graphique. Je branche, rien ne fume o/.

Deuxième constat : je dispose de deux emplacements PCI Express me permettant de conserver mon ancienne carte graphique utilisée par mes deux écrans.

Je branche tout ce petit monde et redémarre mon ordinateur. Magie (et travail) de la communauté open source, tout est reconnu par le système d’exploitation, les pilotes par défaut chargés et tout fonctionne parfaitement. Sauf que…

Je ne perds pas de vue mon objectif : faire massivement des calculs. J’ai donc ma carte graphique d’origine sur laquelle sont branchés mes deux écrans, et ma nouvelle carte graphique sur laquelle rien n’est branché et qui va me servir de calculateur GPU. Sauf que…

J’ai soigneusement choisi mon modèle de carte graphique pour qu’elle intègre un GPU Nvidia qui, au moment où j’écris ce billet, est admirablement programmable avec un environnement qui s’appelle CUDA. Avant de casser ma tirelire, j’avais fait quelques essais avec le vieux portable de ma fille (seul appareil de la maison muni d’une carte Nvidia). Sous GNU/Linux Mint, l’installation de l’environnement de développement CUDA se fait très simplement avec la commande “sudo apt-get install cuda”. Après quelques réglages post-installation, me voici donc en train de jouer avec les 1920 cœurs des 15 processeurs graphiques. Je suis aux anges 🙂 Sauf que…

Sous Windows, quand vous installez un nouveau composant ou un nouveau programme, il faut la plupart du temps redémarrer l’ordinateur. Ce n’est pas vrai sous GNU/Linux. J’ai donc pu m’amuser immédiatement pendant plusieurs heures avec mon nouveau jouet avant d’éteindre mon ordinateur.

Mais le lendemain, catastrophe. En démarrant ma machine, plus rien ne fonctionnait. Enfin, “plus rien” non : je n’avais plus d’interface graphique fonctionnelle. Me voici parti pour plusieurs jours de galères “à l’ancienne” pour essayer d’abord de réparer ma configuration graphique, puis pour essayer de comprendre. Comme avant, j’ai écumé les forums, j’ai cherché les tutos, les howto, les manuels correspondant à mon problème…

Pour faire court : je n’ai pas réussi à trouver d’aide.

Oui, je sais. Je suis EXPERT. Je suis EXPERIMENTE (= vieux). Mais non. Je n’ai pas réussi à faire fonctionner deux cartes graphiques (une vieille Radeon et une jeune Nvidia) dont une devait être consacrée à la gestion de mes deux écrans et l’autre à mes calculs massivement parallèles… Le serveur X Windows et les deux pilotes graphiques associés (libres ou propriétaires) ne semblent pas vouloir fonctionner ensembles après redémarrage. Pourtant j’arrive à faire fonctionner l’ensemble AVANT redémarrage…

Je me suis tapé la configuration d’un xorg.conf (fichier qui est sensé avoir disparu).

J’ai abandonné Mint pour installer Ubuntu (comme conseillé par la doc CUDA).

J’ai réussi la configuration avec une seule carte RADEON.

J’ai réussi la configuration avec la seule carte Nvidia.

MAIS je n’ai pas réussi à créer la configuration avec les deux cartes.

Après plusieurs jours de transpiration, j’en suis arrivé à la conclusion suivante : je suis NUL en configuration graphique GNU/Linux.

J’ai donc ravalé ma fierté, retiré ma nouvelle carte graphique, remis ma configuration d’origine GNU/Mint, et j’ai sorti un vieux PC de son placard. J’y ai installé un Ubuntu server tout frais. J’y ai placé ma carte graphique surpuissante. J’ai constaté l’absence d’alimentation dédiée… J’ai acheté cet adaptateur d’alimentation SATA vers carte vidéo PCI Express 8 broches. J’ai attendu trois jours pour la livraison.

Et j’ai maintenant un calculateur dédié aux calculs parallèles.

Je m’y connecte à distance en ssh. Je lui ai consacré un processus de sauvegarde dédié. Il a une place particulière dans mon bureau et dans mon cœur.

Il faut savoir reculer pour mieux avancer.

Avant je programmais un réseau de neurones qui travaillait sur 60 000 exemples d’apprentissage sur plusieurs semaines. Maintenant j’ai 60 000 réseaux de neurones identiques qui travaillent chacun en parallèle sur leur exemple d’apprentissage, en une nuit à une vitesse de folie sur 8 Go de mémoire graphique ultra rapide.

Le rêve d’un vieux chercheur 🙂

Mais cela, c’est une autre histoire.

Deep learning, BFGS, GSL, OpenMP et CUDA

Il 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 28×28 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à 😉

Intelligence et conscience artificielle

De nombreux articles sortent depuis quelques temps sur des objets connectés et/ou des programmes d’assistance sous l’intitulé “IA” pour “intelligence artificielle”. Je suis un peu surpris du terme, puisque certains de ces objets/programmes ne semblent pas si éloignés du vieux programme “Eliza” (1966) dont j’ai déjà parlé ici en 2007. On est quand même encore loin d’arriver à réussir le test de Turing pour pouvoir parler d’intelligence…

J’ai toujours été passionné par cette question de l’intelligence artificielle, au point de commencer ma carrière dans un laboratoire spécialisé sur ce sujet, et à passer un doctorat sur les réseaux de neurones formels (cf ma série non terminée de billets sur le thème réseau de neurones).

Le terme “intelligence artificielle” étant maintenant un peu galvaudé par les gens du marketing, je préfère utiliser l’expression “conscience artificielle” pour évoquer la question qui m’intéresse vraiment : un programme informatique peut-il imiter la capacité des neurones et
créer un esprit, et finalement, l’expérience de la conscience ?

Ma réponse personnelle est que c’est possible, qu’on y arrivera, et que c’est une aventure extraordinaire, qui nous amène sur la terra incognita de la singularité technologique.

Et j’espère voir ça, parce que j’aurai beaucoup de questions à poser à cette conscience artificielle, en espérant qu’elle évitera de me répondre “42” ou “désolé Zythom, j’ai peur de ne pas pouvoir [ouvrir la porte]“…

J’ai aussi une certitude glaçante : la création de la première machine à conscience
artificielle sera la dernière invention que l’Homme aura besoin de
réaliser.

Les scientifiques sont des gens curieux.

Biblio :

Philosophie de l’intelligence artificielle

Singularité technologique

Conscience artificielle

Informatique affective

Réseaux de neurones 3

Ce billet fait partie d’une série qu’il vaut mieux avoir lu avant, mais c’est vous qui voyez.

Nous avons vu que pour jouer avec un neurone, il fallait calculer son potentiel (la somme pondérée des sorties des neurones qui lui sont connectés), puis sa sortie grâce à sa fonction d’activation. Je n’en ai pas encore parlé, mais pour pouvoir modifier les coefficients du réseau, il faut aussi connaître la dérivée de la fonction d’activation du neurone. Idem pour la variable “erreur” dont je parlerai un peu plus tard, pendant la phase d’apprentissage du réseau.

Si vous êtes étudiant et que vous souhaitez travailler sérieusement sur les réseaux de neurones, je vous conseille d’étudier attentivement le code source d’une bibliothèque telle que FANN qu’un lecteur m’a recommandé et qui a l’air très bien. Dans mon cas, je suis partisan d’un travail artisanal qui permet de mieux comprendre les différents mécanismes en jeu. Et puis, j’aime bien le blog de Libon: fabriqué à mains nues, alors…

Pour moi, un neurone, en langage C, c’est donc cela:

Avec cette déclaration, un réseau de neurones peut être le simple tableau suivant: NEUR* neurone[NBMAXNEUR]; où NBMAXNEUR est une constante indiquant le nombre total de neurones (y compris les entrées du réseau).

La création d’un réseau se fera alors de manière dynamique avec un petit programme du type:

Note à moi-même pour plus tard:  ne pas oublier un appel à “free()” pour chaque appel à “malloc()”.

Parmi tous les réseaux de neurones possibles, j’ai choisi de travailler avec un réseau complètement connecté à une seule sortie. Il s’agit du type de réseau possédant le plus de liens possibles entre les neurones.

Il est assez facile à construire:

– le 1er neurone est relié à toutes les entrées du réseau

– le neurone suivant est relié à toutes les entrées du réseau, et à la sortie du premier neurone,

– le Nème neurone est relié à toutes les entrées du réseau, et à la sortie de tous les neurones précédents,

– la sortie du réseau est la sortie du dernier neurone.

Je vous ai fait un petit dessin qui montre ce type de réseau:

Figure 1: Réseau complètement connecté

avec 3 entrées, 3 neurones et une sortie

Dans un réseau de neurones, le cœur du problème, ce qu’il faut rechercher, ce sont les coefficients des liens reliant les neurones entre eux. Le coefficient reliant le neurone j vers le neurone i s’appelle Cij. Par exemple, sur la figure 1, le coefficient reliant 3 à 5 s’appelle C53 (attention au sens).

Pour faire très simple, et suivre la notation utilisée, j’ai choisi une matrice pour stocker les coefficients Cij : double coef[NBMAXNEUR][NBMAXNEUR];

où NBMAXNEUR contient le nombre d’entrées et le nombre de neurones (soit 6 sur la figure ci-dessus). Ainsi, le coefficient C53 est stocké dans coef[5][3]. Ma matrice aura beaucoup de zéros, mais je privilégie la simplicité.

La propagation de l’information au sein du réseau se fait donc de la manière suivante:

La sortie du réseau est donc neurone[NBMAXNEUR-1]->sortie

Les entrées du réseau sont considérées comme des neurones particuliers très simples (pas de liens vers eux, pas de fonction d’activation, potentiel égal à l’entrée).

Le prochain billet sera consacré à l’apprentissage d’un tel réseau de neurones (ie: au calcul des coefficients du réseau). On révisera aussi un peu les fonctions. Il vaut mieux aller doucement.

Réseaux de neurones 2

Ce billet est la suite de celui-ci qu’il est préférable de lire avant mais c’est vous qui voyez.

Après quelques heures passées sur l’apprentissage du langage Go, je me suis résolu à revenir à mes fondamentaux: le langage C. Cela fait 20 ans que je n’ai pas codé sérieusement, et j’ai eu peur de perdre trop de temps à m’initier à un nouveau langage. Ce qui suit va faire sourire tous les développeurs actuels et définitivement me décrédibiliser auprès d’eux…

Il y a 20 ans, j’étais chercheur et je programmais sur une station de calcul Apollo, dans un environnement de développement équivalent à vi (sous Domain/OS). J’appelais quelques routines graphiques de base, à une époque où l’environnement graphique informatique était en pleine révolution. Mes langages favoris étaient le langage OCCAM et le langage C.

Lorsque de chercheur, je suis devenu professeur en école d’ingénieurs, j’ai enseigné le langage C. La mode était alors au Turbo C de Borland. Mon enseignement n’incluait pas la partie graphique. Mes étudiants se contentaient très bien des tableaux de pointeurs de fonctions de structures doublement chaînées, et des envois de données sur le réseau.

Me voici donc aujourd’hui (enfin, il y a quelques mois), à me demander ce qui pouvait être utilisé comme IDE aujourd’hui, avec tous les progrès informatiques. Je me suis dit qu’un environnement multiplateformes pourrait être intéressant, pour profiter du meilleur des univers Windows ou GNU/Linux.

J’ai choisi Code::Blocks.

Me voici donc en train de compiler quelques programmes simples trouvés sur les différents sites d’initiation à cet IDE. Je redécouvre alors la joie de compiler un code source, d’éditer les liens avec les bibliothèques standards, et de voir les premiers “Hello world” à l’écran. J’ai eu une petite pensée pour Dennis MacAlistair Ritchie

Très vite, je me suis retrouvé à écrire quelques procédures de calcul concernant les réseaux de neurones. J’ai créé mon premier réseau, mes premières structures, mes premiers malloc (et à chaque malloc son free correspondant ;-).

Comme 20 ans auparavant, j’ai vite trouvé l’affichage standard limité: il me fallait tracer des courbes, des nuages de points, des évolutions de critères en cours de minimisation. Il me fallait appeler quelques fonctions graphiques…

Et là… Grosse déception !

En 20 ans de foisonnement d’interfaces graphiques et d’amélioration de processeurs spécialisés, aucune bibliothèque graphique SIMPLE n’a l’air de s’être imposée. Un truc du genre: j’ouvre une fenêtre, je dessine un pixel dedans quand je veux et basta. Si je suis sous Windows, ça m’ouvre une fenêtre Windows, si je suis sous GNU/Linux, et bien ça m’ouvre une fenêtre GNU/Linux… Bon, j’avoue que je n’ai pas beaucoup cherché, et je compte un peu sur vous pour me montrer la voie si je me suis fourvoyé.

J’ai choisi la bibliothèque graphique SDL parce que pas mal de sites ont l’air de dire que c’est très bien pour s’initier. Ça tombe bien, parce que je ne souhaite pas devenir un professionnel du graphisme, je veux juste dessiner quelques courbes.

Ce qui m’a un peu surpris, c’est de devoir “bidouiller” Code:Blocks pour que mes premiers programmes utilisant SDL puissent fonctionner (je n’ai pas conservé les messages d’erreur, mais j’ai ramé). Heureusement, pas mal de monde utilise le combo Code::Blocks + SDL et la communauté publie des tutos bien faits.

Me voici donc en train de faire mes premières courbes. Bon, mes programmes sont devenus beaucoup moins lisibles maintenant que je les ai truffé d’appels à des routines graphiques plus ou moins claires, mais j’ai compris les bases du truc. Jusqu’au jour où j’ai voulu tracer une courbe dans une nouvelle fenêtre… En effet, SDL ne permet pas d’ouvrir plusieurs fenêtres. Mais heureusement SDL2 peut le faire! Sauf qu’il faut tout réécrire car les concepts graphiques n’ont rien à voir. Je me suis donc tapé le guide de migration SDL1.2 vers SDL2.0 dans la même journée que l’apprentissage de SDL1.2. Je râle, je râle, mais je remercie tous les développeurs qui consacrent leur vie à créer tous ces outils (et les manuels qui vont avec). Je sais maintenant manipuler (un peu) les pointeurs de fenêtres et de Renderer.

Comme SDL2 est sortie en août 2013, j’ai un peu galéré à trouver comment adapter Code::Blocks pour faire fonctionner mes premiers programmes SDL2 (mais j’ai trouvé!). Et j’ai pleuré des larmes de joie quand j’ai vu mes premières courbes tracées dans deux fenêtres séparées.

J’ai ensuite pu attaquer les choses sérieuses avec la mise au point des routines d’optimisation. J’en ai déjà expliqué une partie dans ce billet. Mes premiers programmes ont consisté à mettre au point les routines suivantes:

– calcul du gradient par rétropropagation de l’erreur

– méthode d’optimisation par descente de gradient à pas constant

– amélioration de la méthode précédente avec calcul économique d’un pas variable (méthode de Wolfe et Powell)

– amélioration de la méthode précédente avec calcul itératif de l’inverse du Hessien (méthode de Broyden, Fletcher, Goldfarb et Shanno).

Je suis toujours bluffé par l’accélération foudroyante des méthodes quasi-newtoniennes pour s’approcher du minimum de la fonction.

J’en suis là aujourd’hui.

J’ai un programme illisible qui fonctionne malgré tout parfaitement: je peux créer un réseau de neurones complètement connecté qui peut apprendre virtuellement n’importe quel ensemble d’apprentissage, dès lors que celui-ci est constitué d’un nombre fini de couples {entrées connues, sorties désirées}. Je suis à la recherche d’un problème pas trop complexe, en évitant si possible tous les problèmes de classification (type mémorisations de visage ou reconnaissance de caractères). J’aimerais plutôt un problème de modélisation, comme par exemple la prédiction des éruptions du “Old Faithful” (si quelqu’un a des données récentes sur ce geyser, avec températures, pression, etc.).

Il me faut du temps pour rendre mes routines plus lisibles, pour sauvegarder les coefficients calculés, pour tester d’autres environnements, pour créer un tableur Excel et OpenOffice, pour trouver un problème intéressant à ma portée…

Il me faut aussi comprendre comment faire pour exploiter toute la mémoire de ma machine. Je n’arrive pas encore à créer des matrices de grandes tailles (du genre 10000×10000 réels double précision). Je suis pour l’instant limité à un réseau d’au maximum 50 neurones et 1224 connexions.

Mais 50 neurones, c’est déjà beaucoup 😉

Réseaux de neurones 1

Depuis que je me suis mis en tête de reprendre mes travaux de recherches sur les réseaux de neurones (lire le billet “désir de vieux quadra“), j’essaye de voir comment aborder cette question sur ce blog.

Je crois que je vais le faire de la manière la plus simple du monde: en mélangeant les billets avec les autres thèmes abordés ici, dans un joyeux chaos qui me correspond tellement bien.

Les lecteurs qui ne sont pas intéressés pourront rapidement zapper les billets qui seront intitulés “réseaux de neurones N”, et ceux éventuellement intéressés pourront s’abonner au flux Rss (ou Atom) de la rubrique “Réseaux de neurones“.

Mais je vous préviens tout de suite, je n’ai aucune idée de où cela va mener, ni du temps que je pourrai y consacrer, ni même si j’arriverai à quelque chose. Ce que j’appelle ici “recherche” est un travail personnel que je souhaite partager sur ce blog. Vous y verrez certainement des défauts, des failles et des maladresses que je vous encourage à signaler.

Je rappelle enfin aux nouveaux lecteurs que ce blog est un blog personnel sans prétention mais pour lequel j’ai des principes d’indépendance.

Je suis allé regarder la page Wikipédia consacrée aux réseaux de neurones, et je la trouve très bien faite. Elle constitue une très bonne introduction à ce billet et je vous invite à aller l’étudier.

Les neurones biologiques peuvent être modélisés d’une manière très très simplifiée de la façon suivante:

– un neurone reçoit de l’information de la part d’un certain nombre d’autres neurones. Ces informations seront supposées être de nature principalement électrique. Je noterai ces informations Xi où i sera le numéro du neurone dans le réseau.

– toutes les liaisons entre neurones ne sont pas équivalentes en importance, il est donc judicieux de pondérer l’information électrique avec un coefficient associé à la liaison concernée. Ce coefficient sera noté Cij où i et j sont les numéros respectifs de deux neurones reliés l’un à l’autre dans le réseau.

– le neurone reçoit toutes les informations pondérées et en fait la somme (comme pour un potentiel électrique). Cette somme sera appelée “potentiel du neurone”. On le notera Vi.

– un neurone ne peut pas avoir une sortie Xi trop importante, sinon il va “griller”. Il faut donc limiter la valeur de Xi par le haut.

– inversement, un neurone ne génèrera un signal d’information Xi que si son potentiel Vi est suffisamment élevé. Il faut donc fixer un seuil d’activation (au dessous duquel le neurone restera inactif).

Toutes ces constations, issues plus ou moins d’observations sur les neurones biologiques, amènent à établir les relations suivantes:

Le potentiel Vi du neurone i = somme des produits ( Cij Xj ) où j parcourt l’ensemble des indices des neurones qui sont reliés au neurone numéro i, où Cij est le poids de la synapse reliant le neurone j au neurone i (on dit également coefficient synaptique) et où Xj est la sortie du neurone j.

La sortie Xi du neurone i = f ( Vi ) où f est une fonction de transformation du potentiel limitant la valeur de la sortie quand Vi est grand et permettant d’avoir une sortie Xi nulle si Vi est trop faible. Cette fonction est appelée fonction d’activation.

Soit, grace à la concision du langage mathématique, pour chaque neurone d’indice i :

où Pi est l’ensemble des indices des neurones envoyant leurs valeurs au neurone i

La fonction d’activation :

Comme indiqué précédemment, cette fonction a pour rôle d’introduire une non linéarité dans le réseau. Elle limite la valeur maximum de la sortie d’un neurone et reste nulle pour des potentiels trop faibles.

La fonction d’activation souvent utilisée est la fonction sigmoïde ou la tangente hyperbolique. Pour ma part, je choisis cette dernière car elle est centrée sur zéro, ce qui permettra d’amorcer plus facilement l’algorithme d’apprentissage, comme nous le verrons plus tard.

Fonction tangente hyperbolique – Image source Wikipédia

En préparant ce billet, je me suis demandé s’il n’était pas plus simple d’utiliser une fonction sinus pour les potentiels compris entre -pi/2 et +pi/2 et deux demi-droites horizontales à -1 et +1 pour le reste. Je n’ai jamais testé, mais cela me semble une idée intéressante pour abréger l’apprentissage (qui dépend des fonctions dérivées). On verra.

Pour simplifier le comportement du futur ensemble de neurones que l’on appellera “réseau de neurones”, je fais l’hypothèse que tous les neurones utilisent la même fonction d’activation, ce qui me permet de retirer l’indice “i” sur le “f” des formules précédentes.

J’ai volontairement omis des formules précédentes le fait que les potentiels (et donc les sorties) des neurones vont varier au cours du temps. Vi et Xi sont donc des fonctions du temps. J’aurais du écrire Vi(t) et Xi(t). Mais comme je vais travailler dans un univers discrétisé, je préfère écrire Vi(n) et Xi(n).

Nous aurons donc comme modèle pour un neurone i à un instant n :

où Pi est l’ensemble des indices des neurones envoyant leurs valeurs au neurone i.

Un schéma est peut-être plus explicite :

Modèle de neurone

La mise en réseau :

Un réseau de neurones de ce type est un ensemble de neurones reliés les uns aux autres de façon à ce que l’information puisse circuler sans boucle pour que tous les potentiels soient calculables à un instant n.

En outre, afin de relier le réseau au monde extérieur, le réseau dispose de capteurs d’entrée qui seront considérés comme des sorties de neurones imposées par le monde extérieur.

Voici un exemple de réseau de 3 neurones avec deux entrées :

Le monde extérieur impose le comportement des neurones d’entrée E1 et E2.

J’ai choisi de mesurer les sorties des deux neurones X4 et X5.

La sortie du neurone numéro 3 est interne au réseau.

Tous les choix faits ici sont pour l’instant arbitraires (nombre d’entrées, nombre de neurones, architecture des connexions, sorties sélectionnées…).

Il est important de comprendre que les six coefficients Cij de ce réseau sont les inconnues à calculer si l’on souhaite que le réseau ait un comportement particulier pour un ensemble de stimuli imposés.

Nous verrons tous cela dans un autre billet.

Pour l’instant, j’ai beaucoup de choses à apprendre sur le langage Go que Stéphane Bortzmeyer m’a fait découvrir sur Twitter, et cela va me prendre beaucoup de temps… Ça parle de parallélisme, de passage de messages sur des canaux, toutes choses prometteuses pour mes futures simulations. Mais je ne dois pas oublier qu’il va aussi me falloir mettre en place mes routines d’optimisation favorites

Je me sens l’âme d’un étudiant (et j’aime ça ;-).

———————————————-

Source image Megaportail

Désir de vieux quadra

J’ai écrit sur ce blog deux billets consacrés à une période de ma vie où j’espérais beaucoup faire progresser la science dans le domaine de l’intelligence artificielle. Le premier, intitulé “Intelligence artificielle“, et le second, intitulé “Minimisation“, portaient en eux une certaine nostalgie de cette époque.

Aujourd’hui, je partage mon existence entre ma famille, mon travail comme responsable informatique et technique dans une école privée d’ingénieurs, mon activité de conseiller municipal dans une ville de 5000 habitants (c’est d’ailleurs bientôt les élections !), mon activité d’expert judiciaire en informatique, et des loisirs comme l’aviron, la lecture de SF, le suivi de l’exploration spatiale, la tenue de ce blog ou la lecture de mon fil Twitter.

J’aime beaucoup cette existence et je me considère comme un homme heureux, très heureux même. Bien sûr, j’ai quelques petits coups de blues comme tout le monde, et il m’arrive de me demander ce que je serais devenu si j’avais fait tel ou tel choix différemment.

Parmi les milliers de choix que j’ai pu faire dans mon existence (je ne crois pas au destin), l’un m’a particulièrement marqué: j’ai quitté un poste de Maître de conférences à Paris où je menais des recherches passionnantes. J’ai fait ce choix pour des raisons parfaitement justifiées, et si c’était à refaire aujourd’hui, je ferai le même choix sans hésiter. L’Amour emporte tout sur son passage… et je ne me voyais pas fonder une famille en région parisienne.

Cela ne m’empêche pas, à quelques mois de mes 50 ans, tout en profitant pleinement de la vie et du temps d’apprentissage que j’espère encore long devant moi, de regarder un peu derrière moi et faire un petit bilan.

Et tout à coup, je me suis dit: et si je reprenais mes recherches sur les réseaux de neurones, en douce, en solo, sur mon temps libre, par petits bouts… Est-ce une tâche possible et surmontable? Saurai-je trouver l’énergie et le temps nécessaires? Je ne sais pas. Mais qui peut répondre à l’avance à ce genre de question?

Il me faut reprendre le fil de mes travaux, arrêtés en 1993. Pour cela, je peux relire mes articles de l’époque, retravailler ma thèse de doctorat pour me rafraîchir la mémoire. Il me faut re-développer de zéro tous les outils logiciels qui me servaient à l’époque pour faire mes simulations. Ce serait l’occasion pour moi d’apprendre un nouveau langage de programmation (Prolog, OCCAM et C ont le charme désuet des langages d’antan) et d’exploiter les possibilités des mémoires et calculs des machines d’aujourd’hui. Il me faut ré-apprendre tous les outils mathématiques dont je vais avoir besoin et que le temps a effacé de ma mémoire: dérivées partielles, distances, représentation d’états, fonctions de Lyapunov… Aurai-je la patience de tout ré-apprendre? Il me faut ré-accepter de me prendre les pieds dans le tapis, d’explorer des voies sans issues, de passer pour un imbécile aux yeux de ceux qui les ont déjà explorées, de faire des bourdes de débutant, de redevenir un débutant…

Je pourrais tenir une chronique de cette activité sur ce blog, qui mélange déjà toutes mes autres activités. Cela m’obligerait à avoir les idées suffisamment claires pour pouvoir les exposer pédagogiquement, même si mon billet sur la minimisation n’a pas brillé sur ce point. Cela m’obligera aussi à afficher plus d’humilité. Cela donnera de l’eau au moulin de mes (dé)tracteurs.

Je me demande toutefois si je ne suis pas en train de courir après une chimère.

Je me demande si ce n’est pas un désir vain de vieux quadra…

On verra bien.

Je tente le coup.

——————————————

Image: Charlie Chaplin et Albert Einstein, lors d’une projection privée du film “Les Lumières de la Ville” (1931).

Minimisation

Je vous ai déjà raconté que j’avais débuté ma carrière comme chercheur en intelligence artificielle. Tout au long de ces cinq années de travail passionnant, j’ai été encadré par un chercheur de très haut niveau, qui a été mon mentor et pour moi un exemple à suivre.

Il m’a vu arriver au laboratoire, alors que j’étais fraichement diplômé de mon école d’ingénieurs, la tête pleine de concepts mal digérés. Il m’a dit: “tu ne sais rien, tu as tout à apprendre, à commencer par les bases…” J’ai encaissé, mais j’ai immédiatement compris qu’il avait raison, sans savoir que je venais de passer un test décisif à ses yeux: la modestie indispensable du chercheur.

Alors, j’ai recommencé par le commencement. Mon univers de recherche s’intéressait à la modélisation non linéaire. J’ai commencé par apprendre ce qu’était réellement la notion de modèle par rapport à la réalité physique. Puis j’ai appris que les outils mathématiques à la disposition des automaticiens étaient très développés dans le domaine linéaire, et que la pratique courante lorsque l’on fait face à un comportement non-linéaire était de linéariser le problème en l’étudiant autour d’un point de fonctionnement. Pratique lorsque cela a un sens, complètement imbécile si l’on doit linéariser en permanence dans des espaces d’états complexes fortement non linéaires.

Et c’est là que j’ai commencé à découvrir que dans l’univers de la recherche, il y a beaucoup de chercheurs qui publient des articles qui s’avèrent être des imbécilités sans nom. Le filtre des patrons de laboratoire, puis celui des comités de lecture des revues scientifiques spécialisés, ne sont pas suffisant pour écarter des études mal menées par non compréhension des outils utilisés.

Cela s’est avéré comique dans mon domaine spécifique, celui des réseaux de neurones.

Sans entrer dans le détail, les réseaux de neurones sur lesquels je travaillais sont constitués de cellules faisant chacune une somme pondérée des signaux se présentant à leurs entrées, somme qui est ensuite transformée par une fonction d’activation (une sigmoïde dans mon cas), résultat qui est ensuite envoyé par chaque cellule à toutes les entrées des neurones qui lui sont connectés.

L’ensemble “cellule + fonction d’activation + entrées + coefficients de poids des entrées” est un (modèle simplifié de) neurone. En assemblant plusieurs neurones, en sélectionnant des entrées et des sorties particulières, vous obtenez un réseau de neurones.

Le fonctionnement de l’ensemble consiste à injecter certaines valeurs aux entrées du réseau et à observer les sorties calculées par le réseau.

L’apprentissage du réseau consiste à essayer de calculer les coefficients du réseau (les poids des connexions entre cellules) pour que, pour certaines valeurs présentées en entrée, vous obteniez certaines valeurs en sortie.

Par exemple, lorsque les entrées du réseau sont des valeurs de pixels d’une image, les sorties peuvent coder le nom d’un objet, d’une personne ou d’un nombre (la reconnaissance automatique des codes postaux par ex). Autrement dit, on cherche à reproduire ce que notre cerveau fait très simplement, très rapidement, avec un nombre de “calculateurs” très simples mais très élevés et massivement parallèles.

Tous les matheux comprendront que dans le cas trivial d’une fonction de transfert “identité”, le réseau de neurones est un système linéaire et qu’essayer de trouver les coefficients permettant de mettre en correspondance des couples {entrées, sorties} prédéterminés revient à minimiser l’écart entre la mesure constatée et la cible visée. Dans le cas linéaire, la méthode des moindres carrés permet de minimiser l’impact des erreurs expérimentales. On cherche à minimiser une fonction de coût qui est simplement la somme des carrés des écarts entre valeurs mesurées et valeurs de sortie ciblées. Cette fonction est une quadrique dans le cas linéaire, et son minimum peut être calculé immédiatement.

Dans le cas général d’un réseau non linéaire, la fonction de coût (non linéaire) peut être approchée par une fonction plus simple dont on cherchera à chaque itération un jeu de coefficients plus “efficace” (ie faisant baisser la fonction de coût).

Plusieurs approches sont intéressantes: approximation par un hyperplan (linéarisation) ou approximation par une quadrique. Pour illustrer dans le cas monodimensionnel d’un réseau à un seul neurone à une seule entrée (avec donc un seul coefficient), la fonction de coût est la somme des carrés des écarts entre les sorties mesurées et les sorties désirées, sa minimisation consistant à la remplacer autour de la valeur actuelle du coefficient par une droite ou par une parabole.

Dans le cas de l’hyperplan, vous vous déplacez dans l’espace des coefficients d’un “pas” dans le sens de la plus grande pente.

Dans le cas de la quadrique, vous vous placez dans l’espace des coefficients immédiatement sur le minimum calculé (on sait calculer le minimum d’une quadrique).

Mais comme il s’agit d’une diminution supposée de la vraie fonction de coût remplacée par son approximation, il faut s’assurer de la baisse réelle et recommencer jusqu’à se trouver sur un endroit de l’espace des coefficients correspondant à un minimum.

Dans un espace monodimensionnel, un extrémum correspond à une dérivée nulle. Pour déterminer s’il s’agit d’un minimum (et non pas d’un maximum), il faut calculer la dérivée seconde et s’assurer qu’elle est positive.

Dans le cas pluridimensionnel, on parle de “gradient” nul, et de matrice Hessienne définie positive.

La méthode de minimisation la plus utilisée dans l’univers des réseaux de neurones est la descente à pas constant, dite aussi méthode du gradient: approximation de la fonction de coût par un hyperplan et déplacement dans l’espace des coefficients dans le sens de la plus grande pente (le sens opposé au vecteur gradient) d’un “pas” constant.

Le problème de cette méthode numérique d’optimisation est qu’elle peut être très gourmande en temps de calcul. En effet, plus on s’approche du minimum, plus la fonction à un gradient faible, et plus le déplacement sera petit. Autrement dit, plus on s’approche du minimum, et moins on s’en approche rapidement…

J’ai donc étudié une autre méthode d’optimisation bien connue, celle dite de Quasi Newton et en particulier la méthode implantée par Broyden-Fletcher-Goldfarb-Shanno où l’on calcule par itération la matrice inverse de la matrice hessienne. Cette méthode possède une vitesse de convergence bien supérieure à la méthode de gradient à pas constant, vitesse de convergence connue depuis des lustres par les informaticiens spécialistes des algorithmes d’optimisation numérique.

Mais un certain nombre de chercheurs confirmés, à l’époque où je préparais ma thèse, semblaient méconnaître les propriétés de ces algorithmes, et en particulier la lenteur de la méthode de gradient à pas constant. Ou s’ils la connaissaient, ils arrêtaient leurs calculs en tout cas beaucoup trop tôt. J’ai pu reproduire certains des problèmes posés dans leurs articles en trouvant des solutions bien meilleures, parfois même en contradiction avec leurs conclusions, simplement parce que je m’étais intéressé à un domaine que je ne connaissais pas: les méthodes d’optimisations numériques.

Alors maintenant, lorsque je lis les conclusions d’un chercheur, même de haut niveau, même avec une équipe derrière lui, je reste prudent en me demandant s’il maîtrise bien toute la chaine technique qui l’a amené à ses conclusions. A-t-il utilisé la bonne méthode d’optimisation, a-t-il utilisé les outils mathématiques en respectant leurs conditions d’usage (conditions initiales, espaces affines et non pas linéaires, erreurs liées à la discrétisation numérique…). Autant d’erreurs de débutants qui peuvent passer inaperçues.

Et parfois je suis encore surpris aujourd’hui, surtout dans des domaines qui ne sont pas les miens, et que j’aborde avec modestie et humilité, de voir des soi-disant experts se prendre les pieds dans le tapis sans s’en rendre compte…

Intelligence artificielle

Je suis depuis longtemps fasciné par le concept d’intelligence: qu’est-ce que l’intelligence, comment la mesure-t-on, peut-on la simuler artificiellement, comment se développe-t-elle, etc.

Je pense qu’une partie de mon attrait pour l’informatique vient de ce domaine très particulier qu’on appelait auparavant “Intelligence Artificielle” et qui fait maintenant parti du champ plus vaste des Sciences Cognitives qui sont en plein développement.

Je me souviens avec émotion de mes premiers programmes qui résolvaient des problèmes aussi complexes que la réduction de fraction ou le calcul de PGCD et de PPCM. Ces programmes s’appropriaient des compétences réservés jusque là aux seuls humains. J’ai connu la montée en puissance des programmes de jeux d’échec jusqu’au choc final de la première défaite d’un homme face à une machine (Gary Kasparov vs Deep Blue en 1997).

Mais la création d’une véritable intelligence artificielle reste à faire. Aucune machine n’a à ce jour réussi le Test de Turing, à savoir être capable de soutenir une conversation avec un être humain sans que celui-ci puisse deviner avec certitude s’il parle avec une machine ou avec un autre être humain.

J’ai pour ma part réalisé mon mémoire de DEA (Diplôme d’Études Approfondies, diplôme aujourd’hui disparu, une sorte d’année Master2 orientée recherche) sur le sujet de la “Logique temporelle”, extension du calcul des prédicats incluant des opérateurs spécifiques liés au temps (avant, après, pendant telle durée, etc). J’ai préparé ce diplôme en parallèle à ma dernière année d’école d’ingénieurs, et pour cela je suivais des cours à l’Université tous les samedi matin. Je raconte d’ailleurs ici même ma première conférence effectuée sur ces travaux…

Si les considérations théoriques peuvent sans aucun doute faire progresser la recherche d’une intelligence artificielle, j’avais envie d’explorer la question sous un autre angle: le fonctionnement du cerveau. J’ai donc préparé (et passé) une thèse dans le domaine, à la mode à l’époque, des réseaux de neurones formels. J’y a consacré quatre années passionnantes de ma vie à étudier les réseaux de neurones bouclés à apprentissage supervisé (le bouclage du réseau introduisant une récurrence et donc l’introduction du temps dans le système, ce qui était ma spécialité de DEA).

J’ai adoré travailler avec des neurobiologistes, des éducateurs de jeunes enfants, des neurochirurgiens, des psychiatres et des cogniticiens. En tant qu’ingénieur informaticien, j’étais le lien, le liant entre toutes ces disciplines qui me fascinaient. J’avais (et j’ai encore) tout à apprendre, à comprendre. Comment le cerveau est-il structuré, organisé, quel est le rôle supposé de chaque niveau, de chaque structure, pourquoi un ensemble aussi “lent” par rapport au temps électronique est-il capable de reconnaitre un visage parmi des milliers mémorisés, pourquoi quand un morceau du cerveau manque (après un accident par exemple), les facultés restent intactes parfois…

Mes choix de vie personnels m’ont écarté de ce champ de recherche, mais je suis sur que des progrès considérables pourraient encore être accomplis, avec en particulier des applications concrètes en automatique et en météorologie, ou dans tout domaine où l’obtention de modèles non linéaires de type boite-noire pourraient être utiles. Mais je ne suis pas irremplaçable et la recherche se porte très bien sans moi. Donnez-moi 10 millions d’euros et je vous promets de consacrer toute ma vie restante à ce sujet (montant non remboursable, Paypal accepté, sans garanti de résultat). Mais obtenir d’un réseau de neurone une simulation d’un système non linéaire, si cela serait très utile pour les ingénieurs, n’en fait pas une machine intelligente. Et pourtant, plus la science avance, plus le fonctionnement électrique et chimique du cerveau est bien compris. C’est le fonctionnement d’ensemble, l’algorithme, qui n’est pas encore connu.

Bien entendu, l’existence d’une machine intelligente marquerait une étape considérable dans l’histoire de l’humanité. J’ai dévoré tous les ouvrages (ou presque) de science-fiction qui traitent du sujet: les Asimov bien entendu, et autre Clarkeries. Je guette souvent la sortie au cinéma de chaque film de science-fiction traitant plus ou moins du sujet (comme A.I. de Spielberg).

Mais les années passent, les concours d’intelligence artificielle s’enchainent les uns après les autres, mais aucune machine capable de rivaliser avec un cerveau humain, même moyen, n’a encore vu le jour.

Alors quand ma fille ainée de 16 ans, à qui je faisais part de ma déception de ne pas vivre cette révolution, m’a répondu: “Mais enfin, papa, une machine intelligente, ça ne pourra jamais exister”, je me suis dis qu’elle avait peut-être raison.

Mais j’espère encore.

—————————–

Source image le magnifique site “If we don’t, remember me”