Preparazione all'intervista di ML Classico

Domande di Intervista per Ingegneri di Machine Learning Classico

15 domande selezionate di intervista sul machine learning classico raggruppate per livello di anzianità. Usale per ripassare i fondamenti, i compromessi pratici e il ragionamento di produzione a livello senior.

Avvia un'Intervista AI su ML ClassicoNessuna carta di credito richiesta. 1 sessione gratuita disponibile.
Pratica per colloqui tecnici in ingleseUna modalità in cui chi non è madrelingua può esercitarsi a superare i colloqui tecnici.

Domande per Junior

1Qual è la differenza tra un parametro del modello e un iperparametro nell'apprendimento supervisionato?

Nel machine learning supervisionato, i parametri del modello sono variabili interne apprese direttamente dai dati di addestramento tramite un algoritmo di ottimizzazione (come la discesa del gradiente o le equazioni normali). Esempi includono i pesi di regressione e il bias nei modelli lineari o le soglie di split negli alberi decisionali. Al contrario, gli iperparametri sono impostazioni di configurazione esterne specificate prima dell'addestramento che governano il processo di apprendimento, la capacità del modello o l'architettura. Non possono essere appresi direttamente tramite la minimizzazione della perdita di addestramento standard perché l'ottimizzatore sovra-adattarsi in modo banale (ad esempio, impostando la profondità dell'albero all'infinito). Esempi includono il tasso di apprendimento, la forza di regolarizzazione (lambda/C), il numero di alberi in una foresta e la massima profondità dell'albero. Gli iperparametri vengono ottimizzati utilizzando dati di validazione o cross-validazione.

from sklearn.linear_model import Ridge
import numpy as np

X = np.array([[1.0], [2.0], [3.0]])
y = np.array([2.0, 4.0, 6.0])

# Hyperparameter: alpha (regularization strength set beforehand)
model = Ridge(alpha=1.0)

# Fitting optimizes internal parameters on training data
model.fit(X, y)

# Learned parameters
print(f"Weight (Parameter): {model.coef_[0]:.4f}")
print(f"Intercept (Parameter): {model.intercept_:.4f}")
Prova a rispondere a questa domanda con un coach IA

2Quali assunzioni fa la regressione lineare ai minimi quadrati ordinari (OLS) e come la diagnostica dei residui rivelerebbe le violazioni di tali assunzioni?

La regressione lineare ai minimi quadrati ordinari (OLS) si basa su diverse assunzioni fondamentali: 1. Linearità: La relazione tra predittori e risultato è lineare nei parametri. 2. Indipendenza degli errori: Osservazioni ed errori residui sono reciprocamente indipendenti (nessuna autocorrelazione). 3. Omoschedasticità: I termini di errore hanno varianza costante su tutti i livelli dei predittori. 4. Normalità dei residui: I termini di errore sono distribuiti normalmente (necessario per intervalli di confidenza e test di ipotesi validi). 5. Assenza di multicollinearità: I predittori non sono linearmente dipendenti (la matrice di design ha rango di colonna completo). La diagnostica dei residui rivela le violazioni come segue: - Grafico Residui vs. Valori Predetti: Curvatura o schemi non casuali rivelano non-linearità; una forma a imbuto o a ventaglio rivela eteroschedasticità (varianza non costante). - Grafico Q-Q (Quantile-Quantile) normale: La deviazione sistematica dalla linea diagonale retta (ad es. curve a S o code pesanti) rivela non-normalità. - Grafico Residui vs. Ordine/Tempo: Tendenze sistematiche o schemi ciclici rivelano errori autocorrelati. - Grafico di *Leverage* / Distanza di Cook: Identifica *outlier* ad alto *leverage* o punti influenti che spostano in modo sproporzionato il modello stimato.

import numpy as np
import statsmodels.api as sm

np.random.seed(42)
X = np.linspace(1, 10, 50)
# Quadratic underlying pattern creates a linearity violation
y = 2 * X + 0.5 * (X ** 2) + np.random.normal(0, 2, 50)

X_with_const = sm.add_constant(X)
model = sm.OLS(y, X_with_const).fit()
residuals = model.resid

print(f"Mean Residual: {np.mean(residuals):.4f}")
print(f"Curvature in residuals indicates model misspecification.")
Prova a rispondere a questa domanda con un coach IA

3In che modo la regressione logistica modella la classificazione binaria e qual è il ruolo della funzione sigmoide?

La regressione logistica modella la classificazione binaria stimando la probabilità a posteriori di classe $P(Y=1|X)$. Per garantire che le probabilità previste rimangano comprese tra $(0, 1)$, la regressione logistica modella il log-odds (logit) della classe positiva come una funzione lineare degli input: $\ln\left(\frac{p}{1-p}\right) = w^T x + b$. La funzione sigmoide (logistica), $\sigma(z) = \frac{1}{1 + e^{-z}}$, serve come funzione di collegamento che mappa qualsiasi punteggio lineare a valore reale $z = w^T x + b \in (-\infty, +\infty)$ in modo monotono in una probabilità valida $p \in (0, 1)$. Le decisioni discrete di classe vengono prese applicando una soglia di decisione $\tau$ (tipicamente 0.5): $\hat{y} = 1$ se $P(Y=1|X) \ge \tau$, altrimenti $0$. Poiché $\sigma(z) = 0.5$ si verifica precisamente quando $z = 0$, il confine di decisione nello spazio delle feature è l'iperpiano lineare $w^T x + b = 0$, rendendo la regressione logistica standard un classificatore lineare.

import numpy as np

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

w = np.array([1.5, -2.0])
b = 0.5
x = np.array([2.0, 1.0])

z = np.dot(w, x) + b
prob = sigmoid(z)
threshold = 0.5
pred = int(prob >= threshold)

print(f"Log-odds (z): {z:.2f}")
print(f"Probability: {prob:.4f}")
print(f"Class Prediction: {pred}")
Prova a rispondere a questa domanda con un coach IA

4Cos'è la regolarizzazione L2 e in che modo la regressione Ridge modifica la funzione obiettivo e le stime dei coefficienti?

La regolarizzazione L2 (regressione Ridge) aggiunge una penalità proporzionale alla somma dei quadrati dei pesi alla funzione di costo dei minimi quadrati ordinari (OLS): $$\min_w \|y - Xw\|_2^2 + \lambda \|w\|_2^2$$ Analiticamente, Ridge modifica le equazioni normali aggiungendo $\lambda I$ alla matrice di Gram prima dell'inversione: $$w_{\text{ridge}} = (X^T X + \lambda I)^{-1} X^T y$$ Principali impatti sull'obiettivo e sulle stime dei coefficienti: 1. **Restringimento (Shrinkage)**: I coefficienti sono ridotti verso zero in proporzione alla varianza e correlazione delle feature, riducendo la complessità del modello senza forzarli esattamente a zero. 2. **Multicollinearità e Invertibilità**: Quando le feature sono collineari o $p > N$, $X^T X$ è singolare o mal condizionata. L'aggiunta di $\lambda I$ assicura che $(X^T X + \lambda I)$ sia strettamente definita positiva e invertibile, stabilizzando le stime dei parametri. 3. **Compromesso Bias-Varianza (Bias-Variance Trade-off)**: L'aumento di $\lambda$ introduce un bias intenzionale nelle stime dei coefficienti, riducendo significativamente la varianza, il che si traduce in un errore di generalizzazione atteso inferiore sui dati non visti. 4. **Requisito di Scalatura delle Feature**: Poiché la penalità tratta tutti i pesi allo stesso modo, le feature su scale più grandi sarebbero regolarizzate in modo sproporzionato. Le feature devono essere standardizzate (media zero, varianza unitaria) prima del fitting.

import numpy as np

def ridge_regression(X, y, alpha):
    X_std = (X - np.mean(X, axis=0)) / np.std(X, axis=0)
    n_features = X_std.shape[1]
    
    I = np.eye(n_features)
    beta = np.linalg.inv(X_std.T @ X_std + alpha * I) @ X_std.T @ y
    return beta

X = np.array([[1.0, 2.0], [2.0, 4.1], [3.0, 5.9], [4.0, 8.2]])
y = np.array([2.1, 4.0, 6.2, 8.1])
weights = ridge_regression(X, y, alpha=1.0)
print('Ridge Weights:', weights)
Prova a rispondere a questa domanda con un coach IA

5Come un albero decisionale partiziona ricorsivamente lo spazio delle feature, e quali criteri vengono usati per scegliere le suddivisioni di classificazione?

