Backpropagation - Descente de gradient

Un rĂ©seau de neurone peut ĂȘtre vu comme une expression mathĂ©matique :
- On a des donnĂ©es d’entrĂ©e (inputs) sous forme de vecteurs (appelĂ©s Tensor)
- une matrice dont on essaie de calculer les valeurs (poids ou weights) durant la phase d’entrainement
- et on calcule un rĂ©sultat : cela peut ĂȘtre la prĂ©diction d'une valeur numĂ©rique par notre rĂ©seau

Dans le cas oĂč notre rĂ©seau essaie de prĂ©dire une valeur, il nous faut une mĂ©trique qu'on peut suivre et qui va nous permettre d'Ă©valuer la pertinence de notre modĂšle. Cette mĂ©trique est mesurĂ©e par la fonction de perte.
Pour calculer les poids (weigths) on essaie de minimiser une fonction "loss function", car plus on minimise, et moins on a d’écart entre le rĂ©sultat du rĂ©seau et le rĂ©sultat attendu. Le calcul se fait sur des donnĂ©es d’entrĂ©es (inputs) qui servent Ă  l’entrainement du modĂšle.
L’algorithme qui permet itĂ©rativement de converger vers ce minimum s’appelle "la descente de gradient".

On a 4 étapes:

  1. Passage avant (forward pass)
  2. Évaluation de la fonction de perte (loss function)
  3. Rétropropagation (backward pass)
  4. Mise à jour du réseau

Forward pass

Lors du passage avant, les données d'entrée traversent le réseau pour générer des prédictions. Chaque neurone calcule une sortie en appliquant une fonction d'activation à la somme pondérée de ses entrées. La sortie de la derniÚre couche est ensuite comparée aux valeurs cibles à l'aide de la fonction de perte.
Étant donnĂ© des poids (weights) et des donnĂ©s d’entrĂ©es (inputs), le cacul de la sortie (output) se fait en faisant une forward pass.
On part des donnĂ©es d’entrĂ©es, et on calcule Ă  chaque Ă©tape du rĂ©seau de neurones les valeurs intermĂ©diaires jusqu’à arriver au rĂ©sultat final.
On obtient pour chaque jeu de données en entrée [X1,...,Xi] la prédiction Ʒi

Loss function

On cherche un indicateur unique nous donnant la prĂ©cision de notre modĂšle. La fonction de perte mesure l’écart entre les prĂ©dictions du modĂšle et les rĂ©sultats rĂ©els.
On prend comme fonction de perte l'écart entre les valeurs prédites et les valeurs constatées : MSE Mean Square Error

Rétropropagation (backpropagation)

La rĂ©tropropagation permet de calculer le gradient de la fonction de perte par rapport Ă  chaque poids du rĂ©seau. L'idĂ©e clĂ© est de propager l’erreur, Ă  partir de la couche de sortie, vers l'entrĂ©e, en ajustant les poids Ă  chaque Ă©tape.

Backpropagation vs Gradient descent

  • Gradient descent : algorithme d’optimisation gĂ©nĂ©ral pour calculer les poids du modĂšle.
  • Backpropagation : c’est une Ă©tape de l’algorithme gradient descent, oĂč on met Ă  jour les poids du modĂšle en calculant des derivĂ©es partielles (le gradient) qui donne l’ajustement qu’on donne au poids pour converger vers un minimun.
    On part de l’output node, et on remonte le graphe jusqu’aux inputs node. D’oĂč le terme de backpropagation

À chaque itĂ©ration (backpass), on a un ajustement des poids.
- Soit [W]^n les poids Ă  l’étape n
- Lr : learning rate - un paramùtre qui dit à quelle vitesse on veut aller (si c’est trop petit, on converge tout doucement, si c’est trop grand on peut osciller et ne pas trouver le min)
- [G]^n : le gradient - c’est à dire le petit ajusteemnt local des poids qui va permettre au modùle de se rapprocher de l’optimal
[W]^n+1 = [W]^n - Lr * [G]^n

Exemple manuel:

  • Inputs : a , b ,c,f
  • OpĂ©rations : e = a * b ; d = e + c ; d * f = L
  • RĂ©sultat : L

    a --|
    | () --> e --|
    b --| | (+) --> d --|
    c --| | (
    ) --> L
    f --|

On veut connaitre l’effet d’une variation de a sur L. Si on augmente un peu a, est-ce que L augmente ou diminue.
Pour cela on doit calculer la dérivée de L par rapport à a, soit dL/da. Pour y arriver on va calculer les dérivées intermédiaires

  • dL/dd
  • puis dL/de
  • puis dL/da

Effectuons la backpropagation manuelle

1

dL/dL = 1 c’est le gradient local.
Notons la valeur du gradient sous la forme [gradient]

a --|
    | (*) --> e --| 
b --|             | (+) --> d --|
              c --|             | (*) --> L [1]
                            f --|

2

Comme L = d*f
dL/dd = f
dL/df = d

a --|
    | (*) --> e --| 
b --|             | (+) --> d --| [f]
              c --|             | (*) --> L [1]
                            f --| [d]

3

dL/de = dL/dd * dd/de
d = e + c
Sur une addition, cela correspond à faire une fois le gradient précédent
dd/de = 1 * grad[d]

a --|
    | (*) --> e --| [f] 
b --|             | (+)    --> d --| [f]
              c --| [f]            | (*) --> L [1]
                               f --| [d]

4

a --| [f*b]
    | (*)      --> e --| [f] 
