Costruisci un chatbot per la raccomandazione (di ricette) utilizzando RAG e la ricerca ibrida (Parte I) |  di Sebastian Bahr |  Marzo 2024

 | Intelligenza-Artificiale

Per questo progetto utilizzeremo le ricette di Ricette di pubblico dominio. Tutte le ricette vengono archiviate come file di markdown in questo GitHub deposito. Per questo tutorial, ho già eseguito un po' di pulizia dei dati e creato funzionalità dall'input di testo non elaborato. Se desideri eseguire tu stesso la parte di pulizia dei dati, il codice è disponibile sul mio GitHub deposito.

Il set di dati è composto dalle seguenti colonne:

  • titolo: il titolo della ricetta
  • data: la data in cui è stata aggiunta la ricetta
  • tag: un elenco di tag che descrivono il pasto
  • introduzione: un'introduzione alla ricetta, il contenuto varia fortemente da un record all'altro
  • ingredienti: tutti gli ingredienti necessari. Tieni presente che ho rimosso la quantità poiché non è necessaria per creare incorporamenti e il contrario potrebbe portare a raccomandazioni indesiderate.
  • direzione: tutti i passaggi necessari da eseguire per cucinare il pasto
  • tipo_ricetta: indicatore se la ricetta è vegana, vegetariana o normale
  • produzione: contiene il titolo, ingredienti, E direzione della ricetta e verrà successivamente fornito al modello di chat come input.

Diamo uno sguardo alla distribuzione del tipo_ricetta caratteristica. Vediamo che la maggior parte (60%) delle ricette include pesce o carne e non sono adatte ai vegetariani. Circa il 35% è vegetariano e solo il 5% è vegano. Questa funzione verrà utilizzata come filtro rigido per recuperare le ricette corrispondenti dal database dei vettori.

import re
import json
import spacy
import torch
import openai
import vertexai
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from tqdm.auto import tqdm
from transformers import AutoModelForMaskedLM, AutoTokenizer
from pinecone import Pinecone, ServerlessSpec
from vertexai.language_models import TextEmbeddingModel
from utils_google import authenticate
credentials, PROJECT_ID, service_account, pinecone_API_KEY = authenticate()
from utils_openai import authenticate
OPENAI_API_KEY = authenticate()

openai_client = openai.OpenAI(api_key=OPENAI_API_KEY)

REGION = "us-central1"
vertexai.init(project = PROJECT_ID,
location = REGION,
credentials = credentials)

pc = Pinecone(api_key=pinecone_API_KEY)

# download spacy model
#!python -m spacy download en_core_web_sm

recipes = pd.read_json("recipes_v2.json")
recipes.head()
plt.bar(recipes.recipe_type.unique(), recipes.recipe_type.value_counts(normalize=True).values)
plt.show()
Distribuzione delle tipologie di ricette

La ricerca ibrida utilizza una combinazione di vettori sparsi e densi e un fattore di ponderazione alfa, che consente di regolare l'importanza del vettore denso nel processo di recupero. Di seguito creeremo vettori densi basati su titolo, tagE introduzione e vettori sparsi sul ingredienti. Adattandosi alfa potremo quindi determinare in seguito quanta “attenzione” dovrebbe essere prestata agli ingredienti menzionati dall'utente nella sua query.

Prima di creare gli incorporamenti è necessario creare una nuova funzionalità che contenga le informazioni combinate di titoloIL tage il introduzione.

recipes("dense_feature") = recipes.title + "; " + recipes.tags.apply(lambda x: str(x).strip("()").replace("'", "")) + "; " + recipes.introduction
recipes("dense_feature").head()

Infine, prima di approfondire la generazione degli incorporamenti, daremo un'occhiata alla colonna di output. La seconda parte del tutorial riguarderà la creazione di un chatbot utilizzando OpenAI in grado di rispondere alle domande degli utenti utilizzando la conoscenza del nostro database di ricette. Pertanto, dopo aver trovato le ricette che corrispondono meglio alla query dell'utente, il modello di chat necessita di alcune informazioni su cui costruire la sua risposta. È lì che produzione viene utilizzato, poiché contiene tutte le informazioni necessarie per una risposta adeguata

