7. Réseaux de neurones, et les différences finies comme juge
Sections
Réglages de lecture
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
Chaque modèle jusqu’ici avait une forme choisie d’avance : une droite, une frontière, un arbre. Un réseau de neurones n’a presque pas de forme. C’est une pile de deux sortes d’étapes — une application affine y = xW + b, et une fonction non linéaire fixe appliquée à chaque nombre — et ce qu’il peut représenter ne dépend que du nombre d’étapes empilées.
La partie qui mérite d’être apprise à la main n’est pas l’empilement. C’est la rétropropagation : la règle de dérivation en chaîne appliquée à rebours à travers la pile, qui transforme une perte en un gradient pour chaque poids. C’est aussi la partie facile à se tromper subtilement et difficile à remarquer, d’où l’appui de cette leçon sur le seul outil qui tranche ce genre de débat — les différences finies, qui ne savent rien de la règle en chaîne et ne peuvent que mesurer.
| ML.NET | Tribuo | PyTorch | IX | |
|---|---|---|---|---|
| Une couche | — | Layer via TensorFlow |
nn.Linear |
ix_nn::layer::Dense |
| Une pile | — | — | nn.Sequential |
ix_nn::network::Sequential |
| Activations | — | — | nn.ReLU, nn.Sigmoid… |
aucune n’implémentant Layer |
| Pertes | — | — | nn.MSELoss |
mse_loss, binary_cross_entropy |
| Gradients automatiques | — | — | autograd | ix-autograd, un crate séparé avec son propre ruban |
Le programme est examples/l07_networks.rs, sur les 65 builds de la leçon 2, les deux colonnes centrées-réduites.
Une couche, et ses trois gradients
Section intitulée « Une couche, et ses trois gradients »Pour y = xW + b, la règle en chaîne donne trois choses d’un coup. Notons g = dL/dy, le gradient que la couche reçoit d’en haut :
dL/dW = xᵀ g dL/db = Σ rows of g dL/dx = g WᵀLes deux premiers mettent cette couche à jour ; le troisième est le message passé à la couche en dessous. La version à la main renvoie les trois et n’en applique aucun (src/net.rs, lignes 40-55) :
pub fn backward(&self, x: &Array2<f64>, grad_output: &Array2<f64>) -> LinearGrads { LinearGrads { weights: x.t().dot(grad_output), bias: grad_output.sum_axis(Axis(0)), input: grad_output.dot(&self.weights.t()), }}Aucune division par la taille du lot n’apparaît, et aucune ne devrait : la moyenne que veut la perte est déjà dans g. Si la perte est une moyenne sur n lignes, son propre gradient porte le 1/n.
Le juge est d’accord :
== the hand layer, judged by finite differences analytic -0.534895701, numeric -0.534895701, gap 4.35e-11La valeur numérique vient de la différence centrée (f(w + h) - f(w - h)) / 2h avec h = 1e-6, dont l’erreur est d’ordre h², environ 1e-12, plus l’arrondi. Un écart de 4e-11 est une réussite. Toute vraie erreur dans backward — une transposée manquante, un facteur oublié — se voit comme un écart d’ordre 1, pas d’ordre 1e-11.
Le gradient que Dense applique vraiment
Section intitulée « Le gradient que Dense applique vraiment »ix_nn::layer::Dense ne renvoie pas ses gradients ; backward met lui-même les poids à jour et ne renvoie que le message pour la couche du dessous (layer.rs, lignes 40-52) :
let grad_weights = input.t().dot(grad_output) / n;let grad_bias = grad_output.mean_axis(ndarray::Axis(0)).unwrap();let grad_input = grad_output.dot(&self.weights.t());self.weights = &self.weights - &(learning_rate * &grad_weights);La question « que soustrait Dense ? » peut donc se trancher par l’expérience plutôt que par la lecture : fixer les poids à la main, appeler backward avec un taux d’apprentissage de 1, et regarder de combien ils ont bougé. À côté, mesurer le gradient d’ix_nn::loss::mse_loss lui-même par différences centrées.
== ix_nn::layer::Dense, one output column gradient of mse_loss, measured: -0.534895701 what backward subtracts: -0.008229165 ratio: 65.0000, and the batch has 65 rowsExactement 65 : le nombre de lignes. grad_output porte déjà le 1/n que mse_gradient y a mis, et backward divise par n une seconde fois. La direction est bonne, donc l’entraînement fonctionne encore — mais le taux d’apprentissage que vous passez n’est pas celui que vous obtenez. Sur ces 65 lignes, learning_rate = 0.1 se comporte comme 0,0015 ; sur un lot de 10 000 il se comporterait comme 1e-5, et un réseau qui s’entraîne bien sur un petit jeu paraîtrait figé sur un grand. Rien d’autre ne change, puisque chaque Dense divise par le même n, si bien que les couches restent proportionnées entre elles.
Une perte et un gradient qui ne s’accordent pas
Section intitulée « Une perte et un gradient qui ne s’accordent pas »mse_loss moyenne sur chaque case de la matrice, lignes et colonnes. mse_gradient ne divise que par les lignes (loss.rs, lignes 6-16) :
pub fn mse_loss(predicted: &Array2<f64>, target: &Array2<f64>) -> f64 { let diff = predicted - target; diff.mapv(|v| v * v).mean().unwrap()}
pub fn mse_gradient(predicted: &Array2<f64>, target: &Array2<f64>) -> Array2<f64> { let n = predicted.nrows() as f64; 2.0 * (predicted - target) / n}Avec une seule colonne de sortie les deux s’accordent. Avec m colonnes, mse_gradient vaut m fois le vrai gradient de mse_loss, et les deux effets se composent :
== the same layer with two output columns measured gradient [-0.267448, -1.234896] backward subtracts [-0.008229, -0.037997] ratios [32.5000, 32.5000]32,5 vaut 65 / 2 : divisé par 65 lignes par Dense, multiplié par 2 colonnes par mse_gradient. L’exercice pousse le rapport jusqu’à quatre colonnes et obtient 1, 2, 3, 4 exactement. Tout réseau d’IX à plus d’une sortie est donc entraîné à un taux d’apprentissage mis à l’échelle par le nombre de sorties, ce qui est le genre de chose qui transforme « on a réglé le taux d’apprentissage » en folklore.
Des poids que personne ne peut reproduire
Section intitulée « Des poids que personne ne peut reproduire »Dense::new tire ses poids d’une loi normale sans la moindre graine en vue (layer.rs, ligne 27) :
weights: Array2::random((input_size, output_size), Normal::new(0.0, std).unwrap()),ndarray_rand::RandomExt::random tire du générateur du fil d’exécution, donc deux couches construites dans la même exécution diffèrent déjà, et deux exécutions du même programme diffèrent aussi :
== Dense::new(3, 2) twice in the same run the two weight matrices are equal: false all zero biases: true, weights are public, so a course can overwrite themTous les autres algorithmes aléatoires d’IX prennent une graine — KMeans::with_seed, RandomForest::with_seed, Dropout::new(p, seed), transformer::FeedForward::new(d_model, d_ff, seed). Dense est l’exception, et la conséquence est qu’aucun résultat de Sequential ne peut être reproduit ni testé contre les régressions. La sortie de secours est que weights et bias sont des champs publics : construire la couche, puis les écraser, ce que cette leçon fait partout.
Ce qu’une pile de couches Dense peut représenter
Section intitulée « Ce qu’une pile de couches Dense peut représenter »Deux applications affines à la suite n’en font qu’une : (xA + a)B + b = x(AB) + (aB + b). Un réseau ne devient plus qu’une droite que si quelque chose de non linéaire s’intercale entre ses couches.
ix-nn a un trait Layer avec forward et backward, et exactement un type l’implémente : Dense. Il n’y a pas de couche ReLU, pas de couche Sigmoid, rien que Sequential::push pourrait accepter entre deux applications affines. (Le crate a bien un GELU — transformer::gelu — mais il vit à l’intérieur de FeedForward, travaille sur Array3 et n’est pas un Layer.)
Le ou exclusif est le plus petit problème qui montre ce que cela coûte. Quatre points, et aucune droite ne sépare les deux classes :
== exclusive or hand, two affine layers with a sigmoid between them: loss 0.35722182 -> 0.00000000 predictions [0.0000, 1.0000, 1.0000, 0.0000] ix_nn, two Dense layers and nothing between them: loss 0.34500000 -> 0.25000000 predictions [0.5000, 0.5000, 0.5000, 0.5000] f(0,0) + f(1,1) - f(0,1) - f(1,0) = 0.00e0: the stack is one affine mapLe réseau à la main — même taille, même taux d’apprentissage, mêmes 50 000 époques, une sigmoïde entre les couches — le résout exactement. La pile IX converge vers 0,25 et prédit 0,5 partout : 0,25 est la variance des quatre cibles, le mieux qu’une constante puisse faire, et une constante est le mieux qu’une application affine puisse faire ici.
La dernière ligne est la preuve plutôt que le symptôme. Pour toute f affine, f(0,0) + f(1,1) = f(0,1) + f(1,0), parce que les deux membres valent 2f évaluée au même centre. La pile entraînée le satisfait au dernier bit, après 50 000 époques d’entraînement, à toute profondeur. Elle n’est pas sous-entraînée ; elle ne peut pas représenter la fonction.
Rien de tout cela ne rend ix-nn fautif — l’attention, la normalisation de couche, RoPE, ALiBi et les blocs transformeurs du même crate sont là où est son vrai travail, et ceux-là ont bien leurs non-linéarités. Mais Sequential plus Dense est la partie qui ressemble à une API de réseau de neurones pour débutants, et c’est la partie qui ne peut ajuster que des droites.
À retenir
Section intitulée « À retenir »- La passe arrière d’une couche affine donne trois gradients,
xᵀ g, la somme des lignes degetg Wᵀ. Les différences centrées les vérifient à environ1e-11près, et toute vraie erreur apparaît comme un écart de l’ordre de 1. Dense::backwarddivise une deuxième fois par la taille du lot : le taux d’apprentissage que tu passes rétrécit avec le nombre de lignes.mse_lossfait la moyenne sur toutes les cellules maismse_gradientne divise que par les lignes : avecmcolonnes de sortie, le gradient vautmfois le vrai.Dense::newtire ses poids sans graine ; écrase les champs publicsweightsetbiaspour rendre un résultat reproductible.- Deux applications affines à la suite n’en font qu’une. Faute de couche d’activation, un
Sequentialde couchesDensed’IX prédit 0.5 partout sur le ou exclusif, là où une sigmoïde entre les couches le résout.
Exercices
Section intitulée « Exercices »- Choisissez un taux d’apprentissage qui fasse atterrir un pas de
Denseexactement là où atterrit un pas de la couche à la main. - Mesurez
mse_gradientcontre le vrai gradient demse_losspour une à quatre colonnes de sortie. - Quelle largeur faut-il à la couche cachée avant que le réseau à la main résolve le ou exclusif ?
Solutions
Elles sont dans examples/l07_exercises.rs.
1. Multipliez par le nombre de lignes, le facteur mesuré plus haut :
== matching one step of the hand layer with one step of Dense hand weight after one step at rate 0.5 0.667447851 Dense weight after one step at rate 0.5 x 65 0.667447851 gap 0.00e0Non pas approximativement — bit pour bit, parce que la seule différence entre les deux chemins de code est une multiplication. Le point d’arrivée mérite un second regard : 0,667447851 est la pente des moindres carrés en forme close de la leçon 2. Les deux colonnes étant centrées-réduites, la perte vaut w² - 2rw + 1, dont le gradient est 2w - 2r, si bien qu’un pas de taux 0,5 depuis n’importe quel point de départ atterrit exactement sur r. Un pas, aucune itération.
2. Le rapport est le nombre de colonnes de sortie :
== ix_nn::loss::mse_gradient divided by the true gradient of mse_loss 1 column(s): ratio [1.0000], and mse_loss = 0.626042 2 column(s): ratio [2.0000], and mse_loss = 1.859063 3 column(s): ratio [3.0000], and mse_loss = 3.758750 4 column(s): ratio [4.0000], and mse_loss = 6.3251043. Deux unités sigmoïdes suffisent, et une ne suffit pas :
== width of the hidden layer against what it can learn (exclusive or) hidden 1: final loss 0.166788, corners right 3 of 4 hidden 2: final loss 0.000000, corners right 4 of 4 hidden 3: final loss 0.000000, corners right 4 of 4 hidden 4: final loss 0.000000, corners right 4 of 4Une unité sigmoïde trace une frontière, et le ou exclusif en demande deux ; elle obtient trois coins justes et se stabilise à 0,167, mieux que le 0,25 d’une constante mais sans être une solution. Deux unités tracent deux frontières, et la couche de sortie les combine. C’est le plus petit exemple du résultat général — une seule couche cachée de largeur suffisante peut approcher toute fonction continue — et de sa moitié inutile : le théorème dit qu’une largeur existe, pas laquelle.
- Rumelhart, Hinton et Williams, Learning representations by back-propagating errors, 1986
- Goodfellow, Bengio, Courville, Deep Learning, chapitre 6, y compris la vérification du gradient par différences finies
- Nielsen, Neural Networks and Deep Learning, chapitre 2, pour la rétropropagation pas à pas
- PyTorch :
torch.autograd.gradcheck, le même test sous forme de fonction de bibliothèque - IX en
490c395:ix-nn/src/layer.rs,ix-nn/src/network.rs,ix-nn/src/loss.rs,ix-nn/src/transformer.rs