Ce TD suppose que vous maîtrisez les bases des cours 1, 2 et 3 : variables, fonctions, listes 1D et 2D, opérations sur les matrices. Si ce n'est pas le cas, gardez les cours ouverts dans des onglets — on ne vous jugera pas. (Enfin, si, un peu.)
Le problème est simple : on a des points (x, y) qui ressemblent à une droite, mais pas parfaitement. On veut trouver la droite y = a·x + b qui « passe le plus près possible » de tous les points.
x et y représentant
les données suivantes (taille en cm / poids en kg) :
x = [150, 155, 160, 165, 170, 175, 180, 185, 190, 195]
y = [52, 58, 61, 64, 68, 72, 77, 82, 86, 90]
Quelle est la taille de chaque liste ? Quelle est la taille moyenne ?
Quel est le poids moyen ? Calculez-les avec une boucle (interdit
d'utiliser sum() pour l'instant — on fait les choses
bien).
def moyenne(liste):
total = 0
for v in liste:
total = total + v
return total / len(liste)
x = [150, 155, 160, 165, 170, 175, 180, 185, 190, 195]
y = [52, 58, 61, 64, 68, 72, 77, 82, 86, 90]
moy_x = moyenne(x)
moy_y = moyenne(y)
print("Taille moyenne :", moy_x)
print("Poids moyen :", moy_y)
On cherche a et b tels que y ≈ a·x + b. La méthode des moindres carrés (ordinarily least squares, OLS) consiste à minimiser la somme des carrés des erreurs (écarts entre y réel et y prédit).
J(a,b) = Σ (yᵢ − (a·xᵢ + b))²
Les solutions analytiques (démontrées par Gauss en 1809) sont :
a = (Σ (xᵢ − x̄)(yᵢ − ȳ)) / (Σ (xᵢ − x̄)²)
b = ȳ − a·x̄
def regression_lineaire(x, y):
"""Retourne (a, b) tels que y ≈ a*x + b (moindres carrés)."""
n = len(x)
moy_x = moyenne(x)
moy_y = moyenne(y)
num = 0 # Σ (x - x̄)(y - ȳ)
den = 0 # Σ (x - x̄)²
for i in range(n):
num = num + (x[i] - moy_x) * (y[i] - moy_y)
den = den + (x[i] - moy_x) ** 2
a = num / den
b = moy_y - a * moy_x
return a, b
a, b = regression_lineaire(x, y)
print("a =", a)
print("b =", b)
print("Droite : poids =", a, "× taille +", b)
predire(x_val, a, b) qui retourne
le poids prédit pour une taille donnée. Testez pour :MSE = (1/n) × Σ (yᵢ − (a·xᵢ + b))²
def predire(x_val, a, b):
return a * x_val + b
def mse(x, y, a, b):
total = 0
for i in range(len(x)):
erreur = y[i] - predire(x[i], a, b)
total = total + erreur ** 2
return total / len(x)
print("Poids prédit pour 160 cm :", predire(160, a, b))
print("Poids prédit pour 180 cm :", predire(180, a, b))
print("Poids prédit pour 200 cm :", predire(200, a, b))
print("MSE :", mse(x, y, a, b))
La formule analytique (Gauss) est parfaite pour la régression linéaire, mais elle ne passe pas à l'échelle pour les problèmes complexes. L'alternative : la descente de gradient, une méthode itérative qui marche pour (presque) tous les modèles d'IA.
Les formules de mise à jour des paramètres (avec le taux d'apprentissage α) :
a ← a − α × (∂J/∂a) avec ∂J/∂a = (−2/n) × Σ xᵢ·(yᵢ − (a·xᵢ + b))
b ← b − α × (∂J/∂b) avec ∂J/∂b = (−2/n) × Σ (yᵢ − (a·xᵢ + b))
gradient_descent(x, y, a_init, b_init, alpha, iterations)
qui :a=0, b=0, alpha=0.0001
(taux d'apprentissage à ajuster), et iterations=1000.
Comparez avec les résultats de l'exercice 1.2.
def gradient_descent(x, y, a, b, alpha, iterations):
n = len(x)
for it in range(iterations):
grad_a = 0
grad_b = 0
for i in range(n):
pred = a * x[i] + b
grad_a = grad_a + x[i] * (pred - y[i])
grad_b = grad_b + (pred - y[i])
grad_a = (2 / n) * grad_a
grad_b = (2 / n) * grad_b
a = a - alpha * grad_a
b = b - alpha * grad_b
if it % 200 == 0:
print("Itération", it, "— MSE :", mse(x, y, a, b))
return a, b
a_gd, b_gd = gradient_descent(x, y, 0, 0, 0.0001, 1000)
print("a (GD) =", a_gd)
print("b (GD) =", b_gd)
print("a (OLS) =", a)
print("b (OLS) =", b)
Remplacez les données par celles-ci (fréquence CPU en GHz / température en °C) :
x = [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0]
y = [42, 48, 55, 63, 72, 82, 95]
Lancez la régression. À quelle température prévoyez-vous un CPU à 5.0 GHz ? À partir de quelle fréquence dépasse-t-on 100°C ? (Indice : résoudre a·x + b = 100)
La régression linéaire suppose une relation droite entre x et y. Mais si les données sont courbes (ex: sin(x), tendance non linéaire) ? On passe à un polynôme de degré d :
y = a₀ + a₁·x + a₂·x² + a₃·x³ + ... + a_d·xd
Pour adapter la régression linéaire à un polynôme, on transforme chaque x en un vecteur [x⁰, x¹, x², ..., xd] et on applique la régression linéaire multiple (vue en §1) sur toutes ces colonnes.
polynomial_features(x, deg) qui
transforme une liste x en une matrice 2D : chaque ligne i contient
[xᵢ⁰, xᵢ¹, ..., xᵢdeg].x = [2, 3] et deg = 3,
on doit obtenir :
[[1, 2, 4, 8], # 2⁰, 2¹, 2², 2³
[1, 3, 9, 27]] # 3⁰, 3¹, 3², 3³
def polynomial_features(x, deg):
"""Crée la matrice de Vandermonde pour un degré donné."""
n = len(x)
X = [[0 for _ in range(deg + 1)] for _ in range(n)]
for i in range(n):
for j in range(deg + 1):
X[i][j] = x[i] ** j
return X
# On génère des points de la fonction sinus avec un peu de bruit
import random
random.seed(42)
x_sin = [i * 0.2 for i in range(20)] # 0, 0.2, 0.4, ..., 3.8
def sin_bruite(x):
# sin(x) avec un tout petit bruit
return math.sin(x) + (random.random() - 0.5) * 0.1
import math
y_sin = [sin_bruite(v) for v in x_sin]
print("x :", x_sin[:5], "...")
print("y :", [round(v, 3) for v in y_sin[:5]], "...")
Pour plusieurs variables (les colonnes x⁰, x¹, ...), la formule des moindres carrés devient matricielle. On cherche le vecteur θ (thêta) qui minimise l'erreur, avec :
θ = (XT·X)−1·XT·y
C'est l'équation normale (normal equation). Il nous faut donc : transposition, multiplication matricielle, et inversion de matrice. On a déjà les deux premières au cours 3. Pour l'inverse... on va tricher un peu avec une fonction fournie.
regression_polynomiale(x, y, deg) qui :def transpose(m):
lignes = len(m)
colonnes = len(m[0])
return [[m[i][j] for i in range(lignes)] for j in range(colonnes)]
def produit_matrice(A, B):
n, p = len(A), len(A[0])
p2, m = len(B), len(B[0])
C = [[0 for _ in range(m)] for _ in range(n)]
for i in range(n):
for j in range(m):
for k in range(p):
C[i][j] = C[i][j] + A[i][k] * B[k][j]
return C
def inverse_2x2(M):
"""Inverse d'une matrice 2×2 uniquement (pour le degré 1)."""
a, b = M[0][0], M[0][1]
c, d = M[1][0], M[1][1]
det = a * d - b * c
if det == 0:
raise ValueError("Matrice singulière")
return [[d/det, -b/det], [-c/det, a/det]]
def inverse_matrice(M):
"""Inverse par la méthode de Gauss-Jordan (pour n petites)."""
n = len(M)
# Matrice augmentée [M | I]
A = [row[:] for row in M]
I = [[0 for _ in range(n)] for _ in range(n)]
for i in range(n):
I[i][i] = 1
for col in range(n):
# Pivot partiel
pivot = col
for row in range(col + 1, n):
if abs(A[row][col]) > abs(A[pivot][col]):
pivot = row
A[col], A[pivot] = A[pivot], A[col]
I[col], I[pivot] = I[pivot], I[col]
if A[col][col] == 0:
raise ValueError("Matrice singulière")
val_pivot = A[col][col]
for j in range(n):
A[col][j] = A[col][j] / val_pivot
I[col][j] = I[col][j] / val_pivot
for row in range(n):
if row != col:
facteur = A[row][col]
for j in range(n):
A[row][j] = A[row][j] - facteur * A[col][j]
I[row][j] = I[row][j] - facteur * I[col][j]
return I
def regression_polynomiale(x, y, deg):
"""Régression polynomiale par équation normale."""
X = polynomial_features(x, deg) # n × (deg+1)
XT = transpose(X) # (deg+1) × n
XTX = produit_matrice(XT, X) # (deg+1) × (deg+1)
XTX_inv = inverse_matrice(XTX) # (deg+1) × (deg+1)
XTy = produit_matrice(XT, [[v] for v in y]) # (deg+1) × 1
theta = produit_matrice(XTX_inv, XTy) # (deg+1) × 1
return [theta[i][0] for i in range(len(theta))]
# Test sur sinus avec degré 5
coeffs = regression_polynomiale(x_sin, y_sin, 5)
print("Coefficients (deg 5) :", [round(c, 4) for c in coeffs])
evalue_polynome(coeffs, x) (utilisez
Horner du cours 2) et prédisez les valeurs pour les x d'origine.def evalue_polynome(coeffs, x):
resultat = 0
for a in reversed(coeffs):
resultat = resultat * x + a
return resultat
# Test pour degré 3
coeffs_3 = regression_polynomiale(x_sin, y_sin, 3)
print("x\tvrai\tprédit\terr")
for i in range(len(x_sin)):
pred = evalue_polynome(coeffs_3, x_sin[i])
print(round(x_sin[i], 1), round(y_sin[i], 3),
round(pred, 3), round(pred - y_sin[i], 4))
On a deux listes de données. Sont-elles liées ? Si oui, comment ? On peut vouloir trouver une relation de la forme a·x + b·y = c (relation affine entre deux variables), ou simplement mesurer leur corrélation.
r = (Σ (xᵢ − x̄)(yᵢ − ȳ)) / √(Σ (xᵢ − x̄)² × Σ (yᵢ − ȳ)²)
correlation(x, y) qui calcule r.heures = [1, 2, 3, 4, 5, 6, 7, 8]
notes = [2, 5, 8, 9, 12, 14, 16, 18]
Que vaut r ? Interprétez.
def correlation(x, y):
n = len(x)
moy_x = moyenne(x)
moy_y = moyenne(y)
num = 0
sum_x2 = 0
sum_y2 = 0
for i in range(n):
dx = x[i] - moy_x
dy = y[i] - moy_y
num = num + dx * dy
sum_x2 = sum_x2 + dx ** 2
sum_y2 = sum_y2 + dy ** 2
den = math.sqrt(sum_x2 * sum_y2)
if den == 0:
return 0
return num / den
print("Corrélation taille/poids :", round(correlation(x, y), 4))
heures = [1, 2, 3, 4, 5, 6, 7, 8]
notes = [2, 5, 8, 9, 12, 14, 16, 18]
print("Corrélation heures/notes :", round(correlation(heures, notes), 4))
Si deux variables sont corrélées, on peut chercher la droite qui les lie dans l'espace (x, y). C'est une régression linéaire comme au §1, mais on peut aussi se poser la question : existe-t-il une combinaison linéaire a·x + b·y = c ?
# Régression linéaire classique (comme au §1)
a, b = regression_lineaire(heures, notes)
print("y =", a, "× x +", b)
# Sous la forme a·x + b·y = c : y = a*x + b
# → a·x + (−1)·y = −b
a_forme = a
b_forme = -1
c_forme = -b
print("Relation :", round(a_forme, 3), "× x + (", b_forme, ") × y =", round(c_forme, 3))
# Vérifions : a·x − y = −b → y = a·x + b ✓
Testez la corrélation entre le nombre de bières bues et la note obtenue à un exam :
bieres = [0, 1, 2, 3, 4, 5, 6]
note_exam = [18, 15, 12, 10, 7, 4, 2]
Quelle est la corrélation ? Que vaut a·x + b·y = c dans ce cas ? Conclusion : pour réussir un exam, ne buvez pas (ou alors après).
Jusqu'ici, on a tout fait à la main — listes, boucles, algèbre. C'était pour comprendre. Maintenant qu'on a compris, on va utiliser les outils professionnels. Il est temps de présenter :
Depuis le début, on utilise math.sqrt(),
random.random(), liste.append(),
sans jamais expliquer le point. Il est temps.
En Python, le point sert à accéder à un attribut ou une méthode d'un objet.
| Expression | Signification |
|---|---|
math.sqrt(x) |
Dans le module math, prends la fonction sqrt et appelle-la avec x |
notes.append(12) |
Sur la liste notes, appelle sa méthode append avec l'argument 12 |
objet.attribut |
Accède à l'attribut attribut de l'objet (une variable qui lui appartient) |
math.sqrt =
« la fonction sqrt de math ». liste.append
= « la méthode append de liste ». Comme en français :
« le chien du voisin ». Le point, c'est le « du » des programmeurs.
(Personne n'avait jamais fait cette analogie. Vous êtes témoin
d'un moment historique.)
NumPy introduit les arrays (tableaux) qui sont comme des listes mais en beaucoup plus rapides, avec des opérations vectorisées.
import numpy as np
# Créer un array à partir d'une liste
x_np = np.array([1, 2, 3, 4, 5])
y_np = np.array([2, 4, 6, 8, 10])
# Opérations vectorisées (sans boucle !)
print(x_np + y_np) # addition — et PAS la concaténation
print(x_np * 2) # multiplication par scalaire
print(x_np ** 2) # carré (élément par élément)
print(np.mean(x_np)) # moyenne
print(np.corrcoef(x_np, y_np)) # matrice de corrélation
np.array est aux listes ce que le TGV est au vélo :
même principe, mais beaucoup plus rapide. Et avec pleins de fonctionnalités
en plus (sièges inclinables, plateau-repas, matrices 3D).
En IA, tout est array NumPy ou tensor PyTorch.
Les listes Python pures, c'est pour les exercices.
import matplotlib.pyplot as plt
# Données taille/poids
x = [150, 155, 160, 165, 170, 175, 180, 185, 190, 195]
y = [52, 58, 61, 64, 68, 72, 77, 82, 86, 90]
a, b = regression_lineaire(x, y)
# Créer des points pour tracer la droite
x_line = np.linspace(145, 200, 100)
y_line = a * x_line + b
# La magie des points
plt.plot(x, y, 'o', label="Données") # 'o' = cercles (nuage de points)
plt.plot(x_line, y_line, '-', label="Régression") # '-' = ligne continue
plt.xlabel("Taille (cm)")
plt.ylabel("Poids (kg)")
plt.title("Régression linéaire taille vs poids")
plt.legend()
plt.grid(True)
plt.show() # Affiche le graphique
np.linspace(0, 3.8, 200) pour évaluer les polynômes
sur un intervalle fin. Que remarquez-vous sur le degré 9 entre les points ?
Rien ne change fondamentalement : la formule matricielle θ = (XTX)−1XTy fonctionne pour n'importe quel nombre de variables. Chaque colonne de X est une feature. En IA réelle, on a des milliers de colonnes.
Au lieu de calculer le gradient sur tous les points (coûteux), on le calcule sur un seul point choisi aléatoirement. Plus rapide, plus bruité, mais converge quand même. C'est la base du deep learning.
Le R² mesure la qualité du modèle : il varie entre 0 (modèle inutile) et 1 (modèle parfait). Formule :
R² = 1 − (Σ (yᵢ − ŷᵢ)²) / (Σ (yᵢ − ȳ)²)
def r2(x, y, a, b):
residus = sum((y[i] - (a * x[i] + b)) ** 2 for i in range(len(x)))
total = sum((y[i] - moyenne(y)) ** 2 for i in range(len(y)))
return 1 - residus / total
print("R² taille/poids :", round(r2(x, y, a, b), 4))
R² = 0.995 → le modèle explique 99.5% de la variance. Pas mal.
Ce TD vous a fait traverser tout le pipeline du machine learning :
Et tout ça sans bibliothèque (sauf à la fin).
Vous êtes maintenant capables d'expliquer ce qu'il se cache sous
le capot de scikit-learn quand vous écrivez
LinearRegression().fit(X, y).
model.fit() — les gradients, les mises
à jour, les matrices — ça, c'est être ingénieur.