Prédire l’allure d’une personne avec DL4J

grâce aux données de son téléphone portable

André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 :

date		time				username	wrist	activity	acceleration_x	acceleration_y	acceleration_z	gyro_x	gyro_y	gyro_z
30/06/2017	13:51:15:847724020	viktor		0		0			0.2650			-0.7814			-0.0076			-0.0590	0.0325	-2.9296
30/06/2017	13:51:16:246945023	viktor		0		0			0.6722			-1.1233			-0.2344			-0.1757	0.0208	0.1269
30/06/2017	13:51:16:446233987	viktor		0		0			0.4399			-1.4817			0.0722			-0.9105	0.1063	-2.4367
30/06/2017	13:51:16:646117985	viktor		0		0			0.3031			-0.8125			0.0888			0.1199	-0.4099	-2.9336
30/06/2017	13:51:16:846738994	viktor		0		0			0.4814			-0.9312			0.0359			0.0527	0.4379	2.4922
30/06/2017	13:51:17:46806991	viktor		0		0			0.4044			-0.8056			-0.0956			0.6925	-0.2179	2.5750

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 :

 

GUI DL4J

Dashboard principale de DL4J

 

GUI 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 :

<dependency>
	<groupId>org.deeplearning4j</groupId>
	<artifactId>deeplearning4j-core</artifactId>
	<version>${dl4j.version}</version>
</dependency>
<dependency>
	<groupId>org.nd4j</groupId>
	<artifactId>nd4j-cuda-8.0-platform</artifactId>
	<version>${nd4j.version}</version>
</dependency>      
<dependency>
	<groupId>org.nd4j</groupId>
	<artifactId>nd4j-native-platform</artifactId>
	<version>${nd4j.version}</version>
</dependency>
<dependency>
	<groupId>org.deeplearning4j</groupId>
	<artifactId>deeplearning4j-ui_2.11</artifactId>
	<version>${dl4j.version}</version>
</dependency>

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.

	  CSVReader reader = new CSVReader(",", csvPath, true);
        LocalTime lastTime = null;
        LocalDate lastDate = null;
        List<INDArray> timeSeries = new ArrayList<>();
        List<INDArray> labels = new ArrayList<>();
        List<double[]> curTimeSerie = new ArrayList<>();
        List<double[]> curLabel = new ArrayList<>();
        int counter = 0;
        while (reader.readNextLine()) {
            if (counter % 1000 == 0) {
                LOGGER.debug("Processing line {}", counter);
            }
            double accelX = Double.parseDouble(reader.getColName("acceleration_x"));
            double accelY = Double.parseDouble(reader.getColName("acceleration_y"));
            double accelZ = Double.parseDouble(reader.getColName("acceleration_z"));
            double gyroX = Double.parseDouble(reader.getColName("gyro_x"));
            double gyroY = Double.parseDouble(reader.getColName("gyro_y"));
            double gyroZ = Double.parseDouble(reader.getColName("gyro_z"));
            double wrist = Double.parseDouble(reader.getColName("wrist"));
            double activity = Double.parseDouble(reader.getColName("activity"));
            String date = reader.getColName("date");
            String time = reader.getColName("time");
            // Transform the time string into a java.time.LocalTime
            LocalTime curTime = getCurTime(time);
            // Transform the the date string into a java.time.LocalDate
            LocalDate curDate = getCurDate(date);
            // Determinates if this is a new time serie
            if (!isTheSameTimeSerie(lastTime, lastDate, curTime, curDate)) {
                // we create the new INDArrays and we move to the next
                if (lastTime != null) {
                    // This is not the first time
                    INDArray timeSerie = buildNDArray(curTimeSerie);
                    INDArray label = buildNDArray(curLabel);
                    timeSeries.add(timeSerie);
                    labels.add(label);
                    curTimeSerie.clear();
                    curLabel.clear();
                }
            }
            double[] curValsTab = new double[]{accelX, accelY, accelZ, gyroX, gyroY, gyroZ};
            double[] curLabelTab = new double[]{activity == 0 ? 1 : 0, activity == 1 ? 1 : 0};
            curTimeSerie.add(curValsTab);
            curLabel.add(curLabelTab);
            lastTime = curTime;
            lastDate = curDate;
            counter++;
        }

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 :

