Vous êtes sur la page 1sur 33

Gymnase de Burier

E-Magination

Burier, le 8 septembre 2020 Arthur Wuhrmann 3M5


Table des matières

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

do
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
UNE SYNAPSE
UN NEURONE
Chapitre 2

Théorie
ci après

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

2.1 Le réseau de neurones simple


2.1.1 Analogie biologique

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

00
2.1.2 Architecture basique

o
Les réseaux de neurones utilisés en IA possèdent également des neurones, reliées par
des synapses. Chaque neurone n’est rien de plus qu’un nombre entre 0 et 1, qui définit à

o
quel point le neurone est activé. La valeur d’un neurone n’est pas définie à l’origine, mais
sera calculée grâce à celles des synapses.
Le réseau est divisé en plusieurs couches. La première est la couche dite d’entrée, et
c’est à cette couche là que nous allons donner nos informations. Une information peut
être une lettre d’un texte, une note de musique, ou encore un pixel d’une image. Ces
informations représentent les données dont le réseau a besoin pour résoudre la tâche que
nous lui avons ⌧ confiée . Si vous souhaitez faire un réseau qui apprend à jouer aux
échecs, il faudra lui donner la position de toutes les pièces, ainsi d’éventuellement les
derniers coups joués. Ces informations vont parcourir le réseau et parvenir à 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

O
qui suit). Cette liaison représente justement un synapse. Comme je l’ai dit plus tôt, un

o
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 8
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.

Couche Couche Couche


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

Figure 2.1 – Réseau de neurones simple

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

Figure 2.2 – Fonction sigmoı̈de

.
Si x ! +1, e x sera proche de 0 donc (x) ⇡ 1. En revanche,
si x ! 1, 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ù :

suivante
(L)
O
– Wi⇥j désigne le synapse reliant le jème neurone d’une couche au ième de la couche

– 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

ft l
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-
O
pliquée à premier abord, n’y figurent, à part pour la fonction sigmoı̈de, que des additions
et des multiplications.
Le fait de ⌧ calculer un réseau comme cela se nomme ⌧ feedforward , car le passage
de l’information ne se fait que dans un sens.

2.1.4 Étapes de l’apprentissage


L’apprentissage automatique d’un réseau de neurone simple peut se résumer de la
manière suivante :

d sait
9
E-Magination Arthur Wuhrmann

a) initialiser les poids des synapses, généralement aléatoirement ;


b) faire un passage ⌧ feedforward ;
c) calculer à quel point les résultats du réseau du neurone sont faux ;
d) corriger le réseau en fonction de l’erreur obtenue ;
e) répéter les étapes 2 à 5 ;

2.1.5 Fonction de perte mfformulation


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

Cette fonction renvoie un nombre entre 0 et 1. Plus yˆi est di↵érent de yi , plus la valeur
(yi ŷi )2 sera proche de 1. De façon formelle, L(y, ŷ) = 1 () (yi ŷi )2 = 1 8 yi , ŷi 2 y, ŷ
donc () yi ŷi = ±1. A l’opposé, si yi = ŷi 8 yi , ŷi 2 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.

µ rade
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.

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

Couche Couche Couche


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

Figure 2.3 – Caption

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

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

Couche Couche Couche


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

Figure 2.4 – Rétro-propagation dans un réseau complexe

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


2. C’est pourquoi les deux synapses et le neurone illustrés en violet ont une influence sur
le coût. Ainsi, la dérivée de l’erreur en fonction dudit neurone des sorties 1 et 2 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.

12
Arthur Wuhrmann E-Magination

Nous aimerions calculer

@L
(2)
@x1 d
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

0
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 chi↵re écrit à la main provenant d’une image bien
qu’il n’ait encore jamais vu ladite image.

2.2 Réseaux de neurones récurrents


Les réseaux de neurones récurrents (RNNs) sont des réseaux de neurones similaires
aux réseaux de neurones simples mais conçus pour étudier des séquences et les corrélations
entre les di↵érents éléments de la séquences.
Imaginons un texte en français composé de nombreuses phrases. Une phrase étant une
séquence de mots, un RNN pourrait parfaitement les analyser afin de créer un modèle

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.
Oui
Le réseau simple ne pourrait pas étudier les corrélations entre les di↵érents mots de notre
phrase, tandis que notre RNN pourrait aisément prédire un ⌧ La ou ⌧ Le après une

mtn
fin de phrase.

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

Couche Couche Couche


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

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

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

...

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


abdeur
Plusieurs éléments di↵è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 :

comme illustrépar 14 D part


lafigure 2.6
Arthur Wuhrmann E-Magination

Couche Couche Couche


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

Figure 2.6 – RNN ⌧ enroulé le


Dans ce schéma, la boucle explicite la récurrence du réseau. Le neurone de la couche
cachée est représenté par un seul rond, mais un neurone peut être exprimé par un vecteur

n
de nombres, comme on le verra dans 2.4.2. Dans ce cas-là, ce ne serait en réalité plus
un nombre variant de 0 à 1 mais un vecteur de taille n composé de nombres variant de
0 à 1. Ainsi, lorsque je dirai qu’une fonction est appliquée sur un quelconque neurone,
elle s’appliquera en pratique à toutes les valeurs du vecteur dudit neurone. La taille de
ce vecteur est une valeur à définir avant de démarrer le réseau. En théorie, plus elle est
grande, plus le réseau sera doué. Cependant, cela n’est pas vérifié en pratique. En outre,
plus la taille du vecteur est grande, plus il faudra de temps au réseau pour faire ces calculs.

2.2.2 Feedforward
Comme tous (ou presque tous) les réseaux de neurones, le RNN e↵ectue 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
di↵è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 )

0M
où :
(t=k)
– xi⇥j désigne le j ème neurone de la ième couche au temps t=k.

o
– 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).

0
dans
Il n’y a pas de di↵é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 t 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 di↵é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

Kff97
E-Magination dent Arthur Wuhrmann

te
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.

surfaites à
2.3 LSTM
daleïdalermit
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

O.O
Voici un schéma :

Sortie t ohti

Cellule t Cellule t+1

cht 1i ⇥ + chti

tanh

⇥ ⇥

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

µwf hht 1i

Entrée t xhti
hhti

Figure 2.7 – Cellule LSTM

Le schéma a été trouvé sur StackExchange 1 .


Le symbole ⇥ représente une multiplication matricielle et le symbole + une addition
de deux vecteurs. Lorsque deux lignes, qui symbolisent des vecteurs, se rejoignent, lesdits
vecteurs sont concaténés. La case tanh signifie que la fonction tangente hyperbolique est
appliquée au vecteur passant par la case en question.
Le rectangle brun aux bords arrondi de la figure 2.7 représente la cellule LSTM. On
constate qu’elle prend au temps t l’entrée x(t) et deux autres données : la couche cachée
et la cellule calculé au temps précédent. Si cette cellule est la première, comme il n’y a
pas de temps précédent, ces deux informations sont égales à 0.
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 e↵ectué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 e↵ectué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
To
porte candidate cg avant d’être ajoutée ;
1. https ://tex.stackexchange.com/questions/432312/how-do-i-draw-an-lstm-cell-in-tikz, consulté le
05.09.20

17

formulation
E-Magination Arthur Wuhrmann

– 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 e↵ectuée sur les
informations en entrée et celles de la couche cachée précédente. Les valeurs sortantes

0
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


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.

meuf
Notons les calculs des di↵érentes portes :

z (t) = concaténation(h(t 1)
, x(t) )
fg = (wf ⇥ z (t) + bf )
ig = (wi ⇥ z (t) + bi )
cg = tanh(wc ⇥ z (t) + bc )
og = (wo ⇥ z (t) + bo )
Notons maintenant le calcul de la cellule au temps suivant (c(t) ) :

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

o(t) = og ⇥ tanh(c(t) )
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 composante composante
calculs que dans un RNN, c’est par
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 :

Kmh
@L(y, ŷ) @ @
@wf
=
@wf
(y ŷ)2 =
@wf
(y
0
og · tanh(c(t) ))2

0
(t)
Décomposons og · tanh(c ) afin de trouver wf :

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

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) ) =

8
@wf
0
c(t 1)
· og · z (t) · (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 di↵érentes façons. Il existe des RNN capables de décrire une photo, d’autres

0
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
0
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
l’informatique et la programmation. J’ai décidé de travailler avec le langage Python,
développé à l’origine par Guido van Rossum. C’est un langage très populaire que j’ai
choisi pour plusieurs raisons :
– Il est facile à utiliser ;
– comme il est populaire, beaucoup d’outils ont été développés par les utilisateurs ;
– je connaissais déjà bien le langage.
J’ai donc commencé par réaliser un réseau en utilisant le moins d’outils possibles, donc
en faisant le plus de choses moi-même. Je me suis quand même autorisé l’utilisation de
deux bibliothèques majeures : NumPy et Matplotlib. NumPy est un outil mathématique
permettant notamment de manipuler extrêmement rapidement et efficacement les vecteurs
et matrices. Comme mes synapses sont représentées par des matrices, cela m’est très utile.
Matplotlib, quant à lui, o↵re 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.

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
y
µ
Sentence ). À titre d’exemple, si n = 5, alors la séquence est : ⌧ BOS X X X X X Y

tt
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

00
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 di↵é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
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 :
0 1 0 1 0 1 0 1 0 1 0 1
1 0 0 0 0 0
B0C B0C B0C B0C B0C B1C
B C,B C,B C,B C,B C,B C
@0A @1A @1A @0A @0A @0A
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 e↵ectuer 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 e↵ectué
un passage ⌧ feedforward avec la séquence d’entrée BOS X Y :

Couche Couche Couche


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

X X|Y

Y EOS

Figure 2.8 – RNN

Ce schéma représente un RNN simple mais il est tout à fait possible de le remplacer
un LSTM, il faut dans ce cas concevoir les neurones cachées comme des cellules LSTM et
non des simples neurones.
Vous remarquerez que la deuxième sortie est annotée en rouge et que deux caractères
sont représentés. En e↵et, 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 1
0, 5
B 0 C
B C
@0, 5A
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 e↵et, 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
di↵érence entre le vecteur attendu et le vecteur renvoyé et je fais la moyenne des carrés
des écarts.
0 1
(0, 5 0)2 = 0, 25
B 02 = 0 C 0, 25 + 0 + 0, 25 + 0
B C
@(1 0, 5)2 = 0, 25A ! L = 4
= 0, 125
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 e↵et, 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.

K
Il serait e↵ectivement 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 DI
en utilisant un réseau de neurones, pour prouver leur efficacité.

2.5 Ensembles de données


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

22
Chapitre 3

Résultats d
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 di↵érence d’efficience entre un RNN et un LSTM. Commençons par des valeurs
⌧ moyennes :
µ = 10 1 , h = 50, n = 50 y
où µ désigne le taux d’apprentissage, h le nombre de neurones dans la couche cachée et n
le nombre de séquences. Mon ensemble d’entraı̂nement représente 70%, celui de validation
20% et celui de test 10% de toutes les données.
Voici un graphe représentant l’évolution des erreurs calculées sur les ensembles d’en-
traı̂nement et de validation après 1000 époques :

(a) LSTM (b) RNN Etrelles


Figure 3.1 – Comparaison de réseaux

Sur chaque image, on peut voir l’évolution du coût en fonction de chaque époque.
Le graphe bleu représente le coût de validation et le rouge celui d’entraı̂nement. Après
1000 époques, le réseau me donne presque systématiquement la bonne séquence de sortie
lorsque je lui en donne une en entrée 1 . Lorsque le coût est supérieur à 2, les séquences
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

o_O
uniquement par l’apprentissage e↵ectué 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 e↵et, le réseau ne s’est pas corrigé
hm
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 di↵é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.

00
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 di↵érence ici est notable sans pour autant être drastique, alors

SI
que les calculs des cellules des LSTM sont nettement plus compliqué :je m’attendais à
une plus grande di↵érence. Sans en être certain, je suppose que cela est causé par le fait

O O
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) :

O
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
d
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’e↵et 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 di↵é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

Figure 3.2 – LSTM sur 10’000 époques

porte les objets). Voici à quoi ressemble la création d’un réseau et son entraı̂nement avec
le code que j’ai créé :

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


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

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


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

25
E-Magination Arthur Wuhrmann

26
Chapitre 4

Musique

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

4.2 Traitement des données


Malheureusement, mon réseau ne peut analyser que des séquences de notes. Or, la plu-

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

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

27
E-Magination Arthur Wuhrmann

Comme il n’y a pas d’espacement temporel entre les notes du premier accord, les trois
caractères sont collés. Cependant, je note ensuite qu’il y a une durée entre l’accord et le
do, puis entre le do et le mi et entre le mi et le sol à l’aide du symbole dollar. Chacun
de ces caractères est ensuite encodé exactement de la même manière que dans l’exemple
de la section 2.4.2, sauf que cette fois le nombres de caractères di↵érents est largement
supérieur. Il peut y avoir jusqu’à 89 caractères di↵é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.
Il
4.3 Architectures utilisées
Le LSTM que j’ai codé à la main, sans utiliser de bibliothèques autres que celles men-

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

1 from keras import *


2

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


4 reseau = Sequential()
5

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


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

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


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

