Il Nostro Primo Modello di Machine Learning: Training e Predizione
Parte (2/2)
Potrebbe interessarti:
Il progetto Python, che comprende gli script mostrati in questo post, e quelli relativi alla creazione dei grafici Matplotlib e Plotly, è disponibile assieme al corso Introduzione al Machine Learning.
In questo post riprendiamo l’implementazione del modello di regressione lineare semplice, che abbiamo iniziato a discutere qui. Dopo aver compreso come opera la funzione di costo MSE, vediamo ora il processo di addestramento del modello, necessario per individuare i valori ottimali dei parametri del predittore.
Il Processo di Training
Il processo di training ha l’obiettivo di determinare i valori dei parametri $w$ e $b$ che minimizzano la funzione di costo del modello. Nel nostro esempio, utilizziamo come funzione di costo il Mean Squared Error (MSE):
$$ \begin{align*} \text{MSE}(w,b) &= \frac{1}{m}\sum_{i=1}^{m} \left(y^{(i)}-\hat{y}^{(i)}\right)^2 \\[12pt] &= \frac{1}{m}\sum_{i=1}^{m} \left[y^{(i)}-\left(wx^{(i)}+b\right)\right]^2 \end{align*} $$La funzione di costo dipende quindi dai parametri $w$ e $b$: modificando i loro valori cambiano le previsioni $\hat{y}^{(i)}$ e, di conseguenza, cambia anche il valore dell’MSE.
L’obiettivo del training può quindi essere espresso come un problema di ottimizzazione: trovare i valori di $w$ e $b$ per i quali l’MSE assume il valore più basso possibile.
La Forma della Funzione di Costo
Poiché la funzione di costo dipende da due parametri, $w$ e $b$, per rappresentarla completamente dovremmo utilizzare un grafico tridimensionale: un asse per $w$, uno per $b$ e uno per il valore di $\text{MSE}(w,b)$. Per semplificare la visualizzazione, possiamo però mantenere $b$ costante, ad esempio fissandolo a $0$, e osservare come varia l’MSE al variare del solo parametro $w$:

Otteniamo una curva convessa rispetto a $w$, con la caratteristica forma a “coppa”. Il marcatore a croce indica il punto
$$ \left(w,\text{MSE}(w,0)\right) $$nel quale la funzione di costo assume il suo valore minimo.
Da un punto di vista geometrico, una funzione è convessa su un intervallo se, scelti due qualsiasi punti del suo grafico, il segmento che li unisce si trova al di sopra del grafico stesso o coincide con esso.
La convessità è una proprietà particolarmente utile nei problemi di ottimizzazione. Nel nostro caso, la funzione MSE presenta inoltre due caratteristiche importanti:
è derivabile rispetto ai parametri $w$ e $b$, quindi possiamo calcolare la sua pendenza e determinare in quale direzione modificarli per ridurre l’errore;
non presenta minimi locali peggiori del minimo globale: un eventuale minimo locale è anche un minimo globale. In condizioni che garantiscono un’unica soluzione, la funzione presenta quindi un unico punto di minimo.
Osservando il grafico in Figura 7, possiamo immaginare di lasciare cadere una biglia lungo la curva a forma di coppa. La pendenza della curva indica in quale direzione si trova il punto più basso. L’idea è simile a quella utilizzata durante il training: partendo da determinati valori dei parametri, cerchiamo progressivamente di modificarli nella direzione che riduce la funzione di costo, avvicinandoci al suo minimo.
La situazione può essere più complessa nel caso di una funzione non convessa, come quella rappresentata in Figura 8:

In questo caso possono esistere diversi minimi locali, cioè punti che rappresentano il valore più basso della funzione soltanto rispetto a una regione circostante, ma che non corrispondono necessariamente al minimo globale. Nella figura, gli indicatori arancione e rosso rappresentano due minimi locali, mentre il marcatore giallo indica il minimo globale. Riprendendo l’analogia della biglia, se questa partisse da una determinata zona della curva potrebbe raggiungere uno dei minimi locali senza arrivare al punto più basso dell’intera funzione.
La forma convessa della funzione di costo della regressione lineare rende invece il problema di ottimizzazione particolarmente favorevole. Possiamo quindi utilizzare un algoritmo chiamato Discesa del Gradiente (Gradient Descent, GD), che modifica iterativamente i parametri del modello cercando di ridurre progressivamente il valore della funzione di costo.
Per comprendere come funziona la Discesa del Gradiente, dobbiamo però introdurre prima il concetto di gradiente di una funzione.
Il Gradiente di una Funzione
Intuitivamente, il gradiente descrive come varia una funzione in un determinato punto, indicando la direzione in cui essa cresce più rapidamente. Nel caso di una funzione $f(x)$ di una sola variabile, questo ruolo è svolto dalla derivata prima:
$$ f'(x) = \frac{df(x)}{dx} $$La derivata indica infatti la pendenza della funzione in un determinato punto e ci permette di capire se, aumentando $x$, la funzione sta crescendo oppure diminuendo.
Quando invece abbiamo una funzione di più variabili, il concetto di derivata viene generalizzato attraverso il gradiente. Consideriamo una funzione $f(\mathbf{x})$ dove $\mathbf{x}$ è un vettore formato da $n$ variabili:
$$ \mathbf{x} = \begin{bmatrix} x_1 & x_2 & \cdots & x_n \end{bmatrix}^{\intercal} $$Il gradiente di $f$ è il vettore che contiene le derivate parziali della funzione rispetto a ciascuna delle sue variabili:
$$ \nabla f(\mathbf{x}) = \begin{bmatrix} \dfrac{\partial f(\mathbf{x})}{\partial x_1}\\[12pt] \dfrac{\partial f(\mathbf{x})}{\partial x_2}\\[12pt] \vdots\\[12pt] \dfrac{\partial f(\mathbf{x})}{\partial x_n} \end{bmatrix} $$dove:
il simbolo $\nabla$ si chiama nabla;
$\partial f(\mathbf{x}) / \partial x_j$ rappresenta la derivata parziale di $f$ rispetto alla variabile $x_j$.
Ogni componente del gradiente indica quindi quanto e in quale verso varia la funzione quando modifichiamo una determinata variabile, mantenendo costanti le altre. In particolare:
- una componente positiva indica che $f$ aumenta all’aumentare della corrispondente variabile;
- una componente negativa indica che $f$ diminuisce all’aumentare della corrispondente variabile;
- una componente uguale a zero indica che, in quel punto, una piccola variazione della corrispondente variabile non produce una variazione di $f$ al primo ordine.
Nel suo insieme, il gradiente $\nabla f(\mathbf{x})$ punta nella direzione di massima crescita della funzione, cioè nella direzione della salita più ripida. La direzione opposta, $-\nabla f(\mathbf{x})$ indica invece la direzione di massima discesa.
Consideriamo, ad esempio, la funzione:
$$ f(\mathbf{x}) = x_1+x_2 $$Le sue derivate parziali sono:
$$ \frac{\partial f(\mathbf{x})}{\partial x_1}=1 \qquad \frac{\partial f(\mathbf{x})}{\partial x_2}=1 $$e quindi:
$$ \nabla f(\mathbf{x}) = \begin{bmatrix} \dfrac{\partial f(\mathbf{x})}{\partial x_1}\\[12pt] \dfrac{\partial f(\mathbf{x})}{\partial x_2} \end{bmatrix} = \begin{bmatrix} 1\\ 1 \end{bmatrix} $$Il gradiente è costante in ogni punto: la funzione cresce più rapidamente muovendosi nella direzione
$$ \begin{bmatrix} 1\\ 1 \end{bmatrix} $$cioè aumentando contemporaneamente $x_1$ e $x_2$.
Possiamo ora applicare lo stesso concetto alla nostra funzione di costo.
Se fissiamo $b=0$, l’MSE dipende solamente dal parametro $w$.
In questo caso, il gradiente si riduce alla derivata della funzione di costo rispetto a $w$:
che indica la pendenza della curva in ogni punto:

Consideriamo ora la funzione di costo completa, che dipende da entrambi i parametri $w$ e $b$:
$$ \text{MSE}(w,b) $$Il suo gradiente è:
$$ \nabla\text{MSE}(w,b) = \begin{bmatrix} \dfrac{\partial\text{MSE}(w,b)}{\partial w}\\[10pt] \dfrac{\partial\text{MSE}(w,b)}{\partial b} \end{bmatrix} $$La prima componente indica come varia la funzione di costo modificando $w$, mentre la seconda indica come varia modificando $b$.
Il grafico interattivo seguente mostra questo concetto sulla superficie tridimensionale della funzione di costo. Per ogni marcatore giallo presente sulla superficie, il vettore grigio rappresenta, opportunamente scalato, il gradiente $\nabla\text{MSE}(w,b)$, e indica come devono variare i parametri $w$ e $b$ per muoversi nella direzione di massima crescita della funzione di costo.
Il vettore viene rappresentato parallelo al piano $wb$ perché le sue componenti descrivono variazioni dei parametri $w$ e $b$. Il valore dell’MSE viene invece rappresentato sull’asse verticale. Il marcatore arancione mostra la posizione ottenuta modificando $w$ e $b$ nella direzione del gradiente e ricalcolando successivamente il valore della funzione di costo:
Questo esempio mostra quindi che muoversi nella direzione del gradiente aumenta la funzione di costo. Per addestrare il modello vogliamo ottenere esattamente il comportamento opposto: ridurre l’MSE. La Discesa del Gradiente aggiornerà quindi i parametri $w$ e $b$ muovendosi nella direzione opposta al gradiente, cioè lungo:
$$ -\nabla\text{MSE}(w,b) $$in modo da avvicinarsi progressivamente al minimo della funzione di costo.
La Discesa del Gradiente
La Discesa del Gradiente (Gradient Descent) è un algoritmo di ottimizzazione utilizzato per avvicinarsi iterativamente al minimo di una funzione $f$.
Come abbiamo visto, il gradiente $\nabla f$ indica, in un determinato punto, la direzione di massima crescita della funzione. Di conseguenza, la direzione opposta, $-\nabla f$, indica la direzione di massima discesa, cioè quella lungo la quale il valore della funzione diminuisce più rapidamente localmente.
Per comprendere intuitivamente il funzionamento dell’algoritmo, immaginiamo di trovarci su una collina e di voler raggiungere la valle, potendo osservare soltanto una piccola porzione del terreno circostante. Possiamo procedere in questo modo:
- scegliamo un punto di partenza;
- misuriamo la pendenza del terreno in quel punto;
- individuiamo la direzione di discesa più ripida;
- facciamo un piccolo passo in quella direzione;
- dalla nuova posizione, calcoliamo nuovamente la pendenza e ripetiamo il procedimento.
Procedendo iterativamente in questo modo, ci spostiamo verso zone sempre più basse, fino a raggiungere un punto in cui la pendenza è nulla o sufficientemente piccola. In questa analogia, il terreno rappresenta la funzione che vogliamo minimizzare, mentre la valle rappresenta un suo punto di minimo.
Nel nostro modello di regressione lineare, la funzione da minimizzare è la funzione di costo $\text{MSE}(w,b)$, e la posizione sul terreno è determinata dai valori correnti dei parametri $w$ e $b$.
Minimizzare la funzione di costo significa quindi cercare i valori dei parametri che producono il minor errore possibile sul dataset di training. Ad ogni iterazione, la Discesa del Gradiente utilizza il gradiente della funzione di costo per determinare come modificare $w$ e $b$ in modo da ridurre progressivamente l’MSE e avvicinarsi al suo minimo.
Riprendiamo ora lo studio della nostra funzione di costo, e sviluppiamo l’algoritmo tramite i seguenti punti:
Inizializzazione dei parametri della funzione di costo:
$$ \begin{align*} w & = 0\\ b & = 0 \end{align*} $$
ancora una volta, consideriamo solo il parametro $w$, che inizializziamo con un valore casuale (oppure a 0), e teniamo il valore del parametro $b$ costante a $0$:Calcolo del gradiente della funzione di costo:
$$ \begin{align*} g_w &= \pdv{\text{MSE}}{w}\\[8pt] &= \frac{2}{m}\sum_{i=1}^{m} x^{(i)}\left(\hat{y}^{(i)}-y^{(i)}\right) \end{align*} $$
per determinare come varia la funzione di costo rispetto al valore corrente del parametro $w$, dobbiamo calcolare la derivata parziale dell’MSE rispetto a $w$:Il valore $g_w$ indica quindi quanto e in quale direzione varia la funzione di costo rispetto a una variazione del parametro $w$.
Prima di aggiornare $w$, moltiplichiamo il gradiente per una costante positiva $\eta$ (eta), chiamata learning rate o tasso di apprendimento:
$$ \eta\,g_w $$Il learning rate controlla l’ampiezza del passo effettuato a ogni iterazione della Discesa del Gradiente. Un valore sufficientemente piccolo, ad esempio $0.01$, permette di modificare $w$ gradualmente, evitando aggiornamenti troppo grandi che potrebbero farci oltrepassare il minimo della funzione di costo. Il valore appropriato di $\eta$, tuttavia, dipende dal problema considerato.
La derivata dell'MSE rispetto al parametro $w$ considera tutte le osservazioni del dataset di training. Ogni osservazione contribuisce attraverso il termine $$ x^{(i)}\left(\hat{y}^{(i)}-y^{(i)}\right) $$ che combina il valore della feature $x^{(i)}$ con l'errore di previsione $\hat{y}^{(i)}-y^{(i)}$. La somma di questi contributi, divisa per il numero $m$ di osservazioni, determina come deve variare complessivamente il parametro $w$ per modificare la funzione di costo.La derivata della funzione di costo $\text{MSE}$ rispetto al parametro $w$, ovvero $∂\text{MSE}/∂w$, può essere calcolata come: $$ \begin{align*} \pdv{\text{MSE}}{w} & = \frac{\partial}{\partial w} \left( \frac{1}{m} \sum_{i=1}^{m} \left[ y^{(i)} - \left(w x^{(i)} + b\right) \right]^2 \right)\\[12pt] & = \frac{1}{m} \sum_{i=1}^{m} \frac{\partial}{\partial w} \left( \left[ y^{(i)} - \left(w x^{(i)} + b\right) \right]^2 \right) \tag{Nota 1, 2} \\[12pt] & = \frac{1}{m} \sum_{i=1}^{m} 2 \left[ y^{(i)} - \left(w x^{(i)} + b\right) \right] \cdot \frac{\partial}{\partial w} \left[ y^{(i)} - \left(w x^{(i)} + b\right) \right] \quad \tag{Nota 3} \\[12pt] & = \frac{1}{m} \sum_{i=1}^{m} 2 \left[ y^{(i)} - \left(w x^{(i)} + b\right) \right] \cdot \left(-x^{(i)}\right)\\[12pt] & = -\frac{2}{m} \sum_{i=1}^{m} x^{(i)} \left[ y^{(i)} - \left(w x^{(i)} + b\right) \right]\\[12pt] & = \frac{2}{m} \sum_{i=1}^{m} x^{(i)} \left[ \left(w x^{(i)} + b\right) - y^{(i)} \right]\\[12pt] & = \frac{2}{m} \sum_{i=1}^{m} x^{(i)} \left( \hat{y}^{(i)} - y^{(i)} \right)\\[12pt] \end{align*} $$- $\text{Nota 1}$ : la derivata di una costante moltiplicata per una funzione è uguale alla costante moltiplicata per la derivata della funzione.
- $\text{Nota 2}$ : la derivata di una somma è la somma delle derivate.
- $\text{Nota 3}$ : chain rule applicata a $\left[ y^{(i)} - \left(w x^{(i)} + b\right) \right]^2$ $$ \begin{align*} \dv{f(g(x))}{x} & = \dv{f}{g} \cdot \dv{g}{x}\\[6pt] \end{align*} $$
Aggiornamento dei parametri del modello:
$$ \begin{align*} w & = w - \eta g_w \end{align*} $$
dopo aver calcolato il gradiente e averlo scalato tramite il learning rate $\eta$, possiamo aggiornare il parametro $w$:Perché sottraiamo il gradiente? Come abbiamo visto, il gradiente indica la direzione di massima crescita della funzione di costo. Il nostro obiettivo è invece ridurre il valore dell’MSE e procedere verso il suo minimo. Dobbiamo quindi muoverci nella direzione opposta al gradiente, cioè nella direzione di massima discesa.
Il termine $\eta g_w$ determina l’ampiezza e il verso dell’aggiornamento di $w$: il gradiente $g_w$ indica come varia la funzione di costo rispetto a $w$, mentre il learning rate $\eta$ controlla la dimensione del passo effettuato a ogni iterazione.
Esecuzione di una nuova iterazione:
Rieseguiamo i punti 2 e 3 per un certo numero di iterazioni, o fino a che non raggiungiamo un certo valore della funzione di costo.Fine dell’algoritmo:
Alla fine del ciclo di iterazioni, se la scelta del coefficiente $\eta$ è stata corretta, ed il numero di iterazioni è stato sufficiente, il parametro $w$ dovrebbe minimizzare $\text{MSE}$, oppure essere vicino al punto di minima globale di tale funzione.
Gli effetti dell’algoritmo della Discesa del Gradiente, applicato alla funzione di perdita MSE, vengono mostrati dal grafico seguente. Partiamo da $w = -1$, e utilizziamo $6$ iterazioni. Le frecce grigie rappresentano il vettore $\eta g_w$ (usato per incrementare il valore di $w$), calcolato a ogni iterazione:

Modelli Lineari: perché la Linearità nei Parametri semplifica l'Apprendimento
Ritorniamo al concetto di Modello Lineare. Quando il modello è lineare nei parametri, come nella regressione lineare $\hat{y}=wx+b$, e utilizziamo come funzione di costo l’errore quadratico medio (MSE), il problema di ottimizzazione assume una forma particolarmente semplice.
L’MSE è infatti una funzione quadratica e convessa rispetto ai parametri $w$ e $b$. Di conseguenza, non presenta minimi locali peggiori del minimo globale: ogni minimo locale è anche un minimo globale. Quando i dati garantiscono una soluzione unica, esiste inoltre un unico punto di minimo.
La ricerca dei parametri ottimali può essere risolta analiticamente, attraverso una soluzione in forma chiusa (che ora non discutiamo), oppure numericamente, utilizzando algoritmi di ottimizzazione come la Discesa del Gradiente.
Se invece il modello dipende dai parametri in modo non lineare, come ad esempio $\hat{y}=w^2x+b$, oppure $\hat{y}=e^{wx}$, la funzione di costo può assumere una forma più complessa e non è più garantito che sia convessa rispetto ai parametri. In questi casi, il processo di ottimizzazione può diventare più difficile: possono comparire minimi locali o altri punti stazionari e il risultato dell’ottimizzazione può dipendere maggiormente dai valori iniziali dei parametri.
La linearità nei parametri, quindi, non rende automaticamente semplice qualsiasi problema di Machine Learning, ma nel caso della regressione lineare con MSE produce un problema di ottimizzazione convesso e particolarmente favorevole.
Implementazione Parziale
Creiamo una prima implementazione della Discesa del Gradiente tramite il codice della funzione train_w,
che considera solamente il parametro $w$, lasciando il bias $b$ uguale a $0$:
from typing import Tuple
import numpy as np
from matplotlib import pyplot as plt
import seaborn as sns
def predict(X: np.ndarray, w: float, b: float) -> np.ndarray:
"""Compute predictions for each value in the input array X, giving the weight w and bias b."""
return w * X + b
def loss(X: np.ndarray, y: np.ndarray, w: float, b: float) -> float:
"""Compute the MSE cost value for the input examples."""
# MSE = 1/m * sum(i=1 to m) { (y^(i) - ŷ^(i))^2 }
y_hat = predict(X, w, b)
error = y_hat - y
squared_error = error ** 2
loss: float = np.average(squared_error)
return loss
def gradient_w(X: np.ndarray, y: np.ndarray, w: float) -> float:
"""Compute the derivative of the loss function L(w, b) with respect to 'w', keeping 'b' = 0."""
# ∂L/∂w = 2/m * (X.T @ (ŷ - y))
y_hat = predict(X, w, 0)
grad_w: float = 2 * np.average(X * (y_hat - y))
return grad_w
def train_w(X: np.ndarray, y: np.ndarray, iterations: int, learning_rate: float, w: float):
"""Compute the optimal value for "w" parameter, keeping the bias term "b" constant with value 0."""
# keep parameter b constant
b = 0
# array used to plot the loss graph
loss_info = np.full((iterations, 3), np.inf)
# loop through all the iterations
for i in range(iterations):
# save the w, b and loss values for the current iteration
loss_info[i] = [w, b, loss(X, y, w, b)]
# compute the gradient ∂L/∂w
grad_w = gradient_w(X, y, w)
# the scaled step in the direction of the w-axis
# used to reach the flat point on the loss surface
step_w: float = grad_w * learning_rate
# by convention, the gradient at a certain point is the direction vector
# that points directly uphill from that point.
# Because we need to go into the opposite direction,
# we have to subtract the gradient value from the current value of "w"
w = w - step_w
# if the choice of the learning_rate was correct,
# and the number of iterations was sufficient,
# now the parameter w should minimize the loss function,
# or be close to the global minimum point of that function
return w, loss_info
Defininiamo inoltre la funzione target_function per generare il nostro dataset,
e plot_hypothesis_and_losses per tracciare il grafico della funzione di ipotesi rispetto alle osservazioni del dataset,
e la curva relativa alla funzione di costo MSE, per ogni iterazione della fase di addestramento del modello:
def target_function(n_examples) -> np.ndarray:
"""This is the UNKNOWN real target function. Do not read its code!"""
# define the linear function parameters
w = 2.5
b = 10.0
random_coeff = 8
# generate points for the independent variable
X = np.sort(np.random.rand(n_examples) * random_coeff)
# generate the dependent variable with some noise, where the noise is defined by
# random float values sampled from a univariate "normal" (Gaussian) distribution
# of mean 0 and variance 1
noise = np.random.randn(n_examples)
y = w * X + b + noise
return X.reshape(-1, 1), y.reshape(-1, 1)
def plot_hypothesis_and_losses(X, y, w, b, losses, loss_ylim=None):
sns.set_theme()
fig, (ax1, ax2) = plt.subplots(2, 1)
fig.set_size_inches(8, 10)
# =========================================================================
# Subplot #1
# =========================================================================
# plot dataset
ax1.scatter(X, y, color="C0", edgecolor="black", zorder=2, label='Data points')
# plot hypothesis line
x = np.linspace(0, np.max(X), X.shape[0])
y_hat = predict(x, w, b)
ax1.plot(x, y_hat, color="green", zorder=1, label=f"Hypothesis Line")
# annotations
ax1.text(1, 1, f"w: {w:.2f}; b: {b:.2f}; loss: {loss(X, y, w, b):.2f}", fontsize=12)
ax1.set_title("Hypothesis function")
ax1.set_xlabel("x")
ax1.set_ylabel("y")
ax1.set_ylim([-5, np.max(y) + 10])
ax1.legend()
# =========================================================================
# Subplot #2
# =========================================================================
# plot the Loss curve
x = np.arange(0, len(losses))
ax2.plot(x, losses, color="red", label=f"MSE Loss Curve")
ax2.set_title("Losses")
ax2.set_xlabel("iteration")
ax2.set_ylabel("MSE Loss")
ax2.legend()
if loss_ylim is not None:
ax2.set_ylim(loss_ylim)
else:
ax2.set_ylim([-20, np.max(losses) + 20])
plt.tight_layout()
plt.show()
Vediamo che accade:
np.random.seed(42)
# get the data
X, y = target_function(50)
# train the system, computing the optimal value for "w"
w, loss_info = train_w(X, y, iterations=100, learning_rate=0.01, w=-8)
# plot the graph
plot_hypothesis_and_losses(X, y, w, b, loss_info[:, 2], loss_ylim=[-20, 300])

