فاز 10 · درس 1

آشنایی با الگوریتم‌های توکنایزر: BPE، WordPiece و SentencePiece

نوع
ساختنی
زمان
حدود 90 دقیقه
زبان‌ها
Python
پیش‌نیاز
فاز 05 (مبانی پردازش زبان طبیعی)
بلوک‌های هندسی چوبی با نمادها و ارقام گوناگون که به ترتیب از راست به چپ در دسته‌های منظم خرد و مجدداً ادغام می‌شوند
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. سه رویکردی که شکست خوردند (و یکی که پیروز شد)
  5. الگوریتم BPE: کدگذاری جفت‌بایت (Byte Pair Encoding)
  6. الگوریتم Byte-Level BPE (در مدل‌های GPT-2، GPT-3 و GPT-4)
  7. الگوریتم WordPiece (مدل BERT)
  8. الگوریتم SentencePiece (مدل‌های Llama و T5)
  9. موازنه‌های اندازه‌ی واژگان
  10. مالیات چندزبانه (The Multilingual Tax)
  11. پیاده‌سازی عملی
  12. گام 1: توکنایزر در سطح نویسه
  13. گام 2: پیاده‌سازی توکنایزر BPE از صفر
  14. گام 3: رفت‌وبرگشت کامل انکود و دیکود
  15. گام 4: مقایسه با tiktoken
  16. گام 5: تحلیل واژگان
  17. استفاده از ابزارهای آماده
  18. ابزار tiktoken (از شرکت OpenAI)
  19. کتابخانه‌ی tokenizers متعلق به Hugging Face
  20. بارگذاری توکنایزر مدل Llama
  21. مرحله‌ی تحویل (Ship It)
  22. تمرین‌ها
  23. اصطلاحات کلیدی
  24. برای مطالعه‌ی بیشتر

مدل‌های زبانی بزرگ متن متوجه نمی‌شوند؛ آن‌ها صرفاً ماتریس‌هایی از اعداد صحیح را پردازش می‌کنند. تمام پل ارتباطی میان رشته‌های متنی و محاسبات ریاضی مدل را توکنایزر می‌سازد. اگر درک درستی از این فرآیند نداشته باشید، پنجره‌ی زمینه‌ی مدل شما با کلمات تکه‌تکه‌شده به هدر می‌رود و هزینه‌های محاسباتی یا مصرف توکن در فراخوانی‌های API چند برابر می‌شود.

در این درس از پایه‌ترین روش‌ها شروع می‌کنیم، سپس سراغ پیاده‌سازی گام‌به‌گام الگوریتم‌های محبوبی مثل BPE، WordPiece و SentencePiece می‌رویم تا ببینید مدل‌هایی چون BERT، Llama و خانواده‌ی GPT متن را چطور خرد و ادغام می‌کنند. در پایان، یاد می‌گیرید که مفهوم «مالیات چندزبانه» چیست و چرا متن‌های غیرانگلیسی رفتار متفاوتی در توکنایزرها نشان می‌دهند.

مدل زبانی بزرگ شما انگلیسی نمی‌خواند؛ عدد صحیح می‌خواند. این توکنایزر است که تعیین می‌کند آیا این اعداد معنا را منتقل می‌کنند یا آن را هدر می‌دهند.

اهداف یادگیری

  • پیاده‌سازی الگوریتم‌های توکن‌سازی BPE، WordPiece و Unigram از صفر و مقایسه‌ی استراتژی‌های merge آن‌ها
  • توضیح نحوه‌ی اثرگذاری اندازه‌ی واژگان (vocabulary size) بر بازدهی مدل: واژگان بسیار کوچک دنباله‌های طولانی می‌سازد و واژگان بیش از حد بزرگ پارامترهای امبدینگ (embedding) را تلف می‌کند
  • تحلیل آثار نامطلوب توکن‌سازی در زبان‌های مختلف و کدها، و شناسایی نقاط شکست توکنایزرهای خاص
  • استفاده از کتابخانه‌های tiktoken و sentencepiece برای توکن‌سازی متن و بازرسی شناسه‌های توکن (token ID) حاصل

صورت مسئله

مدل زبانی شما انگلیسی نمی‌خواند. اصلاً هیچ زبانی را نمی‌خواند؛ عدد می‌خواند.

فاصله‌ی میان «!Hello, world» و [15496, 11, 995, 0] همان توکنایزر است. هر کلمه، هر فاصله و هر نشانه‌ی نگارشی باید پیش از پردازش در مدل، به یک عدد صحیح تبدیل شود. این تبدیل به‌هیچ‌وجه خنثی نیست؛ بلکه پیش‌فرض‌هایی را در دل مدل تثبیت می‌کند که بعداً نمی‌توان آن‌ها را تغییر داد.

