BPE(Byte Pair Encoding) nedir ve LLM'ler için neden önemli
1 Tem 2026 · 8 dk okuma
GPT gibi modeller, sadece eğitildikleri veri setlerindeki kelimelerle mi sınırlı sizce? Mesela, "uzun" kelimesi veri setinde varsa ama "uzunluk" kelimesi yoksa model bunu nasıl yakalayabiliyor? GPT gibi modeller dildeki morfolojiyi nasıl yakalayabiliyor? İşte bu makalemizin konusu bu.
Bilgisayarın algısında quick, quickest veya quickly gibi kelimeler yok. Yalnızca sayılar var. Bu yüzden metni önce parçalara ayırmamız, her parçaya bir index vermemiz ve ortaya bir vocabulary(kelime dağarcığı) çıkarmamız gerekiyor. Buradaki asıl soru şu: Bir token ne kadar büyük olmalı?
İlk akla gelen iki çözüm olabilir:
- Her kelimeyi tek bir token yapmak.
- Her karakteri tek bir token yapmak.
İkisini de deneyince problemin birbirine zıt iki ucuyla karşılaşıyoruz. Word-level tokenization kısa diziler üretiyor ama görmediği kelimelerle baş edemiyor. Character-level tokenization neredeyse her kelimeyi yazabiliyor ama bu kez de dizileri gereksiz yere uzatıyor ve uzun diziler hem memory hem de computation anlamında baş belamız olabiliyor.
BPE tam olarak bu iki ucun arasında duruyor.
Öncelikle, word-level tokenization neden yetmedi?
Word2Vec gibi basit modellerdeki bir tokenizer şu şekilde olabilir:
def clean_tokenize(text):
return re.findall(r"[a-z]+", text.lower())Metni küçük harfe çeviriyor, kelimeleri ayırıyor ve her kelimeyi ayrı bir token kabul ediyoruz. Ardından belirli bir frekansın altında kalan kelimeleri vocabulary'den çıkarıyoruz:
counter = Counter(
word
for document in tokenized_corpus
for word in document
)
vocab = sorted([
word
for word, count in counter.items()
if count >= min_count
])Küçük bir corpus'ta(kelimelerimizi oluşturduğumuz tüm veri) bu yaklaşım gayet anlaşılır. Fakat veri büyüdükçe vocabulary de büyüyor. quick, quicker, quickest ve quickly model için birbirinden tamamen bağımsız dört token hâline geliyor.
Daha kötüsü, vocabulary'de bulunmayan kelimeleri mevcut kod doğrudan atıyor:
idxs = [
word_to_idx[word]
for word in sentence
if word in word_to_idx
]Yani nadir bir kelime yalnızca <unk> token'ına dönüşmüyor; cümleden tamamen kayboluyor. Böylece model o kelimeyi öğrenemediği gibi etrafındaki context yapısı da değişebiliyor.
Örneğin:
the unusual dogunusual vocabulary'de yoksa modelin gördüğü dizi şuna dönüşüyor:
the dogAslında yan yana olmayan iki kelime artık komşu gibi görünüyor. Word2Vec context penceresini bu dizi üzerinden kurduğu için problem tokenizer aşamasından modelin eğitim verisine kadar taşınıyor.
Character-level tokenization çözüm mü?
Öbür uca geçip her karakteri ayrı token yapabiliriz:
quickest → q · u · i · c · k · e · s · tBu yaklaşım vocabulary'yi oldukça küçük tutuyor. İngilizce bir corpus için harfler, rakamlar ve birkaç noktalama işareti çoğu zaman yeterli.
Bilinmeyen kelime problemi de büyük ölçüde ortadan kalkıyor. Model quickest kelimesini daha önce hiç görmemiş olsa bile karakterlerini ayrı ayrı temsil edebiliyor.
Fakat bu defa sequence uzuyor. Word-level tokenizer'ın tek token ile temsil ettiği thermometer, character-level tokenizer'da on bir tokena dönüşüyor:
thermometer
↓
t · h · e · r · m · o · m · e · t · e · rModelin işlemesi gereken adım sayısı artıyor. Üstelik the, thermo veya meter gibi tekrar eden parçalar her seferinde yeniden karakter karakter işleniyor.
Kısacası word-level yaklaşım fazla büyük, character-level yaklaşım fazla küçük parçalar üretiyor. Aradığımız şey ikisinin ortasında, corpus'a bakarak kendi parçalarını öğrenen bir tokenizer.
BPE'nin fikri
BPE başlangıçta bir veri sıkıştırma algoritmasıydı. NLP'ye uyarlandığında temel fikir çok da değişmiyor: corpus'taki en sık yan yana gelen sembol çiftini buluyor, bu çifti tek sembol hâline getiriyor ve aynı işlemi tekrar ediyoruz.
Nadir kelimeleri subword birimlerine ayıran bu yaklaşım, açık vocabulary problemine çözüm olarak özellikle makine çevirisinde kullanıldı. BPE'nin NLP'deki bilinen uyarlamalarından biri, Sennrich ve diğerlerinin “Neural Machine Translation of Rare Words with Subword Units” çalışması.
Benim implementasyonum gerçek byte değerleri yerine karakterlerle başlıyor. Bu yüzden teknik olarak karakter tabanlı bir BPE subword tokenizer yazıyoruz.
Algoritmanın tamamı şu döngüden oluşuyor:
Kelimeleri karakterlere ayır → bitiş işareti ekle → pair frekanslarını say → en sık pair'i birleştir → tekrar et
Kelimeleri başlangıç sembollerine ayırıyoruz
Önce corpus'taki kelimelerin frekanslarını çıkarıyoruz. Her kelimeyi karakterlere ayırırken sonuna <w> ekliyorum:
def get_word_freqs(all_words):
word_freqs = {}
for word in all_words:
word_key = tuple(word) + ("<w>",)
word_freqs[word_key] = word_freqs.get(word_key, 0) + 1
return word_freqsÖrneğin the kelimesi şöyle temsil ediliyor:
("t", "h", "e", "<w>")<w> burada kelimenin bittiğini gösteriyor. Bu işaret sayesinde kelime sonunda bulunan bir karakter çiftiyle kelimenin ortasındaki aynı çift birbirinden ayrılabiliyoruz.
Ardından bütün komşu sembol çiftlerini sayıyoruz:
def get_pair_counts(word_freqs):
pair_freqs = {}
for tokenized_word, frequency in word_freqs.items():
for first, second in zip(
tokenized_word,
tokenized_word[1:]
):
pair = (first, second)
pair_freqs[pair] = (
pair_freqs.get(pair, 0) + frequency
)
return pair_freqsBurada yalnızca pair'in kaç farklı kelimede bulunduğunu saymıyoruz. Kelimenin corpus frekansını da hesaba katıyoruz. Bir kelime yüz defa geçiyorsa içindeki pair'ler de sayıma yüz defa katkıda bulunuyor.
En sık pair'i birleştiriyoruz
Bir sonraki adım, bulduğumuz pair'i bütün vocabulary üzerinde birleştirmek:
def merge_pair(pair, word_freqs):
new_word_freqs = {}
for word_tuple, frequency in word_freqs.items():
symbols = list(word_tuple)
index = 0
while index < len(symbols) - 1:
if (
symbols[index] == pair[0]
and symbols[index + 1] == pair[1]
):
symbols[index] = (
symbols[index] + symbols[index + 1]
)
del symbols[index + 1]
else:
index += 1
new_key = tuple(symbols)
new_word_freqs[new_key] = (
new_word_freqs.get(new_key, 0) + frequency
)
return new_word_freqsDiyelim ki en sık pair ("t", "h") çıktı. Bu durumda ikisini birleştirip yeni bir th sembolü oluşturuyoruz:
t · h · e · <w>
↓
th · e · <w>Bir sonraki turda ("th", "e") en sık pair olursa bu kez the token'ını öğreniyoruz:
th · e · <w>
↓
the · <w>Tokenizer'a İngilizce morphology kurallarını veya the kelimesinin ne anlama geldiğini anlatmadık. Yalnızca corpus'taki tekrarları saydık ve sık kullanılan parçalar zamanla daha büyük token'lara dönüştü.
Merge kurallarını öğreniyoruz
Training fonksiyonu her turda en sık pair'i seçiyor ve yapılan işlemi bir merge kuralı olarak saklıyor:
def train_bpe(word_freqs, num_merges):
merge_rules = []
while len(merge_rules) < num_merges:
pair_counts = get_pair_counts(word_freqs)
if not pair_counts:
break
best_pair = max(
pair_counts,
key=pair_counts.get
)
merge_rules.append(best_pair)
word_freqs = merge_pair(best_pair, word_freqs)
return merge_rulesDenemelik bir corpus üzerinde on merge çalıştırdığımızda şu kurallar oluşuyor:
1. 't' + 'h' → 'th'
2. 'th' + 'e' → 'the'
3. 'the' + '<w>' → 'the<w>'
4. 's' + '<w>' → 's<w>'
5. 'n' + '<w>' → 'n<w>'
6. 't' + '<w>' → 't<w>'
7. 'd' + 'o' → 'do'
8. 'do' + 'g' → 'dog'
9. 'dog' + '<w>' → 'dog<w>'
10. 'q' + 'u' → 'qu'Corpus'ta sık geçen the ve dog kelimeleri zamanla tek token hâline gelirken daha az görülen kelimeler küçük parçalardan oluşmaya devam ediyor.
Merge sayısını artırırsak vocabulary büyüyor ve sequence'ler kısalıyor. Azaltırsak vocabulary küçülüyor fakat kelimeler daha fazla parçaya ayrılıyor. Yani num_merges, vocabulary boyutuyla sequence uzunluğu arasındaki dengeyi belirleyen temel parametrelerden biri.
Yeni bir kelimeyi encode etmek
Training sırasında öğrendiğimiz kuralları yeni kelimelere aynı sırayla uyguluyoruz:
def encode(word, merge_rules):
symbols = list(tuple(word) + ("<w>",))
for first, second in merge_rules:
index = 0
while index < len(symbols) - 1:
if (
symbols[index] == first
and symbols[index + 1] == second
):
symbols[index] = first + second
del symbols[index + 1]
else:
index += 1
return tuple(symbols)Tokenizer'ın öğrendiği on kuralla birkaç kelimeyi encode edince sonuç şöyle oluyor:
quickest → ('qu', 'i', 'c', 'k', 'e', 's', 't<w>')
smallest → ('s', 'm', 'a', 'l', 'l', 'e', 's', 't<w>')
thermometer → ('the', 'r', 'm', 'o', 'm', 'e', 't', 'e', 'r', '<w>')
dog → ('dog<w>',)Tokenizer quickest kelimesinin tamamını görmemiş olsa bile daha önce öğrendiği qu parçasını kullanabiliyor. Çok sık gördüğü dog ise tek token'a kadar birleşiyor.
Decode işlemi bundan daha basit:
def decode(tokens):
return "".join(tokens).replace("<w>", "")('qu', 'i', 'c', 'k', 'e', 's', 't<w>')
↓
quickestBPE her şeyi çözüyor mu?
Hayır. BPE kelimelerin anlamını veya morphology'sini öğrenmiyor; yalnızca hangi sembollerin sık yan yana geldiğine bakıyor. Bu yüzden oluşturduğu subword'ler dilbilimsel olarak anlamlı olmak zorunda değil.
Sonuçlar corpus'a da doğrudan bağlı. Küçük veya dengesiz bir corpus üzerinde öğrenilen merge kuralları gerçek metinleri iyi temsil etmeyebilir.
Benim implementasyonumun bir başka sınırı da karakterlerden başlaması. Eğitim sırasında hiç görülmemiş yeni bir Unicode karakteri geldiğinde tokenizer bu karakteri ayrı bırakabilir ama downstream modelin vocabulary'sinde ona ait bir embedding bulunmayabilir. Byte-level BPE uygulamaları metni önce sınırlı bir byte alfabesine dönüştürerek bu problemi farklı biçimde ele alıyor.
Yine de algoritmayı sıfırdan yazınca tokenizer'ın ne yaptığını çok daha net görebiliyoruz. Word-level yaklaşımın büyük ve kırılgan vocabulary'siyle character-level yaklaşımın uzun sequence'lari arasında, corpus'un kendi tekrarlarından subword'ler öğreniyoruz.
Ortada sihirli bir kelime ayırıcı yok. Yalnızca frekans sayımı, yan yana duran iki sembol ve tekrar tekrar uygulanan bir merge işlemi var.
Evet, bu noktaya kadar geldiyseniz artık büyük modeller nasıl tokenization yapıyor siz de fikir sahibisiniz. Okuduğunuz için teşekkürler.
Yeni makale yayınlandığında haberiniz olsun ister misiniz?
Yeni bir yazı yayınlandığında size e-posta gelsin. Dilediğiniz an iptal edebilirsiniz.
Yorumlar
Benzer makaleler
GPT'yi Sıfırdan Yazalım: nano-gpt
27 Haz 2026 · 13 dk okuma
Attention'ı Sıfırdan Anlamak
23 Haz 2026 · 3 dk okuma
Gradient Descent Gerçekte Nasıl Çalışır
22 Haz 2026 · 4 dk okuma
Kimlik-Farkında Negatif Örnekleme Neden Önemli
20 Haz 2026 · 3 dk okuma
Claude Code ile Verimli Çalışmak — Bölüm 1: Bağlam (Context), Beceriler ve Prompt Yazımı
1 Tem 2026 · 6 dk okuma
Bu Blog Ne Hakkında?
26 Haz 2026 · 2 dk okuma
Tensor Nedir? Derin Öğrenme için Lineer Cebir
24 Haz 2026 · 4 dk okuma