Usare un database vettoriale per cercare nei discorsi della Casa Bianca
Questo articolo è stato originariamente pubblicato su The New Stack.
La stagione elettorale per la campagna presidenziale degli Stati Uniti si avvicina. È un buon momento per guardare indietro ad alcuni dei discorsi pronunciati dall’amministrazione Biden durante i suoi primi due anni in carica. Non sarebbe fantastico poter cercare tra alcune trascrizioni dei discorsi per saperne di più sui messaggi della Casa Bianca riguardo a determinati argomenti finora?
Supponiamo di voler cercare il contenuto di un discorso. Come potremmo farlo? Potremmo usare la ricerca semantica. La ricerca semantica è uno degli argomenti più caldi nell’intelligenza artificiale (AI) in questo momento. È diventata più importante con l’aumento della popolarità delle applicazioni di elaborazione del linguaggio naturale (NLP) come ChatGPT. Invece di interrogare ripetutamente GPT, cosa costosa sia economicamente sia ecologicamente, possiamo usare un database vettoriale per memorizzare nella cache i risultati (come con GPTCache).
In questo tutorial, avvieremo un database vettoriale localmente così da poter cercare per contenuto nei discorsi di Biden dal 2021 al 2022. Il dataset che usiamo è il dataset “The White House (Speeches and Remarks) 12/10/2022”, che abbiamo trovato su Kaggle e reso disponibile per il download tramite Google Drive per questo esempio. Un notebook guida di questo tutorial è disponibile su GitHub.
Prima di immergerci nel codice, assicurati di scaricare i prerequisiti. Abbiamo bisogno di quattro librerie: PyMilvus, Milvus, Sentence-Transformers e gdown. Puoi ottenere le librerie necessarie da PyPi eseguendo: pip3 install pymilvus==2.2.5 sentence-transformers gdown milvus.
Preparare il dataset dei discorsi della Casa Bianca
Come per quasi ogni progetto AI/ML basato su dataset del mondo reale, dobbiamo prima preparare i dati. Usiamo gdown per scaricare il dataset e zipfile per estrarlo in una cartella locale. Dopo aver eseguito il codice qui sotto, ci aspettiamo di vedere un file intitolato “The white house speeches.csv” in una cartella intitolata “white_house_2021_2022”.
import gdown
url = 'https://drive.google.com/uc?id=10_sVL0UmEog7mczLedK5s1pnlDOz3Ukf'
output = './white_house_2021_2022.zip'
gdown.download(url, output)
import zipfile
with zipfile.ZipFile("./white_house_2021_2022.zip","r") as zip_ref:
zip_ref.extractall("./white_house_2021_2022")
Usiamo pandas per caricare e ispezionare i dati CSV.
import pandas as pd
df = pd.read_csv("./white_house_2021_2022/The white house speeches.csv")
df.head()
Quando diamo un’occhiata alla head dei dati, cosa noti? La prima cosa che noto è che i dati hanno quattro colonne: una colonna titolo, data e ora, luogo e discorso. La seconda è che ci sono valori nulli. I valori nulli non sono sempre un problema, ma lo sono per i nostri dati.
Pulizia del dataset
I discorsi senza alcun contenuto (valori nulli nella colonna “Speech”) sono completamente inutili per noi. Eliminiamo i nostri valori nulli e riesaminiamo i dati.
df = df.dropna()
df
Ora vediamo che in realtà c’è un secondo problema che non era immediatamente evidente guardando solo la head dei dati. Se guardi l’ultima voce, vedrai che questa voce è solo un orario. “12:18 P.M. EST” è difficilmente un discorso. Non ha senso salvare questa voce. Non possiamo ricavare alcun valore dal salvataggio di un embedding vettoriale.
Eliminiamo tutti i discorsi che sono più corti di una certa lunghezza. Per questo esempio, ho scelto 50, ma puoi scegliere qualunque valore abbia senso per te. Ho scelto 50 esplorando molti numeri diversi. Se cerchi trascrizioni di discorsi tra 20 e 50 caratteri, vedrai che molte sono luoghi o orari con qualche frase casuale inserita.
cleaned_df = df.loc[(df["Speech"].str.len() > 50)]cleaned_df
Dopo esserci occupati dei discorsi brevi e privi di sostanza, guardiamo di nuovo i nostri dati per vedere un ulteriore problema. Molti dei discorsi contengono valori \r\n - nuove righe e ritorni a capo. Questi caratteri vengono usati per la formattazione, ma non contengono alcun valore semantico. Il passo successivo nel nostro processo di pulizia dei dati è eliminarli.
cleaned_df["Speech"] = cleaned_df["Speech"].str.replace("\r\n", "")
cleaned_df
Ora l’aspetto è decisamente migliore. Il passaggio finale è convertire la colonna “Date_time” in un formato migliore da archiviare nel nostro database vettoriale e da confrontare con altri datetime. Usiamo la libreria datetime per convertire semplicemente questo formato datetime in un formato universale YYYY-MM-DD.
import datetime
# Convert the 'date' column to datetime objects
cleaned_df["Date_time"] = pd.to_datetime(cleaned_df["Date_time"], format="%B %d, %Y")
cleaned_df
Configurare un database vettoriale per la ricerca semantica
I nostri dati ora sono puliti e pronti per essere utilizzati. Il passo successivo è avviare un database vettoriale per cercare effettivamente i discorsi in base al loro contenuto. Per questo esempio, usiamo Milvus Lite, una versione lite di Milvus che puoi eseguire senza Docker, Kubernetes o dover gestire alcun tipo di file YAML.
La prima cosa che facciamo è definire alcune delle nostre costanti. Abbiamo bisogno di un nome di collection (per il database vettoriale), del numero di dimensioni nel nostro vettore embedded, di una dimensione del batch e di un numero che definisca quanti risultati vogliamo ottenere quando effettuiamo una ricerca. Questo esempio usa il sentence transformer MiniLM L6 v2, che produce vettori di embedding a 384 dimensioni.
COLLECTION_NAME = "white_house_2021_2022"
DIMENSION = 384
BATCH_SIZE = 128
TOPK = 3
Usiamo il default_server di Milvus. Poi, usiamo l’SDK PyMilvus per connetterci al nostro server Milvus locale. Se nel nostro database vettoriale esiste una collection con lo stesso nome della collection che abbiamo definito in precedenza, eliminiamo quella collection per assicurarci di partire da zero.
from milvus import default_server
from pymilvus import connections, utility
default_server.start()
connections.connect(host="127.0.0.1", port=default_server.listen_port)
if utility.has_collection(COLLECTION_NAME):
utility.drop_collection(COLLECTION_NAME)
Come nella maggior parte degli altri database, abbiamo bisogno di uno schema per caricare i dati nel database vettoriale Milvus. Per prima cosa, definiamo i campi dati che vogliamo che ogni oggetto abbia. Meno male che abbiamo esaminato i dati in precedenza. Usiamo cinque campi dati, le quattro colonne che avevamo prima e una colonna ID. Solo che questa volta usiamo l’embedding vettoriale del discorso invece del testo effettivo.
from pymilvus import FieldSchema, CollectionSchema, DataType, Collection
# object should be inserted in the format of (title, date, location, speech embedding)
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=500),
FieldSchema(name="date", dtype=DataType.VARCHAR, max_length=100),
FieldSchema(name="location", dtype=DataType.VARCHAR, max_length=200),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=DIMENSION)
]
schema = CollectionSchema(fields=fields)
collection = Collection(name=COLLECTION_NAME, schema=schema)
L'ultima cosa che dobbiamo definire prima di essere pronti a caricare i dati nel database vettoriale è l'indice. Esistono molti indici e pattern vettoriali, ma per questo esempio usiamo l'indice IVF_FLAT con 128 cluster. Le applicazioni più grandi di solito utilizzano più di 128 cluster, ma in ogni caso abbiamo solo poco più di 600 voci. Per la nostra distanza, misuriamo usando la norma L2. Una volta definiti i parametri del nostro indice, creiamo l'indice nella nostra collection e lo carichiamo per l'uso.
index_params = {
"index_type": "IVF_FLAT",
"metric_type": "L2",
"params": {"nlist": 128},
}
collection.create_index(field_name="embedding", index_params=index_params)
collection.load()
Ottenere embedding vettoriali dai discorsi
Gran parte di ciò che abbiamo trattato finora si applica quando si lavora con quasi qualsiasi database. Abbiamo ripulito alcuni dati, abbiamo avviato un'istanza di database e definito uno schema per il nostro database. Oltre a definire un indice, un'altra cosa che dobbiamo fare in particolare per i database vettoriali è ottenere gli embedding.
Per prima cosa, otteniamo il modello sentence transformer MiniLM L6 v2 come menzionato sopra. Poi creiamo una funzione che esegue una trasformazione sui dati e li inserisce nella collection. Questa funzione prende un batch di dati, ottiene gli embedding per le trascrizioni dei discorsi, crea un oggetto da inserire e lo inserisce nella collection.
Per contestualizzare, questa funzione esegue un aggiornamento batch. In questo esempio, stiamo inserendo in batch 128 voci alla volta. L'unica trasformazione dei dati che facciamo nel nostro insert è trasformare il testo del discorso in un embedding.
from sentence_transformers import SentenceTransformer
transformer = SentenceTransformer('all-MiniLM-L6-v2')
# expects a list of (title, date, location, speech)
def embed_insert(data: list):
embeddings = transformer.encode(data[3])
ins = [
data[0],
data[1],
data[2],
[x for x in embeddings]
]
collection.insert(ins)
Popolare il tuo database vettoriale
Con una funzione che crea embedding in batch e inserimenti completa, siamo pronti a popolare il database. Per questo esempio, iteriamo su ogni riga del nostro dataframe e aggiungiamo a una lista di liste che usiamo per raggruppare i nostri dati in batch. Una volta raggiunta la dimensione del batch, chiamiamo la funzione embed_insert e reimpostiamo il nostro batch.
Se rimangono dati nel batch di dati dopo aver terminato il ciclo, creiamo gli embedding e inseriamo i dati rimanenti. Infine, per completare il popolamento del nostro database vettoriale, chiamiamo flush per assicurarci che il database sia aggiornato e indicizzato.
data_batch = [[], [], [], []]
for index, row in cleaned_df.iterrows():
data_batch[0].append(row["Title"])
data_batch[1].append(str(row["Date_time"]))
data_batch[2].append(row["Location"])
data_batch[3].append(row["Speech"])
if len(data_batch[0]) % BATCH_SIZE == 0:
embed_insert(data_batch)
data_batch = [[], [], [], []]
# Embed and insert the remainder
if len(data_batch[0]) != 0:
embed_insert(data_batch)
# Call a flush to index any unsealed segments.
collection.flush()
Ricerca semantica dei discorsi della Casa Bianca basata su descrizioni
Diciamo che sono interessato a trovare un discorso in cui il presidente ha parlato dell'impatto delle energie rinnovabili al National Renewable Energy Lab (NREL) e un discorso in cui parlano la vicepresidente e il primo ministro del Canada. Posso trovare i titoli dei discorsi più simili tenuti dai membri della Casa Bianca nel 2021-2022 usando il database vettoriale che abbiamo appena creato.
Possiamo cercare nel nostro database vettoriale i discorsi più simili alle nostre descrizioni. Poi, tutto ciò che dobbiamo fare è convertire la descrizione in un embedding vettoriale usando lo stesso modello che abbiamo usato per ottenere gli embedding dei discorsi e quindi cercare nel database vettoriale.
Una volta convertite le descrizioni in un embedding vettoriale, utilizziamo la funzione search sulla nostra collection. Passiamo gli embedding come dati di ricerca, passiamo il campo che stiamo cercando, aggiungiamo alcuni parametri su come effettuare la ricerca, un limite per il numero di risultati e il campo che vogliamo restituire. In questo esempio, i parametri di ricerca che dobbiamo passare sono il tipo di metrica, che deve essere dello stesso tipo utilizzato durante la creazione dell’indice (norma L2), e il numero di cluster in cui vogliamo cercare (impostando nprobe a 10).
import time
search_terms = ["The President speaks about the impact of renewable energy at the National Renewable Energy Lab.", "The Vice President and the Prime Minister of Canada both speak."]
# Search the database based on input text
def embed_search(data):
embeds = transformer.encode(data)
return [x for x in embeds]
search_data = embed_search(search_terms)
start = time.time()
res = collection.search(
data=search_data, # Embeded search value
anns_field="embedding", # Search across embeddings
param={"metric_type": "L2",
"params": {"nprobe": 10}},
limit = TOPK, # Limit to top_k results per search
output_fields=["title"] # Include title field in result
)
end = time.time()
for hits_i, hits in enumerate(res):
print("Title:", search_terms[hits_i])
print("Search Time:", end-start)
print("Results:")
for hit in hits:
print( hit.entity.get("title"), "----", hit.distance)
print()
Quando cerchiamo le frasi in questo esempio, ci aspettiamo di vedere un output come nell’immagine qui sotto. È stata una ricerca riuscita perché i titoli sono quelli che ci aspettiamo di vedere. La prima descrizione restituisce il titolo di un discorso tenuto dal Presidente Biden presso NREL, e la seconda descrizione restituisce un titolo che riflette un discorso tenuto dalla Vicepresidente Harris e dal Primo Ministro Trudeau.
Riepilogo
In questo tutorial, abbiamo imparato come utilizzare un database vettoriale per effettuare una ricerca semantica tra i discorsi tenuti dall’amministrazione Biden prima delle elezioni di metà mandato del 2022. La ricerca semantica ci permette di prendere un breve testo e cercare testi semanticamente simili, non solo testi sintatticamente simili. Questo ci consente di cercare una descrizione generale di un discorso invece di cercare un discorso in base a frasi o citazioni specifiche. Per la maggior parte di noi, questo rende molto più facile trovare discorsi che potrebbero interessarci.
Continua a leggere

Zilliz Cloud Update: Tiered Storage, Business Critical Plan, Cross-Region Backup, and Pricing Changes
This release offers a rebuilt tiered storage with lower costs, a new Business Critical plan for enhanced security, and pricing updates, among other features.

Democratizing AI: Making Vector Search Powerful and Affordable
Zilliz democratizes AI vector search with Milvus 2.6 and Zilliz Cloud for powerful, affordable scalability, cutting costs in infrastructure, operations, and development.

What is the K-Nearest Neighbors (KNN) Algorithm in Machine Learning?
KNN is a supervised machine learning technique and algorithm for classification and regression. This post is the ultimate guide to KNN.