اگر این مرحله را اشتباه بروید، مدل با اختصاص چند توکن به کلمات رایج، ظرفیت خود را هدر می‌دهد؛ مثلاً کلمه‌ی «unfortunately» به‌جای یک توکن، چهار توکن مصرف می‌کند. با این کار، پنجره‌ی زمینه‌ی (context window) 128K شما برای متن‌هایی با کلمات چندسیلابی زیاد، عملاً 75% کوچک‌تر می‌شود. اما اگر آن را درست پیاده کنید، همان پنجره‌ی زمینه دو برابر معنای بیشتری در خود جای می‌دهد. تفاوت میان مدلی که «کدها را عالی می‌فهمد» و مدلی که «در پایتون به تته‌پته می‌افتد»، اغلب به نحوه‌ی آموزش توکنایزر آن برمی‌گردد.

هزینه‌ی هر فراخوانی API به GPT-4 یا Claude بر اساس تعداد توکن محاسبه می‌شود. به ازای هر توکنی که مدل تولید می‌کند، توان پردازشی مصرف می‌شود. هرچه توکن کمتری برای نمایش یک خروجی نیاز باشد، استنتاج سرتاسری (end-to-end inference) سریع‌تر خواهد بود. توکن‌سازی صرفاً پیش‌پردازش نیست؛ خودِ معماری است.

مفاهیم پایه

سه رویکردی که شکست خوردند (و یکی که پیروز شد)

سه روش بدیهی برای تبدیل متن به عدد وجود دارد که دو مورد از آن‌ها در مقیاس بزرگ جواب نمی‌دهند.

توکن‌سازی در سطح کلمه (Word-level tokenization) متن را بر اساس فاصله‌ها و علائم نگارشی می‌شکند. عبارت «The cat sat» به ["The", "cat", "sat"] تبدیل می‌شود. ساده است؛ اما با کلماتی مثل «tokenization» یا عبارت «GPT-4o» یا یک کلمه‌ی مرکب آلمانی مثل «Geschwindigkeitsbegrenzung» چه باید کرد؟ روش سطح کلمه به واژگانی عظیم نیاز دارد تا تمام کلمات را در همه‌ی زبان‌ها پوشش دهد. کافی است کلمه‌ای جا بیفتد تا توکن مخوف [UNK] ظاهر شود؛ روشی که مدل با آن می‌گوید: «هیچ ایده‌ای ندارم این چیست.» فقط زبان انگلیسی بیش از یک میلیون شکل کلمه دارد. کدها، نشانی‌های وب، نمادهای علمی و 100 زبان دیگر را هم اضافه کنید تا ببینید به واژگانی عملاً بی‌نهایت نیاز خواهید داشت.

توکن‌سازی در سطح نویسه (Character-level tokenization) در جهت عکس حرکت می‌کند. مثلاً «hello» به ["h", "e", "l", "l", "o"] تبدیل می‌شود. اندازه‌ی واژگان بسیار کوچک است (چندصد نویسه) و هرگز توکن ناشناخته‌ای تولید نمی‌شود. اما در عوض، دنباله‌ها بیش از حد طولانی می‌شوند. جمله‌ای که در سطح کلمه 10 توکن می‌شد، حالا 50 توکن در سطح نویسه مصرف می‌کند. مدل مجبور است یاد بگیرد که کنار هم قرار گرفتن «t» و «h» و «e» معنای «the» می‌دهد؛ یعنی سوزاندن ظرفیت اتنشن (attention) برای چیزی که یک کودک سه‌ساله می‌داند.

توکن‌سازی زیرکلمه (Subword tokenization) نقطه‌ی تعادل طلایی را پیدا می‌کند. کلمات رایج دست‌نخورده باقی می‌مانند: «the» یک توکن می‌شود. کلمات نادر به بخش‌های معنادار شکسته می‌شوند: «unhappiness» به ["un", "happi", "ness"] تجزیه می‌شود. اندازه‌ی واژگان در محدوده‌ای معقول (30K تا 128K توکن) می‌ماند، دنباله‌ها کوتاه حفظ می‌شوند و توکن‌های ناشناخته عملاً ناپدید می‌شوند، چراکه هر کلمه‌ای را می‌توان از ترکیب این زیرکلمه‌ها ساخت.

تمام مدل‌های زبانی امروزی از توکن‌سازی زیرکلمه استفاده می‌کنند؛ GPT-2، GPT-4، BERT، Llama 3 و Claude، همگی بدون استثنا. مسئله فقط این است که از کدام الگوریتم استفاده شود.

در سطح کلمهدر سطح نویسهSubword BPEمتن: 'unhappiness'استراتژی توکن‌سازی['unhappiness']1 توکن اگر در واژگان باشد[UNK] در غیر این صورت['u','n','h','a','p','p','i','n','e','s','s']11 توکن['un','happi','ness']3 توکن

الگوریتم BPE: کدگذاری جفت‌بایت (Byte Pair Encoding)

الگوریتم BPE در اصل یک الگوریتم فشرده‌سازی حریصانه (greedy) است که برای توکن‌سازی تغییر کاربری یافته است. ایده‌ی کلی آن‌قدر ساده است که پشت یک کارت ویزیت جا می‌شود.

