Vai al contenuto principale

Tutorial su GPT-4o e LangGraph: crea un'applicazione TNT-LLM

Questo articolo presenta una guida pratica all'implementazione di TNT-LLM, un framework che automatizza la generazione di tassonomie e la classificazione del testo tramite LLM, dimostrato con un esempio di clustering di articoli di news usando GPT-4 e LangGraph.
· 14 min leggi

Scopri con l'IA

Microsoft ha introdotto TNT-LLM, un potente sistema progettato per automatizzare la generazione di tassonomie e la classificazione del testo, superando i metodi tradizionali in termini di efficienza e accuratezza.

TNT-LLM sfrutta la potenza dei large language model (LLM) per automatizzare e scalare la creazione di tassonomie e classificatori con un intervento umano minimo. Questo lo rende particolarmente utile per applicazioni come Bing Copilot, dove la gestione di domini testuali diversi e in continua evoluzione è cruciale.

In questo articolo ti guiderò nell'implementazione di TNT-LLM usando GPT-4o e LangGraph per raggruppare in modo efficace articoli di news.

Se vuoi approfondire GPT-4o o LangGraph, ti consiglio questi articoli:

Se vuoi saperne di più su TNT-LLM, ti consiglio l'articolo originale TnT-LLM: Text Mining at Scale with Large Language Models.

Che cos'è TNT-LLM?

TNT-LLM (Taxonomy and Text classification using Large Language Models) è un framework in due fasi sviluppato per generare e classificare tassonomie a partire da documenti di testo. Il sistema prevede due fasi principali.

Fase 1: Generazione della tassonomia

Nella prima fase, il sistema prende un campione di documenti di testo e un'istruzione relativa al caso d'uso, ad esempio "genera una tassonomia per raggruppare articoli di news". Utilizza un large language model (LLM) per riassumere ogni articolo, estraendone le informazioni chiave. Attraverso un processo iterativo, l'LLM genera, aggiorna e perfeziona una tassonomia basata su questi riassunti, producendo infine un insieme strutturato di etichette e descrizioni che categorizzano efficacemente gli articoli.

Illustrazione della fase di generazione della tassonomia basata su LLM

Fonte: Mengting Wan et al.

Fase 2: Classificazione del testo

La seconda fase prevede l'uso della tassonomia generata per etichettare un dataset più ampio. Il sistema chiede a un LLM di applicare queste etichette, che poi fungono da dati di addestramento per un classificatore leggero, come la regressione logistica. Una volta addestrato, questo classificatore può essere distribuito per etichettare in modo efficiente l'intero corpus o effettuare classificazione in tempo reale.

illustrazione della fase di classificazione del testo potenziata da LLM

Fonte: Mengting Wan et al.

La flessibilità di TNT-LLM lo rende applicabile a vari compiti di classificazione del testo, tra cui il rilevamento dell'intento e la categorizzazione degli argomenti, estendendo la sua utilità oltre la sola analisi delle conversazioni.

Vantaggi di TNT-LLM

TNT-LLM si distingue come uno strumento potente per il text mining e la classificazione su larga scala, offrendo diversi vantaggi chiave.

Generazione automatizzata della tassonomia

TNT-LLM semplifica la creazione di tassonomie dettagliate e interpretabili a partire da dati testuali grezzi. Questo processo ha automatizzato un'attività laboriosa che spesso richiede competenze di dominio. Il sistema genera tassonomie su misura per casi d'uso specifici con un input umano minimo, risparmiando tempo e risorse considerevoli.

Classificazione scalabile

Usando le tassonomie generate, TNT-LLM abilita una classificazione del testo scalabile. Consente di creare modelli leggeri che possono gestire dataset di grandi dimensioni o attività di classificazione in tempo reale in modo efficiente, rendendolo ideale per applicazioni con elevati requisiti di throughput.

Approccio conveniente

TNT-LLM ottimizza l'uso delle risorse sfruttando un impiego a livelli degli LLM. Utilizza modelli potenti come GPT-4 per la generazione della tassonomia e opzioni più economiche come GPT-3.5-Turbo per i compiti di sintesi. La classificazione finale è eseguita da un efficiente modello di regressione logistica, riducendo ulteriormente i costi operativi.