Il grafico “Hypothesis function” risultante mostra la retta della funzione di ipotesi passare per il punto $(0, 0)$, in quanto il parametro $b$ non è stato ottimizzato. Non potendo muoversi lungo l’asse $y$, la retta rimane relativamente distante dagli esempi del dataset, e il valore di costo minore che riusciamo ad ottenere è $29.50$.
Il grafico “Losses”, per ogni iterazione, mostra i valori ritornati dalla funzione di costo per i valori attuali dei parametri $w$ e $b$, in questo caso mantenendo il valore di $b$ “bloccato” a $0$.
Implementazione Finale
Per l’implementazione finale dell’algoritmo della Discesa del Gradiente, consideriamo anche il parametro $b$. Le operazioni matematiche che dobbiamo svolgere sono le stesse di prima, a cui vanno aggiunte quelle relative al calcolo della derivata della funzione di costo rispetto a $b$, e l’aggiornamento del valore del bias:
$$ \begin{align*} w & = 0\\[6pt] b & = 0\\[6pt] g_w & = \pdv{\text{MSE}}{w} = \frac{2}{m} \sum_{i=1}^{m} x^{(i)}\left(\hat{y}^{(i)} - y^{(i)}\right)\\[12pt] g_b & = \pdv{\text{MSE}}{b} = \frac{2}{m} \sum_{i=1}^{m} \left(\hat{y}^{(i)} - y^{(i)}\right)\\[12pt] w & = w - \eta g_w\\[6pt] b & = b - \eta g_b\\[6pt] \end{align*} $$Similmente a $∂\text{MSE}/∂w$, la derivata della funzione di costo $\text{MSE}$ rispetto a $b$ può essere calcolata come:
$$ \begin{align*} \pdv{\text{MSE}}{b} & = \frac{\partial}{\partial b} \left( \frac{1}{m} \sum_{i=1}^{m} \left[ y^{(i)} - (w x^{(i)} + b) \right]^2 \right)\\[12pt] & = \frac{1}{m} \sum_{i=1}^{m} \frac{\partial}{\partial b} \left( \left[ y^{(i)} - (w x^{(i)} + b) \right]^2 \right)\\[12pt] & = \frac{1}{m} \sum_{i=1}^{m} 2 \left[ y^{(i)} - (w x^{(i)} + b) \right] \cdot \frac{\partial}{\partial b} \left( y^{(i)} - (w x^{(i)} + b) \right)\\[12pt] & = \frac{1}{m} \sum_{i=1}^{m} 2 \left[ y^{(i)} - (w x^{(i)} + b) \right] \cdot (-1)\\[12pt] & = \frac{2}{m} \sum_{i=1}^{m} \left[ (w x^{(i)} + b) - y^{(i)} \right]\\[12pt] & = \frac{2}{m} \sum_{i=1}^{m} \left( \hat{y}^{(i)} - y^{(i)} \right)\\[12pt] \end{align*} $$Utilizziamo ora i metodi train e gradient (al posto di train_w e gradient_w)
per gestire anche il parametro di bias:
def train(X: np.ndarray, y: np.ndarray, iterations: int, learning_rate: float, w: float, b: float):
"""Compute the optimal values for "w" an "b" parameters."""
# array used to plot the loss graph
loss_info = np.full((iterations, 3), np.inf)
# loop through all the iterations
for i in range(iterations):
# save the w, b and loss values for the current iteration
loss_info[i] = [w, b, loss(X, y, w, b)]
# compute the gradient values ∂L/∂w and ∂L/∂b
grad_w, grad_b = gradient(X, y, w, b)
# compute the scaled step along the w-axis
step_w: float = grad_w * learning_rate
# compute the scaled step along the b-axis
step_b: float = grad_b * learning_rate
# subtract the scaled gradient values from the current values of "w" and "b"
w = w - step_w
b = b - step_b
return w, b, loss_info
def gradient(X: np.ndarray, y: np.ndarray, w: float, b: float) -> Tuple[float, float]:
"""Compute the derivative of the loss function L(w, b) with respect to 'w' and 'b'."""
# ∂L/∂w = 2/m * (X.T @ (ŷ - y))
# ∂L/∂b = 2/m * (1^T)(ŷ - y)
y_hat = predict(X, w, b)
grad_w: float = 2 * np.average(X * (y_hat - y))
grad_b: float = 2 * np.average(y_hat - y)
return grad_w, grad_b
Riusciremo ad avere risultati migliori?
# get the dataset
X, y = target_function(50)
# train the system, computing the optimal values for "w" and "b"
w, b, loss_info = train(X, y, iterations=500, learning_rate=0.01, w=-8, b=-8)
# plot the graph
plot_hypothesis_and_losses(X, y, w, b, loss_info[:, 2], loss_ylim=[-20, 300])