کار را با تک‌تک نویسه‌ها شروع کنید. تمام جفت‌های مجاور را در پیکره‌ی متنی آموزشی بشمارید. پرتکرارترین جفت را merge کنید و یک توکن جدید بسازید. این کار را آن‌قدر تکرار کنید تا به اندازه‌ی واژگان مد نظرتان برسید.

در اینجا اجرای BPE را روی یک پیکره‌ی متنی کوچک با کلمات «lower»، «lowest» و «newest» می‌بینید:

Corpus (with word frequencies):
  "lower"  x5
  "lowest" x2
  "newest" x6

Step 0 -- Start with characters:
  l o w e r       (x5)
  l o w e s t     (x2)
  n e w e s t     (x6)

Step 1 -- Count adjacent pairs:
  (e,s): 8    (s,t): 8    (l,o): 7    (o,w): 7
  (w,e): 13   (e,r): 5    (n,e): 6    ...

Step 2 -- Merge most frequent pair (w,e) -> "we":
  l o we r        (x5)
  l o we s t      (x2)
  n e we s t      (x6)

Step 3 -- Recount and merge (e,s) -> "es":
  l o we r        (x5)
  l o we s t      (x2)    <- 'es' only forms from 'e'+'s', not 'we'+'s'
  n e we s t      (x6)    <- wait, the 'e' before 'we' and 's' after 'we'

Actually tracking this precisely:
  After "we" merge, remaining pairs:
  (l,o): 7   (o,we): 7   (we,r): 5   (we,s): 8
  (s,t): 8   (n,e): 6    (e,we): 6

Step 3 -- Merge (we,s) -> "wes" or (s,t) -> "st" (tied at 8, pick first):
  Merge (we,s) -> "wes":
  l o we r        (x5)
  l o wes t       (x2)
  n e wes t       (x6)

Step 4 -- Merge (wes,t) -> "west":
  l o we r        (x5)
  l o west        (x2)
  n e west        (x6)

...continue until target vocab size reached.

جدول merge همان توکنایزر است. برای انکود کردن متن جدید، عملیات‌های merge را دقیقاً به همان ترتیبی که در زمان آموزش یاد گرفته شده‌اند اعمال کنید. مجموعه‌ی داده‌های آموزشی تعیین می‌کند چه mergeهایی وجود داشته باشند و همین انتخاب، ماهیت آنچه را مدل می‌بیند برای همیشه شکل می‌دهد.

حلقه آموزش BPEخیربلهشروع: واژگان در سطح نویسهشمارش تمام جفت‌های مجاورmerge پرتکرارترین جفتافزودن توکن merge شده به واژگانرسیدن به اندازهواژگان هدف؟پایان: ذخیره جدول merge

الگوریتم Byte-Level BPE (در مدل‌های GPT-2، GPT-3 و GPT-4)

الگوریتم استاندارد BPE روی نویسه‌های یونیکد کار می‌کند. در مقابل، Byte-level BPE مستقیماً روی بایت‌های خام (0 تا 255) اجرا می‌شود. این رویکرد یک واژگان پایه‌ی دقیقاً 256 عضوی به شما می‌دهد، از هر زبان یا انکودینگی پشتیبانی می‌کند و هرگز توکن ناشناخته تولید نخواهد کرد.

مدل GPT-2 این رویکرد را معرفی کرد. واژگان پایه تک‌تک بایت‌های ممکن را پوشش می‌دهد و عمل‌های merge در BPE روی این پایه بنا می‌شوند. کتابخانه‌ی tiktoken از شرکت OpenAI الگوریتم byte-level BPE را با اندازه‌‌های واژگان زیر پیاده‌سازی کرده است:

  • مدل GPT-2: دقیقاً 50,257 توکن
  • مدل‌های GPT-3.5 و GPT-4: حدود 100,256 توکن (انکودینگ cl100k_base)
  • مدل GPT-4o: دقیقاً 200,019 توکن (انکودینگ o200k_base)

الگوریتم WordPiece (مدل BERT)

الگوریتم WordPiece ظاهری شبیه به BPE دارد، اما جفت‌ها را به شیوه‌ی متفاوتی برای merge انتخاب می‌کند. به‌جای بسامد خام، معیاری را بیشینه می‌کند که درست‌نمایی (likelihood) داده‌های آموزش را به حداکثر برساند:

BPE merge criterion:      count(A, B)
WordPiece merge criterion: count(AB) / (count(A) * count(B))

در حالی که BPE می‌پرسد: «کدام جفت بیشترین تکرار را دارد؟»، الگوریتم WordPiece می‌پرسد: «کدام جفت بیش از آنچه بر حسب شانس و تصادف انتظار می‌رفت در کنار هم آمده‌اند؟» این تفاوت ظریف، واژگان متفاوتی تولید می‌کند. WordPiece جاهایی merge را ترجیح می‌دهد که هم‌آیی دو جزء فراتر از تصادف باشد، نه صرفاً پرتکرار.

