Costruzione di un Classificatore di Titoli di Notizie con AWS SageMaker
Home » Projects » AI Projects » Costruzione di un Classificatore di Titoli di Notizie con AWS SageMaker
Panoramica del Progetto
Questo caso di studio descrive lo sviluppo e la distribuzione di un sistema di classificazione multiclasse per titoli di notizie utilizzando AWS SageMaker, i Transformers di Hugging Face e PyTorch. Il sistema categorizza automaticamente i titoli di notizie in una delle quattro categorie: Business, Scienza, Intrattenimento o Salute.
Obiettivi del Progetto
Costruire un classificatore di titoli di notizie ad alta precisione
Implementare un'architettura personalizzata basata su transformer
Distribuire il modello in un ambiente di produzione scalabile
Creare un'API RESTful per l'inferenza in tempo reale
Tecnologie Utilizzate
AWS SageMaker: Per l'addestramento e l'hosting del modello
PyTorch: Per lo sviluppo del modello
Hugging Face Transformers: Per modelli linguistici pre-addestrati (DistilBERT)
AWS Lambda & API Gateway: Per la distribuzione di API serverless
CloudWatch: Per il monitoraggio e la registrazione
Postman: Per il test dell'API
Raccolta e Preparazione dei Dati
Il progetto ha utilizzato un dataset di aggregazione di notizie dal repository UCI Machine Learning contenente oltre 400.000 titoli di notizie da varie fonti. Link del dataset
Fasi di Elaborazione dei Dati:
Caricamento dei dati dal bucket S3 utilizzando pandas
Estrazione dei campi 'TITLE' e 'CATEGORY'
Trasformazione delle etichette di categoria da abbreviazioni (es. 'b') a nomi completi (es. 'Business')
Codifica delle categorie come valori numerici
Suddivisione dei dati in 80% per il training e 20% per il testing
my_dict = {
'e': 'Entertainment',
'b': 'Business',
't': 'Science',
'm': 'Health'
}
def update_category(x):
return my_dict[x]
df_work['CATEGORY'] = df_work['CATEGORY'].apply(lambda x: update_category(x))
# Codifica delle categorie come valori numerici</em>
encode_dict = {}
def encode_cat(x):
if x not in encode_dict.keys():
encode_dict[x] = len(encode_dict)
return encode_dict[x]
df['ENCODE_CAT'] = df['CATEGORY'].apply(lambda x: encode_cat(x))Code language:PHP(php)
Dataset prima e dopo l'encoding delle categorie
Analisi Esplorativa dei Dati
L'analisi del dataset ha rivelato la seguente distribuzione delle categorie:
Business: ~27,5%
Scienza: ~25,6%
Intrattenimento: ~36,1%
Salute: ~10,8%
Questa distribuzione relativamente bilanciata ha significato che non era necessario un trattamento speciale per lo sbilanciamento delle classi.
Architettura del Modello
Ho scelto di personalizzare un modello DistilBERT, che offre un buon equilibrio tra prestazioni ed efficienza. DistilBERT è una versione più piccola e veloce di BERT che mantiene circa il 97% delle sue capacità di comprensione del linguaggio pur essendo il 40% più piccola e il 60% più veloce.
Personalizzazione del Modello:
Utilizzo di DistilBERT pre-addestrato come estrattore di feature
Aggiunta di un layer lineare pre-classificatore (768 → 768 dimensioni)
Implementazione di dropout (0.3) per la regolarizzazione
Aggiunta di una funzione di attivazione ReLU per la non linearitÃ
Aggiunta di un layer di classificazione finale (768 → 4 dimensioni, una per ogni categoria)
# Customized model based on the imported model
class DistilBERTClass(torch.nn.Module):
def __init__(self):
# Definire sempre layer e componenti con parametri trainabili
# sempre dentro il metodo init, altrimenti i paremtri verrebbero
# inizializzati ogni volta
# super().__init__()
super(DistilBERTClass, self).__init__()
# load pretrained model for transfer learning
self.l1 = DistilBertModel.from_pretrained('distilbert-base-uncased')
# additional weights
self.pre_classifier = torch.nn.Linear(768, 768)
# random drop 30% of neurons
self.dropout = torch.nn.Dropout(0.3)
# map out final vector into 1 of our 4 classes
self.classifier = torch.nn.Linear(768, 4)
def forward(self, input_ids, attention_mask):
# qui chiamiamo quello definito sopra
output_1 = self.l1(input_ids=input_ids, attention_mask=attention_mask)
# final output del transformer layer in DistilBert
hidden_state = output_1[0]
# extract first special token CLS
pooler = hidden_state[:, 0]
pooler = self.pre_classifier(pooler)
# Activation function ReLU, aggiunge non linearitÃ
pooler = torch.nn.ReLU()(pooler)
pooler = self.dropout(pooler)
output = self.classifier(pooler)
# RAW LOGITS
return output
Processo di Training
Il modello è stato addestrato su AWS SageMaker utilizzando un'istanza GPU (ml.g4dn.xlarge) per un addestramento accelerato.
Configurazione dell'Addestramento:
Ottimizzatore: Adam con learning rate di 1e-5
Funzione di Loss: Cross-Entropy Loss
Dimensione del Batch: 4 per il training, 2 per la validazione
Epoche: 2 (sufficienti grazie al transfer learning)
Lunghezza Massima della Sequenza: 512 token
def train(epoch, model, device, training_loader, optimizer, loss_function):
tr_loss = 0
n_correct = 0
nb_tr_steps = 0
nb_tr_examples = 0
model.train()
for _, data in enumerate(training_loader, 0):
ids = data['ids'].to(device, dtype=torch.long)
mask = data['mask'].to(device, dtype=torch.long)
targets = data['targets'].to(device, dtype=torch.long)
# chiamiamo il forward method della classe
outputs = model(ids, mask)
loss = loss_function(outputs, targets)
tr_loss += loss.item()
big_val, big_idx = torch.max(outputs.data, dim=1)
n_correct += calculate_accu(big_idx, targets)
nb_tr_steps += 1
nb_tr_examples += targets.size(0)
# 100k / 4 = 25,000 stepsif _ % 5000 == 0:
loss_step = tr_loss/nb_tr_steps
accu_step = (n_correct*100)/nb_tr_examples
print(f"Training loss per 5000 steps: {loss_step}")
print(f"Training Accuracy per 5000 steps: {accu_step}")
# gradient descent and backpropagation# Dobbiamo impostare a 0, perchè in pyThorc accumulano di default
optimizer.zero_grad()
# BACK propagation starting from the loss
loss.backward()
# agggiorna i pesi
optimizer.step()
# print(f"The total accuracy for epoch {epoch}: {(n_correct*100)/nb_tr_examples}")
epoch_loss = tr_loss / nb_tr_steps
epoch_accu = (n_correct*100)/nb_tr_examples
print(f"Training Loss Epoch:{epoch_loss} ")
print(f"Training Accuracy Epoch {epoch_accu}")
returnCode language:PHP(php)
Gestione del Padding e Attention Mask
Un aspetto cruciale della configurazione dell'addestramento, spesso trascurato nelle discussioni, è il trattamento del padding e l'uso dell'attention mask nei modelli transformer.
# Estratto dalla classe NewsDataSet
inputs = self.tokenizer.encode_plus(
title,
None,
add_special_tokens=True,
max_length=self.max_len,
padding='max_length',
return_token_type_ids=True,
return_attention_mask=True,
truncations=True,
)Code language:PHP(php)
Questo approccio:
Aggiunge token di padding [PAD] (ID=0) fino a raggiungere la lunghezza massima di 512 token
Standardizza la dimensione degli input per l'elaborazione in batch
Evita sprechi di risorse computazionali impostando una lunghezza massima ragionevole
Attention Mask: Ignorare il Padding
Il vero potere di questo approccio emerge con l'attention mask, un tensore binario che indica al modello quali token considerare (1) e quali ignorare (0):
ids = inputs['input_ids'] # Token IDs inclusi i padding
mask = inputs['attention_mask'] # 1 per token reali, 0 per paddingCode language:PHP(php)
Quando questi tensori vengono passati al modello:
outputs = model(ids, mask)
L'attention mask viene utilizzato internamente per modificare gli attention scores:
Per ogni position con valore di mask=0 (padding), i punteggi di attenzione vengono impostati a -inf prima dell'applicazione del softmax
Dopo l'applicazione del softmax, questi punteggi diventano effettivamente 0
Di conseguenza, i token di padding non contribuiscono al calcolo dell'output
Previene che il modello "sprechi attenzione" sui token di padding
Mantiene la stabilità numerica dell'addestramento
Consente l'elaborazione efficiente di batch con sequenze di lunghezze diverse
Migliora la qualità delle rappresentazioni contestuali per i token reali
Nella nostra implementazione, questo meccanismo ha permesso di gestire efficacemente titoli di notizie che variavano da poche parole a decine di parole, mantenendo la coerenza dell'addestramento e migliorando la qualità delle predizioni, soprattutto per titoli più complessi e lunghi.
La combinazione di padding controllato e attention mask ha contribuito significativamente all'alto tasso di accuratezza ottenuto (~91.7% in validazione) nonostante la variabilità della lunghezza dei titoli nel dataset.
Risultati di Training e Validazione:
Accuratezza di training: ~93.7%
Accuratezza di validazione: ~91.7%
Il modello ha mostrato una buona convergenza dopo solo 2 epoche, indicando un apprendimento efficace senza overfitting.
Funzione di Loss: Cross Entropy
Per la formazione del nostro modello di classificazione delle notizie, ho utilizzato la funzione di loss Cross Entropy, che è particolarmente adatta per problemi di classificazione multiclasse.
\(y_c\) è l'etichetta vera per la classe \(c\) (1 per la classe corretta, 0 per le altre)
\(\hat{y}_c\) è la probabilità predetta per la classe \(c\) (output del softmax)
C è il numero totale di classi (4 nel nostro caso: Business, Science, Entertainment, Health)
In PyTorch, questo viene implementato attraverso la classe nn.CrossEntropyLoss(), che combina:
nn.LogSoftmax: Applica il logaritmo dopo la normalizzazione softmax
nn.NLLLoss: Applica la loss di entropia negativa
# Definizione della funzione di loss
loss_function = torch.nn.CrossEntropyLoss()
# Durante il training
outputs = model(ids, mask) # Output raw (logits)
loss = loss_function(outputs, targets) # Calcolo della lossCode language:PHP(php)
Comportamento della Loss durante il Training
Durante il training, l'obiettivo è minimizzare questa loss. Una loss perfetta sarebbe 0, che si verificherebbe se il modello assegnasse una probabilità di 1 alla classe corretta e 0 a tutte le altre classi. Tuttavia, questo è raramente raggiungibile in pratica.
Nel nostro caso, la loss di training è diminuita significativamente durante le prime epoche:
All'inizio: ~1.38 (valore alto che indica predizioni casuali)
Fine della prima epoca: ~0.37 (miglioramento significativo)
Fine della seconda epoca: ~0.17 (ulteriore affinamento)
Questa rapida diminuzione della loss indica che il modello stava apprendendo efficacemente dai dati. Allo stesso tempo, la loss di validazione si è stabilizzata intorno a ~0.24, indicando un buon equilibrio tra underfitting e overfitting.
Ottimizzazione basata sulla Loss
La loss viene utilizzata per aggiornare i parametri del modello attraverso la backpropagation:
# Backpropagation e ottimizzazione
optimizer.zero_grad() # Azzera i gradienti accumulati di default da PyThorc
loss.backward() # Calcola i gradienti rispetto alla loss
optimizer.step() # agggiorna i pesiCode language:PHP(php)
L'ottimizzatore Adam utilizza i gradienti calcolati dalla backpropagation per aggiornare i parametri del modello in modo da minimizzare la loss. Il learning rate di 1e-5 garantisce aggiornamenti sufficientemente piccoli per non saltare il minimo ottimale.
Vantaggi della Cross Entropy per la Classificazione Multiclasse
Penalizza fortemente le predizioni errate con alta confidenza
Fornisce un gradiente significativo anche quando la classificazione è corretta ma con bassa confidenza
Funziona bene con l'output softmax che normalizza le probabilità tra le classi
Questi vantaggi hanno reso la Cross Entropy la scelta ideale per il nostro task di classificazione delle notizie, contribuendo alla rapida convergenza e all'alta accuratezza del modello finale.
Distribuzione del Modello
La distribuzione ha seguito un processo multi-fase per garantire affidabilità e scalabilità :
La funzione model_fn si occupa di caricare il modello addestrato dalla sua posizione su disco all'avvio dell'endpoint. Questo processo avviene una sola volta all'inizializzazione dell'endpoint:
defmodel_fn(model_dir):# Log informativo per il debugging
logger.info(f"Loading model from: {model_dir}")
# Istanziazione dell'architettura del modello
model = DistilBERTClass()
# Caricamento dei pesi addestrati
model_state_dict = torch.load(os.path.join(model_dir, 'pytorch_distilbert_news.bin'),
map_location=torch.device('cpu'))
model.load_state_dict(model_state_dict)
# Caricamento del tokenizer - importante per preprocessare nuovi input
tokenizer_path = os.path.join(model_dir, 'tokenizer')
# Gestione robusta del caricamento del tokenizer con fallbacktry:
if os.path.exists(tokenizer_path):
tokenizer = DistilBertTokenizer.from_pretrained(tokenizer_path)
else:
# Fallback al download da Hugging Face<
tokenizer = DistilBertTokenizer.from_pretrained('distilbert-base-uncased')
except Exception as e:
logger.error(f"Errore nel caricamento del tokenizer: {e}")
tokenizer = DistilBertTokenizer.from_pretrained('distilbert-base-uncased')
return modelCode language:Python(python)
B. Preprocessing degli Input (input_fn)
La funzione input_fn si occupa di trasformare i dati in arrivo dal client (tipicamente in formato JSON) in un formato che il modello può processare:
def input_fn(request_body, request_content_type): # Verifica che il formato dei dati in ingresso sia corretto</em>if request_content_type == 'application/json':
# Parsing del JSON
input_data = json.loads(request_body)
# Estrazione del testo da classificare
sentence = input_data['inputs']
return sentence
else:
# Gestione degli errori per formati non supportati
raise ValueError(f"Unsupported content type: {request_content_type}")Code language:PHP(php)
C. Predizione e Formattazione delle Risposte (predict_fn e output_fn)
Queste funzioni costituiscono il cuore dell'inferenza, eseguendo la predizione effettiva sul modello e formattando la risposta da restituire al client:
def predict_fn(input_data, model):
# Selezione del dispositivo (GPU o CPU)
device = torch.device("cuda"if torch.cuda.is_available() else"cpu")
model.to(device)
# Tokenizzazione dell'input
inputs = tokenizer(input_data, return_tensors="pt").to(device)
ids = inputs['input_ids'].to(device)
mask = inputs['attention_mask'].to(device)
# Predizione con il modello
model.eval()
with torch.no_grad():
outputs = model(ids, mask)
# Conversione dei logits in probabilità con softmax
probabilities = torch.softmax(outputs, dim=1).cpu().numpy()
# Mappatura dell'indice della classe prevista al nome leggibile
class_names = ["Business", "Science", "Entertainment", "Health"]
predicted_class = probabilities.argmax(axis=1)[0]
predicted_label = class_names[predicted_class]
# Costruzione della risposta strutturatareturn {
'predicted_label': predicted_label,
'probabilities': probabilities.tolist()
}
def output_fn(prediction, accept):
# Formattazione della risposta nel formato richiesto dal clientif accept == 'application/json':
return json.dumps(prediction), accept
else:
# Gestione degli errori per formati non supportati<
raise ValueError(f"Unsupported accept type: {accept}")Code language:PHP(php)
Questa architettura modulare per l'inferenza consente:
Flessibilità : Supporto per diversi formati di input/output
Robustezza: Gestione degli errori a ogni passaggio
Ottimizzazione: Riutilizzo del tokenizer e altre ottimizzazioni
Manutenibilità : Separazione chiara delle responsabilitÃ
È stato configurato un API Gateway per esporre la funzione Lambda come endpoint API RESTful, con:
Metodo POST per le richieste di inferenza
Formato di richiesta/risposta JSON
Load Testing
Prima del rilascio completo in produzione, ho condotto test di carico per garantire che il sistema potesse gestire il traffico reale:
Creato dati di test con vari titoli di notizie
Utilizzato SageMaker Inference Recommender per ottimizzare la selezione dell'istanza
Simulato richieste concorrenti per determinare i limiti di prestazione
Monitorato latenza, throughput e tassi di errore
I risultati hanno mostrato che il sistema poteva gestire ~100 richieste al minuto su una singola istanza con una latenza media inferiore a 200ms, sufficiente per il traffico previsto.
Esempi di Risultati di Inferenza
Il test dell'API distribuita con titoli di esempio ha prodotto classificazioni accurate:
L'endpoint di inferenza falliva frequentemente con errori "429 Too Many Requests" durante l'uso intensivo a causa di ripetuti download del tokenizer da Hugging Face. L'identificazione di questo problema è stata possibile solo grazie all'attento monitoraggio dei log di CloudWatch, evidenziando l'importanza cruciale degli strumenti di osservabilità in ambienti ML di produzione.
Soluzione: Refactoring del codice di inferenza per caricare il tokenizer una sola volta durante l'inizializzazione del modello, implementando una variabile globale e un sistema di caching locale. L'analisi dei log con CloudWatch ha permesso di individuare rapidamente l'origine del problema, risparmiando ore di debugging. Questa ottimizzazione ha ridotto drasticamente le richieste esterne e ha permesso all'endpoint di gestire volumi elevati di richieste senza interruzioni, dimostrando come un'efficace strategia di monitoraggio possa trasformarsi in miglioramenti tangibili delle prestazioni.
Conclusione Questo progetto ha dimostrato l'intero ciclo di vita ML dalla elaborazione dei dati alla distribuzione in produzione utilizzando AWS SageMaker. Il sistema finale raggiunge un'accuratezza superiore al 90% nella categorizzazione dei titoli di notizie e può essere facilmente integrato in piattaforme di aggregazione di notizie, sistemi di gestione dei contenuti o dashboard analitiche.
Apprendimenti Chiave:
Il transfer learning con modelli pre-addestrati riduce drasticamente il tempo di addestramento e i requisiti di dati
L'architettura del modello personalizzata può migliorare significativamente le prestazioni per compiti specifici
AWS SageMaker semplifica la distribuzione ma richiede una configurazione attenta per l'ottimizzazione dei costi e la gestione delle dipendenze esterne
Le architetture serverless forniscono scaling flessibile per i carichi di lavoro di inferenza ML
La gestione efficiente delle risorse esterne (come i tokenizer) è cruciale per sistemi di produzione affidabili
Miglioramenti Futuri:
Implementare l'auto-scaling basato sui pattern di traffico
Esplorare tecniche di distillazione per ridurre le dimensioni del modello e i costi di inferenza
Aggiungere supporto per più lingue
Espandere a categorie di notizie più granulari
Implementare il packaging completo del modello inclusi i tokenizer per eliminare completamente le dipendenze esterne
Strumenti come questo classificatore di notizie non solo rendono l'organizzazione dei contenuti più efficiente, ma possono alimentare sistemi di raccomandazione, feed di notizie personalizzati e analisi delle tendenze per le aziende media.
Come ho trasformato un esperimento del corso di Ed Donner in un chatbot professionale integrato nel mio sito WordPress 📖 Il Punto di Partenza: Agenti AI Costruiti a Mano La settimana scorsa ho seguito il corso di Ed Donner sulla costruzione di agenti AI, dove abbiamo imparato a creare agenti "from scratch" Qui il link […]
Ogni tanto capita un progetto che ti cambia la prospettiva. Per me, questo è stato uno di quelli. Durante il corso Build AI Agents with AWS di Zero To Mastery, ho avuto la sensazione di aver sbloccato un nuovo livello: Mi ha fatto scoprire un mondo completamente nuovo: quello dell’orchestrazione multi-agente in cloud, dove ogni […]