Output di alta qualità

Il processo iterativo di generazione della tassonomia in TNT-LLM assicura un raffinamento continuo, producendo categorizzazioni di alta qualità, pertinenti e accurate, adattate a specifici dataset e casi d'uso.

Intervento umano minimo

Pur consentendo revisione e aggiustamenti umani, TNT-LLM richiede un input manuale minimo rispetto ai metodi tradizionali. Questo riduce potenziali bias e incoerenze nella creazione della tassonomia.

Flessibilità

TNT-LLM si adatta bene a vari compiti e domini di classificazione del testo, offrendo versatilità d'uso. Il suo design modulare supporta l'integrazione con diversi LLM, metodi di embedding e classificatori a seconda delle necessità.

Nel complesso, TNT-LLM fornisce una soluzione robusta per le organizzazioni che gestiscono grandi quantità di dati testuali non strutturati.

Implementazione di TNT-LLM

Vediamo il processo di implementazione passo dopo passo.

Installazione

Per iniziare con le implementazioni di TNT-LLM, devi installare i pacchetti necessari. Puoi farlo con il seguente comando pip:

pip install langgraph langchain langchain_openai

Inoltre, imposta le variabili d'ambiente per le chiavi API e i nomi dei modelli:

export AZURE_OPENAI_API_KEY='your_api_key_here' export AZURE_OPENAI_MODEL='your_deployment_name_here' export AZURE_OPENAI_ENDPOINT='deployment_endpoint'

Concetti di base

Nel contesto di TNT-LLM, comprendere alcuni concetti fondamentali è cruciale per sfruttarne efficacemente le capacità.

Documenti

In TNT-LLM, per documenti si intendono i dati testuali grezzi, come log di chat o articoli, che devono essere elaborati. Questi sono gli input principali della pipeline TNT-LLM. Ogni documento è strutturato tramite la classe Doc, che include campi come id, content e attributi opzionali come summary, explanation e category.

Tassonomie

Le tassonomie sono insiemi di intenti o argomenti categorizzati. Organizzano i dati in categorie significative, essenziali per compiti come il riconoscimento dell'intento e la classificazione degli argomenti. La classe TaxonomyGenerationState definisce la struttura dello stato per la generazione della tassonomia. Questa classe include documenti grezzi, indici dei minibatch e raggruppamenti di tassonomie candidate.

Costruire una semplice applicazione TNT-LLM

Comprendere gli aspetti fondamentali di TNT-LLM è cruciale per implementarne efficacemente le funzionalità. Per iniziare con TNT-LLM, il processo di generazione della tassonomia prevede diversi passaggi chiave:

Passo 0: Definire la classe di stato del grafo, caricare i dataset e GPT-4o

Per prima cosa, dobbiamo definire la classe di stato del grafo TaxonomyGenerationState, che gestisce gli aspetti a stato della generazione ed evoluzione della tassonomia durante l'esecuzione del grafo. La classe Doc definisce la struttura dei singoli documenti all'interno del grafo.

import operator
from typing import Annotated, List, TypedDict, Optional
class Doc(TypedDict):
    id: str
    content: str
    summary: Optional[str]
    explanation: Optional[str]
    category: Optional[str]
class TaxonomyGenerationState(TypedDict):
    # The raw docs; we inject summaries within them in the first step
    documents: List[Doc]
    # Indices to be concise
    minibatches: List[List[int]]
    # Candidate Taxonomies (full trajectory)
    clusters: Annotated[List[List[dict]], operator.add]

Prima di iniziare il processo di generazione della tassonomia, dobbiamo caricare i dataset contenenti dati testuali grezzi, come articoli di news o log di chat. Questi dataset fungono da input principale per TNT-LLM. Qui stiamo caricando articoli di news da Hugging Face.

import pandas as pd
from datasets import load_dataset
# Load the dataset
dataset = load_dataset("okite97/news-data")
# Access the different splits if available (e.g., train, test, validation)
train_data = dataset['train']
df = pd.DataFrame(train_data)
df = df.dropna()
df.reset_index(drop=True, inplace=True)
def run_to_doc(df: pd.DataFrame) -> Doc:
    all_data = []
    for i in range(len(df)):
        d = df.iloc[i]
        all_data.append({
            "id": i,
            "content": d['Title'] + "\\n\\n" + d['Excerpt']
        })
    
    return all_data
# Only clustering 100 documents
docs = run_to_doc(df[:100])
print(docs[0])
{'id': 0,
 'content': 'Uefa Opens Proceedings against Barcelona, Juventus and Real Madrid Over European Super League Plan\\n\\nUefa has opened disciplinary proceedings against Barcelona, Juventus and Real Madrid over their involvement in the proposed European Super League.'}

Infine, inizializziamo un modello OpenAI GPT-4o basato su Azure da usare come nostro language model (LLM) in tutti i componenti di TNT-LLM. Sebbene in pratica si possano inizializzare LLM diversi per ciascun componente, qui semplifichiamo usando un unico modello per tutti i compiti.

import os
from langchain_openai import AzureChatOpenAI
model = AzureChatOpenAI(
    openai_api_version="2023-06-01-preview"
    azure_deployment="gpt-4o-2024-05-13",
    temperature=0.0
)

Passo 1: Riassumere i documenti

Una volta caricati i dataset, ogni documento viene sottoposto a sintesi. Questo passaggio prevede l'uso di modelli linguistici per generare riassunti concisi che catturino i punti principali di ciascun documento. I riassunti sono cruciali perché distillano testi lunghi in forme più brevi e gestibili, mantenendo al contempo le informazioni essenziali.

Il codice fornito inizializza un prompt LLM per riassumere i documenti usando TNT-LLM, analizzando l'output XML per estrarre riassunti e spiegazioni. Imposta una pipeline per mappare il contenuto dei documenti, elaborarlo in batch per la generazione dei riassunti e integrare questi riassunti nella struttura dati originale dei documenti tramite passaggi di mapping e reduction.

import re
import random
from langchain import hub
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableConfig, RunnableLambda, RunnablePassthrough
summary_prompt = hub.pull("wfh/tnt-llm-summary-generation").partial(
    summary_length=20, explanation_length=30
)
def parse_summary(xml_string: str) -> dict:
    summary_pattern = r"<summary>(.*?)</summary>"
    explanation_pattern = r"<explanation>(.*?)</explanation>"
    summary_match = re.search(summary_pattern, xml_string, re.DOTALL)
    explanation_match = re.search(explanation_pattern, xml_string, re.DOTALL)
    summary = summary_match.group(1).strip() if summary_match else ""
    explanation = explanation_match.group(1).strip() if explanation_match else ""
    return {"summary": summary, "explanation": explanation}
summary_llm_chain = (
    summary_prompt | model | StrOutputParser()
).with_config(run_name="GenerateSummary")
summary_chain = summary_llm_chain | parse_summary
# Input: state
# Output: state and summaries
# Processes docs in parallel
def get_content(state: TaxonomyGenerationState):
    docs = state["documents"]
    return [{"content": doc["content"]} for doc in docs]
map_step = RunnablePassthrough.assign(
    summaries=get_content
    | RunnableLambda(func=summary_chain.batch, afunc=summary_chain.abatch)
)
def reduce_summaries(combined: dict) -> TaxonomyGenerationState:
    summaries = combined["summaries"]
    documents = combined["documents"]
    return {
        "documents": [
            {
                "id": doc["id"],
                "content": doc["content"],
                "summary": summ_info["summary"],
                "explanation": summ_info["explanation"],
            }
            for doc, summ_info in zip(documents, summaries)
        ]
    }
# This is the summary node
map_reduce_chain = map_step | reduce_summaries

Passo 2: Creare i minibatch

Dopo la sintesi, il passo successivo è organizzare i documenti riassunti in minibatch. Il minibatching aiuta a elaborare grandi volumi di dati in modo più efficiente. Suddividendo il dataset in batch più piccoli, TNT-LLM può gestire i calcoli in parallelo, ottimizzando sia il tempo che le risorse computazionali.

def get_minibatches(state: TaxonomyGenerationState, config: RunnableConfig):
    batch_size = config["configurable"].get("batch_size", 200)
    original = state["documents"]
    indices = list(range(len(original)))
    random.shuffle(indices)
    if len(indices) < batch_size:
        # Don't pad needlessly if we can't fill a single batch
        return [indices]
    num_full_batches = len(indices) // batch_size
    batches = [
        indices[i * batch_size : (i + 1) * batch_size] for i in range(num_full_batches)
    ]
    leftovers = len(indices) % batch_size
    if leftovers:
        last_batch = indices[num_full_batches * batch_size :]
        elements_to_add = batch_size - leftovers
        last_batch += random.sample(indices, elements_to_add)
        batches.append(last_batch)
    return {
        "minibatches": batches,
    }

Passo 3: Generare la tassonomia iniziale

Usando il primo minibatch di documenti riassunti, TNT-LLM avvia il processo di generazione della tassonomia. Questo passaggio prevede il clustering degli intenti o degli argomenti identificati nei documenti. Analizzando il contenuto di ciascun documento, il sistema individua temi o categorie comuni, gettando le basi iniziali della tassonomia.

Le due funzioni principali sono:

  • invoke_taxonomy_chain() - Questa funzione integra i passaggi di generazione della tassonomia usando una catena basata su LLM (chain) configurata per i compiti di TNT-LLM. Costruisce tabelle di dati in XML (data_table_xml) a partire dai riassunti dei documenti e dai precedenti cluster di tassonomia. La funzione invoca la catena LLM per aggiornare la tassonomia in base ai parametri di configurazione specificati (config), restituendo uno stato aggiornato con i nuovi cluster di tassonomia.
  • generate_taxonomy() - Questa funzione orchestra la generazione dei cluster della tassonomia. Invoca invoke_taxonomy_chain con una catena configurata per la generazione della tassonomia (generate_taxonomy_chain) usando il minibatch iniziale di documenti (state["minibatches"][0]). Restituisce lo stato della tassonomia aggiornato dopo il processo di generazione iniziale.
from typing import Dict
from langchain_core.runnables import Runnable
def parse_taxa(output_text: str) -> Dict:
    """Extract the taxonomy from the generated output."""
    cluster_pattern = r"<cluster>\\s*<id>(.*?)</id>\\s*<name>(.*?)</name>\\s*<description>(.*?)</description>\\s*</cluster>"
    cluster_matches = re.findall(cluster_pattern, output_text, re.DOTALL)
    clusters = [
        {"id": id.strip(), "name": name.strip(), "description": description.strip()}
        for id, name, description in cluster_matches
    ]
    return {"clusters": clusters}
def format_docs(docs: List[Doc]) -> str:
    xml_table = "\\n"
    for doc in docs:
        xml_table += f'{doc["summary"]}\\n'
    xml_table += ""
    return xml_table
def format_taxonomy(clusters):
    xml = "\\n"
    for label in clusters:
        xml += "  \\n"
        xml += f'    {label["id"]}\\n'
        xml += f'    {label["name"]}\\n'
        xml += f'    {label["description"]}\\n'
        xml += "  \\n"
    xml += ""
    return xml
def invoke_taxonomy_chain(
    chain: Runnable,
    state: TaxonomyGenerationState,
    config: RunnableConfig,
    mb_indices: List[int],
) -> TaxonomyGenerationState:
    configurable = config["configurable"]
    docs = state["documents"]
    minibatch = [docs[idx] for idx in mb_indices]
    data_table_xml = format_docs(minibatch)
    previous_taxonomy = state["clusters"][-1] if state["clusters"] else []
    cluster_table_xml = format_taxonomy(previous_taxonomy)
    updated_taxonomy = chain.invoke(
        {
            "data_xml": data_table_xml,
            "use_case": configurable["use_case"],
            "cluster_table_xml": cluster_table_xml,
            "suggestion_length": configurable.get("suggestion_length", 30),
            "cluster_name_length": configurable.get("cluster_name_length", 10),
            "cluster_description_length": configurable.get(
                "cluster_description_length", 30
            ),
            "explanation_length": configurable.get("explanation_length", 20),
            "max_num_clusters": configurable.get("max_num_clusters", 25),
        }
    )
    return {
        "clusters": [updated_taxonomy["clusters"]],
    }

Ora vediamo come integrare il modello linguistico nel processo di generazione della tassonomia:

# We will share an LLM for each step of the generate -> update -> review cycle
# You may want to consider using Opus or another more powerful model for this
taxonomy_generation_llm = model
## Initial generation
taxonomy_generation_prompt = hub.pull("wfh/tnt-llm-taxonomy-generation").partial(
    use_case="Generate the taxonomy that can be used to label the user intent in the conversation.",
)
taxa_gen_llm_chain = (
    taxonomy_generation_prompt | taxonomy_generation_llm | StrOutputParser()
).with_config(run_name="GenerateTaxonomy")
generate_taxonomy_chain = taxa_gen_llm_chain | parse_taxa
def generate_taxonomy(
    state: TaxonomyGenerationState, config: RunnableConfig
) -> TaxonomyGenerationState:
    return invoke_taxonomy_chain(
        generate_taxonomy_chain, state, config, state["minibatches"][0]
    )

Passo 4: Aggiornare la tassonomia

Man mano che vengono elaborati i minibatch successivi, TNT-LLM aggiorna e affina iterativamente la tassonomia. Questo processo consente alla tassonomia di evolvere e migliorare nel tempo, incorporando nuove intuizioni e categorie scoperte nei batch aggiuntivi di dati. Gli aggiornamenti assicurano che la tassonomia resti completa e rifletta l'intero dataset.

taxonomy_update_prompt = hub.pull("wfh/tnt-llm-taxonomy-update")
taxa_update_llm_chain = (
    taxonomy_update_prompt | taxonomy_generation_llm | StrOutputParser()
).with_config(run_name="UpdateTaxonomy")
update_taxonomy_chain = taxa_update_llm_chain | parse_taxa
def update_taxonomy(
    state: TaxonomyGenerationState, config: RunnableConfig
) -> TaxonomyGenerationState:
    which_mb = len(state["clusters"]) % len(state["minibatches"])
    return invoke_taxonomy_chain(
        update_taxonomy_chain, state, config, state["minibatches"][which_mb]
    )

Passo 5: Revisionare la tassonomia

Una volta elaborati tutti i minibatch e aggiornata la tassonomia, l'ultimo passaggio consiste nella revisione della tassonomia. Questa revisione è cruciale per validare l'accuratezza e la pertinenza delle etichette categorizzate. Garantisce che la tassonomia organizzi e categorizzi il dataset in modo significativo, allineandosi al caso d'uso previsto come il riconoscimento dell'intento o la classificazione degli argomenti.

taxonomy_review_prompt = hub.pull("wfh/tnt-llm-taxonomy-review")
taxa_review_llm_chain = (
    taxonomy_review_prompt | taxonomy_generation_llm | StrOutputParser()
).with_config(run_name="ReviewTaxonomy")
review_taxonomy_chain = taxa_review_llm_chain | parse_taxa
def review_taxonomy(
    state: TaxonomyGenerationState, config: RunnableConfig
) -> TaxonomyGenerationState:
    batch_size = config["configurable"].get("batch_size", 200)
    original = state["documents"]
    indices = list(range(len(original)))
    random.shuffle(indices)
    return invoke_taxonomy_chain(
        review_taxonomy_chain, state, config, indices[:batch_size]
    )

Passo 6: Orchestrare la pipeline TNT-LLM con StateGraph

Finora, i primi cinque passaggi hanno riguardato la preparazione di singoli componenti come la sintesi dei documenti, la creazione dei minibatch, la generazione, l'aggiornamento e la revisione della tassonomia. Ora siamo pronti a integrare questi componenti in un'unica pipeline TNT-LLM coerente usando un framework StateGraph.

Questo grafo orchestra l'esecuzione sequenziale dei compiti: riassumere i documenti, generare tassonomie dai minibatch, aggiornare iterativamente le tassonomie e infine rivedere e convalidare la tassonomia. I bordi condizionali assicurano che le tassonomie vengano aggiornate fino all'elaborazione di tutti i minibatch, dopodiché la tassonomia finale viene revisionata e finalizzata per l'uso nelle attività di etichettatura.