15 #On ajoute la fonction relu dite d'activation


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

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

28
Arthur Wuhrmann E-Magination

20

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


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

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


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

Ï
A titre d’exemple, une des fonctionnalités ajoutées par Keras est appelée ⌧ Dropout .
Cela va volontairement ajouter des erreurs au réseau afin qu’il s’améliore. Il va changer
la valeur de plusieurs biais et synapses alétoirement choisis au sein du réseau. Cela peut
paraı̂tre très contre-intuitif, mais plusieurs raisons justifient ce résultat[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

Figure 4.1 – Fonction ReLU

4.4 Choix des morceaux


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

29
l d
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
qui
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 ;

Mu – 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.
Ji
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é. ressortie
et
Parlons maintenant de la musique qui ressortait du programme après cet entraı̂nement
acharné 1 . Ce que l’on remarque après les premières secondes d’écoute, c’est que ce n’est
pas Bach en personne qui a composé ces musiques. En e↵et, beaucoup de fausses notes
subsistent, notamment dans les accords. Toutefois, ce que l’on peut dire, c’est que ce
morceau, bien qu’il soit très brut, présente immédiatement un style baroque, classique. À
titre personnel, c’est donc une réussite.

1. Un extrait est disponible à l’adresse suivante : https ://soundcloud.com/user-582690328/bach-tm

30
Chapitre 5

Conclusion

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


philosophique et morale à mon travail. En e↵et, les intelligences artificielles soulèvent
nombreuses questions auxquelles il est souvent difficile de répondre. Une machine peut-
elle être créative ? Une machine peut-elle apprendre ?
Le fonctionnement du ⌧ machine learning prouve principes de l’apprentissage. On
dit souvent que l’on n’apprend que de ses erreurs ; en e↵et, l’algorithme des IAs d’appren-
tissage automatique ne fait que se corriger sur les erreurs qu’il produit, et c’est ce qui lui

a permet de s’améliorer. Une IA qui ne se trompe pas ne progresse pas.


On pense souvent que une machine ne fait qu’exécuter des tâches que nous aussi hu-
mains savons faire, mais simplement plus rapidement, comme faire des calculs ou trier des

0
tableaux. Dans l’excellente vidéo de ScienceEtonnante[4], ce dernier nous parle de l’IA

y
dans le domaine des jeux. La première machine a avoir battu un champion d’échec était
⌧ DeepBlue , construite par IBM en 1997. Elle avait mené 4-2 contre le champion de
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

Je ne pense donc pas que mon programme ne soit très créatif.


Comme dit à la fin de la section 4.5, j’estime que j’ai réussi l’objectif que je m’étais

O
lancé, à savoir de faire composer de la musique à une machine. Cependant, en commençant
mon travail, j’étais loin de m’imaginer toutes les contraintes et difficultés par lesquelles
j’allais devoir passer. Tout d’abord, le sujet étant récent, il existe peu de sources et la
quasi-intégralité d’entre elles est en anglais, ce qui ne facilite pas la compréhension. De
plus, le niveau mathématique est largement plus élevé que le mien lorsque j’ai commencé
ce projet, j’ai donc du me familiariser avec de nombreux thèmes comme l’algèbre linéaire
ou la dérivation composée. En outre, même si je savais déjà coder en Python, il m’a fallu
apprendre l’utilisation de plusieurs bibliothèques telles que NumPy, Keras, music21 et
bien d’autres. Je regrette également de ne pas avoir été plus régulier et ponctuel dans les
rendus et rendez-vous, c’était pour moi un réel défi d’organisation. Enfin, j’aurais souhaité
m’investir un peu plus dans la génération de musique à la fin, et pourquoi pas essayer de
varier les bases de données et le style musicale. Toutefois, j’ai dû commencer laI rédaction
de mon travail et mettre de côté la partie pratique.
Tous ces obstacles auxquels je me suis confronté n’ont fait qu’augmenter les connais-
sances que j’ai acquises et ont m’ont rendu d’autant plus fier d’avoir réussi.

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

[1] Chess.com. Alphazero vs stockfish chess match : Game 3. 2017.


[2] N. Hansen, Christensen P., and Johansen A. How to build RNNs and LSTMs from
scratch. 2019.
[3] Nguyên Hoang L. Gloire aux erreurs (dropout) | Intelligence artificielle 45. 2018.
[4] ScienceEtonnante. Une intelligence artificielle peut-elle être créative ? — science
étonnante 57. 2019.

33

Vous aimerez peut-être aussi