सामग्री पर जाएँ
7/30अध्याय 7 / 30

BPE Tokenizer बनाएं: आपका model R क्यों नहीं गिन पाता

60 लाइनों में byte-pair encoder train करें, देखें वह “the” खुद खोजता है—और मापें कि Spanish paragraph 39 % महंगा क्यों पड़ता है।

इस पेज पर

एक ऐसे model से पूछिए जो bar exam पास कर सकता है कि strawberry में कितने अक्षर r हैं, और ठीक-ठाक संभावना है कि वह दो कहेगा।

आम व्याख्या यह होती है कि language models “गिनने में खराब” हैं या “वास्तव में समझते नहीं”। दोनों बातें unfalsifiable हैं और कोई भी असली कारण नहीं है। कारण यांत्रिक है, model चलने से पहले होता है, और आप उसे एक ही पंक्ति में देख सकते हैं:

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

model दस अक्षरों को नहीं देख रहा। वह तीन संख्याएं देख रहा है। r गिनने के लिए उसे अकेले token 496 की पहचान से यह जानना होगा कि एक ऐसी string के भीतर कितने r हैं जिसे वह देख ही नहीं सकता — और फिर 675 तथा 15717 के लिए भी यही करना होगा और उन्हें जोड़ना होगा। उससे ऐसी representation के बारे में सवाल पूछा जा रहा है जिस तक उसकी पहुंच नहीं है।

यह chapter वही चीज बनाता है जो वे तीन संख्याएं पैदा करती है। इसमें लगभग साठ पंक्तियां लगती हैं, यह वही algorithm है जिसे हर बड़ा model इस्तेमाल करता है, और इसे लिख लेने के बाद दर्जन भर असंबंधित-सी लगने वाली अजीबियां एक ही कारण में सिमट जाती हैं।

अक्षर क्यों नहीं, और शब्द क्यों नहीं

सेक्शन का लिंक: अक्षर क्यों नहीं, और शब्द क्यों नहीं

किसी network को text देने के दो स्पष्ट तरीके हैं और दोनों ऐसे कारणों से विफल होते हैं जिन्हें समझना उपयोगी है, क्योंकि वही विफलता समाधान का आकार तय करती है।

शब्द। spaces पर split करें, हर शब्द को एक संख्या दें। English में शब्द-रूपों की संख्या सैकड़ों हजार है और model को हर एक के लिए एक embedding row चाहिए, इसलिए vocabulary — और output layer, जिसे हर entry के लिए score बनाना होता है — विशाल हो जाती है। इससे भी बुरा inference पर होता है: training में model ने जो शब्द कभी नहीं देखा, उसका कोई number नहीं होता। यही out-of-vocabulary समस्या है, और आम patch यह है कि हर unknown चीज को एक ही <UNK> token में map कर दिया जाए, जिससे information फेंक दी जाती है। साथ ही “word” कोई स्पष्ट रूप से परिभाषित अवधारणा नहीं है: Chinese और Japanese में शब्दों के बीच spaces नहीं होते, और German nouns को अनिश्चित रूप से जोड़ता चला जाता है।

Characters। out-of-vocabulary समस्या नहीं, और vocabulary केवल सौ-के-लगभग symbols की। लेकिन sequences बहुत लंबी हो जाती हैं, और Chapter 9 दिखाएगा कि attention cost sequence length के साथ quadratically बढ़ती है। 1000-word document लगभग 5000 characters का होता है — जरूरत से चार से पांच गुना लंबी sequence, और quadratic कीमत के साथ। और हर character अकेले लगभग कोई meaning नहीं रखता, इसलिए पहली कुछ layers उन शब्दों को फिर से जोड़ने में खर्च होती हैं जिन्हें tokenizer intact दे सकता था।

उत्तर इनके बीच है: subwords। Common words एक token बन जाते हैं, rare words टुकड़ों में split होते हैं, और कुछ भी कभी unknown नहीं होता क्योंकि टुकड़े अंत में individual bytes तक पहुंचते हैं। दिलचस्प बात यह है कि split कोई design नहीं करता। tokenizer train किया जाता है, model जैसे data पर, और वह गिनकर सीखता है कि कौन-सी byte sequences अपने number की हकदार हैं क्योंकि वे कितनी बार साथ आती हैं।

यह algorithm 1994 का है, और यह एक compression algorithm था। Philip Gage ने इसे C Users Journal में files को छोटा करने के तरीके के रूप में प्रकाशित किया था: adjacent bytes की सबसे frequent pair को बार-बार ऐसे byte से replace करना जो data में मौजूद न हो।1 यह बाईस साल तक वहीं पड़ा रहा, जब तक Sennrich, Haddow और Birch ने 2016 में out-of-vocabulary समस्या हल करने के लिए इसे machine translation में repurpose नहीं किया।2 अब essentially हर large language model इसी तरह पढ़ता है।

training loop चार steps को दोहराता है:

training text को UTF-8 के रूप में encode करें। हर byte value 0–255 एक token है। Vocabulary size: 256।

sequence पर चलें और गिनें कि neighbouring tokens की हर pair कितनी बार आती है।

winner लें, उसके लिए नया token id mint करें, और sequence में उसकी हर occurrence replace करें। vocabulary एक से बढ़ती है; sequence छोटी हो जाती है।

pair और वह जिस id में बदली, उसे क्रम में store करें। वही ordered list ही tokenizer है — बाद में नए text को encode करने के लिए यही सब कुछ चाहिए।

यह पूरा trainer है:

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

इसे English prose के 151,191 bytes पर चलाइए और पहले बारह merges जैसे-जैसे होते हैं, print कीजिए। यह हिस्सा धीरे पढ़ने लायक है, क्योंकि algorithm को English के बारे में किसी ने कुछ नहीं बताया:

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)

उस list में तीन बातें ध्यान देने लायक हैं।

Merge 12 शब्द “the” है — उससे पहले और बाद के space सहित, एक single unit के रूप में, pair गिनने वाले loop की बारहवीं iteration में खोजा गया। किसी ने dictionary नहीं दी। यह वहां इसलिए है क्योंकि English में ये पांच bytes किसी भी दूसरे पांच bytes से ज्यादा साथ आते हैं।

Merge 3 text है ही नहीं। \xe2\x80 typographic punctuation — em dash, curly quotes — की UTF-8 encoding के पहले दो bytes हैं। algorithm को कोई idea नहीं कि UTF-8 मौजूद है, और उसने बस उसकी structure का एक टुकड़ा फिर से खोज लिया है, क्योंकि multi-byte encodings construction से ही ऐसी byte sequences होती हैं जो हमेशा साथ दिखाई देती हैं।

शुरुआती merges में से अधिकतर में space शामिल होता है, और space आमतौर पर left पर होता है। यही practice में दिखने वाले सबसे confusing behaviours में से एक का मूल है, जिस पर हम थोड़ी देर में लौटेंगे।

हर merge sequence को छोटा और vocabulary को बड़ा बनाता है। इसे कितना आगे बढ़ाना है, यह असली decision है, और इसे मापा जा सकता है — यहां उन्हीं 151,191 bytes पर:

vocabulary sizeresulting tokenscompression (bytes per token)
300101,0651.50
51268,2492.22
1,02450,3693.00
2,04839,3063.85
4,09630,7574.92

Diminishing returns साफ दिखते हैं। 512 से 1024 तक double करने पर 0.78 bytes per token मिलते हैं; 2048 से 4096 तक double करने पर 1.07 — यहां बेहतर केवल इसलिए क्योंकि यह corpus इतना छोटा है कि लंबे merges अब भी payoff देते रहते हैं। real corpus पर curve बहुत तेजी से flatten होती है।

और बड़ी vocabulary की cost केवल memory नहीं है। हर token को एक embedding row चाहिए, और — अधिक महंगे तरीके से — model की output layer को हर step पर vocabulary की हर entry के लिए score produce करना पड़ता है, इसलिए final matrix multiply vocabulary size के साथ scale करता है। Real models 32,000 और 200,000 के बीच बैठते हैं: GPT-2 ने 50,257 इस्तेमाल किए, GPT-4 का cl100k 100,277 इस्तेमाल करता है, GPT-4o का o200k लगभग इसका दोगुना है। trend ऊपर की ओर है, और कारण अगले section में है।

यह वही paragraph है, translated, OpenAI द्वारा ship किए जाने वाले real tokenizers से मापा गया:

languagecharacterstokens (cl100k)tokens (o200k)tokens/charoverhead vs English
English16431310.189
Spanish16943360.254+39 %
Russian17878430.438+152 %
Japanese7279581.097+155 %

वही content, वही meaning, और cl100k के साथ Russian version ढाई गुना tokens खाता है। चूंकि APIs per token bill करती हैं और context windows tokens में measure होते हैं, यह कोई linguistic curiosity नहीं है — यह budget की line item है, छोटा effective context window है, और धीमा response है, तीनों एक साथ, हर उस व्यक्ति के लिए जो English में काम नहीं करता।

mechanism training data है। ज्यादातर English पर trained tokenizer अपना merge budget English byte sequences पर खर्च करता है। Spanish Latin alphabet share करती है इसलिए उसे अभी भी कुछ benefit मिलता है; Russian को लगभग कोई नहीं, क्योंकि Cyrillic characters UTF-8 में दो bytes लेते हैं और उन pairs में से कम ही training corpus में इतने common थे कि merge कमा सकें। Japanese इससे भी खराब है: प्रति character तीन bytes, और 72 characters 79 tokens बन जाते हैं — characters से ज्यादा tokens

o200k column दिखाता है कि यह solvable problem है और इसे solve किया जा रहा है। vocabulary double करने और training data rebalance करने से Spanish overhead +39 % से +16 % पर, और Russian +152 % से +39 % पर आ जाता है। यही असली कारण है कि vocabularies बढ़ती जा रही हैं: compression अपने-आप में नहीं, बल्कि यह तथ्य कि पिछली generation दुनिया के बड़े हिस्से से चुपचाप extra charge कर रही थी।

Encoding, और merges का order क्यों मायने रखता है

सेक्शन का लिंक: Encoding, और merges का order क्यों मायने रखता है

Training ने merges की ordered list बनाई। नए text को encode करना उसे replay करता है — और उसे उसी order में replay करना होगा, क्योंकि merge 12, merges 5 और 1 के results को combine करता है। उन्हें अलग order में apply करें और आपको अलग, गलत tokenization मिलेगी जो training में model ने जो देखा था, उससे match नहीं करेगी।

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 trivial है: हर id के bytes look up करें, concatenate करें, UTF-8 के रूप में decode करें। errors="replace" पर ध्यान दें: model ऐसी token sequence emit कर सकता है जो mid-character खत्म होती है, और यह hypothetical नहीं है — यही होता है जब streaming response emoji के बीच में cut off हो जाता है, इसलिए streaming APIs token by token decode करने के बजाय partial bytes buffer करती हैं।

Round-tripping किसी भी चीज पर काम करता है, यही byte-level BPE का promise है:

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

mechanism साफ हो जाने पर, unrelated-looking शिकायतों का एक set दरअसल वही शिकायत निकलता है।

Arithmetic। Numbers किसी consistent तरीके से split नहीं होते:

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 और 12345 जोड़ने के लिए model को पहले समझना होगा कि ['123','4'] और ['123','45'] ऐसी numbers हैं जिनके digits एक खास तरीके से align होते हैं — और alignment हर pair of numbers के लिए अलग है। किसी number के digits एक number से अगले number तक समान जगहों पर नहीं होते। कुछ newer tokenizers digits को consistent groups of three में split करने के लिए force करते हैं, ठीक इसी obstacle को हटाने के लिए, और उनसे trained models arithmetic में measurably बेहतर होते हैं।

Python indentation।

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

चार spaces और आठ spaces अलग single tokens हैं, और tab उसके बाद वाले character के साथ fuse है। Indentation, जो Python में syntax है, inconsistently represented है — यही एक बड़ा कारण है कि models पहले subtly wrong indentation वाला Python produce करते थे, और code-focused tokenizers common indentation runs के लिए explicit tokens जोड़ते हैं।

Spelling और reversing। वही कारण जो r गिनने में है: model से strawberry reverse करने को कहना तीन opaque ids के भीतर letters को reorder करने को कहना है। Models training के दौरान spellings memorize करके ऐसा करते हैं, देखने से नहीं, इसलिए common words पर अच्छा और rare words पर खराब करते हैं।

Glitch tokens। सबसे striking case SolidGoldMagikarp और similar strings का set है, जिसने GPT-2 और GPT-3 को bizarre behave कराया — उन्हें repeat करने से मना करना, unrelated output produce करना, कभी-कभी user को insult करना। व्याख्या mundane है और सीधे इस fact से निकलती है कि tokenizer model से अलग train होता है: वे strings tokenizer के training corpus में frequent थीं (वे Reddit usernames थीं), इसलिए उन्होंने अपना token कमा लिया, लेकिन model के training corpus में rare या absent थीं। परिणाम एक embedding row है जो randomly initialise हुई और लगभग कभी update नहीं हुई। model के पास एक symbol है जिसे उसने essentially कभी देखा ही नहीं, और वहां उसका behaviour वही है जो random initialisation से संयोगवश निकला।

WordPiece, जिसे BERT इस्तेमाल करता है, selection rule में BPE से अलग है: सबसे frequent pair merge करने के बजाय, यह वह pair merge करता है जो training data की likelihood को सबसे ज्यादा बढ़ाता है — जो इस बात से normalise करता है कि parts पहले से कितने common हैं, इसलिए दो rare pieces की pair दो common pieces की pair को हरा सकती है।

Unigram, Kudo से, उल्टा काम करता है: बड़े candidate vocabulary से शुरू करें और iteratively उन pieces को remove करें जिनकी deletion corpus likelihood को सबसे कम चोट पहुंचाती है। यह हर segmentation को probability भी देता है, जिससे same string की different tokenizations को regulariser की तरह sample किया जा सकता है।

SentencePiece वह implementation है जिसे अधिकतर non-English models इस्तेमाल करते हैं। इसका contribution input को बिना किसी pre-tokenization के raw stream की तरह treat करना है, space को visible character की तरह encode करना है, यानी यह उन भाषाओं के लिए भी identically काम करता है जो words को spaces से अलग नहीं करतीं। नीचे यह BPE या Unigram, किसी पर भी run कर सकता है।

इसकी कीमत क्या है, और बदले में क्या मिलता है

सेक्शन का लिंक: इसकी कीमत क्या है, और बदले में क्या मिलता है

tokenizer text और numbers के बीच एक lossy interface है, और इस chapter का हर strange behaviour वही interface झलकाता है। यह साफ रखना जरूरी है कि यह trade deliberate है: byte-level BPE का मतलब है कोई input कभी unrepresentable नहीं, sequences characters की तुलना में चार से पांच गुना छोटी हैं, और common words intact आते हैं।

कीमत यह है कि model के atoms हमारे atoms नहीं हैं। वह ऐसे text के बारे में reason करता है जिसे वह spell नहीं कर सकता, उन units में जिन्हें ऐसे corpus पर frequency count ने चुना जिसे उसने देखा नहीं, और per-language cost के साथ जिस पर किसी ने negotiation नहीं किया।

अब आपके पास integers की sequence है। Part II के बाकी सब के लिए यही input format है।

आपके पास अभी कोई कारण नहीं है कि एक integer के बाद दूसरा क्यों आए। अगला chapter वह objective introduce करता है जिस पर हर language model train होता है, और वह चौंकाने वाली तरह से simple है: अब तक के tokens दिए गए हों, तो अगला predict करें। वही single objective — कोई labels नहीं, कोई annotation नहीं, बस text जिसका अपना future target है — पूरे internet को training data में बदल देता है, और यहीं से model की पहली genuine representations आती हैं।

इसके लिए Chapter 2 का probability का chain rule बिल्कुल सही होना भी जरूरी है, क्योंकि यह दावा कि एक बार में एक token predict करना पूरे documents को model करने जैसा है, factorisation है, metaphor नहीं।

Chapter 8 autoregressive objective, embeddings, और वह पहली जगह है जहां model कुछ ऐसा सीखता है जो किसी ने वहां डाला नहीं था।


Kudo, T. Subword Regularization: Improving Neural Network Translation Models with Multiple Subword Candidates (arXiv:1804.10959) Unigram model introduce करता है; Kudo and Richardson, SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing (arXiv:1808.06226) वह implementation है जिसे अधिकतर multilingual models इस्तेमाल करते हैं; Schuster and Nakajima, Japanese and Korean Voice Search (ICASSP 2012) WordPiece की origin है। Andrej Karpathy का Let's build the GPT Tokenizer और accompanying karpathy/minbpe repository इस chapter के code के direct ancestors हैं और काफी आगे जाते हैं, जिसमें GPT-4 regex और special-token handling शामिल हैं। Hugging Face LLM Course का Chapter 6 तीनों algorithms को worked examples के साथ side by side cover करता है।

  1. Gage, P. A New Algorithm for Data Compression. The C Users Journal 12(2), pp. 23–38 (1994). language models के लिए इस्तेमाल होने से बाईस साल पहले byte-pair encoding एक compression scheme के रूप में।

  2. Sennrich, R., Haddow, B. and Birch, A. Neural Machine Translation of Rare Words with Subword Units. arXiv:1508.07909 (2015; ACL 2016). वह paper जिसने translation में out-of-vocabulary words से प्रेरित होकर BPE को NLP में लाया।

  3. Radford, A., Wu, J., Child, Luan, D., Amodei, D. and Sutskever, I. Language Models are Unsupervised Multitask Learners (2019). Section 2.2 ऊपर discuss किए गए pre-tokenization regex के साथ byte-level BPE introduce करता है।


निर्माता

David Vicente Campos

NeuraLIA Labs के संस्थापक और MyRealFood के सह-संस्थापक

मैं लेओन विश्वविद्यालय से कंप्यूटर इंजीनियर हूँ। मैंने MyRealFood की सह-स्थापना की, जहाँ CTO के रूप में मैंने वह ऐप बनाया जिसे लाखों लोग बेहतर खान-पान के लिए इस्तेमाल कर चुके हैं, और मैंने NeuraLIA Labs की स्थापना की, जहाँ मैं AI प्रोडक्ट्स बनाता हूँ। यहाँ मैं उन बातों के बारे में लिखता हूँ जो इस सफ़र में मुझे समझनी पड़ीं, उस तरह जिस तरह काश किसी ने मुझे समझाई होतीं।

लेखक के बारे में और जानें

NeuraLIA Labs द्वारा प्रकाशित।

नए पोस्ट अपने इनबॉक्स में पाएं

AI समाचार, गाइड और प्रोडक्ट अपडेट — जब भी हम कुछ उपयोगी प्रकाशित करें, एक छोटा ईमेल।

कोर्स सूची

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev13 मिनट पढ़ें

Jev AI मॉडल गद्य के लिए नहीं, निर्णयों के लिए बना है

TypeSafe AI का Jev ध्यान खींच रहा है क्योंकि यह सॉफ्टवेयर इंटेलिजेंस को संभावना की समस्या मानता है: सही शाखा चुनें, भरोसे का स्तर जोड़ें, और जब कोड को निर्णय चाहिए तो LLM से टेक्स्ट लिखवाने पर खर्च न करें।

Abstract agent runtime sorting documents, memory blocks and pointer nodes inside a bounded context frame.
context-engineering14 मिनट पढ़ें

लॉन्ग-होराइजन AI एजेंट्स के लिए कॉन्टेक्स्ट इंजीनियरिंग

लंबे समय तक चलने वाले एजेंट सिर्फ इसलिए असफल नहीं होते कि विंडो छोटी है। वे तब असफल होते हैं जब फ़ाइलें, टूल आउटपुट और पुराना इतिहास उस काम को ही पीछे धकेल देते हैं जिसे एजेंट को पूरा करना था।

मॉडल चुनने का काम LIA पर छोड़ने के लिए तैयार हैं?

हर AI मॉडल एक ही जगह — आज ही मुफ़्त शुरू करें।