yaxlib 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. yaxlib-0.1.0/.gitignore +77 -0
  2. yaxlib-0.1.0/PKG-INFO +72 -0
  3. yaxlib-0.1.0/README.md +56 -0
  4. yaxlib-0.1.0/demos/cnn_demo_classif.py +123 -0
  5. yaxlib-0.1.0/demos/gnn_demo_nodes.py +134 -0
  6. yaxlib-0.1.0/demos/mlp_demo.py +82 -0
  7. yaxlib-0.1.0/demos/rnn_demo_classif.py +110 -0
  8. yaxlib-0.1.0/demos/transformer_demo_charlm.py +143 -0
  9. yaxlib-0.1.0/demos/unet_demo_segmentation.py +117 -0
  10. yaxlib-0.1.0/demos/yolo_demo_detection.py +131 -0
  11. yaxlib-0.1.0/out/charlm/0/best_loss +0 -0
  12. yaxlib-0.1.0/out/charlm/0/config +0 -0
  13. yaxlib-0.1.0/out/charlm/0/history +0 -0
  14. yaxlib-0.1.0/out/charlm/0/model +0 -0
  15. yaxlib-0.1.0/out/charlm/0/opt_state +0 -0
  16. yaxlib-0.1.0/out/charlm/1/best_loss +0 -0
  17. yaxlib-0.1.0/out/charlm/1/config +0 -0
  18. yaxlib-0.1.0/out/charlm/1/history +0 -0
  19. yaxlib-0.1.0/out/charlm/1/model +0 -0
  20. yaxlib-0.1.0/out/charlm/1/opt_state +0 -0
  21. yaxlib-0.1.0/out/cnn_classif/0/best_loss +0 -0
  22. yaxlib-0.1.0/out/cnn_classif/0/config +0 -0
  23. yaxlib-0.1.0/out/cnn_classif/0/history +0 -0
  24. yaxlib-0.1.0/out/cnn_classif/0/model +0 -0
  25. yaxlib-0.1.0/out/cnn_classif/0/opt_state +0 -0
  26. yaxlib-0.1.0/out/cnn_classif/1/best_loss +0 -0
  27. yaxlib-0.1.0/out/cnn_classif/1/config +0 -0
  28. yaxlib-0.1.0/out/cnn_classif/1/history +0 -0
  29. yaxlib-0.1.0/out/cnn_classif/1/model +0 -0
  30. yaxlib-0.1.0/out/cnn_classif/1/opt_state +0 -0
  31. yaxlib-0.1.0/out/cnn_classif/2/best_loss +0 -0
  32. yaxlib-0.1.0/out/cnn_classif/2/config +0 -0
  33. yaxlib-0.1.0/out/cnn_classif/2/history +0 -0
  34. yaxlib-0.1.0/out/cnn_classif/2/model +0 -0
  35. yaxlib-0.1.0/out/cnn_classif/2/opt_state +0 -0
  36. yaxlib-0.1.0/out/rnn_classif_gru/0/best_loss +0 -0
  37. yaxlib-0.1.0/out/rnn_classif_gru/0/config +0 -0
  38. yaxlib-0.1.0/out/rnn_classif_gru/0/history +0 -0
  39. yaxlib-0.1.0/out/rnn_classif_gru/0/model +0 -0
  40. yaxlib-0.1.0/out/rnn_classif_gru/0/opt_state +0 -0
  41. yaxlib-0.1.0/out/rnn_classif_gru/1/best_loss +0 -0
  42. yaxlib-0.1.0/out/rnn_classif_gru/1/config +0 -0
  43. yaxlib-0.1.0/out/rnn_classif_gru/1/history +0 -0
  44. yaxlib-0.1.0/out/rnn_classif_gru/1/model +0 -0
  45. yaxlib-0.1.0/out/rnn_classif_gru/1/opt_state +0 -0
  46. yaxlib-0.1.0/out/rnn_classif_gru/2/best_loss +0 -0
  47. yaxlib-0.1.0/out/rnn_classif_gru/2/config +0 -0
  48. yaxlib-0.1.0/out/rnn_classif_gru/2/history +0 -0
  49. yaxlib-0.1.0/out/rnn_classif_gru/2/model +0 -0
  50. yaxlib-0.1.0/out/rnn_classif_gru/2/opt_state +0 -0
  51. yaxlib-0.1.0/out/rnn_classif_lstm/0/best_loss +0 -0
  52. yaxlib-0.1.0/out/rnn_classif_lstm/0/config +0 -0
  53. yaxlib-0.1.0/out/rnn_classif_lstm/0/history +0 -0
  54. yaxlib-0.1.0/out/rnn_classif_lstm/0/model +0 -0
  55. yaxlib-0.1.0/out/rnn_classif_lstm/0/opt_state +0 -0
  56. yaxlib-0.1.0/out/rnn_classif_lstm/1/best_loss +0 -0
  57. yaxlib-0.1.0/out/rnn_classif_lstm/1/config +0 -0
  58. yaxlib-0.1.0/out/rnn_classif_lstm/1/history +0 -0
  59. yaxlib-0.1.0/out/rnn_classif_lstm/1/model +0 -0
  60. yaxlib-0.1.0/out/rnn_classif_lstm/1/opt_state +0 -0
  61. yaxlib-0.1.0/out/sin_3/0/best_loss +0 -0
  62. yaxlib-0.1.0/out/sin_3/0/config +0 -0
  63. yaxlib-0.1.0/out/sin_3/0/history +0 -0
  64. yaxlib-0.1.0/out/sin_3/0/model +0 -0
  65. yaxlib-0.1.0/out/sin_3/0/opt_state +0 -0
  66. yaxlib-0.1.0/out/sin_3/1/best_loss +0 -0
  67. yaxlib-0.1.0/out/sin_3/1/config +0 -0
  68. yaxlib-0.1.0/out/sin_3/1/history +0 -0
  69. yaxlib-0.1.0/out/sin_3/1/model +0 -0
  70. yaxlib-0.1.0/out/sin_3/1/opt_state +0 -0
  71. yaxlib-0.1.0/out/unet_seg/0/best_loss +0 -0
  72. yaxlib-0.1.0/out/unet_seg/0/config +0 -0
  73. yaxlib-0.1.0/out/unet_seg/0/history +0 -0
  74. yaxlib-0.1.0/out/unet_seg/0/model +0 -0
  75. yaxlib-0.1.0/out/unet_seg/0/opt_state +0 -0
  76. yaxlib-0.1.0/out/unet_seg/1/best_loss +0 -0
  77. yaxlib-0.1.0/out/unet_seg/1/config +0 -0
  78. yaxlib-0.1.0/out/unet_seg/1/history +0 -0
  79. yaxlib-0.1.0/out/unet_seg/1/model +0 -0
  80. yaxlib-0.1.0/out/unet_seg/1/opt_state +0 -0
  81. yaxlib-0.1.0/out/unet_seg/2/best_loss +0 -0
  82. yaxlib-0.1.0/out/unet_seg/2/config +0 -0
  83. yaxlib-0.1.0/out/unet_seg/2/history +0 -0
  84. yaxlib-0.1.0/out/unet_seg/2/model +0 -0
  85. yaxlib-0.1.0/out/unet_seg/2/opt_state +0 -0
  86. yaxlib-0.1.0/out/yolo_detection/0/best_loss +0 -0
  87. yaxlib-0.1.0/out/yolo_detection/0/config +0 -0
  88. yaxlib-0.1.0/out/yolo_detection/0/history +0 -0
  89. yaxlib-0.1.0/out/yolo_detection/0/model +0 -0
  90. yaxlib-0.1.0/out/yolo_detection/0/opt_state +0 -0
  91. yaxlib-0.1.0/out/yolo_detection/1/best_loss +0 -0
  92. yaxlib-0.1.0/out/yolo_detection/1/config +0 -0
  93. yaxlib-0.1.0/out/yolo_detection/1/history +0 -0
  94. yaxlib-0.1.0/out/yolo_detection/1/model +0 -0
  95. yaxlib-0.1.0/out/yolo_detection/1/opt_state +0 -0
  96. yaxlib-0.1.0/out/yolo_detection/2/best_loss +0 -0
  97. yaxlib-0.1.0/out/yolo_detection/2/config +0 -0
  98. yaxlib-0.1.0/out/yolo_detection/2/history +0 -0
  99. yaxlib-0.1.0/out/yolo_detection/2/model +0 -0
  100. yaxlib-0.1.0/out/yolo_detection/2/opt_state +0 -0
  101. yaxlib-0.1.0/out/yolo_detection/3/best_loss +0 -0
  102. yaxlib-0.1.0/out/yolo_detection/3/config +0 -0
  103. yaxlib-0.1.0/out/yolo_detection/3/history +0 -0
  104. yaxlib-0.1.0/out/yolo_detection/3/model +0 -0
  105. yaxlib-0.1.0/out/yolo_detection/3/opt_state +0 -0
  106. yaxlib-0.1.0/out/yolo_detection/4/best_loss +0 -0
  107. yaxlib-0.1.0/out/yolo_detection/4/config +0 -0
  108. yaxlib-0.1.0/out/yolo_detection/4/history +0 -0
  109. yaxlib-0.1.0/out/yolo_detection/4/model +0 -0
  110. yaxlib-0.1.0/out/yolo_detection/4/opt_state +0 -0
  111. yaxlib-0.1.0/programme_detaille.md +1359 -0
  112. yaxlib-0.1.0/pyproject.toml +27 -0
  113. yaxlib-0.1.0/tests/test_core.py +219 -0
  114. yaxlib-0.1.0/tests/test_layers.py +86 -0
  115. yaxlib-0.1.0/tests/test_training.py +69 -0
  116. yaxlib-0.1.0/yax/__init__.py +28 -0
  117. yaxlib-0.1.0/yax/core.py +346 -0
  118. yaxlib-0.1.0/yax/image/__init__.py +0 -0
  119. yaxlib-0.1.0/yax/image/augmentation.py +123 -0
  120. yaxlib-0.1.0/yax/layers/Conv_layer.py +70 -0
  121. yaxlib-0.1.0/yax/layers/Dropout.py +63 -0
  122. yaxlib-0.1.0/yax/layers/Embedding.py +29 -0
  123. yaxlib-0.1.0/yax/layers/LayerNorm.py +42 -0
  124. yaxlib-0.1.0/yax/layers/Linear.py +20 -0
  125. yaxlib-0.1.0/yax/layers/MLP.py +62 -0
  126. yaxlib-0.1.0/yax/layers/MessagePassing_layer.py +80 -0
  127. yaxlib-0.1.0/yax/layers/MultiHeadAttention.py +127 -0
  128. yaxlib-0.1.0/yax/layers/RNN_layer.py +156 -0
  129. yaxlib-0.1.0/yax/layers/TransformerBlock.py +78 -0
  130. yaxlib-0.1.0/yax/layers/__init__.py +0 -0
  131. yaxlib-0.1.0/yax/layers/positional_encoding.py +37 -0
  132. yaxlib-0.1.0/yax/models/MiniYOLO.py +200 -0
  133. yaxlib-0.1.0/yax/models/UNet.py +107 -0
  134. yaxlib-0.1.0/yax/models/__init__.py +0 -0
  135. yaxlib-0.1.0/yax/training/History.py +52 -0
  136. yaxlib-0.1.0/yax/training/Trainer.py +240 -0
  137. yaxlib-0.1.0/yax/training/__init__.py +0 -0
  138. yaxlib-0.1.0/yax/training/configs.py +20 -0
  139. yaxlib-0.1.0/yax/training/losses.py +77 -0