علاوه بر این، WordPiece از پیشوند «##» برای ادامه‌ی زیرکلمه‌ها استفاده می‌کند:

"unhappiness" -> ["un", "##happi", "##ness"]
"embedding"   -> ["em", "##bed", "##ding"]

پیشوند «##» نشان می‌دهد این قطعه، ادامه‌ی توکن قبلی است. مدل BERT از WordPiece با واژگانی شامل 30,522 توکن استفاده می‌کند. تمام مشتقات BERT مثل DistilBERT هم همین‌طورند؛ البته توکنایزر RoBERTa در واقع BPE است، اما خود BERT از WordPiece استفاده می‌کند.

الگوریتم SentencePiece (مدل‌های Llama و T5)

ابزار SentencePiece با ورودی مانند یک جریان خام از نویسه‌های یونیکد، از جمله فاصله‌ها، برخورد می‌کند. در این روش مرحله‌ی پیش‌توکن‌سازی وجود ندارد و هیچ قاعده‌ی زبانی خاصی برای تعیین مرز کلمات اعمال نمی‌شود. به همین دلیل کاملاً مستقل از زبان است؛ ویژگی بسیار مهمی که کار با زبان‌هایی مثل چینی، ژاپنی، تایلندی و سایر خطوطی را که در آن‌ها کلمات با فاصله جدا نمی‌شوند، ممکن می‌سازد.

ابزار SentencePiece از دو الگوریتم پشتیبانی می‌کند:

  • حالت BPE: همان منطق merge در BPE استاندارد را روی دنباله‌های خام نویسه‌ها پیاده می‌کند.
  • حالت Unigram: کار را با یک واژگان بسیار بزرگ شروع می‌کند و توکن‌هایی را که کمترین تأثیر را روی درست‌نمایی کلی دارند به‌صورت تکرارشونده حذف می‌کند؛ یعنی نقطه‌ی مقابل BPE: هرس کردن (prune) به‌جای ادغام.

مدل Llama 2 از SentencePiece BPE با واژگان 32,000 توکنی استفاده می‌کند. مدل T5 از SentencePiece Unigram با 32,000 توکن بهره می‌برد. البته دقت کنید که Llama 3 به یک توکنایزر byte-level BPE بر پایه‌ی tiktoken با 128,256 توکن تغییر مسیر داده است.

موازنه‌های اندازه‌ی واژگان

این یک تصمیم‌گیری مهندسی واقعی است که پیامدهای ملموس و قابل اندازه‌گیری دارد.

واژگان بزرگ (128K+)مانند Llama 3, GPT-4oواژگان کوچک (32K)مانند BERT, T5توکن‌های بیشتر در هر متنتوالی‌های طولانی‌ترماتریس امبدینگ کوچک‌ترمدیریت بهتر کلمات نادرتوکن‌های کمتر در هر متنتوالی‌های کوتاه‌ترماتریس امبدینگ بزرگ‌تراستنتاج سریع‌تر

بیایید اعداد واقعی را بررسی کنیم. برای واژگانی با 128K توکن و امبدینگ‌هایی با بعد 4,096، صرفاً ماتریس امبدینگ شامل 524 میلیون پارامتر خواهد بود (128,000 × 4,096). برای واژگان 32K، این رقم به 131 میلیون پارامتر کاهش می‌یابد؛ یعنی 400 میلیون پارامتر اختلاف، صرفاً ناشی از انتخاب توکنایزر.

از طرف دیگر، واژگان بزرگ‌تر متن را با قدرت بیشتری فشرده می‌کند. همان پاراگراف انگلیسی که با واژگان 32K به 100 توکن نیاز دارد، ممکن است با واژگان 128K تنها 70 توکن مصرف کند. این یعنی 30% کاهش در تعداد گذر رو به جلو (forward pass) در مرحله‌ی تولید؛ مزیتی که برای مدلی در حال سرویس‌دهی به میلیون‌ها درخواست، مستقیماً هزینه‌های پردازش را پایین می‌آورد.

روند کاملاً مشخص است: اندازه‌ی واژگان مدام در حال رشد است. مدل GPT-2 از 50,257 توکن استفاده می‌کرد، GPT-4 از حدود 100K، مدل Llama 3 از 128K و GPT-4o به 200K توکن رسیده است.

مدل اندازه‌ی واژگان نوع توکنایزر میانگین توکن به ازای هر کلمه‌ی انگلیسی
BERT 30,522 WordPiece حدود 1.4
GPT-2 50,257 Byte-level BPE حدود 1.3
Llama 2 32,000 SentencePiece BPE حدود 1.4
GPT-4 حدود 100,256 Byte-level BPE حدود 1.2
Llama 3 128,256 Byte-level BPE (tiktoken) حدود 1.1
GPT-4o 200,019 Byte-level BPE حدود 1.0

