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
I valori ottimali dei parametri $w$ e $b$ che vanno a minimizzare la funzione di costo associata $(4)$, vengono determinati dal processo di training del modello. Ricordiamoci che, in questo esempio, la funzione di costo $\text{MSE}$ è una funzione nelle due variabili $w$ e $b$, e per essere calcolata, richiama la funzione di ipotesi $\hat{y}^{(i)} = wx^{(i)} + b$:
$$ \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\\[12pt] \end{align*} $$Per prima cosa, osserviamo come appare il grafico della funzione di costo $\text{MSE}$. Poiché la funzione dipende da entrambi i parametri $w$ e $b$, per rappresentarla completamente dovremmo usare un grafico tridimensionale: un asse per $w$, uno per $b$ e uno per il valore di $\text{MSE}(w, b)$. Tuttavia, un grafico 3D è spesso più difficile da analizzare e interpretare. Per semplificare, rappresentiamo la funzione in un piano cartesiano 2D, osservando come $\text{MSE}$ varia al cambiare di $w$, mentre manteniamo $b$ costante (ad esempio fissandolo a $0$):

Quello che vediamo è il grafico di una funzione convessa di una sola variabile, $w$. Il marcatore a croce indica il punto $\left(w, \text{MSE}(w, 0)\right)$ dove il valore della funzione di costo è minimo.
Da un punto di vista geometrico, una funzione è convessa su un intervallo se, per ogni coppia di punti del suo grafico, il segmento di retta che li unisce si trova al di sopra o coincide con il grafico stesso. Nel machine learning, una funzione di costo convessa presenta alcune proprietà molto utili:
è spesso derivabile in tutto il suo dominio (come nel caso di $\text{MSE}$), il che significa che possiamo calcolare la pendenza (o derivata) in ogni punto dove la funzione è definita.
ha un solo minimo locale, che coincide con il minimo globale. In altre parole, il punto in cui la derivata è uguale a zero - cioè dove la funzione smette di crescere o decrescere - corrisponde al punto in cui la funzione assume il suo valore più basso.
Osservando il grafico in Figura 7, possiamo immaginare di lasciare cadere una biglia lungo la curva a forma di “coppa”. Qualunque sia il punto di partenza sull’asse $w$, la biglia scivolerà sempre verso il punto più basso, dove si trova l’indicatore giallo. Questo accade perché la curva non presenta altre buche o zone piatte che possano fermarla prima di raggiungere il minimo globale della funzione.
In Figura 8, invece, la funzione non è convessa. Qui gli indicatori arancione e rosso segnano minimi locali, cioè punti in cui la funzione sembra minima solo in un piccolo intervallo, ma non rappresentano il minimo globale, evidenziato dal marcatore giallo. Se la biglia venisse lasciata cadere, ad esempio, in corrispondenza di $x = 5$, essa si fermerebbe nel minimo locale indicato dall’indicatore arancione, senza raggiungere il punto più basso della funzione:

Le proprietà precedentemente descritte di una funzione convessa permettono di usare un algoritmo di ottimizzazione chiamato Discesa del Gradiente (GD, Gradient Descent), che consente di avvicinarsi al minimo di una funzione in modo iterativo, seguendo la direzione della “discesa più ripida”, indicata dal gradiente della funzione in un punto specifico. Però, prima di poter capire come funziona la Discesa del Gradiente, dobbiamo definire meglio cos’è il gradiente di una funzione.
Il Gradiente di una Funzione
Intuitivamente, il gradiente è una misura della pendenza o della ripidità di una curva o di una superficie in un dato punto della stessa. Matematicamente, il gradiente è un vettore che contiene le derivate parziali della funzione, rispetto a ciascuna delle sue variabili di ingresso.
Nel caso di una funzione $f$ di una sola variabile, il gradiente è semplicemente la derivata prima di $f$ in un punto selezionato. Nel caso di una funzione di più variabili, il gradiente è un vettore di derivate parziali rispetto ad ognuna di queste variabili. Quindi, il gradiente di una funzione n-dimensionale $f\left(\textbf{x}\right)$ di un vettore feature $\textbf{x} = \left(x_1, x_2, \ldots, x_n\right)$ è definito come:
$$ \nabla f\left(\textbf{x}\right) = \begin{bmatrix} ∂f\left(\textbf{x}\right)/∂x_1\\[6pt] ∂f\left(\textbf{x}\right)/∂x_2\\[6pt] \cdots\\[6pt] ∂f\left(\textbf{x}\right)/∂x_n\\[6pt] \end{bmatrix} $$dove:
- il simbolo $\nabla$ si chiama “nabla”.
- $\textbf{x}$ in grassetto indica un vettore a più dimensioni (mentre $x$ indica un singolo valore scalare).
- $∂f(\textbf{x})/∂x_1$ indica la derivata parziale di $f$ rispetto a $x_1$.
- $x_1$, $\ldots$, $x_n$ rappresentano le $n$ variabili indipendenti di $f$, ovvero le feature del vettore $\textbf{x}$.
Ciascun componente del gradiente $\nabla f(\textbf{x})$ indica il tasso di variazione o la pendenza della funzione rispetto alle sue variabili indipendenti $x_1$, $\ldots$, $x_n$. Per convenzione, il gradiente in un determinato punto è un vettore che indica la direzione del massimo incremento di $f$, cioè la direzione della salita più ripida. Per esempio, se diciamo che:
$$ \begin{align*} f(\textbf{x}) & = x_1 + x_2\\[6pt] \nabla f(\textbf{x}) & = \begin{bmatrix} ∂f(\textbf{x})/∂x_1\\[6pt] ∂f(\textbf{x})/∂x_2\\[6pt] \end{bmatrix}\\[6pt] & = \begin{bmatrix} 1\\[6pt] 1\\[6pt] \end{bmatrix}\\[6pt] \end{align*} $$ciò significa che il percorso più ripido è sempre nella direzione $\begin{bmatrix}1&1\end{bmatrix}^\intercal$, relativamente ai parametri $x_1$ e $x_2$. Un gradiente negativo lungo l’asse $x_j$ indica che la curva diminuisce da sinistra a destra, ovvero i valori di $f$ diventano più piccoli all’aumentare del valore di $x_j$. In uno spazio 2D, possiamo visualizzare il gradiente della funzione di costo MSE in questo modo (settando $b = 0$):

