predictions = Nx.tensor([[0.2], [0.8]]) # {2, 1}
cibles = Nx.tensor([0.0, 1.0]) # {2}
Nx.subtract(predictions, cibles) # {2, 2} !
Chaque prédiction rencontre chaque cible : [[0.2, -0.8], [0.8, -0.2]].
Nous voulions une erreur par exemple : les cibles doivent avoir la forme {2, 1}.
| À écrire | Reçoit | Rend |
|---|---|---|
TensorStats.center(x) |
{n, 2} |
la moyenne de chaque coordonnée, {2} |
TensorStats.centered(x) |
{n, 2} |
chaque ligne moins ce centre, {n, 2} |
Error.mse(p, y) |
deux {n, 1} |
la MSE, un scalaire {} |
Error.mse refuse deux formes différentes avec une ArgumentError.
Puis : prédire reshape et transpose, reproduire le piège {3, 1} − {3}.
Nx.tensor(liste, type: :f32), Nx.shape(t) et Nx.type(t) construisent et inspectent.Nx.mean(t, axes: [0]), Nx.subtract(a, b) et Nx.pow(t, 2) calculent hors defn.if condition, do: raise(ArgumentError, "Formes différentes") refuse un contrat non respecté.Nx.to_list(t) et Nx.to_number(t) ramènent un résultat côté Elixir.assert_in_delta a, b, 1.0e-6 compare deux flottants à une tolérance près.Ouvrez ap4-02-tenseurs.livemd. 30 min.
Prédisez chaque forme avant d'exécuter, puis vérifiez-la avec Nx.shape.
Écrivez le centrage et la MSE avec son contrôle de formes.
Reproduisez le piège du broadcasting et corrigez sa cause.
Pour les coordonnées, nous avons choisi {n, 2}.
Pour une prédiction par observation, nous choisirons {n, 1}.
Expliquez quelle dimension doit rester dans le calcul de l'erreur.
Montrez un calcul qui s'exécutait mais ne répondait pas à la question.
Les coordonnées
Par ex., avec
Décidons la classe 1 quand
| Coin | (−1, −1) | (−1, 1) | (1, −1) | (1, 1) |
|---|---|---|---|---|
| Score | −1 | −1 | 1 | 1 |
| Classe décidée | 0 | 0 | 1 | 1 |
| Vraie classe | 1 | 0 | 0 | 1 |
La frontière
defn décrit le calcul pour tout le lotdefmodule Distance do
import Nx.Defn
defn squared(x, center) do
Nx.sum((x - center) * (x - center), axes: [1], keep_axes: true)
end
end
x a la forme {n, 2} et center la forme {2} ; le résultat, une distance au carré par point, a la forme {n, 1}.
parameter a:0 f32[3][2]
parameter b:1 f32[2]
c = subtract a, b f32[3][2]
d = subtract a, b f32[3][2]
e = multiply c, d f32[3][2]
f = sum e, axes: [1], keep_axes: true f32[3][1]
Nx.Defn.jit(&Distance.squared/2, compiler: Nx.Defn.Debug) rend ce graphe au lieu des valeurs.
| Notre langage | Calcul numérique |
|---|---|
| L'AST conservait additions et appels. | Le graphe conserve les opérations sur les tenseurs. |
| Le compilateur transformait cet arbre. | Nx peut transformer le calcul, notamment pour le dériver. |
| La VM exécutait les instructions. | Un évaluateur ou un compilateur exécute le calcul numérique. |
Nous pouvons travailler sur un calcul avant d'en demander le résultat.
params = %{weights: Nx.tensor([2.0, -1.0]), bias: Nx.tensor(0.5)}
x = Nx.tensor([[1.0, 2.0], [3.0, 4.0]])
Score.predict(params, x) # à écrire avec defn : forme {2, 1}
defn, params.weights lit la map ; *, + et Nx.sum agissent sur les tenseurs.defn, x * 2 lève une ArithmeticError : on écrit Nx.multiply(x, 2).Ouvrez ap4-03-graphe.livemd. 25 min.
Écrivez et vérifiez le score sur des points calculés à la main.
Lisez son graphe et retrouvez les formes intermédiaires prévues.
Cherchez des paramètres qui séparent les quatre coins : où bloquez-vous ?
Les points (1, 1) et (-1, -1) appartiennent à la classe 1.
Les points (1, -1) et (-1, 1) appartiennent à la classe 0.
Peut-on trouver un score affine et un seuil t qui séparent ces classes ?
Additionnez les deux inégalités de chaque classe : que doit vérifier le biais ?
Nous chercherons ensuite les paramètres par le gradient, puis nous changerons le modèle.
À dire : Bonjour à tous. Nous commençons par le quiz sur AP3 : dix minutes pour répondre, puis nous le corrigeons ensemble. Aujourd'hui, nous quittons un moment notre petit langage pour travailler sur des données : des points, des tableaux de nombres et des calculs sur ces tableaux. C'est la première de deux séances qui mènent à l'apprentissage automatique ; nous avancerons sur de petits exemples que l'on peut refaire à la main. Je vous montre d'abord où nous allons. Repères : quiz sur AP3 avant le lancement, sujet et clé dans quiz/quiz-4-compilation (chapitre 6 du poly, annoncé en AP3), dix minutes de réponse puis cinq de correction, pris sur la marge du créneau officiel. Ouverture de 10 min avec les trois pages suivantes, dont deux explications d'élèves sur le cycle AP1–AP3. Les autres échanges de reprise du cycle se font individuellement pendant le premier atelier et comptent dans la participation ; ils ne deviennent pas un quatrième exposé.
À dire : Jusqu'ici, c'est nous qui écrivions toutes les règles de nos programmes. En apprentissage supervisé, on procède autrement : on choisit une formule qui contient des nombres libres, les paramètres, on lui montre des exemples dont on connaît la réponse, et un algorithme ajuste ces nombres pour que la formule retrouve les réponses. Pour observer ce mécanisme sans nous perdre dans des données réelles, nous prenons des points du plan : la classe vaut 1 quand x et y ont le même signe, 0 sinon, et 0 aussi sur les axes. Nous connaissons la règle exprès : elle nous permettra de juger ce que le modèle apprend, lui qui ne recevra que des points et leurs classes. Aujourd'hui, nous préparons ces exemples, nous les rangeons dans des tableaux de nombres et nous écrivons un premier calcul sur eux ; en AP5, un algorithme en cherchera les paramètres. Représenter un calcul, d'ailleurs, nous l'avons déjà fait. Repères : redessiner les quatre quadrants au tableau si la projection est peu lisible. Ne pas parler encore de réseau de neurones ni d'une « IA qui comprend » : le mot modèle désigne une formule à paramètres. La règle est une variante de XOR à classes échangées ; ce qui compte est l'alternance des classes d'un quadrant à l'autre.
À dire : Rappelez-vous AP2 : une expression comme 1 + 2 * x devenait un arbre, que l'évaluateur parcourait ensuite. En AP3, le compilateur traduisait ce même arbre en instructions pour notre machine à pile. Dans les deux cas, nous distinguions la représentation d'un calcul et son résultat. Nx, la bibliothèque numérique que nous découvrons aujourd'hui, fait la même chose : quand nous écrirons une fonction numérique, il en gardera la liste des opérations, et c'est ce qui lui permettra en AP5 de calculer des dérivées pour nous. Qui peut me rappeler en une phrase ce que faisait l'évaluateur d'AP2 ? Bien ; voyons comment se déroule la séance. Repères : faire rappeler la différence entre une expression, sa représentation et son résultat. Le rapprochement reste conceptuel, Nx ne produit pas le bytecode de notre VM ; il revient en troisième partie. Aucun parser terminé n'est nécessaire, la grille de points est construite dans le carnet.
À dire : La séance a trois parties sur le même rythme : je présente une idée sur un petit exemple, vous écrivez les fonctions dans un carnet Livebook, puis nous reprenons vos essais ensemble pendant cinq minutes. Ouvrez tout de suite le premier carnet et lancez sa première cellule : elle télécharge et compile Nx et Axon, ce qui prend quelques minutes la première fois. Ensuite tout est local, sur votre processeur, sans GPU ni compte en ligne. Dans chaque carnet, prédisez le résultat avant d'exécuter : c'est quand la prédiction est fausse qu'on apprend quelque chose. Commençons par les données. Repères : les exemples Nx s'exécutent dans Livebook après la cellule Mix.install ; le bouton d'exécution du site ne charge pas Nx. Versions : Nx 1.0, Axon 0.9, CPU BinaryBackend. La première installation demande le réseau. Les petits lots numériques des carnets 02 et 03 sont fournis : un retard dans le carnet 01 ne bloque pas la suite. Chaque carnet ouvre sa propre session, les modules d'un carnet ne sont pas chargés dans le suivant.
À dire : Première partie : fabriquer nos exemples. Un point, c'est deux nombres et une réponse ; nous le rangeons dans une map, que vous avez rencontrée en AP2 et AP3 pour les environnements. On lit une valeur par sa clé : point.x, point.label. Ici, x vaut −0.5 et y vaut 1 : les signes sont opposés, la classe vaut donc 0. Pour fabriquer toute une grille de ces maps, puis la vérifier, nous allons utiliser quatre outils d'Elixir pour les collections : les fonctions anonymes, le module Enum, le pipe et la compréhension for. Commençons par les fonctions anonymes. Repères : cours 01, 15 min jusqu'à la consigne. Sur les axes, où une coordonnée est nulle, notre convention donne 0 ; le dire une fois, sans en faire un piège. En AP5, le modèle ne recevra que des coordonnées et des étiquettes, jamais la formule.
À dire : Une fonction anonyme est une fonction sans nom, construite par fn et end. On peut la ranger dans une variable, ici etiqueter, et l'appeler avec un point avant la parenthèse : etiqueter.({0.6, 0.9}). Son paramètre {x, y} est un motif, comme en AP1 : il décompose chaque tuple en deux coordonnées. Enum.map reçoit la liste et cette fonction, l'applique à chaque élément et rend la liste des résultats. Que va rendre cet appel ? Deux maps, la première de classe 0, la seconde de classe 1. Et la liste points n'a pas changé : Enum.map en fabrique une nouvelle, comme toujours en Elixir. Quand on enchaîne plusieurs appels de ce genre, les parenthèses s'empilent ; le pipe va nous aider. Repères : en Python, l'équivalent serait [etiqueter(p) for p in points]. Relier aux fonctions comme valeurs d'AP2, sans réécrire l'interpréteur. La capture &String.length/1 apparaît une fois dans le carnet, en exemple ; ne pas en faire un objectif.
À dire : Le pipe, cette flèche, prend la valeur de gauche et la place comme premier argument de l'appel de droite. Ainsi points |> Enum.map(etiqueter) s'écrit aussi Enum.map(points, etiqueter), et ce résultat devient à son tour le premier argument d'Enum.filter. Sans le pipe, il faudrait écrire Enum.filter(Enum.map(points, etiqueter), …) et lire de l'intérieur vers l'extérieur ; avec lui, on lit le trajet de la donnée de haut en bas. Ce n'est qu'une réécriture : rien ne s'exécute en parallèle et points n'est pas modifié. Ce filtre ne garde que la classe 1 ; nous verrons à la reprise pourquoi ce serait un mauvais jeu d'exemples. Repères : faire réécrire mentalement l'appel imbriqué. Le filtre sert ici à montrer la syntaxe ; il perdrait toute la classe 0, ce que l'essai du carnet fait remarquer.
À dire : Le pipe ne nous protège pas d'une erreur de forme entre deux étapes. Ici, le filtre reçoit des tuples {x, y}, puisqu'il passe avant la conversion en maps. Que se passe-t-il si je le déplace après Enum.map, sans le modifier ? La fonction anonyme attend un tuple et reçoit une map : aucun motif ne correspond, et Elixir lève une FunctionClauseError. Pour réparer, il faut écrire le filtre pour des maps, avec point.x et point.y. Retenez la question à se poser à chaque étape : quelle forme sort de l'étape précédente ? Voyons maintenant comment fabriquer la grille elle-même. Repères : message réel, « no function clause matching in anonymous fn/1 ». Distinguer cette erreur, qui plante, d'une transformation valide qui enlève une classe sans planter : c'est l'objet de la reprise.
À dire : Pour fabriquer une grille, nous utilisons une compréhension for, l'équivalent des compréhensions de liste de Python. Chaque générateur, comme i <- -2..2, parcourt un intervalle ; avec deux générateurs, for parcourt toutes les combinaisons, comme deux boucles imbriquées. Les conditions placées après les générateurs écartent des combinaisons, ici celles qui tombent sur un axe. Le bloc do rend une valeur pour chaque combinaison retenue, et for rend la liste de ces valeurs. Combien de points obtient-on ? Seize : quatre valeurs non nulles pour i, quatre pour j. Et dans quel ordre ? j varie le plus vite : {-1.0, -1.0}, puis {-1.0, -0.5}, et ainsi de suite. Une fois la grille construite, il faut vérifier ce qu'elle contient. Repères : faire prédire 16 points, pas 4. Une compréhension rend les valeurs de son corps ; ce n'est pas une boucle qui accumule par mutation. Dans le carnet, grid/1 reçoit directement la liste des coordonnées possibles, par ex. [-1, -0.5, 0.5, 1] ; la règle sur les axes reste définie même si cette petite grille les exclut.
À dire : Avant d'entraîner quoi que ce soit, il faut savoir ce que contiennent nos données, et d'abord si les deux classes sont présentes. Ici, grille désigne les seize tuples de la page précédente. Enum.frequencies_by applique la fonction à chaque élément et compte combien de fois chaque résultat apparaît : sur nos seize points étiquetés, huit de classe 0 et huit de classe 1. Vous pourriez écrire le même compte avec Enum.reduce et un accumulateur, comme en AP1 : on part de %{0 => 0, 1 => 0} et chaque point incrémente le compte de sa classe. Ce résumé paraît anodin ; il nous servira pourtant à repérer une transformation qui perd une classe sans planter. Voyons ce que le carnet vous demande. Repères : valeur vérifiée sur la grille de la page précédente. Les rapides peuvent réécrire le compte avec Enum.reduce et Map.update!. Un compte par classe repère un filtre qui supprime une classe ; il ne suffit pas à prouver la qualité des données.
À dire : Le carnet 01 vous demande cinq petites fonctions, rangées dans deux modules. Samples.label donne la classe d'un point ; Samples.grid reçoit la liste des coordonnées possibles et rend une map par combinaison ; features ne garde que les coordonnées, sous forme de listes [x, y], dans le même ordre. Summary.counts compte les points par classe, et positive_features enchaîne un filtre et votre features avec le pipe. Chaque fonction est suivie de vérifications assert : si la condition est vraie, assert ne dit rien, et c'est bon signe ; sinon, il lève une erreur. Ajoutez à chaque fois un cas à vous. Récapitulons les formes d'Elixir dont vous aurez besoin. Repères : les raise("À écrire") sont des bouchons à remplacer. Le dessin VegaLite est fourni et se lit comme un pipe. L'essai « ne garder que la classe 1 » du carnet prépare la reprise.
À dire : Voici, sur une page, tout ce que le carnet 01 utilise ; gardez-la en tête pendant l'atelier. Pour chaque fonction, je vous conseille d'écrire d'abord un exemple d'entrée et la sortie attendue, puis le code. Le prolongement, pour ceux qui iront vite, lit des coordonnées saisies sous forme de texte avec with, que vous avez rencontré en AP2 ; il n'est pas demandé à tous. Pendant l'atelier, je passerai vous voir, notamment pour revenir avec chacun sur un point du premier cycle. À vous ! Repères : with reste un prolongement. Float.parse("0.5cm") rend {0.5, "cm"} : le motif {x, ""} impose une lecture complète, et with s'arrête à la première étape qui échoue. Repères du carnet : poly, chapitres 3 et 4. Le poly se consulte quand un bout de syntaxe manque, pas comme prérequis.
À dire : Vous avez quarante minutes pour le carnet 01. Commencez par la cellule d'installation si ce n'est pas fait, puis avancez dans l'ordre : chaque vérification doit passer avant la suivante. Gardez dans le carnet un essai qui a échoué, avec une phrase qui explique pourquoi. Si vous terminez, prenez le prolongement avec with. Repères : 40 min d'atelier. Socle : grille, règle, transformation, comptes et vérifications ; prolongement : coordonnées saisies et with. Faire les échanges de reprise du cycle AP1–AP3 pendant ce travail, sans ajouter un quatrième atelier. Faire écrire les fonctions, pas seulement changer des constantes dans une solution. Rappeler aux élèves qui attendent une confirmation qu'un assert silencieux est un succès.
À dire : Reprenons. Dans le carnet, quelqu'un « simplifie » les données en ne gardant que la classe 1, et le code s'exécute sans erreur. Qu'est-ce qui ne va pas ? Un modèle qui répond toujours 1 obtiendrait 100 % de bonnes réponses sur ces données sans avoir rien appris. Quelle vérification aurait détecté le souci ? Le compte par classe, avant et après la transformation ; la longueur de la liste, elle, ne le voit pas. Retenez que la forme des données et leur sens sont deux vérifications distinctes. Après la pause, nous rangerons ces coordonnées dans un tableau de nombres. Repères : reprise de 5 min. Faire montrer un résultat réel et une correction par deux élèves. La grille symétrique hors axes présente autant de points de chaque classe. Autre fuite à évoquer si le temps le permet : ajouter label aux coordonnées donnerait la réponse au modèle.
À dire : Dix minutes de pause. Sauvegardez votre carnet ; si l'installation de Nx n'est pas terminée chez vous, dites-le-moi maintenant. Repères : 10 min. Vérifier la fin d'installation de Nx sur les postes et résoudre l'environnement avant de reprendre ; le CPU suffit.
À dire : Deuxième partie. Nos points sont des maps dans une liste : c'est pratique pour préparer, mais pas pour calculer sur tous les points à la fois. Nx range des nombres dans un tenseur, c'est-à-dire un tableau dont tous les éléments ont le même type ; c'est l'équivalent d'un tableau NumPy. Ici, les quatre coins de notre carré deviennent un tableau de quatre lignes et deux colonnes : sa forme est {4, 2}. Le type f32 désigne des flottants sur 32 bits, quatre octets par nombre, le type usuel de l'apprentissage. Nous fixons une convention pour toute la suite : l'axe 0 énumère les exemples, l'axe 1 leurs coordonnées. Prenons le temps de bien lire ces formes. Repères : cours 02, 20 min jusqu'à la consigne. Livebook affiche #Nx.Tensor<f32[4][2] …> : le type, puis la taille de chaque axe. Sans type: :f32, des entiers donneraient un tenseur d'entiers. Ce lot est celui de la première cellule du carnet 02.
À dire : Prenons le vocabulaire dans l'ordre. Un scalaire est un nombre seul : il n'a aucun axe, et sa forme est le tuple vide. Un vecteur est une suite de nombres : un axe, ici de taille deux. Une matrice est un tableau de lignes et de colonnes : deux axes, d'abord le nombre de lignes, puis le nombre de colonnes. Regardez la dernière ligne : deux lignes d'une seule case. Elle contient deux nombres, comme le vecteur, mais sa forme est {2, 1} et non {2}. Cette différence va compter, car nous rangerons toujours une prédiction ou une cible par ligne, dans une colonne {n, 1}. Voyons ce que devient une forme quand on calcule une moyenne. Repères : la forme correspond à shape en NumPy, et le rang à ndim. Faire dire la forme de nos seize points : {16, 2}. Pas d'algèbre multilinéaire : un tenseur est ici un tableau rectangulaire de nombres.
À dire : Une réduction remplace tout un axe par une seule valeur, une somme ou une moyenne. Avec axes: [0], on additionne le long de l'axe 0, c'est-à-dire les lignes entre elles : 1 + 4, 2 + 5, 3 + 6 ; il reste une valeur par colonne, de forme {3}. Avec axes: [1], on fait la moyenne de chaque ligne : 2 et 5, de forme {2}. Sans axe, tout est réduit et il reste un scalaire. Je vous conseille de toujours prédire la forme d'abord, puis les valeurs, car c'est le choix de l'axe qui décide de la question posée. Pour nos points en {n, 2}, une moyenne sur l'axe 0 donne le centre du nuage ; sur l'axe 1, une moyenne par point, qui n'a guère de sens ici. Repères : faire un petit dessin de matrice au tableau, une flèche par axe. Aucun message d'erreur ne distingue une moyenne sur le bon axe d'une moyenne sur le mauvais. keep_axes: true conserve l'axe réduit avec la taille 1 : Nx.sum(a, axes: [1], keep_axes: true) a la forme {2, 1} ; le carnet 03 s'en servira.
À dire : Deux opérations changent la forme d'un tenseur, et elles ne font pas la même chose. reshape relit les valeurs ligne par ligne et les range dans la nouvelle forme ; transpose échange les axes, de sorte que chaque ancienne colonne devient une ligne. Les trois formes du tableau contiennent huit nombres, mais elles ne racontent pas la même chose : quatre observations de deux coordonnées, deux lignes de quatre valeurs, ou huit valeurs sans structure. Le carnet vous demande de prédire reshape et transpose sur un petit exemple ; suivez-y la valeur 4. Passons à une opération plus surprenante, qui combine deux tenseurs de formes différentes. Repères : reshape vers {2, 4} ne transpose pas une matrice {4, 2}. Ne pas montrer les résultats du carnet, qui demande de les prédire. Un tableau rempli de zéros ne permettrait pas de voir la différence. Ne pas transformer jusqu'à ce que l'erreur disparaisse.
À dire : Que signifie ajouter un vecteur de trois nombres à une matrice de deux lignes et trois colonnes ? Nx aligne les formes en partant de la droite : 3 en face de 3, cela correspond ; à gauche, l'axe absent du vecteur est traité comme une taille 1, puis étendu à 2. Le vecteur est donc ajouté à chaque ligne. C'est le broadcasting, ou diffusion, comme en NumPy. Il nous servira à centrer les points : soustraire le centre {2} à chaque ligne d'un tableau {n, 2}. Notez enfin qu'en dehors des fonctions numériques que nous verrons tout à l'heure, l'opérateur + d'Elixir ne sait pas additionner deux tenseurs : on écrit Nx.add, Nx.subtract, etc. Avant de voir le revers du broadcasting, il nous faut une mesure d'erreur. Repères : règle complète, alignement à droite, tailles égales ou égales à 1, axe absent traité comme une taille 1. Le broadcasting évite de fabriquer n copies du centre. Si le centrage servait à apprendre, le centre se calculerait sur l'entraînement seulement.
À dire : Pour juger des prédictions, il faut un nombre qui mesure leur écart aux réponses attendues. Prenons deux exemples : la première prédiction vaut 0.5 pour une cible 0, la seconde est exacte. On calcule l'écart de chaque ligne, on le met au carré, puis on fait la moyenne : 0.125. Lisons la formule symbole par symbole : p indice i est la prédiction de l'exemple numéro i, y indice i sa cible, et la grande lettre sigma signifie « additionner pour i allant de 1 à n » ; on divise ensuite par n. Pourquoi des carrés ? Pour que des écarts de signes opposés ne se compensent pas, et pour que les grosses erreurs pèsent davantage. C'est l'erreur quadratique moyenne, MSE en anglais ; vous l'écrirez avec Nx dans le carnet, et en AP5 elle deviendra la quantité qu'un algorithme cherche à réduire. Encore faut-il comparer chaque prédiction à sa propre cible. Repères : Nx.mean sans option réduit tous les axes et rend un scalaire de forme {}. Ne pas parler encore de dérivée ni de gradient, ce sera AP5. Valeur vérifiée avec Nx 1.0 : 0.125.
À dire : Voici le piège de la séance. Les prédictions sont rangées en colonne, de forme {2, 1} ; les cibles, par mégarde, en vecteur {2}. Que rend la soustraction ? Alignées depuis la droite, les formes {2, 1} et {2} deviennent {2, 1} et {1, 2}, et les deux tailles 1 s'étendent : le résultat a la forme {2, 2}. Chaque prédiction est comparée à chaque cible, au lieu de la sienne seulement. Aucune erreur, aucun avertissement : des nombres plausibles, mais une autre question ; la moyenne des carrés vaut alors 0.34 au lieu de 0.04. Le remède consiste à vérifier les formes avant les valeurs, et à exiger des formes identiques quand on compare ligne à ligne. C'est exactement ce que le carnet vous fait écrire. Repères : contre-exemple central d'AP4, qui reviendra dans la perte d'AP5. Ne pas affirmer que le broadcasting sait quel axe énumère les exemples : il ne connaît que des tailles. Valeurs vérifiées avec Nx 1.0 (f32 affiche −0.19999999 pour la dernière case).
À dire : Le carnet 02 vous demande trois fonctions. center rend le centre du nuage, la moyenne de chaque coordonnée ; centered soustrait ce centre à chaque point, grâce au broadcasting. Error.mse calcule l'erreur quadratique moyenne, mais commence par un contrôle : si les deux formes diffèrent, elle lève une ArgumentError au lieu de rendre un nombre faux. Viennent ensuite deux cellules d'observation : comparer reshape et transpose, puis reproduire le piège que nous venons de voir. Les petits lots sont fournis, vous n'avez pas besoin d'avoir terminé le carnet 01. Récapitulons les formes à utiliser. Repères : la MSE du carnet s'écrit avec Nx.subtract, Nx.pow et Nx.mean, et un if ordinaire contrôle les formes, puisque c'est un def. Le carnet attend 1/3 sur son exemple et 5/9 avec la mauvaise forme.
À dire : Voici les formes Nx et Elixir de l'atelier. Une fois le tenseur construit, gardez les opérations dans Nx ; Nx.to_list et Nx.to_number servent seulement à lire ou à vérifier un petit résultat. Une remarque sur les vérifications : les flottants ne se comparent pas avec ==, car un tiers n'a pas d'écriture exacte en binaire, d'où assert_in_delta, qui accepte un petit écart. Écrivez une prédiction de forme avant chaque cellule et un assert sur la forme à chaque frontière. À vous ! Repères : ne pas défaire les calculs Nx en Enum.map. Ne pas prétendre qu'Enum est lent partout : il convient à la préparation de nos petites données structurées. assert_raise vérifie qu'une erreur est levée ; le carnet l'explique.
À dire : Trente minutes pour le carnet 02. Écrivez la forme attendue avant chaque exécution, même en commentaire ; c'est elle qui vous dira si le résultat a un sens. Gardez le piège du broadcasting dans votre carnet, avec sa correction. Repères : 30 min. Demander une prédiction écrite des formes et un assert par frontière. Le carnet fournit les petits lots, aucune cellule de reprise de Samples n'est requise. Variante pour les rapides : le prolongement sur les invariants, centrer deux fois, une soustraction qui échoue et une autre qui réussit avec un sens erroné.
À dire : Reprenons. Qui peut me montrer un calcul qui s'est exécuté sans erreur mais ne répondait pas à la question ? Avec trois exemples, la soustraction de {3} à {3, 1} a donné neuf différences au lieu de trois, et la moyenne des carrés valait 5/9 au lieu de 1/3. Quelle dimension doit rester dans le calcul de l'erreur ? Celle des exemples : une erreur par ligne, puis une moyenne. Retenez que la forme se justifie par le sens des axes, pas par la disparition d'un message d'erreur. Nous allons maintenant écrire un calcul qui produit ces prédictions. Repères : reprise de 5 min. Ne pas donner seulement les bonnes formes : faire justifier les axes par le sens des données. Conserver le contre-exemple {n, 1} moins {n}, qui rend {n, n}, pour la perte d'AP5.
À dire : Troisième partie. Nous voulons qu'un calcul attribue un nombre, un score, à chaque point. Le plus simple consiste à multiplier chaque coordonnée par un poids, à additionner, puis à ajouter une constante, le biais. Les coordonnées viennent des données ; les poids et le biais sont les paramètres, les nombres que l'on peut régler. Faisons-le à la main avec les poids 2 et −1 et le biais 0.5, pour le point (−1, 1) : 2 fois −1, plus −1 fois 1, plus 0.5, cela donne −2.5. Aujourd'hui, nous choisissons les paramètres nous-mêmes ; en AP5, nous les ferons chercher à partir des exemples. Mais à quoi sert un score pour décider d'une classe ? Repères : cours 03, 10 min jusqu'à la consigne. Un tel score est une combinaison linéaire des coordonnées plus une constante : on dit affine, sans plus. Faire calculer un deuxième point par la salle, par ex. (1, 1) : 2 − 1 + 0.5 = 1.5.
À dire : Pour transformer un score en classe, on choisit un seuil : classe 1 si le score est positif, classe 0 sinon. Prenons les poids 1 et 0, sans biais : le score est simplement la première coordonnée. Les deux coins de droite reçoivent la classe 1, ceux de gauche la classe 0 ; comparez à la vraie classe, cela fait deux erreurs sur quatre. Les points où le score vaut exactement zéro forment une droite, ici l'axe vertical ; en changeant les poids et le biais, on fait tourner et glisser cette droite. Gardez la question pour la fin de l'atelier : existe-t-il une droite qui sépare nos deux classes ? Voyons d'abord comment écrire ce calcul pour tout un lot à la fois. Repères : ne pas donner la réponse ici, la reprise la démontre. Le carnet propose un seuil à 0.5 ; le raisonnement vaut pour n'importe quel seuil.
À dire : Pour calculer sur un lot entier, Nx propose des fonctions numériques, définies avec defn au lieu de def, après import Nx.Defn. À l'intérieur, les opérateurs +, - et * s'appliquent aux tenseurs, élément par élément, en respectant le broadcasting. Suivons les formes de cet exemple, qui calcule la distance au carré entre chaque point et un centre. x moins center diffuse le centre sur chaque ligne et garde {n, 2} ; le produit terme à terme aussi ; la somme sur l'axe 1 réduit les deux coordonnées, et keep_axes conserve cet axe avec la taille 1. Le résultat est une colonne {n, 1}, une valeur par point. Votre score suivra le même trajet de formes. Que fait Nx de cette fonction ? Repères : résultat vérifié, pour les points (1, 2), (3, 4), (0, 0) et le centre (1, 0) : [[4.0], [20.0], [1.0]]. Cette clause représentative ne donne pas le score à écrire. Hors defn, les mêmes calculs s'écrivent Nx.subtract et Nx.multiply.
À dire : Avec le compilateur d'inspection Nx.Defn.Debug, Nx ne calcule rien : il nous montre ce qu'il a retenu de la fonction. Les deux premières lignes sont les entrées, x de forme {3, 2} et le centre de forme {2}. Ensuite vient une opération par ligne, avec un nom, ses opérandes et la forme de son résultat : deux soustractions, un produit, puis la somme sur l'axe 1. Vous reconnaissez le trajet des formes que nous venons de suivre. C'est un graphe : chaque opération dépend de résultats précédents. Dans le carnet, vous lirez de la même manière le graphe de votre score. Ce graphe a des cousins que vous connaissez. Repères : sortie réelle avec Nx 1.0, pour trois points. Le texte exact et les lettres ne sont pas un contrat ; Nx ne partage pas ici les deux soustractions identiques, ne pas en tirer de règle. defn ne signifie ni GPU ni « plus rapide » : nous restons sur l'évaluateur CPU, EXLA est un compilateur possible hors du socle.
À dire : Mettons les deux côte à côte. L'AST d'AP2 conservait les additions et les appels ; le graphe de Nx conserve les opérations sur les tenseurs. Le compilateur d'AP3 transformait l'arbre en instructions ; Nx peut transformer le graphe, notamment pour le dériver, ce qui nous occupera en AP5. Enfin, notre VM exécutait les instructions ; ici, un évaluateur ou un compilateur exécute le calcul numérique. Dans les deux cas, travailler sur la représentation avant d'en demander le résultat permet de vérifier, de transformer ou de dériver. Voyons ce que le carnet 03 vous demande. Repères : le rapprochement est conceptuel, Nx ne produit pas le bytecode de notre VM d'AP3. Distinguer le graphe, qui décrit, et le tenseur résultat. Un même sous-calcul peut alimenter plusieurs nœuds d'un graphe.
À dire : Le carnet 03 vous demande une seule fonction, Score.predict, écrite avec defn. Elle reçoit une map de paramètres, avec des poids de forme {2} et un biais scalaire, et un lot de points {n, 2} ; elle rend un score par point, en colonne {n, 1}. Avant d'exécuter, faites le calcul à la main pour les deux points de la vérification. Ensuite, vous lirez le graphe de votre score avec Nx.Defn.Debug, puis vous chercherez des paramètres qui séparent les quatre coins. Une précaution : hors d'un defn, l'opérateur * d'Elixir ne connaît pas les tenseurs, et x * 2 lève une ArithmeticError ; il faut alors Nx.multiply. À vous ! Repères : message réel, « bad argument in arithmetic expression ». Une matrice à trois colonnes avec des poids de taille deux doit être refusée par Nx : cette erreur, qui plante, se compare au broadcasting trompeur, qui ne plante pas. Pas de promesse de performances sur notre petit lot en CPU BinaryBackend. Ne pas réduire defn à « aucun branchement possible » : Nx prend en charge des if et des while numériques, avec des contraintes de formes ; le score n'en a pas besoin. Pas de macros, de hooks ni de débogage avancé dans le socle.
À dire : Vingt-cinq minutes pour le carnet 03. Faites le calcul à la main avant d'écrire le code, puis comparez avec le graphe. Pour la dernière question, écrivez les quatre inégalités que doit vérifier un seuil sur nos coins ; nous les reprendrons ensemble. Repères : 25 min. Socle : score, formes, un exemple numérique, une incompatibilité expliquée. Lire le graphe suffit, aucun outil d'affichage ou de compilation supplémentaire n'est requis. Les rapides peuvent prendre le prolongement x₁·x₂, sans comparer des chronos qui mélangent compilation et exécution.
À dire : Reprenons la question de l'atelier. Pour la classe 1, il faut w1 + w2 + b > t et −w1 − w2 + b > t ; en additionnant, les poids disparaissent et il reste 2b > 2t, donc b > t. Pour la classe 0, il faut w1 − w2 + b ≤ t et −w1 + w2 + b ≤ t ; en additionnant, b ≤ t. Les deux sont incompatibles : aucune droite ne sépare ces quatre coins, quels que soient les poids, le biais et le seuil. Ce n'est donc pas une erreur de code, c'est une limite de la famille de fonctions choisie. Retenez ce résultat : en AP5, aucun réglage d'apprentissage ne pourra le contourner, et il faudra changer de modèle. Repères : reprise de 5 min. Faire écrire les inégalités par un élève au tableau. Ne pas chercher en AP5 un meilleur taux d'apprentissage pour réparer cette limite. Les points sont déterministes pour rendre le contre-exemple visible.
À dire : Pour terminer, sauvegardez vos trois carnets avec vos prédictions et vos explications. La semaine prochaine, nous ne choisirons plus les paramètres à la main : un algorithme les cherchera en suivant la pente de l'erreur, ce qu'on appelle le gradient. Puis, puisqu'une droite ne suffit pas, nous changerons de modèle. Pour le quiz d'AP5, relisez le chapitre 7 du poly sur les données, les tenseurs et le graphe de calcul. Bonne semaine ! Repères : bilan de 5 min. Annoncer oralement le travail à terminer ; aucun nouveau rendu daté n'est imposé ici. Le quiz d'AP5 porte sur la transformation des exemples, la lecture des formes et la description d'un calcul. Nx sert à apprendre données, calcul et modèles ; les concepts se retrouvent dans d'autres outils.