# example output
{'title': 'Creamy Mashed Potatoes',
'ingredients': 'The quantities here are for about four adult portions. If you are planning on eating this as a side dish, it might be more like 6-8 portions. * 1kg potatoes * 200ml milk* * 200ml mayonnaise* * ~100g cheese * Garlic powder * 12-16 strips of bacon * Butter * 3-4 green onions * Black pepper * Salt *You can play with the proportions depending on how creamy or dry you want the mashed potatoes to be.',
'direction': '1. Peel and cut the potatoes into medium sized pieces. 2. Put the potatoes in a pot with some water so that it covers the potatoes and boil them for about 20-30 minutes, or until the potatoes are soft. 3. About ten minutes before removing the potatoes from the boiling water, cut the bacon into little pieces and fry it. 4. Warm up the milk and mayonnaise. 5. Shred the cheese. 6. When the potatoes are done, remove all water from the pot, add the warm milk and mayonnaise mix, add some butter, and mash with a potato masher or a blender. 7. Add some salt, black pepper and garlic powder to taste and continue mashing the mix. 8. Once the mix is somewhat homogeneous and the potatoes are properly mashed, add the shredded cheese and fried bacon and mix a little. 9. Serve and top with chopped green onions.'}

Inoltre, a ciascuna ricetta deve essere aggiunto un identificatore univoco, che consenta di recuperare i record delle ricette candidate consigliate e i relativi produzione.

recipes("ID") = range(len(recipes))

Genera incorporamenti sparsi

Il passaggio successivo prevede la creazione di incorporamenti sparsi per tutte le 360 ​​osservazioni. Per calcolare questi incorporamenti, viene utilizzato un metodo più sofisticato rispetto all'approccio TF-IDF o BM25 frequentemente utilizzato. Invece lo SPLADE Spculo lexical UNND Eviene applicato il modello di espansione. È possibile trovare una spiegazione dettagliata di SPLADE Qui. Gli incorporamenti densi hanno la stessa forma per ogni input di testo, indipendentemente dal numero di token nell'input. Al contrario, gli incorporamenti sparsi contengono un peso per ogni token univoco nell'input. Il dizionario seguente rappresenta un vettore sparso, dove l'ID del token è la chiave e il peso assegnato è il valore.

model_id = "naver/splade-cocondenser-ensembledistil"

tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForMaskedLM.from_pretrained(model_id)

def to_sparse_vector(text, tokenizer, model):
tokens = tokenizer(text, return_tensors='pt')
output = model(**tokens)
vec = torch.max(
torch.log(1 + torch.relu(output.logits)) * tokens.attention_mask.unsqueeze(-1), dim=1
)(0).squeeze()

cols = vec.nonzero().squeeze().cpu().tolist()
weights = vec(cols).cpu().tolist()
sparse_dict = dict(zip(cols, weights))
return sparse_dict

sparse_vectors = ()

for i in tqdm(range(len(recipes))):
sparse_vectors.append(to_sparse_vector(recipes.iloc(i)("ingredients"), tokenizer, model))

recipes("sparse_vectors") = sparse_vectors

sparse incorporazioni della prima ricetta

Generazione di incorporamenti densi

A questo punto del tutorial, sorgeranno dei costi se si utilizza un modello di incorporamento del testo di VertexAI (Google) o OpenAI. Tuttavia, se utilizzi lo stesso set di dati, i costi saranno al massimo di $ 5. Il costo può variare se utilizzi un dataset con più record o testi più lunghi, poiché ti verrà addebitato tramite token. Se non vuoi sostenere alcun costo ma vuoi comunque seguire il tutorial, in particolare la seconda parte, puoi scaricare il pandas DataFrame ricette_con_vettori.pkl con dati di incorporamento pregenerati dal mio GitHub deposito.

Puoi scegliere di utilizzare VertexAI o OpenAI per creare gli incorporamenti. OpenAI ha il vantaggio di essere facile da configurare con una chiave API, mentre VertexAI richiede l'accesso a Google Console, la creazione di un progetto e l'aggiunta dell'API VertexAI al tuo progetto. Inoltre, il modello OpenAI consente di specificare il numero di dimensioni per il vettore denso. Tuttavia, entrambi creano incorporamenti densi all'avanguardia.

Utilizzo dell'API VertexAI

# running this code will create costs !!!
model = TextEmbeddingModel.from_pretrained("textembedding-gecko@003")

def to_dense_vector(text, model):
dense_vectors = model.get_embeddings((text))
return (dense_vector.values for dense_vector in dense_vectors)(0)

dense_vectors = ()

for i in tqdm(range(len(recipes))):
dense_vectors.append(to_dense_vector(recipes.iloc(i)("dense_feature"), model))

recipes("dense_vectors") = dense_vectors

Utilizzando l'API OpenAI

# running this code will create costs !!!

