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).
- 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
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$.
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
# 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
Optimisation Sans Contrainte
1.1 Conditions d'optimalité
Condition nécessaire du 1er ordre :
Condition suffisante du 2nd ordre :
Alors $x^*$ est un minimum local strict.
1.2 Descente de Gradient
$x^{(k+1)} = x^{(k)} - \alpha \nabla f(x^{(k)})$
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]
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.
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é).
Contraintes d'Égalité & Lagrangien
2.1 Théorie de Lagrange
Lagrangien :
Conditions nécessaires (si qualification des contraintes) :
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
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.
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$.
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.
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)$$
Programmation Linéaire (PL)
3.1 Forme standard
3.2 Méthode du Simplexe (conceptuel)
Choisir variable entrante (coût réduit négatif)
Choisir variable sortante (ratio test minimum)
Pivot de Gauss-Jordan pour nouvelle base
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")
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 ?
É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 ?
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$. ✓
Optimisation Convexe & Conditions KKT
4.1 Convexité
Ensemble convexe : $C$ est convexe si :
Fonction convexe : $f$ est convexe si :
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$ :
Si contrainte inactive ($g_i(x^*) < 0$), alors $\lambda_i = 0$.
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]}")
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).
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").
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}$.
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 :
Quand $\mu \to 0$, on approche la solution contrainte depuis l'intérieur (strictement faisable).
5.2 SLSQP (Sequential Least Squares Programming)
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}")
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.
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.
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).
Métaheuristiques & Optimisation Non-Convexe
Quand la fonction est non-convexe, non-différentiable, ou combinatoire :
6.1 Recuit Simulé (Simulated Annealing)
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
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.
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
Spécifications mathématiques
$y_{i,j,t} \in \{0,1\}$ : activation du transport (coût fixe)
Fonction objectif :
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 finaux
- Notebook Jupyter avec formulation mathématique complète (LaTeX)
- Comparaison des méthodes : LP vs IP vs Heuristique (temps vs qualité)
- Visualisation des flux (networkx + matplotlib)
- Analyse de sensibilité : impact du coût carbone $\alpha$ sur la solution
- Présentation 15 min avec démo live (ajout d'une contrainte urgente en direct)