هندسة الذكاء الاصطناعي التوليدي: كيف تعمل نماذج اللغة الكبيرة (LLMs) برمجياً؟
تتعامل شريحة واسعة من المطورين مع نماذج اللغة الكبيرة (LLMs) كصناديق سوداء تُستدعى عبر واجهات برمجة التطبيقات الجاهزة، مع افتراض وجود إدراك حقيقي للنصوص المدخلة. الواقع البرمجي أكثر تجريداً وحتمية: النموذج ليس سوى دالة رياضية عملاقة لإكمال الأنماط الإحصائية، تتلقى مصفوفات من الأرقام وتُرجع توزيعاً احتمالياً للرمز التالي عبر أوزان عائمة (Floating-point weights). يستعرض هذا المقال الهندسة الداخلية لتلك النماذج، متتبعاً دورة حياة البيانات من معالجة السلاسل النصية وحتى بناء حلقة استدلال برمجية كاملة باستخدام PyTorch.
دورة حياة النص: التحول من المحارف إلى مصفوفات التنسور (Tensors)
لا تفهم الشبكات العصبية الحروف أو الكلمات بصيغتها الأبجدية، بل تتطلب تحويل كل مدخل نصي إلى بنية رقمية منظمة تخضع لعمليات الجبر الخطي. تمر هذه العملية بثلاث محطات رئيسية متعاقبة داخل خط معالجة البيانات:
1. التجزئة والترميز (Tokenization)
الترميز هو الخوارزمية المسؤولة عن تفتيت النص الخام إلى وحدات أصغر تُعرف بالرموز (Tokens). تعتمد معظم النماذج الحديثة على خوارزميات مثل Byte-Pair Encoding (BPE). لا تتطابق هذه الرموز بالضرورة مع الكلمات المفردة؛ فالكلمة الشائعة قد تُمثل برمز واحد، في حين تُجزأ الكلمات النادرة أو المشتقة إلى مقاطع متعددة.
شهدت معمارية المرمزات قفزة نوعية في النماذج القياسية الحديثة؛ إذ انتقلت قواميس المفردات (Vocabulary Size) من 32,000 رمز في Llama-1 و50,257 في GPT-2، لتصل إلى 128,000 رمز في عائلة Llama-3 وتتجاوز 150,000 رمز في نماذج Qwen 2.5. ينعكس اتساع حجم القاموس إيجاباً على اللغات ذات الصرف المعقد كاللغة العربية، حيث يقلل من تجزئة الكلمة الواحدة إلى عدد مفرط من الرموز، مما يوفر استهلاك نافذة السياق (Context Window) ويسرع زمن الاستدلال.
2. طبقات التضمين (Embeddings Lookup)
بمجرد تحويل النص إلى قائمة من المعرفات الرقمية الصحيحة (Token IDs)، تُمرر هذه الأرقام إلى جدول التضمين (Embedding Matrix). هذا الجدول هو مصفوفة ضخمة ذات أبعاد [vocab_size, hidden_dim]. يُستبدل كل معرف برمز موجه مستمر متعدد الأبعاد (Dense Vector) يُعبر عن الدلالة الإحصائية للرمز في فضاء رياضي عالي الأبعاد، يتراوح غالباً بين 2048 و8192 بُعداً بحسب حجم النموذج.
3. تضمين الموضع الدوار (Rotary Positional Embeddings – RoPE)
نظراً لأن طبقات المحول (Transformers) تُعالج المدخلات بالتوازي دون ترتيب زمني متأصل، يحتاج النموذج إلى وسيلة لمعرفة ترتيب الكلمات داخل الجملة. اعتمدت النماذج القديمة على التضمين الموضعي الثابت، بينما تستخدم النماذج الحديثة تقنية RoPE. تعمل هذه التقنية على تدوير متجهات الاستعلام والمفتاح في الفضاء الهندسي بزوايا تتناسب مع موقع الرمز في النص، مما يمنح النموذج قدرة فائقة على قياس المسافات النسبية بين الرموز بدقة واستيعاب سياقات تتجاوز مئات الآلاف من الرموز دون فقدان الترابط.
قلب المعمارية: نمط فك التشفير والانتباه المُموّه (Decoder-Only Architecture)
تخلت جلّ النماذج التوليدية الرائدة اليوم (مثل Llama 3 وDeepSeek وGemma) عن المعمارية الأصلية ثنائية الشق (Encoder-Decoder) التي قُدمت عام 2017، ورسخت مكانة معمارية فك التشفير فقط (Decoder-Only). في هذه البيئة، تتدفق البيانات عبر طبقات متطابقة تعتمد كلياً على آلية الانتباه الذاتي السببي (Causal Self-Attention).
تعتمد آلية الانتباه على إسقاط متجهات التضمين خطياً إلى ثلاثة متجهات فرعية لكل رمز:
- الاستعلام (Query – Q): يُعبر عما يبحث عنه الرمز الحالي داخل النص.
- المفتاح (Key – K): يُعبر عما يحتويه الرمز لتقديمه للرموز الأخرى.
- القيمة (Value – V): المحتوى الدلالي الفعلي الذي سيُنقل إذا تطابق الاستعلام مع المفتاح.
تُحسب درجات الانتباه عبر ضرب مصفوفة الاستعلامات في منقول مصفوفة المفاتيح، ثم قسمة الناتج على الجذر التربيعي لأبعاد المفتاح وموازنته عبر دالة Softmax. النقطة المحورية هنا هي تطبيق “قناع السببية” (Causal Masking)؛ وهو مصفوفة مثلثة تضع قيماً سالبة لانهائية على مواضع الرموز المستقبلية، مما يجبر كل رمز أثناء خطوة الحساب على النظر فقط إلى الرموز التي سبقته، مانعاً تسريب المعلومات من الكلمات التي لم تُولد بعد.
ميكانيكية التوليد بالانحدار الذاتي وضبط الاحتمالات
تعمل نماذج اللغة برمجياً بنظام الانحدار الذاتي (Autoregressive Generation)؛ أي أن إخراج نص مكون من 50 رمزاً يتطلب تشغيل النموذج في حلقة تكرارية مغلقة 50 مرة، بمعدل رمز واحد في كل دورة تمرير أمامي (Forward Pass). في نهاية كل تمرير، تُخرج الطبقة الخطية الأخيرة قيماً غير معيارية تُسمى الـ Logits، يطابق حجمها حجم قاموس النموذج بالكامل.
لتحويل هذه القيم إلى اختيارات فعلية، تتدخل معايير التحكم بأخذ العينات (Sampling Parameters):
- درجة الحرارة (Temperature): معامل رياضي يُقسم عليه متجه الـ Logits قبل إدخاله في دالة Softmax. خفض القيمة نحو الصفر يجعل التوزيع حاداً ويجبر النموذج على اختيار الكلمة الأعلى احتمالاً بصورة شبه حتمية (Greedy Search)، في حين ترفع القيم الأعلى من 1 من احتمالية اختيار رموز نادرة عبر تسطيح المنحنى الاحتمالي.
- Top-k Sampling: استراتيجية برمجية تحصر الاختيار في أعلى
kمن الرموز احتمالاً وتستبعد باقي القاموس تماماً، مما يقلل من فرص إنتاج نصوص غير مترابطة. - Top-p (Nucleus) Sampling: آلية ديناميكية تجمع الرموز الأعلى احتمالاً حتى يصل مجموع احتمالاتها التراكمي إلى النسبة المحددة
p(مثل 0.9)، مما يتيح توسيع خيارات النموذج عندما يكون السياق يحتمل مرادفات عدة، وتضييقها عندما يكون التوقع شبه مؤكد.
بناء حلقة الاستدلال التوليدي يدوياً باستخدام PyTorch
لتجريد العملية من أي وسائط برمجية جاهزة، يوضح الكود التالي كيفية تنفيذ حلقة التوليد التلقائي بالكامل من نقطة الصفر البرمجية، متضمناً استخراج الـ Logits، وتطبيق معامل الحرارة، وسحب العينات، وتحديث السياق بدون استخدام الدالة المؤتمتة model.generate().
import torch
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "Qwen/Qwen2.5-0.5B"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id)
model.eval()
prompt = "Generative AI engineering relies on"
input_ids = tokenizer(prompt, return_tensors="pt")["input_ids"]
max_new_tokens = 10
temperature = 0.8
with torch.no_grad():
for step in range(max_new_tokens):
outputs = model(input_ids)
# Extract logits of the last token: shape [batch_size, vocab_size]
next_token_logits = outputs.logits[:, -1, :]
# Apply temperature scaling
scaled_logits = next_token_logits / temperature
# Convert logits to probability distribution
probabilities = F.softmax(scaled_logits, dim=-1)
# Sample next token from the multinomial distribution
next_token_id = torch.multinomial(probabilities, num_samples=1)
# Break loop if End-Of-Sequence token is encountered
if next_token_id.item() == tokenizer.eos_token_id:
break
# Append predicted token to inputs for the subsequent forward pass
input_ids = torch.cat([input_ids, next_token_id], dim=-1)
token_str = tokenizer.decode(next_token_id[0], skip_special_tokens=True)
print(f"Step {step + 1}: Generated ID {next_token_id.item()} -> '{token_str}'")
full_generated_text = tokenizer.decode(input_ids[0], skip_special_tokens=True)
print("\nFinal Output:\n", full_generated_text)
يشرح التسلسل التالي ما حدث داخل الكود خطوة بخطوة:
- تحميل النموذج ووضعه في نمط الاستدلال: استدعينا نموذج
Qwen2.5-0.5Bومرمزه، ثم ضبطنا النموذج باستخدامmodel.eval()لتعطيل طبقات الإسقاط العشوائي (Dropout) لضمان اتساق العمليات الحسابية. - استخراج مخرجات الخطوة الأخيرة: أبعاد مصفوفة
outputs.logitsهي[1, sequence_length, vocab_size]. نحن نهتم فقط بالرمز الأخير الذي يمثل التوقع المستقبلي، لذا قمنا بقصه عبر[:, -1, :]. - موازنة وتوزيع الاحتمالات: قسّمنا القيم على
temperature، ثم مررنا النتيجة إلى دالةF.softmaxعلى البُعد الأخير ليصبح مجموع احتمالات المفردات مساوياً للرقم 1 تماماً. - أخذ العينات الإحصائية: استخدمنا
torch.multinomialلسحب معرف الرمز عشوائياً وفقاً للوزن الاحتمالي، بدلاً من أخذ القيمة القصوى فقط، مما يمنح المخرجات تنوعاً طبيعياً. - توسيع السياق: دمجنا الرمز الناتج مع مصفوفة المدخلات الأصلية عبر
torch.catعلى البُعد-1لتُعاد تغذية النموذج بها في الخطوة التالية.
كفاءة الاستدلال وإدارة الذاكرة عبر تقنية KV Caching
يعاني الكود السابق من قصور هندسي بنيوي عند التوسع؛ ففي كل دورة داخل حلقة for، نقوم بإعادة تمرير كامل النص السابق لحساب الانتباه، مما يجعل التعقيد الحسابي ينمو تربيعياً بمعدل $O(N^2)$ مع تزايد طول النص. هذا يعني إهداراً كبيراً لقوة المعالجة في البطاقات الرسومية بحساب قيم المفاتيح والقيم (Keys and Values) للرموز القديمة التي حُسبت بالفعل في الدورات السابقة.
يتمثل الحل الهندسي القياسي المتبع في الإنتاج في استخدام KV Caching. تقوم هذه الآلية بحفظ مصفوفات $K$ و $V$ المحسوبة للرموز السابقة داخل ذاكرة البطاقة الرسومية (VRAM)، وتمرير الرمز الجديد فقط في كل خطوة تالية لحساب متجهات $Q$ الخاصة به ومقارنتها مع الذاكرة المخبأة. تُخفض هذه الخطوة التعقيد الحسابي لكل رمز جديد إلى $O(1)$ من حيث عمليات المعالجة اللحظية، مقابل استهلاك أكبر للذاكرة، وهو ما تعالجه المحركات الإنتاجية الحديثة (مثل vLLM) عبر تقنيات تقسيم الذاكرة الافتراضية مثل PagedAttention لمنع الهدر والتجزؤ.
الأخطاء الشائعة للمطورين عند بناء وتطويع نماذج LLM
يقع العديد من المهندسين في أخطاء جوهرية أثناء الانتقال من استخدام مكتبات التوليد السطحية إلى التعامل البرمجي المباشر مع النماذج:
- التعامل مع النموذج كقاعدة بيانات للمعرفة الحتمية: يفترض البعض أن النموذج يخزن الحقائق في جداول منظمة داخل بنيته. النموذج في واقعه هو محرك إحصائي لربط الأنماط؛ لذا فإن إجباره على استرجاع تفاصيل دقيقة دون تزويده بسياق خارجي يؤدي حتماً إلى الهلوسة. الحل: فصل مهام التفكير المنطقي عن تخزين البيانات عبر اعتماد معمارية الاسترجاع المعزز بالتوليد (RAG).
- تجاهل ضريبة الترميز (Tokenization Penalty) في المحتوى العربي: استخدام نماذج قديمة بقواميس محدودة يؤدي إلى تفتيت الكلمة العربية الواحدة إلى 4 أو 6 رموز، مما يستهلك نافذة السياق سريعاً ويضاعف تكلفة المعالجة ثلاث إلى أربع مرات مقارنة بالإنجليزية. الحل: فحص المرمز برمجياً واختيار نماذج حديثة تدعم العربية بقواميس تتجاوز 100 ألف رمز لتقليل معدل الرموز لكل كلمة.
- الخلط بين مفهوم درجة الحرارة ومستوى “ذكاء” النموذج: يعتقد البعض أن رفع الـ Temperature يمنح النموذج قدرات تفكير إبداعية فائقة. الحقيقة البرمجية أنها مجرد مقياس رياضي لتسطيح التوزيع الاحتمالي؛ ورفعها بشكل مفرط يجبر النموذج على اختيار كلمات ذات احتمال إحصائي شبه معدوم، مما ينتج تخريفاً لغوياً. الحل: تثبيت المعامل بين 0.0 و 0.2 في مهام البرمجة واستخراج البيانات لضمان الدقة، وحصره بين 0.7 و 0.9 للمهام التي تتطلب مرونة إنشائية.
- استدعاء التوليد داخل حلقات دون إيقاف تتبع الاشتقاقات (Gradients): تشغيل الاستدلال دون إحاطة الكود بـ
torch.no_grad()يؤدي إلى قيام بايثون ببناء شجرة الاشتقاقات العكسية (Computation Graph) لكل عملية ضرب مصفوفات، مما يتسبب في امتلاء ذاكرة الـ GPU ببيانات غير مطلوبة وانهيار التطبيق بخطأ نفاد الذاكرة (Out of Memory). الحل: تفعيل سياقwith torch.no_grad():دائماً خلال مراحل الاستدلال.
متى تتعامل مع النموذج على مستوى المصفوفات ومتى تكتفي بواجهات الـ API؟
لا تتطلب كافة الأنظمة البرمجية هذا المستوى المتعمق من التحكم بالمصفوفات والأوزان. إذا كان هدف نظامك هو بناء تطبيق محادثة داخلي، أو تلخيص وثائق، أو تصنيف رسائل البريد الإلكتروني، فإن الاعتماد على واجهات برمجة التطبيقات السحابية أو محركات الاستدلال المدارة (مثل vLLM وOllama) هو الخيار الهندسي الأكثر كفاءة وأقلها تكلفة تشغيلية وصيانة. في المقابل، يصبح فهم الهندسة الداخلية والتعامل المباشر مع مصفوفات PyTorch وطبقات الانتباه أمراً لا مفر منه إذا كنت تعمل على تحسين زمن الاستجابة إلى مستويات متدنية جداً (Ultra-low latency)، أو تصميم خوارزميات أخذ عينات مخصصة (Custom Decoding Strategies)، أو تطبيق تقنيات الضغط والتقليم والكمكمة (Quantization)، أو بناء بنية تحتية خاصة لمعالجة البيانات الحساسة على خوادم محلية مقيدة بالموارد.
