# AP4 · Notre formule devient un graphe


```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

En AP2, nous représentions une expression par un arbre ; en AP3, nous la
compilions pour une machine. Nx construit aussi une représentation du calcul
avant de l'exécuter, et c'est elle qui lui permettra, en AP5, de dériver nos
formules. Nous allons écrire le score d'un point, `w₁ x₁ + w₂ x₂ + b`, pour
tout un lot à la fois, puis regarder le graphe que Nx en construit.

> **Repères** : [slides 27–34](https://ap-2026.noce.fr/slides/AP4.html#27).
> [Poly, chap. 7 « Des tenseurs et des graphes »](https://ap-2026.noce.fr/poly/poly-elixir.pdf).
> AP2 : AST · AP3 : compilation · Nx.Defn dans cet atelier : `import Nx.Defn`, `defn`, les opérateurs sur tenseurs, `Nx.sum` avec `axes:` et `keep_axes:`, une map de paramètres, `Nx.Defn.jit`.

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


## Même calcul, autre langage dans la fonction

`import Nx.Defn` rend disponible le mot-clé `defn`, qui définit une *fonction
numérique*. Elle s'écrit comme un `def`, mais son corps est confié à Nx : les
opérateurs ordinaires `+`, `-`, `*` y deviennent des opérations sur tenseurs,
appliquées élément par élément, et Nx en conserve le graphe. À l'extérieur
d'un `defn`, le même calcul s'écrit `Nx.multiply(x, x)`. Les listes, le
dessin et la préparation des exemples restent dans des fonctions Elixir
ordinaires. Prédisez le résultat de l'appel ci-dessous avant d'exécuter ;
l'`assert` ne dit rien s'il est satisfait.

```elixir
defmodule Formula do
  import Nx.Defn
  defn square(x), do: x * x
end
assert Nx.to_list(Formula.square(Nx.tensor([2.0, 3.0]))) == [4.0, 9.0]
```

## À écrire : un score par ligne

Écrivez `Score.predict/2` dans un `defn`. `params` est une map qui contient
`weights`, de forme `{2}`, et `bias`, un scalaire ; `x` a la forme `{n, 2}`.
À l'intérieur du `defn`, `params.weights` lit la clé de la map comme en
Elixir ordinaire. Le résultat doit avoir la forme `{n, 1}`, un score par
exemple. Suivez les formes : le produit terme à terme `x * params.weights`
diffuse les deux poids sur chaque ligne et garde `{n, 2}` ; une somme sur
l'axe 1 réduit les deux coordonnées en une valeur par ligne ; l'option
`keep_axes: true` conserve cet axe avec la taille 1, d'où `{n, 1}` au lieu de
`{n}` ; enfin l'ajout du biais scalaire ne change pas la forme.

Avant d'exécuter, faites le calcul à la main pour les poids `[2.0, -1.0]`, le
biais `0.5` et les deux points de la vérification. Pourquoi réduire l'axe 0
plutôt que l'axe 1 changerait-il le problème posé ?

```elixir
defmodule Score do
  import Nx.Defn
  defn predict(_params, _x), do: raise("À écrire")
end
```

Vérifiez, puis ajoutez un cas à vous.

```elixir
params = %{weights: Nx.tensor([2.0, -1.0]), bias: Nx.tensor(0.5)}
x = Nx.tensor([[1.0, 2.0], [3.0, 4.0]])
assert Nx.shape(Score.predict(params, x)) == {2, 1}
assert Nx.to_list(Score.predict(params, x)) == [[0.5], [2.5]]
assert Nx.shape(Score.predict(params, Nx.tensor([[0.0, 0.0]]))) == {1, 1}
```

## Lire la représentation avant de calculer

`Nx.Defn.jit/2` prépare une fonction numérique pour un compilateur donné.
Avec le compilateur d'inspection `Nx.Defn.Debug`, l'appel ne calcule rien :
il rend le *graphe* du calcul, c-à-d. ses opérations et leurs dépendances.
Livebook l'affiche ligne par ligne : d'abord les entrées, `parameter a:2 f32[2][2]`
pour `x`, puis une opération par ligne, chacune avec un nom, ses
opérandes et la forme de son résultat, par ex. `c = multiply a, b` puis
`d = sum c, axes: [1], keep_axes: true`. Retrouvez les entrées, le produit,
la réduction et l'addition, et vérifiez les formes intermédiaires que vous
aviez prédites. Le texte exact et les lettres ne constituent pas un contrat ;
les dépendances nous intéressent.

Pour calculer, nous utilisons l'évaluateur CPU de Nx. `defn` ne signifie donc
pas automatiquement « GPU » ni « plus rapide » : EXLA est un compilateur
possible pour ce même graphe, hors du socle de cette séance.

```elixir
graph = Nx.Defn.jit(&Score.predict/2, compiler: Nx.Defn.Debug)
graph.(params, x)
```

## Une limite qu'on peut démontrer

Sur les quatre points `(−1,−1)`, `(−1,1)`, `(1,−1)`, `(1,1)`, les classes
sont respectivement 1, 0, 0, 1. Supposons qu'un score affine décide la classe
1 lorsqu'il dépasse un seuil, par ex. `0.5`, et la classe 0 sinon. Écrivez
les quatre inégalités que ce choix impose aux poids et au biais. Additionnez
celles de la classe 1, puis celles de la classe 0. Pouvez-vous les satisfaire
simultanément ? Pourquoi davantage d'entraînement ne changera-t-il pas cette
réponse en AP5 ?

## Prolongement : choisir une représentation

Ajoutez une troisième coordonnée `x₁*x₂` à chaque point. Une combinaison
linéaire des trois coordonnées peut-elle désormais résoudre le problème ?
Nous avons introduit une information utile grâce à notre connaissance de la
règle. En AP5, une couche cachée cherchera une transformation utile à partir
des seuls exemples.

## Un souci de frontières entre les langages

Essayez `x * 2` dans une cellule ordinaire, hors de tout `defn`, et lisez
l'erreur : `bad argument in arithmetic expression`, car l'opérateur `*`
d'Elixir ne connaît pas les tenseurs. Réparez avec `Nx.multiply/2`. Ne
concluez pas pour autant que `defn` interdit tout contrôle : il possède ses
propres `if` et `while` numériques, avec des contraintes de formes. Nous
n'en avons pas besoin pour ce score.

## 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).
