Salta al contenuto principale

Creazione del dataset per Rasa

Profile picture for user luca77king

Rasa è uno degli strumenti più potenti per lo sviluppo di chatbot intelligenti, ma la sua efficacia è direttamente proporzionale alla qualità del dataset su cui viene addestrato. Quando si entra nel vivo del lavoro con Rasa, si scopre che il dataset non è un semplice insieme di frasi, ma una rappresentazione stratificata dell’interazione umana, un microcosmo di intenzioni, variabili e contesti che il sistema deve imparare a riconoscere, gestire e replicare con precisione.

Nel cuore dell’architettura di Rasa, il dataset svolge un ruolo centrale: è l’elemento che definisce l’intelligenza del chatbot. Ogni esempio, ogni intento, ogni entità e ogni passaggio narrativo racconta qualcosa sul comportamento atteso del sistema. Non si tratta solo di insegnargli a rispondere, ma di guidarlo nel capire cosa l’utente vuole, quali informazioni sono rilevanti nella frase e come portare avanti una conversazione coerente anche in presenza di ambiguità, errori o cambi di contesto. La struttura del dataset riflette questa complessità, suddividendosi in tre componenti fondamentali: gli intenti, che racchiudono le intenzioni esplicite dell’utente; le entità, che rappresentano gli elementi informativi che caratterizzano ogni richiesta; e le storie, che fungono da sceneggiature dinamiche in grado di istruire il chatbot su come gestire il flusso di conversazione nel tempo.

Ma il cuore di tutto è la pianificazione. Non si può iniziare a scrivere frasi senza sapere a che tipo di utente ci si sta rivolgendo, quali obiettivi deve raggiungere il chatbot e quali percorsi deve saper gestire. La progettazione di un dataset parte dalla mappatura mentale del dominio applicativo, dall’analisi degli scenari più probabili fino ai casi limite che potrebbero mandare in crisi il sistema. Ogni intento va costruito tenendo conto della varietà linguistica degli utenti, delle forme alternative con cui un concetto può essere espresso, della necessità di distinguere tra formulazioni simili ma semanticamente diverse.

Raccogliere i dati per un dataset di Rasa richiede un mix di metodo, creatività e, dove possibile, automazione. La raccolta manuale permette un controllo qualitativo molto alto, ma è un lavoro lento e spesso soggetto a bias inconsapevoli. L’analisi di log conversazionali preesistenti o lo scraping di dati da forum e piattaforme online può accelerare la costruzione di un dataset iniziale più realistico, soprattutto nei progetti dove si lavora su larga scala. In questi casi, però, è inevitabile un processo successivo di filtraggio, normalizzazione e validazione, poiché l’accuratezza semantica è imprescindibile per l’addestramento di modelli NLP. La combinazione di entrambe le strategie consente di compensare le rispettive debolezze: la macchina raccoglie, l’umano rifinisce.

Generazione automatica di Intenti e Azioni

Un aspetto sempre più rilevante nella creazione di dataset per Rasa è l'automazione della generazione degli intenti, delle risposte e delle entità a partire da contenuti già esistenti, come i documenti o le pagine informative di un sito. In contesti dove la mole di dati è ampia e strutturata in modo semi-standardizzato, è possibile sfruttare modelli linguistici come Mistral per trasformare questi contenuti in set di dati pronti all'uso.

Il processo inizia con un prompt mirato per estrarre da un testo coppie domanda-risposta alternate, generando direttamente un JSON coerente con la struttura attesa dal file nlu.yml. Si può usare un prompt come questo:

Estrai coppie domanda-risposta dal testo seguente. Rispondi con un array JSON dove ogni elemento ha i campi "question" e "answer".

TESTO:
I nostri tempi di spedizione variano tra 2 e 5 giorni lavorativi. Le spedizioni verso Milano richiedono in media 3 giorni.
Accettiamo pagamenti con carta, bonifico bancario e PayPal. Il pagamento alla consegna non è disponibile.

Risposta attesa da Mistral:

[
  {
    "question": "Quali sono i tempi di spedizione?",
    "answer": "I tempi di spedizione variano tra 2 e 5 giorni lavorativi."
  },
  {
    "question": "Quanto tempo ci vuole per spedire a Milano?",
    "answer": "Le spedizioni verso Milano richiedono in media 3 giorni."
  },
  {
    "question": "Quali metodi di pagamento accettate?",
    "answer": "Accettiamo carta, bonifico bancario e PayPal."
  },
  {
    "question": "Posso pagare alla consegna?",
    "answer": "Il pagamento alla consegna non è disponibile."
  }
]

La creazione automatica del nome dell’intento può essere fatta con una semplice funzione Python, geneando un nome univoco, descrittivo e in snake_case per ogni domanda:

import re
import unicodedata
from nltk.corpus import stopwords

stop_words = set(stopwords.words('italian'))

def make_intent_name(question):
    question = question.lower()
    question = re.sub(r'[^\w\s]', '', question)
    words = question.split()
    words = [w for w in words if w not in stop_words]
    return '_'.join(words[:5])

Risultato:

[
  {
    "intent": "tempi_spedizione",
    "question": "Quali sono i tempi di spedizione?",
    "answer": "I tempi di spedizione variano tra 2 e 5 giorni lavorativi."
  },
  ...
]

Per generare parafrasi, restiamo su Mistral con un prompt tipo:

Per ogni domanda seguente, genera 3 variazioni linguistiche della stessa richiesta mantenendo il significato.

DOMANDA: Quali metodi di pagamento accettate?

- Che tipi di pagamento posso usare?
- Come posso pagare?
- Che opzioni di pagamento offrite?

Puoi iterare questo prompt per ogni domanda, e costruire così l'elenco delle frasi d’esempio per ogni intento:

{
  "intent": "pagamento_metodi",
  "examples": [
    "Quali metodi di pagamento accettate?",
    "Che tipi di pagamento posso usare?",
    "Come posso pagare?",
    "Che opzioni di pagamento offrite?"
  ]
}

Estrazione entità con Spacy

Per l’estrazione delle entità possiamo usare spaCy. L’obiettivo non è solo fare il replace delle entità nelle frasi ma anche salvarle in un dizionario che potrai usare per popolare gli slots in domain.yml.

Perfetto, allora lo correggiamo come si deve. Il punto è che vuoi vedere bene quali entità sono state trovate, su quale frase, con valore e tipo, e non solo vedere il testo annotato.

Ecco una versione migliorata del codice che stampa bene le entità estratte per ogni frase, e ti dà anche il risultato annotato.

import spacy
from collections import defaultdict
import re

nlp = spacy.load("it_core_news_sm")

ENTITY_LABEL_MAP = {
    "GPE": "location",
    "LOC": "location",
    "ORG": "organization",
    "DATE": "date",
    "TIME": "time",
    "MONEY": "amount"
}

def extract_entities(sentences):
    nlu_data = []
    entities_by_intent = defaultdict(set)
    extracted_entities = []

    for s in sentences:
        doc = nlp(s["text"])
        new_text = s["text"]
        entities = []

        for ent in doc.ents:
            label = ENTITY_LABEL_MAP.get(ent.label_)
            if not label:
                continue
            pattern = re.escape(ent.text)
            new_text = re.sub(pattern, f"[{ent.text}]({label})", new_text, count=1)
            entities.append({"entity": label, "value": ent.text})
            entities_by_intent[s["intent"]].add(label)

        nlu_data.append({
            "intent": s["intent"],
            "text": new_text,
            "entities": entities
        })

        extracted_entities.append({
            "intent": s["intent"],
            "original_text": s["text"],
            "annotated_text": new_text,
            "entities": entities
        })

    return nlu_data, entities_by_intent, extracted_entities

