مدلهای پایهای مثل Word2Vec با وجود درخشش اولیه، دو ضعف بزرگ داشتند: ناتوانی در بهرهگیری همزمان از آمار کل پیکره و شکست کامل در مواجهه با کلمات جدید یا خارج از واژگان (OOV). این چالش بهویژه در زبانهایی مانند فارسی که ساختار صرفی غنی دارند، پسوند و پیشوندهای زیادی میپذیرند یا املای کلمات در آنها دستخوش تغییر میشود، بهشدت به چشم میآید.
در این درس یاد میگیرید که GloVe چگونه با فاکتورگیری ماتریس همآیی عملکرد را بهبود داد، FastText چطور با شکستن کلمات به قطعات کوچکتر (n-gramهای نویسهای) مشکل کلمات نامأنوس را حل کرد و در نهایت الگوریتم کدگذاری جفتبایت (BPE) چگونه به پایهی ثابت تمام توکنایزرهای ترنسفورمری در مدلهای زبانی مدرن تبدیل شد.
Word2Vec به ازای هر کلمه یک امبدینگ (embedding) آموزش میداد. GloVe ماتریس همآیی را فاکتورگیری کرد. FastText قطعات کلمات را امبد کرد. BPE هم پلی بهسوی ترنسفورمرها زد.
صورت مسئله
مدل Word2Vec دو پرسش اساسی را بیپاسخ گذاشت.
اول اینکه، همزمان پژوهشهای دیگری هم در جریان بود که بهجای بهروزرسانیهای آنلاین skip-gram، ماتریس همآیی (co-occurrence matrix) را مستقیماً فاکتورگیری میکرد (روشهایی مثل LSA و HAL). آیا رویکرد تکرارشوندهی Word2Vec از نظر بنیادین بهتر بود، یا این تفاوت صرفاً حاصل شیوهی برخورد این دو روش با شمارشها بود؟ GloVe به این پرسش پاسخ داد: فاکتورگیری ماتریسی همراه با یک تابع زیان (loss) حسابشده، به کیفیتی در حد Word2Vec یا بهتر از آن میرسد و هزینهی آموزش پایینتری هم دارد.
دوم اینکه هیچکدام از این دو روش راهکاری برای کلمات ناشناخته نداشتند؛ کلماتی مثل Zoomer-approved، dogecoin، هر اسم خاصی که همین هفته ابداع شده، یا صورتهای صرفی یک ریشهی نادر. FastText با امبد کردن n-gramهای نویسهای این مسئله را حل کرد: هر کلمه برابر است با مجموع اجزای سازندهاش (از جمله تکواژها)؛ بنابراین حتی کلمات خارج از واژگان (out-of-vocabulary یا OOV) هم بردار منطقی و معناداری میگیرند.
سوم اینکه با ظهور ترنسفورمرها (transformers)، صورت مسئله دوباره تغییر کرد. واژگانِ در سطح کلمه نهایتاً تا حدود یک میلیون مدخل ظرفیت دارند؛ اما زبان واقعی بسیار بازتر و پویاتر از این رقمهاست. کدگذاری جفتبایت (Byte-pair encoding یا BPE) و روشهای همخانوادهاش این گره را گشودند: آنها واژگانی از واحدهای پرتکرار زیرکلمه (subword) یاد میگیرند که همهچیز را پوشش میدهد. تمام توکنایزرهای مدرن برای همهی مدلهای زبانی بزرگ (LLM) کنونی، توکنایزر زیرکلمهای هستند.
در این درس هر سه رویکرد را گامبهگام بررسی میکنیم و در پایان میبینیم در هر موقعیت باید سراغ کدام بروید.
مبانی نظری
GloVe (برپایهی Global Vectors). ماتریس همآیی کلمه-کلمه یعنی X را میسازید که در آن X[i][j] نشان میدهد کلمهی j چند بار در بافتار کلمهی i آمده است. بردارها را طوری آموزش میدهید که رابطهی v_i · v_j + b_i + b_j ≈ log(X[i][j]) برقرار شود. به تابع زیان ضریب وزنی میدهید تا جفتهای پرتکرار بر محاسبات مسلط نشوند. کار تمام است.
FastText. هر کلمه حاصلجمع n-gramهای نویسهای خودش بهعلاوهی خود کلمه است؛ یعنی where به اجزای <wh, whe, her, ere, re>, <where> تبدیل میشود. بردار کلمه برابر با مجموع بردارهای همین مؤلفههاست. آموزش آن هم مثل Word2Vec انجام میشود. مزیت کار اینجاست: کلمات دیدهنشده (مثل whereupon) از کنار هم قرار گرفتن n-gramهای شناختهشده ساخته میشوند.
BPE (کدگذاری جفتبایت). کار با واژگانی از بایتها (یا نویسهها)ی منفرد شروع میشود. تمام جفتهای مجاور در پیکرهی متنی را میشمارید. پرتکرارترین جفت را در قالب یک توکن جدید merge میکنید. این کار را برای k تکرار ادامه میدهید. نتیجه، واژگانی متشکل از نمادهای پایه بهعلاوهی k مورد ادغامشده (merge) خواهد بود (اگر از بایت شروع کنید، k + 256 توکن به دست میآید)؛ طوری که دنبالههای پرتکرار (ing، tion، the) توکنهای واحدی میشوند و کلمات نادر به قطعات آشنا میشکنند. هر جملهای بالاخره به چیزی توکنایز میشود و کلمهای بدون توکن نمیماند.
پیادهسازی
GloVe: فاکتورگیری ماتریس همآیی
import numpy as np
from collections import Counter
def build_cooccurrence(docs, window=5):
pair_counts = Counter()
vocab = {}
for doc in docs:
for token in doc:
if token not in vocab:
vocab[token] = len(vocab)
for doc in docs:
indexed = [vocab[t] for t in doc]
for i, center in enumerate(indexed):
for j in range(max(0, i - window), min(len(indexed), i + window + 1)):
if i != j:
distance = abs(i - j)
pair_counts[(center, indexed[j])] += 1.0 / distance
return vocab, pair_counts
def glove_train(vocab, pair_counts, dim=16, epochs=100, lr=0.05, x_max=100, alpha=0.75, seed=0):
n = len(vocab)
rng = np.random.default_rng(seed)
W = rng.normal(0, 0.1, size=(n, dim))
W_tilde = rng.normal(0, 0.1, size=(n, dim))
b = np.zeros(n)
b_tilde = np.zeros(n)
for epoch in range(epochs):
for (i, j), x_ij in pair_counts.items():
weight = (x_ij / x_max) ** alpha if x_ij < x_max else 1.0
diff = W[i] @ W_tilde[j] + b[i] + b_tilde[j] - np.log(x_ij)
coef = weight * diff
grad_W_i = coef * W_tilde[j]
grad_W_tilde_j = coef * W[i]
W[i] -= lr * grad_W_i
W_tilde[j] -= lr * grad_W_tilde_j
b[i] -= lr * coef
b_tilde[j] -= lr * coef
return W + W_tilde
دو بخش متحرک و کلیدی این کد ارزش نام بردن دارند: تابع وزندهی f(x) = (x/x_max)^alpha اثر جفتهای بسیار پرتکرار (مانند (the, and)) را کم میکند تا تابع زیان (loss) را تسخیر نکنند. امبدینگ نهایی هم حاصلجمع جدولهای W (مرکزی) و W_tilde (بافتار) است. جمع کردن هر دو جدول ترفندی منتشرشده در مقالهی اصلی است که معمولاً بهتر از بهکارگیری یکی از آنها جواب میدهد.
FastText: امبدینگهای آگاه از زیرکلمه
def char_ngrams(word, n_min=3, n_max=6):
wrapped = f"<{word}>"
grams = {wrapped}
for n in range(n_min, n_max + 1):
for i in range(len(wrapped) - n + 1):
grams.add(wrapped[i:i + n])
return grams
>>> char_ngrams("where")
{'<where>', '<wh', 'whe', 'her', 'ere', 're>', '<whe', 'wher', 'here', 'ere>', '<wher', 'where', 'here>'}
هر کلمه با مجموعهای از n-gramهای خود (معمولاً 3 تا 6 نویسه) نشان داده میشود. امبدینگ کلمه برابر با مجموع امبدینگهای n-gramهای آن است. برای آموزش به روش skip-gram، کافی است این ساختار را دقیقاً جایی قرار دهید که Word2Vec از یک بردار تکی استفاده میکرد.
def fasttext_vector(word, ngram_table):
grams = char_ngrams(word)
vecs = [ngram_table[g] for g in grams if g in ngram_table]
if not vecs:
return None
return np.sum(vecs, axis=0)
برای یک کلمهی دیدهنشده، تا وقتی که دستکم چند n-gram آن شناختهشده باشند، همچنان بردار دریافت میکنید. کلمهی whereupon در n-gramهای <wh، her، ere و <where با where مشترک است؛ بنابراین این دو بردار در فضا نزدیک به هم مینشینند.
BPE: یادگیری واژگان زیرکلمهای
def learn_bpe(corpus, k_merges):
vocab = Counter()
for word, freq in corpus.items():
tokens = tuple(word) + ("</w>",)
vocab[tokens] = freq
merges = []
for _ in range(k_merges):
pair_freq = Counter()
for tokens, freq in vocab.items():
for a, b in zip(tokens, tokens[1:]):
pair_freq[(a, b)] += freq
if not pair_freq:
break
best = pair_freq.most_common(1)[0][0]
merges.append(best)
new_vocab = Counter()
for tokens, freq in vocab.items():
new_tokens = []
i = 0
while i < len(tokens):
if i + 1 < len(tokens) and (tokens[i], tokens[i + 1]) == best:
new_tokens.append(tokens[i] + tokens[i + 1])
i += 2
else:
new_tokens.append(tokens[i])
i += 1
new_vocab[tuple(new_tokens)] = freq
vocab = new_vocab
return merges
def apply_bpe(word, merges):
tokens = list(word) + ["</w>"]
for a, b in merges:
new_tokens = []
i = 0
while i < len(tokens):
if i + 1 < len(tokens) and tokens[i] == a and tokens[i + 1] == b:
new_tokens.append(a + b)
i += 2
else:
new_tokens.append(tokens[i])
i += 1
tokens = new_tokens
return tokens
>>> corpus = Counter({"low": 5, "lower": 2, "newest": 6, "widest": 3})
>>> merges = learn_bpe(corpus, k_merges=10)
>>> apply_bpe("lowest", merges)
['low', 'est</w>']
اولین تکرار، پرتکرارترین جفت مجاور را ادغام میکند. پس از تکرارهای کافی، زیررشتههای پرتکرار (low، est، tion) به توکنهای تکی تبدیل میشوند و کلمات نادر نیز با دقت و بهزیبایی میشکنند.
توکنایزرهای واقعی مدلهای GPT، BERT و T5 بین 30k تا 100k ادغام یاد میگیرند. نتیجه اینکه هر متنی به دنبالهای از شناسههای معتبر و با طول محدود تبدیل میشود و هرگز خطای OOV پیش نمیآید.
استفادهی عملی
در عمل، بهندرت پیش میآید که خودتان این مدلها را آموزش دهید؛ معمولاً چکپوینتهای (checkpoint) از پیش آموزشدیده را بارگذاری میکنید.
import fasttext.util
fasttext.util.download_model("en", if_exists="ignore")
ft = fasttext.load_model("cc.en.300.bin")
print(ft.get_word_vector("whereupon").shape)
print(ft.get_word_vector("zoomerapproved").shape)
برای توکنایز زیرکلمهای به سبک BPE در عصر ترنسفورمرها:
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("gpt2")
print(tok.tokenize("unbelievably tokenized"))
['un', 'bel', 'iev', 'ably', 'Ġtoken', 'ized']
پیشوند Ġ مرزهای کلمات را نشان میدهد (قراردادی در توکنایزر GPT-2). تمام توکنایزرهای مدرن، گونهای از BPE، WordPiece (در BERT) یا SentencePiece (در T5 و LLaMA) هستند.
در هر موقعیت سراغ کدام برویم؟
| شرایط مسئله | انتخاب |
|---|---|
| بردار کلمات چندمنظورهی از پیش آموزشدیده، بدون نیاز به مدیریت OOV | GloVe 300d |
| بردار کلمات چندمنظورهی از پیش آموزشدیده، با الزامِ پوشش غلطهای املایی، واژگان نوظهور یا زبانهای با ساختار صرفی غنی | FastText |
| هر چیزی که قرار است ورودی ترنسفورمر باشد (آموزش یا استنتاج) | هر توکنایزری که همراه خود مدل ارائه شده است؛ هرگز آن را عوض نکنید. |
| آموزش یک مدل زبانی از صفر | ابتدا یک توکنایزر BPE یا SentencePiece روی پیکرهی متنی خود آموزش دهید |
| دستهبندی متن در محیط عملیاتی با یک مدل خطی | همچنان TF-IDF؛ مطابق درس 02. |
تحویل خروجی
این محتوا را با نام outputs/skill-embeddings-picker.md ذخیره کنید:
---
name: tokenizer-picker
description: Pick a tokenization approach for a new language model or text pipeline.
version: 1.0.0
phase: 5
lesson: 04
tags: [nlp, tokenization, embeddings]
---
Given a task and dataset description, you output:
1. Tokenization strategy (word-level, BPE, WordPiece, SentencePiece, byte-level). One-sentence reason.
2. Vocabulary size target (e.g., 32k for an English-only LM, 64k-100k for multilingual).
3. Library call with the exact training command. Name the library. Quote the arguments.
4. One reproducibility pitfall. Tokenizer-model mismatch is the single most common silent production bug; call out which pair must be used together.
Refuse to recommend training a custom tokenizer when the user is fine-tuning a pretrained LLM. Refuse to recommend word-level tokenization for any model targeting production inference. Flag non-English / multi-script corpora as needing SentencePiece with byte fallback.
تمرینها
- ساده. توابع
char_ngrams("playing")وchar_ngrams("played")را اجرا کنید. ضریب همپوشانی ژاکارد (Jaccard overlap) دو مجموعهی n-gram را به دست آورید. قطعات مشترک زیادی خواهید دید (pla،lay،play)؛ علت عملکرد درخشان FastText روی صورتهای گوناگون صرفی دقیقاً همین است. - متوسط. تابع
learn_bpeرا طوری توسعه دهید که رشد اندازهی واژگان را رهگیری کند. نسبتِ توکنها به تعداد نویسههای کل پیکره را برحسب تعداد mergeها رسم کنید. فشردهسازی در ابتدا بسیار سریع رخ میدهد و کمکم نزدیک به حدود 2 تا 3 نویسه به ازای هر توکن به حالت مجانبی میرسد. - سخت. یک توکنایزر BPE با 1000 ادغام (merge) روی مجموعهی آثار کامل شکسپیر آموزش دهید. نحوهی توکنایز شدن کلمات متداول را با اسمهای خاص نادر مقایسه کنید. میانگین توکنها به ازای هر کلمه را قبل و بعد از کار بسنجید. مشاهدات جالب و غافلگیرکنندهتان را بنویسید.
اصطلاحات کلیدی
| اصطلاح | در گفتگوها چه میگویند | معنای دقیق فنی |
|---|---|---|
| Co-occurrence matrix (ماتریس همآیی) | جدول فراوانی کلمه-کلمه | X[i][j] = تعداد دفعاتی که کلمهی j در پنجرهای اطراف کلمهی i ظاهر شده است. |
| Subword (زیرکلمه) | تکهای از یک کلمه | یک n-gram نویسهای (در FastText) یا یک توکن یادگرفتهشده (در BPE/WordPiece/SentencePiece). |
| BPE | کدگذاری جفتبایت | merge مکرر پرتکرارترین جفتهای مجاور تا زمانی که اندازهی واژگان به مقدار هدف برسد. |
| OOV | خارج از واژگان | کلمهای که مدل هرگز ندیده است. Word2Vec و GloVe برای آن پاسخی ندارند؛ FastText و BPE آن را مدیریت میکنند. |
| Byte-level BPE | روش BPE روی بایتهای خام | سازوکار GPT-2. واژگان با 256 بایت اولیه شروع میشود؛ بنابراین هیچ کلمهای هرگز OOV نخواهد شد. |
برای مطالعهی بیشتر
- Pennington, Socher, Manning (2014). GloVe: Global Vectors for Word Representation — مقالهی اصلی GloVe؛ مقالهای هفتصفحهای که همچنان بهترین توضیح ریاضی برای تابع زیان این روش است.
- Bojanowski et al. (2017). Enriching Word Vectors with Subword Information — مقالهی FastText.
- Sennrich, Haddow, Birch (2016). Neural Machine Translation of Rare Words with Subword Units — مقالهای که روش BPE را به NLP مدرن معرفی کرد.
- Hugging Face tokenizer summary — تفاوتهای کاربردی و عملی BPE، WordPiece و SentencePiece در عمل.
