Prédire l’allure d’une personne avec DL4J
grâce aux données de son téléphone portableAndrés Bel Alonso
Andrés est responsable du développement du Moteur de Machine Learning Inceptive.
Dans cet article, un exemple de l’utilisation de la librairie DL4J Java Deep Learning 4J [2] va être présenté, dans un cas pratique, d’utilisation des données du gyroscope d’un téléphone portable pour déterminer si son propriétaire est en train de courir ou de marcher. Accessoirement, nous déterminerons aussi si la personne porte son téléphone dans la main droite ou gauche.
Pour cela, on va entraîner deux réseaux de neurones récurrents (un pour déterminer si la personne marche ou court, l’autre pour savoir avec quelle main elle tient le portable.). Les réseaux de neurones artificiels sont un algorithme apprentissage machine qui s’inspire fortement de la structure neuronale biologique. Ils peuvent apprendre à partir d’un ensemble de données et faire des prédictions sur des données qui leurs ressemblent plus ou moins. Les réseaux de neurones récurrents sont une variante qui tient compte des résultats qui ont été donnés précédemment pour effectuer une nouvelle prédiction. Ceci leur permet de traiter des problématiques dont les données évoluent dans le temps, comme celles que l’on traite dans cet article.
Dans le reste de cette présentation, seront présentés :
- L’ensemble de données utilisées
- La librairie DL4J
- Le code fonctionnel
- Les résultats du code et mes commentaires à propos.
Tout le code utilisé dans cet article se trouve dans le dépôt git [0]
Le jeu de données : Run or Walk
Pour cet exemple on va utiliser un dataset Kaggle appelée Run or Walk[1] (Court ou marche). Kaggle est une plateforme web de Google qui héberge des concours de Machine Learning et des datasets. Ce jeu de données contient 88 588 entrées de l’accéléromètre et le gyroscope d’un iPhone 5c, ainsi que la date et l’heure à laquelle elles ont été prises. Les données sont prises dans des intervalles de 10 secondes avec une fréquence de 5,4 échantillons/seconde. Pour chaque entrée, il est indiqué si la personne court ou marche, ainsi que dans quelle main (wrist) elle tient le portable. Voici un exemple des premières lignes :
Les données ont été utilisées telles qu’elles ont été fournies part Kaggle. Elles ont juste été réordonnées selon les colonnes « date » et « time », pour rendre plus facile leur lecture. Le code pour les ordonner se trouve aussi dans le dépôt git associé à l’article[0] (classe « ReorderDataset »).
La librairie DL4J
Deep Learning 4j [2] est une librairie open source (licence Apache 2.0) qui permet de construire, entraîner et tester une grande diversité d’algorithmes de Deep Learning (depuis les réseaux standard, jusqu’aux réseaux convolutionels, en passant par des architectures plus complexes). Son site web est bien documenté, et dans leur page github il y a de nombreux exemples d’utilisation des différentes fonctionnalités [3]. Mais surtout sa structure de données (Nd4j[4]) peut s’exécuter dans un GPU, ce qui réduit considérablement le temps de calcul, et en fait une librairie de deep learning « dans les normes de l’état de l’art ».
Cependant, la gestion de la mémoire des tableaux de Nd4j est compliquée. Nd4j utilise du code natif ( Cuda oblige), et alloue de l’espace or du tas Java. Ceci est impérativement à prendre en compte lorsque la volumétrie des données est importante. Nd4j offre la possibilité de gérer la mémoire de façon efficace et directe (via des objets dédiés, les Workspaces), mais il s’agit d’une tâche à laquelle les développeurs Java sont peu habitués. Cette possibilité devient une obligation lorsque les volumes de données sont importants et lorsque l’on intègre DL4J dans une application plus complexe. La documentation des Workspaces en permet une utilisation simple. Cependant, elle n’est pas suffisante pour comprendre leur fonctionnement de façon approfondie. En effet, la documentation ne couvre pas toutes les fonctionnalités, les classes et fonctions n’ont pas toutes une Javadoc.
Une autre fonctionnalité très intéressante de DL4J est l’interface utilisateur qui permet de visualiser des statistiques de l’entraînement en temps réel, ainsi que l’état mémoire du système. Voici quelques copies d’écran de l’interface :

Dashboard principale de DL4J

