Part 04 · LLM Internals
Tokenization
Model কখনো অক্ষর দেখে না, শুধু token id দেখে। Byte-pair encoding কীভাবে vocabulary বানায়, কেন English এর চেয়ে Bangla তে বেশি token লাগে, আর সেটা তোমার bill এ কী করে।
⏱ 1-2 ঘণ্টা · Station 24 / 92
জিনিসটা কী, কেন দরকার
তুমি যে অক্ষর টাইপ করো, LLM সেগুলো কখনো দেখে না। Message টা model এ পৌঁছানোর আগে একটা tokenizer সেটাকে token নামের টুকরোয় কাটে, আর প্রতিটা টুকরোকে একটা নির্দিষ্ট list, মানে vocabulary থেকে একটা সংখ্যা দিয়ে বদলে দেয়। "where is my order" হয়তো হয়ে যায় [2940, 374, 856, 2015]। Model শুধু এই সংখ্যাগুলোই পড়ে আর লেখে।
শুনতে যতটা লাগে, ব্যাপারটা তার চেয়ে অনেক বড়। টাকা দিতে হয় প্রতি token এ, context window গোনা হয় token এ, আর generation এর speed হলো প্রতি second এ কয়টা token। তোমার text এ কয়টা token লাগবে সেটা ঠিক করে tokenizer, আর ভাষাভেদে এটা সমান না: একই বাক্য বাংলায় English এর চেয়ে কয়েক গুণ বেশি token খেতে পারে। LLM এর অনেক অদ্ভুত ভুল, যেমন একটা শব্দে কয়টা অক্ষর সেটা ভুল গোনা, সেগুলোও আসে tokenization থেকে।
ভেতরে কীভাবে চলে
পুরো শব্দ বা একটা একটা অক্ষর কেন না? পুরো শব্দের vocabulary নতুন শব্দ, বানান ভুল বা নাম ("Farmgate", "bKash") সামলাতে পারে না। একটা একটা অক্ষর সবকিছু সামলায়, কিন্তু sequence অনেক লম্বা হয়ে যায়, আর attention এর খরচ দৈর্ঘ্যের বর্গে বাড়ে (Part 3)। Tokenizer থাকে মাঝখানে: subword। Common শব্দ একটা token; rare শব্দ কয়েকটা চেনা টুকরোয় ভাঙে।
Byte-pair encoding (BPE), GPT ধরনের model এ যেটা চলে, টুকরোগুলো data থেকে শেখে:
- Byte দিয়ে শুরু। যেকোনো ভাষার যেকোনো text আসলে UTF-8 byte এর sequence, তাই শুরুর vocabulary 256 টা symbol, আর কোনো কিছুই কখনো "অজানা" না।
- Training corpus এ পাশাপাশি থাকা প্রতিটা symbol এর জোড়া গোনো।
- সবচেয়ে বেশিবার আসা জোড়াটা merge করে একটা নতুন symbol বানাও, vocabulary তে যোগ করো।
- Vocabulary লক্ষ্যের সাইজে না পৌঁছানো পর্যন্ত চালাও (আসল model এ 32,000 থেকে 200,000 token)।
নতুন text encode করতে শেখা merge গুলো একই ক্রমে লাগাও। Decode করতে প্রতিটা id এর byte বের করে জোড়া দাও।
বাংলায় খরচ বেশি কেন। UTF-8 এ English এর একটা অক্ষর 1 byte, বাংলার একটা অক্ষর 3 byte। আর BPE যা সবচেয়ে বেশি দেখেছে সেটাই merge করে; training data বেশিরভাগ English হলে English শব্দ লম্বা merge করা token পায়, আর বাংলা ছোট ছোট byte এর টুকরো হয়েই থাকে। একই অর্থ, বেশি token: খরচ বেশি, context window এ কম আঁটে, answer ধীর। নতুন tokenizer গুলো বেশি বহুভাষিক data তে train হয়ে ফাঁকটা কমায়, কিন্তু খুব কমই পুরো বন্ধ করে।
Token অদ্ভুত আচরণ ব্যাখ্যা করে। "strawberry তে কয়টা r?" কঠিন, কারণ model হয়তো দেখছে "straw" + "berry" দুইটা token, দশটা অক্ষর না। সংখ্যা অদ্ভুতভাবে ভাঙে ("12345" → "123" + "45"), যেটা অঙ্কে ক্ষতি করে। সামনের space প্রায়ই token এর অংশ, তাই " order" আর "order" আলাদা id।
তোমার support bot English আর বাংলায় answer দেয়। একই message এ বাংলা version 3 গুণ বেশি token খায়। এতে কী বদলায়?
ছবিতে দেখো
Code এ ছোট্ট একটা BPE
40 লাইনে পুরো একটা byte-level BPE। 60 টা merge দিয়ে train করা এমন একটা corpus এ যেটা বেশিরভাগ English আর একটু বাংলা (web এর বেশিরভাগ data র মতো), তারপর একটা English আর একটা বাংলা বাক্য encode করা। যে token একটা অক্ষরের শুধু একটা অংশ, সেগুলো কোণা bracket এ কাঁচা byte হিসেবে দেখানো।
"""Byte-pair encoding in miniature: start from bytes, repeatedly merge the most common pair."""
from collections import Counter
def train_bpe(text, merges):
ids = list(text.encode("utf-8")) # every text starts as raw bytes: vocabulary of 256
vocab = {i: bytes([i]) for i in range(256)}
rules = []
for step in range(merges):
pairs = Counter(zip(ids, ids[1:]))
(a, b), count = pairs.most_common(1)[0]
new = 256 + step
vocab[new] = vocab[a] + vocab[b]
rules.append((a, b, new))
out, i = [], 0
while i < len(ids): # replace every (a, b) with the new token
if i + 1 < len(ids) and (ids[i], ids[i + 1]) == (a, b):
out.append(new); i += 2
else:
out.append(ids[i]); i += 1
ids = out
if step < 6:
print(f"merge {step + 1}: {vocab[a]!r} + {vocab[b]!r} -> {vocab[new]!r} (seen {count} times)")
return rules, vocab
def encode(text, rules):
ids = list(text.encode("utf-8"))
for a, b, new in rules: # apply merges in the order they were learned
out, i = [], 0
while i < len(ids):
if i + 1 < len(ids) and (ids[i], ids[i + 1]) == (a, b):
out.append(new); i += 2
else:
out.append(ids[i]); i += 1
ids = out
return ids
def show(b):
try:
return b.decode("utf-8")
except UnicodeDecodeError: # a piece of a multi-byte character: show the raw bytes
return "<" + b.hex() + ">"
# A training corpus that is mostly English, like most of the web.
corpus = ("the order is late and the rider is lost . " * 30 +
"where is my order ? the order was paid . " * 30 +
"আমার অর্ডার কোথায় ? " * 3)
rules, vocab = train_bpe(corpus, merges=60)
print()
for s in ["where is my order ?", "আমার অর্ডার কোথায় ?", "the rider is late"]:
ids = encode(s, rules)
print(f"{s!r}: {len(s)} characters, {len(s.encode('utf-8'))} bytes, {len(ids)} tokens")
print(" ", " | ".join(show(vocab[i]) for i in ids))
Output:
merge 1: b'e' + b' ' -> b'e ' (seen 150 times)
merge 2: b'e' + b'r' -> b'er' (seen 150 times)
merge 3: b'd' + b'er' -> b'der' (seen 120 times)
merge 4: b'der' + b' ' -> b'der ' (seen 120 times)
merge 5: b's' + b' ' -> b's ' (seen 120 times)
merge 6: b't' + b'h' -> b'th' (seen 90 times)
'where is my order ?': 19 characters, 19 bytes, 10 tokens
w | h | er | e | is | m | y | | order | ?
'আমার অর্ডার কোথায় ?': 20 characters, 52 bytes, 28 tokens
<e0a6> | <86> | <e0a6> | <ae> | ার | <20e0a6> | <85> | <e0a6> | <b0> | <e0> | <a7> | <8d> | <e0a6> | <a1> | ার | <20e0a6> | <95> | <e0> | <a7> | <8b> | <e0a6> | <a5> | <e0a6bee0a6> | <af> | <e0a6> | <bc> | | ?
'the rider is late': 17 characters, 17 bytes, 6 tokens
the | r | i | der | is lat | e
কী দেখলাম:
- প্রথম merge গুলো corpus এর সবচেয়ে বেশি আসা জোড়া: "e" + space, "e" + "r", তারপর "d" + "er" বড় হয়ে "der "। BPE শুধু গুনে গুনেই common শব্দের টুকরো বানিয়ে ফেলে।
- "the rider is late" 17 অক্ষর কিন্তু মাত্র 6 token: "the ", "der ", "is lat" একেকটা token, কারণ training এ এগুলো common ছিল।
- "আমার অর্ডার কোথায় ?" 20 অক্ষর, 52 byte (প্রতি বাংলা অক্ষরে 3), আর 28 token। শুধু "ার" merge হয়েছে; বাকি প্রায় সব এখনো
<e0a6>আর<86>এর মতো কাঁচা byte, একটা অক্ষরেরই ভাঙা টুকরো। প্রায় একই দৈর্ঘ্য, English প্রশ্নের (10) প্রায় 3 গুণ token। - Token অক্ষরের সীমানা মানে না।
<e0a6>হলো একটা বাংলা অক্ষরের তিন byte এর দুইটা। টুকরোগুলো আবার জোড়া দিলেই শুধু decode কাজ করে; answer token ধরে ধরে stream করলে পরের token না আসা পর্যন্ত অর্ধেক অক্ষর দেখা যেতে পারে।
Production এ যা মাথায় রাখবে
- Model এর নিজের tokenizer দিয়ে token গোনো পাঠানোর আগে (বেশিরভাগ provider একটা দেয়, বা একটা count endpoint)। English ছাড়া অন্য text এ কখনো অক্ষর ÷ 4 দিয়ে আন্দাজ করবে না।
- ভাষা ধরে budget করো। Max length, history ছাঁটা আর দামের হিসাব token এ রাখো, আর তোমার আসল traffic এর মিশ্রণে মাপো, বাংলা আর Banglish সহ।
- Streaming কে অর্ধেক অক্ষর সামলাতে হবে। পুরো UTF-8 অক্ষর না হওয়া পর্যন্ত byte জমিয়ে রাখো, তারপর দেখাও, নইলে UI তে ভাঙা অক্ষর আসবে।
- প্রতিটা model এর নিজের tokenizer। Model বদলালে token এর সংখ্যা, সীমা আর খরচ বদলায়। এক model এর save করা token id অন্য model এর কাছে অর্থহীন।
- Special token একটা security এর কিনারা। Tokenizer এর control token থাকে (message এর শুরু, turn এর শেষ)। User এর text এ তাদের আক্ষরিক string থাকলে সেগুলো যেন আসল control token এ পরিণত না হয়; library র safe encode mode ব্যবহার করো।
এক কথায়
Model অক্ষর না, token id পড়ে। BPE byte থেকে শুরু করে সবচেয়ে বেশি আসা জোড়া merge করে, তাই common text কম token আর rare text বেশি token হয়; token ঠিক করে দাম, context এর সীমা আর speed, আর বাংলায় English এর চেয়ে বেশি খরচ হয়।
নিজে করে দেখো
Training corpus এর বাংলা অংশটা * 3 থেকে * 120 করো, যাতে এখন বাংলাই সবচেয়ে common text, আর 60 merge দিয়ে আবার train করো। "আমার অর্ডার কোথায় ?" এখন কয়টা token নেয়, আর অনেক কম না কেন? শেখা merge গুলো print করে খুঁজে বের করো।
Answer
28 এর জায়গায় মাত্র 23 token, যদিও প্রথম 7 টা merge এখন বাংলার। Merge গুলো দেখলেই কারণ বোঝা যায়: corpus এ প্রতিটা বাংলা বাক্য "? " এর পরে আসে, তাই BPE এমন লম্বা token শিখেছে যেগুলো space দিয়ে শুরু, যেমন " আমার অর্ডার ক..."। Test এর বাক্য space ছাড়া শুরু, তাই ওই merge গুলো কখনো মেলে না। "the rider is late" ও 6 থেকে 7 token হয়, কারণ merge এর বাজেটের একটা অংশ বাংলা নিয়ে গেছে। আসল tokenizer space এর এই সমস্যা এড়ায় pre-tokenise করে: BPE এর আগে text কে শব্দে ভাগ করে (সামনের space সহ), যাতে merge শব্দের ভেতরেই থাকে। আর সেগুলো এমন corpus এ train হয় যেখানে বাংলা যথেষ্ট পরিমাণে আছে নিজের merge পাওয়ার মতো, বাংলা product এর ঠিক এটাই দরকার।
Station stamp
Chapter শেষ করো
পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।
Pro নিয়ে XP পাও →