Vous êtes sur la page 1sur 33

Gymnase de Burier

E-Magination

Burier, le 28 août 2020 Arthur Wuhrmann 3M5


Table des matières

1 Introduction 5

2 Théorie 7
2.1 Le réseau de neurone simple . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.1.1 Analogie biologique . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.1.2 Architecture basique . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.1.3 Feedforward . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.1.4 Étapes de l’apprentissage . . . . . . . . . . . . . . . . . . . . . . . . 9
2.1.5 Fonction de perte . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.1.6 Rétro-propagation . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.1.7 Finalement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.2 Réseaux de neurones récurrents . . . . . . . . . . . . . . . . . . . . . . . . 13
2.2.1 Architecture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.2.2 Feedforward . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.2.3 Rétro-propagation . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.3 LSTM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.3.1 Feedforward . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.3.2 Rétro-propagation . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

3 Application 19
3.1 En bref . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
3.2 Outils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.3 Application concrète . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.4 Ensembles de données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3.5 Résultats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

4 Musique 27
4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
4.2 Traitement des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
4.3 Architectures utilisées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
4.4 Choix des morceaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.5 Résultats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

5 Conclusion 31
E-Magination Arthur Wuhrmann

4
Chapitre 1

Introduction

Mon travail consiste à créer une machine capable de composer de la musique en lui
faisant lire une grande quantité de partitions musicales. Je vais chercher à trouver un
modèle me permettant de générer des morceaux de musique originaux en utilisant le
concept d’intelligence artificielle. Pour cela, j’utilise une technique nommée apprentissage
automatique ou  Machine Learning . C’est une catégorie d’intelligence artificielle qui,
grâce à de nombreuses données, va ajuster ses paramètres afin de générer des données
similaires à celles que nous voulons qu’elle nous renvoie. Cela va de la reconnaissance
d’images à la génération de texte.
Dans mon cas, je donnerai une grande quantité de morceaux de musiques à la machine
afin qu’elle puisse  comprendre  leur fonctionnement et en générer. A chaque morceau
que le programme écoutera, il modifiera légèrement ses paramètres afin de s’améliorer,
pour au final obtenir un résultat à priori ressemblant aux morceaux donnés.
Pour y parvenir, je vais utiliser un type d’apprentissage automatique appelé réseaux
de neurones récurrents (RNN) dont j’expliquerai le fonctionnement dans le travail, et qui
sont très utilisés pour travailler à partir d’informations séquentielles (séquences de mots,
d’images ou ici de notes de musique).

5
E-Magination Arthur Wuhrmann

6
Chapitre 2

Théorie

Comme je l’ai énoncé ci-dessus, je vais utiliser un type d’IA [Intelligence Artificielle]
appelée  réseau de neurones récurrents . C’est une manière d’aborder l’apprentissage
automatique. Commençons d’abord par définir un réseau de neurones simple.

2.1 Le réseau de neurone simple


2.1.1 Analogie biologique
Les réseaux de neurones, dit neuronaux, s’inspirent légèrement des réseaux de neurones
biologiques du cerveau : le cerveau humain possède une centaine milliard de neurones,
reliés entre eux par des synapses. Il y a plusieurs milliers de synapses en moyenne connectés
à chaque neurone. Les neurones transmettent des signaux électriques plus ou moins forts
entre les organes et les muscles du corps humain pour les faire interagir. Un neurone peut
être plus ou moins activé, c’est-à-dire qu’il peut transmettre un signal électrique plus ou
moins fort. Les synapses, en plus de transmettre les signaux entre les neurones, peuvent
les amplifier et les réduire. Par analogie, un neurone peut également être beaucoup ou
peu affecté par un signal entrant.

2.1.2 Architecture basique


Les réseaux de neurones utilisés en IA possèdent également des neurones, reliées par
des synapses. Chaque neurone n’est rien de plus qu’un nombre entre 0 et 1, qui définit à
quel point le neurone est activé. La valeur d’un neurone n’est pas définie à l’origine, mais
sera calculée grâce à celles des synapses.
Le réseau est divisé en plusieurs couches. La première est la couche dite d’entrée, et
c’est à cette couche là que nous allons donner nos informations. Une information peut
être une lettre d’un texte, une note de musique, ou encore un pixel d’une image. Ces
informations représentent les données dont le réseau a besoin pour résoudre la tâche que
nous lui avons  confiée . Si vous souhaitez faire un réseau qui apprend à jouer aux
échecs, il faudra lui donner la position de toutes les pièces, ainsi d’éventuellement les
derniers coups joués. Ces informations vont parcourir le réseau et parvenir par la dernière
couche, dite couche de sortie. Les couches comprises entre la première et la dernière sont
appelées couches cachées. Les informations sont manipulées à l’aide de simples opérations
mathématiques (additions, multiplications).

7
E-Magination Arthur Wuhrmann

Couche Couche Couche


d’entrée cachée de sortie
Entrée #1
Sortie #1
Entrée #2
Sortie #2
Entrée #3

Figure 2.1 – Réseau de neurones simple

Dans un réseau classique tous les neurones de chaque couche est relié avec tous neurone
de la couche suivante (sauf ceux de la dernière couche puisqu’il n’y a pas couche qui suit).
Cette liaison représente justement un synapse. Comme je l’ai dit plus tôt, un synapse
amplifie ou réduit un signal. Dans notre réseau artificiel, un synapse n’est lui aussi rien
de plus qu’un nombre, compris dans R, qui va agir comme coefficient de la puissance
du signal du premier neurone auquel il est relié dans le calcul de la valeur du deuxième
neurone subséquent.
En plus de synapses, les réseaux comportent finalement ce qu’on appelle des biais.
A PRECISER NOMBRE. Chaque neurone en dehors de ceux de la première couche
possèdent un biais. Le biais va également impacter, comme un synapse, la valeur du
neurone, mais pas de la même manière. La figure 2.1 montre un exemple de réseau à 3
couches, dont la première et la deuxième ont trois neurones, et dont la dernière couche
n’en comporte que 2. Les points gris représentent des neurones, et les flèches les reliant
symbolisent les synapses.

2.1.3 Feedforward
Lorsque que nous passons une information au réseau, il va calculer la valeur de chaque
neurone grâce aux synapses et aux biais qui ont des valeurs définies. L’information va se
 déplacer  comme sur les flèches de la figure 2.1.

L’information ne peut passer d’un neurone à l’autre que grâce aux synapses, et les
synapses relient tous les neurones d’une couche avec ceux de la couche suivantes. Il faut
donc commencer par calculer les neurones de la deuxième couche, pour ensuite calculer
ceux de la troisième, et ce jusqu’à la couche de sortie. Plus il y a de couches, plus le calcul
sera long. Pour calculer la valeur d’un neurone de la deuxième couche, nous allons faire
la somme des neurones de la première couche pondérée par les synapses les reliant au
neurone que l’on souhaite calculer, puis nous allons ajouter à cette somme le biais.
Cependant, j’ai dit plus haut que la valeur d’un neurone était toujours comprise entre
0 et 1. Or, rien ne nous garantit qu’ici, le résultat sera compris entre ces deux valeurs.
C’est pour cela qu’il faut faire passer ledit résultat dans une fonction dont l’image est
l’intervalle [0; 1] avant de l’assigner au neurone. Ces fonctions sont dites d’activation, et
il en existe beaucoup. L’une des plus utilisées et la fonction sigmoı̈de, notée σ(x) définie
par :
1
σ(x) =
1 + e−x
.

8
Arthur Wuhrmann E-Magination

0.8

0.6

σ(x)
0.4

0.2

0
−6 −4 −2 0 2 4 6
x

Figure 2.2 – Fonction sigmoı̈de

Si x −→ +∞, e−x sera proche de 0 donc σ(x) ≈ 1. En revanche,