private static boolean isTheSameTimeSerie(LocalTime lastTime, LocalDate lastDate, LocalTime curTime,
            LocalDate curDate) {
        if (lastTime == null || lastDate == null || !lastDate.equals(curDate)) {
            return false;
        }
        if ((curTime.getSecond() - lastTime.getSecond()) <= 2 && (curTime.getMinute() - lastTime.getMinute()) < 2
                && (curTime.getHour() - lastTime.getHour()) < 2) {
            return true;
        }
        return false;
    }

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.

double trainRatio = 0.7;
List<INDArray> timeSeriesTest = new ArrayList<>();
List<INDArray> testLabels = new ArrayList<>();
int trainSize = (int) (timeSeries.size() * trainRatio);
Random random = new Random(33);
int maxTimeSeriesReali = timeSeries.stream().mapToInt(t -> t.rows()).max().getAsInt();
while (timeSeries.size() > trainSize) {
	int index = random.nextInt(timeSeries.size());
	timeSeriesTest.add(timeSeries.remove(index));
	testLabels.add(labels.remove(index));
}
LOGGER.debug("Train time series set size {}", timeSeries.size());
LOGGER.debug("Test time series set siwe {}", timeSeriesTest.size());

Puis on construit le tenseur d’entraînement et de test :

INDArray[] trainDataMask = new INDArray[1];
INDArray trainData = buildTimeSerieTensor(timeSeries, maxTimeSeriesReali, trainDataMask);
int[] sh = trainData.shape();
LOGGER.trace("Nb shapes : {}", sh.length);
LOGGER.trace(" shape dims : {},{},{}", sh[0], sh[1], sh[2]);
INDArray[] trainLabelMask = new INDArray[1];
INDArray trainLabel = buildTimeSerieTensor(labels, maxTimeSeriesReali, trainLabelMask);

Le code de la méthode buildTensor, le pilier de cette partie :

private static INDArray buildTimeSerieTensor(List<INDArray> timeSeries, int maxTimeSeriesReali, INDArray[] trainMask) {
        INDArray res = Nd4j.zeros(maxTimeSeriesReali, timeSeries.get(0).columns(), timeSeries.size());
        trainMask[0] = Nd4j.zeros(maxTimeSeriesReali, timeSeries.size());
        for (int k = 0; k < timeSeries.size(); k++) {
            int[] curShape = timeSeries.get(k).shape();
            for (int i = 0; i < maxTimeSeriesReali; i++) {
                if (curShape[0] <= i) {
                    break;
                }
                for (int j = 0; j < timeSeries.get(0).columns(); j++) {
                    res.put(new int[]{i, j, k}, timeSeries.get(k).getScalar(i, j));
                }
                trainMask[0].put(new int[]{i, k}, Nd4j.ones(1, 1));
            }
        }
        return res;
    }

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 :

NeuralNetConfiguration.Builder builder = new NeuralNetConfiguration.Builder();
        // The number of iterations in the training
        builder.iterations(nbIterations);
        // The initial part of the gradient that will be use in each iteration.
        // A low value will slow the training, but higher value can make the network diverge
        builder.learningRate(0.01);
        // THE algotihm to train neural networks
        builder.optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT);
        builder.seed(123);
        builder.biasInit(0);
        // we use all the data each time we update the network
        builder.miniBatch(false);
        // A standard correct choices
        builder.updater(Updater.RMSPROP);
        builder.weightInit(WeightInit.XAVIER);

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 :

ListBuilder listBuilder = builder.list();
        GravesLSTM.Builder hiddenLayerBuilder = new GravesLSTM.Builder();
        // There are 6 variables that will be used (3 for the gyroscope,
        // 3 for the the accelerometer)
        hiddenLayerBuilder.nIn(6);
        hiddenLayerBuilder.nOut(interNeurons);
        // adopted activation function from GravesLSTMCharModellingExample
        // seems to work well with RNNs
        hiddenLayerBuilder.activation(Activation.TANH);
        // we add the layer in first position
        listBuilder.layer(0, hiddenLayerBuilder.build());
        RnnOutputLayer.Builder outputLayerBuilder = new RnnOutputLayer.Builder(LossFunction.MCXENT);
        // softmax normalizes the output neurons, the sum of all outputs is 1
        // this is required for our sampleFromDistribution-function
        outputLayerBuilder.activation(Activation.SOFTMAX);
        outputLayerBuilder.nIn(interNeurons);
        outputLayerBuilder.nOut(2);
        listBuilder.layer(1, outputLayerBuilder.build());
        // specify if the network must be trained
        listBuilder.pretrain(false);
        listBuilder.backprop(true);