Détail du model dans la GUI DL4J
L’utilisation de DL4J en tant que dépendance est très facile avec Maven, il suffit d’ajouter les librairies concernées dans la section dépendance du pom ainsi :
Dans cet exemple on ajoute 4 dépendances correspondant :
- Au cœur de DL4J (deeplearning4j-core)
- A l’interface graphique (deeplearning4j-ui_2.11)
- Aux backends, correspondant au backend native, c’est à dire le CPU et au backend CUDA, le GPU.
Selon la documentation de DL4J, ce POM configure la librairie pour utiliser les deux « backends » (CPU & GPU) disponibles. Ainsi, dès que le premier est utilisé à 100 %, alors le second backend est utilisé.
Si on souhaite prioriser un des deux backends alors on peut fixer les deux variables d’environnement suivantes : BACKEND_PRIORITY_CPU ou BACKEND_PRIORITY_GP.
Il est cependant bien plus pratique de simplement retirer du POM le backend que l’on ne souhaite pas utiliser.
Description du code
Le code est divisé en 4 parties :
- La lecture du fichier de données
- Le changement de la structure de données de DL4J, et la séparation en jeu de données d’entraînement et de test
- La création et paramétrisation du réseau de neurones
- Son entraînement
Lecture des données
Dans cette partie, les données sont lues depuis le fichier de données, et chaque série temporelle est séparée. Une série temporelle est une succession de réalisations liées. Ici, une même séance de marche ou de course. Cette séparation permet d’indiquer au réseau de neuronnes les liens temporels entre les entrées du fichier de données. Ces informations permettent au réseau de rechercher un lien entre une donnée prélevée au temps T et une donnée prélevée au temps T+1.
La lecture du fichier de données se fait à l’aide d’un lecteur de csv pour rendre le code plus simple.
Après l’exécution du code, la liste TimeSeries contient les séries temporelles séparées. La liste Labels contient la sortie attendue du réseau de neurones pour chaque instant. Il convient de noter que labels contient deux colonnes pour chaque matrice. La première correspond à « on est en train de courir », et la deuxième à « on n’est pas en train de courir ». En effet pour le réseau de neurones il est plus facile de donner une probabilité à posteriori sur chaque catégorie (2 sorties) que la représentation numérique de la classe elle-même (0 ou 1, celons le cas).
Nous avons précisé plus haut que nous souhaitions séparer les données en series temporelles. Pour cela, nous allons ordonner les données en fonction du temps. Ensuite, nous allons effectuer une séparation à chaque fois que des échantillons ont été prélevés à plus de 2 secondes d’écarts.
Le code utilisé est :
Train/Test et structure de données
On sépare tout d’abord les données en deux ensemble : l’entraînement et le test.
Puis on construit le tenseur d’entraînement et de test :
Le code de la méthode buildTensor, le pilier de cette partie :
Dans le code précédent on construit la structure de données de DL4J (la sortie de la méthode buildTimeSerieTensor) avec les données d’entrainement, mais aussi un autre tableau, passé par référence, le « trainMask ». trainMask est un masque qui provient du fait que chaque série temporelle de données est différente. En effet, pour une session de marche ou course, on ne passe pas exactement le même temps. En plus dans les jeux de données, il y a des données manquantes (pendant quelques secondes, il n’y pas de données) ce qui implique de commencer une nouvelle série temporelle. Pourtant, DL4J impose que chaque tableau de chaque série temporelle ait la même taille. Ceci impliquera, que chaque série temporelle qui est plus courte que la série la plus grande, soit remplie de valeurs nulles. Pour éviter que le réseau de neurones soit entraîné sur ces valeurs nulles, on crée ce masque sous forme d’un tableau qui indique avec des 0 ou des 1 si la valeur doit être utilisée pour l’entraînement (un « 1 » indique que oui) pour chaque ligne de la série temporelle.
Configuration du réseau de neurones
Dans DL4J, pour entraîner un réseau de neurones, il faut préalablement construire sa configuration avec un NeuralNetConfiguration.Builder. Grâce au builder les paramètres globaux sont définis :
Puis chaque couche est ajoutée au Builder après l’avoir paramètré. Dans ce cas, on ajoutera une seule couche récurrente de type LSTM avec « interNeurones » comme nombre de neurones. Puis une couche de sortie, utilisant la fonction « softmax » qui transforme les sorties en valeurs entre 0 et 1. Ainsi, ces sorties sont assimilables à des probabilités à posteriori. Voici le code de l’ajout des couches :
Et puis le réseau est initialisé :
Une fois que le réseau de neurones est initialisé, on peut ajouter les « listeners » c’est à dire les éléments qui vont nous permettre de suivre le déroulement de l’entraînement. Pour cet exemple on va en instancier deux :
- L’interface graphique qui a été présentée dans la partie « La librairie DL4J », va recevoir les données à chaque itération
- Des messages de log dans la sortie standard toutes les 5 itérations
Le code pour configurer ceci est le suivant :
Entraînement
Lorsque l’on a mis les données dans la bonne structure de données, l’entraînement se fait facilement :
Puis DL4J permet facilement d’effectuer un entraînement. Il suffit de regrouper les données des entrées (trainData) et des sorties (trainLabel) ainsi que les masques associés à chaque tenseur.
Résultats et discussion
Génération de statistiques
Avec la classe org.deeplearning4j.eval.Evaluation on peut facilement évaluer les performances d’un réseau de neurones. Il suffit de parcourir l’ensemble des données, lui donner successivement les sorties du réseau de neurones et les sorties attendues. Puis la classe évaluation créera des statistiques pertinentes. On effectuera les statistiques avec la fonction :
Puis on génère des statistiques sur l’ensemble de l’entraînement et l’ensemble de test en utilisant cette fonction :
Les résultats du réseau de neurones sur l’ensemble d’entraînement seront biaisés par rapport à ce test, puisque le réseau a appris sur ces données. Mais ils nous indiquent l’état d’entraînement du réseau de manière plus avancée. En plus, en les comparant aux données de test cela nous permet de déterminer les capacités de généralisation. S’il y a un grand écart entre les résultats de test et les résultats d’entraînement, ceci indique que notre modèle « overfit » sur l’ensemble d’entraînement, ce que nous souhaitons à tout prix éviter.
Résultats
Allure
En entraînant sur 70 % des données et en testant sur les 30 % restants, et en effectuant 1500 itérations, DL4J affiche les résultats suivants pour la prédiction de « Courir ou marcher ».
Les statistiques d’entraînement sont :
L’ “accuracy” représente le nombre de réponses correctes du modèle divisé par le nombre de réponses totales (correctes et incorrectes).
La “précision” est le ratio entre le nombre de valeurs étiquetées comme 1 correctes et le nombre de fois où le model à répondu 1.
Le “recall” est le ratio entre le nombre de fois où le modèle a répondu 1 sans se tromper et le nombre total d’éléments étiquetés comme 1.
Finalement le F1 score est une moyenne harmonique entre la “précision” et le “recall”.
Et sur l’ensemble de test, le modèle prédit :
L’interface de DL4J nous montre ces statistiques d’évolution de l’entraînement du réseau de neurones.

