E-Magination
1 Introduction 5
2 Théorie 7
2.1 Le réseau de neurones simple . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.1.1 Analogie biologique . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.1.2 Architecture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.1.3 Feedforward . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.1.4 Étapes de l’apprentissage . . . . . . . . . . . . . . . . . . . . . . . . 10
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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.2.2 Feedforward . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.2.3 Rétro-propagation . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.3 LSTM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.3.1 Feedforward . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3.2 Rétro-propagation . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.4 Application . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.4.1 Outils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.4.2 Exemples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.5 Ensembles de données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
3 Résultats 25
4 Musique 29
4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.2 Traitement des données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4.3 Architectures utilisées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
4.4 Choix des morceaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
4.5 Résultats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
5 Conclusion 33
5.1 Remerciements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
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
est très utilisé 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 (ci-après Intelligence
Artificielle) appelée réseau de neurones récurrents . C’est une manière d’aborder l’ap-
prentissage automatique.
2.1.2 Architecture
Les réseaux utilisés en IA possèdent également des neurones, reliés 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 constante, 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 à 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 en grande partie manipulées à l’aide de
simples opérations mathématiques (additions, multiplications).
7
E-Magination Arthur Wuhrmann
Dans un réseau classique chaque neurone de chaque couche est relié avec tous les
neurones 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 une synapse. Comme je l’ai dit plus tôt, une
synapse amplifie ou réduit un signal. Dans notre réseau artificiel, une synapse n’est elle
aussi rien de plus qu’un nombre 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. Les signaux d’entrées parcourent ainsi les couches du réseau et sont modifiés
par les synapses reliant ces mêmes couches.
En plus de synapses, les réseaux comportent finalement ce qu’on appelle des biais.
Les biais sont eux aussi des simples nombres. Chaque neurone en dehors de ceux de
la première couche possèdent un biais. Ce dernier va également impacter, comme une
synapse, la valeur du neurone, mais pas de la même manière. La figure ci-dessous 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 en suivant 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. Les fonctions qui servent à cela 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
−5 0 5
x
.
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 la synapse reliant le jème neurone d’une couche au ième de la couche
suivante
(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
(i) (i−1) (i)
xj = σ( (xk · Wi×k ) + bj )
k=0
Cette formule est d’abord utilisée pour calculer les neurones de la deuxième couche,
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 au 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
2.1.6 Rétro-propagation
Comme nous l’avons vu dans le paragraphe 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. Elle est souvent nommée en
10
Arthur Wuhrmann E-Magination
anglais Back propagation . 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. 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 :
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.36 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 au total 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 la 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ù :
11
E-Magination Arthur Wuhrmann
∂L(1, ŷ)
= 2(1 − σ(w · x + b)) · σ 0 (w · x + b) · x
∂w
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
le coût. Ainsi, la dérivée de l’erreur en fonction dudit neurone des sorties 1 et 2 permet
de savoir comment il faut changer le biais et le synapse pour faire baisser le coût de
l’ensemble du réseau. Nous allons donc regarder comment chaque paramètre du réseau
(synapses et biais) fait varier l’erreur. En les modifiant en conséquences, le coût baissera
très probablement.
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 :
(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.
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. Ce sont les synapses et les biais que l’on modifie. On les multiplie par un
coefficient appelé taux d’apprentissage, learning rate , noté µ souvent égal à 0.01. Un
taux d’apprentissage 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.
13
E-Magination Arthur Wuhrmann
2.2.1 Architecture
Voici ce à quoi ressemble un réseau de neurones récurrents :
...
14
Arthur Wuhrmann E-Magination
fonction de t. Ainsi, tous les synapses illustrés en vert ont le même poids. Cette particula-
rité s’observe facilement lorsque l’on représente le RNN de cette manière comme illustré
par la figure 2.6 :
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×2 = σ(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 la 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 2.5).
Il n’y a pas de différence 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
15
E-Magination Arthur Wuhrmann
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 deux é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 nommés LSTM pour 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 dont les calculs sont bien plus facile à effectuer lors de la rétro-propagation.
Afin de calculer l’influence qu’a le réseau sur la sortie au temps tz , il faut calculer
l’influence qu’on tous les neurones des couches cachées de 0 à tz 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
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. Une cellule se présente comme
illustré sur la figure 2.7 :
16
Arthur Wuhrmann E-Magination
Sortie t ohti
cht−1i × + chti
tanh
× ×
hht−1i hhti
Entrée t xhti
– 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 les valeurs de 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 in-
formations en entrée et celles de la couche cachée précédente. Elle s’occupe d’ajouter
des nouvelles informations jugées pertinentes. Ses valeurs sont ensuite multipliées
avec celles de la porte candidate cg avant d’être ajoutées aux composantes de la
cellule du temps précédent ;
17
E-Magination Arthur Wuhrmann
2.3.1 Feedforward
De même qu’un RNN basique, le LSTM suit aussi les étapes du paragraphe 2.1.4. Il
réalise donc une opération 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) )
Comme expliqué plus au point 2.2.1, les poids et les synapses ci-dessus sont en réalité
des vecteurs de nombres variant entre 0 et 1. Ainsi, les opération σ et tanh sont appliqués
composante par composante.
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.
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
Arthur Wuhrmann E-Magination
2.4 Application
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ême 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 6ème 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.
2.4.1 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
2.4.2 Exemples
Pour ma première version, je me suis très fortement inspiré d’un tutoriel trouvé sur
GitHub [2]. 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 transcrire en langage mathématique, qui est plus facile à manipuler. Nous
devons 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 le
vecteur nul (de taille 4 dans notre cas) et de mettre à 1 la composante du vecteur dont
l’indice correspond à celui de la lettre en question. La séquences BOS X X Y Y 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
20
Arthur Wuhrmann E-Magination
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 2.4 prédit
des notes. Comme les probabilités sont exprimées 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é, en lui donnant 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 :
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és 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).
Admettons qu’après avoir donné la séquence BOS, le réseau nous ait renvoyé le vecteur
suivant :
21
E-Magination Arthur Wuhrmann
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
02 = 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 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
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.
23
E-Magination Arthur Wuhrmann
24
Chapitre 3
Résultats
µ = 10−1 , h = 50, n = 50
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
1. Comme expliqué dans la section 2.4.2, le réseau fait des erreurs car il sait quand il doit passer aux
Y.
25
E-Magination Arthur Wuhrmann
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 les étapes du
paragraphe 2.1.4 et nous renvoie cette image (grâce à l’outil Matplotlib ).
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 2.5. En effet, le réseau ne s’est
pas corrigé sur les données de l’ensemble de validation. Dans notre cas, 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 a peu de neurones dans la couche cachée et que les vocabulaires sont 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.1 (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 des 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 qu’augmenter le temps de calcul. 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.1 (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-
26
Arthur Wuhrmann E-Magination
porte les objets). Voici à quoi ressemble la création d’un réseau et son entraı̂nement avec
le code que j’ai créé :
27
E-Magination Arthur Wuhrmann
28
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 .
29
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 2.4.2, 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.
18 #On normalise les données pour éviter qu'elles soient trop extr^
emes
19 reseau.add(BatchNorm())
20
30
Arthur Wuhrmann E-Magination
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 (voir cf. [3]).
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
31
E-Magination Arthur Wuhrmann
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, qui 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.
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é.
32
Chapitre 5
Conclusion
33
E-Magination Arthur Wuhrmann
5.1 Remerciements
Je tiens à témoigner mon remerciement à mes amis qui m’ont encouragé dans ce projet
ambitieux et qui ont toujours été curieux et intéressés. Un remerciement particulier à
l’égard de Paul Frund et Hugo Lundberg qui ont pris le temps de lire mon travail et de
me donner un retour. Merci aussi à M. Thivent Besson qui, en me supervisant sur ce
travail, m’a beaucoup encouragé, corrigé et aidé.
34
Bibliographie
35