İçeriğe geç
7/3030 bölümden 7. bölüm

BPE Tokenizer Oluştur: Modelin Neden R’leri Sayamıyor?

60 satırda bir byte-pair encoder eğit; “the” kelimesini kendi keşfetsin, sonra İspanyolcada %39 daha pahalı olduğunu ölç.

Bu sayfada

Baro sınavını geçebilen bir modele strawberry kelimesinde kaç tane r harfi olduğunu sor; iki deme ihtimali hiç de düşük değildir.

Yaygın açıklama, dil modellerinin “saymada kötü” olduğu ya da “gerçekten anlamadığı”dır. İkisi de yanlışlanamaz ve nedeni de bu değildir. Neden mekaniktir, model çalışmadan önce gerçekleşir ve bunu tek satırda görebilirsin:

TEXT
'strawberry'  ->  3 tokens  [496, 675, 15717]  ['str', 'aw', 'berry']

Model on harfe bakmıyor. Üç sayıya bakıyor. r’leri saymak için, yalnızca token 496’nın kimliğinden, göremediği bir string içinde kaç r olduğunu bilmesi — sonra aynısını 675 ve 15717 için yapıp toplamaları — gerekir. Erişemediği bir temsil hakkında soru soruluyor.

Bu bölüm, o üç sayıyı üreten şeyi oluşturuyor. Yaklaşık altmış satır sürüyor, tüm büyük modellerin kullandığı algoritmanın aynısı ve bir kez yazınca, birbirinden bağımsız görünen bir düzine tuhaflık tek bir nedene bağlanıyor.

Metni bir ağa vermenin iki bariz yolu vardır ve ikisi de anlamaya değer nedenlerle başarısız olur; çünkü bu başarısızlık çözümün şeklini belirler.

Kelimeler. Boşluklardan böl, her kelimeye bir sayı ata. İngilizcede yüz binlerce kelime biçimi vardır ve modelin her biri için bir embedding satırına ihtiyacı olur; bu yüzden vocabulary — ve her giriş için skor üretmek zorunda olan çıktı katmanı — devasa hale gelir. Daha da kötüsü inference sırasında olur: modelin eğitimde hiç görmediği bir kelimenin numarası yoktur. Bu out-of-vocabulary problemidir ve yaygın yama, bilinmeyen her şeyi tek bir <UNK> token’a eşlemektir; bu da bilgiyi çöpe atar. Ayrıca “kelime” iyi tanımlanmış bir kavram değildir: Çince ve Japonca kelimelerin arasına boşluk koymaz, Almanca ise isimleri süresiz biçimde birbirine ekler.

Karakterler. Out-of-vocabulary problemi yoktur ve vocabulary yüz civarı sembolden oluşur. Ama diziler çok uzar ve Bölüm 9 attention maliyetinin dizi uzunluğuyla karesel büyüdüğünü gösterecek. 1000 kelimelik bir belge yaklaşık 5000 karakterdir — olması gerekenden dört ila beş kat daha uzun bir dizi ve bunun karesel bir bedeli vardır. Ayrıca her karakter tek başına neredeyse hiç anlam taşımaz; bu yüzden ilk birkaç katman, tokenizer’ın zaten bütün halde verebileceği kelimeleri yeniden birleştirmekle harcanır.

Cevap ikisinin arasındadır: subword’ler. Yaygın kelimeler tek token olur, nadir kelimeler parçalara ayrılır ve hiçbir şey asla bilinmeyen olmaz; çünkü parçalar en altta tekil byte’lara kadar iner. İlginç olan, bölünmeyi kimsenin tasarlamamasıdır. Tokenizer eğitilir, modelle aynı tür veriler üzerinde, ve hangi byte dizilerinin kendi numarasına değeceğini birlikte ne sıklıkla göründüklerini sayarak öğrenir.

Algoritma 1994’ten gelir ve bir sıkıştırma algoritmasıydı. Philip Gage bunu C Users Journal’da, bitişik byte’ların en sık görülen çiftini veride bulunmayan bir byte ile tekrar tekrar değiştirerek dosyaları küçültmenin bir yolu olarak yayımladı.1 Sennrich, Haddow ve Birch 2016’da out-of-vocabulary problemini çözmek için makine çevirisine uyarlayana kadar yirmi iki yıl orada durdu.2 Bugün neredeyse her büyük dil modelinin okuma biçimi budur.

Eğitim döngüsü tekrar eden dört adımdır:

Eğitim metnini UTF-8 olarak encode et. Her byte değeri 0–255 bir token’dır. Vocabulary boyutu: 256.

Dizi boyunca yürü ve komşu token çiftlerinin ne sıklıkla göründüğünü say.

Kazananı al, onun için yeni bir token id bas ve dizideki her oluşumu değiştir. Vocabulary bir artar; dizi kısalır.

Çifti ve dönüştüğü id’yi sırayla sakla. Bu sıralı liste tokenizer’ın kendisidir — daha sonra yeni metni encode etmek için gereken her şey odur.

Trainer’ın tamamı burada:

bpe.pyPYTHON
def get_stats(ids):
    counts = {}
    for a, b in zip(ids, ids[1:]):
        counts[(a, b)] = counts.get((a, b), 0) + 1
    return counts


def merge(ids, pair, idx):
    out, i = [], 0
    while i < len(ids):
        if i < len(ids) - 1 and ids[i] == pair[0] and ids[i + 1] == pair[1]:
            out.append(idx)
            i += 2
        else:
            out.append(ids[i])
            i += 1
    return out


class BPE:
    def __init__(self):
        self.merges = {}
        self.vocab = {i: bytes([i]) for i in range(256)}

    def train(self, text, vocab_size):
        ids = list(text.encode("utf-8"))
        for i in range(vocab_size - 256):
            stats = get_stats(ids)
            if not stats:
                break
            pair = max(stats, key=stats.get)      
            idx = 256 + i                         
            ids = merge(ids, pair, idx)           
            self.merges[pair] = idx               
            self.vocab[idx] = self.vocab[pair[0]] + self.vocab[pair[1]]
        return ids

Bunu 151.191 byte İngilizce düzyazı üzerinde çalıştır ve gerçekleşirken ilk on iki merge’ü yazdır. Yavaş okunmaya değer kısım burası; çünkü algoritmaya İngilizce hakkında hiçbir şey söylenmedi:

TEXT
merge   1: b'e' + b' '   -> b'e '     (occurred 4433 times)
merge   2: b' ' + b't'   -> b' t'     (occurred 3302 times)
merge   3: b'\xe2' + b'\x80' -> b'\xe2\x80'  (occurred 3247 times)
merge   4: b' ' + b'a'   -> b' a'     (occurred 2335 times)
merge   5: b' t' + b'h'  -> b' th'    (occurred 2253 times)
merge   6: b'i' + b'n'   -> b'in'     (occurred 2011 times)
merge   7: b't' + b' '   -> b't '     (occurred 1904 times)
merge   8: b'e' + b'r'   -> b'er'     (occurred 1813 times)
merge   9: b'd' + b' '   -> b'd '     (occurred 1703 times)
merge  10: b'o' + b'u'   -> b'ou'     (occurred 1554 times)
merge  11: b' ' + b's'   -> b' s'     (occurred 1467 times)
merge  12: b' th' + b'e '-> b' the '  (occurred 1270 times)

Bu listede özellikle işaret edilmeye değer üç şey var.

Merge 12, “the” kelimesidir — önündeki ve arkasındaki boşlukla birlikte tek bir birim olarak; çiftleri sayan bir döngünün on ikinci iterasyonunda keşfedilmiştir. Kimse sözlük vermedi. Orada çünkü bu beş byte İngilizcede diğer tüm beşlilerden daha sık birlikte görülüyor.

Merge 3 metin bile değildir. \xe2\x80 tipografik noktalamanın UTF-8 encoding’inin ilk iki byte’ıdır — em dash, kıvrımlı tırnaklar. Algoritmanın UTF-8 diye bir şeyden haberi yoktur ve az önce onun yapısının bir parçasını yeniden keşfetmiştir; çünkü çok byte’lı encoding’ler, tanım gereği her zaman birlikte görünen byte dizileridir.

Erken merge’lerin çoğu boşluk içerir ve boşluk genellikle soldadır. Pratikte en kafa karıştırıcı davranışlardan birinin kökeni budur; birazdan buna döneceğiz.

Her merge diziyi kısaltır ve vocabulary’yi büyütür. Ne kadar ileri gidileceği gerçek bir karardır ve ölçülebilir — burada aynı 151.191 byte üzerinde:

vocabulary boyutuortaya çıkan token sayısısıkıştırma (token başına byte)
300101.0651,50
51268.2492,22
1.02450.3693,00
2.04839.3063,85
4.09630.7574,92

Azalan getiriler açıkça görünüyor. 512’den 1024’e ikiye katlamak token başına 0,78 byte kazandırıyor; 2048’den 4096’ya ikiye katlamak 1,07 kazandırıyor — burada daha iyi görünmesinin tek nedeni, bu corpus’un daha uzun merge’lerin hâlâ karşılığını verecek kadar küçük olması. Gerçek bir corpus’ta eğri sert biçimde düzleşir.

Daha büyük bir vocabulary’nin maliyeti yalnızca bellek değildir. Her token bir embedding satırı gerektirir ve — daha pahalı olarak — modelin çıktı katmanı her adımda vocabulary’deki her giriş için bir skor üretmek zorundadır; bu yüzden son matris çarpımı vocabulary boyutuyla ölçeklenir. Gerçek modeller 32.000 ile 200.000 arasında durur: GPT-2 50.257 kullandı, GPT-4’ün cl100k’i 100.277 kullanır, GPT-4o’nun o200k’i bunu kabaca ikiye katlar. Eğilim yukarı doğru ve nedeni bir sonraki bölümde.

Aynı paragrafın çevirileri, OpenAI’ın sunduğu gerçek tokenizer’larla ölçülmüş haliyle burada:

dilkaraktertoken (cl100k)token (o200k)token/karakterİngilizceye göre ek yük
İngilizce16431310,189
İspanyolca16943360,254+%39
Rusça17878430,438+%152
Japonca7279581,097+%155

Aynı içerik, aynı anlam; cl100k ile Rusça sürüm token’ların iki buçuk katını tüketiyor. API’ler token başına ücretlendirdiği ve context window’lar token ile ölçüldüğü için bu dilbilimsel bir merak değil — İngilizce çalışmayan herkes için aynı anda bütçede bir kalem, daha kısa etkili context window ve daha yavaş yanıt demek.

Mekanizma eğitim verisidir. Çoğunlukla İngilizce üzerinde eğitilmiş bir tokenizer, merge bütçesini İngilizce byte dizilerine harcar. İspanyolca Latin alfabesini paylaştığı için hâlâ biraz fayda görür; Rusça neredeyse hiç görmez, çünkü Kiril karakterleri UTF-8’de iki byte alır ve bu çiftlerin çok azı eğitim corpus’unda bir merge kazanacak kadar yaygındır. Japonca daha da kötüdür: karakter başına üç byte ve 72 karakter 79 token’a dönüşür — karakterden daha fazla token.

o200k sütunu bunun çözülebilir bir problem olduğunu ve çözülmekte olduğunu gösteriyor. Vocabulary’yi ikiye katlamak ve eğitim verisini yeniden dengelemek İspanyolca ek yükünü +%39’dan +%16’ya, Rusça ek yükünü +%152’den +%39’a indiriyor. Vocabulary’lerin büyümeye devam etmesinin gerçek nedeni budur: kendi başına sıkıştırma değil, önceki neslin dünyanın büyük bir kısmından sessizce fazladan ücret alıyor olması.

Encoding ve merge sırasının neden önemli olduğu

Bölüme bağlantı: Encoding ve merge sırasının neden önemli olduğu

Eğitim sıralı bir merge listesi üretti. Yeni metni encode etmek onu yeniden oynatır — ve bunu aynı sırayla yapmak zorundadır; çünkü merge 12, merge 5 ve 1’in sonuçlarını birleştirir. Farklı sırayla uygularsan, modelin eğitimde gördüğü hiçbir şeyle eşleşmeyen farklı ve yanlış bir tokenization elde edersin.

bpe.py (continued)PYTHON
    def encode(self, text):
        ids = list(text.encode("utf-8"))
        while len(ids) >= 2:
            stats = get_stats(ids)
            # the pair whose merge came FIRST during training wins   
            pair = min(stats, key=lambda p: self.merges.get(p, float("inf")))   
            if pair not in self.merges:
                break
            ids = merge(ids, pair, self.merges[pair])
        return ids

    def decode(self, ids):
        return b"".join(self.vocab[i] for i in ids).decode("utf-8", errors="replace")

Decoding karşılaştırıldığında basittir: her id’nin byte’larını bul, birleştir, UTF-8 olarak decode et. errors="replace"’e dikkat: bir model karakterin ortasında biten bir token dizisi emit edebilir ve bu varsayımsal değildir — streaming yanıt bir emoji’nin ortasında kesildiğinde olan budur; bu yüzden streaming API’ler token token decode etmek yerine kısmi byte’ları buffer’lar.

Round-trip her şeyde çalışır; byte-level BPE’nin vaadi budur:

TEXT
'strawberry'                            -> 6 tokens, decode == original: True
'Alice was beginning to get very tired' -> 14 tokens, decode == original: True
'café — naïve — 日本語'                   -> 23 tokens, decode == original: True

Mekanizma netleşince, birbirinden bağımsız görünen bir dizi şikâyetin aynı şikâyet olduğu ortaya çıkar.

Aritmetik. Sayılar tutarlı bir şekilde bölünmez:

TEXT
1234     -> 2 tokens  ['123', '4']
12345    -> 2 tokens  ['123', '45']
1000000  -> 3 tokens  ['100', '000', '0']
3.14159  -> 4 tokens  ['3', '.', '141', '59']
2024     -> 2 tokens  ['202', '4']

1234 ile 12345’yi toplamak için modelin önce ['123','4'] ve ['123','45']’ün basamakları belirli bir biçimde hizalanan sayılar olduğunu çözmesi gerekir — ve hizalama her sayı çifti için farklıdır. Bir sayının basamakları, bir sayıdan diğerine aynı yerlerde değildir. Bazı yeni tokenizer’lar, bu engeli tam olarak kaldırmak için rakamları tutarlı üçlü gruplara bölmeye zorlar; bunlarla eğitilen modellerin aritmetikte ölçülebilir biçimde daha iyi olduğu görülür.

Python girintisi.

TEXT
'    x = 1'      -> 5 tokens  ['   ', ' x', ' =', ' ', '1']
'        x = 1'  -> 5 tokens  ['       ', ' x', ' =', ' ', '1']
'\tx = 1'        -> 4 tokens  ['\tx', ' =', ' ', '1']

Dört boşluk ve sekiz boşluk farklı tekil token’lardır ve tab, kendisinden sonraki karakterle birleşmiştir. Python’da syntax olan girinti tutarsız biçimde temsil edilir — modellerin eskiden hafifçe yanlış girintili Python üretmesinin büyük nedeni budur; code odaklı tokenizer’ların yaygın girinti dizileri için açık token’lar eklemesinin nedeni de.

Yazım ve ters çevirme. r’leri saymadakiyle aynı neden: bir modelden strawberry’yi tersine çevirmesini istemek, ondan üç opak id’nin içindeki harfleri yeniden sıralamasını istemektir. Modeller bunu bakarak değil, eğitim sırasında yazımları ezberlemiş olarak yapar; bu yüzden yaygın kelimelerde iyi, nadir kelimelerde kötü yaparlar.

Glitch token’lar. En çarpıcı örnek SolidGoldMagikarp ve GPT-2 ile GPT-3’ün tuhaf davranmasına neden olan benzer string’ler kümesidir — bunları tekrar etmeyi reddetmek, alakasız output üretmek, bazen kullanıcıya hakaret etmek. Açıklama sıradandır ve tokenizer’ın modelden ayrı eğitilmesi gerçeğinden doğrudan çıkar: bu string’ler tokenizer’ın eğitim corpus’unda sıktı (Reddit kullanıcı adlarıydılar), bu yüzden kendi token’larını kazandılar; ama modelin eğitim corpus’unda nadir ya da yoktular. Sonuç, rastgele başlatılmış ve neredeyse hiç güncellenmemiş bir embedding satırıdır. Modelin esasen hiç görmediği bir sembol vardır ve oradaki davranışı, rastgele ilklendirmenin ne olduğuna bağlıdır.

WordPiece, BERT tarafından kullanılır ve seçim kuralı bakımından BPE’den ayrılır: en sık çifti merge etmek yerine, eğitim verisinin likelihood’unu en çok artıran çifti merge eder — bu, parçaların zaten ne kadar yaygın olduğunu normalize eder; dolayısıyla iki nadir parçadan oluşan bir çift, iki yaygın parçadan oluşan bir çifti geçebilir.

Unigram, Kudo’dan gelir ve ters yönde çalışır: büyük bir aday vocabulary ile başla ve silinmesi corpus likelihood’una en az zarar veren parçaları iteratif olarak kaldır. Ayrıca her segmentation’a bir olasılık verir; bu da aynı string’in farklı tokenization’larını regularizer olarak örneklemeye izin verir.

SentencePiece, İngilizce dışı modellerin çoğunun kullandığı implementation’dır. Katkısı, input’u hiç pre-tokenization olmadan raw bir akış olarak ele almak ve boşluğu görünür bir karakter olarak encode etmektir; bu da kelimeleri boşlukla ayırmayan dillerde aynı şekilde çalıştığı anlamına gelir. Altta BPE veya Unigram çalıştırabilir.

Tokenizer, metin ile sayılar arasında kayıplı bir arayüzdür ve bu bölümdeki her tuhaf davranış, arayüzün kendini göstermesidir. Takasın bilinçli olduğunu net söylemek gerekir: byte-level BPE hiçbir input’un temsil edilemez olmamasını sağlar, diziler karakterlere göre dört ila beş kat daha kısadır ve yaygın kelimeler bütün halde gelir.

Bedeli şu: modelin atomları bizim atomlarımız değildir. Yazamadığı metin üzerinde, görmediği bir corpus’un frekans sayımıyla seçilmiş birimlerde, kimsenin pazarlık etmediği dil başına bir maliyetle akıl yürütür.

Artık bir tam sayı dizin var. Bu, Bölüm II’nin geri kalanındaki her şeyin input formatıdır.

Elinde olmayan şey, bir tam sayının neden diğerini takip etmesi gerektiğine dair herhangi bir nedendir. Bir sonraki bölüm, her dil modelinin eğitildiği objective’i tanıtıyor ve şaşırtıcı derecede basit: şimdiye kadarki token’lar verildiğinde, bir sonrakini tahmin et. Bu tek objective — label yok, annotation yok, yalnızca hedefi kendi geleceği olan metin — tüm interneti eğitim verisine dönüştüren şeydir ve modelin ilk gerçek temsillerinin çıktığı yer burasıdır.

Ayrıca Bölüm 2’deki olasılığın chain rule’unun tam olarak doğru olmasını gerektirir; çünkü her seferinde bir token tahmin etmenin bütün belgeleri modellemekle aynı olduğu iddiası bir factorization’dır, metafor değil.

Bölüm 8, autoregressive objective, embeddings ve bir modelin kimsenin koymadığı bir şeyi ilk kez öğrendiği yerdir.


Kudo, T. Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates (arXiv:1804.10959) Unigram modelini tanıtır; Kudo ve Richardson, SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing (arXiv:1808.06226) çoğu çok dilli modelin kullandığı implementation’dır; Schuster ve Nakajima, Japanese and Korean Voice Search (ICASSP 2012) WordPiece’in kökenidir. Andrej Karpathy’nin Let's build the GPT Tokenizer’ı ve eşlik eden karpathy/minbpe repository’si, bu bölümdeki kodun doğrudan atalarıdır ve GPT-4 regex’i ile special-token handling dahil çok daha ileri gider. Hugging Face LLM Course’un 6. bölümü, üç algoritmayı worked example’larla yan yana ele alır.

  1. Gage, P. A New Algorithm for Data Compression. The C Users Journal 12(2), ss. 23–38 (1994). Dil modellerinde kullanılmasından yirmi iki yıl önce, bir sıkıştırma şeması olarak byte-pair encoding.

  2. Sennrich, R., Haddow, B. and Birch, A. Neural Machine Translation of Rare Words with Subword Units. arXiv:1508.07909 (2015; ACL 2016). BPE’yi NLP’ye taşıyan, çeviride out-of-vocabulary kelimelerden motive olan makale.

  3. Radford, A., Wu, J., Child, R., Luan, D., Amodei, D. and Sutskever, I. Language Models are Unsupervised Multitask Learners (2019). Bölüm 2.2, yukarıda ele alınan pre-tokenization regex’iyle byte-level BPE’yi tanıtır.

Seçimi LIA'ya bırakmaya hazır mısın?

Tüm yapay zeka modelleriyle tek yerde üret — bugün ücretsiz başla.