مالیات چندزبانه (The Multilingual Tax)

توکنایزرهایی که اساساً روی زبان انگلیسی آموزش دیده‌اند، در قبال سایر زبان‌ها بسیار بی‌رحمند. متن کره‌ای در توکنایزر GPT-2 به طور میانگین به 2 تا 3 توکن به ازای هر کلمه شکسته می‌شود و در چینی اوضاع از این هم بدتر است. این یعنی یک کاربر کره‌ای عملاً پنجره‌ی زمینه‌ای با نصف ظرفیت کاربر انگلیسی‌زبان در اختیار دارد و هزینه‌ی یکسانی را برای تراکم اطلاعاتی بسیار کمتری می‌پردازد.

دقیقاً به همین دلیل است که Llama 3 اندازه‌ی واژگان خود را چهار برابر کرد و از 32K به 128K رساند. تخصیص توکن‌های بیشتر به خطوط غیرانگلیسی، فشرده‌سازی عادلانه‌تری را برای تمام زبان‌ها به ارمغان می‌آورد.

موازنه توکنایزراندازه واژگان را بکشید
واژگان بزرگ‌تر متن را به توکن‌های کمتری خرد می‌کند، بنابراین طول توالی‌ها کوتاه‌تر شده و توجه روی آن‌ها کم‌هزینه‌تر خواهد بود. اما جدول‌های امبدینگ و خروجی با اندازه واژگان رشد می‌کنند، در نتیجه این مزیت با افزایش پارامترها جبران می‌شود. توکنایزرهای واقعی در جایی قرار می‌گیرند که این دو فشار به تعادل برسند، یعنی حدود 32K تا 128K.

پیاده‌سازی عملی

گام 1: توکنایزر در سطح نویسه

از پایه‌ای‌ترین بخش شروع می‌کنیم. یک توکنایزر در سطح نویسه هر کاراکتر را به مقدار کدپوینت (code point) یونیکد آن متصل می‌کند. نیازی به آموزش نیست و هیچ توکن ناشناخته‌ای هم تولید نمی‌شود؛ فقط یک نگاشت مستقیم است.

class CharTokenizer:
    def encode(self, text):
        return [ord(c) for c in text]

    def decode(self, tokens):
        return "".join(chr(t) for t in tokens)

کلمه‌ی «hello» به [104, 101, 108, 108, 111] تبدیل می‌شود. هر نویسه برای خودش یک توکن مجزاست. این همان خط مبنایی (baseline) است که می‌خواهیم ارتقایش دهیم.

گام 2: پیاده‌سازی توکنایزر BPE از صفر

پیاده‌سازی واقعی اینجاست. مدل را مستقیماً روی بایت‌های خام آموزش می‌دهیم (مشابه GPT-2)، جفت‌ها را می‌شماریم، پرتکرارترین‌ها را merge می‌کنیم و تک‌تک ادغام‌ها را به ترتیب ثبت می‌کنیم. جدول merge دقیقاً همان خودِ توکنایزر است.

from collections import Counter

class BPETokenizer:
    def __init__(self):
        self.merges = {}
        self.vocab = {}

    def _get_pairs(self, tokens):
        pairs = Counter()
        for i in range(len(tokens) - 1):
            pairs[(tokens[i], tokens[i + 1])] += 1
        return pairs

    def _merge_pair(self, tokens, pair, new_token):
        merged = []
        i = 0
        while i < len(tokens):
            if i < len(tokens) - 1 and tokens[i] == pair[0] and tokens[i + 1] == pair[1]:
                merged.append(new_token)
                i += 2
            else:
                merged.append(tokens[i])
                i += 1
        return merged

    def train(self, text, num_merges):
        tokens = list(text.encode("utf-8"))
        self.vocab = {i: bytes([i]) for i in range(256)}

        for i in range(num_merges):
            pairs = self._get_pairs(tokens)
            if not pairs:
                break
            best_pair = max(pairs, key=pairs.get)
            new_token = 256 + i
            tokens = self._merge_pair(tokens, best_pair, new_token)
            self.merges[best_pair] = new_token
            self.vocab[new_token] = self.vocab[best_pair[0]] + self.vocab[best_pair[1]]

        return self

    def encode(self, text):
        tokens = list(text.encode("utf-8"))
        for pair, new_token in self.merges.items():
            tokens = self._merge_pair(tokens, pair, new_token)
        return tokens

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

حلقه‌ی آموزش، قلب تپنده‌ی BPE است: شمارش جفت‌ها، merge کردن جفت برنده و تکرار فرآیند. هر بار merge از تعداد کل توکن‌ها کم می‌کند. پس از num_merges دور تکرار، واژگان ما از 256 عضو (بایت‌های پایه) به 256 + num_merges عضو می‌رسد.