# Create dense embeddings using OpenAIs text embedding model with 768 dimensions
model = "text-embedding-3-small"

def to_dense_vector_openAI(text, client, model, dimensions):
dense_vectors = client.embeddings.create(model=model, dimensions=dimensions, input=(text))
return (dense_vector.values for dense_vector in dense_vectors)(0)

dense_vectors = ()

for i in tqdm(range(len(recipes))):
dense_vectors.append(to_dense_vector_openAI(recipes.iloc(i)("dense_feature"), openai_client, model, 768))

recipes("dense_vectors") = dense_vectors

Carica i dati nel database vettoriale

Dopo aver generato gli incorporamenti sparsi e densi, disponiamo di tutti i dati necessari per caricarli in un database vettoriale. In questo tutorial, verrà utilizzato Pinecone poiché consente di eseguire una ricerca ibrida utilizzando vettori sparsi e densi e offre uno schema di prezzi serverless con $ 100 di crediti gratuiti. Per eseguire una ricerca ibrida in un secondo momento, la metrica di somiglianza deve essere impostata sul prodotto scalare. Se eseguissimo solo una ricerca densa anziché ibrida, saremmo in grado di selezionare una di queste metriche di somiglianza: prodotto scalare, coseno e distanza euclidea. È possibile trovare ulteriori informazioni sulle metriche di somiglianza e su come calcolano la somiglianza tra due vettori Qui.

# load pandas DataFrame with pre-generated embeddings if you
# didn't generate them in the last step
recipes = pd.read_pickle("recipes_with_vectors.pkl")

# if you need to delte an existing index
pc.delete_index("index-name")

# create a new index
pc.create_index(
name="recipe-project",
dimension=768, # adjust if needed
metric="dotproduct",
spec=ServerlessSpec(
cloud="aws",
region="us-west-2"
)
)

pc.describe_index("recipe-project")

Congratulazioni per aver creato il tuo primo indice Pinecone! Ora è il momento di caricare i dati incorporati nel database vettoriale. Se il modello di incorporamento che hai utilizzato crea vettori con un numero diverso di dimensioni, assicurati di regolare il file dimensione discussione.

Ora è il momento di caricare i dati nell'indice Pinecone appena creato.

# upsert to pinecone in batches
def sparse_to_dict(data):
dict_ = {"indices": list(data.keys()),
"values": list(data.values())}
return dict_

batch_size = 100
index = pc.Index("recipe-project")

for i in tqdm(range(0, len(recipes), batch_size)):
i_end = min(i + batch_size, len(recipes))
meta_batch = recipes.iloc(i: i_end)(("ID", "recipe_type"))
meta_dict = meta_batch.to_dict(orient="records")

sparse_batch = recipes.iloc(i: i_end)("sparse_vectors").apply(lambda x: sparse_to_dict(x))
dense_batch = recipes.iloc(i: i_end)("dense_vectors")

upserts = ()

ids = (str(x) for x in range(i, i_end))
for id_, meta, sparse_, dense_ in zip(ids, meta_dict, sparse_batch, dense_batch):
upserts.append({
"id": id_,
"sparse_values": sparse_,
"values": dense_,
"metadata": meta
})

index.upsert(upserts)

index.describe_index_stats()

Se sei curioso di sapere come appaiono i dati caricati, accedi a Pinecone, seleziona l'indice appena creato e dai un'occhiata ai suoi elementi. Per ora non dobbiamo prestare attenzione al punteggio, poiché viene generato di default e indica la corrispondenza con un vettore generato casualmente da Pinecone. Tuttavia, in seguito calcoleremo la somiglianza della query utente incorporata con tutti gli elementi nel database vettoriale e recupereremo il file K articoli più simili. Inoltre, ogni articolo contiene un ID articolo generato da Pinecone e i metadati, che consistono nella ricetta ID e il suo tipo_ricetta. Gli inglobamenti densi vengono immagazzinati Valori e le sparse incorporazioni Valori sparsi.

I primi tre elementi dell'indice (Immagine dell'autore)

Possiamo recuperare le informazioni dall'alto utilizzando Pinecone Python SDK. Diamo un'occhiata alle informazioni memorizzate del primo elemento con l'ID elemento indice 50.

index.fetch(ids=("50"))

Come nel dashboard Pinecone, otteniamo l'ID dell'elemento, i suoi metadati, i valori sparsi e i valori densi, che vengono archiviati nell'elenco nella parte inferiore dell'output troncato.

Fonte: towardsdatascience.com

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *