Similaritate semantică și înglobare de propoziții: compararea textelor
Cât de asemănătoare sunt două propoziții? Nu în sensul lexical (aceleași cuvinte), ci în sensul sens semantic (același sens). „Câinele urmărește pisica” și „Felina vine urmărit de canin” sunt aproape identice din punct de vedere semantic, dar foarte diferite din punct de vedere lexical. Răspunsul la această întrebare este provocarea Similaritate semantică.
Aplicațiile sunt peste tot: motoare de căutare semantică, sisteme de recomandare, deduplicarea conținutului, răspunsul la întrebări, RAG (Retrieval-Augmented Generation), potrivirea chatbot și Întrebări frecvente. În acest articol vom construi sisteme de similaritate semantică de la zero: de la asemănarea cosinusului la încorporarea de propoziții cu Sentence-BERT, până la căutare rapidă vectorială cu FAISS.
Acesta este al nouălea articol din serie NLP modern: de la BERT la LLM. Acest subiect se conectează direct cu serialul AI Engineering/RAG unde înglobările semantice sunt inima recuperării dense.
Ce vei învăța
- Asemănarea cosinusului și produsul punctual: formule și când să le folosiți
- Limitele BERT pentru asemănarea semantică și de ce este nevoie de Sentence-BERT
- Sentence-BERT (SBERT): Arhitectura siameză și antrenamentul pentru pierderea tripletului
- Modele de transformatoare de propoziții pe HuggingFace: pe care să o alegeți
- Căutare semantică pe corpusuri mari cu FAISS
- Încorporare de propoziții pentru italiană
- Benchmarking: STS-B, SICK și metrici de evaluare
- Cross-encoder vs bi-encoder: compromis calitate/viteză
- Reglarea fină a unui transformator de propoziție pe domeniul dvs
- Implementarea completă a unui sistem de potrivire a întrebărilor frecvente
- Conductă gata de producție, cu memorare în cache și optimizare
1. Problema asemănării semantice
Să luăm în considerare aceste trei grupuri de propoziții și provocările lor:
Exemple de similitudine semantică
- Similaritate mare: „Banca a crescut ratele” / „Ratele dobânzilor sunt majorate de către instituția bancară”
- Asemănare scăzută: „Banca a crescut ratele” / „Pisica doarme pe canapea”
- Înșelător (aceleași cuvinte, înțeles diferit): „Biroul școlii” / „Contatura de pește de la piață”
- Interlingvistică: „Câinele aleargă repede” (aceeași semantică, limbi diferite)
Măsuri tradiționale, cum ar fi Asemănarea lui Jaccard sau cel BM25 se bazează pe suprapunerea lexicală și eșuează complet cu sinonime și parafraze. Chiar și simplu TF-IDF nu surprinde sensul. Soluția constă în înglobări semantice: Reprezentări vectoriale dense unde proximitatea geometric reflectă apropierea semantică.
1.1 Similitudinea cosinusului: metrica fundamentală
La putina asemanare măsoară unghiul dintre doi vectori în spațiu a înglobărilor. Variază de la -1 (opus) la 1 (identic), cu 0 pentru vectorii ortogonali. Formula matematică este:
cos(A, B) = (A · B) / (||A|| · ||B||)
Când vectorii sunt normalizați la norma unitară, asemănarea cosinusului coincide cu produsul punct, ceea ce face calculul mult mai eficient pe hardware-ul GPU.
import numpy as np
import torch
from torch.nn import functional as F
def cosine_similarity(vec1, vec2):
"""Cosine similarity tra due vettori numpy."""
dot_product = np.dot(vec1, vec2)
norm1 = np.linalg.norm(vec1)
norm2 = np.linalg.norm(vec2)
return dot_product / (norm1 * norm2)
# Versione PyTorch (batch-friendly)
def cosine_similarity_batch(emb1, emb2):
"""Cosine similarity tra batch di embedding (normalizzato)."""
# Normalizza a norma unitaria
emb1_norm = F.normalize(emb1, p=2, dim=1)
emb2_norm = F.normalize(emb2, p=2, dim=1)
return (emb1_norm * emb2_norm).sum(dim=1)
# Esempio con vettori semplici
vec_a = np.array([1.0, 0.5, 0.3, 0.8])
vec_b = np.array([0.9, 0.4, 0.4, 0.7]) # simile ad a
vec_c = np.array([-0.2, 0.8, -0.5, 0.1]) # diverso da a
print(f"sim(a, b) = {cosine_similarity(vec_a, vec_b):.4f}") # alta
print(f"sim(a, c) = {cosine_similarity(vec_a, vec_c):.4f}") # bassa
# Matrice di similarità per corpus di frasi
def similarity_matrix(embeddings):
"""Matrice di similarità N x N per un set di embedding."""
# Normalizza
norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
normalized = embeddings / norms
# Prodotto matriciale per tutte le coppie
return normalized @ normalized.T
# Output: matrice (N, N) dove [i,j] = sim(frase_i, frase_j)
1.2 Alte valori ale distanței
Comparație între valorile de similitudine/distanță
| Metric | Formula | Gamă | Caz de utilizare |
|---|---|---|---|
| Asemănarea cosinusului | cos(A, B) | [-1, 1] | Asemănarea semantică standard |
| Distanța euclidiană | ||A - B|| | [0, +inf) | Clustering, k-NN |
| Produs punct | A · B | (-inf, +inf) | Cu vectori normalizați = cosinus |
| Distanța Manhattan | suma(|A-B|) | [0, +inf) | Robustitate la valori aberante |
| Corelația Pearson | cov(A,B)/sigma | [-1, 1] | Evaluare pe benchmark STS |
2. De ce standardul BERT nu funcționează pentru similaritate
Intuitiv, am putea folosi BERT pentru a extrage încorporarea de propoziții și a le compara. Dar cercetările lui Reimers & Gurevych (2019) au arătat că acest lucru abordare e surprinzător de ineficient.
Problema principală este că BERT este pre-antrenat cu Masked Language Modeling (MLM) și
Predicția următoarei propoziții (NSP). Jetonul [CLS] codifică informații
util pentru clasificarea perechilor de propoziții (NSP), dar nu este optimizat pentru
produce înglobări care reflectă asemănarea semantică atunci când sunt comparate
cu putina asemanare.
În plus, punerea în comun a tuturor jetoanelor produce un spațiu de încorporare anizotrop: direcțiile nu sunt distribuite uniform, iar grupuri de propoziții diferite din punct de vedere semantic se suprapun.
from transformers import BertModel, BertTokenizer
import torch
import numpy as np
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')
def bert_mean_pooling(text):
"""Embedding di frase con mean pooling su BERT."""
inputs = tokenizer(text, return_tensors='pt',
truncation=True, max_length=128, padding=True)
with torch.no_grad():
outputs = model(**inputs)
# Mean pooling (esclude padding)
mask = inputs['attention_mask'].unsqueeze(-1)
embeddings = (outputs.last_hidden_state * mask).sum(1) / mask.sum(1)
return embeddings[0].numpy()
# Test: frasi semanticamente simili vs diverse
sent1 = "The weather is lovely today."
sent2 = "It's so beautiful today outside." # simile
sent3 = "My dog bit the mailman." # diversa
emb1 = bert_mean_pooling(sent1)
emb2 = bert_mean_pooling(sent2)
emb3 = bert_mean_pooling(sent3)
sim_1_2 = np.dot(emb1, emb2) / (np.linalg.norm(emb1) * np.linalg.norm(emb2))
sim_1_3 = np.dot(emb1, emb3) / (np.linalg.norm(emb1) * np.linalg.norm(emb3))
print(f"sim(sent1, sent2) = {sim_1_2:.4f}") # ~0.93 - ok
print(f"sim(sent1, sent3) = {sim_1_3:.4f}") # ~0.87 - troppo alto!
# Problema: BERT tende a produrre embedding simili per tutte le frasi
# perchè il token [CLS] e trainato su NSP, non su similarità semantica
# La soluzione e Sentence-BERT
Performanță BERT pe STS-B (benchmark)
În sarcina STS-B (Semantic Textual Similarity Benchmark), BERT cu gruparea medie doar ajunge Pearson r = 0,54, mult sub abordări supravegheat ca SBERT (0,87). Chiar și simbolul [CLS] singur ajunge la 0,20. Pentru asemănarea semantică, SBERT este alegerea corectă.
3. Sentence-BERT (SBERT): Soluția
Sentință-BERT (Reimers & Gurevych, EMNLP 2019) rezolvă problema cu o arhitectură siamez: două cazuri de pondere a BERT, procesează două propoziții separat, iar funcția de pierdere forțează reprezentările similar din punct de vedere semantic cu a fi aproape în spațiul vectorial.
3.1 Arhitectura siameză
Ideea cheie este că cele două „rețele” au exact aceleași ponderi. Acestea nu sunt două modele separate, ci același model numit de două ori. Pierderea se calculează pe perechea de ieșiri:
- Obiectiv de regresie: MSE între similitudinea cosinus prezisă și scorul uman (pentru STS)
- Obiectiv de clasificare: Entropie încrucișată pe [u, v, |u-v|] (pentru NLI)
- Pierdere triplet: pierderea marjei pe ancora/pozitiv/negativ (pentru mineritul parafrazat)
from sentence_transformers import SentenceTransformer, util
import torch
# Carica un modello sentence-transformers
# Modello multilingua (include italiano!)
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
# Modello ottimizzato per inglese (più accurato)
# model = SentenceTransformer('all-MiniLM-L6-v2')
# Encoding di frasi (batch-optimized)
sentences = [
"The weather is lovely today.",
"It's so beautiful today outside.",
"He drove to the stadium.",
"La giornata e bellissima oggi.", # italiano
"Il tempo e meraviglioso questa mattina.", # italiano simile
]
# Encode tutto in una volta (molto più efficiente del loop)
embeddings = model.encode(sentences, batch_size=32, show_progress_bar=False)
print(f"Embedding shape: {embeddings.shape}") # (5, 384)
# Calcola similarità
cos_scores = util.cos_sim(embeddings, embeddings)
print("\nMatrice di similarità:")
for i in range(len(sentences)):
for j in range(i+1, len(sentences)):
score = cos_scores[i][j].item()
if score > 0.6: # mostra solo coppie simili
print(f" {i+1} vs {j+1}: {score:.4f}")
print(f" '{sentences[i][:50]}'")
print(f" '{sentences[j][:50]}'")
# Pairwise similarity per coppie specifiche
sim = util.cos_sim(embeddings[0], embeddings[1]).item()
print(f"\nsim(EN1, EN2) = {sim:.4f}") # ~0.85 (frasi simili)
sim_cross = util.cos_sim(embeddings[0], embeddings[3]).item()
print(f"sim(EN1, IT1) = {sim_cross:.4f}") # ~0.75 (cross-lingual!)
4. Modele de transformatoare de propoziții: Pe care să alegeți
Principalele modele de transformatoare de propoziții (2024-2025)
| Model | Limbi | Dim. | Viteză | STS-B Pearson |
|---|---|---|---|---|
| toate-MiniLM-L6-v2 | EN | 384 | Foarte rapid | 0,834 |
| all-mpnet-base-v2 | EN | 768 | Medie | 0,869 |
| parafraza-multilingv-MiniLM-L12-v2 | Peste 50 de limbi | 384 | Rapid | 0,821 |
| parafraza-multilingv-mpnet-base-v2 | Peste 50 de limbi | 768 | Medie | 0,853 |
| intfloat/multilingual-e5-large | Peste 100 de limbi | 1024 | Lent | 0,892 |
| text-embedding-3-small (OpenAI) | Multilingv | 1536 | Doar API | ~0,90 |
4.1 Alegerea modelului: Ghid practic
Alegerea depinde de trei factori principali: limbaj, viteza și calitatea necesară.
from sentence_transformers import SentenceTransformer
import time
import numpy as np
def benchmark_model(model_name, sentences, n_runs=3):
"""Benchmark velocità e qualità di un modello sentence-transformer."""
model = SentenceTransformer(model_name)
# Warmup
model.encode(sentences[:2])
# Misura velocità
times = []
for _ in range(n_runs):
start = time.time()
embs = model.encode(sentences)
times.append(time.time() - start)
avg_time = np.mean(times)
dim = embs.shape[1]
print(f"Model: {model_name}")
print(f" Embedding dim: {dim}")
print(f" Avg encoding time ({len(sentences)} sentences): {avg_time*1000:.1f}ms")
print(f" Throughput: {len(sentences)/avg_time:.0f} sentences/sec")
sentences_test = [
"Il sole splende oggi a Milano.",
"Oggi e una bella giornata soleggiata.",
"Roma e la capitale dell'Italia.",
"La Juventus ha vinto il campionato.",
"L'intelligenza artificiale sta cambiando il mondo.",
] * 20 # 100 frasi
# Benchmark modelli multilingua
for model_name in [
'paraphrase-multilingual-MiniLM-L12-v2',
'paraphrase-multilingual-mpnet-base-v2',
'intfloat/multilingual-e5-small',
]:
benchmark_model(model_name, sentences_test)
print()
5. Căutare semantică cu FAISS
Pentru corpus mari (milioane de documente), căutare cu forță brută (calculați asemănarea cu toate documentele) și prea lent. FAISS (Facebook AI Similarity Search) permite căutarea aproximativă a celui mai apropiat vecin în timp subliniar cu diferite tipuri de indici.
5.1 Tipuri de index FAISS
Indici FAISS: compromis viteză/precizie
| Index | Tip | Caz de utilizare | Amintiri (%) | Viteză |
|---|---|---|---|---|
| IndexFlatL2 | Corect | < 100.000 documente | 100% | Lent |
| IndexFlatIP | Exact (lucruri mici) | < 100.000 documente | 100% | Lent |
| IndexIVFFlat | Aproximativ | 100K - 10M | ~95% | Rapid |
| IndexHNSW | Aproximativ | 1M+ | ~99% | Foarte rapid |
| IndexIVFPQ | Comprimat | 10 M+, RAM limitată | ~85% | Foarte rapid |
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
import time
model = SentenceTransformer('all-MiniLM-L6-v2')
# Corpus di esempio: articoli Wikipedia
corpus = [
"The Eiffel Tower is a wrought-iron lattice tower on the Champ de Mars in Paris.",
"Apple Inc. is an American multinational technology company founded by Steve Jobs.",
"Python is a high-level, general-purpose programming language.",
"The Mediterranean diet is based on traditional foods from countries bordering the sea.",
"Quantum computing uses quantum-mechanical phenomena such as superposition.",
"The Amazon River is the largest river by discharge volume in the world.",
"Artificial neural networks are computing systems inspired by biological neural networks.",
"The Sistine Chapel ceiling was painted by Michelangelo between 1508 and 1512.",
"Machine learning is a subset of artificial intelligence focused on algorithms.",
"The Colosseum is an oval amphitheatre in the centre of Rome, Italy.",
]
# Encode il corpus (offline, una volta sola)
print("Encoding corpus...")
start = time.time()
corpus_embeddings = model.encode(corpus, convert_to_numpy=True, show_progress_bar=False)
print(f"Encoded {len(corpus)} docs in {time.time()-start:.2f}s")
print(f"Embeddings shape: {corpus_embeddings.shape}") # (10, 384)
# Costruisci indice FAISS
dim = corpus_embeddings.shape[1] # 384
# IndexFlatIP: esatta, cosine similarity su vettori normalizzati
index_ip = faiss.IndexFlatIP(dim)
# Normalizza per usare cosine similarity (dot product su vettori unit-norm)
faiss.normalize_L2(corpus_embeddings)
index_ip.add(corpus_embeddings)
print(f"Index size: {index_ip.ntotal} vettori")
# IndexHNSW: approssimato ma molto veloce, buona per produzione
# M = numero di connessioni per nodo (16-64 in produzione)
index_hnsw = faiss.IndexHNSWFlat(dim, 32, faiss.METRIC_INNER_PRODUCT)
index_hnsw.hnsw.efConstruction = 200 # più alto = migliore recall in build
index_hnsw.hnsw.efSearch = 128 # più alto = migliore recall in search
# Ricerca semantica
def semantic_search(query, index, corpus, model, k=3):
"""Ricerca semantica: restituisce i k documenti più simili alla query."""
query_emb = model.encode([query], convert_to_numpy=True)
faiss.normalize_L2(query_emb)
start = time.time()
distances, indices = index.search(query_emb, k)
search_time = (time.time() - start) * 1000
print(f"\nQuery: '{query}'")
print(f"Search time: {search_time:.2f}ms")
for rank, (dist, idx) in enumerate(zip(distances[0], indices[0]), 1):
print(f" {rank}. [{dist:.4f}] {corpus[idx][:80]}")
return [(corpus[i], float(d)) for i, d in zip(indices[0], distances[0])]
# Test
semantic_search("ancient Roman architecture", index_ip, corpus, model)
semantic_search("programming language features", index_ip, corpus, model)
semantic_search("painting and art in Italy", index_ip, corpus, model)
5.2 Persistența și încărcarea indexului
import faiss
import numpy as np
import json
import os
def build_and_save_index(corpus, model, index_path="faiss_index.bin",
corpus_path="corpus.json"):
"""Costruisce e salva un indice FAISS su disco."""
# Encode
embeddings = model.encode(corpus, convert_to_numpy=True, show_progress_bar=True)
faiss.normalize_L2(embeddings)
dim = embeddings.shape[1]
index = faiss.IndexFlatIP(dim)
index.add(embeddings)
# Salva indice FAISS
faiss.write_index(index, index_path)
# Salva corpus (per recuperare testi)
with open(corpus_path, 'w', encoding='utf-8') as f:
json.dump(corpus, f, ensure_ascii=False, indent=2)
print(f"Index saved: {index.ntotal} vettori -> {index_path}")
return index
def load_index(index_path="faiss_index.bin", corpus_path="corpus.json"):
"""Carica indice FAISS e corpus da disco."""
if not os.path.exists(index_path):
raise FileNotFoundError(f"Index not found: {index_path}")
index = faiss.read_index(index_path)
with open(corpus_path, 'r', encoding='utf-8') as f:
corpus = json.load(f)
print(f"Index loaded: {index.ntotal} vettori")
return index, corpus
# Utilizzo
# Prima volta: costruisce e salva
# index = build_and_save_index(my_corpus, model)
# Riavvii successivi: carica direttamente (molto più veloce)
# index, corpus = load_index()
6. Întrebări frecvente potrivire: caz de utilizare complet
O aplicație practică a similarității semantice: potrivirea automată a întrebărilor utilizator cu întrebări frecvente existente. Acest model este baza multor chatbot și sisteme asistență pentru clienți.
from sentence_transformers import SentenceTransformer, util
import torch
import json
class FAQMatcher:
"""Sistema di FAQ matching semantico con caching e persistenza."""
def __init__(self, model_name='paraphrase-multilingual-MiniLM-L12-v2',
threshold=0.7):
self.model = SentenceTransformer(model_name)
self.threshold = threshold
self.faqs = []
self.faq_embeddings = None
def load_faqs(self, faqs: list):
"""
faqs: lista di dizionari con 'question', 'answer', 'category'
"""
self.faqs = faqs
questions = [faq['question'] for faq in faqs]
print(f"Encoding {len(questions)} FAQ...")
self.faq_embeddings = self.model.encode(
questions,
convert_to_tensor=True,
show_progress_bar=False
)
print("FAQ pronte per la ricerca!")
def match(self, user_query: str, top_k: int = 3) -> list:
"""Trova le FAQ più simili alla domanda utente."""
if self.faq_embeddings is None:
raise ValueError("Carica prima le FAQ con load_faqs()")
query_emb = self.model.encode(user_query, convert_to_tensor=True)
scores = util.cos_sim(query_emb, self.faq_embeddings)[0]
top_k_indices = torch.topk(scores, k=min(top_k, len(self.faqs))).indices
results = []
for idx in top_k_indices:
score = scores[idx].item()
if score >= self.threshold:
results.append({
'question': self.faqs[idx]['question'],
'answer': self.faqs[idx]['answer'],
'category': self.faqs[idx].get('category', 'N/A'),
'score': round(score, 4)
})
return results
def respond(self, user_query: str) -> str:
"""Risposta automatica alla domanda utente."""
matches = self.match(user_query, top_k=1)
if not matches:
return f"Mi dispiace, non ho trovato una risposta per '{user_query}'. Contatta il supporto."
best = matches[0]
return f"[{best['category']}] {best['answer']} (Confidenza: {best['score']:.2f})"
# Esempio di utilizzo
faqs_ecommerce = [
{
"question": "Come posso restituire un prodotto?",
"answer": "Puoi restituire il prodotto entro 30 giorni dall'acquisto contattando il supporto.",
"category": "Resi"
},
{
"question": "Quanto tempo impiega la spedizione?",
"answer": "La consegna standard impiega 3-5 giorni lavorativi, l'express 24 ore.",
"category": "Spedizioni"
},
{
"question": "Come posso pagare?",
"answer": "Accettiamo carte di credito, PayPal, bonifico bancario e contrassegno.",
"category": "Pagamenti"
},
{
"question": "Il prodotto e in garanzia?",
"answer": "Tutti i prodotti hanno 2 anni di garanzia legale del consumatore.",
"category": "Garanzia"
},
{
"question": "Posso tracciare il mio ordine?",
"answer": "Si, riceverai un'email con il numero di tracking dopo la spedizione.",
"category": "Ordini"
},
]
matcher = FAQMatcher()
matcher.load_faqs(faqs_ecommerce)
test_queries = [
"Voglio rimandare indietro la merce",
"Quando arriva il pacco?",
"Accettate il bonifico?",
"Ho bisogno del codice di tracciamento",
"L'articolo si e rotto, cosa faccio?",
]
print("\n=== FAQ Matching ===")
for query in test_queries:
response = matcher.respond(query)
print(f"\nDomanda: {query}")
print(f"Risposta: {response}")
7. Cross-encoder vs Bi-encoder
Există două abordări ale similitudinii semantice care oferă compromisuri diferite calitate/viteza. Înțelegerea lor este esențială pentru alegerea arhitecturii potrivite.
Bi-encoder vs Cross-encoder
| astept | Bi-encoder (SBERT) | Cross encoder |
|---|---|---|
| Arhitectură | Două BERT-uri separate, produc înglobări | Un BERT care procesează perechea |
| Viteză | Foarte rapid (încorporare înainte de calcul) | Lent (procesează fiecare pereche) |
| Scalabilitate | Milioane de documente | Doar câteva sute de cupluri |
| calitate | Bun (~0,87 Pearson pe STS-B) | Excelent (~0,92 Pearson) |
| Caz de utilizare | Recuperare, căutare semantică | Reclasificarea rezultatelor |
| Complexitatea O(n) | O(1) pentru interogări (înglobări precalculate) | O(n) pentru fiecare interogare |
from sentence_transformers import SentenceTransformer, CrossEncoder, util
# Bi-encoder per il retrieval iniziale (veloce)
bi_encoder = SentenceTransformer('all-MiniLM-L6-v2')
# Cross-encoder per il reranking (accurato)
cross_encoder = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
# Pipeline a due stadi (best of both worlds)
def retrieval_and_rerank(query, corpus, corpus_embeddings, top_k=100, final_k=5):
"""
Stage 1: Bi-encoder retrieval (veloce, ritorna top 100)
Stage 2: Cross-encoder reranking (accurato, sui top 100)
"""
# Stage 1: Bi-encoder retrieval
query_emb = bi_encoder.encode(query, convert_to_tensor=True)
hits = util.semantic_search(query_emb, corpus_embeddings, top_k=top_k)[0]
# Stage 2: Cross-encoder reranking
cross_inp = [[query, corpus[hit['corpus_id']]] for hit in hits]
cross_scores = cross_encoder.predict(cross_inp)
# Combina e riordina
for hit, score in zip(hits, cross_scores):
hit['cross_score'] = score
hits = sorted(hits, key=lambda x: x['cross_score'], reverse=True)[:final_k]
print(f"\nQuery: '{query}'")
for rank, hit in enumerate(hits, 1):
bi_score = hit['score']
cross_score = hit['cross_score']
doc = corpus[hit['corpus_id']][:80]
print(f" {rank}. [bi={bi_score:.3f}, cross={cross_score:.3f}] {doc}")
return hits
# Encode corpus una sola volta
corpus_embs = bi_encoder.encode(corpus, convert_to_tensor=True)
retrieval_and_rerank("ancient Roman buildings", corpus, corpus_embs)
8. Evaluare: STS-B și Metric
Evaluarea corectă a unui sistem de similaritate semantică necesită seturi de date standardizate de referință. STS-B este principala referință pentru limba engleză, în timp ce STS-IT este disponibil pentru limba italiană.
from sentence_transformers import SentenceTransformer
from sentence_transformers.evaluation import EmbeddingSimilarityEvaluator
from datasets import load_dataset
import numpy as np
from scipy.stats import pearsonr, spearmanr
# Carica STS-B per l'evaluazione
stsb = load_dataset("mteb/stsbenchmark-sts")
val_data = stsb['validation']
sentences1 = val_data['sentence1']
sentences2 = val_data['sentence2']
scores = [s / 5.0 for s in val_data['score']] # normalizza 0-5 a 0-1
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
# Evaluation automatica
evaluator = EmbeddingSimilarityEvaluator(
sentences1=sentences1,
sentences2=sentences2,
scores=scores,
name="sts-val"
)
pearson = model.evaluate(evaluator)
print(f"STS-B validation - Pearson: {pearson:.4f}")
# Valutazione manuale con correlazione di Pearson e Spearman
emb1 = model.encode(sentences1, show_progress_bar=False)
emb2 = model.encode(sentences2, show_progress_bar=False)
from numpy.linalg import norm
cos_sims = [
np.dot(e1, e2) / (norm(e1) * norm(e2))
for e1, e2 in zip(emb1, emb2)
]
pearson_r, _ = pearsonr(cos_sims, scores)
spearman_r, _ = spearmanr(cos_sims, scores)
print(f"Pearson: {pearson_r:.4f}")
print(f"Spearman: {spearman_r:.4f}")
# Analisi degli errori: trova le coppie più sbagliate
errors = [(abs(p - t), s1, s2, p, t)
for p, t, s1, s2 in zip(cos_sims, scores, sentences1, sentences2)]
errors.sort(reverse=True)
print("\n=== Top 3 Errori ===")
for err, s1, s2, pred, true in errors[:3]:
print(f" Errore: {err:.3f} | Pred: {pred:.3f} | True: {true:.3f}")
print(f" '{s1[:60]}'")
print(f" '{s2[:60]}'")
9. Reglarea fină a unui transformator de propoziție pe domeniul dvs
Modelele pre-antrenate sunt excelente pe texte generale, dar pentru domenii specifice (medicală, juridică, tehnică) este mai bine să ajustați cu perechi de propoziții adnotate.
from sentence_transformers import (
SentenceTransformer,
InputExample,
losses,
evaluation
)
from torch.utils.data import DataLoader
# Dati di training: coppie (frase1, frase2, score)
# Score: 0.0 = totalmente diverse, 1.0 = identiche
train_examples = [
InputExample(texts=["Diagnosi di diabete tipo 2", "Paziente con iperglicemia cronica"], label=0.85),
InputExample(texts=["Prescrizione antibiotico", "Terapia con amoxicillina"], label=0.80),
InputExample(texts=["Intervento chirurgico al ginocchio", "Artroscopia del menisco"], label=0.75),
InputExample(texts=["Pressione arteriosa elevata", "Ipertensione arteriosa"], label=0.95),
InputExample(texts=["Dolore toracico", "Bruciore di stomaco"], label=0.30),
InputExample(texts=["Frattura del femore", "Infarto del miocardio"], label=0.05),
]
# Carica modello base
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
# DataLoader
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)
# Loss: CosineSimilarityLoss per regressione su score continuo
train_loss = losses.CosineSimilarityLoss(model)
# Valutazione su dati di test
test_examples = [
InputExample(texts=["Cefalea tensiva", "Mal di testa da stress"], label=0.88),
InputExample(texts=["Diabete gestazionale", "Diabete in gravidanza"], label=0.92),
]
evaluator_sentences1 = [e.texts[0] for e in test_examples]
evaluator_sentences2 = [e.texts[1] for e in test_examples]
evaluator_scores = [e.label for e in test_examples]
val_evaluator = evaluation.EmbeddingSimilarityEvaluator(
evaluator_sentences1, evaluator_sentences2, evaluator_scores
)
# Fine-tuning
model.fit(
train_objectives=[(train_dataloader, train_loss)],
evaluator=val_evaluator,
epochs=10,
evaluation_steps=50,
warmup_steps=100,
output_path='./medical-sentence-transformer',
save_best_model=True
)
print("Fine-tuning completato!")
print("Modello salvato in './medical-sentence-transformer'")
10. Gata de producție pentru conducte
Un sistem de similaritate semantică în producție trebuie să gestioneze încorporarea în cache, actualizarea progresivă a corpusului și monitorizarea calității.
import faiss
import numpy as np
import json
import hashlib
from sentence_transformers import SentenceTransformer, util
from pathlib import Path
from typing import List, Dict, Optional
class SemanticSearchEngine:
"""
Motore di ricerca semantica production-ready con:
- Caching degli embedding su disco
- Aggiornamento incrementale
- Threshold configurabile
- Logging delle query
"""
def __init__(
self,
model_name: str = 'paraphrase-multilingual-MiniLM-L12-v2',
cache_dir: str = './search_cache',
similarity_threshold: float = 0.5
):
self.model = SentenceTransformer(model_name)
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
self.threshold = similarity_threshold
self.documents: List[Dict] = []
self.embeddings: Optional[np.ndarray] = None
self.index: Optional[faiss.Index] = None
def _doc_hash(self, doc: Dict) -> str:
"""Hash del documento per il caching."""
content = json.dumps(doc, sort_keys=True, ensure_ascii=False)
return hashlib.md5(content.encode()).hexdigest()
def add_documents(self, documents: List[Dict], text_field: str = 'text'):
"""Aggiunge documenti al corpus con caching."""
texts = [doc[text_field] for doc in documents]
new_embeddings = self.model.encode(texts, convert_to_numpy=True, show_progress_bar=True)
if self.embeddings is None:
self.embeddings = new_embeddings
else:
self.embeddings = np.vstack([self.embeddings, new_embeddings])
self.documents.extend(documents)
self._rebuild_index()
print(f"Corpus: {len(self.documents)} documenti")
def _rebuild_index(self):
"""Ricostruisce l'indice FAISS."""
dim = self.embeddings.shape[1]
self.index = faiss.IndexFlatIP(dim)
embs_normalized = self.embeddings.copy()
faiss.normalize_L2(embs_normalized)
self.index.add(embs_normalized)
def search(self, query: str, k: int = 5, text_field: str = 'text') -> List[Dict]:
"""Cerca i documenti più rilevanti per la query."""
if self.index is None or len(self.documents) == 0:
return []
query_emb = self.model.encode([query], convert_to_numpy=True)
faiss.normalize_L2(query_emb)
distances, indices = self.index.search(query_emb, min(k, len(self.documents)))
results = []
for dist, idx in zip(distances[0], indices[0]):
if dist >= self.threshold:
result = dict(self.documents[idx])
result['score'] = float(dist)
results.append(result)
return results
def save(self):
"""Persiste il motore di ricerca su disco."""
faiss.write_index(self.index, str(self.cache_dir / 'index.faiss'))
np.save(str(self.cache_dir / 'embeddings.npy'), self.embeddings)
with open(self.cache_dir / 'documents.json', 'w', encoding='utf-8') as f:
json.dump(self.documents, f, ensure_ascii=False, indent=2)
print(f"Engine saved to {self.cache_dir}")
# Utilizzo
engine = SemanticSearchEngine(similarity_threshold=0.6)
# Aggiungi documenti
docs = [
{"text": "Come configurare un ambiente Python con virtualenv.", "id": "py001", "category": "python"},
{"text": "Installazione e configurazione di Docker su Ubuntu.", "id": "docker001", "category": "devops"},
{"text": "Introduzione alle reti neurali con PyTorch.", "id": "ml001", "category": "ml"},
{"text": "Best practices per la sicurezza delle API REST.", "id": "api001", "category": "security"},
{"text": "Ottimizzazione delle query SQL con indici.", "id": "db001", "category": "database"},
]
engine.add_documents(docs)
# Ricerca
results = engine.search("come creare un ambiente virtuale Python")
for r in results:
print(f"[{r['score']:.3f}] {r['text']}")
11. Sentence Embeddings pentru italiană
Pentru italiană există diverse opțiuni, de la modelul multilingv la reglaj fin specifice corpusurilor italiene. Iată un ghid practic.
from sentence_transformers import SentenceTransformer, util
# Opzione 1: Modello multilingua (più pratico, supporta 50+ lingue)
model_multi = SentenceTransformer('paraphrase-multilingual-mpnet-base-v2')
# Opzione 2: E5 multilingual (stato dell'arte per retrieval)
model_e5 = SentenceTransformer('intfloat/multilingual-e5-large')
# Frasi italiane di test
frasi_it = [
"Il governo italiano ha approvato la nuova legge sul lavoro.",
"Il parlamento ha votato la riforma del mercato del lavoro.", # simile
"La Serie A e il campionato di calcio italiano.", # diversa
"Juventus e Inter si sfideranno nel prossimo derby.", # correlata (calcio)
"Il PIL italiano e cresciuto del 2% nel 2024.", # diversa
]
# Test con modello multilingua
embeddings = model_multi.encode(frasi_it)
sim_matrix = util.cos_sim(embeddings, embeddings)
print("=== Similarità tra frasi italiane (multilingua mpnet) ===")
for i in range(len(frasi_it)):
for j in range(i+1, len(frasi_it)):
score = sim_matrix[i][j].item()
if score > 0.5:
print(f" {score:.3f} | '{frasi_it[i][:50]}'")
print(f" | '{frasi_it[j][:50]}'")
# Per E5: aggiungere prefisso "query: " o "passage: " per retrieval
query = "query: come va l'economia italiana?"
passages = [f"passage: {f}" for f in frasi_it]
q_emb = model_e5.encode(query)
p_embs = model_e5.encode(passages)
scores = util.cos_sim(q_emb, p_embs)
top3 = scores[0].topk(3)
print("\n=== Top 3 risultati con E5 ===")
for score, idx in zip(top3.values, top3.indices):
print(f" {score:.3f} | {frasi_it[idx]}")
12. Erori comune și anti-modele
Anti-Pattern: Folosiți BERT [CLS] direct
Jetonul [CLS] BERT nu este optimizat pentru similitudine semantică.
Folosind-o direct (fără ajustarea fină a sarcinilor de similaritate) dă rezultate
mult mai rău decât SBERT. Utilizați întotdeauna un șablon dedicat pentru transformarea propoziției.
Anti-Pattern: comparați înglobările diferitelor modele
Înglobările de all-MiniLM-L6-v2 si de
paraphrase-multilingual-mpnet-base-v2 trăiesc în spații vectoriale
complet diferit. Nu puteți compara încorporațiile produse de modele diferite.
Utilizați întotdeauna același model pentru toate propozițiile din corpus dvs.
Anti-Pattern: Ignorați normalizarea
Dacă utilizați FAISS cu IndexFlatIP pentru asemănarea cosinusului,
trebuie să normalizați vectorii la norma unitară cu faiss.normalize_L2()
atât în timpul indexării, cât și în timpul căutării. Uită de acest pas
produce rezultate incorecte fără erori explicite.
Cele mai bune practici: Lista de verificare
- STATELE UNITE ALE AMERICII transformatoare de propoziții în loc de BERT brut pentru similitudine semantică
- Alegeți șabloane multilingve pentru conținutul italian sau multilingv
- Normalizați întotdeauna vectorii înainte de indexarea FAISS cu IndexFlatIP
- Persistați încorporarea pe disc pentru a evita recodificarea la fiecare repornire
- Bi-encoder + canalizare cross-encoder pentru recuperare scalabilă + de înaltă calitate
- Evaluați pe STS-B sau un set de date din domeniul dvs. înainte de implementare
- Monitorizați distribuția scorurilor de similaritate în producție pentru a detecta deriva
- Setați un prag minim de încredere pentru a filtra potrivirile irelevante
Concluzii și pașii următori
Asemănarea semantică cu încorporarea propozițiilor este o componentă fundamentală multe aplicații moderne NLP: căutare semantică, potrivire între întrebări frecvente, deduplicare, sisteme de recomandare și RAG. Modele SBERT și transformatoare de propoziții au făcut aceste capabilități accesibile cu doar câteva linii de cod, în timp ce FAISS vă permite să scalați la milioane de documente, menținând în același timp latențe de ordinul milisecundelor.
Pentru modele italiene, multilingve precum paraphrase-multilingual-mpnet-base-v2
e intfloat/multilingual-e5-large oferă performanțe excelente
chiar şi în contexte interlingvistice.
Puncte cheie
- STATELE UNITE ALE AMERICII SBERT în loc de BERT standard pentru similaritate semantică (Pearson 0,87 vs 0,54)
- FAISS și esențială pentru cercetarea pe corpus mare
- Bi-encoder + pipeline cross-encoder: viteza de recuperare + calitatea reclasării
- Șabloane multilingve pentru italiană:
paraphrase-multilingual-mpnet-base-v2omultilingual-e5-large - Evaluați întotdeauna STS-B sau un set de date din domeniul dvs
- Reglare fină specifică domeniului cu
CosineSimilarityLosspentru calitate maxima
Seria continuă
- Articolul 10: Monitorizare NLP în producție — detectarea derivei și recalificarea automată
- Articolul 8: Reglaj local LoRA — adaptați LLM la domeniul dvs. cu GPU-uri pentru consumatori
- Serii înrudite: AI Engineering/RAG — asemănarea semantică ca inimă a recuperării dense
- Serii înrudite: Învățare profundă avansată — pierderea tripletului, învățarea metrică și învățarea contrastivă