Un albero decisionale partiziona lo spazio delle feature attraverso un algoritmo top-down e goloso chiamato **partizionamento binario ricorsivo**. Partendo dal nodo radice con tutti i dati di addestramento, l'algoritmo ricerca su tutte le feature e i possibili valori di soglia per trovare la singola suddivisione allineata agli assi ($X_j \le t$) che massimizza la riduzione dell'impurità. Il dataset viene suddiviso in due nodi figli, e questa procedura viene applicata ricorsivamente su ogni nodo figlio fino a quando non viene raggiunto un criterio di arresto (ad esempio, profondità massima, campioni minimi per foglia o nodi puri). Poiché le suddivisioni valutano una feature alla volta rispetto a una soglia, i confini decisionali risultanti sono iperpiani ortogonali (regioni rettangolari allineate agli assi nello spazio delle feature). Per valutare e selezionare la migliore suddivisione negli alberi di classificazione, vengono utilizzati due principali criteri di impurità: 1. **Impurità di Gini (usata in CART)**: Misura la probabilità che un campione scelto casualmente venga classificato erroneamente se etichettato casualmente in base alla distribuzione delle classi del nodo. Per $K$ classi con proporzioni $p_k$: $$I_G = 1 - \sum_{k=1}^K p_k^2$$ 2. **Entropia e Guadagno di Informazione (usati in ID3, C4.5)**: L'entropia misura l'incertezza in un nodo: $H = -\sum_{k=1}^K p_k \log_2(p_k)$. La suddivisione viene scelta per massimizzare il **Guadagno di Informazione**, che è l'entropia del nodo genitore meno l'entropia media pesata dei nodi figli: $$IG = H(\text{parent}) - \sum_{c \in \{\text{left, right}\}} \frac{N_c}{N} H(c)$$ Entrambe le metriche raggiungono 0 quando un nodo è completamente puro (tutti i campioni appartengono a una singola classe) e raggiungono il loro massimo quando le classi sono equamente distribuite.

import numpy as np

def gini(labels):
    _, counts = np.unique(labels, return_counts=True)
    p = counts / len(labels)
    return 1.0 - np.sum(p ** 2)

def entropy(labels):
    _, counts = np.unique(labels, return_counts=True)
    p = counts / len(labels)
    return -np.sum(p * np.log2(p + 1e-12))

# Evenly split node (impure) vs single-class node (pure)
impure_node = np.array([0]*10 + [1]*10)
pure_node = np.array([0]*20)

print(f"Impure - Gini: {gini(impure_node):.2f}, Entropy: {entropy(impure_node):.2f}")
print(f"Pure   - Gini: {gini(pure_node):.2f}, Entropy: {entropy(pure_node):.2f}")
Prova a rispondere a questa domanda con un coach IA

6Cos'è il k-nearest neighbors e come effettua previsioni per la classificazione e la regressione?

k-Nearest Neighbors (kNN) è un algoritmo di apprendimento supervisionato non parametrico, basato su istanze (lazy). Non addestra un modello parametrico esplicito; invece, memorizza il dataset di addestramento ed esegue tutti i calcoli durante l'inferenza. Flusso di lavoro della previsione: 1. Calcolo della distanza: Quando viene valutata un'istanza di query, l'algoritmo calcola la sua distanza da tutte le istanze di addestramento memorizzate utilizzando una metrica specificata (come la distanza euclidea, di Manhattan o di Minkowski). 2. Selezione dei vicini: Seleziona le $k$ istanze di addestramento con le distanze più piccole dall'istanza di query. 3. Aggregazione: - Classificazione: Assegna la classe tramite voto di maggioranza (moda) tra i $k$ vicini (o voto ponderato per la distanza). - Regressione: Prevede il target continuo prendendo la media locale (media o mediana) dei valori target dei $k$ vicini (o media ponderata per la distanza). Poiché i calcoli di distanza dipendono direttamente dalle scale delle feature, la normalizzazione o la standardizzazione delle feature è essenziale per evitare che feature di grande entità dominino i calcoli di distanza.

from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor
from sklearn.preprocessing import StandardScaler
import numpy as np

X_train = np.array([[1000.0, 1.0], [2000.0, 2.0], [1500.0, 1.5], [5000.0, 5.0]])
y_cls = np.array([0, 0, 0, 1])
y_reg = np.array([10.0, 20.0, 15.0, 50.0])

# Feature scaling is mandatory for distance-based algorithms
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_train)

# 1. Classification (Majority Vote)
clf = KNeighborsClassifier(n_neighbors=3)
clf.fit(X_scaled, y_cls)

# 2. Regression (Local Average)
reg = KNeighborsRegressor(n_neighbors=3)
reg.fit(X_scaled, y_reg)

query = scaler.transform([[1800.0, 1.8]])
print('Classification:', clf.predict(query))
print('Regression:', reg.predict(query))
Prova a rispondere a questa domanda con un coach IA