b --| [f*a]            | (+)    --> d --| [f]
                   c --| [f]            | (*) --> L [1]
                                    f --| [d]

Mini-batch

Le mécanisme général est

  • On utilise 1 jeu de donnĂ©e [X],[Y], on fait la forward pass, calcul de la fonction de perte puis backward pass et mise Ă  jour des poids
    Pour optimiser on peut aussi, travailler sur un batch de donnée
  • On a par exemple 2 donnĂ©es [X1] [X2]

  • On fait en parallĂšle la forward pass (grĂące aux GPU, on parallĂ©lise les calculs),

  • on obtient notre fonction de perte qui est fonction des deux jeux de donnĂ©es, pour une MSE : Loss = (o1-y0)ÂČ + (o2 -y1)ÂČ. Avec o1, o2 les outputs du rĂ©seaux, c'est Ă  dire les valeurs prĂ©dites.
  • on fait la backward pass, en mettant Ă  jour [W] et b.
  • upgrade des valeurs de [W] et b
    La différence entre les méthodes 1 et 2, c'est que la mise à jour du gradient est mutualisée sur un ensemble de jeux de données. On prend un batch de donnée, on calcule le gradient et on mets à jours les poids

Exemple numérique

  • Soit un rĂ©seau avec un neurone, 2 entrĂ©es (et donc un vecteur poids W[w1,w2]) et une fonction d'activation tanh.
  • Soit X[x1,x2] une vecteur d'input, et y la valeur rĂ©elle attendue
  • Soit o1 = tanh(X@W+b)
  • Note : n1 = X@W+b -> c'est un scalaire.

    L = (Ć· -y)ÂČ = Ć·ÂČ + yÂČ -2Ć·y
    L = (o1 -y)ÂČ = o1ÂČ + yÂČ -2o1y
    dL/do1 = 2o1 + 0 -2y
    dLdo1 = 2o1 - 2y

    Calculons do1 / db
    g(x): x->tanh(x) // g'(x) = 1 - tanh(x)tanh(x)
    o1 = tanh(n1)
    do1db = 1- torch.tanh(n1)
    *2
    print(f"{fo1b=}")

    dL/db = dL/o1 * do1/db
    dLdb = dLdo1*do1db
    print(f"Computed gradient: {dLdbitem()=}")

Nouvel exemple avec :

  • 2 donnĂ©es d'entrĂ©e
  • 1 seul neurone
  • on va calculer le gradient de W[]

    dL/dw1 = dL/do1 * do1/dw1
    - dL/do1 = 2o1- 2y[0]
    - do1/dw1 = d(tanh(x1w1+b))/dw1
    g(x) = tanh(ax+b) // g'(x) = a * tanh'(ax+b) = a * (1 - tanh(ax+b)
    tanh(ax+b))
    a -> x1 = x1[0]
    dLdw11 = (2o1- 2y[0])x1[0](1-torch.tanh(n1)**2)
    print(f"{dLdw11=}")

    dLdw12 = (2o1- 2y[0])x1[1](1-torch.tanh(n1)**2)
    print(f"{dLdw12=}")

    dLdw21 = (2o2- 2y[1])x2[0](1-torch.tanh(n2)**2)
    print(f"{dLdw21=}")

    dLdw22 = (2o2- 2y[1])x2[1](1-torch.tanh(n2)**2)
    print(f"{dLdw22=}")
    print(dLdw11+dLdw21)
    print(dLdw12+dLdw22)
    print(f"{w1.grad=}")

Mise Ă  jour des poids

Questions

  • Scalar vs Tensor
    On pourrait travailler sur des scalaires (des floats), mais on ne le fait pas en production pour des raisons de performances. On parallélise les calculs et les bibliothÚques telles que pytorch ne prennent pas en entrée des scalaires mais des Tensors. Un Tensor est un vecteur.
    Dans Pytorch, un Tensor est un objet qui a des mĂ©thodes, une reprĂ©sentation interne et qui a donc un ensemble d’opĂ©rations disponibles de façons optimisĂ©se.
  • diffĂ©rence entre un vecteur et un tensor : pourquoi deux noms diffĂ©rents ?
    Un Tensor est un objet de la bibliothĂšque Pytorch. Il permet de rĂ©aliser plus d’opĂ©rations qu’un simple vecteur. On peut le convertir, changer sa taille, le multiplier, appliquer une fonction dessus.
  • dans un rĂ©seau de neurone, on a les poids qui reprĂ©sentent les valeurs des opĂ©rations Ă  effectuer mais oĂč sont stockĂ©s les opĂ©rations. Ex je veux passer du layer 3 Ă  4. Et je dois faire 3x-7. OĂč est stockĂ© cette Ă©quation ?
    Si on utilise une bibliothĂšque comme pytorch, et qu’on passe par des tensors, ceux-ci modĂ©lisent la structure complĂšte du graph des opĂ©rations.
    Si T1 = log(T2T3 + T4), alors quand on utilise la fonction
    T1.backward(), pytorch "sait" que pour cela il faut calculer les dérivées de log (T2
    T3 + T4)
  • Ă  quoi sert la "forward pass" vs la "backward pass" dans le gradient descent
    Le forward pass sert Ă  caculer la valeur de sortie du rĂ©seau et en particulier Ă  calculer la fonction loss qu’on cherche Ă  optimiser
    La backward pass sert Ă  ajuster la valeur des poids du rĂ©seau en se servant du gradient. Via le gradiant on sait quels poids doivent ĂȘtre augmentĂ©s et lesquels doivent ĂȘtre diminuer afin de faire diminuer la fonction de perte (loss)