si x −→ −∞, e−x sera très grand, donc σ(x) ≈ 0.
Le calcul du premier neurone de la deuxième couche se présente ainsi :
(2) (1) (1) (1) (2)
x1 = σ(x1 · W1×1 + x2 · W2×1 + ... + xn−1 · W(n−1)×1 + x(1)
n · Wn×1 + b1 )

où :
– Wi×j désigne le synapse reliant le jème neurone d’une couche au ième de l’autre couche
(L)
– xj désigne le j ème neurone de la Lème couche
(L)
– bj désigne le j ème biais de la Lème couche
Le tout peut être écrit sous la forme suivante :
n
X
xi×j = σ( (x(i−1)×k · Wk×j ) + bi×j )
k=0

Cette formule est d’abord utilisée pour calculer les neurones de la deuxième couches,
puis de la troisième, et ainsi de suite jusqu’à la dernière couche.
Elle est l’essence même du calcul des valeurs d’un réseau et même si elle paraı̂t com-
pliquée à premier abord, n’y figurent, à part pour la fonction sigmoı̈de, que des additions
et des multiplications.
Le fait de  calculer  un réseau comme cela se nomme  feedforward , car le passage
de l’information ne se fait que dans un sens.

2.1.4 Étapes de l’apprentissage


L’apprentissage automatique d’un réseau de neurone simple peut se résumer de la
manière suivante :
a) initialiser les poids des synapses, généralement aléatoirement ;
b) faire un passage  feedforward  ;

9
E-Magination Arthur Wuhrmann

c) calculer à quel point les résultats du réseau du neurone sont faux ;


d) corriger le réseau en fonction de l’erreur obtenue ;
e) répéter les étapes 2 à 5 ;

2.1.5 Fonction de perte


La bibliothèque NumPy est un outil qui permet de réaliser l’étape 1, et j’ai expliqué
ci-dessus le déroulement de la deuxième étape, ce qui permet d’obtenir des valeurs pour
les neurones de sorties.
Pour calculer à quel point ces valeurs sont fausses, il faut définir ce qui est juste. Lors
de l’apprentissage dit supervisé, nous allons donner au réseau des informations dont nous
connaissons le  résultat . Si nous créons un réseau dans le but de reconnaı̂tre les genres
musicaux, le  résultat  d’un menuet de Mozart sera le genre musical classique.
Dans l’idéal, le réseau devrait nous renvoyer ce même résultat. Cependant, comme
les poids sont d’abord initialisés aléatoirement, le réseau donne des réponses évidemment
fausses. C’est pour cela que nous devons l’entraı̂ner. Il faudra donc vérifier à chaque fois si
la valeur en sortie du réseau correspond à la valeur attendue. Soit ŷ la valeur de sortie et
y le résultat voulu, l’erreur sera calculée grâce à une fonction dite de  perte  L(y, ŷ) (L
pour  Loss  de l’anglais signifiant perte). On appelle la valeur calculée par cette fonction
erreur, perte ou encore coût. Il existe un grand nombre de ces fonctions, mais dans le cas
des réseaux de neurones simples, la plus utilisée est l’erreur quadratique moyenne :
n
1X
L(y, ŷ) = (yi − ŷi )2
n i=0

Cette fonction renvoie un nombre entre 0 et 1. Plus ŷi est différent de yi , plus la valeur
(yi − ŷi )2 sera proche de 1. De façon formelle, L(y, ŷ) = 1 ⇐⇒ (yi − ŷi )2 = 1 ∀ yi , ŷi ∈ y, ŷ
donc ⇐⇒ yi − ŷi = ±1. A l’opposé, si yi = ŷi ∀ yi , ŷi ∈ y, ŷ alors L(ŷ, y) = 0. Nous
pouvons donc en conclure que la fonction de perte sera proche de 1 si les valeurs calculées
par le réseau sont très fausse et sera proche de 0 si, au contraire, elles sont très justes.
Cette fonction est très utile car elle permet de voir où en est l’apprentissage du réseau.
Une fois que la valeur est suffisamment faible, et que donc le réseau est suffisamment
entraı̂né, il est utilisable et n’a plus besoin d’apprendre.

2.1.6 Rétro-propagation
Comme nous l’avons vu dans le chapitre 2.1.3, le fait de donner des informations
à la couche d’entrée afin que le réseau calcule la couche de sortie s’appelle le passage
 feedforward . La rétro-propagation est l’étape inverse. Nous partons des neurones de

sortie pour arriver jusqu’à ceux d’entrée. La rétro-propagation permet calculer où et dans
quel sens il faut modifier les paramètres du réseau pour que les résultats soient plus
 justes . Ce procédé utilise un principe connu de l’analyse mathématique, la dérivée.

La dérivée est un outil qui permet de savoir non seulement dans quel sens nous devons
nous déplacer, mais également à quel point nous devons le faire pour minimiser une
fonction. Une bonne manière de visualiser cela est d’imaginer une surface d’un espace
en 3 dimensions comportant beaucoup de creux et de bosses. Imaginez maintenant une
boule placée sur cette surface. Notre but consiste à déplacer la boule le plus bas possible.

10
Arthur Wuhrmann E-Magination

Couche Couche Couche


d’entrée cachée de sortie
Entrée Sortie

Figure 2.3 – Caption

La dérivée va permettre de savoir dans quelle direction il faut se diriger pour la faire
descendre. Bien que cette tâche paraisse facile dans ce cas ce n’est plus vrai lorsqu’il y a
plusieurs centaines, voir milliers de paramètres à modifier.
Prenons un exemple simple pour expliquer ce principe : soit un réseau de trois couches,
dont chaque couche comporte un neurone (c.f. Figure 2.3)

Pour simplifier les choses, admettons que nous souhaitions obtenir la sortie 1 quelle
que soit la valeur d’entrée. Reprenons nos étapes de la section 2.1.4, et commençons
donc par initialiser des poids et des biais aléatoires. Pour réaliser la deuxième étape il
nous faut des données. Or, comme je l’ai mentionné, il faut que notre réseau renvoie 1
quelle que soit la valeur entrée. Ainsi, nous pouvons tester avec une valeur, elle aussi
choisie aléatoirement. Prenons 0.3. Lorsque je fais un passage  feedforward , le réseau
me renvoie 0.4. Calculons l’erreur grâce à la fonction d’erreur quadratique moyenne. Nous
obtenons :
L(y, ŷ) = L(1, 0.4) = (1 − 0.4)2 = 0.62 = 0.36
Sachant que l’erreur devrait idéalement être égale à 0, et qu’elle ne peut pas être supérieure
à 1, 0.49 n’est pas un bon résultat. Il faut donc modifier les paramètres grâce à la rétro-
propagation de façon à ce que l’erreur diminue. Dans la figure 2.3, il y a deux synapses
et deux biais, cela fait 4 paramètres à modifier.
Comme je l’ai dit plus tôt, l’algorithme de rétro-propagation commence par modifier
les paramètres de la couche de sortie pour enfin arriver à celle d’entrée. Commençons par
calculer la dérivée de l’erreur en fonction du biais de la couche de sortie (dans ce cas, nous
aurions aussi pu commencer par le synapse, cela n’a pas d’importance). Il faut pour cela
utiliser le théorème de dérivation des fonctions composées :

∂L(y, ŷ) ∂ ∂
= (y − ŷ)2 = (1 − σ(w · x + b))2
∂b ∂b ∂b
= 2(1 − σ(w · x + b)) · σ 0 (w · x + b) · 1
où :
– w désigne le synapse reliant le dernier neurone à celui de la couche cachée ;
– x désigne le neurone de la dernière couche ;
– b désigne le biais du neurone de la dernière couche couche ;
– σ désigne la fonction sigmoı̈de ;
– L désigne la fonction d’erreur quadratique moyenne.
Dans le cas du synapse, c’est :

∂L(1, ŷ)
= 2(1 − σ(w · x + b))σ 0 (w · x + b) · x
∂w
11
E-Magination Arthur Wuhrmann

On peut en conclure, c’est que la réponse est composée du produit de trois polynômes :
– le premier représente la vitesse à laquelle l’erreur varie en fonction de la fonction
d’activation ;
– le deuxième à quel point la fonction d’activation varie en fonction de w · x + b ;
– le troisième à quel point w · x + b varie en fonction de b.
En somme, cela revient à écrire :
∂L ∂L ∂σ ∂z
= · ·
∂b ∂σ ∂z ∂b
où z = w · x + b.
Nous pouvons désormais calculer l’influence que le neurone de la couche cachée a sur
le coût :
∂L ∂L ∂σ ∂z
= · ·
∂x ∂σ ∂z ∂x
= 2(1 − σ(w · x + b))σ 0 (w · x + b) · w
A partir de cela, nous pouvons, en utilisant une fois de plus le théorème de dérivation
composée, calculer la variation de l’erreur en fonction des biais et synapses des couches
précédentes.
L’exemple que j’ai pris ne correspond pas à la grande majorité des réseaux de neurones
simples, car un réseau possède habituellement plusieurs neurones par couche. Cependant,
cela ne change pas beaucoup de choses. Prenons le réseau suivant :

Couche Couche Couche


d’entrée cachée de sortie
Entrée #1
Sortie #1
Entrée #2
Sortie #2
Entrée #3

Figure 2.4 – Réseau complexe

Dans cette configuration, l’erreur se calcule en fonction de la sortie 1 et de la sortie 2.


C’est pourquoi les deux synapses et le neurone illustrés en violet ont une influence sur le
coût. Ainsi, la dérivée de l’erreur en fonction dudit neurone des sorties 1 et 2. A FINIR
Nous aimerions calculer
∂L
(2)
∂x1
Or nous n’avons que
∂L ∂L
(2)
et (2)
∂x1 ∂x1
Pour palier à ce problème, il faut additionner les dérivées des deux sorties :

12
Arthur Wuhrmann E-Magination

(3) (3)
∂L ∂L ∂x1 ∂L ∂x2
(2)
= (3)
· (2)
+ (3)
· (2)
∂x1 ∂x1 ∂x1 ∂x2 ∂x1
Le nom que l’on donne à ce type de dérivées est le gradient. Il indique la variation
d’une fonction.
Cette partie est de loin la plus complexe de mon travail, et également de la plupart
des réseaux de neurones.
Il faut finalement modifier ces paramètres en fonction de ces dérivées. On ne peut pas
directement modifier la valeur d’un neurone, car elle change en fonction des entrées du
réseau. C’est les synapses et les biais que l’on modifie. On les multiplie par un coefficient
appelé taux d’apprentissage,  learning rate , soit µ souvent égal à 0.01. Un taux d’ap-
prentissage trop élevé ne permettrait pas de diminuer l’erreur efficacement pour plusieurs
raisons mais entre autres à cause du manque de précision. Un petit taux fait faire des
petits  pas  au réseau dans la direction du point le plus bas (afin de minimiser l’erreur).
Cependant, plus le taux est petit, plus la durée d’apprentissage sera longue, car le réseau
mettra plus de temps avant d’atteindre une erreur suffisamment basse.

2.1.7 Finalement
Les réseaux de neurones simples sont surtout des grosses machines avec de nombreux
boutons (synapses et biais) à tourner afin d’obtenir une erreur la plus proche de 0 possible.
Pour cela, il faut partir de boutons avec des valeurs aléatoires et les tourner dans le bon
sens pour que cela réduise l’erreur. En répétant cette opérations maintes fois, le réseau
s’améliore et une fois assez entraı̂né, il fournit des résultats qui correspondent aux données
que l’on souhaite étudier. Le réseau va générer un modèle permettant d’expliquer les
données afin de prédire certaines valeurs. Par exemple, un réseau de neurones entraı̂né
à cette tâche pourrait reconnaı̂tre un chiffre écrit à la main provenant d’une image bien
qu’il n’ait encore jamais vu ladite image.

2.2 Réseaux de neurones récurrents


Les réseaux de neurones récurrents (RNNs) sont des réseaux de neurones similaires
aux réseaux de neurones simples mais conçus pour étudier des séquences et les corrélations
entre les différents éléments de la séquences. Imaginons un texte en français composé de
nombreuses phrases. Une phrase étant une séquence de mots, un RNN pourrait parfai-
tement les analyser afin de créer un modèle générant des phrases  similaires  à celles
du texte. Nous donnons le début d’une phrase au réseau et il doit nous rendre la fin. Un
réseau de neurone simple aurait beaucoup de peine à réaliser cette tâche car les neurones
d’une même couche ne communiquent pas directement entre elles. Le réseau simple ne
pourrait pas étudier les corrélations entre les différents mots de notre phrase, tandis que
notre RNN pourrait aisément prédire un  La  ou  Le  après une fin de phrase.

2.2.1 Architecture
Voici ce à quoi ressemble un réseau de neurones récurrents :

13
E-Magination Arthur Wuhrmann

Couche Couche Couche


d’entrée cachée de sortie
Entrée au temps t = 1 Sortie au temps t = 1

Entrée au temps t = 2 Sortie au temps t = 2

Entrée au temps t = 3 Sortie au temps t = 3

...

Figure 2.5 – Réseau de neurones récurrent (RNN)

Plusieurs éléments diffèrent du réseau de neurones simple. Premièrement, les noms des
entrée et sorties sont définies par t. t est une variable nommée ainsi car elle représente
le temps. Dans un réseau de neurones simple, les calculs peuvent se faire simultanément,
tandis que dans un RNN, le calcul commence obligatoirement par la première entrée et
finit par la dernière. Il est impossible de faire autrement car le calcul du neurone de la
couche cachée de la deuxième entrée dépend de la valeur du neurone de la première entrée,
comme indiqué par les flèches.
Ensuite, les connections entre les neurones sont bien moins nombreuses. Chaque neu-
rone d’entrée n’est connecté qu’à un neurone, les neurones de la couche cachée ne sont
reliés qu’à deux neurones chacun. Les connections entre les neurones cachés font la par-
ticularité et la force d’une telle architecture. La valeur d’un neurone caché au temps t
dépend non seulement de l’entrée t, mais également du neurone caché t − 1. Ainsi, en
remontant les flèches, la valeur de la sortie t = 3 dépend des entrées t = 1, t = 2, t = 3.
Vous remarquerez qu’une flèche relie le dernier neurone de la couche cachée à  ... .
C’est pour signifier que il peut y avoir n entrées, où n est la longueur de la séquence
étudiée. Contrairement au réseau de neurones simples, le réseau de neurones récurrent
peut étudier des séquences de longueur variable. Les valeurs des poids des synapses ne
varient pas en fonction de t. Ainsi, tous les synapses illustrés en vert ont le même poids.
Cette particularité s’observe facilement lorsque l’on représente le RNN de cette manière :

Couche Couche Couche


d’entrée cachée de sortie
Entrée Sortie

Figure 2.6 – RNN  enroulé 

Dans ce schéma, la boucle explicite la récurrence du réseau. Le neurone de la couche


cachée est représenté par un seul rond, mais il peut y avoir plusieurs neurones cachées.
Dans ce cas-là, ce ne serait en réalité plus un nombre variant de 0 à 1 mais un vecteur
de taille x composé de nombres variant de 0 à 1. La taille de ce vecteur est une valeur à
définir avant de démarrer le réseau. En théorie, plus elle est grande, plus le réseau sera
doué. Cependant, cela n’est pas vérifié en pratique. En outre, plus la taille du vecteur est

14
Arthur Wuhrmann E-Magination

grande, plus il faudra de temps au réseau pour faire ces calculs.

2.2.2 Feedforward
Comme tous (ou presque tous) les réseaux de neurones, le RNN effectue les étapes
du point 2.1.4. Il faut donc réaliser un passage  feedforward  afin de calculer l’erreur
pour la corriger lors de la rétro-propagation. Les calculs liés à l’étape  feedforward  ne
diffèrent que peu de ceux lié à un réseau de neurone simple mis à part le fait que, pour
calculer un neurone de la couche cachée au temps t, il faut utiliser la valeur de ce même
neurone caché au temps t − 1 (ainsi que la valeur d’entrée au temps t) :

(t=3) (t=3) (t=2)


x2×1 = σ(x1×1 · W1×1 + x1×2 · U1 )
où :
(t=k)
– xi×j désigne le j ème neurone de la ième couche au temps t=k.
– Ui désigne le synapse de la couche cachée reliant le ième neurone au temps t − 1 au
ième neurone au temps t (représenté par la flèche en boucle de la figure METTRE
REFERENCE FIGURE).
Il n’y a pas de différences avec le réseau de neurones simple en ce qui concerne le calcul
du neurone de sortie.

2.2.3 Rétro-propagation
La rétro-propagation dans un RNN est elle aussi très similaire à celle d’un réseau
de neurones simple. La fonction de perte est utilisée de la même manière, mais il y a
néanmoins une différence, qui est la cause du problème principal des RNN. Comme je l’ai
expliqué auparavant, le calcul de la sortie au temps t = 10 nécessite le calcul du neurone
caché au temps t = 9, qui lui-même a besoin de celui du temps t = 8, etc, jusqu’au temps
t = 1. Ainsi, la sortie au temps t = 10 dépend des calculs de tous les neurones cachés
depuis le temps t = 1. C’est pourquoi, afin de corriger le réseau, il faut remonter toutes
les valeurs afin de corriger les synapses et biais des couches d’entrée et couches cachées.
Cependant, plus on remonte, plus la valeur à calculer sera petite. Cela s’explique par le
fait que plus des éléments sont éloignés, plus la corrélation entre les deux est faible.
Prenons par exemple la phrase  J’aime la musique  et considérons-la comme une
séquence de lettres. La lettre  q  a une forte influence sur les lettres  u  et  e  car
en français, ces lettres se suivent souvent. Cependant, la lettre  i  du mot  aime  n’a
que très peu d’influence sur les lettres  u  et  e  du mot  musique .
En anglais, ce problème est appelé  Vanishing gradients problem , problème d’évanescence
des gradients. La solution la plus efficace pour résoudre ce problème est d’utiliser des
réseaux  LSTM ,  Long Short Term Memory . Ils possèdent des petites capsules dans
la couche cachée qui conservent l’information des couches précédentes, et sont bien plus
facile à calculer lors de la rétro-propagation.
Afin de calculer l’influence qu’a le réseau sur la sortie au temps to , il faut calculer
l’influence qu’on tous les neurones des couches cachées de 0 à to grâce au Théorème de
dérivation des fonctions composées. En tenant compte de ces influences, on peut modifier
les paramètres afin d’améliorer les performances du réseau. Il faut faire cette opération

15
E-Magination Arthur Wuhrmann

pour toutes les sorties, de la dernière jusqu’à la première, soit de t = N jusqu’à t = 0


pour une séquence de taille N .
Je ne développerai pas les calculs de la rétro-propagation du RNN, ils sont relativement
similaires à ceux du réseau de neurones simple et à ceux du LSTM qui seront expliqués
plus loin.

2.3 LSTM
Les LSTM sont des réseaux de neurone récurrents, comme ceux que l’on vient de voir.
Ils ont toutefois la particularité de garder plus longtemps les informations en mémoire et de
trier efficacement les informations entrantes et sortantes. Plutôt que de parler de neurones,
on utilise le terme de cellules dans un LSTM ; une couche LSTM est composée d’un certain
nombre de cellules connectées entre elles de la même manière que les neurones d’un RNN
(c.f 2.5). Dans une couche d’un LSTM, chaque cellule prend en entrée les informations
de l’entrée actuelle du réseau ainsi que la sortie de la cellule précédente, à l’instar d’un
RNN. Ensuite, elle renvoie ces informations traitées qui serviront à la fois à construire la
sortie du réseau et pour la cellule de la prochaine entrée.
VERIFIER FORMULATION
Voici un schéma :

Sortie t ohti

Cellule t Cellule t+1

cht−1i × + chti

tanh

× ×

Caché t fg ig cg og Caché t+1

hht−1i hhti

Entrée t xhti

Figure 2.7 – Cellule LSTM

METTRE REFERENCE SCHEMA


Le symbole × représente une multiplication matricielle et le symbole + une addition
de deux vecteurs. Lorsque deux lignes, qui symbolisent des vecteurs, se rejoignent, lesdits
vecteurs sont concaténés. La case tanh signifie que la fonction tangente hyperbolique est
appliquée au vecteur passant par la case en question.
Le rectangle brun aux bords arrondi de la figure 2.7 représente la cellule LSTM. On
constate qu’elle prend au temps t l’entrée x(t) et deux autres données : la couche cachée
et la cellule calculé au temps précédent. Si cette cellule est la première, comme il n’y a
pas de temps précédent, ces deux informations sont égales à 0.

16
Arthur Wuhrmann E-Magination

Comme déjà dit, la cellule renvoie au temps t la sortie o(t) mais aussi les informations
nécessaires pour le calcul de la sortie du temps t + 1 (h(t) et c(t) ).
Quatre carrés sont représentés dans le schéma ; ils correspondent aux  portes  à tra-
vers lesquelles les informations passent. Ces portes permettent de conserver l’information
plus longtemps et donc d’étudier des corrélations entre des données temporellement plus
éloignées. Ces portes sont des neurones avec des fonctions particulières ; elles possèdent
ainsi un poids et un biais, qui se modifie à travers la rétro-propagation afin de réduire
l’erreur du réseau. Il existe quatre types de portes :
– fg :  forget gate , porte d’oubli. C’est une opération sigmoı̈de effectuée sur les
informations en entrée et celles de la couche cachée précédente. Elle s’occupe de
retirer les informations jugées inutiles. Les informations qui sortent de cette porte
sont ensuite multipliées avec la cellule du temps précédent ;
– ig :  input gate , porte d’entrée. C’est une opération sigmoı̈de effectuée sur les
informations en entrée et celles de la couche cachée précédente. Elle s’occupe d’ajou-
ter des nouvelles informations jugées pertinentes. Elle est ensuite multipliée avec la
porte candidate cg avant d’être ajoutée ;
– cg :  candidate gate , porte candidate. C’est la fonction tangente hyperbolique
appliquée sur les informations en entrée et celles de la couche cachée précédente.
Elle s’occupe choisir quelles informations vont être enregistrées dans la cellule avec
la porte d’entrée ;
– og :  out gate , porte de sortie. C’est une opération sigmoı̈de effectuée sur les
informations en entrée et celles de la couche cachée précédente. Les valeurs sortantes
de cette porte sont ensuite multipliées aux valeurs cellule actuelle et alors constituent
la sortie de la cellule ainsi que la valeur cachée qui sera utilisé pour le temps suivant.

2.3.1 Feedforward
Notons les calculs des différentes portes :

z (t) = concaténation(h(t−1) , x(t) )


fg = σ(wf × z (t) + bf )
ig = σ(wi × z (t) + bi )
cg = tanh(wc × z (t) + bc )
og = σ(wo × z (t) + bo )
Notons maintenant le calcul de la cellule au temps suivant (c(t) ) :

c(t) = c(t−1) × fg + ig × cg
Et finalement la sortie (o(t) ), qui est aussi l’état caché du temps prochain (h(t) ) :

o(t) = og × tanh(c(t) )
EXPLIQUER QUE LON FAIT LA TANGENTE HYPERBOLIQUE DE TOUTES
LES VALEURS DE TANH
Il faut faire quatre fois plus de calculs que dans un RNN, c’est pourquoi un LSTM est
bien plus lent qu’un simple RNN. Il est toutefois fabuleusement plus efficace.

17
E-Magination Arthur Wuhrmann

2.3.2 Rétro-propagation
Lors de la rétro-propagation, il faut calculer l’influence que chaque synapse et chaque
biais ont sur le coût total. Pour cela, il faut dériver le coût en fonction de ces paramètres.
Admettons que nous souhaitions modifier le paramètre wf , le synapse de la porte d’oubli.
Notons la dérivée du coût en fonction de ce paramètre :

∂L(y, ŷ) ∂ ∂
= (y − ŷ)2 = (y − og · tanh(c(t) ))2
∂wf ∂wf ∂wf
Décomposons og · tanh(c(t) ) afin de trouver wf :

og · tanh(c(t) ) = og · tanh(c(t−1) · (σ(wf · z (t) + bf )) + ig · cg )


Pour savoir comment modifier le paramètre wf afin d’améliorer le coût total du réseau,
calculons la dérivée de l’expression de droite à l’aide du théorème de dérivation composée :

og · tanh(c(t) ) =
∂wf
c(t−1) · og · z (t) · σ 0 (wf · z (t) + bf ) · tanh0 (c(t−1) · σ(wf · z (t) + bf ) + ig · cg )

18
Chapitre 3

Application

3.1 En bref
Mon travail, en plus de comprendre les réseaux de neurone récurrents (RNN ci-après),
consiste à les utiliser et confirmer pour moi leur efficacité. Je me suis donc lancé dans un
projet ambitieux comme énoncé dans l’introduction : réaliser un programme apprenant à
 composer  des morceaux de musique.

Les RNN peuvent s’utiliser de plusieurs manières. Ils analysent toujours des séquences,
mais de différentes façons. Il existe des RNN capables de décrire une photo, d’autres
permettant de générer des morceaux de musiques, et mêmes qui résument des textes.
Dans ces exemples, les séquences sont respectivement des séquences de pixels, de notes et
de mots.
Dans le cas de la génération de musique, le réseau prend en entrée un séquence de
notes. Cela peut être une ou plusieurs notes, qui sont extraites de morceaux de musique
existants. La nème sortie sera calculée en fonction des entrées 1 à n (cf. 2.5). Le réseau
va ici essayer de  comprendre  quelle sera la (n + 1)ème note. Il va renvoyer un vecteur
indiquant quelles sont les probabilités d’apparition de chaque note de la gamme.
Par exemple, si on prend le début de  Frère Jacques  qui commence par
do ré mi do do ré
et qu’on donne cette séquence au RNN, ce dernier calcule quelle est la note suivante la
plus probable. Il nous renvoie un message que l’on pourrait traduire de la sorte :  La
prochaine note a beaucoup de chances d’être un mi . Si le réseau arrive à prédire quelle
est la note suivante, il est probablement apte à reproduire cette opération. On peut donc
donner au réseau la même séquence qu’avant mais en ajoutant le mi à la fin. Le réseau,
s’il est bien entraı̂né, nous renvoie un do, qui est la note après ledit mi dans  Frère
Jacques .
Lors de la génération de musique, le RNN va essayer de deviner la fin de morceaux
que l’on lui donne. S’il donne des bonnes réponses, alors nous pouvons espérer qu’il puisse
inventer des morceaux à partir de morceaux qu’il n’a pas étudié. C’est comme si je vous
donnais les 5 premières mesures d’un choral de Bach et vous demandais de me dire quelle
sont les notes les plus probables de la 6eme mesure. Si vous donnez une bonne réponse à
chaque fois que je vous pose cette question, c’est qu’il y a des chances que vous arriviez
à me composer des chorals qui ressemblent à ceux de Bach. Cependant, si vous donnez
des mauvaises réponses, je vous explique ce que vous devez améliorer, pour que vous

19
E-Magination Arthur Wuhrmann

ne refassiez pas ces fautes. C’est le même procédé qui est utilisé dans la génération de
morceaux de musique par un programme qui utilise un RNN.

3.2 Outils
Il est presque inimaginable de faire fonctionner un réseau de neurones sans utiliser
d’outils informatiques ; non seulement car cela prendrait beaucoup trop de temps (on
peut parler de milliards de calculs à réaliser) mais aussi car les humains ne sont pas
 parfaits  et font des erreurs (même si cela peut se révéler utile...). Il faut donc utiliser

l’informatique et la programmation. J’ai décidé de travailler avec le langage Python,


développé à l’origine par Guido van Rossum. C’est un langage très populaire que j’ai
choisi pour plusieurs raisons :

– Il est facile à utiliser ;


– comme il est populaire, beaucoup d’outils ont été développés par les utilisateurs ;
– je connaissais déjà bien le langage.

J’ai donc commencé par réaliser un réseau en utilisant le moins d’outils possibles, donc
en faisant le plus de choses moi-même. Je me suis quand même autorisé l’utilisation de
deux bibliothèques majeures : NumPy et Matplotlib. NumPy est un outil mathématique
permettant notamment de manipuler extrêmement rapidement et efficacement les vecteurs
et matrices. Comme mes synapses sont représentées par des matrices, cela m’est très utile.
Matplotlib, quant à lui, offre la possibilité d’afficher des graphes à partir de données. Dans
mon cas, je regarde l’évolution de la fonction de perte en fonction du temps.

3.3 Application concrète


Pour ma première version, je me suis très fortement inspiré d’un tutoriel trouvé sur
GitHub [1]. Je n’ai pas tout de suite cherché à créer des morceaux de musique mais
d’avantage à comprendre comment appliquer la théorie des RNN en informatique. Le
réseau que j’ai programmé s’entraı̂ne à reproduire des séquences de lettres construites de
la manière suivante : une séquence commence par un caractère spécial, BOS ( Beginning
Of Sentence ). Ensuite, elle est composée de n fois la lettre  X  puis de n fois la
lettre  Y  pour n allant de 3 à 9 puis un nouveau caractère spécial EOS ( End Of
Sentence ). À titre d’exemple, si n = 5, alors la séquence est :  BOS X X X X X Y
Y Y Y Y EOS . Le réseau va d’abord déterminer la taille des vecteurs avec lesquels il
va travailler. Nous ne pouvons pas envoyer une lettre ou un caractère au réseau en tant
que tel, il faut la transcrire en langage mathématique, qui est plus facile à manipuler.
Il faut donc créer un vecteur de taille 4, puisqu’il y a 4 éléments différents dans notre
séquence ( BOS, EOS, X, Y). Chaque indice du vecteur est associé à un caractère ; soit
BOS : 1, EOS : 2, X : 3, Y : 4. Lors de la transcription d’une lettre, il suffit de prendre
un vecteur nul (de taille 4 dans notre cas) et d’incrémenter à 1 la composante du vecteur
dont l’indice correspond avec celui de la lettre en question. La séquences BOS X X Y Y

20
Arthur Wuhrmann E-Magination

EOS est ainsi traduite en une liste de vecteurs de la manière suivante :


           
1 0 0 0 0 0
0 0 0 0 0 1
 , , , , , 
0 1 1 0 0 0
0 0 0 1 1 0
Je vais donner en entrée au réseau ladite séquence en enlevant le dernier caractère (ici
EOS) et effectuer un passage  feedforward .
Pour chaque caractère, c’est-à-dire pour chaque temps, le réseau va calculer une valeur
de sortie, comme illustré sur la figure 2.5. Afin de prédire la suite d’une séquence donnée,
il faudrait que la sortie au temps t corresponde à l’entrée au temps t + 1. Par exemple, si
la valeur d’entrée du réseau est la séquence BOS, et comme nous savons que le caractère
BOS est forcément suivi de X, alors le réseau devrait nous renvoyer X pour cette entrée.
La sortie du réseau est elle aussi un vecteur de taille 4 où chaque valeur représente la
probabilité d’apparition (selon le réseau) du caractère de même indice au temps t + 1,
c’est en quelque sorte un vecteur de prédiction, de même que le réseau du sujet 3.1
prédit des notes. Comme les probabilités sont exprimées par convention entre 0 et 1, les
valeurs de chaque indice sont comprises dans cette intervalle. Le réseau calcule donc en
premier lieu un vecteur pour chaque sortie qui, dans le cas où le réseau est parfaitement
entraı̂né (ce qui n’arrive jamais en pratique), doit être égal au vecteur de l’entrée du
temps suivant. Ainsi, un réseau bien entraı̂né, si on lui donne la phrase BOS X Y en
entrée, renvoie EOS. Voici une image représentant un RNN bien entraı̂né qui a effectué
un passage  feedforward  avec la séquence d’entrée BOS X Y :

Couche Couche Couche


d’entrée cachée de sortie
BOS X

X X|Y

Y EOS

Figure 3.1 – RNN

Ce schéma représente un RNN simple mais il est tout à fait possible de le remplacer
un LSTM, il faut dans ce cas concevoir les neurones cachées comme des cellules LSTM et
non des simples neurones.
Vous remarquerez que la deuxième sortie est annotée en rouge et que deux caractères
sont représentés. En effet, si la séquence d’entrée ne comprend pas de Y ni de EOS, ce
qui est le cas des deux premières entrées, alors le réseau ne peut pas savoir combien il
reste de X avant de passer aux Y, puisqu’il ne connaı̂t pas la taille de la séquence (de
même que nous humains ne le saurions pas non plus à partir de la séquence seule). En
pratique, un réseau bien entraı̂né aura tendance à renvoyer X si l’entrée ne comprend pas
encore de caractères Y jusqu’à ce que ce soit le cas.
L’entraı̂nement du réseau consiste à passer une phrase dans le réseau en utilisant la
technique  feedforward , analyser les résultats, modifier ses paramètres et répéter ces
opérations jusqu’à ce qu’on lui demande d’arrêter (cf. 2.1.4).

21
E-Magination Arthur Wuhrmann

Admettons qu’après avoir donné la séquence BOS, le réseau nous ait renvoyé le vecteur
suivant :
 
0, 5
 0 
 
0, 5
0
Ce vecteur indique qu’il y a une probabilité 0,5 d’avoir BOS, 0 d’avoir EOS, 0,5
d’avoir X et 0 d’avoir Y.
Ici, nous voyons que le réseau n’est pas parfaitement entraı̂né. En effet, il aurait dû
avoir toutes les valeurs à 0 sauf l’index X, le troisième, à 1 (puisque selon notre modèle le
caractère BOS est toujours suivi de X). Afin d’obtenir l’erreur de ce vecteur, je calcule la
différence entre le vecteur attendu et le vecteur renvoyé et je fais la moyenne des carrés
des écarts.
 
(0, 5 − 0)2 = 0, 25
 02 = 0  − L = 0, 25 + 0 + 0, 25 + 0 = 0, 125

(1 − 0, 5)2 = 0, 25 →

4
2
0 =0
Cette erreur nous permet de savoir à quel point le réseau est efficace. On parle
également d’urgence à modifier les paramètres. En effet, plus la valeur de coût sera élevée,
plus il y aura  urgence  à modifier les paramètres afin d’améliorer le réseau.
Il serait effectivement très facile de comprendre (et de faire comprendre à une machine)
comment ces séquences sont créées. Cependant, mon but était de résoudre ce problème
en utilisant un réseau de neurones, pour prouver leur efficacité.

3.4 Ensembles de données


Avant de vous montrer mes résultats, il est important d’aborder les ensembles et la
partition de données. En réalité, le réseau ne va pas s’entraı̂ner sur toutes les données que
l’on lui fournit, mais seulement sur une partie (souvent 70-80%). Le reste des données est
réparti en un ou deux ensembles. Le premier, nécessaire, est l’ensemble de validation. Il
permet au à l’utilisateur de savoir si le réseau se dirige dans la bonne direction ou pas.
Il est envisageable que le réseau arrive très bien à répliquer les modèles sur lesquels il
s’est entraı̂né mais ne soit pas capable de comprendre des données nouvelles. Le réseau
ne corrige jamais ses paramètres sur l’ensemble de validation ; cependant, une erreur est
calculée pour chaque époque, pour l’ensemble d’entraı̂nement et l’ensemble de validation.
Une époque est un passage complet de toutes les séquences des ensemble de validation et
d’entraı̂nement dans le réseau.
Le réseau n’utilise que l’erreur d’entraı̂nement pour se corriger, l’erreur de validation
ne sert qu’à l’utilisateur. Si l’erreur de la phase de validation et l’erreur de la phase
d’entraı̂nement sont proches à une même époque, cela signifie que le réseau est apte à
appliquer son modèle à des données inconnues (sur lesquelles il ne s’est pas entraı̂né).
Au contraire, si le coût de validation est nettement supérieur à celui d’entraı̂nement, cela
veut dire que le réseau est surentraı̂né sur les données de l’ensemble d’entraı̂nement et
qu’il n’arrive pas à appliquer son modèles à des informations nouvelles. Ce phénomène

22
Arthur Wuhrmann E-Magination

est appelé  Overfitting  ou  Sur-entraı̂nement . Le dernier ensemble qu’il est possible


de créer est un ensemble  de test . Nous nous en servons lorsque le réseau est entraı̂né
dans le but de vérifier que tout marche bien ou encore pour faire des démonstrations. Il
n’est pas systématiquement utilisé, et est parfois remplacé par celui de validation.

3.5 Résultats
Passons maintenant au résultats. Je pense qu’il est intéressant et important de montrer
comment les réseaux varient en fonction des paramètres que l’on leur fournit (nombre
de données, tailles de la couche cachée, taux d’apprentissage, etc.). Je vais également
montrer la différence d’efficience entre un RNN et un LSTM. Commençons par des valeurs
 moyennes  :

µ = 10−1 , h = 50, n = 50
où µ désigne le taux d’apprentissage, h le nombre de neurones dans la couche cachée et n
le nombre de séquences. Mon ensemble d’entraı̂nement représente 70%, celui de validation
20% et celui de test 10% de toutes les données.
Voici un graphe représentant l’évolution des erreurs calculées sur les ensembles d’en-
traı̂nement et de validation après 1000 époques :

(a) LSTM (b) RNN

Figure 3.2 – Comparaison de réseaux

Sur chaque image, on peut voir l’évolution du coût en fonction de chaque époque.
Le graphe bleu représente le coût de validation et le rouge celui d’entraı̂nement. Après
1000 époques, le réseau me donne presque systématiquement la bonne séquence de sortie
lorsque je lui en donne une en entrée 1 . Lorsque le coût est supérieur à 2, les séquences
renvoyées par le réseau sont clairement erronées. Entre 2 et environ 0.8, le réseau oublie
souvent le caractère EOS lorsqu’il y a eu autant de X que de Y. En dessous de 0.8, la
seule erreur est de mettre un X de trop avant de passer aux Y (car le réseau ne sait pas
qu’il faut mettre un Y). C’est une erreur pardonnable puisque elle ne peut pas être réglée
uniquement par l’apprentissage effectué par le réseau.
Ainsi, lorsque le réseau a exécuté 1000 époques, il arrête de réaliser ces opérations et
nous renvoie cette image (grâce à l’outil  Matplotlib ).
1. Comme expliqué dans la section 3.3, le réseau fait des erreurs car il sait quand il doit passer aux Y.

23
E-Magination Arthur Wuhrmann