7Enuncia l'assunzione di indipendenza condizionata del Naïve Bayes e spiega perché il Naïve Bayes può comunque funzionare bene anche quando viene violata.

L'assunzione di indipendenza condizionata del Naïve Bayes afferma che, data l'etichetta di classe Y = y, tutte le feature X_1, X_2, ..., X_d sono mutuamente indipendenti: P(X_1, ..., X_d | Y = y) = \prod_{j=1}^d P(X_j | Y = y). Usando il teorema di Bayes, la probabilità a posteriori è: P(Y = y | X) \propto P(Y = y) \prod_{j=1}^d P(X_j | Y = y), dove P(Y = y) è la probabilità a priori della classe e P(X_j | Y = y) è la verosimiglianza condizionata alla classe (ad esempio, Gaussiana per feature continue, Multinomiale per i conteggi). Il Naïve Bayes spesso funziona bene nella pratica nonostante le violazioni dell'indipendenza perché la classificazione si basa sulla regola di decisione `argmax` (argmax_y P(Y=y | X)) piuttosto che su una calibrazione accurata della probabilità. Anche se le correlazioni tra le feature causano probabilità predette eccessivamente sicure o distorte, la classe corretta spesso mantiene il ranking relativo più alto. Finché la correlazione non inverte il ranking delle verosimiglianze di classe, la decisione di classificazione 0-1 rimane accurata.

from sklearn.naive_bayes import GaussianNB
import numpy as np

X = np.array([[1.0, 1.1], [1.2, 0.9], [-1.0, -1.2], [-0.8, -1.1]])
y = np.array([1, 1, 0, 0])

model = GaussianNB()
model.fit(X, y)
# Prediction uses argmax over class posterior scores
print("Predicted class:", model.predict([[1.1, 1.0]]))
Prova a rispondere a questa domanda con un coach IA

Domande per Middle

8Derivare o spiegare la soluzione in forma chiusa per OLS (Minimi Quadrati Ordinari) e indicare quando essa esiste in modo unico.

L'obiettivo dei Minimi Quadrati Ordinari (OLS) minimizza la somma dei quadrati dei residui: $S(\beta) = \|y - X\beta\|^2 = (y - X\beta)^T (y - X\beta) = y^T y - 2\beta^T X^T y + \beta^T X^T X \beta$. Impostando il gradiente rispetto a $\beta$ a zero: $$\nabla_\beta S(\beta) = -2 X^T y + 2 X^T X \beta = 0 \implies X^T X \beta = X^T y$$ Queste sono le equazioni normali. Quando $X^T X$ è non-singolare (invertibile), la soluzione unica in forma chiusa è: $$\hat{\beta} = (X^T X)^{-1} X^T y$$ Geometricamente, $\hat{y} = X\hat{\beta} = X(X^T X)^{-1} X^T y = H y$ rappresenta la proiezione ortogonale del vettore target $y$ sullo spazio delle colonne della matrice di design $X$, dove $H$ è la matrice di proiezione (hat matrix). La soluzione esiste in modo unico se e solo se $X^T X$ è invertibile, il che richiede che la matrice di design $X$ di dimensioni $N \times P$ abbia rango per colonna pieno ($Rank(X) = P$). Ciò richiede $N \ge P$ e assenza di multicollinearità esatta (nessuna feature è una combinazione lineare delle altre). Se $X$ è a rango deficitario (rank-deficient), $X^T X$ è singolare, portando a infinite soluzioni, spesso affrontate tramite regolarizzazione o la pseudoinversa di Moore-Penrose $X^+ y$.

import numpy as np

# Design matrix X (with intercept column) and target y
X = np.array([[1, 1], [1, 2], [1, 3], [1, 4]])
y = np.array([2.1, 3.9, 6.2, 8.0])

# Normal equations: (X^T X)^(-1) X^T y
beta_hat = np.linalg.inv(X.T @ X) @ X.T @ y
H = X @ np.linalg.inv(X.T @ X) @ X.T
y_hat = H @ y

print(f"Beta: {beta_hat}")
print(f"Predictions: {y_hat}")
Prova a rispondere a questa domanda con un coach IA

9Cos'è la Stima di Massima Verosimiglianza (MLE) e come porta all'obiettivo di cross-entropy per la regressione logistica?

La Stima di Massima Verosimiglianza (MLE) è un metodo per stimare i parametri del modello $\theta$ scegliendo valori che massimizzano la verosimiglianza $L(\theta) = P(\mathcal{D}|\theta)$ del dataset osservato. Nella regressione logistica binaria, ogni etichetta $y_i \in \{0, 1\}$ è modellata come una variabile casuale di Bernoulli indipendente condizionata da $x_i$, con probabilità di successo $p_i = \sigma(w^T x_i + b)$. La funzione di massa di probabilità per l'osservazione $i$ è $P(y_i|x_i) = p_i^{y_i} (1 - p_i)^{1 - y_i}$. Assumendo campioni i.i.d. (indipendenti e identicamente distribuiti), la verosimiglianza congiunta è: $$L(w, b) = \prod_{i=1}^N p_i^{y_i} (1 - p_i)^{1 - y_i}$$ Prendendo il logaritmo naturale, il prodotto si converte in una somma di log-verosimiglianze computazionalmente trattabile: $$\ell(w, b) = \sum_{i=1}^N \left[ y_i \ln(p_i) + (1 - y_i) \ln(1 - p_i) \right]$$ Poiché gli algoritmi di ottimizzazione sono solitamente formulati come problemi di minimizzazione, neghiamo la log-verosimiglianza e normalizziamo per la dimensione del campione $N$, ottenendo la Log-Verosimiglianza Negativa (NLL), che è esattamente l'obiettivo di Cross-Entropy Binaria (log loss): $$J(w, b) = -\frac{1}{N} \sum_{i=1}^N \left[ y_i \ln(p_i) + (1 - y_i) \ln(1 - p_i) \right]$$ Questo obiettivo è convesso rispetto ai logits/pesi lineari, quindi i risolutori numerici appropriati ottimizzano un obiettivo globale. Una stretta convessità e un'unica MLE finita richiedono condizioni aggiuntive come un rango sufficiente delle feature, regolarizzazione e nessuna separazione perfetta delle classi.

import numpy as np

y_true = np.array([1, 0, 1, 1])
y_prob = np.array([0.9, 0.2, 0.8, 0.4])

# Binary cross-entropy (Negative Log-Likelihood)
epsilon = 1e-15  # prevent log(0)
y_prob = np.clip(y_prob, epsilon, 1 - epsilon)
bce_loss = -np.mean(y_true * np.log(y_prob) + (1 - y_true) * np.log(1 - y_prob))

print(f"Binary Cross-Entropy Loss: {bce_loss:.4f}")
Prova a rispondere a questa domanda con un coach IA

10Come ottimizza la discesa del gradiente (gradient descent) un obiettivo di ML classico, e in che modo il tasso di apprendimento, la convergenza e la convessità influenzano l'addestramento?

La discesa del gradiente minimizza una funzione di perdita empirica aggiornando iterativamente i parametri del modello nella direzione opposta al gradiente della funzione obiettivo rispetto a tali parametri: $\theta_{t+1} = \theta_t - \eta \nabla L(\theta_t)$. I fattori chiave che influenzano l'addestramento includono: 1. **Tasso di Apprendimento ($\eta$):** Controlla la dimensione del passo. Se impostato troppo piccolo, la convergenza è eccessivamente lenta e l'addestramento potrebbe bloccarsi. Se impostato troppo grande, gli aggiornamenti supereranno il minimo, causando oscillazioni o divergenza numerica. 2. **Convergenza:** Determinata dal monitoraggio di criteri di arresto come una piccola norma del gradiente ($||\nabla L(\theta)|| \le \epsilon$), uno spostamento minimo dei parametri o un plateau della perdita per iterazioni consecutive. 3. **Convessità:** Negli obiettivi convessi (es. regressione lineare OLS standard o regressione logistica), ogni minimo locale è garantito essere un minimo globale, consentendo alla discesa del gradiente di convergere in modo affidabile con passi di dimensione appropriata. Negli obiettivi non convessi (es. reti neurali multistrato), il paesaggio della perdita contiene più minimi locali, punti sella e plateau, rendendo la soluzione finale sensibile all'inizializzazione. 4. **Ottimizzazione vs. Generalizzazione:** La convergenza sulla perdita di addestramento riflette il successo dell'ottimizzazione, mentre la perdita di validazione valuta la generalizzazione. Raggiungere una bassa perdita di addestramento con un elevato errore di validazione indica overfitting (eccessivo adattamento) piuttosto che un fallimento dell'ottimizzazione.

import numpy as np

def gradient_descent(X, y, lr=0.01, max_iters=1000, tol=1e-6):
    n_samples, n_features = X.shape
    theta = np.zeros(n_features)
    prev_loss = float('inf')
    
    for i in range(max_iters):
        predictions = X @ theta
        error = predictions - y
        loss = (1 / (2 * n_samples)) * np.dot(error, error)
        
        if abs(prev_loss - loss) < tol:
            print(f'Converged at iteration {i}')
            break
        prev_loss = loss
        
        grad = (1 / n_samples) * (X.T @ error)
        theta -= lr * grad
        
    return theta
Prova a rispondere a questa domanda con un coach IA

11Confronta la regolarizzazione L1, L2 ed ElasticNet in termini di sparsità, caratteristiche correlate e selezione pratica del modello.

La regolarizzazione L1 (Lasso), L2 (Ridge) ed ElasticNet differiscono nella formulazione della penalità, nella geometria del vincolo, nella sparsità e nella gestione dei predittori correlati: 1. **Sparsità e geometria:** - L1 utilizza una penalità del valore assoluto ($\lambda \|w\|_1$). Il suo confine di vincolo è un diamante/politopo con vertici acuti sugli assi coordinati. Quando i contorni della funzione di perdita intersecano questi angoli, i pesi vengono spinti a zero esatto, eseguendo una selezione automatica delle caratteristiche. - L2 utilizza una penalità della norma euclidea al quadrato ($\lambda \|w\|_2^2$). Il suo confine di vincolo è un ipersfera liscia senza angoli, che riduce i pesi verso zero asintoticamente ma raramente li imposta a zero esatto. 2. **Caratteristiche correlate:** - In caso di forte collinearità, L1 tende a scegliere arbitrariamente una caratteristica da un gruppo di predittori correlati e a impostare a zero i coefficienti rimanenti, con conseguenti stime instabili tra i ricampionamenti. - L2 mantiene tutte le caratteristiche correlate, distribuendo i pesi tra di esse e riducendole insieme. 3. **ElasticNet:** - Combina entrambe le penalità: $\lambda_1 \|w\|_1 + \lambda_2 \|w\|_2^2$ (spesso parametrizzata con $\alpha$ e $l_1\_\text{ratio}$). - Offre la sparsità e la selezione delle caratteristiche di Lasso, pur preservando l'effetto di raggruppamento di Ridge, selezionando insieme cluster di predittori correlati. È particolarmente utile quando $p > N$ o in caso di grave multicollinearità.

from sklearn.linear_model import Ridge, Lasso, ElasticNet
import numpy as np

np.random.seed(42)
X1 = np.random.randn(100, 1)
X2 = X1 + np.random.randn(100, 1) * 0.01  # highly correlated
X = np.hstack([X1, X2])
y = 3 * X1.ravel() + np.random.randn(100) * 0.1

ridge = Ridge(alpha=1.0).fit(X, y)
lasso = Lasso(alpha=0.1).fit(X, y)
elastic = ElasticNet(alpha=0.1, l1_ratio=0.5).fit(X, y)

print('Ridge coefs:', ridge.coef_)
print('Lasso coefs:', lasso.coef_)
print('ElasticNet coefs:', elastic.coef_)
Prova a rispondere a questa domanda con un coach IA

12Qual è la differenza tra regressione Ridge, Regressione a Componenti Principali e Minimi Quadrati Parziali a un livello generale?

La Regressione Ridge, la Regressione a Componenti Principali (PCR - Principal Component Regression) e i Minimi Quadrati Parziali (PLS - Partial Least Squares) sono tre tecniche lineari utilizzate per gestire la multicollinearità e l'alta dimensionalità, ma differiscono nel modo in cui riducono la varianza e se la riduzione è continua o supervisionata: 1. **Regressione Ridge**: Mantiene tutte le $p$ caratteristiche originali e applica una contrazione continua alle magnitudini dei coefficienti tramite una penalità L2. Non costruisce componenti latenti a dimensione inferiore né scarta dimensioni di caratteristiche; piuttosto, riduce la varianza lungo le direzioni a basso autovalore di $X^T X$. 2. **Regressione a Componenti Principali (PCR)**: Un metodo di riduzione della dimensionalità non supervisionato a due fasi. Applica prima l'Analisi delle Componenti Principali (PCA - Principal Component Analysis) strettamente alla matrice dei predittori $X$ per trovare direzioni ortogonali di massima varianza, mantiene le prime $k$ componenti principali e adatta una regressione OLS (Ordinary Least Squares) su quelle $k$ componenti. Poiché la PCA ignora la variabile target $y$, la PCR rischia di scartare componenti che hanno bassa varianza in $X$ ma alto potere predittivo per $y$. 3. **Minimi Quadrati Parziali (PLS)**: Un metodo di riduzione della dimensionalità supervisionato. Costruisce $k$ componenti latenti ortogonali trovando combinazioni lineari di $X$ che massimizzano la covarianza tra $X$ e la risposta $y$. Incorporando esplicitamente le informazioni sul target, il PLS identifica componenti che spiegano sia la varianza delle caratteristiche che la variazione della risposta.

