Série 16
Exercice de révision et de « remise en jambe » (Niveau 1 à 3).
Le but de cet exercice est de continuer à vous faire réviser les acquis du premier semestre (fonctions, structures, alias de type, types énumérés) et de vous faire pratiquer les nouveautés de la semaine passée : gestion des exceptions, espaces de nommage et arguments de main
Motivation
Certains neurones du cerveau (dans le cortex cérébral par exemple) sont très réceptifs à des stimuli spécifiques. Ceci signifie que ces neurones ne sont "activés" que s'ils reçoivent un signal correspondant au stimuli auquel ils sont sensibles. Supposons que le signal envoyé par le monde extérieur soit caractérisé par une séquence de 20 données binaires, avec par exemple:signal = 11111000000000011111 (en l'absence du stimulus, signal négatif) signal = 11111000001111100000 (en présence du stimulus, signal positif)
Supposons que de façon aléatoire, de nombreux neurones soient réceptifs à un stimulus donné :

Nous nous intéressons alors à trouver parmi ces neurones lesquels sont les plus réceptifs à ce stimulus, y compris dans le cas où la transmission du signal correspondant est bruitée. En clair, nous souhaitons trouver les neurones dont l'activité est hautement corrélée à la présence/absence du stimulus.

Modèle
Nous partons ici du modèle de détection d'un objet dans le champ visuel suivant : soit ζ un stimulus binaire correspondant à la présence ou non d'un objet dans le champ de vision d'un individu. Le stimulus ζ déclenche l'émission d'un signal nerveux s, codé sur vingt bits, correspondant à la valeur prise par le stimulus :
ζ = 0 → s = 11111000000000011111 (signal négatif) ζ = 1 → s = 11111000001111100000 (signal positifs)
La transmission de ce signal est effectuée par le nerf optique.
Des erreurs de transmission peuvent avoir lieu, ce que l'on modélise par un bruitage inversant chaque bit avec une probabilité de
Le dernier chaînon dans le processus de détection est un neurone modélisé par vingt poids synaptiques ω. En notant φ le produit scalaire euclidien, on pose que l'activation ξ du neurone ayant reçu un signal s
ξ = 1 si φ(ω,sOn dira que l'activation ξ du neurone est corrélée avec le stimulus initial ζ si ξ = ζ.∼ ) ≥ 0 ξ = 0 sinon
Le but de cet exercice est de construire un neurone (par détermination de ses poids synaptiques) dont l'activation est corrélée, au mieux, avec des stimuli initiaux.
On procédera pour cela comme suit
- génération d'un nombre important de neurones (avec poids synaptiques aléatoires);
- génération d'un nombre important de signaux (correspondant à la présence ou absence aléatoires d'objets dans le champs visuel). Notez qu'il y a autant d'objets que de signaux positifs;
- transmission (bruitée) de chaque signal à tous les neurones
- sélection du neurone ayant un total de corrélation le plus proche du nombre d'objets.
Mise en place
Commencez par créer un sous-répertoire neurones dans votre répertoire cpp/serie16.
Mise en place pour Geany
Copiez l'archive neurones.zip dans ce sous-répertoire puis dezzipez-la :
cd cd cpp/serie16/neurones unzip neurones.zip
Mise en place pour QTCreator
- copiez l'archive fournie neurones.zip dans le répertoire cpp/serie16/neurones
- Dézippez cette archive : unzip neurones.zip :
cd cd Desktop/myfiles/Programmation/cpp/serie16/neurones unzip neurones.zip rm neurones.zip
- Lancez le programme QTCreator, et ouvrez y le fichier fourni dans ce répertoire et nommé CMakeLists.txt. Procédez comme indiqué ici https://iccsv.epfl.ch/series-prog/install/installation-tutorial.php#qtcreator-cmakeproject depuis la section "Création du projet".
- Lancez le programme avec la petite flèche verte sur le bandeau latéral gauche en choisissant la cible neurones.
Complétez ensuite le fichier fourni neurones.cc ou neurones.cpp selon les indications ci-dessous :
Définition de types utiles
Implémentation
Dans le fichier fourni neurones.cc ou neurones.cpp, définissez les alias de types suivants :- bit_t comme alias de short int (qui peut s'écrire aussi short tout court);
- signal_t comme alias de std::array<bit_t,20>;
- weights_t comme alias de std::array<double,20>.
#include <array>
Toutes ces définitions seront placées dans un espace de nommage appelé impl.
Représentation d'un signal nerveux
Les signaux nerveux seront représentés par une structure Signal. Cette structure sera caractérisée fondamentalement par :
- un booléen objectPresent permettant de connaître l'information initialement détenue par le signal (présence ou absence d'objet dans le champ visuel);
- une séquence de vingt bits de type impl::signal_t modélisant le signal;
Vous écrirez ensuite les fonctions suivantes :
- void display_signal(ostream& o, const Signal& signal) permettant d'afficher sur le flot o, l'ensemble de bits du signal s;
- Signal init_signal(bool object_present) créant un Signal; en utilisant un booléen passé en argument. Ce booléen signifie la présence ou non d'un objet dans le champ de vision.
Si l'objet est présent, le signale sera initialisé au moyen de l'ensemble suivant : 11111000001111100000. Sinon, au moyen de cet autre ensemble 11111000000000011111 .
Une fois cette initialisation faite, la fonction init_signal devra bruiter les bits du signal.
Pour bruiter le signal vous pouvez ajouter à l'espace de nommage impl, une fonction bernoulli codée comme suit :bool bernoulli(double threshold) { // Retourne vrai si le générateur produit un nombre inférieur // à threshold et faux sinon return rand_range(0.0, 1.0) < threshold; }
avec la fonction rand_range décrite plus bas.Pour déterminer si un bit doit être modifié lors du bruitage (avec probabilité 0.1), il suffit d'invoquer impl::bernoulli(0.1). Si elle retourne vrai, la valeur du bit doit être inversée.
Les inclusions suivantes seront nécessaires :
#include "rand_range.h"
Pour tester cette partie, vous pouvez ajouter au début du programme principal fourni, les instructions suivantes :
Signal s1(init_signal(true)); cout << "Affichage d'un signal avec présence de l'objet,"; cout << "après bruitage : " << endl; display_signal(cout, s1); cout << endl; Signal s2(init_signal(false)); cout << "Affichage d'un signal sans présence de l'objet,"; cout << "après bruitage : " << endl; display_signal(cout, s2); cout << endl;
Représentation d'un neurone
Les neurones seront représentés par une structure Neuron. Cette structure sera caractérisée essentiellement par :- ses poids synaptiques (de type impl::weights_t);
- un booléen indiquant si le neurone est actif ou pas;
Vous coderez ensuite les fonctions suivantes relatives aux neurones :
- void display_neuron(ostream& o, const Neuron& n) qui affiche sur le flot o les poids synaptiques du neurone n en les séparant par un espace.
- bool process(Neuron& neuron, const Signal& sig) en charge de traiter un signal nerveux et d'activer le neurone, le cas échéant.
- Neuron init_neuron() qui crée et retourne un neurone. Vingts appels successifs à la fonction rand_range détermineront les poids synaptiques caractéristiques du neurone, compris dans l'intervalle [-15., 15.]).
La fonction rand_range est fournie dans le fichier rand_range.h fourni dans l'archive que vous avez dezzipé au début de l'exercice. Elle sera utilisée pour tirer un nombre pseudo-aléatoire dans un intervalle donné.
Pour générer aléatoirement un nombre entre [min, max], selon une distribution uniforme, il suffit d'appeler rand_range(min, max).
Pour le codage de la fonction process vous vous référerez aux choix de modélisation et pourrez utiliser la fonction STL std::inner_product (cette référence, les modalités d'utilisation de cette fonction deviendront plus claires lorsque nous aurons abordé la STL), de la façon suivante :
// produit scalaire des ensembles sig.s_ et neuron.w_ : double prod(std::inner_product(sig.s_.begin(), sig.s_.end(), neuron.w_.begin(), 0.0));
où sig est de type Signal, s_ est l'ensemble des bits du signal, neuron est de type Neuron et w_ est l'ensemble de poids synaptiques du neurone.
Pour tester cette partie, vous pouvez compléter le programme principal fourni par les instructions suivantes :
Neuron n(init_neuron()); display_neuron(cout,n); cout << endl; bool activated(process(n,s1)); if (activated) { cout << "Le neurone a été activé par le signal" << endl; } else { cout << "Le neurone n'a pas été activé par le signal" << endl; }
Si vous lancez plusieurs fois le programme vous devriez voir s'afficher tantôt le message Le neurone a été activé par le signal tantôt le message Le neurone n'a pas été activé par le signal
Du stimulus au neurone (algorithme)
L'algorithme simulant la création, le bruitage et le traitement d'un signal avec, en fin de code, la détermination de la corrélation entre le stimulus de départ ζ et l'activation ξ du neurone aura donc l'allure suivante :Neuron n; // le stimulus aléatoire engendre un signal qui est ensuite bruité Signal s(rand_range(0, 1)); // retourne le signal bruité S∼ = s.getSignal(); // traitement du signal (bruité) par le neurone n.process(S∼ ); // détermination de la corrélation bool ζ = s.objectPresent(); bool ξ = n.activated(); if (ξ == ζ) { // corrélation } else { // pas de corrélation
Programme principal
Le programme principal devra :- créer aléatoirement un grand nombre de neurones (10000 par défaut);
- créer un grand nombre de signaux (1000 par défaut);
- faire en sorte que chaque neurone traite l'ensemble des signaux. Le score de chaque neurone pourra alors être calculé comme étant le taux de corrélations (sur le total des signaux). Le meilleur neurone pourra enfin être sélectionné : celui ayant le plus grand taux de corrélations.
./neurones -d [-s [val] -n [val]]ou
./neurones -b [-s [val] -n [val]]Les options -d et -b s'écrasent l'une l'autre :
- -d affiche sur la sortie standard le score de tous les neurones en les séparant par un saut de ligne (ces données seront utilisées pour créer un graphique de la distribution des scores, voir plus bas);
- -b trouve un meilleur neurone et l'affiche sur la sortie standard;
- -s spécifie un nombre de signaux à traiter. Par défaut et implicitement, 1000;
- -n spécifie un nombre de neurones à considérer. Par défaut et implicitement, 10000;
Pour parvenir à ce résultat, il vous est demandé :
- de définir un type énuméré ExecTarget permettant de modéliser les 2 modes d'exécution principaux du programme (trouver le meilleur neurone ou afficher le score de tous les neurone); les types énumérés ont été présentés dans une des vidéos de révision du MOOC I. Si vous ne vous en souvenez pas, il vous suffit de définir une alias de type ExecTarget pour tout type ayant du sens ici (chaîne de caractères, entier etc.);
- de définir une structure Parameters permettant de modéliser toutes les informations nécessaires à l'exécution, à savoir : le type d'exécution voulue (de type ExecTarget), le nombre de signaux à générer et le nombre de neurones à générer;
- d'implémenter une fonction Parameters init_parameters(int argc, char* argv[]) générant une donnée de type Parameters en fonction des informations contenues dans argc et argv (qui seront les arguments de main);
- d'implémenter la fonction void execute(const Parameters& p) permettant d'obtenir l'exécution voulue en fonction de l'argument p.
- Vous pourrez reprendre les fonctions starts_with et read_integer déjà codé dans l'exercice précédent.
- La fonction init_parameters lancera des exceptions prédéfinies de type invalid_argument ou logic_error pour toutes les appels invalides au programme en ligne de commande (inspirez-vous aussi de l'exercice précédent).
Pour vérifier que votre programme fonctionne correctement, vous pouvez le lancer en mode "affichage de tous les scores" avec 1000 neurones et 1000 signaux (décommentez les instructions fournies une fois codés les éléments manquants).
Vous pouvez ensuite générer un graphique de la distribution des scores.
Si vous êtes sur une VM de l'EPFL ou si vous êtes sur Linux/ MacOS, vous pouvez utiliser pour cela le script fourni make_graph.gp (le script histogram.awk doit être présent dans le dossier), selon les modalités suivantes :
gnuplot make_graph.gp

neurones > data.txtoù data.txt est un nom de votre choix. Au lieu de s'afficher dans le terminal, le résultat de l'exécution du programme neurones se trouvera dans le fichier data.txt.
Retour à la série