المُرمِّزات v1: الترميز وفك الترميز والتوسّع، مقيسة
يحوّل Tokenizers v1 خط أنابيب الترميز–فك الترميز إلى مكوّن قابل للقياس: مكتبة واحدة، وتطبيع متسق، ومعايير أداء تكشف كيف تتدرّج الإنتاجية والذاكرة مع حجم المفردات وطول التسلسل. يستعرض هذا الدليل الترميز وفك الترميز والأرقام العملية التي ينبغي للفرق تتبّعها قبل اعتماد مُرمِّز كمعيار.
ملخص سريع
يحوّل Tokenizers v1 خط أنابيب الترميز–فك الترميز إلى مكوّن قابل للقياس: مكتبة واحدة، وتطبيع متسق، ومعايير أداء تكشف كيف تتدرّج الإنتاجية والذاكرة مع حجم المفردات وطول التسلسل. يستعرض هذا الدليل الترميز وفك الترميز والأرقام العملية التي ينبغي للفرق تتبّعها قبل اعتماد مُرمِّز كمعيار.
Tokenizers v1: الترميز وفك الترميز والتوسّع، مقيسًا
قد تقضي منظومة تقديم الخدمة وقتًا أطول في تحويل النص إلى أعداد صحيحة مما تقضيه في كتلة transformer الأولى. هذا القول استفزازي عن قصد، وهو ليس صحيحًا دائمًا — فبالنسبة للمطالبات القصيرة على GPU دافئ، يكون الترميز عادةً خطأ تقريب. لكنه يصبح صحيحًا بما يكفي من التكرار، وفي بما يكفي من أحمال العمل، حتى إن منشور مدونة Hugging Face بعنوان tokenizers v1: encode, decode and scaling, measured (huggingface.co/blog/tokenizers-v1) يستحق القراءة كوثيقة هندسية لا كإعلان. تتعامل هذه المقالة معه بهذه الطريقة: ما الذي تكلّفه حدود الترميز/فك الترميز فعليًا، وكيف تُثبّت المكتبة وتُشغّلها، وكيف تقيس التوسّع دون أن تخدع نفسك.
حيثما يورد المصدر أرقامًا محددة، تعامل مع تلك الأرقام على أنها تنتمي إلى عتاد المصدر وتوزيع نصه وإعداده. ما يلي هو المنهجية والشيفرة العاملة التي تحتاجها لإعادة إنتاجها أو دحضها على جهازك.
لماذا يستحق الترميز وفك الترميز أرقامًا خاصة بهما
الترميز هو الجزء الوحيد في خط استدلال حديث يكون متسلسلًا جوهريًا لكل مستند ومقيّدًا بوحدة المعالجة المركزية. الانتباه متوازٍ عبر التسلسل؛ لكن تمريرة regex للمُرمِّز المسبق ليست كذلك. هذا التفاوت هو سبب كون سلوك التوسّع مثيرًا للاهتمام: إضافة وحدات GPU لا تجعل المُرمِّز أسرع، وإضافة حجم دفعة يساعد فقط إلى النقطة التي يتشبع فيها تجمّع خيوط نواة Rust.
ثلاث كميات تهم عمليًا:
- كلفة الترميز — نصّ داخل، معرّفات صحيحة خارج. تُدفع مرة لكل طلب على المسار الحرج، ومرة لكل مستند في المعالجة غير المتصلة للمدوّنة.
- كلفة فك الترميز — معرّفات خارج، نصّ عائد. تُدفع عند كل رمز متدفق إذا فككت الترميز تدريجيًا، وهي كلفة شائعة وغالبًا غير ملحوظة في واجهات المحادثة.
- الخصوبة (Fertility) — رموز لكل وحدة نص. ليست قياس سرعة، لكنها تحكم مقدار طول التسلسل الذي تشتريه لكل حرف، وبالتالي مقدار كلفة الانتباه التي تستلزمها مدوّنة معينة.
التوسّع، بهذا التأطير، ليس منحنى واحدًا. إنه أربعة على الأقل: طول التسلسل، وحجم الدفعة، وعدد الخيوط، ومجال النص. معيار أداء يثبّت ثلاثة منها ويغيّر واحدًا مفيد. أما معيار يغيّر الأربعة دفعة واحدة فينتج رقمًا رئيسيًا لا يستطيع أحد التصرف بناءً عليه.
المتطلبات
قبل تثبيت أي شيء، تأكد من توفر ما يلي:
- مفسّر CPython 3.x مدعوم. تحقق من إصدارك باستخدام
python --versionوراجع بيانات وصف الحزمة للإصدار الذي تثبته؛ فنطاق الدعم يتغير بمرور الوقت. pip، ويفضّل بيئة افتراضية حتى لا تتعارض مكتبة الترميز مع تبعيات أخرى مثبّتة.- ملف
tokenizer.json، أو مدوّنة يمكنك تدريب واحد منها. المكتبة الأساسية محايدة عمدًا بشأن مصدر المفردات. - جهاز يمكنك إبقاءه هادئًا أثناء القياس. سيتسبب تحجيم تردد CPU، وحصص CPU في الحاويات، والجيران المزعجون في الظهور في p95 قبل أن تظهر المكتبة نفسها.
- اختياري: سلسلة أدوات Rust، فقط إذا كنت تنوي بناء الارتباطات من الشيفرة المصدرية بدلًا من تثبيت wheel.
بالنسبة لدراسة توسّع، قرّر مسبقًا أيضًا أي محور ستغيّره. اكتبه قبل أن تشغّل أي شيء.
التثبيت خطوة بخطوة
أنشئ بيئة معزولة أولًا، حتى لا يستطيع pip install لاحق ترقية تبعية كان معيار الأداء الخاص بك معايرًا عليها من دون أن تشعر.
python -m venv .venv
source .venv/bin/activate # ويندوز: .venv\Scripts\activateحدّث أدوات التغليف، لأن إصدارات pip الأقدم تحل أحيانًا ملفات wheel دون المستوى الأمثل.
python -m pip install --upgrade pipثبّت مكتبة tokenizers. ملفات wheel المبنية مسبقًا هي المسار المعتاد وتتفادى الحاجة إلى مترجم Rust.
pip install tokenizersأكّد الاستيراد وسجّل الإصدار الدقيق في ملاحظاتك — فمعيار أداء بلا سلسلة إصدار ليس قابلًا لإعادة الإنتاج.
python -c "import tokenizers; print(tokenizers.__version__)"إذا احتجت إلى البناء من الشيفرة المصدرية — مثلًا لاختبار تغيير غير مُصدَر أو منصة بلا ملفات wheel — فثبّت سلسلة أدوات Rust وخلفية بناء، ثم ابنِ ارتباطات Python من نسخة مستنسخة من مستودع tokenizers التابع لـ Hugging Face.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
pip install maturingit clone https://github.com/huggingface/tokenizers
cd tokenizers/bindings/python
pip install -e .ملاحظتان في الإعداد تهمان للقياس. أولًا، ترميز الدفعات مُنفَّذ في Rust وقد يستخدم تجمّع خيوط يسرق العمل؛ وحيث يحترم وقت التشغيل متغير بيئة مثل RAYON_NUM_THREADS، ثبّته حتى يكون محور عدد الخيوط لديك فعلًا هو المتغير الذي تظنه. ثانيًا، تعرض بعض عمليات النشر TOKENIZERS_PARALLELISM للتحكم فيما إذا كان العمل المتوازي يتفرّع؛ اضبطه صراحة وبشكل متطابق في كل تشغيل ضمن مقارنة بدلًا من تركه يأخذ افتراضيًا مختلفًا حسب البيئة.
الترميز وفك الترميز عمليًا
كائن Encoding أغنى من قائمة أعداد صحيحة. فهو يحمل:
ids— فهارس المفردات التي يستهلكها النموذج.tokens— السلاسل السطحية المقابلة، وهي ما تريده عند تنقيح تقسيم ما.offsets— امتدادات المحارف عائدة إلى النص الأصلي، وهي ما تريده للتظليل أو الإسناد أو وسم الامتدادات.attention_maskوtype_idsوspecial_tokens_mask— التنسورات المساعدة التي تحتاجها حلقة تدريب عادةً.overflowing— الترميزات المنتَجة عندما يتجاوز تسلسل حدّ الاقتطاع لديك وتكون قد طلبت الإبقاء على الباقي.
فك الترميز هو عملية عكسية، لا دالة عكسية. التطبيع كثيرًا ما يكون فاقدًا: طي حالة الأحرف، وتطبيع Unicode، ودمج المسافات البيضاء، وإعادة تعيين على مستوى البايت، كلها يمكن أن تجعل decode(encode(x)) يختلف عن x كسلسلة. الاختبار ذو المعنى هو اللامتغيّرية (idempotence) — إذ ينبغي أن يعيد ترميز المخرجات المفكوكة نفس المعرّفات — لا تساوي السلسلة مع المدخل.
أمثلة استخدام
المثال 1 — رحلة ذهاب وإياب دنيا
حمّل مُرمِّزًا من تعريف JSON محفوظ وافحص ما يعود.
from tokenizers import Tokenizer
tok = Tokenizer.from_file("tokenizer.json")
text = "Tokenizers sit between raw text and model weights."
enc = tok.encode(text)
print(enc.tokens) # القطع السطحية
print(enc.ids) # أعداد صحيحة موجهة إلى النموذج
print(tok.decode(enc.ids)) # نص معاد بناؤه
print(enc.offsets[:5]) # امتدادات المحارفاللامتغيّرية هي الفحص الذي يصمد أمام التطبيع الفاقد: أعد ترميز السلسلة المفكوكة وأكّد تطابق المعرّفات.
ids_once = tok.encode(text).ids
ids_twice = tok.encode(tok.decode(ids_once)).ids
print(ids_once == ids_twice) # ينبغي أن يكون True؛ قيمة False هنا تعني عدم استقرارالمثال 2 — ترميز دفعي مع الحشو والاقتطاع
اضبط المُرمِّز مرة واحدة، ثم سلّمه قائمة. طرق الدفعات هي حيث تستحق نواة Rust وجودها، لأنها تستطيع التوازي عبر المستندات.
tok.enable_truncation(max_length=512)
tok.enable_padding(length=512)
texts = [open(p, encoding="utf-8").read() for p in ["a.txt", "b.txt", "c.txt"]]
encs = tok.encode_batch(texts)
print(len(encs), len(encs[0].ids), len(encs[0].attention_mask))إذا كنت تهمك الإنتاجية أكثر من التنسورات ذات الشكل الثابت، فتجاوز الحشو والاقتطاع كليًا: فالحشو إلى طول ثابت ينفخ أعداد الرموز ويغيّر بهدوء ما يعنيه رقم الرموز في الثانية لديك.
المثال 3 — تدريب مُرمِّز BPE على مستوى البايت من مُكرِّر
للعمل الخاص بمدوّنة معينة، درّب بدلًا من أن ترث. تتيح واجهة المُكرِّر بثّ مدوّنة كبيرة دون تجسيدها في الذاكرة.
from tokenizers import Tokenizer, models, trainers, pre_tokenizers, decoders
tok = Tokenizer(models.BPE(unk_token="[UNK]"))
tok.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
tok.decoder = decoders.ByteLevel()
trainer = trainers.BpeTrainer(
vocab_size=30000,
special_tokens=["[UNK]", "[PAD]", "[CLS]", "[SEP]", "[MASK]"],
)
def corpus():
with open("corpus.txt", encoding="utf-8") as f:
for line in f:
yield line
tok.train_from_iterator(corpus(), trainer=trainer)
tok.save("tokenizer.json")لاحظ أن تغيير vocab_size أو المُرمِّز المسبق أو قائمة الرموز الخاصة يغيّر كلا الجودة والسرعة. كل تغيير من هذا القبيل يبطل القياسات السابقة؛ أعد التشغيل بدلًا من الاستقراء.
قياس التوسّع: المحاور التي تتحرك فعليًا
أربعة محاور تستحق العزل في تشغيلات منفصلة.
طول التسلسل. بالنسبة لميزانية إجمالية ثابتة من الرموز، تتغير الإنتاجية بحسب كيفية توزيع تلك الرموز. مستند واحد من 8,000 رمز وثمانية مستندات من 1,000 رمز ليسا قابلين للتبادل، لأن كلفة الإعداد لكل مستند تُستهلك بشكل مختلف.
حجم الدفعة. يحسّن ترميز الدفعات عادة الإنتاجية إلى النقطة التي يتشبع فيها تجمّع الخيوط ويهيمن مرور الذاكرة. ارسم المنحنى؛ لا تفترض أن أكبر دفعة تفوز في المسارات الحساسة للزمن الكامن.
عدد الخيوط. هذا هو المحور الذي يُترك دون ضبط في أغلب الأحيان. إذا اختلف حجم تجمّع الخيوط بين تشغيلين، فأنت قد قست مُجدول جهازك، لا المُرمِّز.
مجال النص. النثر الإنجليزي النظيف هو أفضل حالة. الشيفرة المصدرية، وHTML بوسوم طويلة غير مغلقة، وCJK، والنص المثقل بالإيموجي، تضغط على أجزاء مختلفة من الخط — وبخاصة قواعد التقسيم لدى المُرمِّز المسبق. أبلغ عن أرقام لكل مجال، أو أبلغ عن المجال.
لكل تركيبة، سجّل الإنتاجية (رموز في الثانية وتسلسلات في الثانية) و توزيع الزمن الكامن. أبلغ عن p50 وp95. المتوسط يخفي الذيل الذي يحدد ما إذا كانت خدمتك تلبي اتفاقية مستوى الخدمة SLO.
أداة قياس قابلة لإعادة الإنتاج
تقيس الأداة التالية إنتاجية الترميز في الحالة المستقرة مع تسخين، وتشغيلات متكررة، وإبلاغ بالمئينات. إنها بسيطة عمدًا لتتمكن من تدقيقها بدلًا من الوثوق بها.
import statistics
import time
from tokenizers import Tokenizer
tok = Tokenizer.from_file("tokenizer.json")
# استبدل هذا بعيّنة واقعية من مدوّنتك.
texts = [open("sample.txt", encoding="utf-8").read()] * 512
# تسخين: يستبعد أخطاء الصفحات والتهيئة الكسولة من القياس.
for _ in range(3):
tok.encode_batch(texts)
durations = []
token_counts = []
for _ in range(20):
t0 = time.perf_counter()
encs = tok.encode_batch(texts)
elapsed = time.perf_counter() - t0
durations.append(elapsed)
token_counts.append(sum(len(e.ids) for e in encs))
durations.sort()
median = statistics.median(durations)
p95 = durations[int(0.95 * len(durations)) - 1]
tokens = statistics.mean(token_counts)
print(f"median batch time: {median:.4f} s")
print(f"p95 batch time: {p95:.4f} s")
print(f"tokens per batch: {tokens:.0f}")
print(f"tokens/second: {tokens / median:.0f}")كرّر هذا مع تغيير محور واحد في كل مرة. وعندما تقارن تشغيلين، أبقِ ملف المُرمِّز، والمفسّر، وإعدادات الخيوط، وعيّنة النص متطابقة على مستوى البايت؛ ولا تغيّر شيئًا غير المتغير قيد الدراسة.
يستحق توسيع واحد الجهد: أعد تشغيل الأداة نفسها داخل حلقة تقديم الخدمة الفعلية لديك، مع انشغال GPU. الترميز الذي يبدو مجانيًا في العزلة قد يبدو مكلفًا بمجرد أن ينافس على أنوية CPU نفسها التي تغذي المسرّع.
قراءة الأرقام دون خداع النفس
رقم الإنتاجية ادعاء عن إعداد. قبل أن تكرر واحدًا، اسأل:
- هل كان القياس في الحالة المستقرة أم عند البدء البارد؟ تحميل النموذج وتهيئة أول استدعاء ينتميان إلى رقم منفصل.
- هل كان الحشو والاقتطاع نشطين؟ الحشو ينفخ عدد الرموز وبالتالي البسط.
- ما كان مجال النص؟ رقم مشتق من نثر نظيف لن ينتقل إلى HTML مكشوط.
- هل كان تجمّع الخيوط مثبّتًا؟ إن لم يكن، فالنتيجة ليست قابلة للنقل إلى جهاز بعدد أنوية مختلف.
- هل الإحصاء المُبلَّغ عنه متوسط؟ إذن الذيل غير مقيس.
مساهمة المقال المصدر هي التأطير — الترميز وفك الترميز والتوسّع ثلاثة أسئلة مختلفة وتستحق ثلاثة قياسات مختلفة — بالإضافة إلى مجموعة قياسات أُخذت في ظروف مذكورة. استخدمه لمعايرة توقعاتك ولاختيار ما تقيسه. لا تستخدمه بديلًا عن قياس مدوّنتك أنت، لأن مدوّنتك هي التي ستخدمها خدمتك.
ما يمكن للمصدر أن يحسمه وما لا يمكنه
ما يمكنه دعمه: أن حدود الترميز/فك الترميز قابلة للقياس وتستحق القياس؛ وأن سلوك التوسّع يعتمد على أكثر من متغير إدخال واحد؛ وأن منهجية الإبلاغ لا تقل أهمية عن الرقم الرئيسي.
ما لا يمكنه دعمه: ضمان أن أي رقم محدد ينتقل إلى عتادك أو مفرداتك أو توزيع نصك. الأرقام الفردية تعتمد على الجهاز، وإعداد الخيوط، وملف المُرمِّز، والعيّنة. أي مقارنة تغيّر اثنين من هذه دفعة واحدة ليست مقارنة.
حدود مفتوحة. يتفاعل تصميم المفردات، واختيار المُرمِّز المسبق، ومجال المدوّنة بطرق لا يستطيع معيار أداء عام فصلها بالكامل. إذا كان حمل عملك يهيمن عليه مجال لا يغطيه معيار الأداء — الشيفرة، أو CJK، أو السجلات المهيكلة، أو المستندات القانونية الطويلة — فتعامل مع الأرقام المنشورة كفرضية أولية وأعد إنتاج التجربة محليًا.
الخاتمة
إن Tokenizers v1، كما أطّره منشور Hugging Face، يُقرأ على أفضل وجه كدعوة إلى القياس لا كمجموعة أرقام تُقتبس. سير العمل العملي قصير: ثبّت المكتبة في بيئة معزولة، وثبّت الإصدار، وحمّل أو درّب مُرمِّزًا، وتحقق من لامتغيّرية الترميز/فك الترميز، ثم غيّر محورًا واحدًا بالضبط في كل مرة — طول التسلسل، أو حجم الدفعة، أو عدد الخيوط، أو مجال النص — مع الإبلاغ عن توزيعات لا متوسطات.
ابدأ بالأداة أعلاه وبعيّنة من مدوّنتك الحقيقية. خمس عشرة دقيقة من القياس المنضبط ستخبرك عن كلفة تقديم الخدمة لديك أكثر من أي معيار أداء منشور، بما في ذلك هذا.