from langgraph.graph import StateGraph
graph = StateGraph(TaxonomyGenerationState)
graph.add_node("summarize", map_reduce_chain)
graph.add_node("get_minibatches", get_minibatches)
graph.add_node("generate_taxonomy", generate_taxonomy)
graph.add_node("update_taxonomy", update_taxonomy)
graph.add_node("review_taxonomy", review_taxonomy)
graph.add_edge("summarize", "get_minibatches")
graph.add_edge("get_minibatches", "generate_taxonomy")
graph.add_edge("generate_taxonomy", "update_taxonomy")
def should_review(state: TaxonomyGenerationState) -> str:
    num_minibatches = len(state["minibatches"])
    num_revisions = len(state["clusters"])
    if num_revisions < num_minibatches:
        return "update_taxonomy"
    return "review_taxonomy"
graph.add_conditional_edges(
    "update_taxonomy",
    should_review,
    # Optional (but required for the diagram to be drawn correctly below)
    {"update_taxonomy": "update_taxonomy", "review_taxonomy": "review_taxonomy"},
)
graph.set_finish_point("review_taxonomy")
graph.set_entry_point("summarize")
app = graph.compile()

Rappresentazione del grafo

Passo 7: Clustering e visualizzazione della tassonomia degli articoli di news con TNT-LLM

In quest'ultimo passaggio, utilizziamo TNT-LLM per raggruppare articoli di news. La tassonomia risultante viene mostrata in formato Markdown, con i nomi e le descrizioni di ciascun cluster.

use_case = (
    "Generate the taxonomy that can be used to label the news article that would benefit the user."
)
stream = app.stream(
    {"documents": docs},
    {
        "configurable": {
            "use_case": use_case,
            # Optional:
            "batch_size": 10,
            "suggestion_length": 30,
            "cluster_name_length": 10,
            "cluster_description_length": 30,
            "explanation_length": 20,
            "max_num_clusters": 25,
        },
        "max_concurrency": 2,
        "recursion_limit": 50,
    },
)
for step in stream:
    node, state = next(iter(step.items()))
    print(node, str(state)[:20] + " ...")
from IPython.display import Markdown
def format_taxonomy_md(clusters):
    md = "## Final Taxonomy\\n\\n"
    md += "| ID | Name | Description |\\n"
    md += "|----|------|-------------|\\n"
    # Iterate over each inner list of dictionaries
    for cluster_list in clusters:
        for label in cluster_list:
            id = label["id"]
            name = label["name"].replace("|", "\\\\|")  # Escape any pipe characters within the content
            description = label["description"].replace("|", "\\\\|")  # Escape any pipe characters
            md += f"| {id} | {name} | {description} |\\n"
    return md
markdown_table = format_taxonomy_md(step['review_taxonomy']['clusters'])
Markdown(markdown_table)

Tabella che mostra l'output di TNT-LLM

Ed ecco fatto! Seguendo questi passaggi strutturati, TNT-LLM ha automatizzato la creazione di tassonomie dettagliate e interpretabili da dati testuali grezzi. Questo approccio semplifica ciò che tradizionalmente richiede notevole impegno umano e competenze di dominio, rendendolo uno strumento potente per attività di text mining e classificazione su larga scala.

Conclusione

TNT-LLM rappresenta un passo avanti significativo nel text mining e nella classificazione, offrendo una soluzione efficiente per analizzare grandi volumi di dati non strutturati. Automatizzando la generazione di tassonomie e la classificazione scalabile, riduce il tempo e lo sforzo necessari per ricavare insight dal testo.

Man mano che TNT-LLM evolve, ha il potenziale per trasformare il modo in cui le organizzazioni analizzano enormi quantità di dati testuali. Questo strumento abilita decisioni data‑driven in diversi settori, rendendo gli insight dai dati testuali più accessibili e incisivi.

Se vuoi saperne di più sullo sviluppo di applicazioni LLM, ti consiglio questo corso su Sviluppare applicazioni LLM con LangChain.

Ryan Ong

Autore

Ryan Ong

Ryan è un lead data scientist specializzato nello sviluppo di applicazioni di IA con LLM. È dottorando in Elaborazione del Linguaggio Naturale e Knowledge Graph all’Imperial College London, dove ha anche conseguito la laurea magistrale in Informatica. Oltre alla data science, scrive una newsletter settimanale su Substack, The Limitless Playbook, in cui condivide un’idea pratica dei migliori pensatori al mondo e, di tanto in tanto, scrive sui concetti fondamentali dell’IA.

Argomenti

Correlato