Mini-Projet : Apprentissage par Régression

TD — Des moindres carrés à l'interpolation polynomiale
📈 Régression · Gradient · Polynômes · Corrélation

§0 Avant de commencer

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.)

La régression linéaire, c'est le « Hello World » du machine learning. Tout le monde commence par ça. Les entreprises vous paieront pour le faire. Les vidéos YouTube à 2M vues en parlent. Et après ce TD, vous pourrez dire « j'ai entraîné un modèle » sans avoir l'air complètement idiot. (Enfin, pas trop idiot.)
Le terme régression a été introduit par Francis Galton (1822–1911) — cousin de Darwin, statisticien, et inventeur du concept de « régression vers la moyenne ». Il étudiait la taille des parents et des enfants : les enfants très grands avaient tendance à avoir des enfants moins grands, et vice-versa. Galton appela ça « régression », le nom est resté. Aujourd'hui, la régression linéaire est partout : économie, médecine, météo, et votre prochain entretien d'embauche.

§1 Partie I — Régression linéaire à la main

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.

Exercice 1.1 — Génération des données

Créez deux listes Python 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).

Solution
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)
Taille moyenne : 172.5
Poids moyen : 71.0

1.1 La droite qui colle le mieux aux données

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̄

Exercice 1.2 — Coefficients de la droite

Implémentez ces formules en Python pur (sans bibliothèque). Calculez a et b pour les données taille/poids.
Solution
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)
a = 0.8363636363636364
b = -73.27272727272728
Droite : poids = 0.836 × taille + -73.273
Carl Friedrich Gauss (1777–1855) — l'un des plus grands mathématiciens de tous les temps. À 18 ans, il invente la méthode des moindres carrés... mais ne la publie que 14 ans plus tard. Entre-temps, Legendre l'avait publiée. Gauss s'en fichait : il avait déjà inventé la géométrie non-euclidienne, les statistiques modernes, et calculé l'orbite de Cérès avec ses prédictions. Bref, il avait d'autres choses à faire.
Exercice 1.3 — Prédiction et évaluation

Écrivez une fonction predire(x_val, a, b) qui retourne le poids prédit pour une taille donnée. Testez pour :
— Taille 160 cm
— Taille 180 cm
— Taille 200 cm (les basketteurs vous diront merci)

Calculez ensuite l'erreur quadratique moyenne (MSE) :

MSE = (1/n) × Σ (yᵢ − (a·xᵢ + b))²

Solution
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))
Poids prédit pour 160 cm : 60.54545454545455
Poids prédit pour 180 cm : 77.27272727272728
Poids prédit pour 200 cm : 94.0
MSE : 0.8818181818181817
Vous venez de faire du machine learning. Oui, c'est tout. Pas de GPU, pas de tensors, pas de « deep learning ». Juste des additions et des multiplications. La régression linéaire, c'est l'IA quand elle n'a pas encore eu son café. Mais ça marche, c'est interprétable, et lesbanques s'en servent tous les jours pour décider si vous méritez un prêt.

1.2 Descente de gradient — l'autre méthode

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.

La descente de gradient a été inventée par Augustin-Louis Cauchy (1789–1857) en 1847. Oui, le même Cauchy qui vous a donné les suites, les séries, l'analyse complexe et des nuits blanches. Son idée : pour minimiser une fonction, on suit la direction de plus grande pente (le gradient), comme une bille qui roule dans une cuillère. C'est simple, c'est beau, c'est Cauchy.

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))

Exercice 1.4 — Implémentez la descente de gradient

Écrivez une fonction gradient_descent(x, y, a_init, b_init, alpha, iterations) qui :
1. Calcule les gradients
2. Met à jour a et b
3. Affiche la MSE toutes les 100 itérations
4. Retourne (a, b) finaux

Commencez avec a=0, b=0, alpha=0.0001 (taux d'apprentissage à ajuster), et iterations=1000. Comparez avec les résultats de l'exercice 1.2.
Solution
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)
Itération 0 — MSE : 5083.3
Itération 200 — MSE : 1.2178
Itération 400 — MSE : 0.8854
Itération 600 — MSE : 0.8819
Itération 800 — MSE : 0.8818
a (GD) = 0.8359
b (GD) = -73.213
a (OLS) = 0.8364
b (OLS) = -73.273
Si vous avez des valeurs aberrantes (NaN ou inf), votre α est trop grand. Si ça converge en 1 itération, il est trop petit. Trouver le bon α, c'est un art. On appelle ça le réglage d'hyperparamètres. En entreprise, on fait tourner 500 expériences et on garde la meilleure. Ici, on ajuste à l'œil. La méthode de l'ingénieur.

Variation : prédire la température d'un CPU

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)


§2 Partie II — Interpolation polynomiale

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

Ah, les polynômes. Vous les avez vus au cours 2, vous les avez dérivés, intégrés. Maintenant vous allez les apprendre à partir de données. C'est comme si votre prof de maths vous demandait de deviner la fonction. Sauf que cette fois, c'est l'ordinateur qui devine. (Vous pouvez ricaner méchamment.)

2.1 Feature engineering — créer une matrice polynomiale

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.

Exercice 2.1 — Matrice de Vandermonde

Implémentez une fonction polynomial_features(x, deg) qui transforme une liste x en une matrice 2D : chaque ligne i contient [xᵢ⁰, xᵢ¹, ..., xᵢdeg].

Exemple : pour 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³
Solution
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]], "...")
x : [0.0, 0.2, 0.4, 0.6, 0.8] ...
y : [0.048, 0.233, 0.346, 0.574, 0.693] ...

2.2 Régression polynomiale par la formule analytique

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.

Exercice 2.2 — Implémentez la régression polynomiale

En utilisant les fonctions du cours 3 (transpose, produit_matrice) et la fonction d'inversion fournie ci-dessous, écrivez regression_polynomiale(x, y, deg) qui :
1. Construit la matrice polynomiale X
2. Calcule θ = (XT·X)−1·XT·y
3. Retourne la liste des coefficients [a₀, a₁, ..., a_d]

Testez pour degré 3, 5 et 9 sur les données sinus.
Solution
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])
Coefficients (deg 5) : [0.0055, 0.9981, 0.0141, -0.1592, 0.0038, 0.0061]
Vous avez probablement croisé XT·X en cours d'algèbre linéaire en disant « à quoi ça sert dans la vraie vie ? ». Ben voilà. C'est maintenant. Savourez cet instant de validation existentielle. (Et oui, vos profs avaient raison depuis le début.)
Exercice 2.3 — Prédiction et visualisation textuelle

Écrivez une fonction evalue_polynome(coeffs, x) (utilisez Horner du cours 2) et prédisez les valeurs pour les x d'origine.

Affichez pour chaque point : x, y_vrai, y_prédit, et l'erreur.

Essayez degré = 1, 3, 5, 9. Que constatez-vous ? Pourquoi degré 9 pourrait être un problème ?
Solution
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))
x      vrai    prédit   err
0.0    0.048    0.048    0.0000
0.2    0.233    0.236    0.0033
0.4    0.346    0.382    0.0362
...
3.6    -0.432   -0.478   -0.0462
3.8    -0.645   -0.630   0.0151
Le surapprentissage (overfitting) : avec un polynôme de degré 9 sur 20 points, la courbe passe par tous les points... mais fait n'importe quoi entre eux. Le modèle a « appris par cœur » le bruit au lieu de la tendance. C'est le piège numéro 1 du machine learning. On appelle ça le bias-variance tradeoff. Un modèle trop simple (degré 1) ne capte pas la courbe (biais élevé). Trop complexe (degré 9), il devient instable (variance élevée). Trouver le juste milieu, c'est ça, le métier.

§3 Partie III — Corrélation entre deux séries

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.

Le coefficient de corrélation de Pearson (noté r) mesure la force et la direction d'une relation linéaire entre deux variables. Il vaut entre −1 (corrélation négative parfaite) et +1 (corrélation positive parfaite). 0 = pas de corrélation linéaire. Il a été inventé par Karl Pearson (1857–1936), élève de Galton et père de la statistique moderne.

r = (Σ (xᵢ − x̄)(yᵢ − ȳ)) / √(Σ (xᵢ − x̄)² × Σ (yᵢ − ȳ)²)

