Vilaine Amont — Flood Forecast (Station-Attention)
Modèle de prévision de crues pour les 11 stations hydrométriques du bassin Vilaine Amont (Bretagne, France), avec intervalles de confiance natifs via quantile regression.
Architecture
Per-station LSTM → Cross-station Multi-Head Attention → Quantile Regression
- Encodeur : LSTM partagé (2 couches, hidden 128) traite chaque station indépendamment (7 variables × 72 pas de temps)
- Attention : 3 couches Multi-Head Attention (8 têtes) avec résidus et LayerNorm permettent l'échange d'information entre stations
- Précipitations futures : encodeur MLP intégrant les prévisions de précipitations à 24h (11 stations × 24h = 264 valeurs)
- Décodeurs : têtes séparées pour hauteur (H) et débit (Q), chacune produisant 24 horizons × 3 quantiles
- Quantiles : q10, q50, q90 — entraînés avec pinball loss asymétrique en crue
Entrées / Sorties
Entrées
| Nom | Shape | Description |
|---|---|---|
past_input |
(batch, 72, 77) |
72h de données passées — 11 stations × 7 variables/station (paddé à 77) |
future_precip |
(batch, 264) |
Précipitations prévues — 11 stations × 24h |
Les 7 variables par station sont : h (hauteur), q (débit), precip (précipitations), dh (variation H), dq (variation Q), soil_moisture_0_to_7cm, soil_moisture_7_to_28cm. Certaines stations remplacent q/dq par release (lâchers de barrage).
Toutes les valeurs sont normalisées min-max avec les paramètres de norm_params.json.
Sorties
| Nom | Shape | Description |
|---|---|---|
predictions |
(batch, 1296) |
432 sorties × 3 quantiles (interleaved) |
Les 1296 sorties sont organisées : pour chaque station, pour chaque variable (H puis Q), pour chaque horizon (1-24h), les 3 quantiles [q10, q50, q90] sont interleaved. Le mapping exact est dans station_attn_meta.json.
Les prédictions sont des deltas normalisés — il faut les dénormaliser puis les ajouter à la dernière valeur observée pour obtenir les valeurs absolues.
Stations
| Code | Nom | Variables prédites |
|---|---|---|
| J700061001 | Janzé (Seiche) | H, Q |
| J701064001 | Lac de la Valière | H |
| J702401001 | Louvigné-de-Bais (Quincampoix) | H, Q |
| J702403001 | Étang de Marcillé | H |
| J702402001 | Rannée (Ardenne) | H, Q |
| J701061001 | Vitré (Vilaine) | H, Q |
| J704301001 | Pocé-les-Bois (Cantache) | H |
| J705302001 | Lac de la Haute Vilaine | H |
| J706062001 | Châteaubourg (Vilaine) | H, Q |
| J708311001 | La Bouëxière (Illet) | H, Q |
| J709063002 | Cesson-Sévigné (Vilaine) | H, Q |
Métriques
Résultats sur le jeu de test — hiver 2025-2026 (Châteaubourg, q50) :
| Horizon | NSE | RMSE |
|---|---|---|
| t+1h | 0.9999 | 6 mm |
| t+6h | 0.9968 | 36 mm |
| t+12h | 0.9884 | 69 mm |
| t+24h | 0.9699 | 112 mm |
Calibration des quantiles : P(y < q10) ≈ 10%, P(y < q90) ≈ 85% (cibles 10%/90%).
Utilisation
import numpy as np
import onnxruntime as ort
import json
# Charger le modèle et les métadonnées
session = ort.InferenceSession("station_attn.onnx")
meta = json.load(open("station_attn_meta.json"))
norm = json.load(open("norm_params.json"))
# Préparer les entrées (normalisées min-max)
past_input = np.zeros((1, 72, 77), dtype=np.float32) # 72h passées
future_precip = np.zeros((1, 264), dtype=np.float32) # précip prévues 24h
# Inférence
outputs = session.run(None, {
"past_input": past_input,
"future_precip": future_precip,
})
predictions = outputs[0] # shape (1, 1296)
# Extraire les prédictions pour Châteaubourg (H, q50, t+6h)
station = "J706062001"
h_start = meta["output_map"][station]["h_start"]
n_quantiles = meta["n_quantiles"]
horizon_idx = 5 # t+6h (0-indexed)
q50_idx = 1 # [q10, q50, q90]
delta_norm = predictions[0, (h_start + horizon_idx) * n_quantiles + q50_idx]
# Dénormaliser
h_min = norm[f"{station}_h"]["min"]
h_max = norm[f"{station}_h"]["max"]
delta = delta_norm * (h_max - h_min)
# Valeur absolue = dernière observation + delta
# prediction_h = last_observed_h + delta
Données d'entraînement
- Hydrométrie : hauteurs d'eau (H) et débits (Q) horaires depuis 2000 — Hydro EauFrance
- Météo : précipitations et humidité du sol horaires — Open-Meteo
- Split : train 2000–2025/06, val 2025/07–2025/12, test 2026/01+
Entraînement
- GPU : NVIDIA DGX Spark (Grace Blackwell GB10)
- Optimiseur : Adam, lr=5e-4
- Batch size : 256
- Loss : Pinball loss avec pénalité asymétrique en crue (H ≥ 800mm à Châteaubourg)
- Suréchantillonnage : crues ×2/×4/×8 selon le seuil
- Framework : PyTorch → export ONNX
Liens
- Application web : vilaine-amont.haruni.net
- Code source : github.com/sebastien-burel/vilaine-amont