E-Magination
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.
7
E-Magination Arthur Wuhrmann
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
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.
9
E-Magination Arthur Wuhrmann
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
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 :
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.1 Architecture
Voici ce à quoi ressemble un réseau de neurones récurrents :
13
E-Magination Arthur Wuhrmann
...
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 :
14
Arthur Wuhrmann E-Magination
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) :
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
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
cht−1i × + chti
tanh
× ×
hht−1i hhti
Entrée t xhti
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 :
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 :
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
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.
20
Arthur Wuhrmann E-Magination
X X|Y
Y EOS
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é.
22
Arthur Wuhrmann E-Magination
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 :
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éé :
24
Arthur Wuhrmann E-Magination
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.
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.
18 #On normalise les données pour éviter qu'elles soient trop extr^
emes
19 reseau.add(BatchNorm())
28
Arthur Wuhrmann E-Magination
20
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
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é.
30
Chapitre 5
Conclusion
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