Sémantická podobnost a vkládání vět: Porovnávání textů
Nakolik jsou si dvě věty podobné? Ne v lexikálním smyslu (stejná slova), ale v sémantický smysl (stejný význam). „Pes honí kočku“ a „Přichází kočka honěný špičákem“ jsou sémanticky téměř totožné, ale lexikálně velmi odlišné. Zodpovědět tuto otázku je výzvou Sémantická podobnost.
Aplikace jsou všude: sémantické vyhledávače, systémy doporučení, deduplikace obsahu, odpovídání na otázky, RAG (Retrieval-Augmented Generation), shoda chatbota a FAQ. V tomto článku budeme budovat systémy sémantické podobnosti od nuly: od kosinové podobnosti k vkládání vět pomocí Sentence-BERT, až po rychlé vektorové vyhledávání pomocí FAISS.
Toto je devátý článek ze série Moderní NLP: od BERT po LLM. Toto téma přímo souvisí se seriálem AI inženýrství/RAG kde sémantická vložení jsou srdcem hustého vyhledávání.
Co se naučíte
- Kosinová podobnost a bodový součin: vzorce a kdy je použít
- Limity BERT pro sémantickou podobnost a proč je potřeba Sentence-BERT
- Sentence-BERT (SBERT): Siamská architektura a trénink trojitých ztrát
- Sentence-transformers modely na HuggingFace: který si vybrat
- Sémantické vyhledávání na velkých korpusech pomocí FAISS
- Vkládání vět pro italštinu
- Benchmarking: STS-B, SICK a vyhodnocovací metriky
- Křížový kodér vs bikodér: kompromis mezi kvalitou a rychlostí
- Jemné doladění větného transformátoru na vaší doméně
- Kompletní implementace systému porovnávání FAQ
- Produkční kanál s ukládáním do mezipaměti a optimalizací
1. Problém sémantické podobnosti
Podívejme se na tyto tři skupiny vět a jejich problémy:
Příklady sémantické podobnosti
- Vysoká podobnost: „Banka zvýšila sazby“ / „Úrokové sazby zvyšuje bankovní instituce“
- Nízká podobnost: „Banka zvýšila sazby“ / „Kočka spí na gauči“
- Klamný (stejná slova, jiný význam): "Školní stůl" / "Rybí pult na trhu"
- Mezilingvistika: "Pes běží rychle" (stejná sémantika, různé jazyky)
Tradiční metriky jako např Jaccard podobnost nebo BM25 spoléhají na lexikální přesah a zcela selhávají u synonym a parafráze. Ani jednoduché TF-IDF nezachycuje význam. Řešení spočívá v sémantická vložení: Husté vektorové reprezentace, kde blízkost geometrický odráží sémantickou blízkost.
1.1 Kosinová podobnost: základní metrika
La malá podobnost měří úhel mezi dvěma vektory v prostoru vložení. Rozsahy od -1 (opačné) do 1 (identické), s 0 pro ortogonální vektory. Matematický vzorec je:
cos(A, B) = (A · B) / (||A|| · ||B||)
Když jsou vektory normalizovány na jednotkovou normu, kosinová podobnost se shoduje s tečkovým produktem, díky kterému jsou výpočty mnohem efektivnější na hardwaru 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 Další metriky vzdálenosti
Porovnání metrik podobnosti/vzdálenosti
| Metrický | Vzorec | Rozsah | Případ použití |
|---|---|---|---|
| Kosinová podobnost | cos(A, B) | [-1, 1] | Standardní sémantická podobnost |
| Euklidovská vzdálenost | ||A - B|| | [0, +inf) | Shlukování, k-NN |
| Dot Product | A · B | (-inf, +inf) | S normalizovanými vektory = kosinus |
| Vzdálenost Manhattan | součet(|A-B|) | [0, +inf) | Odolnost vůči odlehlým hodnotám |
| Pearsonova korelace | cov(A,B)/sigma | [-1, 1] | Hodnocení na benchmarku STS |
2. Proč standardní BERT nefunguje pro podobnost
Intuitivně bychom mohli použít BERT k extrahování vložení vět a jejich porovnání. Ale výzkum Reimers & Gurevych (2019) ukázal, že toto přístup e překvapivě neúčinné.
Hlavním problémem je, že BERT je předem proškolen s Masked Language Modeling (MLM) a
Predikce další věty (NSP). Token [CLS] kóduje informace
užitečné pro klasifikaci párů vět (NSP), ale není optimalizováno pro
vytvářet vložení, která při srovnání odrážejí sémantickou podobnost
s malou podobností.
Navíc průměrné sdružování všech tokenů vytváří prostor pro vkládání anizotropní: směry nejsou rovnoměrně rozloženy, a shluky významově odlišných vět se překrývají.
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
Výkon BERT na STS-B (benchmark)
Na úloze STS-B (Sémantická textová podobnost Benchmark), BERT s průměrným sdružováním prostě dosáhne Pearson r = 0,54, hluboko pod přístupy pod dohledem jako SBERT (0,87). I samotný token [CLS] dosahuje pouze 0,20. Pro sémantickou podobnost je SBERT správnou volbou.
3. Věta-BERT (SBERT): Řešení
Věta-BERT (Reimers & Gurevych, EMNLP 2019) problém řeší s architekturou siamský: dva případy podílových vah BERT, zpracovávají dvě věty odděleně a ztrátová funkce si vynucuje reprezentace sémanticky podobné blízkosti ve vektorovém prostoru.
3.1 Siamská architektura
Klíčovou myšlenkou je, že obě „sítě“ sdílejí naprosto stejnou váhu. Nejedná se o dva samostatné modely, ale o stejný model nazývaný dvakrát. Ztráta se vypočítá na výstupním páru:
- Cíl regrese: MSE mezi předpokládanou kosinovou podobností a lidským skóre (pro STS)
- Cíl klasifikace: Křížová entropie na [u, v, |u-v|] (pro NLI)
- Trojitá ztráta: ztráta marže u kotvy/pozitivní/negativní (pro parafrázovou těžbu)
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. Modely větných transformátorů: Který si vybrat
Modely hlavních větných transformátorů (2024–2025)
| Model | Jazyky | Ztlumit. | Rychlost | STS-B Pearson |
|---|---|---|---|---|
| vše-MiniLM-L6-v2 | EN | 384 | Velmi rychle | 0,834 |
| all-mpnet-base-v2 | EN | 768 | Průměrný | 0,869 |
| parafráze-vícejazyčný-MiniLM-L12-v2 | 50+ jazyků | 384 | Rychle | 0,821 |
| parafráze-multilingual-mpnet-base-v2 | 50+ jazyků | 768 | Průměrný | 0,853 |
| intfloat/multilingual-e5-large | 100+ jazyků | 1024 | Pomalý | 0,892 |
| text-embedding-3-small (OpenAI) | Vícejazyčný | 1536 | Pouze API | ~0,90 |
4.1 Výběr modelu: Praktický průvodce
Výběr závisí na třech hlavních faktorech: jazyku, rychlosti a potřebné kvalitě.
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. Sémantické vyhledávání pomocí FAISS
U velkých korpusů (miliony dokumentů) prohledávání hrubou silou (vypočítejte podobnost se všemi dokumenty) a příliš pomalé. FAISS (Facebook AI Similarity Search) umožňuje přibližné vyhledávání nejbližšího souseda v sublineárním čase s různými typy indexů.
5.1 Typy indexu FAISS
Indexy FAISS: Vyrovnání rychlosti/přesnosti
| Index | Typ | Případ použití | Stažení (%) | Rychlost |
|---|---|---|---|---|
| IndexFlatL2 | Přesný | < 100 tisíc dokumentů | 100 % | Pomalý |
| IndexFlatIP | Přesně (maličkosti) | < 100 tisíc dokumentů | 100 % | Pomalý |
| IndexIVFFlat | Přibližný | 100 tis. – 10 mil | ~95 % | Rychle |
| IndexHNSW | Přibližný | 1M+ | ~99 % | Velmi rychle |
| IndexIVFPQ | Stlačený | 10M+, omezená RAM | ~85 % | Velmi rychle |
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 Stálost indexu a načítání
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. FAQ Matching: Kompletní případ použití
Praktická aplikace sémantické podobnosti: automatické porovnávání otázek uživatel s existujícími FAQ. Tento vzor je základem mnoha chatbotů a systémů zákaznickou podporu.
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. Křížový kodér vs. Bi-kodér
Existují dva přístupy k sémantické podobnosti, které nabízejí různé kompromisy kvalita/rychlost. Jejich pochopení je zásadní pro výběr správné architektury.
Bi-kodér vs křížový kodér
| čekám | Bi-kodér (SBERT) | Křížový kodér |
|---|---|---|
| Architektura | Dva samostatné BERTy vytvářejí vložení | BERT, který zpracuje pár |
| Rychlost | Velmi rychlé (předvýpočetní vkládání) | Pomalé (zpracovat každý pár) |
| Škálovatelnost | Miliony dokumentů | Jen pár stovek párů |
| kvalitní | Dobré (~0,87 Pearson na STS-B) | Vynikající (~0,92 Pearson) |
| Případ použití | Vyhledávání, sémantické vyhledávání | Přeřazení výsledků |
| Složitost O(n) | O(1) pro dotazy (předem vypočítaná vložení) | O(n) pro každý dotaz |
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. Hodnocení: STS-B a metriky
Správné vyhodnocení systému sémantické podobnosti vyžaduje standardizované referenční datové sady. STS-B je hlavní reference pro angličtinu, zatímco STS-IT je k dispozici pro italštinu.
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. Jemné doladění Sentence Transformer na vaší doméně
Předtrénované modely jsou skvělé pro obecné texty, ale pro specifické oblasti (lékařské, právní, technické) je lepší doladit dvojicemi komentovaných vět.
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. Výroba potrubí – připravena
Systém sémantické podobnosti v produkci musí spravovat ukládání do mezipaměti vkládání, přírůstková aktualizace korpusu a sledování kvality.
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. Vkládání vět pro italštinu
Pro italštinu existují různé možnosti, od vícejazyčného modelu až po jemné doladění specifické pro italské korpusy. Zde je praktický návod.
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. Běžné chyby a anti-vzory
Anti-Pattern: Použijte přímo BERT [CLS]
Token [CLS] BERT není optimalizován pro sémantickou podobnost.
Přímé použití (bez dolaďování úloh podobnosti) poskytuje výsledky
mnohem horší než SBERT. Vždy používejte vyhrazenou šablonu převodníků vět.
Anti-Pattern: Porovnejte vložení různých vzorů
Vložení all-MiniLM-L6-v2 a z
paraphrase-multilingual-mpnet-base-v2 žijí ve vektorových prostorech
úplně jiný. Nelze porovnávat vložení vyrobené různými modely.
Vždy používejte stejný vzor pro všechny věty v korpusu.
Anti-Pattern: Ignorovat normalizaci
Pokud používáte FAISS s IndexFlatIP pro kosinusovou podobnost,
musíte normalizovat vektory na jednotkovou normu faiss.normalize_L2()
jak při indexování, tak při vyhledávání. Zapomeňte na tento krok
poskytuje nesprávné výsledky bez explicitních chyb.
Nejlepší postupy: Kontrolní seznam
- USA větné transformátory místo surového BERT pro sémantickou podobnost
- Vyberte si vícejazyčné šablony pro italský nebo vícejazyčný obsah
- Vždy normalizujte vektory před indexováním FAISS pomocí IndexFlatIP
- Zachovejte vkládání na disk, abyste se vyhnuli překódování při každém restartu
- Bi-kodér + potrubí křížového kodéru pro škálovatelné + vysoce kvalitní vyhledávání
- Před nasazením vyhodnoťte na STS-B nebo datové sadě z vaší domény
- Sledujte distribuci skóre podobnosti v produkci, abyste odhalili posun
- Nastavte minimální práh spolehlivosti pro odfiltrování irelevantních shod
Závěry a další kroky
Sémantická podobnost s vkládáním vět je základní složkou z mnoha moderních aplikací NLP: sémantické vyhledávání, shoda FAQ, deduplikace, systémy doporučení a RAG. SBERT a modely větných transformátorů zpřístupnili tyto schopnosti pomocí několika řádků kódu, zatímco FAISS umožňuje škálovat na miliony dokumentů při zachování latence v řádu milisekund.
Pro italštinu, vícejazyčné modely jako např paraphrase-multilingual-mpnet-base-v2
e intfloat/multilingual-e5-large nabízejí vynikající výkon
i v mezijazykových kontextech.
Klíčové body
- USA SBERT místo standardního BERT pro sémantickou podobnost (Pearson 0,87 vs 0,54)
- FAISS a zásadní pro velký korpusový výzkum
- Bi-encoder + cross-encoder pipeline: rychlost vyhledávání + kvalita rerankingu
- Vícejazyčné šablony pro italštinu:
paraphrase-multilingual-mpnet-base-v2omultilingual-e5-large - Vždy ohodnoťte STS-B nebo datovou sadu z vaší domény
- Jemné doladění specifické pro doménu pomocí
CosineSimilarityLosspro maximální kvalitu
Série pokračuje
- Článek 10: Monitorování NLP ve výrobě — detekce posunu a automatické přeškolení
- Článek 8: Lokální LoRA Jemné doladění — přizpůsobte LLM vaší doméně pomocí spotřebitelských GPU
- Související série: AI inženýrství/RAG — sémantická podobnost jako srdce hustého vyhledávání
- Související série: Pokročilé hluboké učení — trojitá ztráta, metrické učení a kontrastivní učení







