# CAMA : ma33 Gradient pour résoudre Ax = b -- Exercice
# Cours du 17/05
# La méthode du gradient pour résoudre A x = b
Le but de ce TP est de vous laisser avancer tout seul. Reprenez les cours et programmez la méthode du gradient
pour résoudre le système matriciel $A {\bf x} = {\bf b}$ avec A symétrique et à diagonale dominante
($a_{ii} > \sum_{k \ne i} |a_{ik}|$).
* Commencez en 2D avec une matrice 2x2, vérifier que le résultat est bon et tracer la courbe de convergence
* Passez en nxn (on montrera que cela marche avec une matrice 9x9)
Il peut être intéressant de normaliser la matrice A pour éviter que les calculs explosent.
:::spoiler Solution
## 2x2
```python=
# plein de copier coller du cours
import numpy as np
import scipy.linalg as lin
import matplotlib.pylab as plt
import plotly.offline as py
import plotly.graph_objects as go
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
np.set_printoptions(precision=3, linewidth=150, suppress=True)
plt.style.use(['seaborn-whitegrid','data/cours.mplstyle'])
```
```python=
N = 2
A = np.random.randint(-10, 10, size=(N,N))
A = A * 1.0 # pour passer en reels
A[np.diag_indices(N)] = 0.1 + np.abs(A).sum(axis=0) # diag dominante
A = A + A.T # symétrique
A = A / np.abs(A).sum(axis=0).mean()
b = np.random.randint(-10,10,size=(N))
print(A, "\n\n", b)
```
```
[[1.037 0.184]
[0.184 0.596]]
[7 2]
```
```python=
def grad_J(x):
return A@x - b
```
```python=
def minimum_J(start_value, µ=0.1, e = 0.001):
x = [np.array(start_value)]
while True:
x.append(x[-1] - µ * grad_J(x[-1]))
if np.square(x[-1] - x[-2]).sum() < e**2:
break
# la suite n'est que des tests pour se protéger
if np.square(x[-1] - x[-2]).sum() > 1E9: # au cas où on diverge
print("DIVERGE")
break
if len(x) > 1000: # c'est trop long, je crains la boucle infinie
print('Trop long, boucle infinie ?')
break
return np.array(x)
x = minimum_J(np.zeros(N))
```
```python=
x[-1] - lin.solve(A, b)
```
```
array([-0.007, 0.016])
```
```python=
plt.plot(x[:,0], x[:,1], 'x:')
```
![](https://i.imgur.com/S5fK3Je.png)
## nxn
```python=
np.abs(A)
```
```
array([[1.037, 0.184],
[0.184, 0.596]])
```
```python=
N = 9
A = np.random.randint(-10, 10, size=(N,N))
A = A * 1.0 # pour passer en reels
A[np.diag_indices(N)] = 0.1 + np.abs(A).sum(axis=0) # diag dominante
A = A + A.T # symétrique
A = A / np.abs(A).sum(axis=0).mean()
b = np.random.randint(-10,10,size=(N))
```
```python=
x = minimum_J(np.zeros(N))
```
```python=
x[-1] - lin.solve(A, b)
```
```
array([ 0. , -0.006, 0.001, 0.006, 0.017, 0.008, -0. , 0.014, 0.004])
```
```python=
print("Converge en %d itérations" % len(x))
x
```
```
Converge en 178 itérations
```
```
array([[ 0. , 0. , 0. , ..., 0. , 0. , 0. ],
[ 0.3 , -0.2 , 0. , ..., 0.8 , -0.6 , -0.4 ],
[ 0.576, -0.396, -0.001, ..., 1.548, -1.176, -0.783],
...,
[ 3.088, -4.714, 0.223, ..., 13.007, -14.827, -9.853],
[ 3.088, -4.714, 0.223, ..., 13.007, -14.827, -9.853],
[ 3.088, -4.714, 0.223, ..., 13.007, -14.828, -9.854]])
```
:::
# Introduire de l'inertie
Introduire de l'inertie dans la méthode du gradient. Que constate-t-on ?
:::spoiler Reponse
Ajouter de l'inertie dans une méthode itérative veut dire qu'on avance moins vite vers le point suivant :
```python=
x_next = ...
x = w * x_next + (1 - w) * x
```
avec `w` qui représente la force d'avancée (ou l'inverse du poids de l'inertie).
Dans le cas de la méthode du gradient cela donne :
```python=
x_next = x - |µ| grad_J(x)
x = w * x_next + (1 - w) * x
```
ce qui se développe ainsi :
```python=
x = w * (x - |µ| grad_J(x)) + (1 - w) x
```
ou
```python=
x = x - w * |µ| grad_J(x)
```
On voit donc qu'ajouter de l'inertie ne fait que modifier le paramètre µ qui justement sert à avancer plus ou moins vite. µ est déjà une sorte d'inertie.
Donc cela ne change pas la méthode et cela n'amméliore pas l'algorithme.
:::
# Valeur optimale de µ
On note que deux directions de pente sucessives sont orthogonales si le point suivant est le minumum dans
la direction donnée ($\nabla J ({\bf x}^k$)).
$$
\nabla J ({\bf x}^{k+1})^T \; \nabla J ({\bf x}^k) = 0
$$
## Démonstration
On veut régler µ pour arriver au minimum de J lorsqu'on avance dans la direction $- \nabla J({\bf x}^{k})$.
Cela veut dire que la dérivée partielle de $J({\bf x}^{k+1})$ par rapport à µ doit être
égale à 0 ou bien en faisant apparaitre µ dans l'équation :
$$
\frac{\partial J ({\bf x}^k - µ \; \nabla J ({\bf x}^k))}{\partial µ} = 0
$$
En développant on a
$$
\begin{align}
\frac{\partial J ({\bf x}^{k+1})}{\partial {\bf x}^{k+1}} \;
\frac{\partial {\bf x}^{k+1}}{\partial µ} & = 0 \\
J'({\bf x}^{k+1}) \, . \, (- \nabla J ({\bf x}^k)) & = 0 \\
(A\, {\bf x}^{k+1} - b) \, . \, \nabla J ({\bf x}^k) & = 0 \quad \textrm{puisque A est symétrique}\\
\nabla J ({\bf x}^{k+1}) \, . \, \nabla J ({\bf x}^k) & = 0 \quad \textrm{CQFD}
\end{align}
$$
![](https://i.imgur.com/yxvHNX9.png)
En utilisant cette propriété, évaluer la valeur optimale de µ pour atteindre le minimum dans la direction de
$\nabla J ({\bf x}^k)$.
## Exercice
Écrire le méthode du gradient avec le calcul du µ optimal à chaque itération pour résoudre $A {\bf x} = {\bf b}$.
:::spoiler Solution
On reprend l'avant-dernière ligne de la démonstration et on remplace $\bf x^{k+1}$ par $\bf x^{k} -\mu\nabla J(\bf x^k)$:
$$
\begin{aligned}
(A(\bf x^k - \mu\nabla J(\bf x^k)) -b)\cdot\nabla J(\bf x^k) &= 0\\
(A\bf x^k -b - \mu A\nabla J(\bf x^k))\cdot\nabla J(\bf x^k) &= 0\\
(A\bf x^k -b)\cdot\nabla J(\bf x^k) - \mu A\nabla J(\bf x^k)) -b\cdot\nabla J(\bf x^k) &= 0\\
\mu &= \frac{\nabla J(\bf x^k)\cdot\nabla J(\bf x^k)}{A\nabla J(\bf x^k)\cdot\nabla J(\bf x^k)}
\end{aligned}
$$
```python=
def minimum_J(start_value, e = 0.001):
x = [np.array(start_value)]
while True:
gradJ = grad_J(x[-1])
µ = np.dot(gradJ, gradJ) / np.dot(A @ gradJ, gradJ)
x.append(x[-1] - µ * grad_J(x[-1]))
if np.square(x[-1] - x[-2]).sum() < e**2:
break
# la suite n'est que des tests pour se protéger
if np.square(x[-1] - x[-2]).sum() > 1E9: # au cas où on diverge
print("DIVERGE")
break
if len(x) > 1000: # c'est trop long, je crains la boucle infinie
print('Trop long, boucle infinie ?')
break
return np.array(x)
```
```python=
x = minimum_J(np.zeros(N))
x[-1] - lin.solve(A, b)
```
```
array([-0., -0., 0., -0., 0., 0., 0., 0., -0.])
```
```python=
print("Converge en %d itérations" % len(x))
x
```
```
Converge en 14 itérations
```
```
array([[ 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. ],
[ 5.295, -3.53 , 0. , -15.884, -8.824, -1.765, 14.119, -10.589, -7.06 ],
[ 3.488, -5.355, -0.197, -12.432, -13.603, -3.608, 12.295, -13.31 , -8.402],
[ 3.085, -4.72 , 0.257, -14.479, -14.586, -3.802, 12.956, -14.127, -9.531],
[ 3.128, -4.877, 0.194, -13.924, -15.279, -4.164, 12.973, -14.572, -9.669],
[ 3.076, -4.743, 0.232, -14.255, -15.457, -4.161, 13. , -14.712, -9.837],
[ 3.091, -4.75 , 0.226, -14.166, -15.569, -4.242, 13.013, -14.786, -9.842],
[ 3.083, -4.72 , 0.226, -14.224, -15.6 , -4.239, 13.009, -14.815, -9.863],
[ 3.087, -4.718, 0.225, -14.208, -15.618, -4.257, 13.011, -14.829, -9.859],
[ 3.086, -4.711, 0.224, -14.22 , -15.623, -4.256, 13.008, -14.836, -9.861],
[ 3.087, -4.71 , 0.223, -14.217, -15.627, -4.26 , 13.009, -14.839, -9.859],
[ 3.087, -4.708, 0.223, -14.219, -15.627, -4.26 , 13.008, -14.84 , -9.859],
[ 3.087, -4.708, 0.222, -14.219, -15.628, -4.261, 13.008, -14.841, -9.858],
[ 3.087, -4.708, 0.222, -14.219, -15.628, -4.261, 13.007, -14.841, -9.858]])
```
:::