Il grafico interattivo sottostante mostra lo stesso concetto, in uno spazio tridimensionale. Per ogni marcatore giallo presente sulla superficie generata dalla funzione di costo, il vettore grigio, parallelo al piano $wb$, indica il vettore di direzione (scalato) $\begin{bmatrix}∂\text{MSE}(w, b)/∂w & ∂\text{MSE}(w, b)/∂b\end{bmatrix}^\intercal$, che punta nella direzione del massimo incremento della funzione. Il marcatore arancio indica la nuova posizione del marcatore giallo, ottenuta incrementando i parametri $w$ e $b$ con i relativi gradienti, e ricalcolando la funzione di costo tramite i coefficienti aggiornati:
La Discesa del Gradiente
La Discesa del Gradiente è un algoritmo comunemente usato per avvicinarsi iterativamente al minimo di una funzione $f$, seguendo la direzione di discesa più ripida indicata dal gradiente di $f$ in un punto specifico. Come già detto, il gradiente di $f$, indicato con $\nabla f$, indica la direzione in cui la funzione cambia più rapidamente in quel punto.
Immaginiamo di essere su una collina, e di voler trovare la direzione più breve per raggiungere la valle, ma di poter vedere solo una piccola parte del territorio alla volta. Inizialmente, dobbiamo scegliere di proseguire per una certa direzione, e dopo un certo tragitto, dobbiamo ricalcolare la direzione più conveniente per raggiungere la nostra destinazione.
La Discesa del Gradiente è una strategia che ci aiuta ad arrivare “a valle” nel minor tempo possibile. Ecco come funziona:
- Per prima cosa, scegliamo a caso un punto di partenza sulla collina.
- Misuriamo la pendenza del terreno in quel punto, per individuare la direzione più ripida per arrivare a valle.
- Facciamo un piccolo passo in tale direzione.
- Ripetiamo i passaggi 2 e 3, per scendere gradualmente dalla collina.
- Continuiamo a procedere in questo modo fino a raggiungere un punto in cui la pendenza è quasi piatta, il che indica che siamo vicino alla valle.
In questa analogia, la collina rappresenta una funzione matematica che vogliamo minimizzare (ad esempio, una funzione di perdita come $\text{MSE}$). La valle, il punto più basso, corrisponde al valore minimo della funzione. Minimizzare una funzione $f$ significa trovare i valori dei parametri di $f$ che producono il valore di ritorno più piccolo possibile di quella funzione. In altre parole, si tratta di trovare il punto “più basso” del grafico di $f$.
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\\[6pt] b & = 0\\[6pt] \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} = \frac{2}{m} \sum_{i=1}^{m} x^{(i)}\left(\hat{y}^{(i)} - y^{(i)}\right)\\[6pt] \end{align*} $$
per ottenere il gradiente della funzione di costo rispetto al valore corrente di $w$, dobbiamo calcolare $∂\text{MSE}/∂w$:e scalare il valore risultante per una costante $\eta$ ($\text{eta}$), chiamata anche “learning rate”, solitamente avente un basso valore, come ad esempio $0.01$. Tale scalatura serve per procedere nella direzione segnalata “a piccoli passi”, in quanto usare tutta la lunghezza del vettore di direzione sarebbe controproducente.
La derivata di $\text{MSE}$ rispetto al parametro $w$, è calcolata tramite il valore medio dell'errore $\left(\hat{y}^{(i)} - y^{(i)}\right)$, considerando tutti gli esempi del dataset. Tale valore medio assicura che l'aggiornamento del parametro $w$ durante la discesa del gradiente rifletta il comportamento collettivo del dataset, piuttosto che essere dominato dalle singole osservazioni.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 della funzione di costo:
$$ \begin{align*} w & = w - \eta g_w \end{align*} $$
avendo il gradiente scalato, possiamo aggiornare il parametro $w$ sottraendo ad esso tale valore. Perchè eseguiamo una sottrazione? Abbiamo già visto che, per definizione, il gradiente punta nella direzione della salita più ripida. Noi dobbiamo scendere “verso valle” seguendo la discesa più ripida, e quindi dobbiamo andare nella direzione contraria: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. Se i suoi parametri compaiono in modo lineare, il processo di ottimizzazione - nel nostro caso, la ricerca dei valori migliori di $w$ e $b$ - diventa molto più semplice.
L’errore medio $\text{MSE}$ è una funzione convessa dei parametri; di conseguenza, esiste un solo minimo globale, e non ci sono “trappole” locali. L’ottimizzazione può essere risolta analiticamente, con una formula chiusa (un’espressione matematica che fornisce il risultato esatto in un solo passaggio), o numericamente, in modo efficiente, con algoritmi come la Discesa del Gradiente.
Se invece i parametri comparissero in modo non lineare (ad esempio: $y=w^2x + b$, oppure $y = e^{wx}$, tenendo costante $x$), la funzione di costo diventerebbe non convessa. Quindi sarebbe più difficile da minimizzare, con possibili minimi locali (al posto di un unico minimo globale), o instabilità numeriche del calcolo del gradiente, dovute alla superficie complessa e irregolare.
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ò notare dal nuovo grafico “Predictions”, la retta della funzione di ipotesi, potendo ora muoversi lungo l’asse $y$, può “avvicinarsi” maggiormente agli esempi del dataset. Il valore di costo, da $29.50$, è sceso a $1.14$. In questo caso, il grafico “Losses”, dopo l’interazione $300$, mostra una curva “quasi piatta”.
Vediamo ora il percorso calcolato dalla Discesa del Gradiente sulla superficie generata dalla funzione di costo. Il grafico seguente mostra le posizioni $\left(w, b, \text{MSE}(w, b)\right)$ del marcatore giallo - la nostra “biglia virtuale” che vogliamo far scendere verso il punto di minima della funzione - per una ventina di iterazioni, scelte tra le 500 usate dal processo di training.
All’inizio della funzione di training, i parametri $w$ e $b$ vengono inizializzati entrambi al valore $-8$, per posizionare il primo marcatore in cima alla superficie. I successivi cinque indicatori dall’alto rappresentano i valori calcolati nelle prime cinque iterazioni di training. Si noti come i relativi passi siano molto ampi, a causa dell’elevata pendenza della supeficie lungo l’asse $w$, che genera un valore del gradiente $w$ relativamente alto. Successivamente, verso valle, i passi diminuiscono di ampiezza, in quanto la pendenza della superficie diminuisce sensibilmente.
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.