Classificazione del Testo con PyTorch: Guida Completa e codice

Addestrare un classificatore testuale in pytorch con 500 righe di python?

Condividi:

In questo articolo vediamo come implementare un classificatore testuale in pytorch, dalle fondamenta più teoriche agli esempi di codice usando modelli diversi scritti in pytorch.

La classificazione del testo è un task fondamentale nella Natural Language Processing (NLP). Permette di assegnare etichette o categorie predefinite a frammenti di testo o interi documenti.

Analizzeremo la pipeline: dalla tokenization dell'input, passando per i layer di embedding, fino al calcolo della loss function.

Infine, vedremo del codice pytorch con un esempio concreto di classificatore testuale basato su attention. (gist su github)

Che cos'è la classificazione del testo?

La classificazione del testo permette agli algoritmi di comprendere e categorizzare il linguaggio umano. I casi d'uso più semplici con cui iniziare includono la sentiment analysis, il rilevamento dello spam e il routing automatico dei ticket (pensa ai centri di supporto con migliaia di richieste).

In PyTorch, questo task può essere modellato come un problema di apprendimento supervisionato. Il modello riceve una sequenza testuale in input, lo trasforma in rappresentazioni vettoriali continue (embeddings) e calcola la probabilità che appartenga a una determinata classe.

Il cuore matematico del layer finale per la classificazione multiclasse è la funzione softmax. Questa funzione converte i logit non normalizzati ziz_i in probabilità pip_i:

pi=ezij=1Kezjp_i=\frac{e^{z_i}}{\sum_{j=1}^{K}e^{z_j}}

Dove KK rappresenta il numero totale di classi disponibili.

Durante la fase di training, ottimizziamo i pesi del modello minimizzando una loss function. Nelle architetture multiclasse utilizziamo la Cross-Entropy Loss. Se definiamo ycy_c come la classe target e y^c\hat{y}_c come la probabilità predetta:

L=c=1Kyclog(y^c)L=-\sum_{c=1}^{K}y_c\log(\hat{y}_c)

Dalle parole agli embeddings: Tokenization e Vocabolario

I modelli di deep learning non possono leggere il testo grezzo; masticano esclusivamente numeri e matrici. Il primo step fondamentale in qualsiasi pipeline di NLP è quindi la tokenization.

Questo processo scompone una frase in unità più piccole e gestibili chiamate token, che possono rappresentare parole intere, sotto-parole o persino singoli caratteri (come nell'implementazione proposta in questo post). Una volta frammentato il testo, ogni token viene mappato in un indice intero univoco facendo riferimento a un vocabolario predefinito.

Il numero totale di token conosciuti dal modello definirà il parametro vocab_size, essenziale per inizializzare i layer successivi.

Oltre gli indici: Embedding e Positional Encoding

Passare dei semplici numeri interi a una rete neurale non è sufficiente. Matematicamente, l'ID 45 e l'ID 46 non hanno alcuna relazione semantica tra loro. È qui che entra in gioco il layer di Embedding (nn.Embedding in PyTorch).

Questo modulo agisce come una grande lookup table (una tabella di ricerca) che trasforma ogni ID intero in un vettore continuo e denso a dimensionalità fissa (ad esempio, 128 o 256 dimensioni). In questo nuovo spazio vettoriale, le parole con significati simili o che compaiono in contesti simili finiscono per raggrupparsi vicine tra loro.

Tuttavia, c'è un ultimo ostacolo. Poiché stiamo per implementare un'architettura basata su Attention (che elabora tutti i token contemporaneamente perdendo il concetto di sequenza), dobbiamo reintrodurre la cognizione del tempo e dell'ordine:

  • Positional Encoding: Consiste nell'iniettare (solitamente tramite addizione) un segnale matematico nei vettori di embedding originali. Sfruttando funzioni sinusoidali, diamo al modello un'indicazione precisa della posizione di ogni parola. Senza questo passaggio, il nostro modello tratterebbe la frase "il cane morde l'uomo" in modo identico a "l'uomo morde il cane" o in altri termini, come una semplice "bag of words".

Implementazione di un classificatore testuale in pytorch

Adesso vediamo un'implementazione pratica di un text classifier. In questo caso tiriamo su un modello efficiente e moderno basato su Attention.

Innanzitutto, come buona prassi ingegneristica, abbiamo bisogno di realizzare un modulo che rappresenti il backbone, la spina dorsale che poi possiamo estendere con la nostra classification head.

Perchè? Riutilizzo del codice e finetuning. Così puoi riprendere il backbone per altri task, piuttosto che riscrivere ogni layer da capo. Inoltre, potrai caricare i pesi nel caso in cui hai preaddestrato il backbone con tecniche di pretraining, come masked language modeling.

Nota: Questo backbone agisce come un puro Encoder, elaborando l'input per estrarne le feature fondamentali. Se ti interessa capire come questa componente interagisca con un Decoder per task generativi (come la traduzione automatica o il riassunto di testi), ti consiglio di leggere il mio articolo dedicato ai modelli Encoder-Decoder. Qui, invece, ci limiteremo ad "attaccare" all'Encoder una semplice classification head.

class AttentionTextBackbone(nn.Module):
    def __init__(
        self,
        vocab_size: int,
        pad_token_id: int,
        embed_dim: int = 128,
        num_heads: int = 4,
        dropout: float = 0.3,
        max_len: int = 2048,
    ) -> None:
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=pad_token_id)
        nn.init.normal_(self.embedding.weight, std=0.02)
        self.pos_encoder = SinusoidalPositionalEncoding(embed_dim, max_len, dropout)
        self.norm = nn.RMSNorm(embed_dim)
        self.attn = nn.MultiheadAttention(
            embed_dim, num_heads, dropout=0.0, batch_first=True
        )

    def forward(
        self,
        input_ids: torch.Tensor,
        attention_mask: torch.Tensor,
        prefix_embeds: Optional[torch.Tensor] = None,
    ) -> torch.Tensor:
        tok = self.embedding(input_ids)  # (B, L, E)

        if prefix_embeds is not None:
            x = torch.cat([prefix_embeds, tok], dim=1)  # (B, P+L, E)
            B, P = prefix_embeds.size(0), prefix_embeds.size(1)
            prefix_mask = torch.zeros(
                B, P, dtype=torch.bool, device=input_ids.device
            )  # prefix positions are never masked
            key_padding_mask = torch.cat(
                [prefix_mask, attention_mask == 0], dim=1
            )  # (B, P+L)
        else:
            x = tok  # (B, L, E)
            key_padding_mask = attention_mask == 0  # (B, L)

        x = self.pos_encoder(x)

        x_norm = self.norm(x)
        attn_out, _ = self.attn(x_norm, x_norm, x_norm, key_padding_mask=key_padding_mask)
        return x + attn_out  # (B, P+L, E) or (B, L, E)

