Diferència entre revisions de la pàgina «RAG: embeddings en català»
(Es crea la pàgina amb «Si volem fer embeddings de textos en català, podem utilitzar models multilingües com '''sentence-transformers'''. Per exemple, '''paraphrase-multilingual-MiniLM-L12-...».) |
m |
||
| Línia 1: | Línia 1: | ||
| + | __TOC__ | ||
| + | |||
| + | =Primera prova: paraphrase-multilingual-MiniLM-L12-v2= | ||
| + | |||
| + | Aquests embeddings, que no són els millors per al català, els he fet funcionar amb el portàtil. | ||
| + | |||
Si volem fer embeddings de textos en català, podem utilitzar models multilingües com '''sentence-transformers'''. Per exemple, '''paraphrase-multilingual-MiniLM-L12-v2''' funciona amb català i és força lleuger. | Si volem fer embeddings de textos en català, podem utilitzar models multilingües com '''sentence-transformers'''. Per exemple, '''paraphrase-multilingual-MiniLM-L12-v2''' funciona amb català i és força lleuger. | ||
| Línia 67: | Línia 73: | ||
Per a un projecte de RAG, cerca semàntica o una base de dades vectorial, també podem utilitzar models més moderns i específicament multilingües, i comparar quins funcionen millor per català. | Per a un projecte de RAG, cerca semàntica o una base de dades vectorial, també podem utilitzar models més moderns i específicament multilingües, i comparar quins funcionen millor per català. | ||
Però si amb aquest que m'he instal·lat ja obtenim bons resultats, doncs ja estaria. | Però si amb aquest que m'he instal·lat ja obtenim bons resultats, doncs ja estaria. | ||
| + | |||
| + | =Embeddings per al català= | ||
| + | Anem a fer els embeddings i una primera prova d'inserció dels embeddings al QDrant. | ||
| + | * https://claude.ai/chat/7b094ba1-6352-4d2a-b9c6-a1bafb953c2a | ||
| + | |||
| + | prompt: | ||
| + | <pre> | ||
| + | Estic fent el TFM del màster de Tecnologies de Llenguatge de la UNED. En el TFM faré un RAG sobre l'obra de Josep Pla. | ||
| + | Ja sé fer els chunks dels documents, i ara crec que aquests chunks els he de convertir a embeddings (català) i que en el QDrant hauré d'inserir aquests embeddings, i no pas els chunks en format text. És així la cosa? | ||
| + | |||
| + | R: ... | ||
| + | </pre> | ||
| + | |||
| + | Cada element que insereixes a Qdrant és un punt amb tres parts: | ||
| + | |||
| + | * id: un identificador únic (enter o UUID). | ||
| + | * vector: l’embedding del chunk. És el que Qdrant fa servir per buscar per similitud. | ||
| + | * payload: un JSON lliure on guardes el text original del chunk i les metadades (obra, capítol, any, pàgina, etc.). | ||
| + | |||
| + | El text és imprescindible. En un RAG, quan arriba una pregunta, la converteixes en embedding, Qdrant et retorna els punts més semblants, i el que passes al LLM com a context és el text d’aquests chunks, no pas els vectors. Si només guardes vectors, no podràs recuperar el contingut. Les metadades també et serviran per filtrar (per exemple, “només El quadern gris”) i per citar les fonts a la resposta. | ||
| + | |||
| + | Un exemple mínim amb python: | ||
| + | <pre> | ||
| + | from qdrant_client import QdrantClient | ||
| + | from qdrant_client.models import VectorParams, Distance, PointStruct | ||
| + | from sentence_transformers import SentenceTransformer | ||
| + | |||
| + | model = SentenceTransformer("BAAI/bge-m3") # multilingüe, funciona bé en català | ||
| + | client = QdrantClient("localhost", port=6333) | ||
| + | |||
| + | client.create_collection( | ||
| + | collection_name="josep_pla", | ||
| + | vectors_config=VectorParams( | ||
| + | size=model.get_sentence_embedding_dimension(), # 1024 per a bge-m3 | ||
| + | distance=Distance.COSINE, | ||
| + | ), | ||
| + | ) | ||
| + | |||
| + | chunks = [ | ||
| + | {"text": "...", "obra": "El quadern gris", "data": "1918-03-12"}, | ||
| + | # ... | ||
| + | ] | ||
| + | |||
| + | vectors = model.encode([c["text"] for c in chunks], normalize_embeddings=True) | ||
| + | |||
| + | client.upsert( | ||
| + | collection_name="josep_pla", | ||
| + | points=[ | ||
| + | PointStruct(id=i, vector=v.tolist(), payload=c) | ||
| + | for i, (v, c) in enumerate(zip(vectors, chunks)) | ||
| + | ], | ||
| + | ) | ||
| + | |||
| + | # Consulta | ||
| + | q = model.encode("Què pensa Pla de Palafrugell?", normalize_embeddings=True) | ||
| + | hits = client.query_points("josep_pla", query=q.tolist(), limit=5).points | ||
| + | context = [h.payload["text"] for h in hits] | ||
| + | </pre> | ||
| + | |||
| + | Alguns aspectes a tenir en compte per al TFM: | ||
| + | |||
| + | * El mateix model per als chunks i per a les preguntes. Si canvies de model, has de tornar a indexar-ho tot. La mida del vector de la col·lecció ha de coincidir amb la dimensió del model. | ||
| + | * Prefixos d’alguns models. La família multilingual-e5 requereix prefixos ("query: " per a la pregunta i "passage: " per als chunks). BGE-M3 no en necessita. | ||
| + | * Models per al català. BGE-M3 i intfloat/multilingual-e5-large són bones opcions multilingües. També val la pena mirar els models de sentence embeddings del Projecte AINA (BSC), que estan entrenats específicament amb català. Comparar-ne dos o tres amb un petit conjunt de preguntes de prova et donaria una secció d’avaluació interessant. | ||
| + | * Cerca híbrida (opcional). Qdrant admet vectors densos i dispersos (sparse) en el mateix punt. Combinar l’embedding semàntic amb un de tipus BM25 ajuda molt amb noms propis i topònims, que en Pla n’hi ha a dojo. | ||
| + | |||
| + | prompt: | ||
| + | <pre> | ||
| + | Per a embeddings de català, quin model em recomanes: | ||
| + | paraphrase-multilingual-MiniLM-L12-v2 o bé BAAI/bge-m3. O algun altre? | ||
| + | |||
| + | R: ... | ||
| + | </pre> | ||
| + | Per què és millor bge-m3: | ||
| + | |||
| + | * Està entrenat específicament per a recuperació multilingüe i funciona bé en català. | ||
| + | * Accepta fins a 8.192 tokens, així que no et limita la mida dels chunks. | ||
| + | * Genera vectors densos i dispersos alhora. Pots fer cerca híbrida a Qdrant amb un sol model, cosa que va molt bé per a noms propis i topònims (Calella, Llofriu, Cadaqués…). | ||
| + | * Inconvenient: és més pesat (uns 570M paràmetres, 1024 dimensions). Amb GPU va de pressa. Amb CPU, indexar tota l’obra de Pla trigarà hores, però només ho fas un cop. | ||
| + | |||
| + | prompt: | ||
| + | <pre> | ||
| + | Vull executar el codi mínim que m'has proposat en el meu servidor amb una NVIDIA RTX 5060 Ti 16GB. Suposo que és millor fer-ho en un venv. | ||
| + | M'expliques pas a pas com fer-ho? | ||
| + | |||
| + | R: ... | ||
| + | </pre> | ||
| + | |||
| + | 3. Crea i activa el venv | ||
| + | <pre> | ||
| + | mkdir -p ~/embeddings && cd ~/embeddings | ||
| + | python3 -m venv .venv | ||
| + | source .venv/bin/activate | ||
| + | pip install --upgrade pip | ||
| + | </pre> | ||
| + | |||
| + | 4. Instal·la PyTorch amb CUDA 12.8 | ||
| + | <pre> | ||
| + | pip install torch --index-url https://download.pytorch.org/whl/cu128 | ||
| + | </pre> | ||
| + | Comprova que detecta la GPU: | ||
| + | <pre> | ||
| + | python -c "import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0), torch.cuda.get_arch_list())" | ||
| + | </pre> | ||
| + | |||
| + | 5. Instal·la la resta de llibreries | ||
| + | |||
| + | <pre> | ||
| + | pip install sentence-transformers qdrant-client | ||
| + | pip freeze > requirements.txt # útil per a la reproduïbilitat del TFM | ||
| + | </pre> | ||
| + | |||
| + | 6. Engega Qdrant amb Docker (això ja ho tinc fet) | ||
| + | |||
| + | de moment no faré servir el Docker amb opció GPU perquè presumiblement la GPU la necessitaré per altres tasques. | ||
| + | |||
| + | (canviar aquesta comanda al meu storage...) | ||
| + | |||
| + | <pre> | ||
| + | docker run -d --name qdrant \ | ||
| + | -p 6333:6333 -p 6334:6334 \ | ||
| + | -v ~/tfm-pla/qdrant_data:/qdrant/storage \ | ||
| + | qdrant/qdrant | ||
| + | </pre> | ||
| + | |||
| + | El -v fa que les dades persisteixin encara que aturis el contenidor. Pots comprovar que funciona amb curl http://localhost:6333 o obrint http://IP-DEL-SERVIDOR:6333/dashboard al navegador. | ||
| + | |||
| + | script '''prova_rag.py''': | ||
| + | |||
| + | És el codi d’abans amb tres millores: fa servir la GPU en mitja precisió (fp16, més ràpid i amb menys memòria), no peta si la col·lecció ja existeix, i codifica per lots. | ||
| + | |||
| + | <pre> | ||
| + | import torch | ||
| + | from qdrant_client import QdrantClient | ||
| + | from qdrant_client.models import VectorParams, Distance, PointStruct | ||
| + | from sentence_transformers import SentenceTransformer | ||
| + | |||
| + | COLLECTION = "josep_pla" | ||
| + | |||
| + | # Model a la GPU en fp16 | ||
| + | model = SentenceTransformer( | ||
| + | "BAAI/bge-m3", | ||
| + | device="cuda", | ||
| + | model_kwargs={"torch_dtype": torch.float16}, | ||
| + | ) | ||
| + | dim = model.get_sentence_embedding_dimension() | ||
| + | |||
| + | client = QdrantClient("localhost", port=6333) | ||
| + | |||
| + | if not client.collection_exists(COLLECTION): | ||
| + | client.create_collection( | ||
| + | collection_name=COLLECTION, | ||
| + | vectors_config=VectorParams(size=dim, distance=Distance.COSINE), | ||
| + | ) | ||
| + | |||
| + | # Substitueix-ho pels teus chunks reals | ||
| + | chunks = [ | ||
| + | {"text": "Palafrugell és un poble de la costa empordanesa...", "obra": "El quadern gris"}, | ||
| + | {"text": "El vent de tramuntana bufa amb força tot l'hivern...", "obra": "Les hores"}, | ||
| + | ] | ||
| + | |||
| + | vectors = model.encode( | ||
| + | [c["text"] for c in chunks], | ||
| + | batch_size=32, | ||
| + | normalize_embeddings=True, | ||
| + | show_progress_bar=True, | ||
| + | ) | ||
| + | |||
| + | client.upsert( | ||
| + | collection_name=COLLECTION, | ||
| + | points=[ | ||
| + | PointStruct(id=i, vector=v.tolist(), payload=c) | ||
| + | for i, (v, c) in enumerate(zip(vectors, chunks)) | ||
| + | ], | ||
| + | ) | ||
| + | |||
| + | # Consulta de prova | ||
| + | pregunta = "Què diu Pla de Palafrugell?" | ||
| + | q = model.encode(pregunta, normalize_embeddings=True) | ||
| + | hits = client.query_points(COLLECTION, query=q.tolist(), limit=5).points | ||
| + | |||
| + | for h in hits: | ||
| + | print(f"{h.score:.3f} [{h.payload['obra']}] {h.payload['text'][:80]}") | ||
| + | </pre> | ||
{{Autor}}, octubre 2026 | {{Autor}}, octubre 2026 | ||
Revisió del 07:15, 16 oct 2026
Primera prova: paraphrase-multilingual-MiniLM-L12-v2
Aquests embeddings, que no són els millors per al català, els he fet funcionar amb el portàtil.
Si volem fer embeddings de textos en català, podem utilitzar models multilingües com sentence-transformers. Per exemple, paraphrase-multilingual-MiniLM-L12-v2 funciona amb català i és força lleuger.
Primer instal·lem la llibreria:
pip install sentence-transformers
I ja podem fer els embeddings d'unes frases d'exemple:
from sentence_transformers import SentenceTransformer
# Model multilingüe compatible amb català
model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
texts = [
"Barcelona és la capital de Catalunya.",
"El català és una llengua romànica.",
"Avui fa un dia molt assolellat."
]
# Generar els embeddings
embeddings = model.encode(texts)
print("Nombre de textos:", len(embeddings))
print("Dimensions de cada embedding:", embeddings.shape[1])
print(embeddings[0])
Resposta:
Nombre de textos: 3 Dimensions de cada embedding: 384 [ 0.45152336 -0.17739134 0.33369076 0.32314456 0.01625137 -0.10211392 0.02397913 -0.00964131 -0.02689971 0.1543721 -0.31138584 -0.53306985 -0.3249653 0.06214958 -0.01033912 -0.36389765 0.1459078 -0.44203445 ...
I això ens serveix per comparar textos, segons la seva similaritat:
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
texts = [
"M'agrada molt anar a la muntanya.",
"Disfruto molt fent excursions per la muntanya.",
"Avui he menjat una paella."
]
embeddings = model.encode(texts)
similarity = cosine_similarity(embeddings)
print(similarity)
Resposta:
[[1.0000001 0.9182879 0.11477765] [0.9182879 1.0000001 0.11056139] [0.11477765 0.11056139 1.0000006 ]]
Podem veure com els resultats són els esperats. Les dues primeres frases tenen similaritat (parlen de muntanya), i no s'assemblen amb la tercera frase. És usual utilitzar el cosinus per calcular la similaritat.
Per a un projecte de RAG, cerca semàntica o una base de dades vectorial, també podem utilitzar models més moderns i específicament multilingües, i comparar quins funcionen millor per català. Però si amb aquest que m'he instal·lat ja obtenim bons resultats, doncs ja estaria.
Embeddings per al català
Anem a fer els embeddings i una primera prova d'inserció dels embeddings al QDrant.
prompt:
Estic fent el TFM del màster de Tecnologies de Llenguatge de la UNED. En el TFM faré un RAG sobre l'obra de Josep Pla. Ja sé fer els chunks dels documents, i ara crec que aquests chunks els he de convertir a embeddings (català) i que en el QDrant hauré d'inserir aquests embeddings, i no pas els chunks en format text. És així la cosa? R: ...
Cada element que insereixes a Qdrant és un punt amb tres parts:
- id: un identificador únic (enter o UUID).
- vector: l’embedding del chunk. És el que Qdrant fa servir per buscar per similitud.
- payload: un JSON lliure on guardes el text original del chunk i les metadades (obra, capítol, any, pàgina, etc.).
El text és imprescindible. En un RAG, quan arriba una pregunta, la converteixes en embedding, Qdrant et retorna els punts més semblants, i el que passes al LLM com a context és el text d’aquests chunks, no pas els vectors. Si només guardes vectors, no podràs recuperar el contingut. Les metadades també et serviran per filtrar (per exemple, “només El quadern gris”) i per citar les fonts a la resposta.
Un exemple mínim amb python:
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-m3") # multilingüe, funciona bé en català
client = QdrantClient("localhost", port=6333)
client.create_collection(
collection_name="josep_pla",
vectors_config=VectorParams(
size=model.get_sentence_embedding_dimension(), # 1024 per a bge-m3
distance=Distance.COSINE,
),
)
chunks = [
{"text": "...", "obra": "El quadern gris", "data": "1918-03-12"},
# ...
]
vectors = model.encode([c["text"] for c in chunks], normalize_embeddings=True)
client.upsert(
collection_name="josep_pla",
points=[
PointStruct(id=i, vector=v.tolist(), payload=c)
for i, (v, c) in enumerate(zip(vectors, chunks))
],
)
# Consulta
q = model.encode("Què pensa Pla de Palafrugell?", normalize_embeddings=True)
hits = client.query_points("josep_pla", query=q.tolist(), limit=5).points
context = [h.payload["text"] for h in hits]
Alguns aspectes a tenir en compte per al TFM:
- El mateix model per als chunks i per a les preguntes. Si canvies de model, has de tornar a indexar-ho tot. La mida del vector de la col·lecció ha de coincidir amb la dimensió del model.
- Prefixos d’alguns models. La família multilingual-e5 requereix prefixos ("query: " per a la pregunta i "passage: " per als chunks). BGE-M3 no en necessita.
- Models per al català. BGE-M3 i intfloat/multilingual-e5-large són bones opcions multilingües. També val la pena mirar els models de sentence embeddings del Projecte AINA (BSC), que estan entrenats específicament amb català. Comparar-ne dos o tres amb un petit conjunt de preguntes de prova et donaria una secció d’avaluació interessant.
- Cerca híbrida (opcional). Qdrant admet vectors densos i dispersos (sparse) en el mateix punt. Combinar l’embedding semàntic amb un de tipus BM25 ajuda molt amb noms propis i topònims, que en Pla n’hi ha a dojo.
prompt:
Per a embeddings de català, quin model em recomanes: paraphrase-multilingual-MiniLM-L12-v2 o bé BAAI/bge-m3. O algun altre? R: ...
Per què és millor bge-m3:
- Està entrenat específicament per a recuperació multilingüe i funciona bé en català.
- Accepta fins a 8.192 tokens, així que no et limita la mida dels chunks.
- Genera vectors densos i dispersos alhora. Pots fer cerca híbrida a Qdrant amb un sol model, cosa que va molt bé per a noms propis i topònims (Calella, Llofriu, Cadaqués…).
- Inconvenient: és més pesat (uns 570M paràmetres, 1024 dimensions). Amb GPU va de pressa. Amb CPU, indexar tota l’obra de Pla trigarà hores, però només ho fas un cop.
prompt:
Vull executar el codi mínim que m'has proposat en el meu servidor amb una NVIDIA RTX 5060 Ti 16GB. Suposo que és millor fer-ho en un venv. M'expliques pas a pas com fer-ho? R: ...
3. Crea i activa el venv
mkdir -p ~/embeddings && cd ~/embeddings python3 -m venv .venv source .venv/bin/activate pip install --upgrade pip
4. Instal·la PyTorch amb CUDA 12.8
pip install torch --index-url https://download.pytorch.org/whl/cu128
Comprova que detecta la GPU:
python -c "import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0), torch.cuda.get_arch_list())"
5. Instal·la la resta de llibreries
pip install sentence-transformers qdrant-client pip freeze > requirements.txt # útil per a la reproduïbilitat del TFM
6. Engega Qdrant amb Docker (això ja ho tinc fet)
de moment no faré servir el Docker amb opció GPU perquè presumiblement la GPU la necessitaré per altres tasques.
(canviar aquesta comanda al meu storage...)
docker run -d --name qdrant \ -p 6333:6333 -p 6334:6334 \ -v ~/tfm-pla/qdrant_data:/qdrant/storage \ qdrant/qdrant
El -v fa que les dades persisteixin encara que aturis el contenidor. Pots comprovar que funciona amb curl http://localhost:6333 o obrint http://IP-DEL-SERVIDOR:6333/dashboard al navegador.
script prova_rag.py:
És el codi d’abans amb tres millores: fa servir la GPU en mitja precisió (fp16, més ràpid i amb menys memòria), no peta si la col·lecció ja existeix, i codifica per lots.
import torch
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct
from sentence_transformers import SentenceTransformer
COLLECTION = "josep_pla"
# Model a la GPU en fp16
model = SentenceTransformer(
"BAAI/bge-m3",
device="cuda",
model_kwargs={"torch_dtype": torch.float16},
)
dim = model.get_sentence_embedding_dimension()
client = QdrantClient("localhost", port=6333)
if not client.collection_exists(COLLECTION):
client.create_collection(
collection_name=COLLECTION,
vectors_config=VectorParams(size=dim, distance=Distance.COSINE),
)
# Substitueix-ho pels teus chunks reals
chunks = [
{"text": "Palafrugell és un poble de la costa empordanesa...", "obra": "El quadern gris"},
{"text": "El vent de tramuntana bufa amb força tot l'hivern...", "obra": "Les hores"},
]
vectors = model.encode(
[c["text"] for c in chunks],
batch_size=32,
normalize_embeddings=True,
show_progress_bar=True,
)
client.upsert(
collection_name=COLLECTION,
points=[
PointStruct(id=i, vector=v.tolist(), payload=c)
for i, (v, c) in enumerate(zip(vectors, chunks))
],
)
# Consulta de prova
pregunta = "Què diu Pla de Palafrugell?"
q = model.encode(pregunta, normalize_embeddings=True)
hits = client.query_points(COLLECTION, query=q.tolist(), limit=5).points
for h in hits:
print(f"{h.score:.3f} [{h.payload['obra']}] {h.payload['text'][:80]}")
creat per Joan Quintana Compte, octubre 2026