Une rapide introduction
CentraleSupélec
Problème Simuler un système d’Ising (Sethna (2006), Hayes (2000)). Moment magnétique des atomes arrangés sur une grille régulière \(\sigma_i \{-1, 1\}\). Pour une grille finie, il existe un nombre fini de configurations mais avec des dynamiques intriguantes.
La probabilité d’occuper un état du système \(\sigma\) est \(P(\sigma) = \frac{1}{Z}\exp(-\beta E(\sigma))\), avec
\[ E(\sigma) = -\sum_{i < j} J_{i,j} \sigma_i \sigma_j \]
Il existe des transitions de phase, en particulier pour \(\beta \approx 2.269\).
Voir la description complète du système sur https://tutos.metz.centralesupelec.fr/TPs/Intro-Python/index.html#an-example-project-simulating-the-ising-model
Concept fondamental de programmation orientée objet (OOP).
On peut construire une instance d’un objet :
On dispose également de beaucoup de méthodes spéciales : __lt__, __add__, __str__, …
On peut construire une hiérarchie de classes, on doit alors appeler le constructeur de la classe mère.
Problème Écrire un évaluateur d’expressions mathématiques.
class BinaryOp(Expression):
def __init__(self, fun, left, right):
self.fun = fun
self.left, self.right = left, right
def __call__(self, env):
return self.fun(self.left(env), self.right(env))
class Add(BinaryOp):
def __init__(self, left, right):
super().__init__(lambda x, y: x+y, left, right)
def __str__(self):
return f"({self.left} + {self.right})"
class Multiply(BinaryOp):
def __init__(self, left, right):
super().__init__(lambda x, y: x*y, left, right)
def __str__(self):
return f"({self.left} * {self.right})"Exemple
On décompose le projet en \(3\) classes :
ConsoleIsingGUIUn programme peut déclencher plusieurs fils d’exécutions, les threads. Ici :
Cela permet à la simulation de continuer à tourner même si l’utilisateur n’interagit pas.
import threading
class Simulator(threading.Thread):
def __init__(self, lattice_size, kT):
threading.Thread.__init__(self)
self.lock = threading.RLock()
self.spins = np.random.random((lattice_size, lattice_size)) > 0.5
def run(self):
while self.is_kept_alive():
if self.running:
self.step()
def stop(self):
with self.lock:
self.keeps_alive = False
def get_spins(self):
with self.lock:
return self.spins.copy()Au lieu des threads, on aurait pu utiliser du multiprocessing.
Installation (grâce au setup.py)
Exécution
Le projet est packagé avec setuptools.
La python packaging authority (pypa) maintient un tutorial sur le packaging, voir https://setuptools.pypa.io/en/latest/userguide/quickstart.html
Le packaging peut se faire avec un setup.py :
import setuptools
with open("README.md", "r") as fh:
long_description = fh.read()
setuptools.setup(
name="Ising",
version="1.0",
author="CentraleSupelec",
author_email="Jeremy.Fixcentralesupelec.fr",
description='A simulator of the Ising model with '
'the Metropolis algorithm',
long_description=long_description,
long_description_content_type="text/markdown",
url='http://tutos.metz.centralesupelec.fr/Intro_Python',
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires='>=3.6',
install_requires=['numpy', 'scipy', 'PyQt5', 'matplotlib', 'opencv-python-headless']
)Mais pourrait aussi utiliser pyproject.toml.
Et vous pourriez déposer votre librairie sur pypi, ou conda.
Numpy : Numerical Python
NumPy is the fundamental package for scientific computing in Python. It is a Python library that provides a multidimensional array object, various derived objects (such as masked arrays and matrices), and an assortment of routines for fast operations on arrays, including mathematical, logical, shape manipulation, sorting, selecting, I/O, discrete Fourier transforms, basic linear algebra, basic statistical operations, random simulation and much more.
Plusieurs approches pour créer un tableau Numpy :
np.zeros, np.ones, np.arange, np.linspace, np.random.rand, etc.np.loadtxt, np.genfromtxt, np.load, etc.# Tableau 1D de taille (5, )
np.array([1, 2, 3, 4, 5])
# Tableau 2D de taille (2, 3)
np.array([[1, 2, 3], [4, 5, 6]])
# Tableau 2D de taille (3, 4) rempli de zéros
np.zeros((3, 4))
# Tableau 6D de taille (2, 3, 4, 5, 6, 7) rempli de uns
np.ones((2, 3, 4, 5, 6, 7))
# Tableau 1D de 0 à 10 par pas de 2
np.arange(0, 10, 2)
# Tableau 1D de taille (5, ) avec des valeurs entre 0 et 1
np.linspace(0, 1, 5)
# Tableau 2D de taille (3,3) avec des valeurs aléatoires entre 0 et 1
np.random.rand(3, 3)Les tableaux ont :
On accède aux éléments :
start:stop:step,Pour l’efficacité en mémoire et temps:
On peut le vérifier avec l’attribut base
a = np.array([[1, 2, 3], [4, 5, 6]])
# Accès par index
# Premier élément de la première ligne
a[0, 0] # 1
# Accès par slicing
# Deuxième ligne
# Equivalent à a[1]
a[1, :] # [4 5 6]
# Première colonne
a[:, 0] # [1 4]
# Toutes les lignes, colonnes d'indice pair
a[:, ::2] #
a[:, ::2] = 0 # Modifie a
# Masquage booléen : fancy indexing
mask = a > 3 # np.array de dtype bool
b = a[mask] # [4 5 6]
b[0] = 10 # Ne Modifie pas aNe jamais faire
mais utiliser l’ellipsis :
On peut modifier la forme d’un tableau par :
np.reshape : retourne une vue du tableau avec une nouvelle forme,np.flatten ou np.ravel : retourne une copie ou une vue du tableau aplati (1D),np.transpose : retourne une vue du tableau transposé,np.pad : ajoute une bordure autour du tableau.np.expand_dims, x = x[np.newaxis, :] : ajoute une nouvelle dimension.On peut combiner/diviser plusieurs tableaux par :
np.concatenate : concatène plusieurs tableaux le long d’un axe,np.stack : empile plusieurs tableaux le long d’un nouvel axe,np.hstack et np.vstack : concatène horizontalement ou verticalement plusieurs tableaux.np.split, np.hsplit, np.vsplit : divise un tableau en plusieurs sous-tableaux.On peut permuter les éléments d’un tableau par :
np.transpose, np.swapaxesnp.rollLes opérations arithmétiques sont appliquées élément par élément +, ‘/’, -, *. Il existe également les opérations logiques.
a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[10, 20, 30], [40, 50, 60]])
# Addition
c = a + b # [[11 22 33] [44 55 66]]
# Soustraction
d = b - a # [[9 18 27] [36 45 54]]
# Multiplication terme à terme
e = a * b # [[10 40 90] [160 250 360]]
# Division terme à terme
f = b / a # [[10. 10. 10.] [10. 10. 10.]]Attention : * est le produit terme à terme, pas le produit matriciel. Le produit matriciel est obtenu par np.matmul ou simplement @
Problème Simuler le système de réaction-diffusion de GrayScott dont l’évolution des champs spatialisés \(u(x,t)\), \(v(x,t)\), \(x\in \mathbb{R}^2\) est régie par :
\[ \begin{eqnarray*} \frac{\partial u}{dt}(x,t) &=& D_u \nabla^2 u(x,t) - u(x,t) v^2(x,t) \\ & &+ F(1-u(x,t))\\ \frac{\partial v}{dt}(x,t) &=& D_u \nabla^2 v(x,t) + u(x,t) v^2(x,t) \\ && - (F+k) v(x,t) \end{eqnarray*} \]
def step(ut_1, vt_1, Du, Dv, F, k):
uvv = ut_1 * vt_1**2
lu = laplacian(ut_1)
lv = laplacian(vt_1)
ut[...] = ut_1 + dt * (Du * lu - uvv + F*(1-ut_1))
vt[...] = vt_1 + dt * (Dv * lv + uvv - (F + k) * vt_1)
def laplacian(src):
"""
u is a 2D nd array
"""
padded_src = np.pad(src, pad_width=1, mode="constant") # zero padding src
laplacian = (
0
+ padded_src[0:-2, 1:-1]
+ 0
+ padded_src[1:-1, 0:-2]
- 4 * padded_src[1:-1, 1:-1]
+ padded_src[1:-1, 2:]
+ 0
+ padded_src[2:, 1:-1]
+ 0
)
return laplacian[1:-1, 1:-1]avec \(\nabla^2\) le laplacien discret :
\[ \nabla^2 u(x,y) = \frac{u(x-\delta, y) + u(x+\delta, y) + u(x, y-\delta) + u(x, y+\delta) - 4 u(x,y)}{\delta^2} \]
Les fonctions Mathématiques s’appliquent sur tout les éléments, e.g. np.exp, np.sin, np.log, etc mais également les puissances.
e.g. Calcul d’une gaussienne centrée en \(x=2\) avec écart-type \(\sigma=0.5\)
On peut également calculer (selon un axis=..):
np.sum,np.min, np.max, np.argmin, np.argmax,np.mean \(\frac{1}{N} \sum_i X[..., i, ...]\), l’écart type np.std \(\sqrt{\frac{1}{N} \sum_i (X[..., i, ...] - \mu_i)^2}\)np.cov \(\sum_i (x_i - \mu)^T (x_i - \mu)\)Le broadcasting est un mécanisme qui permet d’effectuer des opérations entre des tableaux de formes différentes.
Numpy “étend” automatiquement les dimensions des tableaux pour qu’ils soient compatibles pour l’opération.
De la documentation sur le broadcasting
La règle : des opérations sur deux tableaux sont applicables si les dimensions sont compatibles.
Les dimensions sont compatibles si, 1) les tableaux ont le même nombre de dimensions, 2) partant de la droite (des dimensions)
Sinon vous obtiendrez l’exception ValueError: operands could not be broadcast together
Par exemple, on souhaite calculer la valeur d’une fonction gaussienne \(f(x)\) centrée en \(\mu\in\mathbb{R}^2\), sur une grille régulière centrée sur \(\mu\) :
\[ \forall x \in \mathbb{R}^2, f(x) = \frac{1}{\sigma \sqrt{2\pi}} \exp(\frac{- \|x - \mu\|_2^2}{2\sigma^2}) \]
if __name__ == '__main__':
mu = np.array([1, 2])
sigma = 0.3
step = 0.1
Nsteps = 10
X = gaussian(mu, sigma, step, Nsteps)
def gaussian(mu: np.ndarray, sigma: float, step: float, Nsteps: int) -> np.ndarray:
# Generate the regular grid
# (-N, -(N-1), ..., -1, 0, 1, ..., N-1, N) * step
intsteps = (np.arange(Nsteps) + 1)
deltasteps = np.concatenate([-intsteps[::-1], [0], intsteps]) * step
# Generate the steps centered on mu
# mu is (2, ), we use broadcasting on mu and deltasteps to generate
# mu (2, 1) + deltasteps(1, M) => mu + deltasteps is (2, M)
mu_delta = mu[:, np.newaxis] + deltasteps[np.newaxis, :]
# Generate the meshgrid with this values
# X and Y are of shape (2*Nsteps + 1, 2*Nsteps + 1)
X, Y = np.meshgrid(mu_delta[0], mu_delta[1])
# Concatenate the X, Y matrices
# xy is (( (2*Nsteps + 1) * (2*NSteps + 1), 2)
xy = np.concatenate((X.ravel()[:,np.newaxis], Y.ravel()[:, np.newaxis]), axis=1)
# Compute tha gausian values
# To compute the distance between the grid points is computed between xy and mu
# To broadcast, we do : xy (M, 2) - mu(1, 2)
dist = ((xy - mu[np.newaxis, :])**2).sum(axis=1)
values = 1.0 / (2.0 * sigma * np.sqrt(2.0 * np.pi)) * np.exp(- dist / (2.0 * sigma**2))
# Values is of shape (( (2*Nsteps + 1) * (2*NSteps + 1), ) that we reshape as
# (2*Nsteps + 1, 2*Nsteps + 1)
values = values.reshape((2*Nsteps + 1, 2*Nsteps + 1))
return values
Numpy supporte bien évidemment toutes les routines utiles en algèbre linéaire, dans le sous-package np.linalg:
np.dotnp.linalg.trace,np.linalg.inv, du déterminant np.linalg.detnp.norm - norme \(p\) pour un vecteur \((\sum_i x_i^p)^{1/p}\), Frobenius pour une matrice,np.linalg.eignp.linalg.cholesky \(X = L L^T\) (L triangulaire inférieure), SVD np.linalg.svd \(X = U S V^T\)Moindres carrés régularisés Soit \(n, m \in \mathbb{N}\), \(A\in \mathbb{R}^{n,m}\), \(b\in \mathbb{R}^{n}\), trouver \(x \in \mathbb{R}^m\) qui minimise :
\[ argmin_x J(x) = \|A x - b\|_2^2 = \sum_{i=0}^{n-1} (\sum_{j=0}^{m-1} a_{i,j}x_j - b_i)^2 \]
En fonction du range de \(A\), il peut exister une infinité de solution. Si \(A\) est singulière, une solution est obtenue en résolvant le problème des moindres carrés régularisés \(argmin_x J(x) + \lambda \|x\|_2^2\) , \(\lambda \in \mathbb{R}^+\) :
\[ x = (A^T A + \lambda \mathbf{I})^{-1} A^T b \]
Avec numpy :
import numpy as np
def rls(A: np.ndarray, b: np.ndarray, lbd: float) -> np.ndarray:
n, m = A.shape
inv = np.linalg.inv(A.T@A + lbd * np.eye(m))
x = np.dot(inv@A.T, b)
return x
if __name__ == '__main__':
n, m = 3, 4
A, b = np.random.random((n, m)), np.random.random((n,))
lbd = 0.001
x = rls(A, b, lbd)
print(x)Analyse en composantes principales Soit \(N\) vecteurs \(x_i \in \mathbb{R}^d\), trouver le vecteur \(w_0 \in \mathbb{R}^d\) et \(r\) vecteurs de projections \(w_j\) qui minimisent l’erreur de reconstruction :
\[ \begin{align} \min_{\{w_0, w_1, ..w_r\} \in \mathbb{R}^d} \sum_{i=0}^{N-1} \left|x_i - (w_0 + \sum_{j=1}^r (w_j^T (x_i-w_0))w_j )\right|_2^2 \end{align} \]
sujet à la contrainte \(\forall i, j \geq 1, w_i^T w_j = \delta_{i,j}\).
Algorithme:
Implémentation naïve https://github.com/rougier/ML-Recipes
class PCA:
def fit(self, X):
'''
Performs the PCA of X and stores the principal components.
The datapoints are supposed to be stored in the row vectors of X.
It keeps only the n_components projection vectors, associated
with the n_components largest eigenvalues of the covariance matrix X.T X
'''
# Center the datapoints
self.centroid = np.mean(X, axis=0)
# Computes the covariance matrix
sigma = np.dot((X - self.centroid).T, X - self.centroid)
# Compute the eigenvalue/eigenvector decomposition of sigma
eigvals, eigvecs = np.linalg.eigh(sigma)
# Note :The eigenvalues returned by eigh are ordered in ascending order.
# Stores the n_components eigenvectors/eigenvalues associated
# with the largest eigen values
#self.eigvals = eigvals[-self.n_components:]
#self.eigvecs = eigvecs[:, -self.n_components:]
# Stores all the eigenvectors/eigenvalues
# Useful for later computing some statistics of the variance we keep
self.eigvals = eigvals
self.eigvecs = eigvecs
def transform(self, Z):
'''
Uses a fitted PCA to transform the row vectors of Z
Remember the eigen vectors are ordered by ascending order
Denoting Z_trans = transform(Z),
The first component is Z_trans[:, -1]
The second component is Z_trans[:, -2]
...
'''
return np.dot(Z - self.centroid, self.eigvecs[:, -self.n_components:])
Produced with quarto