مرحله‌ی انکود کردن، عملیات‌های merge را دقیقاً به همان ترتیبی اعمال می‌کند که یاد گرفته است. ترتیب در اینجا حیاتی است؛ اگر merge شماره‌ی 1 ترکیب «th» را ساخته باشد و merge شماره‌ی 5 کلمه‌ی «the» را، هنگام انکود باید ابتدا merge شماره‌ی 1 اعمال شود تا «the» بتواند در merge شماره‌ی 5 از ترکیب «th» و «e» شکل بگیرد.

دیکود کردن مسیر معکوس است: یافتن شناسه‌ی هر توکن در واژگان، چسباندن بایت‌ها به یکدیگر و دیکود کردن نهایی به UTF-8.

گام 3: رفت‌وبرگشت کامل انکود و دیکود

corpus = (
    "The cat sat on the mat. The cat ate the rat. "
    "The dog sat on the log. The dog ate the frog. "
    "Natural language processing is the study of how computers "
    "understand and generate human language. "
    "Tokenization is the first step in any NLP pipeline."
)

tokenizer = BPETokenizer()
tokenizer.train(corpus, num_merges=40)

test_sentences = [
    "The cat sat on the mat.",
    "Natural language processing",
    "tokenization pipeline",
    "unhappiness",
]

for sentence in test_sentences:
    encoded = tokenizer.encode(sentence)
    decoded = tokenizer.decode(encoded)
    raw_bytes = len(sentence.encode("utf-8"))
    ratio = len(encoded) / raw_bytes
    print(f"'{sentence}'")
    print(f"  Tokens: {len(encoded)} (from {raw_bytes} bytes) -- ratio: {ratio:.2f}")
    print(f"  Roundtrip: {'PASS' if decoded == sentence else 'FAIL'}")

نسبت فشرده‌سازی (compression ratio) نشان‌دهنده‌ی کارایی توکنایزر است. نسبت 0.50 یعنی توکنایزر متن را به نصف تعداد بایت‌های خام فشرده کرده است. هرچه این عدد کمتر باشد، عملکرد بهتر است. روی پیکره‌ی داده‌های آموزشی، این نسبت خوب خواهد بود؛ اما روی متنی خارج از توزیع داده‌ها مثل «unhappiness» (که در داده‌های آموزشی نبوده)، نسبت فشرده‌سازی ضعیف‌تر می‌شود و توکنایزر برای الگوهای دیده‌نشده به همان انکود در سطح نویسه برمی‌گردد.

گام 4: مقایسه با tiktoken

import tiktoken

enc = tiktoken.get_encoding("cl100k_base")

texts = [
    "The cat sat on the mat.",
    "unhappiness",
    "Hello, world!",
    "def fibonacci(n): return n if n < 2 else fibonacci(n-1) + fibonacci(n-2)",
    "Geschwindigkeitsbegrenzung",
]

for text in texts:
    our_tokens = tokenizer.encode(text)
    tiktoken_tokens = enc.encode(text)
    tiktoken_pieces = [enc.decode([t]) for t in tiktoken_tokens]
    print(f"'{text}'")
    print(f"  Our BPE:   {len(our_tokens)} tokens")
    print(f"  tiktoken:  {len(tiktoken_tokens)} tokens -> {tiktoken_pieces}")

کتابخانه‌ی tiktoken دقیقاً از همین الگوریتم استفاده می‌کند، اما روی صدها گیگابایت متن و با 100,000 عملیات merge آموزش دیده است. منطق الگوریتم کاملاً یکسان است؛ تفاوت فقط در داده‌های آموزش و تعداد ادغام‌هاست. توکنایزر شما که روی یک پاراگراف و با 40 بار merge آموزش دیده، توان رقابت با 100K بار merge کتابخانه‌ی tiktoken روی داده‌های عظیم را ندارد؛ اما سازوکار هر دو یکی است.

گام 5: تحلیل واژگان

def analyze_vocabulary(tokenizer, test_texts):
    total_tokens = 0
    total_chars = 0
    token_usage = Counter()

    for text in test_texts:
        encoded = tokenizer.encode(text)
        total_tokens += len(encoded)
        total_chars += len(text)
        for t in encoded:
            token_usage[t] += 1

    print(f"Vocabulary size: {len(tokenizer.vocab)}")
    print(f"Total tokens across all texts: {total_tokens}")
    print(f"Total characters: {total_chars}")
    print(f"Avg tokens per character: {total_tokens / total_chars:.2f}")

    print(f"\nMost used tokens:")
    for token_id, count in token_usage.most_common(10):
        token_bytes = tokenizer.vocab[token_id]
        display = token_bytes.decode("utf-8", errors="replace")
        print(f"  Token {token_id:4d}: '{display}' (used {count} times)")

    unused = [t for t in tokenizer.vocab if t not in token_usage]
    print(f"\nUnused tokens: {len(unused)} out of {len(tokenizer.vocab)}")

