# AP4 · Des collections aux exemples


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

Comment préparer des exemples pour une machine qui ne connaît que des nombres ?
Nous allons classer des points du plan : la classe vaut 1 quand les deux
coordonnées ont le même signe, et 0 sinon. Sur les axes, où l'une des
coordonnées est nulle, nous choisissons 0. C'est une variante du problème XOR,
avec les classes échangées ; la difficulté est la même, et nous la retrouverons
en AP5 : aucune droite ne sépare les deux classes.

Nous connaissons volontairement la règle. Elle nous permet de contrôler les
données et de juger ce que le modèle apprendra, lui qui ne recevra que des
exemples. Dans une application réelle, les étiquettes seraient observées, pas
calculées ; ici, l'entraînement ne verra jamais la formule `x * y > 0`.

> **Repères** : [slides 5–14](https://ap-2026.noce.fr/slides/AP4.html#5).
> [Poly, chap. 3 « Collections et le pipe » et chap. 4 « Fonctions, clauses, récursion »](https://ap-2026.noce.fr/poly/poly-elixir.pdf).
> Formes Elixir de cet atelier : fonctions anonymes `fn … end`, maps `%{x: …}`, `Enum.map`, `Enum.filter`, le pipe `|>` et la compréhension `for`.

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


## Suivre une transformation

Une *fonction anonyme* est une fonction sans nom, construite par `fn x -> x * 2 end`.
Liée à un nom, elle s'appelle avec un point avant la parenthèse : `double.(3)`.
`Enum.map(liste, f)` applique `f` à chaque élément et rend la liste des
résultats ; `Enum.filter(liste, f)` garde les éléments pour lesquels `f` rend
`true`. Ni l'une ni l'autre ne modifie `values` : chacune rend une liste neuve.

Le *pipe* `|>` place la valeur de gauche comme premier argument de l'appel de
droite : `values |> Enum.filter(f)` vaut `Enum.filter(values, f)`, et le
résultat d'une étape devient le premier argument de la suivante. Il n'y a là
ni magie ni tâche concurrente, seulement une réécriture plus lisible.
Prédisez les deux valeurs du tuple rendu, puis exécutez.

```elixir
double = fn x -> x * 2 end
values = [1, 2, 3]
{double.(3), values |> Enum.filter(fn x -> x > 1 end) |> Enum.map(double)}
```

## À écrire : fabriquer la grille

Chaque point sera une *map*, une collection de clés et de valeurs :
`%{x: -0.5, y: 1.0, label: 0}`. Une valeur se lit par `p.x` ou `p.label`.
Une compréhension `for` construit une liste à partir de générateurs ; avec
deux générateurs, elle parcourt toutes les combinaisons, le second variant le
plus vite : `for x <- [1, 2], y <- [3, 4], do: {x, y}` rend
`[{1, 3}, {1, 4}, {2, 3}, {2, 4}]`. La forme `if(cond, do: a, else: b)` tient
sur une ligne et rend `a` ou `b`.

Écrivez `Samples.label/2`, qui rend la classe d'un point, puis
`Samples.grid/1`, qui reçoit la liste des coordonnées possibles et rend une
map par point, pour toutes les combinaisons. Enfin `features/1` rend, pour
chaque point, la liste `[x, y]` de ses deux coordonnées, dans l'ordre de la
grille ; `Enum.map/2` suffit. Les `raise("À écrire")` sont des bouchons à
remplacer par votre code. Aucun tenseur n'est encore nécessaire.

```elixir
defmodule Samples do
  def label(_x, _y), do: raise("À écrire")
  def grid(_coordinates), do: raise("À écrire")
  def features(_points), do: raise("À écrire")
end
```

Vérifiez, puis ajoutez un cas à vous.

```elixir
assert Samples.label(-1, -1) == 1
assert Samples.label(-1, 1) == 0
assert Samples.label(0, 1) == 0
assert Samples.features(Samples.grid([-1, 1])) == [[-1, -1], [-1, 1], [1, -1], [1, 1]]
assert Samples.grid([]) == []
```

## À écrire : compter sans modifier les exemples

Avant d'entraîner quoi que ce soit, il faut savoir ce que contiennent nos
données, et notamment si les deux classes sont présentes. `Enum.frequencies_by(liste, f)`
compte les éléments selon la valeur de `f` et rend une map : sur
`["a", "bb", "c"]` avec `&String.length/1`, elle rend `%{1 => 2, 2 => 1}`.

Écrivez `Summary.counts/1`, le nombre de points de chaque classe, puis
`Summary.positive_features/1`, les coordonnées des seuls points de classe 1,
en enchaînant un filtre, puis votre `Samples.features/1`, avec le pipe.
La seconde fonction ne sert qu'à observer une classe, pas à entraîner un
modèle. Pourquoi serait-ce un mauvais jeu d'entraînement ?

```elixir
defmodule Summary do
  def counts(_points), do: raise("À écrire")
  def positive_features(_points), do: raise("À écrire")
end
```

Vérifiez, puis ajoutez un cas à vous.

```elixir
points = Samples.grid([-1, -0.5, 0.5, 1])
assert Summary.counts(points) == %{0 => 8, 1 => 8}
assert length(Summary.positive_features(points)) == 8
assert length(points) == 16
```

## Voir ce qu'on a construit

Cette brique d'affichage est fournie : elle décrit un graphique pour la
bibliothèque VegaLite, que Livebook sait dessiner. Lisez-la comme un pipe :
on part d'un graphique vide, on lui donne nos maps comme données, on choisit
des points comme marque, puis on relie chaque clé des maps à un axe ou à une
couleur. Les noms `"x"`, `"y"` et `"label"` sont ceux de nos clés.
Exécutez la cellule, qui définit le module, puis la suivante, qui dessine.

```elixir
defmodule Plot do
  def points(rows, color) do
    VegaLite.new(width: 360, height: 300)
    |> VegaLite.data_from_values(rows)
    |> VegaLite.mark(:point, filled: true, size: 45)
    |> VegaLite.encode_field(:x, "x", type: :quantitative, scale: [domain: [-1, 1]])
    |> VegaLite.encode_field(:y, "y", type: :quantitative, scale: [domain: [-1, 1]])
    |> VegaLite.encode_field(:color, color, type: :nominal)
  end
end
```

La cellule ci-dessous affiche le graphique dans Livebook. Changez les
coordonnées de la grille, puis prédisez ce qui changera dans le dessin avant
de relancer : combien de points, et dans quels quadrants chaque couleur ?

```elixir
Plot.points(Samples.grid([-1, -0.5, 0.5, 1]), "label")
```

## Un essai qui semble raisonnable

Pour « simplifier », quelqu'un garde seulement les points de classe 1.
Un modèle qui répond toujours 1 obtiendra alors 100 % de bonnes réponses sur
ces données. Que nous apprend ce score ? Proposez un contre-exemple, c-à-d.
un point sur lequel ce modèle se trompe. Autre fuite, plus discrète : ajouter
`label` aux coordonnées. Le score deviendrait facile à améliorer, mais cette
valeur est précisément celle qu'on veut prédire !

## Prolongement : valider avant de transformer

`Float.parse/1` lit un nombre au début d'une chaîne et rend `{nombre, reste}`,
ou `:error` : `Float.parse("0.5cm")` rend `{0.5, "cm"}`. La forme `with`
enchaîne des motifs qui doivent tous réussir :
`with {:ok, a} <- etape1(x), {:ok, b} <- etape2(a) do {:ok, b} else _ -> {:error, :raison} end`.
Dès qu'un motif ne correspond pas, `with` passe dans `else` avec la valeur
refusée. Écrivez une fonction qui accepte deux coordonnées textuelles
seulement si elles sont consommées entièrement, c-à-d. si le reste est `""`,
puis rend `{:ok, %{x: …, y: …}}` ou `{:error, :coordinates}`. Testez `"0.5"`,
`"0.5cm"` et `"?"`. Ce prolongement réutilise les contrats d'AP2.

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