Sur les images, on constate que la barre bleue est généralement plus élevée que la rouge,
ce qui est normal comme expliqué dans la section 3.4. En effet, le réseau ne s’est pas corrigé
sur les données de l’ensemble de validation. Dans notre cas précis, il est imaginable que
la phrase avec 4 X et 4 Y ne soit présente que dans l’ensemble de validation, ce qui
expliquerait une moins bonne performance pour cet ensemble (car le réseau n’a jamais
corrigé ses paramètres pour qu’il  réussisse  cette séquence).
Il est intéressant de noter que les courbes évoluent très différemment entre les deux
réseaux. D’une part, le réseau LSTM passe sous le seuil de 1 de coût après environ 180
époques alors que le RNN ne l’a toujours pas passé après plus de 1000 époques. Le LSTM
descend très vite plus se stabilise, alors que le RNN évolue selon une pente plus faible.
Notons également que le LSTM fait une légère bosse entre les époques 50 et 190 avant de
se stabiliser.
On ne les voit pas sur l’image, mais j’ai chronométré précisément les réseaux pour
savoir combien de temps la tâche leur a pris. Il a fallu ∼ 52 secondes au RNN contre ∼
59 pour le LSTM. Cette différence ici est notable sans pour autant être drastique, alors
que les calculs des cellules des LSTM sont nettement plus compliqué :je m’attendais à
une plus grande différence. Sans en être certain, je suppose que cela est causé par le fait
qu’il y ait peu de neurones dans la couche cachée et que les vocabulaires soient petits. Le
réseau, quel que soit son type, ne met pas beaucoup de temps à réaliser les calculs.
Voici ce qu’il advient lorsque l’on fait tourner un réseau  trop  longtemps (10’000
époques, ∼ 22 minutes) :
On voit que le début (époques 0-1000) est assez similaire à celui de la figure 3.2 (a)
mais après l’époque 1000 le réseau perd complètement sa stabilité et des grandes piques
surgissent, faisant monter le coût de 2 jusqu’à 9. On pourrait croire que la stabilité revient
un peu avant l’époque 6000 mais cela ne se confirme pas avec les piques observables vers la
fin. Nous pouvons donc en conclure que le réseau ne peut à priori plus s’améliorer lorsqu’il
est sur-entraı̂né.
Dans notre cas, il est inutile de trop augmenter le nombre de séquences par ensemble.
Nous connaissons l’ensemble des données possibles (comme n est la seule variable faisant
changer une séquence et qu’il varie entre 3 et 9, il y a 7 possibilités). Trop l’augmenter
ne ferait que ralentir les époques. De plus, le réseau pourrait subir l’effet inverse du sur-
entraı̂nement (ou sur-apprentissage), le sous-apprentissage.
Voici ce que donnent les réseaux lorsque l’on double le nombre de neurones de la couche
cachée par rapport aux paramètres de la figure 3.2 (soit quand h = 100) :
Ensuite, j’ai décidé de recréer entièrement le programme et de l’adapter pour que je
puisse le  nourrir  de morceaux de musiques afin de les lui faire apprendre. J’ai modu-
larisé le code que j’avais écrit et me le suis approprié. La modularisation consiste, dans le
principe, à ranger les instructions et les valeurs dans des compartiments différents afin de
les utiliser plus facilement. C’est une étape nécessaire dans la réalisation d’un programme
efficace, pratique d’utilisation et compréhensible (du moment que le langage utilisé sup-
porte les objets). Voici à quoi ressemble la création d’un réseau et son entraı̂nement avec
le code que j’ai créé :

1 hidden_size = 50 #Nombre de neurones dans la couche cachée


2 reseau = LSTM(name="Mon Réseau", size=hidden_size)
3 reseau.train()

24
Arthur Wuhrmann E-Magination

Figure 3.3 – LSTM sur 10’000 époques

Il est possible de spécifier plusieurs paramètres à travers l’instruction  train() , tel


que le nombre d’époques à réaliser, le nombre de données, le taux d’apprentissage, etc.

25
E-Magination Arthur Wuhrmann

26
Chapitre 4

Musique

4.1 Introduction
Il existe de nombreuses manière de créer un algorithme générant des morceaux de
musique ou des mélodies. Il est même possible de ne pas utiliser de réseaux de neurones,
cependant je souhaitais réellement y recourir.
Et même en adoptant des réseaux de neurones, il existe plusieurs méthodes. On peut
considérer un morceau comme une séquence de notes (ce que je vais faire), mais aussi
comme un sonogramme.
J’ai choisi d’analyser des séquences de notes parce que c’est la méthode la plus efficace
et la plus développée dans le domaine, mes résultats seront donc plus concluants et mes
travaux moins laborieux.

4.2 Traitement des données


Malheureusement, mon réseau ne peut analyser que des séquences de notes. Or, la plu-
part des musiques que l’on trouve sur internet ne sont pas composées de notes directement.
Il est par exemple difficile de représenter des paroles sur une partition.
Toutefois, il existe un format de fichier dédié à la musique qui est composé en partie
de notes : le format MIDI ( Musical Instrument Digital Interface ).
Il est donc possible de lire ce format, de l’encoder en séquences comme dans la section
précédente et de le donner en entrée au réseau. Plutôt que directement passer du fichier
midi à des vecteurs comme dans la section 3.3, il est coutume de transformer le fichier en
texte et enfin de transformer le texte en vecteurs.
J’ai décidé de remplacer chaque note par un caractère (lettres de l’alphabet, chiffres,
caractères spéciaux) sans considérer la durée de la note. Ainsi, pour le réseau, toutes les
notes ont la même durée et la musique composée par le réseau ne sera construite qu’à
partir de note de même durée. En effet, la durée d’une note est un paramètre variable et
difficile à encoder de manière efficace. Prenons la partition suivante :

G ˇˇ ˇ ˇ
ˇ ˇ
En partant du principe que les do, mi et sol de l’octave médiane sont traduits respecti-
vement  c ,  g  et  e , alors la séquence suivante serait traduite ainsi :  cge$c$g$e .

27
E-Magination Arthur Wuhrmann

Comme il n’y a pas d’espacement temporel entre les notes du premier accord, les trois
caractères sont collés. Cependant, je note ensuite qu’il y a une durée entre l’accord et le
do, puis entre le do et le mi et entre le mi et le sol à l’aide du symbole dollar. Chacun de
ces caractères est ensuite encodé exactement de la même manière que dans l’exemple de la
section 3.3, sauf que cette fois le nombres de caractères différents est largement supérieur.
Il peut y avoir jusqu’à 89 caractères différents, à l’instar des 88 touches du clavier d’un
piano et du caractère dollar : la taille du vecteur d’entrée sera donc 89.
La gestion du temps est très complexe. Il existe de nombreuses durées de notes, et
toutes les ajouter ne ferait qu’agrandir la taille du vocabulaire qui est déjà conséquente.
En application, traduire tous les paramètres d’un morceau est un réel défi, même pour les
experts. J’ai donc décidé de ne tenir compte que des intervalles de temps supérieurs ou
égaux à une croche.

4.3 Architectures utilisées


Le LSTM que j’ai codé à la main, sans utiliser de bibliothèques autres que celles men-
tionnées dans la section 3.2, n’est pas très efficient. En effet, beaucoup d’améliorations plus
ou moins complexes peuvent être ajoutées à ce réseau. Cependant, ce genre d’amélioration
rend la compréhension du code plus fastidieuse. C’est pourquoi j’ai décidé d’utiliser, en
plus du LSTM que j’ai codé, l’outil  Keras . Keras est une bibliothèque permettant
la création de réseaux de neurones facilement. En outre, les calculs sont optimisés et
accélèrent considérablement la phase d’entraı̂nement du réseau.
Google a aussi développé un outil de  Machine Learning ,  TensorFlow , lui-même
basé sur Keras.
Voici à quoi ressemble la création d’un LSTM avec Keras :