این تحلیل توزیع زیف (Zipf distribution) را در واژگان شما آشکار می‌کند. بخش اندکی از توکن‌ها بیشترین استفاده را دارند (فاصله‌ها، «the»، «e») و بیشتر توکن‌ها به‌ندرت استفاده می‌شوند. توکنایزرهای محیط عملیاتی برای همین توزیع بهینه‌سازی می‌شوند؛ الگوهای رایج شناسه‌های توکن کوتاهی می‌گیرند و الگوهای نادر با بازنمایی‌های طولانی‌تر نشان داده می‌شوند.

استفاده از ابزارهای آماده

پیاده‌سازی دستی BPE شما کار می‌کند؛ حالا ببینیم ابزارهای محیط عملیاتی چه شکلی هستند.

ابزار tiktoken (از شرکت OpenAI)

import tiktoken

enc = tiktoken.get_encoding("cl100k_base")

text = "Tokenizers convert text to integers"
tokens = enc.encode(text)
print(f"Tokens: {tokens}")
print(f"Pieces: {[enc.decode([t]) for t in tokens]}")
print(f"Roundtrip: {enc.decode(tokens)}")

کتابخانه‌ی tiktoken با زبان Rust نوشته شده و رابط‌های پایتون دارد. این ابزار میلیون‌ها توکن را در ثانیه انکود می‌کند. همان الگوریتم BPE، اما در قالب پیاده‌سازی صنعتی و مستحکم.

کتابخانه‌ی tokenizers متعلق به Hugging Face

from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import ByteLevel

tokenizer = Tokenizer(BPE())
tokenizer.pre_tokenizer = ByteLevel()

trainer = BpeTrainer(vocab_size=1000, special_tokens=["<pad>", "<eos>", "<unk>"])
tokenizer.train(["corpus.txt"], trainer)

output = tokenizer.encode("The cat sat on the mat.")
print(f"Tokens: {output.tokens}")
print(f"IDs: {output.ids}")

کتابخانه‌ی tokenizers در Hugging Face هم در لایه‌ی زیرین از زبان Rust قدرت می‌گیرد. این ابزار می‌تواند BPE را روی پیکره‌های متنی در مقیاس چند گیگابایت ظرف چند ثانیه آموزش دهد. وقتی بخواهید مدل خودتان را آموزش دهید، سراغ این کتابخانه می‌روید.

بارگذاری توکنایزر مدل Llama

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B")

text = "Tokenizers are the unsung heroes of LLMs"
tokens = tokenizer.encode(text)
print(f"Token IDs: {tokens}")
print(f"Tokens: {tokenizer.convert_ids_to_tokens(tokens)}")
print(f"Vocab size: {tokenizer.vocab_size}")

multilingual = ["Hello world", "Hola mundo", "Bonjour le monde"]
for text in multilingual:
    ids = tokenizer.encode(text)
    print(f"'{text}' -> {len(ids)} tokens")

واژگان 128K مدل Llama 3 متن‌های غیرانگلیسی را به‌مراتب بهتر از واژگان 50K مدل GPT-2 فشرده می‌کند. خودتان می‌توانید این موضوع را بررسی کنید؛ یک جمله را در چند زبان مختلف انکود کنید و تعداد توکن‌هایش را بشمارید.

مرحله‌ی تحویل (Ship It)

حاصل این درس فایل outputs/prompt-tokenizer-analyzer.md خواهد بود؛ پرامپتی با قابلیت استفاده‌ی مجدد که بازدهی توکن‌سازی را برای هر ترکیبی از متن و مدل تحلیل می‌کند. یک نمونه متن به آن بدهید تا به شما بگوید توکنایزر کدام مدل بهترین عملکرد را در پردازش آن دارد.

تمرین‌ها

  1. توکنایزر BPE را طوری تغییر دهید که در هر مرحله از merge، واژگان را چاپ کند. ببینید چگونه «t» و «h» تبدیل به «th» می‌شوند و سپس «th» و «e» کلمه‌ی «the» را می‌سازند. روند ساخته شدن کلمات رایج انگلیسی را گام به گام دنبال کنید.

  2. توکن‌های ویژه (<pad>، <eos> و <unk>) را به توکنایزر BPE اضافه کنید. شناسه‌های 0، 1 و 2 را به آن‌ها اختصاص دهید و شناسه‌ی سایر توکن‌ها را متناسب با آن جابه‌جا کنید. یک مرحله‌ی پیش‌توکن‌سازی پیاده کنید که قبل از اجرای BPE، متن را بر اساس فاصله‌ها بشکند.

  3. معیار merge در الگوریتم WordPiece (نسبت درست‌نمایی به‌جای بسامدِ صِرف) را پیاده‌سازی کنید. هر دو الگوریتم BPE و WordPiece را روی یک پیکره‌ی داده و با تعداد merge یکسان آموزش دهید. واژگان نهایی آن‌ها را مقایسه کنید؛ کدام الگوریتم زیرکلمه‌های معنادارتری از نظر زبانی تولید می‌کند؟

  4. یک بنچمارک برای سنجش بازدهی توکنایزرهای چندزبانه بسازید. 10 جمله به زبان‌های انگلیسی، اسپانیایی، چینی، کره‌ای و عربی انتخاب کنید. هر کدام را با tiktoken (انکودینگ cl100k_base) توکن‌سازی کرده و میانگین توکن به ازای هر نویسه را اندازه بگیرید. «مالیات چندزبانه» را برای هر زبان به صورت عددی به دست آورید.

  5. توکنایزر BPE خود را روی پیکره‌ی متنی بزرگ‌تری آموزش دهید (مثلاً یک مقاله‌ی ویکی‌پدیا را دانلود کنید). تعداد ادغام‌ها را طوری تنظیم کنید که نسبت فشرده‌سازی شما روی همان متن، با اختلافی کمتر از 10% به tiktoken برسد. این تمرین شما را وادار می‌کند رابطه‌ی میان حجم داده‌ها، تعداد merge و کیفیت فشرده‌سازی را عمیقاً درک کنید.

