===== Tutoriel : utilisation des réseaux de neurones ARL (Army Research Laboratory) =====
==== Introduction ====
Au cours du projet, nous avons décidé d'exploiter les architectures de réseaux de neurones proposées par le Army Research Laboratory (le laboratoire de recherche de l'armée des États-Unis). Il s'agit de réseaux de neurones basés sur les technologies Tensorflow et Keras.
Ces IA s'appuient sur une structure de réseau de neurones convolutif et semblent être à la fois très performantes et adaptatives. Elles peuvent ainsi être autant utilisées pour détecter des mouvements imaginés que pour reconnaître des émotions, moyennant l'utilisation de données fiables et un apprentissage sur ces données. De plus, il s'agit de réseaux "end-to-end", c'est-à-dire qu'ils ne nécessitent aucun pré-traitement du signal et prennent en entrée directement les signaux EEG bruts.
Enfin, ces réseaux sont disponibles sous licence Creative Commons Zero 1.0 Universal (CC0 1.0) Public Domain Dedication. Il s'agit de la licence permettant à un auteur de renoncer le plus possible à ses droits d'auteurs et est donc très similaire au domaine public. Nous avons ainsi le droit de copier, modifier, distribuer et représenter l’œuvre, même à des fins commerciales, sans avoir besoin de demander l’autorisation ([[https://creativecommons.org/publicdomain/zero/1.0/deed.fr|site de Creative Commons précisant la licence]]).
==== Pré-requis ====
=== Installation ===
Les réseaux sont disponibles [[https://github.com/vlawhern/arl-eegmodels|sur cette page]]. Il suffit juste de déposer le fichier EEGModels.py dans le dossier du projet.
=== Détail des différents modèles ===
EEGModels met à disposition plusieurs architectures de réseaux de neurones :
* EEGNet est un réseau convolutionnel calculant dynamiquement des filtres temporels et spatiaux. Il est particulièrement adapté pour des problèmes de reconnaissance de mouvements par exemple.
* EEGNet_SSVEP est une variante d'EEGNet spécialisée dans la reconnaissance des potentiels évoqués.
* DeepConvNet est un réseau de neurones profond convolutionnel.
* ShallowConvNet est encore une autre variante de réseau convolutionnel.
Par défaut, tous ces réseaux sont conçus pour recevoir en entrée des segments EEG de 2 secondes et échantillonnés à 128 Hz (soit 256 points). Les données d'entrée doivent être au format Essais x Électrodes, Données x Noyaux. Nous verrons par la suite comment convertir des données issues d'une Epoch dans ce format.
Afin d'assister la prise de décision quant à l'architecture de réseau de neurones à choisir pour le projet, nous avons entrepris une démarche visant à comparer les performances de chacun de ces réseaux en termes d'accuracy. Les résultats seront exposés dans une partie qui sera traitée ultérieurement.
==== Utilisation des architectures de réseaux de neurones EEGModels ====
=== Utiliser les classes EEGDecodingModel et ComposedClassifier pour classifier les signaux EEG ===
== Explications ==
Dans le cadre du projet, nous avons développé des classes nommées EEGDecodingModel et ComposedClassifier qui vont permettre de classifier de manière aisée des signaux EEG. La première vise à exploiter directement un des réseaux de l'ARL tandis que ComposedClassifier permet d'utiliser simultanément plusieurs classifieurs de type EEGDecodingModel (réseau ARL) ou EEGDecodingModelFBCSP (SVR + FBCSP).
Dans cette partie, nous verrons comment utiliser ces deux classes pour classifier les signaux EEG.
== Apprentissage et classification avec EEGDecodingModel ==
Afin de classifier les signaux, il suffit d'instancier la classe EEGDecodingModel. Cette classe prend en paramètres d'initialisation le type définissant quel réseau sera utilisé (parmi "EEGNet", "ShallowConvNet", "DeepConvNet" et "MixedConvNet").
Ensuite, à partir d'une instance de la classe Epochs de MNE issue d'un dataset, on peut définir le nombre d'électrodes (chans), le nombre d'échantillons par essais (samples) et le nombre de classes (classes).
On peut alors exploiter la méthode train pour entraîner le réseau. load_model permet de charger un modèle déjà entraîné dès le début de l'entraînement. Dans ce cas, le modèle en question doit se trouver à la racine et se nommer "model_TYPE.h5" où il faut remplacer TYPE par EEGNet, ShallowConvNet, DeepConvNet ou MixedConvNet.
epochs_nb = 350
eegmodel = EEGDecodingModel(model_type="EEGNet",chans=epochs.get_data().shape[1],samples=epochs.get_data().shape[2],classes=4)
eegmodel.train(load_model=False,epochs=epochs,labels=labels,epochs_number=epochs_nb,save_as="checkpoint.h5")
Une fois le modèle entraîné, il est alors possible de classifier un signal EEG en appelant la méthode predict :
preds = eegmodel.predict(epochs=test_epochs,true_labels=test_labels)
Il est également possible de charger les poids d'un modèle précédemment entraîné en utilisant la méthode load_model de notre instance de la classe EEGDecodingModel :
eegmodel.load_model(filename)
== Apprentissage et classification avec ComposedClassifier ==
De la même manière que pour la classe EEGDecodingModel, nous allons pouvoir entraîner un classifieur d'ensemble à partir des données d'une instance de la classe Epochs. Il faut ensuite déclarer une liste qui va contenir l'ensemble des classifieurs qui vont composer notre ComposedClassifier. Dans l'exemple ci-dessous, nous avons mis l'ensemble des type de EEGDecodingModel possibles afin de montrer comment ajouter chacune.
Il est aussi important de noter qu'à l'instanciation, la classe ComposedClassifier prend en paramètre un identifiant. Celui-ci va constituer le nom de dossier dans lequel vont être déposés les modèles sauvegardés après un entraînement.
data = np.array(epochs.get_data())
chans = data.shape[1]
samples = data.shape[2]
sfreq = 250
model_lst = [EEGDecodingModel("EEGNet", chans, samples), EEGDecodingModel("ShallowConvNet", chans, samples),
EEGDecodingModel("MixedConvNet", chans, samples),EEGDecodingModel("DeepConvNet", chans, samples)]
composed_classifier = ComposedClassifier(model_lst,"10221")
composed_classifier.train(epochs,labels,load,epochs_number)
Maintenant que notre classifieur d'ensemble est entraîné, il va être capable de prédire la classe d'un signal EEG grâce à la méthode du vote à la majorité en appelant successivement les méthodes predict de chaque sous-modèle.
Il suffit alors simplement d'utiliser la méthode predict pour obtenir le tableau contenant l'ensemble des prédictions :
final_pred = composed.predict(test_epochs,test_labels)
Enfin, il peut être intéressant de charger un modèle pré-entraîné. Pour cela, il faut disposer les fichiers dans un dossier ID_model_data situé à la racine du projet, où ID est l'identifiant passé en paramètre à la construction du ComposedClassifier. Il suffit alors simplement d'appeler la méthode load_model pour charger les modèles :
composed2.load_model()
=== Pour aller plus loin : comment démarrer l'apprentissage d'un réseau ARL et classifier des signaux EEG avec ===
== Apprentissage d'un modèle ==
Étape 1 : Récupération des données
Tout d'abord, il va être nécessaire de récupérer les données d'apprentissage. Si celles-ci sont stockées dans une variable de type Epochs de la librairie MNE, il suffit d'utiliser le fragment de code suivant :
X = epochs.get_data() * 1000 y = labels|
Ici, on multiplie par 1000 les données d'entraînement pour contrer la grande sensibilité des réseaux de neurones profonds.
Étape 2 : Formattage des données
La prochaine étape consiste à transformer les données issues d'une Epoch dans un format compréhensible par un réseau de neurones ARL. Ceci peut se faire de la manière suivante :
kernels, chans, samples = 1, X.shape[1], X.shape[2]
Étape 3 : Configuration du modèle
Nous allons enfin pouvoir instancier notre modèle. Cette instanciation va dépendre du réseau que l'on souhaite utiliser.
- Réseau EEGNet :
model = EEGNet(nb_classes=4, Chans=chans, Samples=samples, dropoutRate=0.5, kernLength=32, F1=8, D=2, F2=16,dropoutType='Dropout')
Dans cette ligne de code, nous définissons le nombre de classes (nb_classes, dans notre cas 4), puis nous fixons le nombre d'électrodes utilisées (Chans) et le nombre d'échantillon par donnée (Samples).
- Réseau DeepConvNet :
model = DeepConvNet(nb_classes=4, Chans=chans, Samples=samples)
- Réseau ShallowConvNet :
model = ShallowConvNet(nb_classes=4, Chans=chans, Samples=samples)
Étape 4 : Compilation du modèle et définition des fonctions de rétro-propagation
Il faut ensuite compiler le modèle et choisir au passage la fonction de loss qui sera utilisée (afin de calculer la perte lors de la phase de rétropropagation) ainsi que l'optimiseur :
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
Dans cet exemple, nous utilisons la fonction de loss categorical crossentropy et l'optimiseur adam. Le dernier paramètre permet de définir les métriques que nous voulons afficher à l'écran pendant l'entraînement (afin de faciliter la prise de décision et l'évaluation du modèle). Il s'agit d'un tableau regroupant l'ensemble des métriques que l'on souhaite observer parmi ceux contenus dans [[https://www.tensorflow.org/api_docs/python/tf/keras/metrics|tensorflow.keras.metrics]].
Étape 5 : Création du point de passage
Il va ensuite être nécessaire de définir un Checkpointer qui va avoir pour vocation de sauvegarder le modèle à différentes phases de l'apprentissage :
checkpointer = ModelCheckpoint(filepath='checkpoint.h5', verbose=1, save_best_only=True)
Dans ce fragment de code, filepath doit contenir le nom du fichier dans lequel sera sauvegardé le modèle (au format h5 pour être à nouveau exploitable). L'argument save_best_only est très utile : il permet de définir si le modèle doit effectivement être sauvegardé uniquement dans le cas où le modèle courant est meilleur que tous les autres produits auparavant.
Étape 6 : Lancement de l'entraînement
Nous pouvons finalement lancer l'entraînement de notre modèle :
fittedModel = model.fit(X_train, Y_train, batch_size=8, epochs=300, verbose=2, validation_data=(X_validate, Y_validate), callbacks=[checkpointer], class_weight=class_weights)
Cette ligne va donc permettre de lancer directement l'apprentissage du modèle choisi auparavant. Il est à noter que l'apprentissage peut être particulièrement long selon le nombre d'époques choisi et la taille du dataset.
X_train et Y_train représentent respectivement la matrice de valeurs EEG et les étiquettes respectives. Batch_size est un hyperparamètre intéressant, il permet de regrouper les données par lot ; cette valeur peut donc être augmentée pour d'importantes quantités de données. L'argument epochs va permettre de définir le nombre d'époques à réaliser successivement pour l'apprentissage, c'est-à-dire le nombre de fois que l'entièreté du dataset passera dans le réseau à la suite. Ce nombre devra être fixé expérimentalement mais dans notre cas nous avons constaté que les performances de classification stagnent relativement à partir de 100 Epochs. Enfin, class_weights devra contenir un tableau contenant les poids associés à chaque classe, soit un tableau dont une valeur est associée à chaque classe et permet de quantifier les inégalités entre les classes dans le dataset. Dans le cas où les classes sont équilibrées entre elles (même quantité de données dans toutes les classes), on a class_weights = {0:1,1:1,2:1,3:1}.
== Utilisation d'un modèle entraîné ==
Étape 1 : Chargement des poids issus de l'entraînement
Une fois un modèle entraîné, il est possible de l'exploiter, même à postériori, grâce au fichier au format h5 récupéré après l'entraînement.
Pour cela, il suffit de configurer le modèle comme définit auparavant puis de charger les poids sauvegardés dans le fichier h5 avec l'instruction :
model.load_weights('model_eegnet.h5')
Étape 2 : Prédiction à l'aide du modèle entraîné
Nous pourrons alors directement prédire la classe d'une nouvelle donnée en exécutant l'instruction :
model.predict(X_test)
Il est important de noter que les classes seront redéfinies après l'étape de prédiction afin que la première classe corresponde à la valeur 0. Ainsi, si nos 4 classes étaient 1, 2, 3 et 4 ; celles-ci deviendront 0, 1, 2 et 3.
== Résultats et comparatif ==
Afin de tester et comparer ces différents réseaux, nous avons mis en place une procédure exploitant les données du dataset BCI Competition 2008 2a. Ceci correspond au programme composed_classifier_cnn.py.
Nous avons également programmé un algorithme permettant la fusion des données de prédiction. Il s'agit d'un classifieur d'ensemble basé sur la méthode du vote à la majorité : la classe finalement retenue est celle qui aura eu le plus de prédictions parmi les classes prédites par les 3 réseaux de neurones.
Dans ce programme, nous entrainons, pour chaque sujet des 9 de l'expérimentation, chaque réseau de neurones ainsi que le classifieur d'ensemble sur les fichiers d'entraînement (au format "A0XE.gdf" où X est le numéro du sujet courant). Nous testons ensuite tous ces classifieurs sur les fichiers de test (au format "A0XT.gdf" où X est le numéro du sujet courant) et nous sauvegardons les valeurs d'accuracy obtenues pour dresser un rapport sous forme d'un graphique à la fin de l'exécution.
Ainsi, après un entraînement de plusieurs heures, nous avons obtenu les graphiques suivants :
{{:evaluation_results_composed_classifier.jpg?400|}}
Sur ce graphique, nous pouvons observer, pour chaque réseau ainsi que pour le classifieur d'ensemble, les valeurs d'accuracy obtenues lors de la phase de test pour chaque sujet de 1 à 9.
{{:kappa_score_composed_classifier.jpg?400|}}
Ce second graphique présente les scores kappa obtenus pour chaque phase de test et pour chaque classifieur. Le score kappa est une valeur comprise entre 0 et 1 représentant la capacité du classifieur à être éloigné du "hasard" (par exemple une accuracy de 0.25 pour un cas à 4 classes). Il est particulièrement utilisé dans le cadre de la comparaison des performances d'intelligences artificielles et a été le critère de référence pour comparer les performances des participants au BCI Competition 2008 2a.
{{:classification_results_nn.png?400|}}
Au final, nous avons obtenu les valeurs moyennes ci-dessus pour l'ensemble des sujets. Nous voyons ainsi qu'en moyenne le classifieur d'ensemble est bien plus performant que l'ensemble des autres réseaux pris individuellement. Il s'agit donc d'une méthode intéressante pour espérer améliorer les performances globales de notre intelligence artificielle.
Nous avons ensuite souhaité comparer les performances obtenues ici avec les résultats du BCI Competition 2008 2a.
Le graphique ci-dessous présente les scores kappa obtenus pour chaque participant du BCI Competition 2008 2a en nous incluant (Us). Nous voyons ainsi que les résultats obtenus sur le dataset de test sont plutôt bons : nous arrivons en troisième position dans ce classement.
{{:comparatif_bci_competition_2008_2a.jpg?400|}}
Enfin, nous avons voulu exploiter un classifieur d'ensemble sur des extraits de une seconde afin de simuler le temps réel et vérifier que nos performances de classification restent bonnes.
Nous obtenons alors le graphique suivant, détaillant l'accuracy obtenue pour chaque sujet du dataset :
{{:accuracy_composed_classifier.png?400|}}
Nous voyons ainsi que, même sur des petits extraits d'à peine une seconde, nous arrivons tout de même à des résultats satisfaisants avec le classifieur d'ensemble. Nous remarquons en particulier un sujet pour lequel l'accuracy atteint 80% et une accuracy moyenne de 59%. Il s'agit donc de résultats satisfaisants et prometteurs.
==== Conclusion ====
Les réseaux proposés par l'ARL dans la librairie EEGModels semblent très intéressants pour la classification de signaux EEG liés à l'imagination motrice. Ils offrent des résultats de classification très intéressants pour une première approche.
Il sera intéressant pour la suite d'essayer d'adapter et d'améliorer les architectures de réseaux de neurones proposées dans la librairie. Il sera aussi possible de combiner une classification basée sur ces réseaux (par exemple en utilisant le classifieur d'ensemble basé sur le vote à la majorité) avec une méthode plus classique basée sur du machine learning afin d'obtenir de meilleurs résultats.