@@ -0,0 +1,77 @@
1
+ # ===== Environnements virtuels =====
2
+ .venv/
3
+ venv/
4
+ env/
5
+ ENV/
6
+ .conda/
7
+ .python-version
8
+
9
+ # ===== Artefacts de build / cache Python =====
10
+ __pycache__/
11
+ *.py[cod]
12
+ *$py.class
13
+ build/
14
+ dist/
15
+ *.egg
16
+ *.egg-info/
17
+ .eggs/
18
+ .pytest_cache/
19
+ .mypy_cache/
20
+ .ruff_cache/
21
+ .ipynb_checkpoints/
22
+
23
+ # ===== Node / JS (au cas où) =====
24
+ node_modules/
25
+
26
+ # ===== Éditeurs & assistants IA =====
27
+ .idea/
28
+ .vscode/
29
+ .claude/
30
+ .gemini/
31
+ .agent/
32
+ .agents/
33
+ *.swp
34
+ *~
35
+ .DS_Store
36
+
37
+ # ===== Archives =====
38
+ *.zip
39
+
40
+ # ===== Médias lourds — sauvegardés par Drive, hors git =====
41
+ *.mp4
42
+ *.mov
43
+ *.webm
44
+ *.mkv
45
+ *.avi
46
+ *.mp3
47
+ *.wav
48
+ *.flac
49
+ *.m4a
50
+ *.ogg
51
+ # (images gardées dans git)
52
+
53
+ # ===== Données scientifiques lourdes — sauvegardées par Drive =====
54
+ *.npy
55
+ *.npz
56
+ # Tables EOS supernova (données CompOSE; attention: pas 'eos.*' qui
57
+ # ignorerait aussi les eos.py du code)
58
+ **/eos.thermo
59
+ **/eos.compo
60
+ **/eos.micro
61
+ **/eos.t
62
+ **/eos.nb
63
+ **/eos.yq
64
+ # (tout autre fichier > 50 Mo est bloqué par le hook pre-commit)
65
+
66
+ # ===== Fichiers-pointeurs Google Drive (liens, pas du vrai contenu) =====
67
+ *.gdoc
68
+ *.gsheet
69
+ *.gslides
70
+ *.gdraw
71
+ *.gform
72
+ *.gmap
73
+ *.gsite
74
+ *.gtable
75
+ *.glink
76
+ *.gjam
77
+ *.gnote
yaxlib-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.5
2
+ Name: yaxlib
3
+ Version: 0.1.0
4
+ Summary: Un mini-framework de réseaux de neurones pour jax, à but pédagogique (import : yax)
5
+ Author: Vincent Vigon
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: jax>=0.4.30
9
+ Requires-Dist: numpy
10
+ Requires-Dist: optax>=0.2
11
+ Provides-Extra: dev
12
+ Requires-Dist: equinox; extra == 'dev'
13
+ Requires-Dist: matplotlib; extra == 'dev'
14
+ Requires-Dist: pytest; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # yaxlib
18
+
19
+ Un mini-framework de réseaux de neurones pour jax, à but pédagogique.
20
+ Distribution `yaxlib`, import `yax` :
21
+
22
+ ```bash
23
+ pip install yaxlib # ou : pip install <url du wheel>
24
+ ```
25
+
26
+ ```python
27
+ import jax.random as jr
28
+ import yax
29
+
30
+ model = yax.MLP((2, 32, 32, 1), "tanh", jr.key(0))
31
+ ```
32
+
33
+ ## Principes
34
+
35
+ - **Un modèle est un pytree.** `yax.Module` range les tableaux dans les
36
+ feuilles et tout le reste (`yax.StaticField`) dans la structure :
37
+ `jax.grad(loss)(model)`, `jax.jit` et `optimizer.init(model)` acceptent le
38
+ modèle tel quel, sans machinerie de filtrage. Les champs dynamiques ne
39
+ peuvent contenir que des tableaux jax, des sous-modules ou des conteneurs de
40
+ ceux-ci — tout écart est une erreur immédiate et explicite à la
41
+ construction. Un `StaticField` peut contenir un tableau : il devient une
42
+ constante du modèle (encodage positionnel, grille figée), invisible pour les
43
+ gradients.
44
+ - **Signature uniforme `apply(x, rkey=None)`**, écrite pour UN échantillon
45
+ (le batch vient de `jax.vmap`). `rkey` est une *source d'aléatoire*
46
+ (dropout, échantillonnage), jamais un mode.
47
+ - **Le mode se bascule par `model = model.set_inference(True/False)`**
48
+ (récursif, immuable). En pratique on n'y touche pas : le `Trainer` entraîne
49
+ en `False`, valide et rend le meilleur modèle en `True`.
50
+ - **Immutabilité** : on « modifie » un module avec `yax.tree_at`.
51
+
52
+ ## Contenu
53
+
54
+ - `yax.layers` : Linear, MLP, Dropout, LayerNorm, Embedding, Conv_layer,
55
+ RNN_layer (GRU/LSTM), MultiHeadAttention, TransformerBlock,
56
+ MessagePassing_layer, encodage positionnel.
57
+ - `yax.models` : UNet, MiniYOLO (références des mini-projets).
58
+ - `yax.training` : Trainer (checkpoints par `mother_folder`), History, pertes
59
+ (`loss_fn(model, x, y, rkey)`).
60
+ - `yax.image` : augmentation différentiable et vmap-able.
61
+ - `demos/` : une démonstration synthétique par famille de modèles, qui
62
+ converge en quelques secondes sur CPU.
63
+
64
+ ## Tests
65
+
66
+ ```bash
67
+ pip install yaxlib[dev]
68
+ pytest tests/
69
+ ```
70
+
71
+ Les couches réimplémentées (GRU, LSTM, convolution) sont vérifiées
72
+ numériquement contre `equinox.nn`, qui ne sert qu'à cela.
yaxlib-0.1.0/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # yaxlib
2
+
3
+ Un mini-framework de réseaux de neurones pour jax, à but pédagogique.
4
+ Distribution `yaxlib`, import `yax` :
5
+
6
+ ```bash
7
+ pip install yaxlib # ou : pip install <url du wheel>
8
+ ```
9
+
10
+ ```python
11
+ import jax.random as jr
12
+ import yax
13
+
14
+ model = yax.MLP((2, 32, 32, 1), "tanh", jr.key(0))
15
+ ```
16
+
17
+ ## Principes
18
+
19
+ - **Un modèle est un pytree.** `yax.Module` range les tableaux dans les
20
+ feuilles et tout le reste (`yax.StaticField`) dans la structure :
21
+ `jax.grad(loss)(model)`, `jax.jit` et `optimizer.init(model)` acceptent le
22
+ modèle tel quel, sans machinerie de filtrage. Les champs dynamiques ne
23
+ peuvent contenir que des tableaux jax, des sous-modules ou des conteneurs de
24
+ ceux-ci — tout écart est une erreur immédiate et explicite à la
25
+ construction. Un `StaticField` peut contenir un tableau : il devient une
26
+ constante du modèle (encodage positionnel, grille figée), invisible pour les
27
+ gradients.
28
+ - **Signature uniforme `apply(x, rkey=None)`**, écrite pour UN échantillon
29
+ (le batch vient de `jax.vmap`). `rkey` est une *source d'aléatoire*
30
+ (dropout, échantillonnage), jamais un mode.
31
+ - **Le mode se bascule par `model = model.set_inference(True/False)`**
32
+ (récursif, immuable). En pratique on n'y touche pas : le `Trainer` entraîne
33
+ en `False`, valide et rend le meilleur modèle en `True`.
34
+ - **Immutabilité** : on « modifie » un module avec `yax.tree_at`.
35
+
36
+ ## Contenu
37
+
38
+ - `yax.layers` : Linear, MLP, Dropout, LayerNorm, Embedding, Conv_layer,
39
+ RNN_layer (GRU/LSTM), MultiHeadAttention, TransformerBlock,
40
+ MessagePassing_layer, encodage positionnel.
41
+ - `yax.models` : UNet, MiniYOLO (références des mini-projets).
42
+ - `yax.training` : Trainer (checkpoints par `mother_folder`), History, pertes
43
+ (`loss_fn(model, x, y, rkey)`).
44
+ - `yax.image` : augmentation différentiable et vmap-able.
45
+ - `demos/` : une démonstration synthétique par famille de modèles, qui
46
+ converge en quelques secondes sur CPU.
47
+
48
+ ## Tests
49
+
50
+ ```bash
51
+ pip install yaxlib[dev]
52
+ pytest tests/
53
+ ```
54
+
55
+ Les couches réimplémentées (GRU, LSTM, convolution) sont vérifiées
56
+ numériquement contre `equinox.nn`, qui ne sert qu'à cela.
@@ -0,0 +1,123 @@
1
+ """Classification d'images synthétiques par un petit CNN.
2
+
3
+ Trois classes de formes — disque, carré, croix — posées à position et taille
4
+ aléatoires sur un fond bruité. Le jeu se génère en une ligne de vmap, converge
5
+ en quelques secondes, et l'invariance par translation de la convolution y est
6
+ réellement mise à l'épreuve (la position varie d'une image à l'autre).
7
+
8
+ Le sous-échantillonnage est fait par stride=2 (l'alternative au pooling,
9
+ chapitre 11) : 16x16 -> 8x8 -> 4x4, puis tête dense sur l'aplati.
10
+ """
11
+
12
+ import os
13
+
14
+ from yax.core import Module
15
+ import jax
16
+ import jax.numpy as jnp
17
+ import jax.random as jr
18
+
19
+ from yax.layers.Conv_layer import Conv_layer
20
+ from yax.layers.Linear import Linear
21
+ from yax.training.Trainer import train, load_run, OUT_FOLDER
22
+ from yax.training.configs import TrainConfig
23
+ from yax.training.losses import softmax_ce_loss, batch_apply
24
+
25
+ IMG_SIZE = 16
26
+ NB_CLASSES = 3 # 0: disque, 1: carré, 2: croix
27
+
28
+
29
+ def make_image(rkey, label):
30
+ """UNE image (1, 16, 16) : une forme `label` sur fond bruité.
31
+
32
+ Les rayons restent assez grands (4.5 à 5.5) pour qu'un disque et un carré
33
+ diffèrent de plusieurs pixels aux coins : avec des formes plus petites,
34
+ seuls les coins les distinguent, la tâche devient réellement ambiguë sous
35
+ le bruit et la confusion disque/carré plafonne la justesse."""
36
+ k_pos, k_radius, k_noise = jr.split(rkey, 3)
37
+ center = jr.uniform(k_pos, (2,), minval=6.0, maxval=IMG_SIZE - 6.0)
38
+ radius = jr.uniform(k_radius, minval=4.5, maxval=5.5)
39
+
40
+ rows = jnp.arange(IMG_SIZE)[:, None]
41
+ cols = jnp.arange(IMG_SIZE)[None, :]
42
+ dr, dc = rows - center[0], cols - center[1]
43
+
44
+ disque = dr**2 + dc**2 < radius**2
45
+ carre = jnp.maximum(jnp.abs(dr), jnp.abs(dc)) < radius
46
+ croix = ((jnp.abs(dr) < 1.0) | (jnp.abs(dc) < 1.0)) & (jnp.abs(dr) + jnp.abs(dc) < 2 * radius)
47
+
48
+ shape = jnp.stack([disque, carre, croix])[label]
49
+ img = jnp.where(shape, 1.0, 0.0) + 0.25 * jr.normal(k_noise, (IMG_SIZE, IMG_SIZE))
50
+ return img[None, :, :] # channels-first : (1, H, W)
51
+
52
+
53
+ def make_data(rkey, nb):
54
+ k_labels, k_images = jr.split(rkey)
55
+ labels = jr.randint(k_labels, (nb,), 0, NB_CLASSES)
56
+ X = jax.vmap(make_image)(jr.split(k_images, nb), labels)
57
+ return X, labels
58
+
59
+
60
+ def present_data():
61
+ from matplotlib import pyplot as plt
62
+
63
+ X, Y = make_data(jr.key(0), 12)
64
+ fig, axs = plt.subplots(3, 4, figsize=(8, 6))
65
+ for ax, img, label in zip(axs.flat, X, Y):
66
+ ax.imshow(img[0], cmap="gray")
67
+ ax.set_title(["disque", "carré", "croix"][int(label)])
68
+ ax.axis("off")
69
+ plt.tight_layout()
70
+ plt.show()
71
+
72
+
73
+ class CNN_classifier(Module):
74
+ conv1: Conv_layer
75
+ conv2: Conv_layer
76
+ head: Linear
77
+
78
+ def __init__(self, rkey):
79
+ k1, k2, k3 = jr.split(rkey, 3)
80
+ self.conv1 = Conv_layer(1, 8, 3, k1, stride=2) # (1,16,16) -> (8,8,8)
81
+ self.conv2 = Conv_layer(8, 16, 3, k2, stride=2) # -> (16,4,4)
82
+ self.head = Linear(16 * 4 * 4, NB_CLASSES, k3)
83
+
84
+ def apply(self, x, rkey=None):
85
+ x = jax.nn.relu(self.conv1.apply(x))
86
+ x = jax.nn.relu(self.conv2.apply(x))
87
+ return self.head.apply(x.reshape(-1)) # logits (NB_CLASSES,)
88
+
89
+
90
+ def accuracy(model, x, y):
91
+ logits = batch_apply(model, x, None)
92
+ return float(jnp.mean(jnp.argmax(logits, axis=-1) == y))
93
+
94
+
95
+ def train_classif(verbose=True):
96
+ key_train, key_val, key_model, key_shuffle = jr.split(jr.key(0), 4)
97
+ X_train, Y_train = make_data(key_train, 1024)
98
+ X_val, Y_val = make_data(key_val, 512)
99
+
100
+ model = CNN_classifier(key_model)
101
+ acc_before = accuracy(model, X_val, Y_val)
102
+
103
+ config = TrainConfig(batch_size=64, learning_rate=0.003, n_epoch=100,
104
+ lr_final_ratio=0.01)
105
+ mother_folder = os.path.join(OUT_FOLDER, "cnn_classif")
106
+ best_model, best_loss, folder = train(
107
+ model, mother_folder, config, X_train, Y_train, X_val, Y_val,
108
+ rkey=key_shuffle, loss_fn=softmax_ce_loss, verbose=verbose)
109
+
110
+ acc_after = accuracy(best_model, X_val, Y_val)
111
+ print(f"\naccuracy {acc_before:.3f} -> {acc_after:.3f} (best val CE = {best_loss:.3g})")
112
+ return folder, best_loss, acc_after
113
+
114
+
115
+ if __name__ == "__main__":
116
+ from matplotlib import pyplot as plt
117
+
118
+ present_data()
119
+ folder, best_loss, acc = train_classif()
120
+ history = load_run(folder, "history")["history"]
121
+ history.plot(title=f"CNN formes : CE={best_loss:.2g}, accuracy={acc:.3f}")
122
+ plt.tight_layout()
123
+ plt.show()
@@ -0,0 +1,134 @@
1
+ """Classification de nœuds sur UN graphe fixe, par message passing.
2
+
3
+ Un graphe à deux communautés (modèle à blocs stochastiques) : les liens sont
4
+ denses à l'intérieur d'une communauté, rares entre les deux. Chaque nœud porte
5
+ une caractéristique très bruitée de sa communauté — si bruitée qu'un nœud seul
6
+ ne dit presque rien : l'optimum théorique nœud par nœud est ~69 %, et le MLP,
7
+ qui sur-apprend ses nœuds d'entraînement dans les 7 dimensions de bruit pur,
8
+ fait pire encore. Le message passing moyenne l'information sur le voisinage,
9
+ qui est surtout de la même communauté : le bruit s'écrase, le GNN dépasse
10
+ 90 %. C'est l'argument du chapitre en une expérience.
11
+
12
+ La moitié des nœuds est étiquetée, la justesse est mesurée sur les autres.
13
+ Boucle d'entraînement full-batch écrite à la main (comme mlp_demo) : un seul
14
+ graphe, pas de mini-batchs — le Trainer n'a rien à mélanger ici.
15
+ """
16
+
17
+ from yax.core import Module
18
+ import jax
19
+ import jax.numpy as jnp
20
+ import jax.random as jr
21
+ import numpy as np
22
+ import optax
23
+
24
+ from yax.layers.Linear import Linear
25
+ from yax.layers.MLP import MLP
26
+ from yax.layers.MessagePassing_layer import MessagePassing_layer
27
+
28
+ NB_PER_BLOCK = 60
29
+ DIM_FEATURES = 8
30
+ SIGMA = 2.0 # écart-type du bruit sur les caractéristiques
31
+ NB_LABELED = 30 # nœuds étiquetés par communauté (la moitié)
32
+
33
+
34
+ def make_graph(seed=0):
35
+ """Rend (features, labels, senders, receivers, train_idx, test_idx).
36
+ Génération en numpy : les arêtes sont des tableaux concrets, construits une
37
+ fois pour toutes hors de tout jit — le graphe est FIXE."""
38
+ rng = np.random.default_rng(seed)
39
+ nb_nodes = 2 * NB_PER_BLOCK
40
+ labels = np.repeat([0, 1], NB_PER_BLOCK)
41
+
42
+ # liens : p_in dans une communauté, p_out entre communautés
43
+ p = np.where(labels[:, None] == labels[None, :], 0.2, 0.02)
44
+ upper = np.triu(rng.random((nb_nodes, nb_nodes)) < p, k=1)
45
+ i, j = np.nonzero(upper)
46
+ senders = np.concatenate([i, j]) # un lien non orienté = deux arêtes
47
+ receivers = np.concatenate([j, i])
48
+
49
+ # caractéristiques : +/-1 sur la première coordonnée, noyé dans le bruit
50
+ mu = np.zeros((2, DIM_FEATURES))
51
+ mu[0, 0], mu[1, 0] = 1.0, -1.0
52
+ features = mu[labels] + SIGMA * rng.normal(size=(nb_nodes, DIM_FEATURES))
53
+
54
+ labeled = np.concatenate([rng.choice(np.arange(b * NB_PER_BLOCK, (b + 1) * NB_PER_BLOCK),
55
+ NB_LABELED, replace=False) for b in range(2)])
56
+ test_idx = np.setdiff1d(np.arange(nb_nodes), labeled)
57
+
58
+ to_j = lambda a, dtype: jnp.asarray(a, dtype=dtype)
59
+ return (to_j(features, jnp.float32), to_j(labels, jnp.int32),
60
+ to_j(senders, jnp.int32), to_j(receivers, jnp.int32),
61
+ to_j(labeled, jnp.int32), to_j(test_idx, jnp.int32))
62
+
63
+
64
+ class GNN_node_classifier(Module):
65
+ mp1: MessagePassing_layer
66
+ mp2: MessagePassing_layer
67
+ head: Linear
68
+
69
+ def __init__(self, rkey):
70
+ k1, k2, k3 = jr.split(rkey, 3)
71
+ # agrégation "mean" : l'échelle de l'agrégat ne dépend pas du degré,
72
+ # qui varie beaucoup d'un nœud à l'autre dans ce graphe
73
+ self.mp1 = MessagePassing_layer(DIM_FEATURES, 16, 16, k1, aggregation="mean")
74
+ self.mp2 = MessagePassing_layer(16, 16, 16, k2, aggregation="mean")
75
+ self.head = Linear(16, 2, k3)
76
+
77
+ def apply(self, h, rkey=None, *, senders, receivers):
78
+ h = self.mp1.apply(h, senders=senders, receivers=receivers)
79
+ h = self.mp2.apply(h, senders=senders, receivers=receivers)
80
+ return self.head.apply(h) # logits (nb_noeuds, 2)
81
+
82
+
83
+ def train_nodes(logits_fn, model, labels, train_idx, nb_steps=300, lr=0.01):
84
+ """logits_fn(model) -> (nb_noeuds, 2). Perte : CE sur les seuls nœuds
85
+ étiquetés — l'indexation par entiers reste valide sous jit."""
86
+ optimizer = optax.adam(lr)
87
+ opt_state = optimizer.init(model)
88
+
89
+ def loss_fn(model):
90
+ logits = logits_fn(model)
91
+ ce = optax.softmax_cross_entropy_with_integer_labels(
92
+ logits[train_idx], labels[train_idx])
93
+ return jnp.mean(ce)
94
+
95
+ @jax.jit
96
+ def step(model, opt_state):
97
+ loss, grads = jax.value_and_grad(loss_fn)(model)
98
+ updates, opt_state = optimizer.update(grads, opt_state)
99
+ return optax.apply_updates(model, updates), opt_state, loss
100
+
101
+ for _ in range(nb_steps):
102
+ model, opt_state, loss = step(model, opt_state)
103
+ return model, float(loss)
104
+
105
+
106
+ def accuracy_on(logits_fn, model, labels, idx):
107
+ pred = jnp.argmax(logits_fn(model)[idx], axis=-1)
108
+ return float(jnp.mean(pred == labels[idx]))
109
+
110
+
111
+ def run_comparison(seed=0, verbose=True):
112
+ X, labels, senders, receivers, train_idx, test_idx = make_graph(seed)
113
+ k_gnn, k_mlp = jr.split(jr.key(seed))
114
+
115
+ # référence : un MLP nœud par nœud, aveugle au graphe
116
+ mlp = MLP((DIM_FEATURES, 32, 2), "relu", k_mlp)
117
+ mlp_logits = lambda m: m.apply(X)
118
+ mlp, _ = train_nodes(mlp_logits, mlp, labels, train_idx)
119
+ acc_mlp = accuracy_on(mlp_logits, mlp, labels, test_idx)
120
+
121
+ # le GNN : mêmes caractéristiques, plus la structure du graphe
122
+ gnn = GNN_node_classifier(k_gnn)
123
+ gnn_logits = lambda m: m.apply(X, senders=senders, receivers=receivers)
124
+ gnn, _ = train_nodes(gnn_logits, gnn, labels, train_idx)
125
+ acc_gnn = accuracy_on(gnn_logits, gnn, labels, test_idx)
126
+
127
+ if verbose:
128
+ print(f"MLP (sans le graphe) : {acc_mlp:.3f}")
129
+ print(f"GNN (2 couches de message passing) : {acc_gnn:.3f}")
130
+ return acc_mlp, acc_gnn
131
+
132
+
133
+ if __name__ == "__main__":
134
+ run_comparison()
@@ -0,0 +1,82 @@
1
+ """Approximation de sinus par un MLP.
2
+
3
+ Descente de gradient a la main : pas d'optax, pas de filter_grad, pas de
4
+ partition/combine. Tout repose sur le fait qu'un yax.Module est un pytree
5
+ dont les feuilles sont exactement les parametres, a condition d'avoir
6
+ declare en `static` tout ce qui n'est pas un tableau.
7
+ """
8
+
9
+ import jax
10
+ import jax.numpy as jnp
11
+ import jax.random as jr
12
+ import matplotlib.pyplot as plt
13
+ from yax.layers.MLP import MLP
14
+
15
+
16
+ def make_data(rkey, nb, x_min, x_max):
17
+ x = jr.uniform(rkey, (nb, 1), minval=x_min, maxval=x_max)
18
+ y = jnp.sin(x)
19
+ return x, y
20
+
21
+
22
+ def loss_fn(model, x, y):
23
+ y_pred = jax.vmap(model.apply)(x)
24
+ return jnp.mean((y_pred - y) ** 2)
25
+
26
+
27
+ @jax.jit
28
+ def step(model, x, y, lr):
29
+ loss, grads = jax.value_and_grad(loss_fn)(model, x, y)
30
+ new_model = jax.tree.map(lambda p, g: p - lr * g, model, grads)
31
+ return new_model, loss
32
+
33
+
34
+ def train(model, x, y, lr, nb_steps):
35
+ losses = []
36
+ for i in range(nb_steps):
37
+ model, loss = step(model, x, y, lr)
38
+ losses.append(loss)
39
+ if i % (nb_steps // 10) == 0:
40
+ print(f"step {i:5d} loss = {loss:.3e}")
41
+ return model, jnp.array(losses)
42
+
43
+
44
+ def plot(model, x_train, y_train, losses, x_min, x_max):
45
+ x_plot = jnp.linspace(x_min, x_max, 400)[:, None]
46
+ y_plot = jax.vmap(model.apply)(x_plot)
47
+ fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(11, 4))
48
+
49
+ ax0.plot(x_train[:, 0], y_train[:, 0], ".", ms=4, alpha=0.35,
50
+ color="tab:gray", label="donnees")
51
+ ax0.plot(x_plot[:, 0], jnp.sin(x_plot[:, 0]), "--", lw=2,
52
+ color="tab:blue", label="sin(x)")
53
+ ax0.plot(x_plot[:, 0], y_plot[:, 0], lw=2,
54
+ color="tab:red", label="MLP")
55
+ ax0.set_xlabel("x")
56
+ ax0.set_title("approximation")
57
+ ax0.legend()
58
+
59
+ ax1.semilogy(losses, lw=1, color="tab:red")
60
+ ax1.set_xlabel("step")
61
+ ax1.set_title("loss (MSE)")
62
+ ax1.grid(alpha=0.3)
63
+
64
+ fig.tight_layout()
65
+ return fig
66
+
67
+
68
+ if __name__ == "__main__":
69
+ x_min, x_max = -3.0, 3.0
70
+ lr = 0.02
71
+ nb_steps = 10000
72
+ key_data, key_model = jr.split(jr.PRNGKey(0))
73
+ x, y = make_data(key_data, 256, x_min, x_max)
74
+ model = MLP((1, 32, 32, 1), "tanh", key_model)
75
+
76
+ print("loss initiale :", loss_fn(model, x, y))
77
+ model, losses = train(model, x, y, lr, nb_steps)
78
+ print("loss finale :", losses[-1])
79
+
80
+ fig = plot(model, x, y, losses, x_min, x_max)
81
+ fig.savefig("mpl_demo.png", dpi=120)
82
+ plt.show()
@@ -0,0 +1,110 @@
1
+ import os
2
+ import jax.numpy as jnp
3
+ import jax.random as jr
4
+ from matplotlib import pyplot as plt
5
+ from yax.core import Module, StaticField
6
+ import jax
7
+ import optax
8
+ from yax.layers.Linear import Linear
9
+ from yax.layers.RNN_layer import RNN_layer
10
+ from yax.training.Trainer import train, load_run, OUT_FOLDER
11
+ from yax.training.configs import TrainConfig
12
+ from yax.training.losses import bce_logits_loss, batch_apply
13
+
14
+
15
+ #C'est l'exemple qui est proposé dans la doc d'equinox
16
+ def get_data(dataset_size:int, rkey):
17
+ t = jnp.linspace(0, 2 * jnp.pi, 16)
18
+ offset = jr.uniform(rkey, (dataset_size, 1), minval=0, maxval=2 * jnp.pi)
19
+ x1 = jnp.sin(t + offset) / (1 + t)
20
+ x2 = jnp.cos(t + offset) / (1 + t)
21
+
22
+ half_dataset_size = dataset_size // 2
23
+ x1 = x1.at[:half_dataset_size].multiply(-1)
24
+ y = jnp.ones((dataset_size, 1))
25
+ y = y.at[:half_dataset_size].set(0)
26
+ #axis=-1 : on veut (dataset_size, 16, 2), soit 16 pas de temps de 2 features.
27
+ #Avec axis=1 on obtiendrait (dataset_size, 2, 16), et le scan parcourrait
28
+ #2 pas de temps de 16 features.
29
+ x = jnp.stack([x1, x2], axis=-1)
30
+
31
+ #les 2 classes sont rangees en bloc, mais batchs_for_one_epoch melange a
32
+ #chaque epoque, et train/val proviennent de deux appels distincts, chacun
33
+ #equilibre. Ne PAS decouper un seul get_data en tranches train/val.
34
+ return x, y
35
+
36
+
37
+ def present_data():
38
+ n_data = 10
39
+ x, y = get_data(n_data,jr.key(0))
40
+ print(x.shape, y.shape)
41
+
42
+ fig, axs = plt.subplots(n_data, 2, figsize=(8, 1.2 * n_data), sharex="all", sharey="all")
43
+ for i in range(n_data):
44
+ axs[i, 0].plot(x[i, :, 0])
45
+ axs[i, 1].plot(x[i, :, 1])
46
+ axs[i, 0].set_ylabel(f"label={int(y[i, 0])}")
47
+
48
+ axs[0, 0].set_title("feature 0")
49
+ axs[0, 1].set_title("feature 1")
50
+ plt.show()
51
+
52
+
53
+ class RNN_bin_classifier(Module):
54
+ rnn_layer: RNN_layer
55
+ linear_layer: Linear
56
+
57
+ dim_hidden:int = StaticField()
58
+
59
+ def __init__(self,dim_hidden,rkey,*,cell_type="gru"):
60
+ self.dim_hidden=dim_hidden
61
+
62
+ rk1,rk2=jr.split(rkey)
63
+ self.rnn_layer = RNN_layer(2,dim_hidden,False,rk1,cell_type=cell_type)
64
+ self.linear_layer=Linear(dim_hidden,1,rk2)
65
+
66
+ def apply(self,xs,rkey=None):
67
+ # rkey ignoré : modèle déterministe (signature uniforme apply(x, rkey))
68
+ y=self.rnn_layer.apply(xs)
69
+ y=self.linear_layer.apply(y)
70
+ return y #pas de sigmoid: il est ajouté dans la loss
71
+
72
+
73
+ def accuracy(model, x, y):
74
+ logits = batch_apply(model, x, None)
75
+ return float(jnp.mean((logits > 0) == (y > 0.5))) #logit>0 <=> sigmoid>0.5
76
+
77
+
78
+ def train_classif(cell_type="gru", dim_hidden=16, verbose=True):
79
+ key_train, key_val, key_model, key_shuffle = jr.split(jr.key(0), 4)
80
+ X_train, Y_train = get_data(512, key_train)
81
+ X_val, Y_val = get_data(256, key_val)
82
+
83
+ model = RNN_bin_classifier(dim_hidden, key_model, cell_type=cell_type)
84
+ acc_before = accuracy(model, X_val, Y_val)
85
+
86
+ config = TrainConfig(batch_size=32, learning_rate=0.05, n_epoch=60, lr_final_ratio=0.01)
87
+ mother_folder = os.path.join(OUT_FOLDER, f"rnn_classif_{cell_type}")
88
+ best_model, best_loss, folder = train(
89
+ model, mother_folder, config, X_train, Y_train, X_val, Y_val,
90
+ rkey=key_shuffle, loss_fn=bce_logits_loss, verbose=verbose)
91
+
92
+ acc_after = accuracy(best_model, X_val, Y_val)
93
+ print(f"{cell_type} : accuracy {acc_before:.3f} -> {acc_after:.3f}"
94
+ f" (best val BCE = {best_loss:.3g})")
95
+ return folder, best_loss, acc_after
96
+
97
+
98
+ def train_and_plot(cell_type="gru"):
99
+ folder, best_loss, acc = train_classif(cell_type)
100
+ history = load_run(folder, "history")["history"]
101
+ history.plot(title=f"RNN {cell_type} : BCE={best_loss:.2g}, accuracy={acc:.3f}")
102
+ plt.tight_layout()
103
+ plt.show()
104
+
105
+
106
+ if __name__ == "__main__":
107
+ train_and_plot("gru")
108
+
109
+
110
+