اصطلاحات کلیدی

اصطلاح باور عامیانه معنای دقیق فنی
توکن (Token) «یک کلمه» واحدی در واژگان مدل؛ می‌تواند نویسه، زیرکلمه، کلمه یا قطعه‌ای چندکلمه‌ای باشد
BPE «یک جور فشرده‌سازی» کدگذاری جفت‌بایت؛ merge تکرارشونده‌ی پرتکرارترین جفت توکن‌های مجاور تا رسیدن به اندازه‌ی واژگان هدف
WordPiece «توکنایزر BERT» شبیه BPE است، اما mergeها نسبت درست‌نمایی count(AB)/(count(A)*count(B)) را بیشینه می‌کنند، نه صرفاً بسامد خام را
SentencePiece «یک کتابخانه‌ی توکنایزر» توکنایزری مستقل از زبان که بدون نیاز به پیش‌توکن‌سازی روی یونیکد خام اجرا می‌شود و از الگوریتم‌های BPE و Unigram پشتیبانی می‌کند
اندازه‌ی واژگان (Vocabulary size) «مدل چند کلمه بلد است» تعداد کل توکن‌های یکتا: GPT-2 دارای 50,257، BERT دارای 30,522 و Llama 3 دارای 128,256 توکن است
باروری (Fertility) «اصطلاحی در توکنایزر نیست» میانگین تعداد توکن به ازای هر کلمه؛ معیاری برای بازدهی توکنایزر در زبان‌های مختلف (1.0 ایده‌آل است، 3.0 یعنی مدل سه برابر بیشتر کار می‌کند)
Byte-level BPE «توکنایزر GPT» الگوریتم BPE که به‌جای نویسه‌های یونیکد روی بایت‌های خام (0 تا 255) اجرا می‌شود و عدم تولید توکن ناشناخته را برای هر ورودی تضمین می‌کند
جدول merge «فایلِ خودِ توکنایزر» فهرست مرتبی از ادغام‌های دوتایی که در زمان آموزش یاد گرفته شده‌اند؛ خودِ توکنایزر دقیقاً همین است و ترتیب در آن اهمیت دارد
پیش‌توکن‌سازی (Pre-tokenization) «شکستن روی فاصله‌ها» قواعدی که قبل از توکن‌سازی زیرکلمه اعمال می‌شوند: جداسازی با فاصله، تفکیک ارقام و مدیریت علائم نگارشی
نسبت فشرده‌سازی (Compression ratio) «توکنایزر چقدر بهینه است» تعداد توکن‌های تولیدشده تقسیم بر بایت‌های ورودی؛ مقدار کمتر یعنی فشرده‌سازی بهتر و استنتاج سریع‌تر

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

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

تفاوت اصلی بین BPE و WordPiece در چیست؟

الگوریتم BPE برای ادغام توکن‌ها صرفاً به بسامد تکرار جفت‌ها تکیه می‌کند، اما WordPiece بر اساس بیشینه‌سازی نسبت درست‌نمایی عمل کرده و جفت‌هایی را ادغام می‌کند که هم‌آیی آن‌ها فراتر از شانس و تصادف باشد.

چرا Byte-level BPE در مدل‌های جدید مثل GPT-4 محبوب است؟

چون روی بایت‌های خام (0 تا 255) اجرا می‌شود و با داشتن یک واژگان پایه‌ی 256 عضوی، هر ورودی در هر زبانی را پوشش می‌دهد و هرگز توکن ناشناخته یا [UNK] تولید نمی‌کند.

مالیات چندزبانه (Multilingual Tax) در توکنایزرها یعنی چه؟

وقتی توکنایزری عمدتاً روی زبان انگلیسی آموزش داده شود، کلمات زبان‌های دیگر به قطعات و توکن‌های بیشتری شکسته می‌شوند. این مسئله باعث مصرف سریع‌تر پنجره‌ی زمینه و افزایش هزینه‌ی پردازش برای آن زبان‌ها می‌شود.

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

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