Interface de DL4J
Les trois graphiques représentent l’évolution de différents indicateurs au cours du temps. Le graphique d’en haut représente le score (à minimiser). Dans ce cas il s’agit de la fonction d’entropie croisée multi classe (la version multi classe de l’entropie croisé )[5]. Le graphique en bas à gauche représente l’évolution (en échelle logarithmique) du ratio de changement des paramètres. Il nous indique à quel point le réseau de neurones est, en moyenne, en train de faire évoluer ses poids. Il nous indique si l’apprentissage avance ou non. En bas à droite, il représente la déviation standard des poids des fonctions d’activation. Il représente à quel point les poids sont différents les uns des autres.
Main
Si on adapte le code pour prédire dans quelle main est porté le portable (fichier Wrist dans le dépôt git du projet de l’article), on effectue la même séparation entraînement/test avec les mêmes itérations, DL4J affiche les résultats suivants, sur les données d’entrainement :
Pour les données de test :
Et les mêmes graphiques d’entraînement :

Interface de DL4J
Conclusion
Pour les deux problématiques, on observe de bons taux de réussite à 95 et 96,5 %. Ces résultats sont certainement améliorables avec une meilleure configuration d’hyperparamètres (autrement dit, une architecture de réseaux différente, d’autres fonctions d’activation…).
Cependant on observe que les résultats (pour les deux problématiques) sur l’ensemble d’entraînement et de test sont très semblable. Ceci est un très bon point puisqu’il montre la robustesse des modèles entraînés. En machine learning, un grand écart entre les résultats sur l’ensemble d’entraînement et de test est un indicateur de défaillance du modèle entraîné. Cela indique généralement que le modèle sur-apprend/overfit excessivement sur les données d’entraînement et qu’iil est incapable de généraliser pour traiter correctement les données de test.
Est-ce qu’avec un entraînement plus long (autrement dit, plus d’itérations), les résultats auraient été meilleurs ? D’une part, la marge de progression n’est pas très large. Mais d’autre part si on examine les courbes de l’interface on peut donner quelques éléments :
- Pour courir ou marcher, on observe que la déviation standard entre les poids d’activation du réseau n’évolue presque pas. On peut assimiler ça à une stagnation de l’entraînement. Pire, sur les changements moyens des poids on observe des oscillations qui font aussi varier légèrement le score final et leur amplitude augmente au fur et à mesure des entraînements. Selon moi et tenant compte du fait que le score à minimiser était déjà presque à 0, cela indique que plus d’itérations n’amèneront pas des meilleurs résultats. Il existe un risque majeur d’avoir de moins bons résultats.
- Pour la problématique de la main, l’analyse est la même. Dans ce cas, on observe des fluctuations beaucoup plus importantes dans la courbe des changements… qui se traduisent par une augmentation des erreurs dans la courbe du score. Je peux donc en déduire que les conclusions sont les mêmes. Cette problématique devrait être abordée à nouveau en diminuant le taux d’apprentissage (learning rate) qui est souvent le principal responsable de ces oscillations. En revanche, une diminution de ce taux provoquera une convergence du réseau lente. Dans ce cas, il faudra certainement augmenter le nombre d’itérations.
Références
[0] Dépôt github avec le code utilisé dans cet article : https://github.com/inceptive-tech/RunOrWalk
[1] Dataset Run or Walk, disponible sur Kaggle https://www.kaggle.com/vmalyi/run-or-walk (dernière visite le 9/05/2018)
[2] Site web de DL4J : https://deeplearning4j.org (dernière visite le 11/06/2018)
[3] Dossier git des exemples : https://github.com/inceptive-tech/RunOrWalk/tree/master/src/main/java/tech/inceptive/oss/runorwalk
[4] Site web de Nd4j : https://nd4j.org (dernière visite le 11/06/2018)
[5] Entropie croisée : https://fr.wikipedia.org/wiki/Entropie_crois%C3%A9e