Esempio d’uso:

sentences = [
    {"intent": "richiesta_spedizione", "text": "Quanto tempo ci mette la spedizione a Milano?"},
    {"intent": "richiesta_pagamento", "text": "Posso pagare con bonifico bancario entro il 15 maggio?"},
    {"intent": "richiesta_saldo", "text": "Quanto devo pagare se l'importo è di 50 euro?"}
]

nlu_data, slots, extracted = extract_entities(sentences)

for e in extracted:
    print(f"Intent: {e['intent']}")
    print(f"Frase originale: {e['original_text']}")
    print(f"Annotata: {e['annotated_text']}")
    for ent in e['entities']:
        print(f" - Entità trovata: {ent['value']} (tipo: {ent['entity']})")
    print()

Output leggibile:

Intent: richiesta_spedizione
Frase originale: Quanto tempo ci mette la spedizione a Milano?
Annotata: Quanto tempo ci mette la spedizione a [Milano](location)
 - Entità trovata: Milano (tipo: location)

Intent: richiesta_pagamento
Frase originale: Posso pagare con bonifico bancario entro il 15 maggio?
Annotata: Posso pagare con bonifico bancario entro il [15 maggio](date)
 - Entità trovata: 15 maggio (tipo: date)

Intent: richiesta_saldo
Frase originale: Quanto devo pagare se l'importo è di 50 euro?
Annotata: Quanto devo pagare se l'importo è di [50 euro](amount)
 - Entità trovata: 50 euro (tipo: amount)

Una volta raccolti i dati grezzi, entra in gioco una fase altrettanto delicata: la loro trasformazione nel formato accettato da Rasa. YAML è il linguaggio strutturale su cui si fonda la logica interna del framework, e ogni parte del dataset – dagli intenti alle storie – deve essere redatta secondo convenzioni precise. La coerenza nella formattazione non è solo una questione tecnica: incide sulla chiarezza del progetto, sulla sua scalabilità e sulla facilità con cui altri membri del team potranno mettervi mano. Un dataset ben formattato, leggibile e coerente è più facile da mantenere e da estendere nel tempo, soprattutto nei progetti in continua evoluzione.

Evoluzione che non si ferma con la scrittura iniziale. Ogni dataset, per quanto curato, va testato. Serve addestrare il modello, valutare le risposte, simulare conversazioni reali, misurare la precisione nella classificazione degli intenti e il riconoscimento delle entità. Il feedback ottenuto da queste prove guida il processo di raffinamento: si aggiungono esempi, si riscrivono frasi mal interpretate, si correggono errori di logica narrativa nelle storie. In questa fase, la documentazione delle decisioni prese e la tracciabilità delle modifiche diventano strumenti essenziali per evitare regressioni e garantire la coerenza evolutiva del sistema.

È importante interiorizzare che la creazione del dataset non è mai veramente conclusa. Il dialogo tra utenti e chatbot evolve nel tempo. Emergono nuovi modi di porre le stesse domande, nuove esigenze funzionali, nuove situazioni di contesto. Un dataset statico smette rapidamente di essere efficace. Per questo motivo, il ciclo di test, analisi, miglioramento e re-training deve essere continuo. Non basta scrivere un buon dataset: bisogna saperlo ascoltare, interpretare e correggere.

In definitiva, il dataset di Rasa è l’anima operativa del chatbot. È il punto di contatto tra la complessità del linguaggio umano e la logica algoritmica del modello NLP. Costruirlo con attenzione, visione e rigore metodologico è l’unica strada per ottenere un assistente virtuale capace di dialogare davvero, non solo di rispondere. La qualità del dataset è ciò che distingue un chatbot banale da uno che sorprende per fluidità, precisione e intelligenza apparente. E in questo equilibrio delicato tra struttura e spontaneità, tra regole e flessibilità, si gioca tutta la partita.