فاز 5 · درس 4

آموزش GloVe، FastText و توکنایزرهای زیرکلمه‌ای (BPE)

نوع
ساختنی
زمان
حدود 45 دقیقه
زبان‌ها
Python
پیش‌نیاز
فاز 5 · بخش 03 (پیاده‌سازی Word2Vec از صفر)
بلوک‌های چوبی حروف مجزا که گام‌به‌گام به قطعات متصل بزرگ‌تر و واژه‌ها پیوند می‌خورند روی زمینه‌ای خاکستری با ماتریس‌های ریاضی
فهرست مطالب
  1. صورت مسئله
  2. مبانی نظری
  3. پیاده‌سازی
  4. GloVe: فاکتورگیری ماتریس هم‌آیی
  5. FastText: امبدینگ‌های آگاه از زیرکلمه
  6. BPE: یادگیری واژگان زیرکلمه‌ای
  7. استفاده‌ی عملی
  8. در هر موقعیت سراغ کدام برویم؟
  9. تحویل خروجی
  10. تمرین‌ها
  11. اصطلاحات کلیدی
  12. برای مطالعه‌ی بیشتر

مدل‌های پایه‌ای مثل 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) توکن‌های واحدی می‌شوند و کلمات نادر به قطعات آشنا می‌شکنند. هر جمله‌ای بالاخره به چیزی توکنایز می‌شود و کلمه‌ای بدون توکن نمی‌ماند.

توکن‌سازی زیرکلمه‌ایجفت‌ها به توکن تبدیل می‌شوند
SUBWORD TOKENIZATIONByte-pair encoding starts from characters and repeatedly merges the most frequent adjacent pair: (l,o) and (e,r) first, then (lo,w), then (low,er). Frequent words end up as single tokens while rare words split into familiar pieces, so no input is ever out of vocabulary.merge صف: (l,o) (e,r) (lo,w) (low,er)lowerloerlowlower
الگوریتم کدگذاری جفت بایت (BPE) از کاراکترها شروع می‌کند و پرتکرارترین جفت‌های مجاور را مکرراً merge می‌کند: ابتدا (l,o) و (e,r)، سپس (lo,w) و بعد (low,er). کلمات پرکاربرد در نهایت به توکن‌های واحد تبدیل می‌شوند، در حالی که کلمات نادر به بخش‌های آشنا تقسیم می‌گردند؛ بنابراین هیچ ورودی هرگز خارج از دایره‌ی لغات (OOV) نخواهد بود.

پیاده‌سازی

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.

تمرین‌ها

  1. ساده. توابع char_ngrams("playing") و char_ngrams("played") را اجرا کنید. ضریب هم‌پوشانی ژاکارد (Jaccard overlap) دو مجموعه‌ی n-gram را به دست آورید. قطعات مشترک زیادی خواهید دید (pla، lay، play)؛ علت عملکرد درخشان FastText روی صورت‌های گوناگون صرفی دقیقاً همین است.
  2. متوسط. تابع learn_bpe را طوری توسعه دهید که رشد اندازه‌ی واژگان را رهگیری کند. نسبتِ توکن‌ها به تعداد نویسه‌های کل پیکره را برحسب تعداد mergeها رسم کنید. فشرده‌سازی در ابتدا بسیار سریع رخ می‌دهد و کم‌کم نزدیک به حدود 2 تا 3 نویسه به ازای هر توکن به حالت مجانبی می‌رسد.
  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 نخواهد شد.

برای مطالعه‌ی بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسمدل GloVe چه نوآوری نسبت به Word2Vec ارائه داد؟
  2. پیش از درسمدل FastText کدام مشکل را که Word2Vec و GloVe قادر به حل آن نیستند، برطرف می‌کند؟
  3. حین درسدر مدل GloVe، تابع وزن‌دهی f(x) = (x/x_max)^alpha چه نقشی ایفا می‌کند؟
  4. حین درسمرحله merge در الگوریتم BPE چگونه جفت بعدی را برای ترکیب انتخاب می‌کند؟
  5. حین درسچرا مدل GPT-2 از BPE در سطح بایت استفاده می‌کند؟
  6. پس از درسهنگام فاین‌تیونینگ یک ترنسفورمر از پیش آموزش‌دیده، از کدام توکنایزر باید استفاده کنید؟
  7. پس از درسچه زمانی برای بردارهای کلمه از پیش آموزش‌دیده، FastText را به GloVe ترجیح می‌دهید؟
  8. پس از درسچه واحدی پایه دایره واژگان الگوریتم‌های WordPiece، BPE و SentencePiece را تشکیل می‌دهد؟

سؤالات متداول

تفاوت اصلی الگوریتم FastText با Word2Vec چیست؟

در Word2Vec برای هر کلمه یک بردار مستقل آموزش داده می‌شود، اما FastText هر کلمه را به قطعات کوچک‌تر (n-gramهای نویسه‌ای) خرد می‌کند و بردار کلمه حاصل‌جمع این قطعات است؛ به همین دلیل کلمات ناشناخته (OOV) نیز صاحب بردار معنادار می‌شوند.

چرا مدل‌های مدرن مثل GPT از توکنایزر BPE استفاده می‌کنند؟

الگوریتم BPE با ادغام جفت‌بایت‌های پرتکرار واژگانی بهینه و با طول محدود می‌سازد که می‌تواند هر رشته‌ی متنی را بدون ایجاد خطای OOV و با تفکیک مناسب کلمات نادر به توکن‌ها تبدیل کند.

آیا برای مدل‌های ترنسفورمری مثل BERT یا GPT می‌توان توکنایزر را عوض کرد؟

خیر؛ هر مدل ترنسفورمر متکی به واژگان و توکنایزری است که با آن از ابتدا آموزش دیده و هرگز نباید در مرحله استنتاج یا تنظیم دقیق (fine-tuning) توکنایزر مدل را تغییر داد.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.