from sklearn.linear_model import Ridge, LinearRegression
from sklearn.decomposition import PCA
from sklearn.cross_decomposition import PLSRegression
from sklearn.pipeline import make_pipeline

# 1. Ridge: Regularized full feature space
ridge = Ridge(alpha=1.0)

# 2. PCR: Unsupervised PCA followed by OLS
pcr = make_pipeline(PCA(n_components=2), LinearRegression())

# 3. PLS: Supervised latent component projection and regression
pls = PLSRegression(n_components=2)
Prova a rispondere a questa domanda con un coach IA

Domande per Senior

13In che modo le moderne implementazioni di gradient boosting come XGBoost, LightGBM e CatBoost ottimizzano l'addestramento o gestiscono le feature tabulari in modo diverso?

I moderni framework GBDT (Gradient Boosting Decision Tree) differiscono sostanzialmente nei loro algoritmi di ricerca dello split, nelle strategie di crescita degli alberi e nella gestione delle feature tabulari/categoriche: 1. **XGBoost**: Tradizionalmente si affida alla ricerca dello split greedy esatta o approssimata tramite quantile sketch (e successivamente Fast Hist), utilizza la crescita degli alberi level-wise (depth-wise) e gestisce i valori mancanti imparando una direzione ottimale del ramo predefinito durante la ricerca dello split. 2. **LightGBM**: Utilizza la ricerca dello split basata su istogrammi (suddividendo le feature continue in bucket discreti, tipicamente 256), crescita degli alberi leaf-wise (best-first) per una riduzione più rapida della perdita, Gradient-based One-Side Sampling (GOSS) per mantenere le istanze con grandi gradienti mentre si sottocampionano quelle con piccoli gradienti, ed Exclusive Feature Bundling (EFB) per unire feature sparse mutuamente esclusive. Per le variabili categoriche, trova gli split ottimali ordinando i bin dell'istogramma categorico ($O(K \log K)$). 3. **CatBoost**: Utilizza alberi di decisione oblivious (simmetrici) in cui tutti i nodi a una data profondità condividono lo stesso split esatto, consentendo uno scoring vettorializzato veloce su CPU/GPU. La sua innovazione principale è Ordered Target Statistics e l'ordered boosting, che calcola le statistiche target su permutazioni casuali dei dati di addestramento per prevenire target leakage e prediction shift.

from catboost import CatBoostClassifier
import lightgbm as lgb
import pandas as pd

df = pd.DataFrame({
    'city': ['NY', 'LDN', 'NY', 'PAR', 'LDN', 'TOK'],
    'age': [25, 42, 30, 22, 55, 38],
    'target': [1, 0, 1, 0, 1, 0]
})
cat_cols = ['city']
df['city'] = df['city'].astype('category')

# LightGBM handles pandas 'category' dtype natively via integer binning
lgb_clf = lgb.LGBMClassifier(max_depth=3, n_estimators=10)
lgb_clf.fit(df[['city', 'age']], df['target'])

# CatBoost handles categorical column names natively with ordered TS
cb_clf = CatBoostClassifier(iterations=10, cat_features=cat_cols, verbose=False)
cb_clf.fit(df[['city', 'age']], df['target'])
Prova a rispondere a questa domanda con un coach IA

14Come decideresti se una funzione di perdita personalizzata sia appropriata per un modello di gradient boosting in presenza di costi aziendali asimmetrici?