Et puis le réseau est initialisé :

MultiLayerConfiguration conf = listBuilder.build();
        MultiLayerNetwork net = new MultiLayerNetwork(conf);
        net.init();

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 :

// Here we set the port that will be used to deploy the graphical interface
        System.setProperty("org.deeplearning4j.ui.port", Integer.toString(guiPort));
        UIServer uiServer = UIServer.getInstance();
        StatsStorage statsStorage = new InMemoryStatsStorage();
        uiServer.attach(statsStorage);
        net.setListeners(new ScoreIterationListener(5), new StatsListener(statsStorage, 1));

Entraînement

Lorsque l’on a mis les données dans la bonne structure de données, l’entraînement se fait facilement :

DataSet dataSet = new DataSet(trainData, trainLabel, trainDataMask[0], trainLabelMask[0]);
net.fit(dataSet);

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.

DataSet dataSet = new DataSet(trainData, trainLabel, trainDataMask[0], trainLabelMask[0]);
net.fit(dataSet);

 

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 :

private static void evaluateDataset(MultiLayerNetwork net, List<INDArray> unPaddedDataset,
            List<INDArray> unPaddedlabels, int nbOutComes) {
        Evaluation eval = new Evaluation(nbOutComes);
        for (int i = 0; i < unPaddedDataset.size(); i++) {
            net.rnnClearPreviousState();
            INDArray out = net.rnnTimeStep(unPaddedDataset.get(i));
            eval.eval(unPaddedlabels.get(i), out);
        }
        LOGGER.debug(eval.stats());
    }

Puis on génère des statistiques sur l’ensemble de l’entraînement et l’ensemble de test en utilisant cette fonction :

LOGGER.debug("Training stats ouput");
        evaluateDataset(net, timeSeries, labels, dataSet.numOutcomes());

        // compute test
        LOGGER.debug("Tests stats output");
        evaluateDataset(net, timeSeriesTest, testLabels, dataSet.numOutcomes());

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 :

Examples labeled as 0 classified by model as 0: 29242 times
Examples labeled as 0 classified by model as 1: 2287 times
Examples labeled as 1 classified by model as 0: 1370 times
Examples labeled as 1 classified by model as 1: 30099 times
==========================Scores========================================
 # of classes:    2
 Accuracy:        0,9420
 Precision:       0,9423
 Recall:          0,9420
 F1 Score:        0,9427

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 :

Examples labeled as 0 classified by model as 0: 11143 times
Examples labeled as 0 classified by model as 1: 669 times
Examples labeled as 1 classified by model as 0: 581 times
Examples labeled as 1 classified by model as 1: 12266 times


==========================Scores========================================
 # of classes:    2
 Accuracy:        0,9493
 Precision:       0,9494
 Recall:          0,9491
 F1 Score:        0,9515

L’interface de DL4J nous montre ces statistiques d’évolution de l’entraînement du réseau de neurones.

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 :

Examples labeled as 0 classified by model as 0: 28304 times
Examples labeled as 0 classified by model as 1: 1218 times
Examples labeled as 1 classified by model as 0: 955 times
Examples labeled as 1 classified by model as 1: 32521 times

==========================Scores========================================
 # of classes:    2
 Accuracy:        0,9655
 Precision:       0,9656
 Recall:          0,9651
 F1 Score:        0,9677

Pour les données de test :

Examples labeled as 0 classified by model as 0: 11282 times
Examples labeled as 0 classified by model as 1: 595 times
Examples labeled as 1 classified by model as 0: 336 times
Examples labeled as 1 classified by model as 1: 12446 times

==========================Scores========================================
 # of classes:    2
 Accuracy:        0,9622
 Precision:       0,9627
 Recall:          0,9618
 F1 Score:        0,9639

Et les mêmes graphiques d’entraînement :

L’interface de DL4J nous montre ces statistiques d’évolution de l’entraînement du réseau de neurones.

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