1. ml — Machine Learning Classico con scikit-learn
Il punto di partenza: tre algoritmi fondamentali su dataset reali. L'obiettivo non era padroneggiare l'API di sklearn, ma capire cosa succede sotto — le equazioni, i loop, le scelte di design.
Regressione lineare
Dataset: California Housing (20.640 campioni, 8 feature). Il modello impara i pesi β che minimizzano il Mean Squared Error. La soluzione esatta si trova con la Normal Equation:
Con sklearn si usa LinearRegression, che implementa questa equazione internamente (via SVD per stabilità numerica). Metriche monitorate: MSE, R² (coefficiente di determinazione).
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.datasets import fetch_california_housing
X, y = fetch_california_housing(return_X_y=True)
model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print(f"MSE: {mean_squared_error(y_test, y_pred):.4f}")
print(f"R²: {r2_score(y_test, y_pred):.4f}")
Regressione logistica
Dataset: Iris (3 classi). La sigmoide schiaccia l'output lineare in [0,1]; la loss è la Binary Cross-Entropy. Per multi-classe si usa lo schema One-vs-Rest: un classificatore per classe, vince chi ha probabilità più alta.
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
model = LogisticRegression(multi_class='ovr', max_iter=200)
model.fit(X_train, y_train)
print(f"Test accuracy: {model.score(X_test, y_test):.3f}")
K-Means
Dataset: Digits (1797 immagini 8×8 di cifre scritte a mano). K-Means è un algoritmo EM: l'E-step assegna ogni punto al centroide più vicino, l'M-step ricalcola i centroidi come media dei punti assegnati. La convergenza è garantita ma il risultato dipende dall'inizializzazione — K-Means++ sceglie i centroidi iniziali in modo intelligente, migliorando consistentemente i risultati.
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
kmeans = KMeans(n_clusters=10, init='k-means++', n_init=10, random_state=42)
kmeans.fit(X_digits)
# PCA a 2 componenti per visualizzare i cluster
pca = PCA(n_components=2)
X_2d = pca.fit_transform(X_digits)
2. parameters-tuning — Overfitting e Regolarizzazione
Un albero di decisione può memorizzare perfettamente il training set — ma poi non generalizza. Questo repository esplora il tradeoff bias-varianza in modo molto concreto.
Decision Tree su Breast Cancer Wisconsin
L'albero usa la Gini impurity come criterio di split. Ad ogni nodo, sceglie il feature e la soglia che massimizzano l'Information Gain:
Bias-Variance Tradeoff
L'errore di generalizzazione si scompone in tre termini:
Un albero profondo ha bias basso (si adatta bene al training) ma alta varianza (sensibile al rumore). Un albero poco profondo ha il problema opposto. L'esperimento concreto:
| Configurazione | Train accuracy | Test accuracy |
|---|---|---|
| Depth illimitata | 1.000 | 0.937 |
| max_depth=4 | 0.988 | 0.951 |
Il tree con depth illimitata memorizza il training set ma generalizza peggio. Con max_depth=4 si rinuncia a 1.2% di training accuracy e si guadagna 1.4% di test accuracy.
Cross-validation per trovare il parametro ottimale
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_val_score
import numpy as np
depths = range(1, 21)
cv_scores = []
for depth in depths:
tree = DecisionTreeClassifier(max_depth=depth, random_state=42)
scores = cross_val_score(tree, X, y, cv=5, scoring='accuracy')
cv_scores.append(scores.mean())
best_depth = depths[np.argmax(cv_scores)]
print(f"Best depth: {best_depth}, CV score: {max(cv_scores):.3f}")
3. pytorch-ml — PyTorch da Zero
Il salto concettuale principale: da sklearn (black box ottimizzata) a PyTorch (grafo computazionale esplicito). Questo repository reimplementa gli stessi algoritmi del primo, ma con autograd.
Autograd e grafo computazionale
PyTorch usa un approccio define-by-run: il grafo computazionale viene costruito dinamicamente durante l'esecuzione del forward pass. Ogni tensore tiene traccia delle operazioni che lo hanno prodotto, permettendo di calcolare i gradienti con la chain rule:
Il training loop canonico
import torch
import torch.nn as nn
model = MyModel()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.BCELoss()
for epoch in range(num_epochs):
for X_batch, y_batch in dataloader:
y_pred = model(X_batch) # forward
loss = criterion(y_pred, y_batch) # calcola loss
optimizer.zero_grad() # azzera gradienti
loss.backward() # backward pass
optimizer.step() # aggiorna pesi
MLP per classificazione su make_moons
I dati make_moons non sono linearmente separabili. Un singolo layer lineare fallisce; serve la non-linearità. ReLU (Rectified Linear Unit) — max(0, x) — è il motivo per cui le reti neurali profonde funzionano: ogni layer con ReLU crea una nuova partizione dello spazio delle feature.
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(2, 64),
nn.ReLU(),
nn.Linear(64, 64),
nn.ReLU(),
nn.Linear(64, 1),
nn.Sigmoid()
)
def forward(self, x):
return self.net(x).squeeze()
Adam optimizer
SGD aggiorna tutti i parametri con lo stesso learning rate. Adam mantiene un momentum (media mobile del gradiente) e un learning rate adattivo per parametro (inversamente proporzionale alla radice della media dei gradienti quadratici). In pratica converge molto più velocemente di SGD vanilla.
K-Means gradient-based
Un esperimento: i centroidi come nn.Parameter, la loss come somma delle distanze minime dai centroidi. Backprop aggiorna i centroidi. Non è il modo standard di fare K-Means, ma dimostra che l'autograd può ottimizzare qualsiasi funzione differenziabile.
4. deep-learning — MLP MNIST, ResNet, Object Detection
Il repository più denso. Tre aree: classificazione di immagini da zero, transfer learning con ResNet-50, e object detection.
MNIST: il "Hello World" del deep learning
60.000 immagini 28×28 di cifre scritte a mano, 10 classi. Il dataset è talmente standard che spesso viene usato per testare che l'infrastruttura funzioni, non tanto il modello. Un MLP semplice raggiunge ~98% di test accuracy.
class MNISTClassifier(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Flatten(),
nn.Linear(784, 256),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, 10)
)
def forward(self, x):
return self.net(x)
# CrossEntropyLoss = LogSoftmax + NLLLoss in un solo passo
criterion = nn.CrossEntropyLoss()
Dropout disattiva casualmente una fraction dei neuroni durante il training, forzando la rete a non dipendere da singoli neuroni (regolarizzazione). Early stopping ferma il training quando la validation loss smette di migliorare per N epoche consecutive.
ResNet-50 e transfer learning
Le reti profonde soffrono del vanishing gradient: il gradiente si attenua strato dopo strato fino a diventare inutile. ResNet risolve il problema con le skip connections:
Con il transfer learning, si usa ResNet-50 pre-addestrata su ImageNet: i primi layer (che riconoscono edge, texture, forme) sono già ottimizzati e si "congelano". Si sostituisce solo l'ultimo layer di classificazione, che viene addestrato sul nostro dataset specifico.
import torchvision.models as models
resnet = models.resnet50(weights='IMAGENET1K_V2')
# Congela tutti i layer
for param in resnet.parameters():
param.requires_grad = False
# Sostituisce solo il classificatore finale
num_classes = 5
resnet.fc = nn.Linear(resnet.fc.in_features, num_classes)
Object Detection
La detection con Sliding Window scorre finestre di dimensioni diverse sull'immagine, classificando ogni patch. È concettualmente semplice ma computazionalmente enorme. YOLO (You Only Look Once) divide l'immagine in una griglia e predice bounding box e classi in un singolo forward pass — decine di volte più veloce. Per il dataset di test ho generato un dataset sintetico di forme geometriche (cerchi, rettangoli, triangoli) usando PIL.
5. image-ocr-form — Deployment ML in Django
Il gap tra "il modello funziona in un notebook" e "il modello funziona in produzione" è spesso sottovalutato. Questo repository esplora esattamente quel gap: una web app Django dove l'utente disegna una cifra su canvas HTML5 e il modello risponde in tempo reale.
Pipeline canvas → inference
Domain shift: il problema dell'inversione
MNIST è addestrato su cifre bianche su sfondo nero. Un canvas HTML5 di default ha sfondo bianco. Il modello addestrato su MNIST fallisce completamente. Soluzione: invertire i pixel prima dell'inferenza:
def preprocess_canvas_image(image_data_url):
# Decodifica base64 dal canvas
img_bytes = base64.b64decode(image_data_url.split(',')[1])
img = Image.open(io.BytesIO(img_bytes)).convert('L')
img = img.resize((28, 28))
tensor = transforms.ToTensor()(img)
tensor = 1.0 - tensor # inversione: bianco→nero, nero→bianco
tensor = transforms.Normalize((0.1307,), (0.3081,))(tensor)
return tensor.unsqueeze(0) # aggiunge batch dimension
Inference in Django
# Pattern singleton: carica una volta, riusa sempre
_model = None
def get_model():
global _model
if _model is None:
_model = MNISTClassifier()
_model.load_state_dict(torch.load('mnist_model.pth'))
_model.eval() # disattiva Dropout e BatchNorm per inference
return _model
def predict_digit(request):
tensor = preprocess_canvas_image(request.POST['image'])
with torch.no_grad(): # risparmia memoria, non serve il grafo
output = get_model()(tensor)
pred = output.argmax(dim=1).item()
return JsonResponse({'digit': pred})
model.eval() disattiva il Dropout (tutti i neuroni attivi) e usa le statistiche accumulate per BatchNorm. Dimenticarselo è un bug sottile che dà risultati peggiori senza errori espliciti.
6. nlp — NLP con Hugging Face Transformers
Da PyTorch da zero ai Transformer pre-addestrati. Hugging Face mette a disposizione centinaia di modelli con un'API uniforme. Questo repository copre 5 task NLP classici.
Self-Attention: il cuore dei Transformer
Ogni token produce tre vettori: Query, Key e Value. L'attention score misura quanto ogni token "deve prestare attenzione" agli altri:
Il termine √dk normalizza i prodotti scalari per evitare che i gradienti del softmax diventino troppo piccoli. La Multi-Head Attention esegue questo meccanismo h volte in parallelo, ognuna su uno spazio proiettato diverso: ogni "testa" può specializzarsi su relazioni diverse (sintassi, coreference, dipendenze semantiche).
Encoder-only vs Decoder-only vs Encoder-Decoder
Encoder-only (BERT, RoBERTa)
Vede tutto il contesto (bidirezionale). Ideale per classificazione, NER, QA estrattiva. Pre-training: Masked LM + Next Sentence Prediction (BERT), solo MLM ottimizzato (RoBERTa).
Decoder-only (GPT)
Autoregressive: genera un token alla volta, guardando solo il passato. Naturale per generazione di testo. Pre-training: Causal LM su centinaia di miliardi di token.
Encoder-Decoder (T5, BART)
Encoder legge il testo in input, decoder genera l'output. Ideale per summarization, traduzione, riformulazione. BART è un denoising autoencoder: impara a ricostruire testo corrotto.
5 task con Hugging Face pipeline
from transformers import pipeline
# 1. Sentiment Analysis
sentiment = pipeline("sentiment-analysis")
print(sentiment("Questo modello funziona alla grande!"))
# 2. Question Answering estrattivo
qa = pipeline("question-answering", model="deepset/roberta-base-squad2")
print(qa(question="Chi ha scritto questo?", context=contesto))
# 3. Summarization
summarizer = pipeline("summarization", model="facebook/bart-large-cnn")
print(summarizer(testo_lungo, max_length=130, min_length=30))
# 4. Traduzione
translator = pipeline("translation_en_to_it", model="Helsinki-NLP/opus-mt-en-it")
print(translator("The attention mechanism is fundamental."))
# 5. Text Generation
generator = pipeline("text-generation", model="gpt2")
print(generator("Il deep learning è", max_new_tokens=50))
7. nlp-form — QA in Web App Django con Hub Dinamico
Un passo avanti rispetto ai notebook: una web app Django dove l'utente sceglie il modello da un menu popolato dinamicamente dall'Hub di Hugging Face, inserisce contesto e domanda, e riceve la risposta.
Popolamento dinamico da HuggingFace Hub
from huggingface_hub import HfApi
def get_qa_models_from_hub():
api = HfApi()
models = api.list_models(
task="question-answering",
sort="downloads",
direction=-1,
limit=20
)
return [(m.modelId, m.modelId) for m in models]
# forms.py
class QAForm(forms.Form):
model_name = forms.ChoiceField(choices=get_qa_models_from_hub())
context = forms.CharField(widget=forms.Textarea())
question = forms.CharField()
QA estrattiva: come funziona RoBERTa
Il modello non genera la risposta: la estrae. Riceve in input [CLS] domanda [SEP] contesto [SEP] e predice due indici: start e end dello span di risposta nel contesto. Le due teste di classificazione producono una distribuzione di probabilità su ogni token.
Pipeline caching in-process
# Cache dei modelli già caricati (dentro il processo Django)
_pipeline_cache = {}
def get_qa_pipeline(model_name):
if model_name not in _pipeline_cache:
_pipeline_cache[model_name] = pipeline(
"question-answering",
model=model_name
)
return _pipeline_cache[model_name]
| Modello | Latency (prima richiesta) | Latency (cached) |
|---|---|---|
| DistilBERT | ~3s (download) | ~50ms |
| RoBERTa-base | ~8s (download) | ~120ms |
| BERT-large | ~20s (download) | ~400ms |
8. openai-projects — LLM via API OpenAI
GPT come strumento, non come oggetto di studio. L'attenzione si sposta su come usare i modelli in modo efficace: gestione del contesto, costi, pattern multi-turn.
Come funziona GPT (in breve)
GPT è un decoder Transformer addestrato con Causal Language Modeling: impara a predire ogni token dato il contesto precedente.
Pre-training su centinaia di miliardi di token, poi RLHF per allinearlo alle preferenze umane: prima un Supervised Fine-Tuning su dimostrazioni umane, poi un Reward Model addestrato su confronti, infine PPO (Proximal Policy Optimization) per massimizzare il reward.
Chat Completions API
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Sei un assistente tecnico."},
{"role": "user", "content": "Spiega cos'è la backpropagation."}
],
temperature=0.7, # creatività: 0=deterministico, 1=casuale
top_p=0.9 # nucleus sampling: considera solo il top 90%
)
print(response.choices[0].message.content)
Chatbot multi-turn con trim automatico
class ChatBot:
def __init__(self, system_prompt, max_tokens=4000):
self.messages = [{"role": "system", "content": system_prompt}]
self.max_tokens = max_tokens
def _trim_history(self):
# Mantiene il messaggio system + gli ultimi N scambi
while len(self.messages) > 10:
self.messages.pop(1) # rimuove il più vecchio (dopo system)
def chat(self, user_message):
self.messages.append({"role": "user", "content": user_message})
self._trim_history()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=self.messages
)
reply = response.choices[0].message.content
self.messages.append({"role": "assistant", "content": reply})
# Monitoraggio costi
usage = response.usage
print(f"Tokens: {usage.prompt_tokens}+{usage.completion_tokens}")
return reply
9. langchain-projects — Framework per Applicazioni LLM
LangChain astrae i pattern ripetitivi delle applicazioni LLM: chain, agenti, memory, RAG. Non è sempre la scelta giusta (a volte è troppo astratto), ma capire come funziona è utile anche per evitarlo consapevolmente.
Chains con LCEL
LangChain Expression Language (LCEL) usa l'operatore | per comporre componenti come una pipeline Unix:
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_template(
"Spiega {concetto} in modo semplice."
)
llm = ChatOpenAI(model="gpt-4o-mini")
parser = StrOutputParser()
# PromptTemplate | LLM | OutputParser
chain = prompt | llm | parser
result = chain.invoke({"concetto": "backpropagation"})
ReAct Agents con tool use
from langchain.agents import AgentExecutor, create_react_agent
from langchain_community.tools import DuckDuckGoSearchRun
tools = [DuckDuckGoSearchRun()]
agent = create_react_agent(llm, tools, prompt=react_prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# Il loop: Thought → Action → Observation → Final Answer
result = executor.invoke({"input": "Quali sono i modelli AI più scaricati su HuggingFace?"})
RAG pipeline completo
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Indicizzazione
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = splitter.split_documents(docs)
vectorstore = FAISS.from_documents(chunks, OpenAIEmbeddings())
# Retrieval con MMR (diversità + rilevanza)
retriever = vectorstore.as_retriever(
search_type="mmr",
search_kwargs={"k": 5, "fetch_k": 20}
)
# Generation
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt_template
| llm
| StrOutputParser()
)
MMR (Maximum Marginal Relevance) bilancia rilevanza e diversità: invece di restituire i 5 chunk più simili alla query (che potrebbero essere quasi identici), alterna tra rilevanza e differenza dagli altri risultati già selezionati.
Memory
LangChain offre tre strategie per la memoria conversazionale: BufferMemory (mantiene tutto, diventa pesante), SummaryMemory (riassume periodicamente con un LLM, compatta), TokenBufferMemory (taglia la history quando supera N token, la scelta più pratica).
Risorse e Link
Tutti i repository sono pubblici su GitHub:
| Repository | Tecnologie | Link |
|---|---|---|
| ml | scikit-learn, numpy, matplotlib | github.com/fgasparetto/ml |
| parameters-tuning | scikit-learn, Decision Tree, CV | github.com/fgasparetto/parameters-tuning |
| pytorch-ml | PyTorch, autograd, Adam | github.com/fgasparetto/pytorch-ml |
| deep-learning | PyTorch, MNIST, ResNet, YOLO, PIL | github.com/fgasparetto/deep-learning |
| image-ocr-form | Django, PyTorch inference, Fabric.js | github.com/fgasparetto/image-ocr-form |
| nlp | Hugging Face Transformers, BERT, BART | github.com/fgasparetto/nlp |
| nlp-form | Django, HuggingFace Hub, RoBERTa | github.com/fgasparetto/nlp-form |
| openai-projects | OpenAI API, GPT-4o, RLHF | github.com/fgasparetto/openai-projects |
| langchain-projects | LangChain, LCEL, FAISS, agents | github.com/fgasparetto/langchain-projects |
— Francesco Gasparetto, sviluppatore senior backend & AI enthusiast. Milano.
github.com/fgasparetto