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 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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 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 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
5.1 Remerciements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
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 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 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 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 un 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 le 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 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. Elle est souvent nommée en
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
10
Arthur Wuhrmann E-Magination
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 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 ;
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
∂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. 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.
13
E-Magination Arthur Wuhrmann
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. Ainsi, s’il est parfaitement entraı̂né et arrive à
chaque fois à prédire la fin d’une phrase, il est raisonnable d’affirmer qu’il peut générer
des phrases sensées. 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 :
...
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. Il faut donc d’abord calculer la valeur de la première entrée,
puisque celui de la deuxième en dépend.
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 à ... .
Cela signifie qu’il peut y avoir n entrées, où n est la longueur de la séquence étudiée.
Chaque entrée de t = 1 à t = n prend un élément de la séquence en entrée, dans l’ordre.
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 particu-
larité 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) :
(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 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 2.5).
dans
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
15
E-Magination Arthur Wuhrmann
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 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 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 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.
16
Arthur Wuhrmann E-Magination
Voici un schéma :
Sortie t ohti
cht−1i × + chti
tanh
× ×
hht−1i hhti
Entrée t xhti
17
E-Magination Arthur Wuhrmann
2.3.1 Feedforward
De même qu’un RNN basique, le LSTM suit aussi les étapes de 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
sur chaque élément du vecteur.
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
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 )
Ensuite, il faut faire les mêmes opérations que dans un réseau de neurones simple,
c’est-à-dire modifier les synapses et poids en fonction des valeurs des gradients.
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ê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
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.
19
E-Magination Arthur Wuhrmann
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 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
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 .
20
Arthur Wuhrmann E-Magination
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 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 :
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).
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
21
E-Magination Arthur Wuhrmann
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) = 0, 25 →
2
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 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
Chapitre 3
Résultats
µ = 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
1. Comme expliqué dans la section 2.4.2, le réseau fait des erreurs car il sait quand il doit passer aux
Y.
23
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 ces opérations 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 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.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 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.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-
24
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éé :
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 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. 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[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
29
E-Magination Arthur Wuhrmann
beaucoup 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érait 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.
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
l’époque, Garry Kasparov. C’était une intelligence artificielle, mais elle ne faisait que cal-
culer un nombre de coup gigantesque et choisir le meilleur . Ce type d’intelligence peut
difficilement être considéré comme créatif, puisqu’il ne fait que suivre des ordres qu’on lui
a donné, à la lettre. Or, depuis, plusieurs nouveaux programmes ont fait leur apparition :
AlphaZero, un programme de chez Google, fonctionne de manière très particulière. Il ne
calcule pas tous les coups possible et on ne lui donne pas de données exemplaires sur
lesquelles il pourrait se baser (comme des parties jouées par des maı̂tres), mais on le fait
jouer contre lui-même des millions de parties en le récompensant quand il gagne, de la
même manière que mon programme que je pénalise quand il ne trouve pas la bonne note.
AlphaZero a largement surpassé les meilleurs programmes lors de sa sortie (qui eux-même
battaient les meilleurs joueurs humains). Et en analysant les parties, on remarque qu’elle
a un style de jeu qui ressemble beaucoup à celui d’un humain[1]. Le programme a des
coups qui toutefois ne seraient pas joués par un humain, jugés trop dangereux, mais qui
s’avèrent décisifs pour la victoire d’AlphaZero. On peut donc en conclure qu’il y a pro-
bablement une part de créativité dans cette intelligence artificielle. Mais, est-ce le cas de
mon programme ? C’est difficile de répondre, car mon programme ne fait qu’essayer de
copier un des plus grands maı̂tre de la composition musicale, J.S. Bach. Contrairement à
AlphaZero mais à l’instar d’autres IA de jeux telles que AlphaGo 1 , mon programme ne
fait que se baser sur ce que l’humain fait de mieux, et n’essaie pas vraiment par lui-même.
1. AlphaGo est le premier programme qui a battu un champion au jeu de Go. Comme mon programme,
il s’inspirait de parties jouées par les meilleurs joueurs de Go et essayait de les copier.
31
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é.
32
Bibliographie
33