1 from keras import *


2

3 #Création d'un réseau "vide".


4 reseau = Sequential()
5

6 #On ajoute une couche de LSTM en précisant le nombre de neurones de la


,→ couche, et le nombre de neurones d'entrées (return_sequences permet
,→ d'empiler plusieurs couches de LSTM).
7 reseau.add(LSTM(
8 300,
9 input_shape=(taille_voc, ),
10 return_sequences=False))
11

12 #Le concept de Dropout est expliqué ci-dessous


13 reseau.add(Dropout(.5))
14

15 #On ajoute la fonction relu dite d'activation


16 reseau.add(Activation('relu'))
17

18 #On normalise les données pour éviter qu'elles soient trop extr^
emes
19 reseau.add(BatchNorm())

28
Arthur Wuhrmann E-Magination

20

21 #On applique la fonction softmax à la sortie qui permet d'obtenir un


,→ vecteur de probabilités
22 reseau.add(Activation('softmax'))
23

24 #On compile le réseau en précisant que l'on utilise la fonction de co^ ut


,→ d'entropie croisée. Nous aurions aussi pu choisir la somme des
,→ carrés des écarts comme dans les exemples de ce travail. Le champ
,→ optimizer permet de sélectionner quelle méthode va ^etre utilisée
,→ pour faire baisser le co^ ut. Celle que je présente dans le travail
,→ est appelée "Stochastic Gradient Descent", soit "sgd".
25 reseau.compile(loss='categorical_crossentropy', optimizer='sgd')

A titre d’exemple, une des fonctionnalités ajoutées par Keras est appelée  Dropout .
Cela va volontairement ajouter des erreurs au réseau afin qu’il s’améliore. Il va changer
la valeur de plusieurs biais et synapses alétoirement choisis au sein du réseau. Cela peut
paraı̂tre très contre-intuitif, mais plusieurs raisons justifient ce résultat[2].
La fonction ReLU est une alternative à la fonction sigmoı̈de qui est en pratique plus
efficiente. Elle se présente ainsi :

x<0→ − 0
ReLU (x) =
x>0→ − x
ce qui donne sur un graphe :

4
ReLU (x)

0
−6 −4 −2 0 2 4 6
x

Figure 4.1 – Fonction ReLU

4.4 Choix des morceaux


Il a fallu trouver un genre musical et une base de données compatibles avec mon
travail. Heureusement, je ne suis pas le premier à me lancer dans un tel projet, j’ai donc pu
m’inspirer de nombreux travaux. Il est très difficile de choisir des morceaux avec beaucoup

29
E-Magination Arthur Wuhrmann

d’instruments, car le réseau préfère quand les données ont un modèle similaire, et trouver
plusieurs dizaines de morceaux du même genre avec les mêmes instruments s’avère très
compliqué. De plus, il me fallait des fichiers avec l’extension MIDI comme expliqué dans
la section 4.2. J’ai fait de nombreux essais mais l’ensemble de données qui s’est révélé
être le plus performant est l’ensemble des chorals de J.-S. Bach. Ils sont tous composés à
4 voix, sont à peu près de la même longueur et se ressemblent assez pour que le réseau
arrive à repérer des similitudes. METTRE SITE TELECHARGEMENT ATTENTION
TEMPS TOUT AU PASSE

4.5 Résultats
Il a fallu plusieurs essais pour arriver enfin à de bons résultats, car trouver la bonne
architecture de réseau est compliqué. Le calcul d’un essai peut prendre plusieurs heures,
une configuration doit être choisie méticuleusement. J’ai donc utilisé Keras pour obtenir
des résultats concluant. Voici l’architecture qui s’est avérée la plus efficace :
– Une couche LSTM de 512 neurones ;
– Fonction  Dropout  ;
– La fonction ReLU ;
– La normalisation des données pour éviter qu’elles ne deviennent trop grandes ;
– Une autre fonction  Dropout  ;
– Une couche d’un réseau de neurones simples ;
– La fonction softmax.
La technique utilisée pour faire baisser le coût n’est pas la descente du gradient,
présentée dans ce travail, mais une autre appelée  RMSprop . Le taux d’apprentis-
sage,  learning rate  est de 0.001. L’algorithme de  RMSprop  ressemble cependant
beaucoup à celui de la descente du gradient.
J’ai donc lancé mon programme avec la configuration ci-dessus et ai laissé la machine
tourner pendant 220 époques, soit un peu plus de deux heures. Ce temps est dans l’absolu
pas très long, mais en fonction de l’architecture utilisée, le réseau peut devenir nettement
plus lent. Dans le cas ci-dessus, ma machine a mis un peu moins de deux minutes par
époque ; j’ai en revanche fait des essais dans lesquels une époque prenait une dizaine de
minutes.
Après ces 220 époques entraı̂nées et corrigées sur les 38 chorals, le réseau avait atteint
un coût de 0,17. J’ai laissé tourner le programme une quinzaine d’époques supplémentaires,
mais le coût n’arrivait plus à baisser ; il avait au contraire une tendance à augmenter. Je
ne pourrai pas montrer une évolution du coût en fonction du temps, car l’utilisation de
 Keras  m’en a empêché.

Parlons maintenant de la musique qui ressortait du programme après cet entraı̂nement


acharné 1 . Ce que l’on remarque après les premières secondes d’écoute, c’est que ce n’est
pas Bach en personne qui a composé ces musiques. En effet, beaucoup de fausses notes
subsistent, notamment dans les accords. Toutefois, ce que l’on peut dire, c’est que ce
morceau, bien qu’il soit très brut, présente immédiatement un style baroque, classique. À
titre personnel, c’est donc une réussite.
1. Un extrait est disponible à l’adresse suivante : https ://soundcloud.com/user-582690328/bach-tm

30
Chapitre 5

Conclusion

Avant de réellement conclure ce travail, je souhaitais apporter une brève dimension


philosophique et morale à mon travail. En effet, les intelligences artificielles soulèvent
nombreuses questions auxquelles il est souvent difficile de répondre. Une machine peut-
elle être créative ? Une machine peut-elle apprendre ?
Le fonctionnement du  machine learning  prouve certaines particularités de l’ap-
prentissage, comme celle qui affirme que l’on n’apprend que de ses erreurs. En effet,
l’algorithme des IAs d’apprentissage automatique ne font que se corriger sur les erreurs
qu’elles produisent, et c’est ce qui leur permet de s’améliorer. Une IA qui ne se trompe
pas ne progresse pas.
Comme dit à la fin de la section 4.5, j’estime que j’ai réussi l’objectif que je m’étais
lancé, à savoir de faire composer de la musique à une machine. Cependant, en commençant
mon travail, j’étais loin de m’imaginer toutes les contraintes et difficultés par lesquelles
j’allais devoir passer. Tout d’abord, le sujet étant récent, il existe peu de sources et la
quasi-intégralité d’entre elles est en anglais, ce qui ne facilite pas la compréhension. De
plus, le niveau mathématique est largement plus élevé que le mien lorsque j’ai commencé
ce projet, j’ai donc du me familiariser avec de nombreux thèmes comme l’algèbre linéaire
ou la dérivation. En outre, même si je savais déjà coder en Python, il m’a fallu apprendre
l’utilisation de plusieurs bibliothèques telles que NumPy, Keras, music21 et bien d’autres.
Toutefois, tous ces obstacles auxquels je me suis confronté n’ont fait qu’augmenter les
connaissances que j’ai acquises et ont m’ont rendu d’autant plus fier d’avoir réussi.

31
E-Magination Arthur Wuhrmann

32
Bibliographie

[1] N. Hansen, Christensen P., and Johansen A. How to build RNNs and LSTMs from
scratch. 2019.
[2] Nguyên Hoang L. Gloire aux erreurs (dropout) | Intelligence artificielle 45. 2018.

33

Vous aimerez peut-être aussi