Decidere se implementare una funzione di perdita personalizzata nel gradient boosting in presenza di costi aziendali asimmetrici richiede di valutare se l'asimmetria possa essere gestita a valle tramite calibrazione della probabilità e ottimizzazione della soglia, o se alteri fondamentalmente il panorama di ottimizzazione durante l'induzione dell'albero: 1. Ottimizzazione della Soglia vs Funzione di Perdita Personalizzata: Per compiti di classificazione con costi di errore asimmetrici (ad es., falsi negativi che costano $10\times$ i falsi positivi), la cross-entropy standard è una `scoring rule` appropriata che mira a stimare le probabilità posteriori $P(y=1|x)$, ma la calibrazione dovrebbe essere verificata e, se necessario, corretta sui dati di validazione. Spostare la soglia di decisione della classificazione basandosi sulla matrice dei costi aziendali $\tau = \frac{C_{FP}}{C_{FP} + C_{FN}}$ o applicare pesi ai campioni è spesso più pulito ed evita derivate personalizzate. Tuttavia, per la regressione asimmetrica (ad es., `pinball loss` asimmetrica per la domanda di inventario) o penalità aziendali non lineari dove gli obiettivi standard non possono guidare la ricerca dello split, una funzione di perdita personalizzata è giustificata. 2. Requisiti Matematici per GBDT (Gradient Boosting Decision Tree): Nei `boosters` di secondo ordine (XGBoost, LightGBM), una funzione di perdita personalizzata $L(y, \hat{y})$ normalmente richiede gradienti del primo ordine calcolabili ($g_i = \partial L / \partial \hat{y}_i$) e valori validi di curvatura del secondo ordine/Hessiana ($h_i = \partial^2 L / \partial \hat{y}_i^2$) per i calcoli del guadagno dello split e del peso delle foglie ($w^* = -\sum g_i / (\sum h_i + \lambda)$). Le Hessiane dovrebbero essere non negative o approssimate/clippate in modo sicuro per stabilità numerica; alcune implementazioni supportano obiettivi del primo ordine o approssimati, quindi il requisito è specifico del framework. Metriche aziendali non differenziabili o discontinue dovrebbero essere sostituite con approssimazioni surrogate lisce (ad esempio, varianti Huberized o log-cosh).

import numpy as np
import xgboost as xgb

def asymmetric_mse_objective(preds, dtrain):
    labels = dtrain.get_label()
    residual = preds - labels
    # Penalize underestimation (residual < 0) 5x more heavily than overestimation
    penalty = np.where(residual < 0, 5.0, 1.0)
    grad = 2.0 * penalty * residual
    hess = 2.0 * penalty
    return grad, hess

# Usage:
# model = xgb.train(params, dtrain, obj=asymmetric_mse_objective)
Prova a rispondere a questa domanda con un coach IA

15Cos'è LambdaMART e come adatta il gradient boosting agli obiettivi di apprendimento al ranking?

LambdaMART è un algoritmo di Learning-to-Rank (LTR) che combina MART (Multiple Additive Regression Trees / Gradient Boosting) con il framework LambdaRank. Nel ranking, le metriche target come NDCG (Normalized Discounted Cumulative Gain) e MAP (Mean Average Precision) dipendono dall'ordine di ordinamento discreto (ranking), rendendole quasi piatte ovunque e non differenziabili rispetto ai punteggi continui del modello. LambdaMART aggira questo problema costruendo gradienti virtuali, chiamati 'gradienti lambda' ($\lambda_{ij}$), per coppie di elementi $(i, j)$ all'interno della stessa query. Il gradiente base per coppia deriva da una funzione di perdita logistica sulle differenze di punteggio ($s_i - s_j$). LambdaMART scala questo gradiente in base all'esatto cambiamento nella metrica di ranking target ($|\Delta \text{NDCG}_{ij}|$) che si verificherebbe se le posizioni del documento $i$ e del documento $j$ fossero scambiate: $$\lambda_{ij} = \frac{-\sigma}{1 + e^{\sigma(s_i - s_j)}} |\Delta \text{NDCG}_{ij}|$$ Per ogni singolo documento $i$, il gradiente netto viene calcolato aggregando i lambda per coppia attraverso tutte le coppie che coinvolgono il documento $i$: $\lambda_i = \sum_{j: j \succ i} \lambda_{ij} - \sum_{k: i \succ k} \lambda_{ki}$. Gli alberi di regressione standard nell'ensemble di boosting adattano quindi questi gradienti lambda compositi per documento a ogni iterazione di boosting, ottimizzando direttamente le metriche di ranking listwise.

import lightgbm as lgb
import numpy as np

# Simulated query-grouped data: 2 queries with 3 docs each
X = np.random.randn(6, 10)
y = np.array([3, 1, 0, 2, 0, 1])  # Relevance grades (0-3)
group = [3, 3]                     # Query group sizes

train_data = lgb.Dataset(X, label=y, group=group)
params = {
    'objective': 'lambdarank',
    'metric': 'ndcg',
    'ndcg_eval_at': [1, 3],
    'learning_rate': 0.1,
    'n_estimators': 50
}

ranker = lgb.train(params, train_data)
Prova a rispondere a questa domanda con un coach IA