Osadzanie i wyszukiwanie wektorów: BERT kontra transformatory zdań
W pierwszym artykule z tej serii zbadaliśmy architekturę RAG i jej rolę w rozwiązać halucynacje LLM. Bijącym sercem każdego systemu RAG jest wyszukiwanie: umiejętność odnajdywania w potencjalnej bazie wiedzy ogromne, najbardziej istotne dokumenty do wniosku. Ta umiejętność opiera się całkowicie włączone osadzania i dalej wyszukiwanie wektorów.
Osadzanie to numeryczna reprezentacja znaczenia tekstu: sekwencja liczby (wektor), które przechwytują relacje semantyczne między słowami, zdaniami i dokumentami. Jakość osadzania bezpośrednio determinuje jakość wyszukiwania, a co za tym idzie jakość całego systemu RAG. Wybór niewłaściwego modelu osadzania oznacza zbuduj fundament domu na piasku.
W drugim artykule z tej serii Inżynieria AI i zaawansowane RAG, odbędziemy pełną podróż: od początków osadzania w Word2Vec, aż do rewolucję BERT, aż po nowoczesne Transformatory Zdań. Zobaczymy jak wygenerować osadzania, jak porównywać teksty w przestrzeni wektorowej, jak zbudować silnik wyszukiwanie semantyczne za pomocą FAISS i jak wybrać odpowiedni model dla swojego przypadku użycia.
Przegląd serii
| # | Przedmiot | Centrum |
|---|---|---|
| 1 | RAG wyjaśnione | Podstawy i pełna architektura |
| 2 | Jesteś tutaj - Osadzania i wyszukiwanie semantyczne | Jak teksty stają się wektorami |
| 3 | Szczegółowa baza danych wektorowych | Przechowywanie, indeksowanie, wyszukiwanie podobieństw |
| 4 | RAG z LangChainem i Pythonem | Praktyczna kompleksowa realizacja |
| 5 | Odzyskiwanie hybrydowe i zmiana rankingu | Hybrydowe słowo kluczowe + wyszukiwanie semantyczne |
| 6 | Okno kontekstowe i szybka inżynieria | Zoptymalizuj kontekst dla LLM |
| 7 | RAG w produkcji | Skalowanie, monitorowanie, ewaluacja |
| 8 | Grafy wiedzy i RAGs | Wykresy wiedzy + wyszukiwanie |
| 9 | Systemy wieloagentowe | Współpracujący agenci AI |
| 10 | Przyszłość RAG | Trendy, badania i kolejne kroki |
Czego się nauczysz
- Co to jest osadzanie i jak reprezentuje znaczenie w formie liczbowej
- Ewolucja od Word2Vec przez BERT do Transformatorów Zdań
- dlaczego waniliowy BERT nie działa na podobieństwo i jak SBERT rozwiązuje problem
- Jak wybrać odpowiedni model osadzania spośród kilkudziesięciu opcji
- Zaimplementuj wyszukiwanie semantyczne za pomocą transformatorów zdań i FAISS w Pythonie
- Metryki podobieństwa wektorów i kiedy ich używać
- Porównanie architektoniczne głównych wyszukiwarek wektorowych
- Jak dostosować osadzanie dla określonych domen
1. Co to jest osadzanie
Un osadzanie jest to funkcja matematyczna przekształcająca dyskretny obiekt (słowo, zdanie, dokument, obraz) na wektor liczb rzeczywistych w jednym ciągła przestrzeń o stałej wymiarowości. Zasadniczo konwertuje tekst czytelny dla człowieka na zrozumiałą maszynowo listę liczb, zachowując relacje semantyczne wśród tekstów oryginalnych.
Podstawową ideą jest to, że teksty o podobnym znaczeniu muszą mieć bliskie wektory w przestrzeń, natomiast teksty o różnych znaczeniach muszą mieć odległe wektory. Ta nieruchomość to się nazywa izomorfizm semantyczny: struktura relacji semantycznych między słowami jest zachowana w geometrii przestrzeni wektorowej.
1.1 Od kodowania One-Hot do gęstych wektorów
Aby zrozumieć, dlaczego osadzanie jest konieczne, rozważmy najprostszą alternatywę: the jedno-gorące kodowanie. Ze słownictwem obejmującym 50 000 słów, każde słowo jest reprezentowany przez wektor o 50 000 wymiarach, zawierający tylko jedną jedynkę i wszystkie pozostałe zera.
ONE-HOT ENCODING (vocabolario di 50.000 parole):
"gatto" = [0, 0, ..., 1, ..., 0, 0] (50.000 dimensioni, un solo 1)
"cane" = [0, 0, ..., 0, ..., 1, 0] (50.000 dimensioni, un solo 1)
Distanza tra "gatto" e "cane" = stessa di "gatto" e "frigorifero"
Nessuna informazione semantica!
DENSE EMBEDDING (es. 384 dimensioni):
"gatto" = [0.23, -0.45, 0.89, ..., 0.12] (384 dimensioni, tutti numeri reali)
"cane" = [0.21, -0.42, 0.91, ..., 0.15] (384 dimensioni, tutti numeri reali)
Distanza tra "gatto" e "cane" = PICCOLA (animali domestici)
Distanza tra "gatto" e "frigorifero" = GRANDE (concetti diversi)
Il significato è catturato nella geometria!
Problemy z kodowaniem one-hot są oczywiste: wektory są ogromne (wymiarowość równe słownictwu), rozproszone (prawie same zera) i przede wszystkim wszystkie względem siebie ortogonalne. Każde dwa słowa mają tę samą odległość, niezależnie od znaczenia. nie ma sposób na odróżnienie „kota” od „kociaka” od „kota” od „ekonomii”.
I gęste osady rozwiązać wszystkie trzy problemy: wektory są zwarty (kilkaset wymiarów), gęsty (wszystkie wartości są istotne) i wychwytują relacje semantyczne w swojej geometrii. Podobne słowa mają bliskie wektory, a kierunki w przestrzeni odpowiadają pojęciom językowym.
1.2 Przestrzeń semantyczna
Fascynującą właściwością osadzania jest to, że relacje semantyczne tak przekształcić w zależności geometryczne. Klasycznym przykładem jest arytmetyka wektor: wektor „król” minus „mężczyzna” plus „kobieta” tworzy bardzo zbliżony wektor do „królowej”. To nie jest magia: oznacza to, że przestrzeń uchwyciła koncepcję „płeć” jako jeden kierunek, a koncepcja „królewstwa” jako drugi.
Relazioni semantiche come operazioni vettoriali:
vec("re") - vec("uomo") + vec("donna") ~ vec("regina")
vec("Parigi") - vec("Francia") + vec("Italia") ~ vec("Roma")
vec("buono") - vec("migliore") + vec("grande") ~ vec("più grande")
Cluster nello spazio:
[gatto, cane, cavallo, pesce] --> vicini (animali)
[Python, Java, C++, Rust] --> vicini (linguaggi di programmazione)
[felice, contento, gioioso] --> vicinissimi (sinonimi)
Podstawowa intuicja
Osadzanie to zasadniczo a czyli kompresor. Trwa znaczenie tekstu, ze wszystkimi jego niuansami, i rozumie go w jednym punkcie w przestrzeni wielowymiarowej. Położenie tego punktu względem wszystkich pozostałych punkty przechwytują wszystkie relacje semantyczne, których nauczył się model. To jest na której opierają się całe badania semantyczne.
2. Klasyczne osadzanie słów: Word2Vec, GloVe, FastText
Nowoczesna historia osadzania rozpoczyna się w 2013 roku od Word2Stary, opublikowane przez Tomasa Mikolova i współpracowników z Google. Rewolucyjny pomysł był prosty: znaczenia słowa można nauczyć się z kontekstu, w jakim ono się pojawia. Jak powiedział językoznawca John Firth w 1957 r.: „Słowo poznasz po towarzystwie, w jakim się ono znajduje”.
2.1 Word2Vec: CBOW i pomijanie gramów
Word2Vec proponuje dwie architektury neuronowe do osadzania uczenia się:
- CBOW (ciągły zbiór słów): Biorąc pod uwagę okno słów kontekstowych, przewiduj słowo centralne. Przykład: biorąc pod uwagę „___ miauczy głośno”, przewiduj „kota”
- Przeskok: Biorąc pod uwagę słowo centralne, przewiduj słowa kontekstu. Przykład: biorąc pod uwagę „kot”, przewiduj „the”, „miau”, „głośno”
CBOW (Continuous Bag of Words):
Input: parole di contesto ["il", "___", "miagola", "forte"]
Output: parola target "gatto"
Contesto ──> [Embedding Layer] ──> Media vettori ──> [Softmax] ──> "gatto"
Veloce, buono per parole frequenti
SKIP-GRAM:
Input: parola target "gatto"
Output: parole di contesto ["il", "miagola", "forte"]
"gatto" ──> [Embedding Layer] ──> [Softmax] ──> parole contesto
Più lento, migliore per parole rare
Parametri tipici:
- Dimensioni embedding: 100-300
- Finestra di contesto: 5-10 parole
- Vocabolario: 100k-1M parole
- Training: miliardi di parole (Wikipedia, Common Crawl)
2.2 GloVe i FastText
Rękawica (Global Vectors for Word Representation, Stanford 2014) przyjmuje a inne podejście: buduje globalną macierz współwystępowań i rozkłada ją na czynniki zdobądź osady. Uchwyć globalne relacje Word2Vec za pomocą jego okna lokalny, może przegrać.
Szybki Tekst (Facebook 2016) rozszerza Word2Vec o pracę na poziomie słowa podrzędne (n-gramów znaków). Przedstawione jest słowo „osadzanie”. także według jego komponentów: „emb”, „mbe”, „bed”, „edd” itp. Umożliwia to generowanie osadzanie również słów nigdy nie widzianych podczas szkolenia (words poza słownictwem), jest to kluczowa zaleta w przypadku języków o bogatej morfologii, takich jak włoski.
Porównanie klasycznych osadzań słów
| Model | Rok | Zbliżać się | Siła | Limit |
|---|---|---|---|---|
| Word2Stary | 2013 | Przewidywanie kontekstu lokalnego | Szybki, skuteczny | Żadnego OOV, żadnego kontekstu |
| Rękawica | 2014 | Globalne współwystępowanie | Globalne relacje | Żadnego OOV, żadnego kontekstu |
| Szybki Tekst | 2016 | N-gramów znaków | Zarządza OOV | Jeden wektor na słowo |
2.3 Podstawowy limit: jeden wektor na słowo
Wszystkie klasyczne osadzania słów mają wspólne ograniczenie strukturalne: tworzą pojedynczy wektor dla każdego słowa, niezależnie od kontekstu. Słowo „biurko” ma to samo znaczenie zarówno w słowach „biurko szkolne”, jak i „biurko”. Włoch”. Jest to poważny problem, ponieważ znaczenie słowa zależy prawie zawsze przez kontekst, w jakim się pojawia.
Co więcej, modele te działają na poziomie pojedyncze słowa: nie może utworzyć osadzenie zdania lub akapitu. Reprezentować A zdanie, musimy uciekać się do podstawowych strategii, takich jak uśrednianie wektorów tworzących go słów, tracąc informacje o porządku i strukturze składniowej.
Dlaczego nie użyć Word2Vec dla RAG
Klasyczne osadzanie słów jest nieodpowiednie dla współczesnego wyszukiwania semantycznego, ponieważ: (1) nie oddają kontekstu, (2) nie osadzają na poziomie zdania, (3) średnia wektorowa traci krytyczne informacje. Wyrażenie „pies pogryzł człowieka” i „człowiek pogryzł psa” miałyby to samo osadzenie. Do RAG potrzebujesz modeli którzy rozumieją znaczenie całego zdania w jego kontekście.
3. Osadzanie kontekstowe: rewolucja BERT
W 2018 roku Google publikuje BERT (Dwukierunkowe reprezentacje kodera z Transformers) i radykalnie zmienia krajobraz. BERT produkuje osadzania kontekstowe: reprezentacja każdego słowa zależy z całego kontekstu zdania, w którym się pojawia. Słowo „biurko” w „biurku szkolnym” będzie miał inną oprawę niż „il banca d'Italia”.
3.1 Jak działa BERT
BERT opiera się na architekturze Enkoder transformatorowy. Pobiera dane wejściowe sekwencję tokenów i tworzy dla każdego tokena kontekstualizowany wektor 768 wymiary (podstawa BERT) lub wymiary 1024 (BERT-duży). Siła BERT tkwi w mechanizm samouważność: każdy token „patrzy” na wszystkie inne tokeny w pliku sekwencji, zarówno w lewo, jak i w prawo (dwukierunkowo), aby obliczyć własną reprezentacja.
Input: [CLS] il banco di scuola [SEP]
Tokenizzazione:
[CLS] il ban ##co di scuo ##la [SEP]
12 layer di Transformer Encoder:
Layer 1: Self-attention + Feed-forward
Layer 2: Self-attention + Feed-forward
...
Layer 12: Self-attention + Feed-forward
Output: un vettore 768-dim per OGNI token
[CLS] --> [0.23, -0.45, ..., 0.12] (vettore "riassunto")
il --> [0.11, -0.32, ..., 0.08]
ban --> [0.56, 0.12, ..., 0.34] (contestualizzato!)
##co --> [0.45, 0.09, ..., 0.29]
di --> [0.03, -0.21, ..., 0.05]
scuo --> [0.67, 0.33, ..., 0.41]
##la --> [0.59, 0.28, ..., 0.38]
[SEP] --> [0.02, -0.05, ..., 0.01]
3.2 Strategie łączenia zasobów: od tokena do frazy
BERT tworzy wektor dla każdy żeton, ale do wyszukiwania semantycznego potrzebujemy tego a pojedynczy wektor dla całego zdania. Jak to zdobyć? Jest ich kilka strategie łączenie:
Strategie łączenia zasobów dla BERT
| Strategia | Opis | jakość |
|---|---|---|
| [CLS] Tokeny | Użyj specjalnego nośnika tokenów [CLS] | Przeciętny pod względem podobieństwa |
| Średnie łączenie | Średnia wszystkich wektorów tokenów | Ogólnie dobrze |
| Maksymalne łączenie | Maksymalna wartość dla każdego wymiaru | Uchwyć najważniejsze cechy |
| Średnia ważona | Średnia ważona z wagami uwagi | Dobre, bardziej złożone |
3.3 dlaczego BERT Vanilla nie działa na podobieństwo
Pomimo mocy BERT, użyj go bezpośrednio, aby obliczyć podobieństwo między zdania jest wyjątkowo nieefektywne i dające słabe rezultaty. Powód jest dwojaki:
- Nieefektywność obliczeniowa: Aby porównać ze sobą N zdań, musisz przekazać każdą parę zdań przez BERT. Przy 10 tysiącach wyroków są potrzebni 10 000 x 10 000 / 2 = 50 milionów podań do przodu. Z czasem około 65 ms na parom, zajęłoby to około 65 godzin
- Zdegenerowana przestrzeń osadzania: Osadzenia powstałe w wyniku łączenia na BERT nie są zoptymalizowane pod kątem podobieństwa cosinus. Wszystkie zdania mają tendencję kończą się w wąskim obszarze przestrzeni, co utrudnia rozróżnienie zdań podobne z różnych zdań. Zjawisko to nazywa się anizotropia
Problem z kodowaniem krzyżowym
BERT został zaprojektowany jako koder krzyżowy: pobiera DWA zdania jako dane wejściowe i generuje wynik podobieństwa. Dla każdej pary zdań musisz ponownie obliczyć wszystko od zera. Przy korpusie 10 000 dokumentów i zapytaniu potrzeba 10 000 podanie do przodu. Aby skonstruować indeks wszystkich możliwych porównań, koszt rośnie kwadratowo. To sprawia, że BERT jest niepraktyczny do wyszukiwania w czasie rzeczywistym.
4. Transformatory zdań: rozwiązanie
W 2019 roku publikują Nils Reimers i Iryna Gurevych Zdanie-BERT (SBERT), co elegancko rozwiązuje oba problemy. Kluczową ideą jest przekształcenie BERT-a z koder krzyżowy a bi-enkoder: zamiast przekazywać pary zdań, każde zdanie jest niezależnie kodowane w gęstym wektorze. Podobieństwo tak następnie oblicza za pomocą prostej operacji wektorowej (podobieństwo cosinus).
4.1 Architektura syjamska i sieci tripletowe
SBERT jest przeszkolony w zakresie architektury a Sieć syjamska: dwie kopie identyczne modele BERT (ze wspólnymi wagami) przetwarzają dwa zdania osobno. Przewoźnicy otrzymane wyniki są następnie porównywane z funkcją straty, która „zbliża się” do wektorów podobnych zdań i „usuń” wektory różnych zdań.
CROSS-ENCODER (BERT vanilla):
["frase A", "frase B"] ──> [BERT] ──> punteggio similarità
Deve processare OGNI coppia. O(n^2) per n frasi.
BI-ENCODER (SBERT):
"frase A" ──> [BERT + Pooling] ──> vettore_A (384-dim)
"frase B" ──> [BERT + Pooling] ──> vettore_B (384-dim)
similarità = cosine(vettore_A, vettore_B)
Ogni frase viene codificata UNA VOLTA. O(n) per n frasi.
I vettori possono essere pre-calcolati e indicizzati!
TRAINING con Siamese Network:
Frase 1 ──> [BERT] ──> pool ──> u
├──> loss(u, v, label)
Frase 2 ──> [BERT] ──> pool ──> v
Label = 1 se frasi simili, 0 se diverse
Loss: Contrastive Loss o Cosine Similarity Loss
Le sieci trójek rozszerzyć podejście o trzy dane wejściowe: kotwicę, przykład pozytywny (podobny) i przykład negatywny (inny). Strata (potrójna strata marży) popycha model tak, aby odległość dodatnia kotwicy była mniejsza niż odległość kotwica-negatyw co najmniej o z góry określony margines.
4.2 Zalety SBERT w porównaniu do BERT
Porównanie wydajności
| Metryczny | Koder krzyżowy BERT | Bi-enkoder SBERT |
|---|---|---|
| Podobieństwo 10 000 zdań | ~65 godzin | ~5 sekund |
| Wstępne obliczenia wektorowe | Niemożliwe | Tak, tylko raz |
| Indeksowanie | Nie dotyczy | FAISS, HNSW itp. |
| Jakość wzorcowa STS | ~87 (Włócznik) | ~85 (Włócznik) |
| Zastosowanie produkcyjne | Tylko zmiana rankingu | Pobieranie + ranking |
SBERT poświęca niewielki procent dokładności (około 2 punktów w teście porównawczym STS) ale zarób jednego 10 000 razy większa prędkość. W praktyce wybór jest obowiązkowe: cross-enkoder może być używany jako re-ranking na pierwszych k wynikach SBERT, łącząc najlepsze z obu podejść (wzór zwany odzyskać i ponownie uszeregować).
5. Porównanie modeli osadzania
Ekosystem modeli osadzania jest rozległy i szybko się rozwija. Wybór właściwy model zależy od przypadku użycia: wyszukiwanie semantyczne, grupowanie, klasyfikacja, Wielojęzyczny RAG, budżet i opóźnienie. Oto szczegółowe porównanie najbardziej odpowiednich modeli.
Porównanie modeli osadzania (2024-2025)
| Model | Dostawcy | Wymiary | Maksymalny znak | Średnia MTEB | Notatki |
|---|---|---|---|---|---|
| all-MiniLM-L6-v2 | SBERTA | 384 | 256 | 56,26 | Bardzo szybki, idealny do prototypowania |
| all-mpnet-base-v2 | SBERTA | 768 | 384 | 57,78 | Najlepszy stosunek jakości do prędkości open source |
| e5-duży-v2 | Microsoftu | 1024 | 512 | 62.20 | Wysoka jakość, wymaga przedrostka „query:” / „passage:” |
| BGE-M3 | BAAI | 1024 | 8192 | 63,55 | Wielojęzyczny, gęsty+rzadki+colbert |
| osadzanie tekstu-3-małe | OpenAI | 1536 | 8191 | 62,26 | Cloud API, tanie (token 0,02 USD/1 mln) |
| osadzanie tekstu-3-duże | OpenAI | 3072 | 8191 | 64,59 | Cloud API, najwyższa jakość (token $0,13/1M) |
| osadzanie-v3 (angielski) | Przystać do siebie | 1024 | 512 | 64,47 | Cloud API, idealne do badań |
| nomic-embed-text-v1.5 | Nomiczny | 768 | 8192 | 62,28 | Open-source, długi kontekst, Matrioszka |
Jak czytać testy porównawcze MTEB
MTEB (Massive Text Embedding Benchmark) jest de facto standardem ocenić modele osadzania. Zawiera ponad 56 zbiorów danych w 8 kategoriach (klasyfikacja, grupowanie, klasyfikacja par, reranking, wyszukiwanie, STS, podsumowanie). Wynik średni to średnia we wszystkich kategoriach, ale w przypadku RAG wynik cząstkowy Wyszukiwanie jest to najbardziej istotne. Zawsze sprawdzaj konkretny wynik dla swojego przypadku użycia.
5.1 Kryteria wyboru
- Szybkie prototypowanie: all-MiniLM-L6-v2 - szybki, lekki, darmowy
- Produkcja open source: all-mpnet-base-v2 lub e5-large-v2
- Najwyższa jakość chmury: text-embedding-3-large (OpenAI) lub embed-v3 (Cohere)
- Wielojęzyczny / włoski: BGE-M3 lub wielojęzyczny-e5-duży
- Długi kontekst (długie dokumenty): BGE-M3 (8192 tokenów) lub nomic-embed-text-v1.5
- Ograniczony budżet: text-embedding-3-small (OpenAI) lub hostowane samodzielnie szablony typu open source
6. Metryki podobieństwa wektorów
Kiedy już masz wektory, potrzebujesz funkcji, która zmierzy, ile wynoszą dwa wektory „podobne”. Wybór metryki wpływa zarówno na wyniki, jak i wydajność.
6.1 Podobieństwo cosinusowe
La małe podobieństwo mierzy cosinus kąta między dwoma wektorami. Zakresy od -1 (wektory przeciwne) do +1 (wektory identyczne), gdzie 0 dla wektorów ortogonalnych. Zignoruj wielkość wektorów, skupiając się wyłącznie na kierunku: jest to metryka najczęściej używany do osadzania tekstu.
COSINE SIMILARITY:
A . B sum(a_i * b_i)
cos(A,B) = ─────── = ──────────────────────────────
|A| |B| sqrt(sum(a_i^2)) * sqrt(sum(b_i^2))
Range: [-1, +1]
+1 = identici, 0 = ortogonali, -1 = opposti
Invariante alla scala (normalizzata)
DOT PRODUCT (Prodotto Scalare):
dot(A,B) = sum(a_i * b_i)
Range: (-inf, +inf)
Sensibile alla magnitudine dei vettori
Più veloce (nessuna normalizzazione)
Equivalente a cosine se i vettori sono già normalizzati (norma L2 = 1)
EUCLIDEAN DISTANCE (L2):
d(A,B) = sqrt(sum((a_i - b_i)^2))
Range: [0, +inf)
0 = identici, valori alti = molto diversi
Sensibile alla magnitudine
Nota: DISTANZA, non similarità (valori bassi = più simili)
Kiedy używać jakiego metryki
| Metryczny | Kiedy go używać | Wydajność |
|---|---|---|
| Cosinus podobieństwa | Domyślne dla osadzania tekstu, szablony nieznormalizowane | Średnia (wymaga normalizacji) |
| Produkt kropkowy | Już znormalizowane osadzania (transformatory zdań), gdy potrzebna jest prędkość | Wysoka (łatwiejsza obsługa) |
| Euklidesowy (L2) | Gdy wielkość jest znacząca, grupowanie | Przeciętny |
Przydatna wskazówka: jeśli użyjesz Transformatorów Zdań, wektory można znormalizować zgodnie z jednolitym standardem. W tym przypadku podobieństwo cosinus i iloczyn skalarny są równoważne, a produkt kropkowy jest szybszy. Większość wektorowych baz danych obsługuje wszystkie trzy metryki i pozwala na wybór podczas tworzenia indeksu.
7. Praktyczna implementacja w Pythonie
Przejdźmy do kodu. Wdrożymy kompletny system wyszukiwania semantycznego wykorzystujący
sentence-transformers do osadzania, np FAISS dla
indeksowanie i wyszukiwanie wektorowe.
7.1 Konfiguracja i instalacja
# Crea ambiente virtuale
python -m venv embedding-env
source embedding-env/bin/activate
# Installa le dipendenze principali
pip install sentence-transformers==3.3.1
pip install faiss-cpu==1.9.0 # Per CPU (usa faiss-gpu per GPU)
pip install numpy==2.1.3
pip install pandas==2.2.3
pip install tqdm==4.67.1
# Opzionale: per visualizzazione
pip install matplotlib==3.9.3
pip install scikit-learn==1.6.0
7.2 Generowanie osadzania za pomocą transformatorów zdań
from sentence_transformers import SentenceTransformer
import numpy as np
# Carica il modello (download automatico al primo utilizzo)
model = SentenceTransformer("all-MiniLM-L6-v2")
# Frasi di esempio
sentences = [
"Il gatto dorme sul divano",
"Il felino riposa sul sofa",
"Python è un linguaggio di programmazione",
"Java è usato per lo sviluppo enterprise",
"La ricerca semantica usa gli embeddings",
]
# Genera embeddings
embeddings = model.encode(sentences, show_progress_bar=True)
# Ogni embedding è un vettore numpy
print(f"Tipo: {type(embeddings)}")
print(f"Shape: {embeddings.shape}") # (5, 384) - 5 frasi, 384 dimensioni
print(f"Norma L2 del primo vettore: {np.linalg.norm(embeddings[0]):.4f}")
# Visualizza i primi 10 valori del primo embedding
print(f"\nPrimi 10 valori: {embeddings[0][:10]}")
7.3 Oblicz podobieństwo między frazami
from sentence_transformers import SentenceTransformer, util
import numpy as np
model = SentenceTransformer("all-MiniLM-L6-v2")
# Frasi da confrontare
sentences = [
"Il gatto dorme sul divano",
"Il felino riposa sul sofa",
"Python è un linguaggio di programmazione",
"La pioggia cade sulla citta",
]
embeddings = model.encode(sentences, convert_to_tensor=True)
# Matrice di similarità completa
cosine_scores = util.cos_sim(embeddings, embeddings)
print("Matrice di Similarità Coseno:")
print("-" * 60)
for i in range(len(sentences)):
for j in range(len(sentences)):
score = cosine_scores[i][j].item()
if i != j:
print(f" {sentences[i][:30]:<30} vs {sentences[j][:30]:<30}")
print(f" Similarità: {score:.4f}")
# Calcolo manuale della cosine similarity per verifica
def cosine_similarity_manual(vec_a, vec_b):
"""Calcola cosine similarity manualmente."""
dot_product = np.dot(vec_a, vec_b)
norm_a = np.linalg.norm(vec_a)
norm_b = np.linalg.norm(vec_b)
return dot_product / (norm_a * norm_b)
emb_np = model.encode(sentences)
manual_score = cosine_similarity_manual(emb_np[0], emb_np[1])
print(f"\nVerifica manuale (frase 0 vs 1): {manual_score:.4f}")
Oczekiwany wynik
Zdania „Kot śpi na sofie” i „Kot odpoczywa na sofie” będą miały jedno duże podobieństwo (~0,85), ponieważ wyrażają tę samą koncepcję różnymi słowami. Podobieństwo do „Python to język programowania” będzie niskie (~0,10), ponieważ koncepcje są zupełnie inne. Oto siła badań semantyka a wyszukiwanie słów kluczowych.
7.4 Wyszukiwanie semantyczne za pomocą FAISS
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
class SemanticSearchEngine:
"""Motore di ricerca semantica basato su FAISS."""
def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
self.model = SentenceTransformer(model_name)
self.dimension = self.model.get_sentence_embedding_dimension()
self.index = None
self.documents = []
def build_index(self, documents: list[str], use_gpu: bool = False):
"""Costruisce l'indice FAISS dai documenti."""
self.documents = documents
# Genera embeddings per tutti i documenti
embeddings = self.model.encode(
documents,
show_progress_bar=True,
normalize_embeddings=True, # Normalizza per cosine similarity
batch_size=64,
)
embeddings = embeddings.astype("float32")
# Crea indice FAISS (Inner Product = Cosine per vettori normalizzati)
self.index = faiss.IndexFlatIP(self.dimension)
if use_gpu:
# Trasferisci indice su GPU per ricerca più veloce
res = faiss.StandardGpuResources()
self.index = faiss.index_cpu_to_gpu(res, 0, self.index)
self.index.add(embeddings)
print(f"Indice costruito: {self.index.ntotal} documenti indicizzati")
def search(self, query: str, top_k: int = 5) -> list[dict]:
"""Cerca i documenti più simili alla query."""
# Genera embedding della query
query_embedding = self.model.encode(
[query],
normalize_embeddings=True,
).astype("float32")
# Ricerca nell'indice FAISS
scores, indices = self.index.search(query_embedding, top_k)
results = []
for score, idx in zip(scores[0], indices[0]):
if idx != -1: # -1 indica nessun risultato
results.append({
"document": self.documents[idx],
"score": float(score),
"index": int(idx),
})
return results
def save_index(self, path: str):
"""Salva l'indice su disco."""
faiss.write_index(self.index, path)
def load_index(self, path: str):
"""Carica l'indice da disco."""
self.index = faiss.read_index(path)
# Utilizzo
if __name__ == "__main__":
# Corpus di documenti
documents = [
"Python è un linguaggio di programmazione ad alto livello, interpretato e multiparadigma",
"Java è un linguaggio orientato agli oggetti progettato per essere portabile",
"Il machine learning è un sottoinsieme dell'intelligenza artificiale",
"I database relazionali usano SQL per le query sui dati strutturati",
"Docker permette di containerizzare le applicazioni per il deployment",
"Kubernetes orchestra i container in ambienti di produzione distribuiti",
"React è una libreria JavaScript per costruire interfacce utente",
"Angular è un framework TypeScript per applicazioni web enterprise",
"I transformer sono l'architettura alla base dei modelli linguistici moderni",
"FAISS è una libreria per la ricerca efficiente di similarità tra vettori",
"La sicurezza informatica protegge i sistemi da accessi non autorizzati",
"Il cloud computing fornisce risorse computazionali on-demand via internet",
]
engine = SemanticSearchEngine()
engine.build_index(documents)
# Ricerca semantica
queries = [
"Come funziona l'intelligenza artificiale?",
"Framework per sviluppo web frontend",
"Come distribuire applicazioni in modo scalabile?",
]
for query in queries:
print(f"\nQuery: '{query}'")
print("-" * 50)
results = engine.search(query, top_k=3)
for i, result in enumerate(results, 1):
print(f" {i}. [{result['score']:.4f}] {result['document']}")
7.5 Przetwarzanie wsadowe dla dużych zbiorów danych
import faiss
import numpy as np
from sentence_transformers import SentenceTransformer
from tqdm import tqdm
import json
from pathlib import Path
class LargeScaleEmbedder:
"""Gestisce embedding e indicizzazione per grandi dataset."""
def __init__(self, model_name: str = "all-mpnet-base-v2"):
self.model = SentenceTransformer(model_name)
self.dimension = self.model.get_sentence_embedding_dimension()
def encode_in_batches(
self,
texts: list[str],
batch_size: int = 256,
output_path: str | None = None,
) -> np.ndarray:
"""Genera embeddings in batch con salvataggio incrementale."""
all_embeddings = []
for start_idx in tqdm(range(0, len(texts), batch_size)):
batch = texts[start_idx : start_idx + batch_size]
batch_embeddings = self.model.encode(
batch,
normalize_embeddings=True,
batch_size=batch_size,
show_progress_bar=False,
)
all_embeddings.append(batch_embeddings)
# Salvataggio incrementale (checkpoint)
if output_path and (start_idx % (batch_size * 10) == 0):
partial = np.vstack(all_embeddings)
np.save(f"{output_path}_partial.npy", partial)
result = np.vstack(all_embeddings).astype("float32")
if output_path:
np.save(f"{output_path}.npy", result)
print(f"Embeddings salvati: {result.shape}")
return result
def build_ivf_index(
self,
embeddings: np.ndarray,
n_clusters: int = 100,
n_probe: int = 10,
) -> faiss.Index:
"""Costruisce un indice IVF per ricerca approssimata veloce."""
# Quantizzatore per assegnare vettori ai cluster
quantizer = faiss.IndexFlatIP(self.dimension)
# Indice IVF con Inner Product
index = faiss.IndexIVFFlat(
quantizer, self.dimension, n_clusters, faiss.METRIC_INNER_PRODUCT
)
# Training: impara la struttura dei cluster
print(f"Training indice IVF con {n_clusters} cluster...")
index.train(embeddings)
# Aggiungi i vettori
index.add(embeddings)
index.nprobe = n_probe # Numero di cluster da esplorare in ricerca
print(f"Indice IVF costruito: {index.ntotal} vettori, {n_clusters} cluster")
return index
# Esempio di utilizzo con un grande corpus
if __name__ == "__main__":
embedder = LargeScaleEmbedder(model_name="all-MiniLM-L6-v2")
# Simula un grande corpus (in produzione: carica da file/database)
corpus = [f"Documento di esempio numero {i} sul tema tecnologia" for i in range(100_000)]
# Genera embeddings in batch
embeddings = embedder.encode_in_batches(
corpus,
batch_size=512,
output_path="corpus_embeddings",
)
# Costruisci indice IVF (più veloce di Flat per grandi corpus)
index = embedder.build_ivf_index(embeddings, n_clusters=256, n_probe=16)
# Ricerca
query_embedding = embedder.model.encode(
["intelligenza artificiale e machine learning"],
normalize_embeddings=True,
).astype("float32")
scores, indices = index.search(query_embedding, 5)
for score, idx in zip(scores[0], indices[0]):
print(f" [{score:.4f}] {corpus[idx]}")
7.6 Osadzanie za pomocą szablonów E5 (opartych na prefiksach)
from sentence_transformers import SentenceTransformer
import numpy as np
# I modelli E5 richiedono prefix specifici
model = SentenceTransformer("intfloat/e5-large-v2")
# Per i documenti nel corpus: prefix "passage: "
documents = [
"passage: Python è un linguaggio di programmazione versatile e potente",
"passage: I database NoSQL sono progettati per dati non strutturati",
"passage: Kubernetes gestisce il deployment di applicazioni containerizzate",
"passage: Il deep learning usa reti neurali con molti layer",
]
# Per le query di ricerca: prefix "query: "
queries = [
"query: Come funziona l'apprendimento automatico?",
"query: Strumenti per gestire i container",
]
# Genera embeddings
doc_embeddings = model.encode(documents, normalize_embeddings=True)
query_embeddings = model.encode(queries, normalize_embeddings=True)
# Calcola similarità
for i, query in enumerate(queries):
scores = np.dot(query_embeddings[i], doc_embeddings.T)
best_idx = np.argmax(scores)
print(f"Query: {query}")
print(f" Miglior match: {documents[best_idx]}")
print(f" Score: {scores[best_idx]:.4f}\n")
Zwróć uwagę na przedrostki
Modele E5 i BGE wymagają określonych przedrostków, aby odróżnić zapytania od dokumentów.
Dla E5: query: e passage: . Dla BGE:
Represent this sentence: dla zapytań. Zapomnienie przedrostka pogarsza się
wydajność znacznie. Zawsze sprawdzaj dokumentację modelu.
8. Wyszukiwarki wektorowe: porównanie architektoniczne
FAISS doskonale nadaje się do implementacji na niskim poziomie, ale w produkcji jest potrzebny rozwiązania zarządzające trwałością, filtrowaniem metadanych, skalowalnością poziomą i aktualizacje w czasie rzeczywistym. Oto główne bazy danych wektorów i ich charakterystyka.
Porównanie baz danych wektorowych
| Bazy danych | Typ | Język | Indeksy | Idealny przypadek użycia |
|---|---|---|---|---|
| FAISS | Półka na książki | C++/Pythona | Płaskie, IVF, HNSW, PQ | Czyste badania, osadzanie, prototypowanie |
| Qdrant | Bazy danych | Rdza | HNSW | Produkcja, złożone filtry, wysoka wydajność |
| Szyszka | Zarządzane w chmurze | Właściciel | Właściciel | Zero operacji, uruchamianie, automatyczne skalowanie |
| Milvus | Bazy danych | Przejdź do C++ | Płaskie, IVF, HNSW, DiskANN | Skala przedsiębiorstwa, miliardy wektorów |
| pgwektor | Rozszerzenie | C | IVFFlat, HNSW | Korzystasz już z PostgreSQL, moderuj zbiory danych |
| ChromaDB | Bazy danych | Python/Rdza | HNSW | Prototypowanie, integracja LangChain |
| Tkać | Bazy danych | Go | HNSW | Multimodalny interfejs API GraphQL |
8.1 Przykład z Qdrant
from qdrant_client import QdrantClient
from qdrant_client.models import (
Distance,
VectorParams,
PointStruct,
Filter,
FieldCondition,
MatchValue,
)
from sentence_transformers import SentenceTransformer
# Inizializza client (in-memory per test, o URL per produzione)
client = QdrantClient(":memory:") # oppure QdrantClient("http://localhost:6333")
model = SentenceTransformer("all-MiniLM-L6-v2")
# Crea collection
client.create_collection(
collection_name="articles",
vectors_config=VectorParams(
size=384, # Dimensione vettore all-MiniLM-L6-v2
distance=Distance.COSINE,
),
)
# Documenti con metadati
documents = [
{"text": "Python è ottimo per il data science", "category": "programming", "year": 2024},
{"text": "React è il framework frontend più popolare", "category": "web", "year": 2024},
{"text": "Kubernetes gestisce i container in produzione", "category": "devops", "year": 2023},
{"text": "TensorFlow è usato per il deep learning", "category": "ai", "year": 2024},
{"text": "PostgreSQL è un database relazionale avanzato", "category": "database", "year": 2023},
]
# Indicizzazione
points = []
for i, doc in enumerate(documents):
embedding = model.encode(doc["text"]).tolist()
points.append(
PointStruct(
id=i,
vector=embedding,
payload={"text": doc["text"], "category": doc["category"], "year": doc["year"]},
)
)
client.upsert(collection_name="articles", points=points)
# Ricerca semantica semplice
query = "machine learning e intelligenza artificiale"
query_vector = model.encode(query).tolist()
results = client.search(
collection_name="articles",
query_vector=query_vector,
limit=3,
)
print(f"Query: '{query}'")
for result in results:
print(f" [{result.score:.4f}] {result.payload['text']} ({result.payload['category']})")
# Ricerca con filtro per metadati
filtered_results = client.search(
collection_name="articles",
query_vector=query_vector,
query_filter=Filter(
must=[FieldCondition(key="year", match=MatchValue(value=2024))]
),
limit=3,
)
print(f"\nRicerca filtrata (solo 2024):")
for result in filtered_results:
print(f" [{result.score:.4f}] {result.payload['text']}")
9. Algorytmy indeksowania: kompromis między szybkością a przywołaniem
Dokładne (brutalne) przeszukiwanie milionów wektorów jest zbyt wolne. Algorytmy z Przybliżony najbliższy sąsiad (ANN) poświęcają niewielki procent precyzji, aby uzyskać rzędy wielkości prędkości. Zrozum te algorytmy istotne jest prawidłowe skonfigurowanie bazy wektorów.
9.1 Indeks płaski (wyszukiwanie dokładne)
Indeks Płaski porównuje zapytanie z każdym pojedynczym wektorem w bazie danych. Gwarantowane 100% wycofanie, ale koszt O(n*d), gdzie n to liczba przewoźników, a d to liczba przewoźników wymiarowość. Praktyczne tylko w przypadku małych zestawów danych (do ~ 100 tys. wektorów).
9.2 IVF (odwrócony indeks plików)
Zapłodnienie in vitro dzieli przestrzeń wektorową na klastry za pomocą k-średnich. W tej chwili
wyszukiwania, eksplorowane są tylko klastry znajdujące się najbliżej zapytania. Parametr
nprobe kontroluje liczbę klastrów do eksploracji: wysokie wartości zwiększają zapamiętywanie
ale spowalniają poszukiwania.
9.3 HNSW (hierarchiczny żeglowny mały świat)
HNSW jest to najpopularniejszy algorytm we współczesnych wektorowych bazach danych. To buduje wielopoziomowy graf, w którym każdy węzeł jest wektorem połączonym z sąsiadami najbliżej. Wyszukiwanie rozpoczyna się od najwyższego poziomu (kilka węzłów, długie połączenia) i schodzi do poziomu bazowego (wszystkie węzły, krótkie połączenia), zwężając się obszar badań stopniowo.
FLAT (Brute Force):
Costruzione: O(n) | Ricerca: O(n*d) | Recall: 100%
Memoria: n*d*4 bytes | Aggiornamento: O(1)
Ideale per: <100k vettori, quando il recall perfetto e necessario
IVF (Inverted File):
Costruzione: O(n*k) | Ricerca: O(nprobe*n/k*d) | Recall: 90-99%
Memoria: n*d*4 bytes | Aggiornamento: richiede re-training
Parametri: nlist (cluster), nprobe (cluster esplorati)
Ideale per: 100k-10M vettori, quando serve controllo fine
HNSW (Hierarchical Navigable Small World):
Costruzione: O(n*log(n)) | Ricerca: O(log(n)*d) | Recall: 95-99.9%
Memoria: n*d*4 + grafo | Aggiornamento: O(log(n))
Parametri: M (connessioni), ef_construction, ef_search
Ideale per: qualsiasi scala, migliore latenza, più memoria
Product Quantization (PQ):
Comprime vettori 768-dim in ~64 bytes (12x compressione)
Recall inferiore ma enorme risparmio memoria
Combinabile con IVF: IVF-PQ per miliardi di vettori
Praktyczny przewodnik po wyborze
| Skaluj zbiór danych | Zalecany algorytm | Typowe wspomnienie |
|---|---|---|
| <100 tys. przewoźników | Płaskie (brutalna siła) | 100% |
| 100 tys. - 1 mln | HNSW | 98-99% |
| 1M - 100M | HNSW lub IVF-HNSW | 95-99% |
| 100M - 1B | IVF-PQ lub DiskANN | 90-95% |
| >1B | Rozproszone IVF-PQ (Milvus) | 85-95% |
10. Dostrajanie osadzania
Wstępnie wyszkolone modele sprawdzają się dobrze w ogólnych przypadkach użycia, ale w domenach specjalista (medycyna, prawo, finanse, kod) j dostrajanie może znacznie poprawić jakość wyszukiwania. Zasada jest prosta: dostosuj model do danych i słownictwa swojej domeny.
10.1 Kiedy dostroić
- Słownictwo specjalistyczne: Twoja domena używa terminologii technicznej, której nie ma w ogólnych danych szkoleniowych
- Różne relacje semantyczne: W Twojej domenie słowa, które zwykle nie są ze sobą powiązane, mają ze sobą bliskie powiązania
- Niedostatecznie reprezentowane języki: W modelu podstawowym nie było wystarczającej ilości tekstu w Twoim języku
- Słabe pobieranie: Wyniki wyszukiwania semantycznego nie są zadowalające pomimo dobrych fragmentów
10.2 Implementacja z uczeniem kontrastowym
from sentence_transformers import (
SentenceTransformer,
InputExample,
losses,
evaluation,
)
from torch.utils.data import DataLoader
# Carica modello base
model = SentenceTransformer("all-MiniLM-L6-v2")
# Prepara i dati di training: coppie (frase_a, frase_b, similarità)
# similarità: 1.0 = semanticamente identiche, 0.0 = completamente diverse
train_examples = [
# Coppie positive (dominio medico)
InputExample(texts=["infarto del miocardio", "attacco cardiaco acuto"], label=0.95),
InputExample(texts=["ipertensione arteriosa", "pressione alta cronica"], label=0.90),
InputExample(texts=["cefalea tensiva", "mal di testa da stress"], label=0.85),
InputExample(texts=["diabete mellito tipo 2", "diabete dell'adulto"], label=0.90),
# Coppie negative
InputExample(texts=["infarto del miocardio", "frattura del femore"], label=0.10),
InputExample(texts=["ipertensione arteriosa", "gastrite cronica"], label=0.05),
# Aggiungi migliaia di coppie per risultati ottimali...
]
# DataLoader
train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=16)
# Loss function: Cosine Similarity Loss
train_loss = losses.CosineSimilarityLoss(model)
# Valutazione (opzionale ma consigliata)
eval_examples = [
InputExample(texts=["tachicardia sinusale", "battito cardiaco accelerato"], label=0.85),
InputExample(texts=["polmonite batterica", "infezione polmonare"], label=0.80),
]
evaluator = evaluation.EmbeddingSimilarityEvaluator.from_input_examples(
eval_examples, name="medical-eval"
)
# Fine-tuning
model.fit(
train_objectives=[(train_dataloader, train_loss)],
evaluator=evaluator,
epochs=3,
warmup_steps=100,
evaluation_steps=500,
output_path="models/medical-embedding-model",
)
print("Fine-tuning completato! Modello salvato.")
# Test del modello fine-tuned
finetuned_model = SentenceTransformer("models/medical-embedding-model")
emb1 = finetuned_model.encode("infarto miocardico acuto")
emb2 = finetuned_model.encode("attacco di cuore")
from sentence_transformers import util
score = util.cos_sim([emb1], [emb2])
print(f"Similarità post fine-tuning: {score.item():.4f}")
Ile danych potrzebujesz do dostrajania?
Co do zasady: minimum 1000 par aby zobaczyć poprawę, 5 000-10 000 par dla solidnych wyników, Ponad 50 000 par dla optymalnych rezultatów. Jakość danych jest ważniejsza niż ilość: pary hałaśliwe lub źle oznakowane, degradują model. Jeśli nie masz wystarczającej ilości danych z adnotacjami, rozważ techniki takie jak twarde wydobycie negatywne lub generacja syntetyczna z LLM.
11. Osadzania dla języka włoskiego
Większość modeli osadzania jest trenowana głównie w oparciu o tekst w języku angielskim. W przypadku zastosowań w języku włoskim wybór modelu jest szczególnie krytyczny. Zobaczmy dostępne opcje i ich wydajność.
11.1 Modele wielojęzyczne
Modele wielojęzyczne trenują na kilkudziesięciu językach jednocześnie. Jakość w przypadku języka włoskiego jest ogólnie dobry, ale gorszy niż w przypadku języka angielskiego. Polecane modele dla języka włoskiego to:
- wielojęzyczny-e5-duży: Doskonała wielojęzyczna jakość, wymaga przedrostka „query:”/”passage:”
- BGE-M3: Obsługa ponad 100 języków o jednolitej jakości, obsługa gęstego+rzadkiego+colberta
- parafraza-wielojęzyczna-MiniLM-L12-v2: Dobry kompromis szybkości i jakości dla ponad 50 języków
- osadzanie tekstu-3-małe/duże (OpenAI): Dobra wielojęzyczna wydajność poprzez API
11.2 Strategie poprawy jakości
- Dostrajanie danych włoskich: Nawet kilka tysięcy par z adnotacjami w języku włoskim może znacznie poprawić wydajność
- Zwiększanie danych: Użyj LLM, aby wygenerować włoskie parafrazy swoich dokumentów
- Odzyskiwanie hybrydowe: Połącz wyszukiwanie semantyczne z BM25 (słowo kluczowe), aby zrekompensować słabości językowe modelu
- Tłumaczenie zapytania: W przypadku wysokiej jakości modeli w języku angielskim przetłumacz zapytanie na język angielski przed osadzeniem (pragmatyczne obejście)
Test praktyczny z języka włoskiego
Przed wybraniem szablonu projektu w języku włoskim zawsze przeprowadź test podręcznik z zapytaniami i prawdziwymi dokumentami. Utwórz mały zestaw danych ewaluacyjnych (50-100 zapytań z oczekiwanymi odpowiedziami) i mierzy precyzję@k i wycofanie@k. Modelka z wysokim wynikiem MTEB z języka angielskiego mógł osiągnąć znacznie gorsze wyniki na włoskim. Benchmark MTEB zawiera wielojęzyczne podzadania - sprawdź konkretnie te.
12. Osadzenie w kontekście RAG
Osadzania to fundacja każdego systemu RAG. Jakość odzyskanie – a tym samym całego rurociągu – zależy bezpośrednio od jakości reprezentacje wektorowe. W ten sposób osadzanie integruje się z architekturą RAG kompletny.
FASE DI INDICIZZAZIONE (offline):
Documenti ──> Chunking ──> [EMBEDDING MODEL] ──> Vettori ──> Vector DB
|
Stesso modello per query!
FASE DI QUERY (online):
Query utente ──> [EMBEDDING MODEL] ──> Vettore query
|
v
[VECTOR DB: similarity search]
|
v
Top-k chunk rilevanti
|
v
[LLM + Contesto] ──> Risposta con citazioni
REGOLE D'ORO:
1. Usare SEMPRE lo stesso modello per indicizzazione e query
2. La qualità degli embeddings determina la qualità del retrieval
3. Scegliere il modello in base al dominio e alla lingua
4. Normalizzare i vettori per cosine similarity
5. Monitorare retrieval precision e recall in produzione
Najczęstszym błędem w systemach RAG jest niedocenianie znaczenia wyboru model osadzania. Zespoły, które inwestują tygodnie w optymalizację podpowiedzi LLM często nigdy nie testowali różnych modeli osadzania. W praktyce przejdź all-MiniLM-L6-v2 do modelu takiego jak e5-large-v2 lub BGE-M3 może poprawić jakość odzyskania 15-25%, co ma bezpośredni wpływ na jakość ostatecznych odpowiedzi.
Lista kontrolna osadzania dla RAG
- Czy przetestowałeś co najmniej 3 różne modele w swoim zbiorze danych?
- Czy masz zestaw ewaluacyjny zawierający zapytania i oczekiwane odpowiedzi?
- Czy model obsługuje Twój język w wystarczającej jakości?
- Czy wymiarowość jest zgodna z budżetem na przechowywanie?
- Czy maksymalna długość kontekstu (maksymalny token) jest odpowiednia dla Twoich fragmentów?
- Czy zastanawiałeś się nad dostrojeniem swojej domeny?
- Czy monitorujesz jakość pobierania na produkcji?
13. Koszty i skalowanie w produkcji
Wybór pomiędzy osadzaniem na własnym serwerze a interfejsami API w chmurze ma znaczący wpływ na koszty, na opóźnienia i złożoność operacyjną. Przeanalizujmy kluczowe czynniki, które należy wziąć świadomą decyzję.
Porównanie kosztów API osadzania (2024-2025)
| Dostawcy | Model | Cena za 1M tokena | Wymiary |
|---|---|---|---|
| OpenAI | osadzanie tekstu-3-małe | 0,02 USD | 1536 |
| OpenAI | osadzanie tekstu-3-duże | 0,13 USD | 3072 |
| Przystać do siebie | osadź-v3 | 0,10 USD | 1024 |
| Hostowane samodzielnie | all-MiniLM-L6-v2 | Koszt procesora graficznego | 384 |
| Hostowane samodzielnie | BGE-M3 | Koszt procesora graficznego | 1024 |
13.1 Własny hosting a interfejs API w chmurze
- Cloud API (OpenAI, Cohere): Zero infrastruktury, płatność za użycie, opóźnienia sieci, zależność od usług zewnętrznych, prywatność danych do oceny
- Własny hosting (dedykowany procesor graficzny): Wysoki koszt początkowy (GPU ~1-3 USD/godz.), brak kosztów za token, minimalne opóźnienia, pełna kontrola danych, złożoność operacyjna
Praktyczna zasada: Za niecałe 10 milionów tokenów/miesiąc, API w chmurze są tańsze. Powyżej 100 milionów tokenów miesięcznie staje się hostingiem własnym wygodne. Od 10 M do 100 M, w zależności od wymaganego opóźnienia i potrzeb w zakresie prywatności.
13.2 Koszt przechowywania wektorów
Każdy przewoźnik zajmuje dimensioni * 4 bytes (pływak32). Z modelem 768
rozmiar i 1 milion dokumentów: 768 * 4 * 1M = ~3 GB tylko pamięci wektorowej,
do których należy dodać metadane, indeksy i narzut. Dzięki kwantyzacji produktu jest to możliwe
skompresuj do 8-16x, ale z utratą pamięci.
Szybki kosztorys
W przypadku systemu RAG z 1 milionem dokumentów, w porcjach po 500 tokenów na porcję, Model 768-dim: potrzebujesz około 3 GB RAM na wektory + 2-4 GB na indeks HNSW. Wystarczająca jest instancja w chmurze z 8 GB pamięci RAM (około 50-100 USD miesięcznie). Dla pokolenia początkowe osadzenie w API OpenAI (text-embedding-3-small): około 500M tokenów = 10 dolarów. Z modelem hostowanym samodzielnie i procesorem graficznym T4: około 2-3 godziny obliczeń = 3-5 USD.
Wnioski i dalsze kroki
W tym artykule omówiliśmy całą ewolucję osadzania tekstu: daj spokój statyczne osadzanie słów Word2Vec i GloVe poprzez kontekstowe osadzanie BERT, aż do nowoczesnych transformatorów zdań zoptymalizowanych pod kątem wyszukiwania semantycznego. Mamy widzieliśmy, dlaczego waniliowy BERT nie nadaje się do odzyskiwania i jak SBERT rozwiązuje problem z architekturą bi-enkodera.
Wdrożyliśmy kompletną wyszukiwarkę semantyczną z Pythonem, transformatorami zdań i FAISS, badając strategie przetwarzania wsadowego i dużych zbiorów danych. Porównaliśmy główne bazy danych wektorowych i algorytmy indeksowania, analizując kompromisy pomiędzy szybkością, przypominaniem i pamięcią.
Kluczowe punkty do zapamiętania:
- Wybór modelu osadzania jest najważniejszą decyzją w systemie RAG – ważniejszy niż model LLM
- Transformatory zdań (bi-enkoder) są standardem wyszukiwania; Koder krzyżowy BERT do ponownego rankingu
- Testuj coraz więcej modeli na swoim prawdziwym zbiorze danych przed dokonaniem wyboru
- FAISS i HNSW sprawiają, że wyszukiwanie wśród milionów wektorów jest wydajne i wykonalne
- Dostrajanie może radykalnie poprawić jakość wyspecjalizowanych domen
- Dla Włocha, użyj modeli wielojęzycznych (BGE-M3, wielojęzyczny-e5) i rozważ ich dostrojenie
W następnym artykule
W trzecim artykule z serii zagłębimy się w temat Baza danych wektorowych: architektura wewnętrzna, zaawansowana konfiguracja indeksu, strategie shardingu i replikacja, filtrowanie metadanych oraz wybór odpowiedniej bazy danych Twój przypadek użycia. Zobaczymy kompletne wdrożenia z Qdrant, ChromaDB i pgvector.