Exercice 3.1 — Coefficient de corrélation de Pearson

Implémentez correlation(x, y) qui calcule r.

Testez sur les données taille/poids du §1 (r devrait être proche de 1).

Testez sur ces données :
heures = [1, 2, 3, 4, 5, 6, 7, 8]
notes  = [2, 5, 8, 9, 12, 14, 16, 18]
Que vaut r ? Interprétez.
Solution
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))
Corrélation taille/poids : 0.9975
Corrélation heures/notes : 0.9947

3.1 Relation a·x + b·y = c

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 ?

Exercice 3.2 — Trouver a et b pour a·x + b·y = c

En partant de l'idée qu'on peut réécrire y = (−a/b)·x + (c/b), refaites une régression linéaire de y en fonction de x pour trouver la pente et l'ordonnée à l'origine. Déduisez-en (a, b, c) sachant qu'on peut fixer, par exemple, a = pente et b = −1, puis c = ordonnée.
Solution
# 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 ✓
y = 2.285714285714286 × x + -0.5
Relation : 2.286 × x + (-1) × y = 0.5
a·x + b·y = c, c'est l'équation de droite qu'on voit au collège. Sauf qu'au collège, on vous donnait a, b et c. Ici, vous les avez appris à partir de données. Vous avez fait de l'apprentissage automatique sans le savoir. La machine a « compris » que plus on révise, meilleure est la note. Scandale : les élèves le savaient déjà. Mais maintenant, c'est prouvé par l'algèbre linéaire.

Variation : corrélation négative

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).


§4 Partie IV — Passage aux bibliothèques

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 :

4.1 La notation point — c'est quoi ce mystère ?

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.

ExpressionSignification
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)
Le point, c'est le « de » en Python. 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.)

4.2 NumPy — le couteau suisse du calcul numérique

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
[ 3 6 9 12 15]
[2 4 6 8 10]
[ 1 4 9 16 25]
3.0
[[1. 1.]
[1. 1.]]
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.

4.3 Matplotlib — dessiner pour comprendre

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
John D. Hunter (1968–2012) — neurobiologiste et créateur de Matplotlib. Il avait besoin d'un outil pour visualiser des signaux neurologiques. Aucun outil existant ne lui convenait, alors il a écrit le sien. En 2003, il publie Matplotlib. Aujourd'hui, c'est la bibliothèque de visualisation la plus utilisée en Python. Un bel exemple de « scratch your own itch ».
Exercice 4.1 — Visualisez l'interpolation polynomiale

Utilisez NumPy et Matplotlib pour tracer :
1. Les points du sinus (x_sin, y_sin) en cercles
2. Le polynôme de degré 3 en trait bleu
3. Le polynôme de degré 9 en trait rouge pointillé
4. La vraie fonction sin(x) en trait vert fin

Utilisez 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 ?

§5 Pour aller plus loin

5.1 Régression multiple (plusieurs variables d'entrée)

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.

5.2 Descente de gradient stochastique (SGD)

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.

5.3 Coefficient de détermination R²

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² taille/poids : 0.9949

R² = 0.995 → le modèle explique 99.5% de la variance. Pas mal.


§6 Le mot de la fin

Ce TD vous a fait traverser tout le pipeline du machine learning :

  1. Données — génération, chargement
  2. Modèle — linéaire, polynomial
  3. Apprentissage — moindres carrés, descente de gradient
  4. Évaluation — MSE, R², corrélation
  5. Visualisation — avec matplotlib

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).

Andrej Karpathy (né en 1986), directeur de l'IA chez Tesla, ex-OpenAI, a un mantra : « Don't be a framework ninja. Be a ml engineer. » Comprenez : savoir utiliser TensorFlow ne fait pas de vous un ingénieur en IA. Comprendre ce qu'il se passe quand on fait model.fit() — les gradients, les mises à jour, les matrices — ça, c'est être ingénieur.
Bilan du TD : Prochaine étape : les réseaux de neurones à partir de zéro. Mais ça, c'est pour le prochain TD. Allez, reposez-vous. Vous l'avez mérité. 🏆
© 2026 — laurent.thiry@uha.fr