Come si può osservare dal nuovo grafico “Predictions”, la retta della funzione di ipotesi può ora spostarsi anche verticalmente lungo l’asse $y$, grazie alla variazione del parametro $b$, e adattarsi meglio agli esempi del dataset. Il valore della funzione di costo è infatti sceso da $29.50$ a $1.14$. Nel grafico “Losses”, dopo circa 300 iterazioni, la curva diventa quasi piatta: il valore dell’MSE continua a diminuire, ma le variazioni diventano progressivamente più piccole.
Vediamo ora il percorso seguito dalla Discesa del Gradiente sulla superficie generata dalla funzione di costo. Il grafico seguente mostra una selezione di circa venti posizioni $\left(w,b,\text{MSE}(w,b)\right)$ tra le 500 calcolate durante il processo di training. Il marcatore giallo rappresenta la nostra “biglia virtuale”, che vogliamo far scendere lungo la superficie verso il minimo della funzione di costo.
All’inizio del training, i parametri $w$ e $b$ vengono inizializzati entrambi al valore $-8$, posizionando il primo marcatore nella parte alta della superficie. I cinque indicatori successivi rappresentano le posizioni calcolate durante le prime cinque iterazioni. Possiamo osservare che inizialmente gli spostamenti sono relativamente ampi: in questa regione la superficie presenta una forte pendenza, in particolare rispetto al parametro $w$, e quindi la corrispondente componente del gradiente $\partial\text{MSE} / \partial w$ assume un valore elevato in valore assoluto.
Poiché l’ampiezza dell’aggiornamento dipende dal gradiente:
$$ w = w - \eta \frac{\partial\text{MSE}}{\partial w} $$un gradiente di maggiore intensità produce, a parità di learning rate $\eta$, uno spostamento più ampio. Avvicinandosi al minimo, la superficie diventa progressivamente meno inclinata,e la magnitudine del gradiente diminuisce. Di conseguenza, anche gli aggiornamenti dei parametri $w$ e $b$ diventano più piccoli e la Discesa del Gradiente procede con passi sempre più ridotti verso il minimo della funzione di costo:
Un Esempio di Predizione
Effettuato il training, e verificato il fatto che la curva della funzione di costo indichi un buon addestramento,
possiamo utilizzare il nostro modello per effettuare previsioni di valori continui chiamando il metodo predict:
def plot_predictions(X, y, X_pred, w, b, ylim=None):
plt.figure(figsize=(8, 5))
# plot hypothesis line
x = np.linspace(0, X.max(), X.shape[0])
plt.plot(x, predict(x.reshape(-1, 1), w, b), color="green", zorder=1, alpha=0.5, label=f"h")
# plot dataset
plt.scatter(X, y, color="C0", edgecolor="black", zorder=2, alpha=0.3, label='Data points')
# plot predictions
plt.scatter(X_pred, predict(X_pred, w, b), color="green", edgecolor="black", zorder=3,
label='Predictions')
plt.xlabel("x")
plt.ylabel("y")
if ylim is not None:
plt.ylim(ylim)
plt.legend()
plt.tight_layout()
plt.show()
sns.set_theme()
np.random.seed(42)
X, y = target_function(50)
# train the system, computing the optimal value for "w"
w, b, loss_info = train(X, y, iterations=500, learning_rate=0.05, w=0, b=0)
# make some predictions for x = 4, 5 and 6
X_pred = np.array([[4], [5], [6]])
y_hat = predict(X_pred,w, b)
plot_predictions(X, y, X_pred, w, b, ylim=[-5, np.max(y) + 10])
for x_i, y_hat_i in zip(X_pred.flatten(), y_hat.flatten()):
print(f"prediction for x = {x_i} : {y_hat_i:.2f}")

prediction for x = 4 : 20.01
prediction for x = 5 : 22.53
prediction for x = 6 : 25.05
Guardando il grafico Figura 15 e la retta verde generata dalla funzione di ipotesi, possiamo vedere come le predizioni ritornate - i punti verdi - corrispondano ai valori $y$ sulla retta, relativi ai valori immessi sull’asse $x$.
Ricapitolando
In questa sezione abbiamo visto i concetti più importanti relativi alla definizione e all’implementazione di un modello ML di regressione lineare. Un regressore lineare è un tipo di algoritmo di apprendimento supervisionato usato per prevedere una variabile di output continua e numerica, in base a una o più variabili indipendenti di input. Per semplicità, abbiamo usato la regressione lineare semplice, che utilizza una sola variabile indipendente di input. Abbiamo quindi visto:
- la funzione target
- la funzione di ipotesi ed i suoi parametri
- la funzione di costo associata
- il gradiente di una funzione
- il training iterativo del modello, e la minimizzazione della funzione di costo tramite l’algoritmo della Discesa del Gradiente
- l’uso della funzione di ipotesi per predirre valori continui.