Ed ecco infine il nostro classificatore:

class AttentionTextClassifier(nn.Module):

    def __init__(
        self,
        num_classes: int,
        vocab_size: int,
        pad_token_id: int,
        embed_dim: int = 128,
        num_heads: int = 4,
        hidden_dim: int = 256,
        dropout: float = 0.2,
        max_len: int = 2048,
    ) -> None:
        super().__init__(num_classes)

        self.backbone = AttentionTextBackbone(
            vocab_size=vocab_size,
            pad_token_id=pad_token_id,
            embed_dim=embed_dim,
            num_heads=num_heads,
            dropout=dropout,
            max_len=max_len,
        )
        self.cls_tokens = nn.Parameter(torch.empty(1, NUM_CLS_TOKENS, embed_dim))
        nn.init.trunc_normal_(self.cls_tokens, std=0.02)
        self.fc1 = nn.Linear(NUM_CLS_TOKENS * embed_dim, hidden_dim)
        self.relu = nn.ReLU()
        self.dropout = nn.Dropout(dropout)
        self.fc2 = nn.Linear(hidden_dim, num_classes)

    def forward(self, batch: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
        input_ids = batch["input_ids"]  # (B, L)
        attention_mask = batch["attention_mask"]  # (B, L)

        B = input_ids.size(0)
        cls = self.cls_tokens.expand(B, -1, -1)  # (B, 4, E)

        # Backbone prepends CLS tokens, applies pos enc + attention
        x = self.backbone(input_ids, attention_mask, prefix_embeds=cls)  # (B, 4+L, E)

        # Flatten the 4 CLS output tokens and classify
        cls_out = x[:, :NUM_CLS_TOKENS, :].flatten(start_dim=1)  # (B, 4*E)
        logits = self.fc2(self.dropout(self.relu(self.fc1(cls_out))))  # (B, C)

        return {"logits": logits}

Puoi trovare il codice completo in questo gist da circa 550 righe. Il modello viene addestrato su mascIT/itacola (è un task molto difficile from scratch!).

E' un unico script, con dentro tutto il necessario. Docs, config, dataloader, modello, trainer.

Puoi eseguirlo con uv one-shot: uv run https://gist.github.com/masc-it/862d6597f9b29e2127a31eb071f60f07/raw

Domande frequenti sulla text classification in pytorch

Mauro Sciancalepore - Notizie AI, Deep Learning e Ricerca

Resta aggiornato sulle ultime notizie di Intelligenza Artificiale e Deep Learning. Approfondimenti completi sulla ricerca e stato dell'arte.

© 2026 mauroscia.it
Tutti i diritti riservati.