Erişim Optimizasyonu: Tokenizasyondan Vektör Nicemlemeye
Deeplearning.ai’daki “Retrieval Optimization: From Tokenization to Vector Quantization” kursunun Türkçe özetidir.
For English:
“Tokenizasyon, kullandığımız modellerin gerçekten kritik ama sıklıkla göz ardı edilen ve yeterince takdir edilmeyen yönlerinden biridir. ”-Andrew NG.
Bu kurs, RAG (Retrieval-Augmented Generation) ve arama uygulamalarının iyileştirilmesi ve optimize edilmesine odaklanmaktadır. Kursa, büyük dil modellerinde (LLM’ler) ve gömme (embedding) modellerinde nasıl tokenleştirme yapıldığını ve bir tokenleştiricinin arama kalitesini nasıl etkileyebileceğini öğrenerek başlıyoruz. Tokenleştiriciler, genellikle bir metindeki kelimelere veya kelime parçalarına karşılık gelen tamsayılarla temsil edilen sayısal kimlik dizileri oluşturur. Bir metni token dizisine dönüştürmenin birden fazla yolu vardır. Örneğin, bir kelime tokenlayıcı (word tokenizer), cümleleri kelime kelime tokenize ederken, bir alt kelime tokenlayıcı (subword tokenizer), karakterleri veya karakter kombinasyonlarını tokenize eder.
Bu kursta, workpiece byte-pair encoding ve unigram tokenization gibi tokenizasyon tekniklerini ve token sözlüğünün, özellikle emoji, yazım hataları ve sayısal değerler gibi özel karakterlerde arama sonuçlarını nasıl etkilediğini öğreneceğiz.
Sisteminizi optimize etmenin ilk adımı, çıktılarının kalitesini ölçmektir. Bu yüzden bu yazıda, arama kalitemizi çeşitli kalite metrikleri kullanarak değerlendirmeyi öğreneceğiz. Vektör veritabanları, en yakın komşuların aramasını yaklaşık olarak gerçekleştirmek için özel veri yapıları kullanır. Hiyerarşik Gezinilebilir Küçük Dünya (HNSW: Hierarchical Navigable Small World), en yaygın kullanılanlardan biridir ve size yaklaşık sonuçların ne kadar iyi olacağını kontrol etme imkanı veren bazı parametrelere sahiptir. HNSW araması, çok katmanlı bir grafik üzerine inşa edilmiştir; tıpkı bir paketi posta yoluyla göndermek gibidir. En üst katman sizi paketin teslim edileceği eyalete yaklaştırır, bir alt katman ise şehri bulur ve katmanlar arasında aşağı indikçe hedefe giderek daha fazla yaklaşırsınız. Daha yüksek hız ve maksimum alaka düzeyi için HNSW grafiğinin oluşturulması ve aranması için parametrelerin nasıl dengeleneceğini göreceğiz.
Eğer milyonlarca vektör aramanız gerekiyorsa, bunları depolamak, indekslemek ve aramak kaynak yoğun hale gelebilir ve yavaşlayabilir. Örneğin, bir endüstriyle ilgili tüm haberleri toplayıp özetleyen bir haber analiz uygulaması geliştirdiğinizi varsayalım. Her gün yayınlanan binlerce makaleyi bölerek, bir ay içinde milyonlarca vektör embedding’e ulaşabilirsiniz. Eğer 5 milyon vektörünüz varsa ve OpenAI’nin yaklaşık 1500'den fazla boyuta sahip bir vektör oluşturan Ada embedding modelini kullanıyorsanız, yaklaşık 30 GB belleğe ihtiyacınız olacaktır ve bu miktar her ay artmaya devam edecektir. İşte bu noktada nicemleme teknikleri devreye girmektedir.
Bu kursta öğrendiğiniz teknikleri kullanarak, vektör aramanız için gereken belleği 64 kata kadar azaltabilirsiniz. Üç ana nicemleme tekniğini öğreneceksiniz. İlki, “product quantization” (ürün nicemleme). Bu teknik, alt sektörleri en yakın centroid’e (merkeze) eşleyerek bellek kullanımını azaltır veya indeksleme süresini artırır. İkincisi, her bir float değerini 1 baytlık tam sayılara dönüştüren “scalar quantization” (sayısal nicemleme) tekniğidir. Bu, belleği önemli ölçüde azaltır ve hem indeksleme hem de arama işlemlerini hızlandırır, ancak hassasiyeti biraz düşürür. Üçüncü teknik ise, “binary quantization” (ikili nicemleme) olarak adlandırılan, aşırı bir nicemleme şeklidir ve float değerlerini Boolean (mantıksal) değerlere dönüştürür. Bu, bellek kullanımını önemli ölçüde iyileştirir ve arama hızını artırır, ancak hassasiyet açısından daha büyük kayıplara neden olur.
Gömme Modelleri (Embedding Models)
İçerik gömme (embedding) modellerinin iç yapısını öğrenelim. Metinlerin, bir gömme modeli aracılığıyla birden fazla katman kullanılarak nasıl vektörlere dönüştürüldüğünü göreceğiz. Eğer gömme modeliniz bilimsel makaleler üzerinde eğitildiyse ve siz Tweet’ler üzerinde çalışmaya çalışıyorsanız, bu durum modelin doğruluğunu olumsuz yönde etkileyecektir. Gömme modeli bir metni alır ve sabit boyutlu bir vektör oluşturur, böylece herhangi iki belge arasındaki mesafeyi karşılaştırabilir ve benzerliklerini belirleyebiliriz.
Gömme modelleri metin üzerinde doğrudan çalışmazlar. Bunun yerine, metni daha küçük parçalara ayırmak için tokenizasyon adı verilen ek bir işleme ihtiyaç duyarlar. Transformer mimarisinde bir kodlayıcı (encoder) ve bir çözücü (decoder) bulunur. Gömme modelleri yalnızca kodlayıcı transformer mimarilerini kullanır. Modelin girdileri, gömme modellerinde girdi gömmelerine eşlenir ve her girdinin eğitim sürecinde öğrenilen karşılık gelen bir gömme vektörü vardır. Bu, transformer’ların yalnızca eğitim sırasında öğrendikleri girdi sembollerini işleyebileceği anlamına gelir. Gömme modelleri söz konusu olduğunda, yalnızca eğitim verilerinde karşılaştıkları metin birimleri ile çalışabilirler. Tokenizer’lar, verilen bir metinde görülen token’lara karşılık gelen bir dizi sayısal ID üretir. Bu ID’ler, tokenizer ve ardından kullanılan model arasında bir tür anlaşma tanımlar. Bu nedenle, eğitim tamamlandıktan sonra bile bileşenleri kolayca değiştiremeyiz, çünkü ID’ler önemlidir.
En temel yaklaşım, modelin doğrudan karakter veya bayt seviyesinde çalışmasına izin vermek olacaktır; bu, küçük bir kelime dağarcığına ve daha fazla belirsizliğe yol açar. Kelime dağarcığının boyutu, model tarafından öğrenilen girdi gömmelerinin sayısını da etkiler. Ancak her karakter farklı bağlamlarda ortaya çıkabilir, bu nedenle gömme anlamlı olmayabilir. Ağın parametreleri, harfler arasındaki ilişkiyi öğrenmeli ve dizinin ne anlama geldiğini çıkarmalıdır.
Karakter tabanlı tokenizasyondan farklı olarak, her kelimenin kendi tanımlayıcısına sahip olduğu ve modeli bunların tamamını kapsamak için daha fazla gömme öğrenmesi gereken kelime token’larını kullanabiliriz. Bu, daha fazla eğitim verisi ve eğitime harcanan süre demektir. Ayrıca, hiç görülmemiş bir kelimeyi bir token dizisi olarak temsil edemeyiz çünkü yalnızca tam kelimeleri kapsardık. Bu durum, karakter veya bayt seviyesi tokenizasyon ile asla gerçekleşmez. Bu fikir, dilin bir yapıya sahip olduğunu ve belirli kelimelerin anlam taşıyan ortak parçalara sahip olabileceğini göz ardı eder.
Alt kelime (subword) seviyesinde tokenizasyon, bu iki temel yaklaşım arasında bir uzlaşma gibi görünmektedir. Örneğin, aynı kök forma sahip kelimeler anlam açısından benzerlik gösterir. Bu yaklaşım, kök bulma (stemming) veya kök ayrıştırma (lemmatization) gibi doğal dil işleme (NLP) tabanlıdır. İstatistiklere dayalı olarak eğitilebilir. Örneğin: yürü|yüş|yürü|dü. Ancak, diğer diller için bu yöntem daha kötü performans gösterebilir.
Metni gömmelere dönüştürme sürecini adım adım izleyelim. Öncelikle, seçilen tokenizer kullanılarak token’lara ayırıyoruz. Ardından tokenizer, bir dizi token ID’si üretir ve bunları modele gönderir. Model, her bir token ID’sini öğrenilmiş girdi gömmesine eşler. Transformer’ın ilk adımı bir arama tablosudur. Farklı metinler için girdi gömmelerinin nasıl görüneceğini görmek ilginçtir. Mevcut cümle gömmelerinden birini inceleyelim.
import warnings
warnings.filterwarnings('ignore')
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
print(model)
Bu model bir dönüştürücü, bir anlam birleştirme katmanı ve normalizasyondan oluşur. Bu çıktı, aynı zamanda önemli olan tokenleştirici bileşenini gizler. Metni, modelin tokenlenmiş bir yöntemini kullanarak tokenleştirebilirsiniz. Aynı anda birden fazla metin kabul eder ve bir dizi dizenin geçirilmesini gerektirir.
tokenized_data = model.tokenize(["walker walked a long walk"])
print(tokenized_data)
Girdi kimliklerinin bir listesini kullanmak, girdi metninin parçalara nasıl bölündüğünü denetlemenin en basit veya insan dostu yolu değildir. Modelin tokenleyici ID’leri tokenlere dönüştürme yöntemine karşılık gelen kimliklerin bir listesini geçirerek tokenlerin bir listesini görebilirsiniz.
print(model.tokenizer.convert_ids_to_tokens(tokenized_data["input_ids"][0]))
Örneğimizdeki her bir kelime, tokenleyici tarafından öğrenilen karşılık gelen bir belirtece sahiptir. Ayrıca köşeli parantezlerle işaretlenmiş bazı teknik tokenler de vardır. CLS ve SEP her bir diziyi başlatır ve sonlandırır.
# Transformer birden fazla stack modülünden oluşur. Token'lar ilkinin girdisidir, bu yüzden geri kalanını görmezden gelebiliriz.
first_module = model._first_module()
print(first_module.auto_model)
# girdi tokeni gömmeleri
embeddings = first_module.auto_model.embeddings
print(embeddings)
Kelime gömmeleri kelimelerin anlamını yakalar.
import torch
import plotly.express as px
device = torch.device("mps" if torch.has_mps else "cpu") # Apple için MPS, diğerleri için CUDA kullanın veya CPU'ya geri dönün
first_sentence = "vector search optimization"
second_sentence = "we learn to apply vector search optimization"
with torch.no_grad():
# Her iki metni de tokenleştiriciler
first_tokens = model.tokenize([first_sentence])
second_tokens = model.tokenize([second_sentence])
# İlgili gömmeleri alın
first_embeddings = embeddings.word_embeddings(
first_tokens["input_ids"].to(device)
)
second_embeddings = embeddings.word_embeddings(
second_tokens["input_ids"].to(device)
)
first_embeddings.shape, second_embeddings.shape
from sentence_transformers import util
distances = util.cos_sim(
first_embeddings.squeeze(),
second_embeddings.squeeze()
).cpu().numpy() # Tensörü CPU'ya taşıyın ve NumPy dizisine dönüştürün
px.imshow(
distances,
x=model.tokenizer.convert_ids_to_tokens(
second_tokens["input_ids"][0]
),
y=model.tokenizer.convert_ids_to_tokens(
first_tokens["input_ids"][0]
),
text_auto=True,
)
Kelime gömme dizisi herhangi bir konumsal bilgi içermez. Bunu çözmek için pozisyon gömmeleri kullanıyoruz ve bunun sonucunda, tüm metindeki token’lar arasındaki ilişkileri de yakalayabilen, biraz değiştirilmiş bir token gömme seti elde ediyoruz. Konumsal kodlamalar genellikle sinüs fonksiyonu ile üretilir. Hem girdi token gömmeleri hem de konumsal kodlamalar toplandıktan sonra, bunları girdi gömmelerini alarak bazı çıktı gömmeleri üreten bir dizi üst üste yığılmış katmandan geçiririz. İçsel olarak, bu modüllerin her biri, girdi gömmeleri arasındaki ilişkileri belirlemek için dikkat mekanizmaları kullanır; bu nedenle, tüm token’lar arası bilgiler burada yakalanır. Her model aynı anda yalnızca belirli bir sayıda token alabilir. Ayrıca, konumsal kodlamalar da sabit boyutlu bir matris içinde tutulduklarından yalnızca belirli bir sayıda token’ı aynı anda alabilir. Modeliniz 256 token üzerinde çalışacak şekilde eğitildiyse, daha uzun dizilerle çalışamaz. Genellikle, maksimum uzunluk aşıldığında tokenizer son token’ları düşürür. Girdi token gömmeleri bağlamdan bağımsızdır ve aynı zamanda transformer modelinin parametreleridir. Model, eğitim aşamasında her bir token’ın anlamını en iyi şekilde temsil edecek şekilde girdi token’larını öğrenir. Token gömmeleri bağlamdan bağımsız olduğundan, her bir vektörü karşılık gelen token ile eşleyebiliriz.
Kelime dağarcığımızda 3 grup token bulunuyor. Modele özgü teknik token’lar, ##ön eklerine sahip alt kelime token’ları ve ## dışında bir şeyle başlayan kelimeler.
# girdi gömmelerini görselleştirme
token_embeddings = first_module.auto_model \
.embeddings \
.word_embeddings \
.weight \
.detach() \
.cpu() \
.numpy()
token_embeddings.shape
import random
vocabulary = first_module.tokenizer.get_vocab()
sorted_vocabulary = sorted(
vocabulary.items(),
key=lambda x: x[1], # sözlük girdisinin değerini kullanır
)
sorted_tokens = [token for token, _ in sorted_vocabulary]
random.choices(sorted_tokens, k=100)
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2, metric="cosine", random_state=42)
tsne_embeddings_2d = tsne.fit_transform(token_embeddings)
tsne_embeddings_2d.shape
token_colors = []
for token in sorted_tokens:
if token[0] == "[" and token[-1] == "]":
token_colors.append("red")
elif token.startswith("##"):
token_colors.append("blue")
else:
token_colors.append("green")
import plotly.graph_objs as go
scatter = go.Scattergl(
x=tsne_embeddings_2d[:, 0],
y=tsne_embeddings_2d[:, 1],
text=sorted_tokens,
marker=dict(color=token_colors, size=3),
mode="markers",
name="Token embeddings",
)
fig = go.FigureWidget(
data=[scatter],
layout=dict(
width=600,
height=900,
margin=dict(l=0, r=0),
)
)
fig.show()
# çıktı tokenleri gömmeleri
output_embedding = model.encode(["walker walked a long walk"])
output_embedding.shape
output_token_embeddings = model.encode(
["walker walked a long walk"],
output_value="token_embeddings"
)
output_token_embeddings[0].shape
first_sentence = "vector search optimization"
second_sentence = "we learn to apply vector search optimization"
with torch.no_grad():
first_tokens = model.tokenize([first_sentence])
second_tokens = model.tokenize([second_sentence])
first_embeddings = model.encode(
[first_sentence],
output_value="token_embeddings"
)
second_embeddings = model.encode(
[second_sentence],
output_value="token_embeddings"
)
distances = util.cos_sim(
first_embeddings[0],
second_embeddings[0]
)
px.imshow(
distances.cpu().numpy(), # Tensörü CPU'ya taşıyın ve NumPy dizisine dönüştürün
x=model.tokenizer.convert_ids_to_tokens(
second_tokens["input_ids"][0]
),
y=model.tokenizer.convert_ids_to_tokens(
first_tokens["input_ids"][0]
),
text_auto=True,
)
Tokenleştiricilerin Rolü (Role of the Tokenizers)
Bu derste, birkaç tokenleştirici eğiteceğiz. Transformatör modelinin girdi metnini nasıl işlediğini belirledikleri için, onlara ekstra ilgi göstermek önemlidir. Çeşitli tokenleştirici algoritmaları mevcuttur. LLM eğitiminin aksine, tokenleştirici eğitimi tamamen belirleyicidir ve girdi verilerinin istatistiklerine dayanır.
Çeşitli modeller farklı tokenleştirme yöntemleri seçer. OpenAI kodlama başına bayt tercih eder, Cohere Egnlish V3 WordPiece’i tercih eder, Cohere Multilingual V3 Unigram’ı tercih eder ve Sentence Transformer all-MiniLM-L6-V2 WordPiece’i kullanır. Kelime dağarcığı boyutu, önceden seçmemiz gereken bir parametredir ve genellikle en az 30k tokendir ve çok dilli modeller için bu, 200k token gibi birkaç kat daha fazla olabilir.
En popüler tokenleştirme algoritmalarını kontrol edin ve metni nasıl böldüklerini görün.
BPE (Byte Pair Encoding)
BPE, LLM’lerde yaygın bir tercihtir. BPE’de kelimeler genellikle boşluk karakterleriyle ayrılır ve ardından karakterlere veya baytlara bölünür. Yeni token’lar, yan yana en sık kullanılan iki token’ın birleştirilmesiyle yinelemeli olarak oluşturulur. En yaygın kısım seçilir ve kelime hazinesine eklenir, ancak onu oluşturmak için kullanılan token’lar silinmez.
Bir tokenizer, belirli bir tokenizasyon modelinin argüman olarak geçirilmesini gerektiren genel bir bileşendir. Ayrıca her bir girdi metni üzerinde tokenizasyona başlamadan önce uygulanacak bazı tokenizasyon ayarlarını yapmaya olanak tanır. Boşluk bazlı ön-tokenizasyon, metni kelimelere ayırmaya yardımcı olur. Ayarlayacağınız son şey, trainer nesnesi ve hedef kelime hazinesinin boyutudur.
Şimdi, bir Python iteratöründen bir eğitim setini ilgili bir trainer örneği ile geçirerek tokenizer’ı eğiteceksiniz. Daha sonra, öğrenilen token’ların neler olduğunu görmek için kelime hazinesini alabilirsiniz. Eğitim süreci yinelemeli olduğundan, yeni token’ların kelime hazinesine eklenme sırasını, basitçe ID’lerini kontrol ederek görebiliriz.
training_data = [
"walker walked a long walk",
]
from tokenizers.trainers import BpeTrainer
from tokenizers.models import BPE
from tokenizers import Tokenizer
from tokenizers.pre_tokenizers import Whitespace
bpe_tokenizer = Tokenizer(BPE())
bpe_tokenizer.pre_tokenizer = Whitespace()
bpe_trainer = BpeTrainer(vocab_size=14) # Genellikle birkaç bin ama demo için 14
bpe_tokenizer.train_from_iterator(training_data, bpe_trainer)
bpe_tokenizer.get_vocab()
bpe_tokenizer.encode("walker walked a long walk").tokens
# ['walke', 'r', 'walke', 'd', 'a', 'l', 'o', 'n', 'g', 'walk']
bpe_tokenizer.encode("wlk").ids
# [9, 5, 4]
bpe_tokenizer.encode("wlk").tokens
# ['w', 'l', 'k']
bpe_tokenizer.encode("she walked").tokens # s ve h eğitim aşamasında hiç gerçekleşmedi bu yüzden bunlar atlandı
# ['e', 'walke', 'd']
WordPiece
BPE’ye benzer ve sıklıkla gömme modellerinde kullanılır. WordPiece, ilk harfleri orta harflerden, çift karma öneki ekleyerek ayırır. Genel fikir, kelimeleri, önekleri ve sonekleri ayrı ayrı öğrenmektir.
Eğitim süreci, her kelimenin harflerinden ve çift karma öneki eklenmiş kelimelerin orta harflerinden oluşturulan bir kelime dağarcığıyla başlar. Daha sonra algoritma, token çiftlerini yinelemeli olarak birleştirir ancak birleştirilecek tokenleri, BPE’ye kıyasla farklı olan puana göre seçer. Artık en sık görülen kısmı seçmiyoruz, ayrıca bu iki belirtecin diğer bağlamlarda ne sıklıkla meydana geldiğini de dikkate alıyoruz. İlk yinelemede her token çifti için puanları hesaplarsak, iki token her zaman yan yana meydana gelirse, birleştirme için seçilecekleri ortaya çıkar. BPE’ye benzer şekilde, yinelemeli olarak yeni tokenler ekliyoruz. İşlem, istenen token boyutuna ulaştığında duracaktır.
score(u,v) = frequency(uv) / (frequency(u)*frequency(v))
WordPiece tüm tek harfleri ve orta harfleri ayrı tokenler olarak eklediğinden, bu düzenlilikleri yakalamak için genellikle daha büyük bir sözlüğe ihtiyaç duyarız.
from real_wordpiece.trainer import RealWordPieceTrainer
from tokenizers.models import WordPiece
real_wordpiece_tokenizer = Tokenizer(WordPiece())
real_wordpiece_tokenizer.pre_tokenizer = Whitespace()
real_wordpiece_trainer = RealWordPieceTrainer(
vocab_size=27,
)
real_wordpiece_trainer.train_tokenizer(
training_data, real_wordpiece_tokenizer
)
real_wordpiece_tokenizer.get_vocab()
real_wordpiece_tokenizer.encode("walker walked a long walk").tokens
real_wordpiece_tokenizer.encode("wlk").tokens
# Aşağıdaki satır bilinmeyen karakterler içerdiğinden bir hata üretecektir. Lütfen satırın yorumunu kaldırın ve hatayı görmek için çalıştırın.
# real_wordpiece_tokenizer.encode("she walked").tokens
HuggingFace WordPiece ve Özel Tokenler (HuggingFace WordPiece and Special Tokens)
from tokenizers.trainers import WordPieceTrainer
unk_token = "[UNK]"
wordpiece_model = WordPiece(unk_token=unk_token)
wordpiece_tokenizer = Tokenizer(wordpiece_model)
wordpiece_tokenizer.pre_tokenizer = Whitespace()
wordpiece_trainer = WordPieceTrainer(
vocab_size=28,
special_tokens=[unk_token]
)
wordpiece_tokenizer.train_from_iterator(
training_data,
wordpiece_trainer
)
wordpiece_tokenizer.get_vocab()
wordpiece_tokenizer.encode("walker walked a long walk").tokens
wordpiece_tokenizer.encode("wlk").tokens
wordpiece_tokenizer.encode("she walked").tokens
Unigram
BPE ve WordPiece’in aksine, Unigram eğitim sürecinde azaltılan devasa bir kelime dağarcığıyla başlar. Unigram devasa bir kelime dağarcığıyla başlar ve daha sonra hesaplanan kayba göre bazı tokenleri kaldırır. Başlangıçtaki kelime dağarcığı tek bir kelimeyi birden fazla şekilde tokenleştirmeye olanak tanır.
Tüm olası tokenleştirmelerin kümesi, Unigram eğitim sürecindeki kaybı hesaplamak için önemlidir. Her yinelemede, algoritma belirli bir token kaldırılırsa genel kaybın ne kadar artacağını hesaplar ve bunu en az artıracak tokenleri arar. Olasılıklar tokenlerin sıklıkları ile tanımlanır.
Toplamda 63 token oluşumu var. Bu, belirli bir tokenleştirmenin olasılığını kullanılan tüm tokenlerin frekanslarının bir ürünü olarak hesaplayabileceğimiz anlamına geliyor. Bu basit örnekte bile, dikkate alınması gereken tokenleştirme sayısı oldukça önemlidir. Unigram eğitimi, her bir tokeni kaldırmanın kaybı nasıl etkileyeceğini kontrol edecek ve ardından kayıp değeri üzerinde en küçük etkiye sahip olan kaldırılabilecek olanı seçecektir. Unigram eğitim sürecinin her adımını analiz etmek çok sayıda hesaplama gerektirecektir. Unigram her adımda birkaç token kaldırır ve yalnızca kelime dağarcığının boyutunun istenenden daha büyük olmayacağını garanti eder. Ortak öneklerin önemli olduğu bulundu, bu nedenle tokenleştirmemizin anlamı doğru bir şekilde yakalayabileceğini umuyoruz. Yalnızca temel alfabe ve kelimelerin ortak öneklerine sahibiz, çift harfli token yok.
from tokenizers.trainers import UnigramTrainer
from tokenizers.models import Unigram
unigram_tokenizer = Tokenizer(Unigram())
unigram_tokenizer.pre_tokenizer = Whitespace()
unigram_trainer = UnigramTrainer(
vocab_size=14,
special_tokens=[unk_token],
unk_token=unk_token,
)
unigram_tokenizer.train_from_iterator(training_data, unigram_trainer)
unigram_tokenizer.get_vocab()
unigram_tokenizer.encode("walker walked a long walk").tokens
unigram_tokenizer.encode("wlk").tokens
unigram_tokenizer.encode("she walked").tokens
# ['sh', 'e', 'walke', 'd']
unigram_tokenizer.encode("she walked").ids
SentencePiece, metin hakkında ek bir varsayımla, aynı tokenleme algoritmalarının bir uygulamasıdır, kendisi farklı bir algoritma değildir. SentencePiece, boşluklarla metin oluşturmaz, ancak bunları diğer karakterler gibi ele alır. Bu, bunları ayırıcı olarak kullanmayan diller için çalışmasını sağlar. SentencePiece, dahili olarak kelime dağarcığını oluşturmak için kodlama başına bayt veya Unigram kullanır. SentencePiece, boşluklar dahil olmak üzere birden fazla kelimeyi kapsayan tokenler oluşturmaya izin verir. Python gibi bir kod tokenleyici oluşturuyorsanız veya San Fransisco veya Real Madrid gibi benzersiz adlar kullanıyorsanız bu yaklaşım önemlidir.
Tokenizasyonun Pratik Etkileri (Practical Implications of the Tokenization)
Yalnızca vektör aramasının revize edilmesi gerekebilir. Karşılaşabileceğiniz en yaygın zorluklardan bazılarının üzerinde çalışacak ve vektör veritabanlarını etkili bir şekilde kullanarak bunları nasıl ele alacağınızı göreceksiniz. Eğitim verilerinde belirli bir token birçok farklı bağlamda ortaya çıkarsa, bunun net bir anlamı yoktur. Bu, genellikle birden fazla bağlamda ortaya çıkan alt sözcük tokenleri için özellikle geçerlidir.
BPE makalesi, test setini kapsayan daha az belirtecin daha iyi sonuçlar verebileceğini öne sürüyor. Yazım hataları, tarihler, sayısal değerler ve emojiler, işe yaramayabilecek sorunlu noktalardır. OpenAI bunu bayt düzeyinde kodlamayla halleder.
from sentence_transformers import SentenceTransformer, util
sbert_model = SentenceTransformer("all-MiniLM-L6-v2")
out_vector = sbert_model.encode("Vector search optimization")
out_vector.shape
sbert_tokenizer = sbert_model.tokenizer._tokenizer
sbert_tokenizer
sbert_tokenizer.encode("I feel 😊").tokens
# ['[CLS]', 'i', 'feel', '[UNK]', '[SEP]']
sbert_tokenizer.encode("I feel happy").tokens
sbert_tokenizer.encode("Broadcom BCM2712").tokens
#['[CLS]', 'broad', '##com', 'bc', '##m', '##27', '##12', '[SEP]']
sentences = [
"Great accommodation",
"Great acommodation", # yazım yanlışı nedeniyle, tokenleştirme farklıdır
]
for sentence in sentences:
print(sbert_tokenizer.encode(sentence).tokens)
sentences = [
"This shirt costs $55.",
"This shirt costs fifty five dollars.",
"This shirt costs $50.",
"This shirt costs $559.",
"This shirt has a 10% discount from $60.",
]
for sentence in sentences:
print(sbert_tokenizer.encode(sentence).tokens)
sentences = [
"16th February 2024",
"2024-02-16",
"17th February 2024",
"18th February 2024",
"19th February 2024",
"20th February 2024",
"15th February 2024",
]
for sentence in sentences:
print(sbert_tokenizer.encode(sentence).tokens)
# Anlamsal benzerlik üzerindeki etkiler
import plotly.express as px
sentences = [
"I feel 😊",
"I feel happy",
"I feel 🙁",
"I feel sad",
]
embeddings = sbert_model.encode(sentences)
cosine_scores = util.cos_sim(embeddings, embeddings)
px.imshow(
cosine_scores,
x=sentences,
y=sentences,
text_auto=True,
)
# Yazım hataları
sentences = [
"Great accommodation",
"Great acommodation",
]
embeddings = sbert_model.encode(sentences)
cosine_scores = util.cos_sim(embeddings, embeddings)
px.imshow(
cosine_scores,
x=sentences,
y=sentences,
text_auto=True,
)
# Sayısal değerler ve tarih/saat
sentences = [
"This shirt costs $55.",
"This shirt costs fifty five dollars.",
"This shirt costs $50.",
"This shirt costs $559.",
"This shirt has a 10% discount from $60.",
]
embeddings = sbert_model.encode(sentences)
cosine_scores = util.cos_sim(embeddings, embeddings)
fig = px.imshow(
cosine_scores,
x=sentences,
y=sentences,
text_auto=True,
)
fig.update_xaxes(tickangle=-30)
sentences = [
"16th February 2024",
"2024-02-16",
"17th February 2024",
"18th February 2024",
"19th February 2024",
"20th February 2024",
"15th February 2024",
]
embeddings = sbert_model.encode(sentences)
cosine_scores = util.cos_sim(embeddings, embeddings)
fig = px.imshow(
cosine_scores,
x=sentences,
y=sentences,
text_auto=True,
)
fig.update_layout(
xaxis={"type": "category"},
yaxis={"type": "category"}
)
fig.update_xaxes(tickangle=-30)
# Farklı modeller üzerindeki etkisi
import tiktoken
openai_tokenizer = tiktoken.encoding_for_model("text-embedding-3-large")
openai_tokenizer.n_vocab
token_ids = openai_tokenizer.encode("I feel 😊")
openai_tokenizer.decode_tokens_bytes(token_ids)
token_ids = openai_tokenizer.encode("I feel happy")
openai_tokenizer.decode_tokens_bytes(token_ids)
token_ids = openai_tokenizer.encode("Broadcom BCM2712")
openai_tokenizer.decode_tokens_bytes(token_ids)
sentences = [
"Great accommodation",
"Great acommodation",
]
for sentence in sentences:
token_ids = openai_tokenizer.encode(sentence)
print(openai_tokenizer.decode_tokens_bytes(token_ids))
sentences = [
"This shirt costs $55.",
"This shirt costs fifty five dollars.",
"This shirt costs $50.",
"This shirt costs $559.",
"This shirt has a 10% discount from $60.",
]
for sentence in sentences:
token_ids = openai_tokenizer.encode(sentence)
print(openai_tokenizer.decode_tokens_bytes(token_ids))
sentences = [
"16th February 2024",
"2024-02-16",
"17th February 2024",
"18th February 2024",
"19th February 2024",
"20th February 2024",
"15th February 2024",
]
for sentence in sentences:
token_ids = openai_tokenizer.encode(sentence)
print(openai_tokenizer.decode_tokens_bytes(token_ids))
# Vektör benzerliği
from openai import OpenAI
from helper import get_openai_api_key
openai_client = OpenAI(api_key=get_openai_api_key())
sentences = [
"I feel 😊",
"I feel happy",
"I feel 🙁",
"I feel sad",
]
embeddings = [
embedding.embedding
for embedding in openai_client.embeddings.create(
input=sentences, model="text-embedding-3-large"
).data
]
cosine_scores = util.cos_sim(embeddings, embeddings)
px.imshow(
cosine_scores,
x=sentences,
y=sentences,
text_auto=True,
)
sentences = [
"Great accommodation",
"Great acommodation",
]
embeddings = [
embedding.embedding
for embedding in openai_client.embeddings.create(
input=sentences, model="text-embedding-3-large"
).data
]
cosine_scores = util.cos_sim(embeddings, embeddings)
fig = px.imshow(
cosine_scores,
x=sentences,
y=sentences,
text_auto=True,
)
fig.update_xaxes(tickangle=-30)
sentences = [
"This shirt costs $55.",
"This shirt costs fifty five dollars.",
"This shirt costs $50.",
"This shirt costs $559.",
"This shirt has a 10% discount from $60.",
]
embeddings = [
embedding.embedding
for embedding in openai_client.embeddings.create(
input=sentences, model="text-embedding-3-large"
).data
]
cosine_scores = util.cos_sim(embeddings, embeddings)
fig = px.imshow(
cosine_scores,
x=sentences,
y=sentences,
text_auto=True,
)
fig.update_xaxes(tickangle=-30)
sentences = [
"16th February 2024",
"2024-02-16",
"17th February 2024",
"18th February 2024",
"19th February 2024",
"20th February 2024",
"15th February 2024",
]
embeddings = [
embedding.embedding
for embedding in openai_client.embeddings.create(
input=sentences, model="text-embedding-3-large"
).data
]
cosine_scores = util.cos_sim(embeddings, embeddings)
fig = px.imshow(
cosine_scores,
x=sentences,
y=sentences,
text_auto=True,
)
fig.update_layout(
xaxis={"type": "category"},
yaxis={"type": "category"}
)
fig.update_xaxes(tickangle=-30)
# Uygulamada vektör araması
from qdrant_client import QdrantClient, models
client = QdrantClient("http://localhost:6333")
client.get_collections()
examples = [
("Sleeveless maxi dress with a V-neckline and wrap front."
"Comes with a tie belt at the waist.", 29.95),
("Slim-fit jeans in washed stretch denim with a button fly "
"and tapered legs.", 39.99),
("Double-breasted blazer in textured-weave fabric with peak "
"lapels and front flap pockets.", 59.50),
("Lightweight bomber jacket with ribbed cuffs, a baseball collar, "
"and zip front closure.", 45.00),
("Chunky knit sweater with dropped shoulders and ribbed trim around "
"the neck, cuffs, and hem.", 25.99),
("Tailored trousers in a smooth woven fabric with a concealed "
"hook-and-eye closure and welt back pockets.", 34.99),
("Classic trench coat with adjustable belt, storm flap, and button "
"front closure.", 79.90),
("High-rise pencil skirt in a stretch fabric with a hidden rear zip "
"and back slit.", 22.99),
("Athletic-fit polo shirt in moisture-wicking fabric with a ribbed "
"collar and two-button placket.", 19.95),
("Soft flannel pajama set with long sleeves, matching pants, and a "
"comfortable elastic waistband.", 32.00),
("Quilted puffer jacket with a detachable hood and zippered side "
"pockets.", 48.99),
("Cropped denim jacket with distressed details and button-flap chest "
"pockets.", 36.50),
("Fitted bodysuit with a scoop neckline and snap-button closure at "
"the bottom.", 15.99),
("Lightly padded parka with a faux fur-lined hood, drawstring waist, "
"and snap front pockets.", 69.95),
("Mesh panel sports leggings with a high waist and reflective details "
"for nighttime visibility.", 27.99),
("Button-up cardigan in a soft knit with long sleeves and ribbed "
"trim.", 24.50),
("Leather moto jacket with zippered cuffs, a notched collar, and "
"asymmetrical zip closure.", 95.00),
("Velvet slip dress with a lace trim neckline and adjustable "
"spaghetti straps.", 31.99),
("Cargo shorts with multiple pockets and a durable belt loop "
"waistband.", 22.95),
("Wide-leg palazzo pants with a high-rise fit and side zip "
"closure.", 38.99),
("Graphic print tee featuring an original artwork design and classic "
"crew neck.", 14.99),
("Boho-style maxi skirt with an elastic waistband and tiered ruffle "
"detailing.", 33.50),
("Men's linen shirt with a Mandarin collar and buttoned chest "
"pocket.", 29.95),
("Cable knit beanie with a fold-over cuff and soft fleece "
"lining.", 12.99),
("Sequin cocktail dress with a plunging V-neck and bodycon "
"fit.", 49.99),
]
client.delete_collection("clothes")
client.create_collection(
"clothes",
vectors_config=models.VectorParams(
size=384,
distance=models.Distance.COSINE,
)
)
import uuid
client.upsert(
"clothes",
points=[
models.PointStruct(
id=uuid.uuid4().hex,
vector=sbert_model.encode(description),
payload={"description": description, "price": price},
)
for description, price in examples
]
)
client.search(
"clothes",
query_vector=sbert_model.encode("for cold weather"),
with_payload=True,
limit=3,
)
# Ek kısıtlamalarla vektör araması
client.search(
"clothes",
query_vector=sbert_model.encode("for cold weather under $40"),
with_payload=True,
limit=3,
)
client.create_payload_index(
"clothes",
field_name="price",
field_schema=models.PayloadSchemaType.FLOAT,
)
client.search(
"clothes",
query_vector=sbert_model.encode("for cold weather"),
with_payload=True,
limit=3,
query_filter=models.Filter(
must=[
models.FieldCondition(
key="price",
range=models.Range(
lte=40.0,
)
)
]
)
)
Arama Alakalılığını Ölçme (Measuring Search Relevance)
RAG’de bilgi alma için araçları ve en sık kullanılan ölçümleri öğreneceğiz. Ölçemediğinizi iyileştiremezsiniz. Sistem çıktılarının kalitesini ölçmek, optimizasyonda yapılacak ilk şeydir. İlk olarak, en iyi eşleşen belgelere sahip sorgular içeren çiftler içeren bir temel gerçek veri kümesi oluşturuyoruz. Bunu oluşturmak çok fazla insan emeği gerektiriyor. Sorgu aynı olsa bile farklı öğeler farklı insan grupları için daha alakalı olabilir.
Belge tarafından verilen bir sorgunun alakalılığını açıklamanın 2 tipik yolu vardır. İkili (öğe alakalı veya değil) veya sayısal (alaka bir öğe olarak ifade edilir).
import pandas as pd
products_df = pd.read_csv(
"shared_data/WANDS/product.csv",
sep="\t",
index_col="product_id",
keep_default_na=False, # bazı ürünlerin açıklaması yok
)
products_df.head()
num_products = 5000
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
product_name_embeddings = model.encode(
products_df["product_name"][0:num_products].tolist()
)
product_name_embeddings.shape
product_description_embeddings = model.encode(
products_df["product_description"][0:num_products].tolist()
)
product_description_embeddings.shape
# Koleksiyonun oluşturulması
from qdrant_client import QdrantClient, models
client = QdrantClient("http://localhost:6333")
client.delete_collection("wands-products")
client.create_collection(
collection_name="wands-products",
vectors_config={
"product_name": models.VectorParams(
size=384,
distance=models.Distance.COSINE,
),
"product_description": models.VectorParams(
size=384,
distance=models.Distance.COSINE,
),
},
optimizers_config=models.OptimizersConfigDiff(
default_segment_number=2,
indexing_threshold=1000,
),
)
client.upload_collection(
collection_name="wands-products",
vectors={
"product_name": product_name_embeddings,
"product_description": product_description_embeddings,
},
payload=products_df.to_dict(orient="records"),
ids=products_df.index.tolist(),
batch_size=64,
)
client.count("wands-products")
import time
time.sleep(1.0)
collection = client.get_collection("wands-products")
while collection.status != models.CollectionStatus.GREEN:
time.sleep(1.0)
collection = client.get_collection("wands-products")
collection
# Test sorguları
queries_df = pd.read_csv(
"shared_data/WANDS/query.csv",
sep="\t",
index_col="query_id",
)
queries_df.head()
# Temel Doğru
labels_df = pd.read_csv(
"shared_data/WANDS/label.csv",
sep="\t",
)
labels_df.sample(n=5)
relevancy_scores = {
"Exact": 10,
"Partial": 5,
"Irrelevant": 0,
}
labels_df["score"] = labels_df["label"].map(relevancy_scores.get)
labels_df["query_id"] = labels_df["query_id"].map(lambda x: f"query_{x}")
labels_df["product_id"] = labels_df["product_id"].map(lambda x: f"doc_{x}")
labels_df.sample(n=5)
Arama motorları, belirli bir sorgu için en alakalı N belgeyi seçer. Vektör araması için mesafe fonksiyonu, bir alaka ölçüsü için doğal bir adaydır. Sonuçlar, puanlarına göre sıralanır. Kalite ölçütleri kabaca üç kategoriye ayrılabilir:
- Alaka tabanlı
- Sıralama ile ilgili metrikler.
- Puanla ilgili metrikler.
Alaka Bazlı Metrikler (Relevancy Based Metrics)
En iyi k sonuçlar arasındaki ilgili belgelerin oranı. Araştırma sisteminin kalitesini hesaplamanın yaygın bir yolu precision@k’dır. Bu ölçüm, sistemimiz tarafından döndürülen en iyi k sonuçlardaki ilgili öğelerin oranını ölçer. K’den az ilgili belge döndürürsek %100 puan elde etmek imkansız olabilir ancak yine de farklı arama kanallarını karşılaştırmak için iyi bir yoldur. K’deki hassasiyet, sorgu başına hesaplanır y ve genellikle yer gerçeği veri kümesinden gelen tüm sorgular için k’de ortalama bir hassasiyet bildiririz.
precision@k = |relevant documents in the top k results| / k
Benzer şekilde, recall@k en iyi k sonuçta kaç tane ilgili belgeyi döndürebileceğimizi ölçer. Mükemmel bir şekilde bunların %100'ünü döndürüyor olurduk ancak sorgu başına k’den fazla ilgili belge varsa bu zor olabilir.
recall@k = |relevant documents in the top k results| / |all relevant documents|
Hem kesinliği hem de geri çağırmayı birlikte sunmak yaygındır. Belgenin sırası önemlidir. Ortalama Karşılıklı Sıralama, bu gruba ait yaygın olarak kullanılan bir ölçümdür. Sonuç listesindeki ilk ilgili öğenin konumuna dayanır. Bir sistemin mümkün olduğunca erken son derece ilgili sonuçları döndürme etkinliğini değerlendirmek için kullanılır. Sadece ilk ilgili öğenin konumunu dikkate alır. Google sonuçlarındaki ilk konum en çok tıklanan konumdur.
rank_i ilk ilgili öğenin pozisyonudur.
Puanla ilgili metrikler de yaygın olarak kullanılır. İndirimli Toplam Kazanç örneklerden biridir. İade edilen belgelerin toplam alaka düzeyini ölçer ancak aynı zamanda sonuç listesindeki öğelerin azalan alaka düzeyi sorununu da ele alır. Listede daha önce görünen belgelere daha fazla ağırlık verir ve böylece pozisyon için logaritmik bir ölçek aracılığıyla azalan alaka düzeyini yansıtır.
İlgililik puanını vurgulayan alternatif bir formülasyon da mevcuttur.
Eğer temel gerçekliğimizde alaka puanları varsa, DCG ile aynı formülü kullanarak İdeal İndirimli Toplam Kazancı hesaplayabiliriz.
Ranx
Ranx, metriklerimizi yazmak için bir kütüphanedir. 2 ana kavram vardır Qrels veya sorgu alaka yargıları (temel gerçekler) alma sisteminin bir çıktısını çalıştırır. Genellikle her sorgu için bir dizi belge ve önemlerini tanımladık. Qrels durumunda, önem bir tam sayıdır, ne kadar yüksekse eşleşme o kadar iyidir. Tüm sorguları değerlendirmelerinde kullanabilmeniz için gömmelere dönüştürün. Ranx, belirli bir sorgu için döndürülen sonuçları açıklayan bir çalıştırma nesnesi gerektirir. Döndürülen her belge, arama mekanizması tarafından atanan puanı almalıdır. Bunu önce ürün adı üzerinde arama yaparak iki kez yapacaksınız. Çalıştırma nesnesi verilerini tutacak bir sözlük tanımlayacağız. Bunun için çalıştırma sorguları üzerinde yineleme yapmamız ve ardından daha önce oluşturduğumuz koleksiyonda bir arama işlemi çalıştırmamız gerekir. Bu sözlük bir Çalıştırma nesnesine dönüştürülür. Son olarak, her iki arama hattını karşılaştırırız. Ranx, metriklerin dizeler olarak geçirilmesine izin verir.
# ranx
from ranx import Qrels
qrels = Qrels.from_df(
labels_df.astype({"query_id": "str", "product_id": "str"}),
q_id_col="query_id",
doc_id_col="product_id",
score_col="score",
)
# Tüm sorguları çalıştırıyorum
queries_df["query_embedding"] = model.encode(
queries_df["query"].tolist()
).tolist()
queries_df.sample(n=5)
from collections import defaultdict
name_run_dict = defaultdict(dict)
for id, row in queries_df.iterrows():
query_id = f"query_{id}"
results = client.search(
collection_name="wands-products",
query_vector=models.NamedVector(
name="product_name",
vector=row["query_embedding"]
),
with_vectors=False,
with_payload=False,
limit=100,
)
for point in results:
document_id = f"doc_{point.id}"
name_run_dict[query_id][document_id] = point.score
name_run_dict
from ranx import Run
product_name_run = Run(name_run_dict, name="product_name")
description_run_dict = defaultdict(dict)
for id, row in queries_df.iterrows():
query_id = f"query_{id}"
results = client.search(
collection_name="wands-products",
query_vector=models.NamedVector(
name="product_description",
vector=row["query_embedding"]
),
with_vectors=False,
with_payload=False,
limit=100,
)
for point in results:
document_id = f"doc_{point.id}"
description_run_dict[query_id][document_id] = point.score
product_description_run = Run(
description_run_dict,
name="product_description"
)
from ranx import compare
compare(
qrels=qrels,
runs=[
product_name_run,
product_description_run
],
metrics=[
"precision@10",
"recall@10",
"mrr@10",
"dcg@10",
"ndcg@10",
],
)
Tüm metrikler, ürün adının anlamsal arama için daha iyi bir aday olduğunu gösteriyor. Geri Alma Artırılmış Üretimi, LLM’nin doğru cevabı formüle etmek için kullanabileceği ilgili bağlamla istemleri genişletmekle ilgilidir. İstemlerimizi, onları temel olmayan bilgilerle doldurursak yardımcı olmaz. Ancak, arama mekanizmamız LLM’ye anlamlı bir şey sağlayamazsa tüm süreç başarısız olur. Bu yüzden onu dikkatlice test etmek çok önemlidir.
HNSW Aramasını Optimize Etme (Optimizing HNSW Search)
Verimli en yakın komşu aramalarına olanak tanıyan vektör veritabanlarının temeli olan HNSW’nin kısa bir özeti. Bu yaklaşımın ne kadar iyi çalıştığını kontrol edebileceğimiz belirli yollar vardır. Hiyerarşik gezilebilir Small Words, en yakın komşu aramasını yaklaşıklayan çok katmanlı bir grafiktir. Benzerliklerine göre bağlanan düğümler olarak vektörleri kullanır. Bu nedenle, benzerlik ölçüsü önceden seçilmelidir. En yakın noktalar arasında bağlantılar oluşturulan çok katmanlı bir vektör grafiği. Bu veri yapısı, en üstteki en seyrek olan birkaç yığılmış grafik oluşturur. Bu, tüm vektörlerin yalnızca küçük bir alt kümesini içerir. Her ardışık katman giderek daha fazlasını içerir ancak kural, üstteki katmandaki tüm vektörlerin geçerli katmanda da mevcut olmasıdır. Alt katman, sahip olduğumuz tüm vektörleri içerdiği için yoğundur.
Benzerlik tabanlı kenarlar haricinde, katmanlar arasında geçiş yapmamıza yardımcı olan dahili katman bağlantıları da vardır. Bunlar, yalnızca farklı katmanlarda aynı vektörler arasında oluşturulur. HNSW’nin kontrol edebileceğimiz iki parametresi vardır. İlki M, düğüm başına kaç kenar oluşturduğumuzu tanımlar. Bu parametrenin değerini artırmak, yaklaşıklık kalitesini artırmalıdır, ancak gömme modelinizle ilgili herhangi bir sorunu çözmez. Temsilleriniz çok zayıfsa, ANN aramasının verilerin semantiğini sihirli bir şekilde türetmesini bekleyemezsiniz. Daha yüksek M değerleri sizi yalnızca tam k en yakın komşu aramasına yaklaştırır. Ancak, bu parametrenin kontrol edilmesi gecikmeyi ayarlamaya da yardımcı olabilir. Daha hızlı alma için kaliteden ödün verebileceğiniz bazı durumlar vardır.
HNSW grafiğinin yardımıyla gerçekleştirilen en yaygın işlem aramadır. Bir sorgu aldığımızda veya grafiğe yeni bir nokta eklemek istediğimizde en üst katmandan başlarız. Bir sorgu aldığımızda, aynı gömme modeli kullanılarak vektörleştirilir ve vektör gerçek bir sorgu gibi davranır. Arama işlemi en üst katmandan başlar. Burada algoritma ef’ye en yakın noktaları bulur. Ef, dikkate alabileceğimiz ikinci parametredir. Bizim durumumuzda, değeri ikiye ayarlandı, bu nedenle ilk katmanda kaç nokta seçtiğimizdir. Daha fazlasını seçersek daha iyi sonuçlar elde edebilirdik, ancak bu aynı zamanda süreci yavaşlatırdı.
Üst katmanda seçilen noktalar daha sonra katman içi kenarlar sayesinde alttaki katmanda seçilir. Burada tüm noktaları geçmeyiz, sadece bu önceki adımda seçilen noktaların doğrudan komşularına odaklanırız. Tekrar en yakın eşleşmeleri seçeriz, sonra işlem devam eder. Alt katmana ulaşana kadar aynı prosedürü tekrarlarız. Her seferinde sadece daha önce seçilen noktaları ve onların doğrudan komşularını kontrol ederiz. Bu, aramanın kapsamını etkili bir şekilde daraltır. Son katmanda, en üstteki k girdiyi seçeriz ve bu vektörler döndürdüğümüz en alakalı belgeler haline gelir.
from qdrant_client import QdrantClient, models
client = QdrantClient("http://localhost:6333", timeout=600)
client.delete_collection("wands-products")
client.recover_snapshot(
"wands-products",
"https://storage.googleapis.com/deeplearning-course-c1/snapshots/wands-products.snapshot",
)
collection = client.get_collection("wands-products")
collection
# HNSWM Parametreleri
collection.config.hnsw_config
# Test Sorguları
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
import pandas as pd
queries_df = pd.read_csv(
"shared_data/WANDS/query.csv",
sep="\t",
index_col="query_id",
)
queries_df["query_embedding"] = model.encode(
queries_df["query"].tolist()
).tolist()
queries_df.sample(n=5)
# ANN Arama
client.search(
"wands-products",
query_vector=models.NamedVector(
name="product_name",
vector=model.encode(queries_df.loc[0, "query"])
),
limit=3,
with_vectors=False,
with_payload=False,
)
# KNN Arama
client.search(
"wands-products",
query_vector=models.NamedVector(
name="product_name",
vector=model.encode(queries_df.loc[0, "query"])
),
limit=3,
with_vectors=False,
with_payload=False,
search_params=models.SearchParams(
exact=True, # Tam arama modunu açar
),
)
# Temel Doğru
from collections import defaultdict
from ranx import Qrels
knn_qrels_dict = defaultdict(dict)
for id, row in queries_df.iterrows():
query_id = f"query_{id}"
results = client.search(
collection_name="wands-products",
query_vector=models.NamedVector(
name="product_name",
vector=row["query_embedding"]
),
with_vectors=False,
with_payload=False,
limit=100,
search_params=models.SearchParams(
exact=True, # tam aramayı etkinleştir
),
)
for point in results:
document_id = f"doc_{point.id}"
# Tam sayıya dönüştürme gereklidir çünkü ranx tam sayıları bekler
knn_qrels_dict[query_id][document_id] = int(point.score * 100)
qrels = Qrels(knn_qrels_dict)
qrels
# ANN Arama
from ranx import Run
run_dict = defaultdict(dict)
for id, row in queries_df.iterrows():
query_id = f"query_{id}"
results = client.search(
collection_name="wands-products",
query_vector=models.NamedVector(
name="product_name",
vector=row["query_embedding"]
),
with_vectors=False,
with_payload=False,
limit=100,
search_params=models.SearchParams(
exact=False, # tam aramayı devre dışı bırak
),
)
for point in results:
document_id = f"doc_{point.id}"
run_dict[query_id][document_id] = point.score
initial_run = Run(
run_dict,
name="initial",
)
initial_run
from ranx import evaluate
evaluate(
qrels=qrels,
run=initial_run,
metrics=["precision@25"]
)
HNSW Segmentleri (HNSW Segments)
Çoğu vektör veritabanında, HNSW grafiği küresel olarak oluşturulmaz, ancak noktalar örtüşmeyen segmentlere bölünür. Her segmentin ayrı bir yapısı oluşturulur. Ölçeklenebilirlik açısından çok yardımcı olur çünkü bu segmentler tüm bir makine sınıfına dağıtılabilir. Bu yaklaşım ayrıca sistemin birden fazla CPU çekirdeğini aynı anda kullanarak tek düğümlü bir senaryoda eşzamanlı olarak çalışabilme yeteneğini de geliştirir. Ayrıca, tek tek yapabileceğimiz için HNSW grafiklerinin bu tek segmentini yeniden oluşturmak çok daha kolaydır. Özet olarak, HNSW parametrelerini kontrol etmek size arama kalitesi açısından ek bir destek sağlayabilir.
# HNSW Parametrelerinin Ayarlanması
client.update_collection(
collection_name="wands-products",
hnsw_config=models.HnswConfigDiff(
m=64,
ef_construct=200,
)
)
import time
time.sleep(1.0)
collection = client.get_collection("wands-products")
while collection.status != models.CollectionStatus.GREEN:
time.sleep(1.0)
collection = client.get_collection("wands-products")
collection
tweaked_run_dict = defaultdict(dict)
for id, row in queries_df.iterrows():
query_id = f"query_{id}"
results = client.search(
collection_name="wands-products",
query_vector=models.NamedVector(
name="product_name",
vector=row["query_embedding"]
),
with_vectors=False,
with_payload=False,
limit=100,
search_params=models.SearchParams(
exact=False, # tam aramayı devre dışı bırak
),
)
for point in results:
document_id = f"doc_{point.id}"
tweaked_run_dict[query_id][document_id] = point.score
tweaked_run = Run(
tweaked_run_dict,
name="tweaked"
)
tweaked_run
evaluate(
qrels=qrels,
run=tweaked_run,
metrics=["precision@25"]
)
Vektör Nicemleme (Vector quantization)
Mevcut bir vektör arama mekanizmasını bellek kullanımını azaltmak için optimize edeceğiz. Sadece anlamsal aramayı daha uygun fiyatlı hale getirmekle kalmayıp aynı zamanda verimliliğini de artırabilen farklı vektör niceleme türlerini keşfedeceksiniz.
Varsayılan olarak, vektörlerin her boyutu bir float32 değeriyle temsil edilir. Örneğin OpenAI Ada gömmesi gibi 1536 boyutlu bir vektör 6 kb bellek gerektirir. 1 vektör 6 kb, 1 milyon vektör 6 GB ve 1 milyar vektör 6 TB bellek gerektirir. Temsili kısıtlayarak bellek kullanımını azaltmanın belirli yolları vardır. Niceleme tekniklerinin çok daha güçlü olduğu ve arama kalitesi üzerinde çok az etkisi olan anlamsal aramayı daha uygun fiyatlı hale getirmenin bir yolunu sunduğu ortaya çıktı. Ürün nicelemesi bugün öğreneceğiniz ilk yöntemdir.
Orijinal vektörler parçalara bölünür; parça sayısı arttıkça sıkıştırma oranı düşer. Alt vektörlerin sayısı seçilen sıkıştırma oranına bağlıdır. Örneğin, sıkıştırma oranı 16 ise, her alt vektör dört boyuta sahip olur, çünkü sıkıştırma oranı bayt cinsinden tanımlanır. Vektörler parçalara ayrıldıktan sonra sıkıştırmaya başlayabiliriz. Bir sonraki adımda, k-means gibi kümeleme algoritmasına girdi olarak kullanılan alt vektörlerle başlarız. Sonuç olarak, her alt vektör en yakın merkezi noktaya atanacaktır. Merkezlerin sayısı yapılandırılamaz, her zaman 256 olarak ayarlanmıştır. Tek bir bayt ile temsil edebileceğimiz toplam olası değer sayısı 256'dır. Artık alt vektörleri saklamak yerine, en yakın merkezlerin kimliklerini saklıyoruz. Bizim durumumuzda, her alt vektör 4 bayttı, bu nedenle toplamda 16 bayt gerekiyordu. Ürün kantizasyonundan sonra, yalnızca merkez kimliğini saklamamız yeterli olur ve bu sadece bir bayttır. On altı bayt sadece bir bayta düştü, dolayısıyla elde edilen sıkıştırma oranı da 16'dır. Ürün kantizasyonu ‘ürün’ olarak adlandırılır çünkü yüksek boyutlu bir vektörü daha küçük alt vektörlere böler ve her alt vektörü kendi kod kitabını kullanarak ayrı ayrı kantize eder. Bu kantize edilmiş alt vektörlerin kombinasyonu, bireysel kod kitaplarının bir ürünü oluşturarak tüm alan için kapsamlı bir ürün kodu oluşturur. Bu yaklaşım, orijinal yüksek boyutlu veriyi verimli bir şekilde yaklaşık olarak temsil etmek için alt uzayların Kartezyen çarpımını kullanır.
Ürün kantizasyonunun birden fazla olası yapılandırması vardır ve etkisi nasıl ayarladığınıza bağlı olarak değişir. Eğer bütçeniz sınırlıysa, sıkıştırma oranını 64 katına kadar ayarlamayı düşünebilirsiniz, ancak bu nadiren yeterince hassas çalışır.
Bizim durumumuzda, en agresif 32 kat sıkıştırma, aramanın doğruluğunu yaklaşık 0.66'ya düşürdü. Orijinal vektörlerle yapılan arama ise yaklaşık 0.98'e ulaşabiliyordu, bu da büyük bir fark. Ürün nicemlemeyi her zaman arama hassasiyetinde azalmaya yol açar, ancak bazen işleri biraz hızlandırabilir. Ancak, noktalarınızı eklemek ve koleksiyonu oluşturmak her zaman daha yavaş olacaktır çünkü K-means kümeleme işlemi yapması gerekir. Nicemlemenin bozulan kalitesi, yeniden puanlama yardımıyla iyileştirilebilir. Kuantize edilmiş vektörlerle arama yaptığımızda, birçok belge aynı temsile sahip olabilir. Vektör veritabanları genellikle orijinal vektörleri diskte tutar ve sonuçları geri yüklemek için bunları yükler. Kuantize arama tarafından döndürülen bir sonuç setimiz olduğunda, orijinal gömmeleri yükleyip benzerliği bunlar kullanarak hesaplayabiliriz. Bu, aynı mahalleden gelen noktalar arasındaki mesafeleri ayırt etmeye yardımcı olur. Geri yükleme, arama kalitesini artırmalıdır ve bunu kapatma kararı daha bilinçli bir tercih olmalıdır.
from qdrant_client import QdrantClient, models
client = QdrantClient("http://localhost:6333", timeout=600)
client.delete_collection("wands-products")
client.recover_snapshot(
"wands-products",
"https://storage.googleapis.com/deeplearning-course-c1/snapshots/wands-products.snapshot",
)
collection = client.get_collection("wands-products")
collection
# Test Sorguları
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
import pandas as pd
queries_df = pd.read_csv(
"shared_data/WANDS/query.csv",
sep="\t",
index_col="query_id",
)
queries_df["query_embedding"] = model.encode(
queries_df["query"].tolist()
).tolist()
queries_df.sample(n=5)
from collections import defaultdict
from ranx import Qrels
knn_qrels_dict = defaultdict(dict)
for id, row in queries_df.iterrows():
query_id = f"query_{id}"
results = client.search(
collection_name="wands-products",
query_vector=models.NamedVector(
name="product_name",
vector=row["query_embedding"]
),
with_vectors=False,
with_payload=False,
limit=50,
search_params=models.SearchParams(
exact=True, # tam aramayı etkinleştir
),
)
for point in results:
document_id = f"doc_{point.id}"
# Tam sayıya dönüştürme gereklidir çünkü ranx tam sayıları bekler
knn_qrels_dict[query_id][document_id] = int(point.score * 100)
qrels = Qrels(knn_qrels_dict)
qrels
# Ürün Niceleme (PQ)
client.update_collection(
"wands-products",
quantization_config=models.ProductQuantization(
product=models.ProductQuantizationConfig(
compression=models.CompressionRatio.X64,
always_ram=True,
)
),
)
import time
time.sleep(1.0)
collection = client.get_collection("wands-products")
while collection.status != models.CollectionStatus.GREEN:
time.sleep(1.0)
collection = client.get_collection("wands-products")
collection
import numpy as np
response_times = []
pq_run_dict = defaultdict(dict)
for id, row in queries_df.iterrows():
query_id = f"query_{id}"
# Başlangıç zamanını ölçün
start_time = time.monotonic()
results = client.search(
collection_name="wands-products",
query_vector=models.NamedVector(
name="product_name",
vector=row["query_embedding"]
),
search_params=models.SearchParams(
quantization=models.QuantizationSearchParams(
rescore=False # Orijinal vektörleri kullanarak yeniden puanlamayı devre dışı bırak
)
),
with_vectors=False,
with_payload=False,
limit=50,
)
# Yanıt süresini listede sakla
response_times.append(time.monotonic() - start_time)
for point in results:
document_id = f"doc_{point.id}"
pq_run_dict[query_id][document_id] = point.score
np.mean(response_times)
from ranx import Run, evaluate
product_name_pq_run = Run(
pq_run_dict,
name="product_name_pq"
)
evaluate(
qrels=qrels,
run=product_name_pq_run,
metrics=["precision@25"]
)
# 0.826666666666666667
Skaler niceleme, anlamsal aramayı daha az bellek yoğun hale getirmek için uygulayabileceğiniz başka bir tekniktir. Kayan noktaları tam sayılara dönüştürme fikrine dayanır. Ancak, orijinal vektörleri kayan noktalarla temsil ettiğimizde, bireysel sayılar belirli bir aralıktan gelir; bu, kayan noktalarla temsil edebileceğimiz tüm değerlerin yalnızca bir alt kümesidir. Daha sonra bazı yeni vektörler aldığımızda aralık değişebilir, bu nedenle minimum ve maksimum değerleri asla bilemeyiz. Bu nedenle niceleme, tüm vektörler için küresel olarak değil, verilerin alt kümeleri üzerinde ayrı ayrı yapılır. Veritabanı her segmentin ayrılabilir olduğunu varsaydığından, her segmenti ayırmak, HNSW’de olduğu gibi Skaler nicelemeye de yardımcı olur. Böylece sayıların aralığını ölçebilir ve minimum ve maksimum değerleri hesaplayabiliriz. Daha sonra tüm niceleme işlemi bu sayıları 0 ile 255 arasındaki tam sayı aralığından veya -128 ile 127 arasındaki tam sayı aralığına dönüştürmekle ilgilidir. Skaler nicelemenin sıkıştırma oranı her zaman 4'tür. 4 bayt tek bir bayta sıkıştırılır.
Skaler niceleme uygulandıktan sonra vektörleri tutmak için gereken bellek %75'e kadar daha yavaştır. Bu, arama hassasiyeti açısından bir maliyetle birlikte gelir, ancak hız avantajları da vardır. Genel olarak Skaler niceleme, aramanızı daha hızlı ve daha uygun fiyatlı hale getirmek istiyorsanız genellikle dikkate alınması gereken ilk şeydir.
response_times = []
pq_rescore_run_dict = defaultdict(dict)
for id, row in queries_df.iterrows():
query_id = f"query_{id}"
start_time = time.monotonic()
results = client.search(
collection_name="wands-products",
query_vector=models.NamedVector(
name="product_name",
vector=row["query_embedding"]
),
search_params=models.SearchParams(
quantization=models.QuantizationSearchParams(
rescore=True # Orijinal vektörleri kullanarak yeniden puanlamayı etkinleştirin
)
),
with_vectors=False,
with_payload=False,
limit=50,
)
response_times.append(time.monotonic() - start_time)
for point in results:
document_id = f"doc_{point.id}"
pq_rescore_run_dict[query_id][document_id] = point.score
np.mean(response_times)
product_name_pq_rescore_run = Run(
pq_rescore_run_dict,
name="product_name_pq_rescore"
)
evaluate(
qrels=qrels,
run=product_name_pq_rescore_run,
metrics=["precision@25"]
)
# Sayısal Nicemleme (SQ)
client.update_collection(
"wands-products",
quantization_config=models.ScalarQuantization(
scalar=models.ScalarQuantizationConfig(
type=models.ScalarType.INT8,
always_ram=True,
)
),
)
time.sleep(1.0)
collection = client.get_collection("wands-products")
while collection.status != models.CollectionStatus.GREEN:
time.sleep(1.0)
collection = client.get_collection("wands-products")
collection
response_times = []
sq_run_dict = defaultdict(dict)
for id, row in queries_df.iterrows():
query_id = f"query_{id}"
start_time = time.monotonic()
results = client.search(
collection_name="wands-products",
query_vector=models.NamedVector(
name="product_name",
vector=row["query_embedding"]
),
search_params=models.SearchParams(
quantization=models.QuantizationSearchParams(
rescore=False # Orijinal vektörleri kullanarak yeniden puanlamayı devre dışı bırak
)
),
with_vectors=False,
with_payload=False,
limit=50,
)
response_times.append(time.monotonic() - start_time)
for point in results:
document_id = f"doc_{point.id}"
sq_run_dict[query_id][document_id] = point.score
np.mean(response_times)
product_name_sq_run = Run(
sq_run_dict,
name="product_name_sq"
)
evaluate(
qrels=qrels,
run=product_name_sq_run,
metrics=["precision@25"]
)
response_times = []
sq_rescore_run_dict = defaultdict(dict)
for id, row in queries_df.iterrows():
query_id = f"query_{id}"
start_time = time.monotonic()
results = client.search(
collection_name="wands-products",
query_vector=models.NamedVector(
name="product_name",
vector=row["query_embedding"]
),
search_params=models.SearchParams(
quantization=models.QuantizationSearchParams(
rescore=True # Orijinal vektörleri kullanarak yeniden puanlamayı etkinleştirin
)
),
with_vectors=False,
with_payload=False,
limit=50,
)
response_times.append(time.monotonic() - start_time)
for point in results:
document_id = f"doc_{point.id}"
sq_rescore_run_dict[query_id][document_id] = point.score
np.mean(response_times)
product_name_sq_rescore_run = Run(
sq_rescore_run_dict,
name="product_name_sq_rescore"
)
evaluate(
qrels=qrels,
run=product_name_sq_rescore_run,
metrics=["precision@25"]
)
# 0.99375
İkili niceleme bireysel boyutları daha da sıkıştırır. Her kayan nokta sayısını 0 veya 1 olan bir boolean’a dönüştürür. 32 bit bilgi tek bir bite sıkıştırılır. Rol, her pozitif değeri bire ve her negatif veya 0'ı 0'a dönüştürmek kadar basittir. Birçok nokta benzer şekilde temsil edileceğinden önemli bir şey ikili örneklemedir. İhtiyacınız olan daha fazla noktayı çıkarmak ve sorgunuz ile kayan nokta olarak temsil edilen orijinal vektörler arasındaki mesafeyi hesaplamak istersiniz. İkili nicelemenin gerçek dünyadaki etkisini tekrar görelim.
İkili niceleme, bellek kullanımını 32 kata kadar azaltır. Yine de, geri alma hızını 40 kata kadar artırabildiği için performans avantajları daha da yüksek olabilir. Doğru nicelemeyi seçmek, uygulamanızın özelliklerine bağlıdır.
# İkili Nicemleme (BQ)
client.update_collection(
"wands-products",
quantization_config=models.BinaryQuantization(
binary=models.BinaryQuantizationConfig(
always_ram=True,
)
),
)
time.sleep(1.0)
collection = client.get_collection("wands-products")
while collection.status != models.CollectionStatus.GREEN:
time.sleep(1.0)
collection = client.get_collection("wands-products")
collection
response_times = []
bq_run_dict = defaultdict(dict)
for id, row in queries_df.iterrows():
query_id = f"query_{id}"
start_time = time.monotonic()
results = client.search(
collection_name="wands-products",
query_vector=models.NamedVector(
name="product_name",
vector=row["query_embedding"]
),
search_params=models.SearchParams(
quantization=models.QuantizationSearchParams(
rescore=False # Orijinal vektörleri kullanarak yeniden puanlamayı devre dışı bırak
)
),
with_vectors=False,
with_payload=False,
limit=50,
)
response_times.append(time.monotonic() - start_time)
for point in results:
document_id = f"doc_{point.id}"
bq_run_dict[query_id][document_id] = point.score
np.mean(response_times)
product_name_bq_run = Run(
bq_run_dict,
name="product_name_bq"
)
evaluate(
qrels=qrels,
run=product_name_bq_run,
metrics=["precision@25"]
)
response_times = []
bq_rescore_run_dict = defaultdict(dict)
for id, row in queries_df.iterrows():
query_id = f"query_{id}"
start_time = time.monotonic()
results = client.search(
collection_name="wands-products",
query_vector=models.NamedVector(
name="product_name",
vector=row["query_embedding"]
),
search_params=models.SearchParams(
quantization=models.QuantizationSearchParams(
rescore=True # Orijinal vektörleri kullanarak yeniden puanlamayı etkinleştirin
)
),
with_vectors=False,
with_payload=False,
limit=50,
)
response_times.append(time.monotonic() - start_time)
for point in results:
document_id = f"doc_{point.id}"
bq_rescore_run_dict[query_id][document_id] = point.score
np.mean(response_times)
product_name_bq_rescore_run = Run(
bq_rescore_run_dict,
name="product_name_bq_rescore"
)
evaluate(
qrels=qrels,
run=product_name_bq_rescore_run,
metrics=["precision@25"]
)
from ranx import compare
compare(
qrels=qrels,
runs=[
product_name_pq_run,
product_name_pq_rescore_run,
product_name_sq_run,
product_name_sq_rescore_run,
product_name_bq_run,
product_name_bq_rescore_run,
],
metrics=["precision@25"],
)
Kaynak
Deeplearning.ai, (2024), Retrieval Optimization: Tokenization to Vector Quantization: