Parcours D.I.A.M — Module Mathématiques pour l'IA

Optimisation sous Contraintes

Du gradient descent aux méthodes d'optimisation convexe et métaheuristiques

0

Fondamentaux Mathématiques

Avant d'aborder l'optimisation sous contraintes, vérifions les bases essentielles pour le parcours D.I.A.M (Data Intelligence Artificielle & Mathématique).

Prérequis obligatoires :
  • Calcul différentiel (gradient, Hessienne, Jacobienne)
  • Algèbre linéaire (espaces vectoriels, matrices définies positives)
  • Python : NumPy, Matplotlib, SciPy
  • Notions de convexité (ensembles convexes, fonctions convexes)

Formulation générale du problème

Problème d'optimisation sous contraintes (P) :
$$ \begin{aligned} \text{Minimiser} \quad & f(x) \\ \text{sous contraintes} \quad & g_i(x) \leq 0 \quad \text{pour } i = 1, \dots, m \quad \text{(inégalités)} \\ & h_j(x) = 0 \quad \text{pour } j = 1, \dots, p \quad \text{(égalités)} \\ & x \in \mathbb{R}^n \end{aligned} $$
Exercice 0.1 — Identification des contraintes Facile

Identifiez $f$, $g_i$ et $h_j$ dans ce problème : Minimiser $f(x,y) = x^2 + y^2$ sous $x + y \leq 1$ et $x \geq 0$.

Analyse :

  • $f(x,y) = x^2 + y^2$ (fonction objectif, distance à l'origine)
  • $g_1(x,y) = x + y - 1 \leq 0$ (contrainte d'inégalité)
  • $g_2(x,y) = -x \leq 0$ (équivalent à $x \geq 0$)
  • Aucune contrainte d'égalité $h_j$ (donc $p=0$)

Le domaine réalisable est l'intersection du demi-plan sous la droite $x+y=1$ et du demi-plan $x \geq 0$.

Exercice 0.2 — Ensemble réalisable Facile

Décrivez géométriquement l'ensemble réalisable défini par : $$C = \{(x,y) \in \mathbb{R}^2 \mid x^2 + y^2 \leq 4, \; x \geq 0, \; y \geq 0\}$$ Quelle est sa nature (convexe, compact, ouvert, fermé) ?

Analyse géométrique :

  • $x^2 + y^2 \leq 4$ : disque fermé de centre $(0,0)$ et rayon $2$
  • $x \geq 0, y \geq 0$ : restriction au premier quadrant

L'ensemble $C$ est donc un quart de disque (secteur circulaire).

Propriétés :

  • Convexe : Oui (intersection de convexes)
  • Compact : Oui (fermé et borné)
  • Fermé : Oui (inégalités larges $\leq$ et $\geq$)

Installation des outils

Terminal Bash
# Environnement D.I.A.M Optimization
pip install numpy scipy matplotlib cvxpy
pip install ipywidgets  # Pour visualisations interactives

# Pour l'optimisation symbolique (vérification des gradients)
pip install sympy
1

Optimisation Sans Contrainte

1.1 Conditions d'optimalité

Conditions d'optimalité pour $f \in C^2$ :

Condition nécessaire du 1er ordre :

$$\nabla f(x^*) = 0 \quad \text{(point stationnaire)}$$

Condition suffisante du 2nd ordre :

$$\nabla f(x^*) = 0 \quad \text{et} \quad \nabla^2 f(x^*) \succ 0 \quad \text{(définie positive)}$$

Alors $x^*$ est un minimum local strict.

1.2 Descente de Gradient

Algorithme : Gradient Descent à pas fixe
  • Initialiser $x^{(0)} \in \mathbb{R}^n$, choisir le taux d'apprentissage $\alpha > 0$
  • Répéter jusqu'à convergence :
        $x^{(k+1)} = x^{(k)} - \alpha \nabla f(x^{(k)})$
  • Retourner $x^{(k)}$ lorsque $\|\nabla f(x^{(k)})\| < \varepsilon$
  • gradient_descent.py Python
    import numpy as np
    
    def gradient_descent(f, grad_f, x0, alpha=0.1, tol=1e-6, max_iter=1000):
        """
        Minimisation de f par descente de gradient avec backtracking line search
        
        Args:
            f: fonction objectif (pour le suivi)
            grad_f: gradient de f (doit retourner un np.array)
            x0: point initial
            alpha: pas d'apprentissage initial
        """
        x = x0.copy()
        history = [x.copy()]
        f_history = [f(x)]
        
        for k in range(max_iter):
            gradient = grad_f(x)
            
            # Critère d'arrêt sur la norme du gradient
            if np.linalg.norm(gradient) < tol:
                print(f"Convergence atteinte en {k} itérations")
                break
            
            # Backtracking line search (condition d'Armijo)
            t = alpha
            while f(x - t * gradient) > f(x) - 0.5 * t * np.linalg.norm(gradient)**2:
                t *= 0.5
            
            # Mise à jour
            x = x - t * gradient
            history.append(x.copy())
            f_history.append(f(x))
        
        return x, np.array(history), f_history
    
    # Exemple : f(x,y) = x^2 + 2y^2 (conditionnement 2)
    f = lambda x: x[0]**2 + 2*x[1]**2
    grad_f = lambda x: np.array([2*x[0], 4*x[1]])
    
    x_opt, hist, f_hist = gradient_descent(f, grad_f, x0=np.array([5.0, 5.0]), alpha=0.1)
    print(f"Minimum trouvé: {x_opt}")  # [0, 0]
    ⚠️ Piège du pas fixe : Si $\alpha$ est trop grand, la méthode diverge. Si $\alpha$ est trop petit, convergence lente. Pour une fonction quadratique $f(x) = \frac{1}{2}x^T A x$, le pas optimal est $\alpha^* = \frac{2}{\lambda_{\max}(A) + \lambda_{\min}(A)}$.
    Exercice 1.1 — Fonction de Rosenbrock Intermédiaire

    Implémentez la descente de gradient pour la fonction de Rosenbrock : $$f(x,y) = (1-x)^2 + 100(y-x^2)^2$$ Le minimum global est at $(1,1)$. Visualisez la trajectoire. Pourquoi converge-t-elle lentement dans la vallée étroite ? Calculez le conditionnement de la Hessienne au point $(-1, 1)$.

    def rosenbrock(x):
        return (1-x[0])**2 + 100*(x[1]-x[0]**2)**2
    
    def grad_rosenbrock(x):
        dx = -2*(1-x[0]) - 400*x[0]*(x[1]-x[0]**2)
        dy = 200*(x[1]-x[0]**2)
        return np.array([dx, dy])
    
    # Le gradient est presque orthogonal à la direction du minimum
    # dans la vallée étroite. Conditionnement ~ 2500 au point (-1,1).
    # Solution: gradient conjugué ou Newton.
    Exercice 1.2 — Méthode de Newton Difficile

    Implémentez la méthode de Newton pour minimiser $f(x) = -\sum_{i=1}^n \log(1-x_i^2)$ sur $]-1,1[^n$ (barrière logarithmique). La direction de Newton est donnée par : $$d_k = -[\nabla^2 f(x_k)]^{-1} \nabla f(x_k)$$ Comparez le nombre d'itérations avec la descente de gradient pour $n=10$.

    La Hessienne est diagonale : $\nabla^2 f(x) = \text{diag}\left(\frac{2(1+x_i^2)}{(1-x_i^2)^2}\right)$

    def newton_method(f, grad_f, hess_f, x0, tol=1e-6):
        x = x0.copy()
        for k in range(100):
            g = grad_f(x)
            if np.linalg.norm(g) < tol:
                break
            H = hess_f(x)
            d = -np.linalg.solve(H, g)  # Direction de Newton
            x = x + d  # Pas unitaire (quadratique)
        return x

    Newton converge en ~5 itérations vs ~500 pour le gradient (conditionnement élevé).

    2

    Contraintes d'Égalité & Lagrangien

    2.1 Théorie de Lagrange

    Problème avec contraintes d'égalité :
    $$ \begin{aligned} \min_{x} \quad & f(x) \\ \text{s.c.} \quad & h(x) = 0 \end{aligned} $$

    Lagrangien :

    $$\mathcal{L}(x, \lambda) = f(x) + \lambda^T h(x)$$

    Conditions nécessaires (si qualification des contraintes) :

    $$ \begin{cases} \nabla_x \mathcal{L} = \nabla f(x) + \lambda^T \nabla h(x) = 0 \\ \nabla_\lambda \mathcal{L} = h(x) = 0 \end{cases} $$
    Interprétation économique : $\lambda$ représente le "prix" de la contrainte (coût marginal de la violation). En Machine Learning, c'est le multiplicateur dans les SVM à marge douce.
    resolution_symbolique.py SymPy
    import sympy as sp
    
    # Résolution analytique: min x^2 + y^2 sous x + y = 1
    x, y, lam = sp.symbols('x y lambda', real=True)
    
    # Lagrangien
    L = x**2 + y**2 + lam*(x + y - 1)
    
    # Système: gradient L = 0
    eq1 = sp.diff(L, x)  # 2x + lambda = 0
    eq2 = sp.diff(L, y)  # 2y + lambda = 0
    eq3 = sp.diff(L, lam) # x + y - 1 = 0
    
    sol = sp.solve([eq1, eq2, eq3], [x, y, lam])
    print(sol)  # x=0.5, y=0.5, lambda=-1
    Exercice 2.1 — Régression linéaire contrainte Intermédiaire

    Résolvez : $$\min_w \|Aw - b\|^2 \quad \text{sous} \quad \sum_{i=1}^n w_i = 1$$ (portfolio équipondéré). Montrez que : $$w = (A^TA)^{-1}\left(A^Tb + \frac{1}{2}\lambda \mathbf{1}\right)$$ où $\lambda$ est choisi pour satisfaire la contrainte.

    Démonstration :
    Lagrangien : $\mathcal{L} = \|Aw-b\|^2 + \lambda(\mathbf{1}^T w - 1)$

    Condition d'optimalité : $\nabla_w \mathcal{L} = 2A^T(Aw-b) + \lambda \mathbf{1} = 0$

    Donc : $A^TA w = A^Tb - \frac{\lambda}{2}\mathbf{1}$

    Soit : $w = (A^TA)^{-1}A^Tb - \frac{\lambda}{2}(A^TA)^{-1}\mathbf{1}$

    En injectant dans $\mathbf{1}^T w = 1$, on trouve $\lambda$.
    Exercice 2.2 — Projection sur un sous-espace affine Intermédiaire

    Trouvez la projection euclidienne d'un point $y \in \mathbb{R}^n$ sur l'hyperplan $\{x \mid Ax = b\}$ où $A \in \mathbb{R}^{m \times n}$ est de rang plein. Formulez comme un problème de minimisation quadratique sous contraintes linéaires.

    Solution :
    $$\min_x \frac{1}{2}\|x-y\|^2 \quad \text{s.c.} \quad Ax = b$$
    Lagrangien : $\mathcal{L} = \frac{1}{2}(x-y)^T(x-y) + \lambda^T(Ax-b)$

    Conditions KKT :
    (1) $x - y + A^T\lambda = 0$
    (2) $Ax = b$

    De (1) : $x = y - A^T\lambda$
    En injectant dans (2) : $A(y - A^T\lambda) = b$
    Donc : $\lambda = (AA^T)^{-1}(Ay - b)$

    Solution finale : $$x^* = y - A^T(AA^T)^{-1}(Ay - b)$$
    3

    Programmation Linéaire (PL)

    3.1 Forme standard

    Programme Linéaire :
    $$ \begin{aligned} \min_{x} \quad & c^T x \\ \text{s.c.} \quad & Ax \leq b \\ & x \geq 0 \end{aligned} $$

    3.2 Méthode du Simplexe (conceptuel)

    Algorithme du Simplexe
  • Transformer en forme canonique avec variables d'écart : $Ax + s = b$, $s \geq 0$
  • Trouver une solution de base réalisable (sommet du polyèdre)
  • Tant que amélioration possible :
        Choisir variable entrante (coût réduit négatif)
        Choisir variable sortante (ratio test minimum)
        Pivot de Gauss-Jordan pour nouvelle base
  • Optimalité atteinte quand tous les coûts réduits $\geq 0$
  • linear_programming.py SciPy
    from scipy.optimize import linprog
    
    # Problème: Diet Problem (minimiser coût nutritionnel)
    # Variables: x1 = pain, x2 = viande, x3 = légumes
    
    # Coûts à minimiser (€ par unité)
    c = [2.0, 10.0, 3.0]
    
    # Contraintes nutritionnelles (Ax >= b devient -Ax <= -b)
    A_ub = [
        [-100, -200, -50],   # Calories >= 2000
        [-10, -50, -5]       # Protéines >= 100g
    ]
    b_ub = [-2000, -100]
    
    # Bornes (x >= 0)
    bounds = [(0, None), (0, None), (0, None)]
    
    result = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method='highs')
    
    if result.success:
        print(f"Coût optimal: {result.fun:.2f}€")
        print(f"Quantités: {result.x}")
    else:
        print("Pas de solution réalisable")
    Exercice 3.1 — Problème de Transport Difficile

    3 usines (capacités 100, 200, 150) et 4 magasins (demandes 80, 120, 100, 150). Matrice des coûts : $$C = \begin{pmatrix} 2 & 3 & 1 & 4 \\ 3 & 2 & 4 & 2 \\ 4 & 1 & 2 & 3 \end{pmatrix}$$ Formulez le PL et résolvez-le. Quelle est la solution dégénérée si on augmente la demande du magasin 1 à 100 ?

    Exercice 3.2 — Dualité en PL Difficile

    Écrivez le problème dual du suivant et vérifiez le théorème de dualité forte : $$\max 3x_1 + 2x_2 \quad \text{s.c.} \quad x_1 + x_2 \leq 4, \; x_1 \leq 2, \; x_2 \leq 3, \; x \geq 0$$ Quelle est la valeur optimale du dual ?

    Problème Dual :
    $$\min 4y_1 + 2y_2 + 3y_3$$ $$\text{s.c. } y_1 + y_2 \geq 3, \; y_1 + y_3 \geq 2, \; y \geq 0$$
    Solution : $y^* = (2, 1, 0)$, valeur optimale = $4(2) + 2(1) + 3(0) = 10$.
    Vérification primal : $x^* = (2, 2)$, valeur = $3(2) + 2(2) = 10$. ✓
    4

    Optimisation Convexe & Conditions KKT

    4.1 Convexité

    Définitions fondamentales :

    Ensemble convexe : $C$ est convexe si :

    $$\forall x,y \in C, \forall \lambda \in [0,1], \quad \lambda x + (1-\lambda)y \in C$$

    Fonction convexe : $f$ est convexe si :

    $$f(\lambda x + (1-\lambda)y) \leq \lambda f(x) + (1-\lambda)f(y)$$

    Propriété clé : Tout minimum local est global. Si $f$ est strictement convexe, le minimum est unique.

    4.2 Conditions KKT (Karush-Kuhn-Tucker)

    Pour les problèmes convexes sous contraintes d'inégalité $g_i(x) \leq 0$ :

    Conditions KKT :
    $$ \begin{aligned} \text{(Stationnarité)} \quad & \nabla f(x^*) + \sum_{i=1}^m \lambda_i \nabla g_i(x^*) = 0 \\ \text{(Faisabilité primale)} \quad & g_i(x^*) \leq 0 \\ \text{(Faisabilité duale)} \quad & \lambda_i \geq 0 \\ \text{(Complémentarité)} \quad & \lambda_i g_i(x^*) = 0 \end{aligned} $$

    Si contrainte inactive ($g_i(x^*) < 0$), alors $\lambda_i = 0$.

    cvxpy_example.py CVXPY
    import cvxpy as cp
    import numpy as np
    
    # Problème: QP (Quadratic Programming)
    # min 0.5*x^T*P*x + q^T*x sous Gx <= h
    
    n = 2
    x = cp.Variable(n)
    
    # Données
    P = np.array([[4, 1], [1, 2]])  # Définie positive
    q = np.array([1, 1])
    G = np.array([[-1, 0], [0, -1], [1, 1], [-1, 2]])
    h = np.array([0, 0, 1, 1])
    
    # Formulation
    objective = cp.Minimize(0.5 * cp.quad_form(x, P) + q.T @ x)
    constraints = [G @ x <= h]
    prob = cp.Problem(objective, constraints)
    
    result = prob.solve()
    print(f"Valeur optimale: {result}")
    print(f"Solution: {x.value}")
    print(f"Multiplicateurs duaux: {[c.dual_value for c in constraints]}")
    Exercice 4.1 — SVM Dual Difficile

    Formulez le problème dual de l'SVM à marge douce comme un QP convexe. Montrez que la matrice $Q$ (Gram) est semi-définie positive. Implémentez avec CVXPY sur le dataset Iris (classes 0 vs 1).

    Exercice 4.2 — Water-filling Algorithm Difficile

    Résolvez le problème de répartition de puissance : $$\max_{p_i} \sum_{i=1}^n \log(1 + \alpha_i p_i) \quad \text{s.c.} \quad \sum p_i \leq P_{\max}, \; p_i \geq 0$$ Utilisez les conditions KKT pour montrer que la solution est de la forme $p_i^* = \max(0, \frac{1}{\lambda} - \frac{1}{\alpha_i})$ (algorithme "water-filling").

    Solution :
    Lagrangien : $\mathcal{L} = -\sum \log(1+\alpha_i p_i) + \lambda(\sum p_i - P_{\max}) - \sum \mu_i p_i$

    Stationnarité : $\frac{-\alpha_i}{1+\alpha_i p_i} + \lambda - \mu_i = 0$

    Par complémentarité, si $p_i > 0$ alors $\mu_i = 0$ :
    $\frac{\alpha_i}{1+\alpha_i p_i} = \lambda \Rightarrow p_i = \frac{1}{\lambda} - \frac{1}{\alpha_i}$

    Si $\frac{1}{\lambda} \leq \frac{1}{\alpha_i}$, alors $p_i = 0$ (on ne remplit pas ce "seau").
    $\lambda$ est ajusté pour $\sum p_i = P_{\max}$.
    5

    Méthodes Numériques Avancées

    5.1 Méthodes de Barrière (Intérieur)

    Pour les contraintes d'inégalité $g_i(x) \leq 0$, on utilise une barrière logarithmique :

    Problème barrière :
    $$\min_x f(x) - \mu \sum_{i=1}^m \ln(-g_i(x))$$

    Quand $\mu \to 0$, on approche la solution contrainte depuis l'intérieur (strictement faisable).

    5.2 SLSQP (Sequential Least Squares Programming)

    constrained_optimization.py SciPy
    from scipy.optimize import minimize
    
    def objective(x):
        return x[0]**2 + x[1]**2 + x[2]**2
    
    def constraint1(x):
        return x[0] + x[1] + x[2] - 1  # = 0
    
    def constraint2(x):
        return 0.5 - x[0]  # >= 0 donc x[0] <= 0.5
    
    constraints = [
        {'type': 'eq', 'fun': constraint1},
        {'type': 'ineq', 'fun': constraint2}
    ]
    
    x0 = np.array([0.5, 0.5, 0.0])
    
    sol = minimize(objective, x0, method='SLSQP', 
                   constraints=constraints,
                   options={'ftol': 1e-9, 'disp': True})
    
    print(f"Solution: {sol.x}")
    print(f"Valeur f: {sol.fun}")
    Exercice 5.1 — Frontière Efficiente (Markowitz) Difficile

    Maximiser le ratio de Sharpe : $$\max_w \frac{\mu^T w - r_f}{\sqrt{w^T \Sigma w}} \quad \text{s.c.} \quad \mathbf{1}^T w = 1, \; w \geq 0$$ Transformez ce problème fractionnaire en QP (technique de Schaible) et résolvez pour 10 actifs avec SLSQP.

    Exercice 5.2 — Optimisation de forme Expert

    Minimisez la surface d'un cylindre de volume fixé $V_0$ : $$\min_{r,h} 2\pi r^2 + 2\pi r h \quad \text{s.c.} \quad \pi r^2 h = V_0, \; r > 0, \; h > 0$$ Résolvez analytiquement par KKT, puis vérifiez numériquement avec un algorithme de pénalisation intérieure.

    Solution analytique :
    Lagrangien : $\mathcal{L} = 2\pi r^2 + 2\pi r h + \lambda(\pi r^2 h - V_0)$

    Conditions :
    $\frac{\partial \mathcal{L}}{\partial r} = 4\pi r + 2\pi h + \lambda(2\pi r h) = 0$
    $\frac{\partial \mathcal{L}}{\partial h} = 2\pi r + \lambda(\pi r^2) = 0$

    De la deuxième équation : $\lambda = -\frac{2}{r}$
    En injectant dans la première : $4\pi r + 2\pi h - \frac{2}{r}(2\pi r h) = 0$
    $4\pi r + 2\pi h - 4\pi h = 0 \Rightarrow 4\pi r = 2\pi h \Rightarrow h = 2r$

    Solution : La hauteur égale le diamètre (canette de soda optimale).
    6

    Métaheuristiques & Optimisation Non-Convexe

    Quand la fonction est non-convexe, non-différentiable, ou combinatoire :

    6.1 Recuit Simulé (Simulated Annealing)

    simulated_annealing.py Python
    def simulated_annealing(f, x0, T_init=1000, cooling=0.95, n_iter=1000):
        """
        Minimisation par recuit simulé
        Accepte les mauvaises solutions avec proba exp(-deltaE/T)
        """
        x = x0.copy()
        x_best, f_best = x.copy(), f(x)
        T = T_init
        
        for i in range(n_iter):
            # Voisin aléatoire
            x_new = x + np.random.normal(0, 0.1, size=x.shape)
            f_new = f(x_new)
            delta = f_new - f(x)
            
            # Critère de Metropolis
            if delta < 0 or np.random.random() < np.exp(-delta / T):
                x = x_new
                if f_new < f_best:
                    x_best, f_best = x_new, f_new
            
            T *= cooling
        
        return x_best, f_best

    6.2 Algorithmes Génétiques

    Exercice 6.1 — Feature Selection Difficile

    Utilisez un GA pour sélectionner le sous-ensemble optimal de 20 features parmi 100 pour minimiser l'AIC (Akaike) d'une régression logistique. Chromosome = vecteur binaire. Croisement en un point, mutation bit-flip.

    Exercice 6.2 — Voyageur de Commerce (TSP) Difficile

    Implémentez un algorithme de colonies de fourmis (ACO) pour résoudre le TSP sur 50 villes. Comparez avec la solution exacte obtenue par programmation dynamique (Held-Karp) pour $n \leq 20$.

    Projet Final D.I.A.M : Supply Chain Optimizer

    Contexte : Vous êtes Data Scientist pour une chaîne logistique. Optimisez les flux de 5 entrepôts vers 20 magasins avec contraintes de capacité, fenêtres temporelles, et minimisation de l'empreinte carbone.

    Spécifications mathématiques

    Variables de décision :
    $x_{i,j,t} \in \mathbb{N}$ : quantité transportée de l'entrepôt $i$ vers le magasin $j$ au temps $t$
    $y_{i,j,t} \in \{0,1\}$ : activation du transport (coût fixe)

    Fonction objectif :
    $$\min \sum_{i,j,t} \left(c_{i,j}^{\text{var}} \cdot x_{i,j,t} + c_{i,j}^{\text{fix}} \cdot y_{i,j,t} + \alpha \cdot \text{CO}_2(i,j) \cdot x_{i,j,t}\right)$$

    Contraintes :
    • Capacité : $\sum_j x_{i,j,t} \leq C_i^{\text{entrepôt}}$
    • Demande : $\sum_i x_{i,j,t} \geq D_{j,t}$ (satisfaction 95%)
    • Activation : $x_{i,j,t} \leq M \cdot y_{i,j,t}$ (big-M)
    • Fenêtres temporelles : $t \in [8\text{h}, 12\text{h}] \cup [14\text{h}, 18\text{h}]$

    Phases du projet

    Livrables attendus
  • Phase 1 : Relaxation LP — Résoudre la relaxation linéaire (variables continues) pour borne inférieure. Analyse du gap d'intégrité.
  • Phase 2 : Branch-and-Bound — Implémenter un B&B simple ou utiliser PuLP/CVXPY pour l'IP (Integer Programming). Comparer avec la relaxation.
  • Phase 3 : Heuristique — Algorithme glouton + recherche locale (2-opt) pour solution temps réel (moins de 1 seconde).
  • Phase 4 : Robustesse — Programmation stochastique où $\tilde{D}_{j,t} \sim \mathcal{N}(D_{j,t}, \sigma^2)$. Minimiser le coût espéré.
  • Contrainte éthique D.I.A.M : Votre solution doit garantir qu'aucun entrepôt n'est sur-sollicité (>90% capacité) pour protéger les conditions de travail. Ajoutez cette contrainte dure ou pénalisez-la fortement dans l'objectif.

    Livrables finaux