# AP4 · Qu'est-ce qu'une forme ?


```elixir
Mix.install([
  {:nx, "1.0.0"},
  {:axon, "0.9.0"},
  {:polaris, "0.2.0"},
  {:kino_vega_lite, "0.1.13"}
])
```


## La question de l'atelier

Un point contient deux nombres ; un lot en contient plusieurs. Comment
indiquer à Nx ce qui désigne un exemple et ce qui désigne une coordonnée ?
Nous allons traduire les collections de l'atelier précédent en tenseurs,
prédire les formes de nos calculs, puis chercher une erreur qui produit des
nombres plausibles sans provoquer la moindre exception.

> **Repères** : [slides 16–26](https://ap-2026.noce.fr/slides/AP4.html#16).
> [Poly, chap. 7 « Des tenseurs et des graphes »](https://ap-2026.noce.fr/poly/poly-elixir.pdf).
> Nx dans cet atelier : `Nx.tensor`, `Nx.shape`, `Nx.type`, `Nx.mean` avec `axes:`, `Nx.subtract`, `Nx.reshape`, `Nx.transpose` et le broadcasting ; listes et maps de l'atelier 1.

Travaillez dans Livebook : le bouton d'exécution des pages du site ne charge
pas Nx. La première cellule, ci-dessus, installe les bibliothèques avec
`Mix.install` ; elle est identique dans les six ateliers et demande Internet à
la première ouverture, le temps de télécharger et de compiler les dépendances.
Ensuite, tout reste local : les points sont fabriqués dans le carnet et les
calculs tournent sur le processeur, sans GPU.

La cellule suivante prépare deux choses. `import ExUnit.Assertions` rend
disponible `assert`, qui lève une erreur quand sa condition est fausse et ne
dit rien sinon : c'est ainsi que nous vérifierons chaque brique.
`Nx.default_backend(Nx.BinaryBackend)` choisit le *backend*, c-à-d. la brique
qui stocke les tenseurs et exécute les opérations ; celui-ci calcule en Elixir
pur, ce qui suffit à nos petits lots. Prédisez avant d'exécuter, puis ajoutez
un cas qui met votre réponse à l'épreuve. Les cellules « À écrire » doivent
être complétées avant leurs vérifications.


```elixir
import ExUnit.Assertions
Nx.default_backend(Nx.BinaryBackend)
```


## Une représentation compacte

Un *tenseur* est un tableau numérique dont tous les éléments ont le même type.
Son *rang* est le nombre d'axes ; sa *forme* est le tuple des tailles de ces
axes. Un scalaire n'a aucun axe, sa forme est `{}` ; un vecteur de quatre
nombres a la forme `{4}` ; un tableau de quatre lignes et deux colonnes, la
forme `{4, 2}`. `Nx.tensor/2` construit un tenseur à partir d'une liste, ou
d'une liste de listes, et en déduit la forme. L'option `type: :f32` demande
des flottants sur 32 bits, quatre octets par nombre, le type usuel de
l'apprentissage ; sans elle, des entiers donneraient un tenseur d'entiers.

Dans tout le cours, l'axe 0 énumère les exemples et l'axe 1 leurs
coordonnées. Le petit lot ci-dessous est fourni : il reprend les quatre coins
de notre grille et permet de commencer même si vos fonctions de préparation
ne sont pas terminées. Prédisez `Nx.shape(x)`, `Nx.type(x)` et la forme du
scalaire, puis exécutez. Livebook affiche un tenseur sous la forme
`#Nx.Tensor<f32[4][2] …>` : le type, puis la taille de chaque axe entre
crochets, puis les valeurs.

```elixir
x = Nx.tensor([[-1, -1], [-1, 1], [1, -1], [1, 1]], type: :f32)
{Nx.shape(x), Nx.type(x), Nx.shape(Nx.tensor(0.5))}
```

## À écrire : réduire le bon axe

Une *réduction* remplace tout un axe par une seule valeur : `Nx.mean(x, axes: [0])`
fait la moyenne le long de l'axe 0, c-à-d. des lignes, et il reste une valeur
par colonne. Sur une forme `{3, 2}`, le résultat a la forme `{2}`.
Que ferait `axes: [1]` sur le même tenseur, et quelle serait sa forme ?

Écrivez `TensorStats.center/1`, la moyenne des exemples pour chaque
coordonnée ; le résultat a la forme `{2}`. Écrivez ensuite `centered/1`, qui
soustrait ce centre à chaque ligne. Nx aligne pour cela la forme `{2}` sur
chaque ligne de `{3, 2}` : c'est le *broadcasting*, que nous examinerons de
près plus bas. Hors d'un `defn`, les opérations passent par des fonctions,
`Nx.subtract/2`, `Nx.add/2`, `Nx.multiply/2`, car l'opérateur `-` d'Elixir ne
sait pas soustraire deux tenseurs. Dans les vérifications, `Nx.to_list/1`
reconvertit un tenseur en listes imbriquées pour le comparer à une valeur
attendue.

```elixir
defmodule TensorStats do
  def center(_x), do: raise("À écrire")
  def centered(_x), do: raise("À écrire")
end
```

Vérifiez, puis ajoutez un cas à vous.

```elixir
sample = Nx.tensor([[1.0, 10.0], [3.0, 20.0], [5.0, 30.0]])
assert Nx.shape(TensorStats.center(sample)) == {2}
assert Nx.to_list(TensorStats.center(sample)) == [3.0, 20.0]
assert Nx.to_list(TensorStats.centered(sample)) == [[-2.0, -10.0], [0.0, 0.0], [2.0, 10.0]]
```

## Changer la forme ou permuter les axes ?

Deux opérations rendent ici une forme `{3, 2}` à partir de `{2, 3}`, et
elles ne font pas la même chose. `Nx.reshape/2` conserve l'ordre des valeurs,
lues ligne par ligne, et les range dans la nouvelle forme ; `Nx.transpose/1`
permute les axes, de sorte que chaque ancienne colonne devient une ligne.
Avant d'exécuter, écrivez les deux tableaux attendus, puis suivez la valeur
`4` dans chacun. Pourquoi un tableau carré rempli de zéros serait-il un
mauvais exemple pour vérifier la différence ?

```elixir
a = Nx.tensor([[1, 2, 3], [4, 5, 6]])
{Nx.to_list(Nx.reshape(a, {3, 2})), Nx.to_list(Nx.transpose(a))}
```

## À écrire : une erreur moyenne par exemple

Pour mesurer l'écart entre des prédictions et les réponses attendues, nous
prendrons la moyenne des carrés des différences, ligne par ligne : c'est
l'*erreur quadratique moyenne*, abrégée MSE, que nous dériverons en AP5.
`predictions` et `labels` doivent avoir la même forme `{n, 1}`, une valeur
par exemple. Écrivez `Error.mse/2` avec `Nx.subtract`, `Nx.pow` (chaque
élément à la puissance donnée) et `Nx.mean` sans option `axes:`, qui réduit
tous les axes : le résultat est un scalaire, de forme `{}`.

Ajoutez en tête de fonction un contrôle qui refuse des formes différentes
avec `raise ArgumentError, "Formes différentes"`. Un `if` Elixir ordinaire
convient, puisque cette fonction est un `def` et non un `defn` ; nous verrons
`defn` au prochain atelier. Dans les vérifications, `assert_in_delta a, b, ε`
accepte un écart inférieur à `ε`, indispensable avec des flottants ;
`assert_raise` vérifie qu'une erreur est bien levée ; `Nx.to_number/1`
convertit un tenseur scalaire en nombre Elixir.

```elixir
defmodule Error do
  def mse(_predictions, _labels), do: raise("À écrire")
end
```

Vérifiez, puis ajoutez un cas à vous.

```elixir
predictions = Nx.tensor([[0.0], [1.0], [1.0]])
labels = Nx.tensor([[0.0], [0.0], [1.0]])
assert Nx.shape(Error.mse(predictions, labels)) == {}
assert_in_delta Nx.to_number(Error.mse(predictions, labels)), 1 / 3, 1.0e-6
assert_raise ArgumentError, fn -> Error.mse(predictions, Nx.reshape(labels, {3})) end
```

## L'essai qui donne un résultat faux sans erreur

Pourquoi ce contrôle de formes ? Parce que Nx aligne les formes à partir de
la droite et qu'un axe de taille 1, ou absent, peut être étendu pour
s'accorder à l'autre tenseur : c'est le *broadcasting*. Il nous a servi pour
le centrage. Ici, il se retourne contre nous : `{3, 1}` et `{3}` s'alignent
comme `{3, 1}` et `{1, 3}`, et le résultat a la forme `{3, 3}`. Il ne s'agit
plus de comparer chaque prédiction à son étiquette, mais chaque prédiction à
toutes les étiquettes, sans qu'aucune exception ne nous prévienne.

Prédisez la forme et le sens de chaque case ci-dessous, puis calculez la
moyenne des carrés de `wrong` et comparez-la au tiers obtenu plus haut.
Ajouter des parenthèses ne corrige rien : il faut rendre les formes
cohérentes, par ex. avec `Nx.reshape(labels, {3, 1})`.

```elixir
wrong = Nx.subtract(predictions, Nx.reshape(labels, {3}))
{Nx.shape(wrong), Nx.to_list(wrong)}
```

## Prolongement : des invariants plutôt qu'un exemple

Un bon contrôle ne se contente pas d'un exemple : il vérifie une propriété.
`Nx.all_close(a, b)` rend `1` quand deux tenseurs sont égaux à une tolérance
près, ce qui convient aux flottants. Vérifiez que centrer deux fois donne le
même résultat que centrer une fois. Trouvez ensuite une forme pour laquelle
une soustraction échoue immédiatement, puis une autre où elle réussit avec un
sens erroné. Expliquez pourquoi le nombre total d'éléments ne suffit pas à
garantir un contrat.

## Si ce centrage servait à apprendre

Notre centrage est ici un exercice sur les axes. Dans un apprentissage, le
centre se calcule sur les données d'entraînement, puis la même valeur est
soustraite aux nouveaux points. Recalculer le centre séparément sur le test
changerait la transformation et utiliserait des informations de ce lot
réservé. En AP5, nous garderons directement les coordonnées de notre grille,
sans prétraitement.

## Avant de reprendre les slides

Gardez votre code, une prédiction comparée au résultat et un souci expliqué
avec vos mots. Sauvegardez dans `AP4/`. Je préciserai en séance les
étapes à terminer et les modalités du rendu ; les prolongements restent
facultatifs tant qu'ils ne sont pas demandés.

Le [poly](https://ap-2026.noce.fr/poly/poly-elixir.pdf) se consulte quand un bout de syntaxe vous manque : la forme
d'une fonction anonyme, d'un pipe ou d'un motif aux chapitres 1 à 4, Nx et
Axon aux chapitres 7 et 8.

Pour toute question : [aurelien.noce@minesparis.psl.eu](mailto:aurelien.noce@minesparis.psl.eu).
