اذهب إلى المحتوى

Ola Abbas

الأعضاء
  • المساهمات

    189
  • تاريخ الانضمام

  • تاريخ آخر زيارة

آخر الزوار

6680 زيارة للملف الشخصي

إنجازات Ola Abbas

عضو نشيط

عضو نشيط (3/3)

34

السمعة بالموقع

  1. سنشرح في هذا المقال آلية معالجة العقد ضمن شجرة المشاهد والترتيب الذي يتبعه محرك الألعاب جودو Godot للتعامل معها، كما سنوضح ما هي مسارات العقد وكيفية التنقل بينها، سيساعدنا ذلك على فهم طريقة تنظيم لعبتنا، والتحكم بها بفعالية أكبر. ترتيب معالجة العقد في شجرة المشاهد يتضمن محرك ألعاب جودو مفهوم يسمى شجرة المشاهد Scene Tree تتكون هذه الشجرة من عدة عقد Nodes، تمثل كل عقدة جزءًا من مشهد اللعبة، ويُذكَر مصطلح ترتيب الشجرة Tree Order في توثيق جودو، ولكنه غير واضح بالنسبة للمبتدئين، حيث يكون هذا الترتيب من أعلى الشجرة لأسفلها بدءًا من الجذر نزولًا إلى كل فرع بدوره، أي يبدأ الترتيب من العقد الرئيسية ثم يتنقل عبر الفروع وصولًا للعقد الفرعية، وهذا الترتيب مهم لأن كل عقدة تؤثر في العقد التي تحتها، لاحظ هذا الترتيب في الشكل التالي: سنرفق هذا الكود بكل عقدة: extends Node func _init(): # ملاحظة: العقدة ليس لها اسم بعد هنا print("TestRoot init") func _enter_tree(): print(name + " enter tree") func _ready(): print(name + " ready") # ‫يضمن ما يلي أننا نطبع مرة واحدة فقط في process()‎ var test = true func _process(delta): if test: print(name + " process") test = false يوضح الكود أعلاه كيفية تفاعل العقدة مع الأحداث المختلفة في شجرة المشاهد، ولنوضح ما يمثله استدعاء كل دالة من الدوال الواردة فيه قبل أن نتحدث عن النتائج: تستدعى الدالة ‎_init()‎ عند إنشاء العقدة أو الكائن لأول مرة، وتكون العقدة حينها موجودة في ذاكرة الحاسوب ولم تُضف لشجرة المشاهد تستدعى الدالة ‎_enter_tree()‎ عند إضافة العقدة للشجرة لأول مرة، ويمكن أن يحدث ذلك عند إنشاء نسخة من العقدة أو عند إنشاء عقدة ابن من عقدة ما باستخدام التابع add_child()‎ مثلًا تستدعى الدالة ‎_ready()‎ عند اكتمال إضافة العقدة وأبنائها بنجاح لشجرة المشاهد وجاهزيتها للعمل تستدعى الدالة ‎_process()‎ بشكل دوري في كل إطار -60 مرة في الثانية عادةً- وذلك لكل عقدة في الشجرة ويمكن استخدامها للتعامل مع التحديثات المتكررة إذا شغّلنا الكود على عقدة واحدة بمفردها، فسيكون ترتيب استدعاء الدوال كالمتوقع وفق ما يلي: TestRoot init TestRoot enter tree TestRoot ready TestRoot process لكن إذا أضفنا عقد أبناء، فسيصبح الأمر أكثر تعقيدًا وقد يحتاج إلى بعض التوضيح: TestRoot init TestChild1 init TestChild3 init TestChild2 init TestRoot enter tree TestChild1 enter tree TestChild3 enter tree TestChild2 enter tree TestChild3 ready TestChild1 ready TestChild2 ready TestRoot ready TestRoot process TestChild1 process TestChild3 process TestChild2 process طبعت جميع هذه العقد رسائلها بترتيب الشجرة من الأعلى إلى الأسفل باستثناء الشيفرة البرمجية الخاصة بالتابع ‎_ready()‎ والذي يُستدعَى عندما تكون العقدة جاهزة، أي عندما تدخل العقدة وأبناؤها شجرة المشاهد، فإذا كان للعقدة أبناء، فستُشغَّل استدعاءات التابع ‎_ready الخاصة بالأبناء أولًا، وستتلقى العقدة الأب إشعار الجاهزية بعد ذلك كما يوضح توثيق جودو الرسمي. يقودنا ذلك إلى قاعدة أساسية مهمة يجب تذكرها عند إعداد بنية العقد وهي: يجب أن تدير العقد الأب أبناءها وليس العكس، ويجب أن تكون أيّ شيفرة برمجية للعقدة الأب قادرةً على الوصول الكامل إلى بيانات أبنائها، لذا يجب معالجة استدعاءات التابع ‎_ready()‎ بترتيب الشجرة العكسي. علينا تذكّر ذلك عند محاولة الوصول لعقد أخرى في التابع ‎_ready()‎، فإذا كنا بحاجة للانتقال لأعلى الشجرة إلى عقدة أب أو عقدة جَد، فيجب تشغيل هذه الشيفرة البرمجية في العقدة الأب وليس في العقدة الابن. فهم مسارات العقد والتنقل في شجرة المشاهد تُستخدم مسارات العقد Node Paths في جودو لفهم كيفية التنقل بين العقد في شجرة المشاهد Scene Tree. وهذه المسارات أساسية لفهم كيفية الوصول للعقد المختلفة داخل الشجرة وتجنب مشكلة وجود مرجع عقدة غير صالح، والتي تظهر على هيئة رسالة خطأ كالتالي: Invalid get index ‘position’ (on base: ’null instance’). يُعَد الجزء الأخير من رسالة الخطأ null instance مصدر هذه المشكلة، وهو يسبب إرباكًا للمبتدئين في جودو، ويمكن تجنّب هذه المشكلة من خلال فهم مفهوم مسارات العقد. مسارات العقد تتكون شجرة المشهد من عقد ترتبط ببعضها البعض بعلاقات أب-ابن، ومسار العقد هو المسار المُتّخَذ للانتقال من عقدة إلى أخرى من خلال التحرّك عبر هذه الشجرة. لنأخذ مثلًا مشهد لاعب بسيط كما يلي: يوجد كود هذا المشهد في العقدة Player. إذا كان السكربت بحاجة إلى استدعاء الدالة play()‎ مع العقدة AnimatedSprite، فسيحتاج إلى مرجع إلى تلك العقدة: get_node("AnimatedSprite").play() إن وسيط الدالة get_node()‎ هو سلسلة نصية تمثّل المسار إلى العقدة المطلوبة، وتكون هذه السلسلة النصية في حالتنا هي ابن العقدة التي يوجد ضمنها الكود. إذا كان المسار المُقدّم لها غير صالح، فسنحصل على خطأ null instance وخطأ عدم العثور على العقدة Node not found أيضًا. يُعَد الحصول على مرجع عقدة باستخدام الدالة get_node()‎ حالة شائعة لدرجة أن لغة GDScript لديها اختصار له حيث يمكنك كتابة $ للوصول إلى العقدة مباشرة بدلًا من استدعاء الدالة، على سبيل المثال للوصول إلى العقدة AnimatedSprite‎ وتشغيل الدالة ()play عليها مباشرة نكتب: $AnimatedSprite.play() ملاحظة: تعيد الدالة get_node()‎ مرجعًا Reference إلى العقدة المطلوبة. لنأخذ الآن مثالًا لشجرة مشهد أكثر تعقيدًا كما يلي: إذا احتاج الكود المرفق بالعقدة Main إلى الوصول إلى العقدة ScoreLabel، فيمكنه ذلك باستخدام هذا المسار: get_node("HUD/ScoreLabel").text = "0" # ‫أو باستخدام الاختصار: $HUD/ScoreLabel.text = "0" ملاحظة: سيكمل محرر جودو المسارات تلقائيًا نيابةً عنا عند استخدام صيغة $، ويمكننا أيضًا النقر بزر الفأرة الأيمن على عقدة ما في تبويب المشهد Scene واختيار نسخ مسار العقدة Copy Node Path. إذا كانت العقدة التي نريد الوصول إليها موجودة في مكان أعلى من الشجرة، فيمكننا استخدام الدالة get_parent()‎ أو ".." للإشارة إلى العقدة الأب، حيث يمكننا الحصول على العقدة Player من العقدة ScoreLabel في شجرة المثال السابق كما يلي: get_node("../../Player") يمثّل المسار ‎"../../Player"‎ الحصول على العقدة التي تقع في مستوى واحد أعلى HUD ثم العقدة التي تقع في مستوى أعلى وهي Main ثم الوصول إلى العقدة الابن لها وهي Player. ملاحظة: تعمل مسارات العقد مثل مسارات المجلدات في نظام التشغيل، حيث تشير الشرطة المائلة / إلى علاقة أب-ابن، وتعني .. مستوى واحد أعلى. المسارات النسبية Relative والمسارات المطلقة Absolute تستخدم جميع الأمثلة السابقة مسارات نسبية، لأنها تبدأ من العقدة الحالية وتتبع المسار إلى الوجهة، ولكن يمكن أن تكون مسارات العقد مطلقة أيضًا بحيث تبدأ من العقدة الجذر للمشهد، فمثلًا يكون المسار المطلق إلى عقدة اللاعب هو: get_node("/root/Main/Player") لا يعيد المسار ‎/root والذي يمكن الوصول إليها أيضًا باستخدام get_tree().root العقدة الجذر لمشهدنا الحالي، ولكنه يعيد العقدة الجذر لنافذة العرض Viewport التي توجد دائمًا في شجرة المشهد SceneTree افتراضيًا. مشكلة في التعامل مع مسارات العقد في جودو تعمل الأمثلة السابقة بنجاح، ولكن توجد بعض الأشياء التي يجب أن نكون على دراية بها والتي قد تسبب مشكلات لاحقًا. لنفترض أن لدينا الحالة التالية: تحتوي العقدة Player على الخاصية health التي نريد عرضها في العقدة HealthBar في مكان ما في واجهة المستخدم الخاصة بنا، لذا يمكن كتابة شيء يشبه ما يلي في كود اللاعب: func take_damage(amount): health -= amount get_node("../Main/UI/HealthBar").text = str(health) قد يكون هذا السكربت جيدًا في البداية، ولكن يمكن أن يواجه خطأ بسهولة، إذ توجد مشكلتان رئيسيتان في هذا النوع من الترتيب وهما: لا يمكننا اختبار مشهد اللاعب بصورة مستقلة، فإذا شغلنا مشهد اللاعب بمفرده أو في مشهد اختبار دون واجهة مستخدم، فسيسبّب سطر الدالة get_node()‎ في حدوث عطل لا يمكننا تغيير واجهة المستخدم الخاصة بنا، فإذا قررنا إعادة ترتيبها أو تصميمها، فلن يكون المسار صالحًا بعد الآن ويجب تغييره لذا علينا تجنب استخدام مسارات العقد التي تنتقل إلى الأعلى في شجرة المشهد. إذا أصدر اللاعب في المثال السابق إشارة عند تغير مستوى صحته health، فيمكن لواجهة المستخدم الاستماع إلى هذه الإشارة لتحديث نفسها، ثم يمكننا إعادة ترتيب العقد والفصل بينها دون الخوف من توقف اللعبة. الخاتمة نأمل أن يكون هذا المقال قد ساعدكم على تكوين فكرة واضحة حول استخدام مسارات العقد في جودو، والطريقة الصحيحة للتنقل بين العقد والاتصال بالعناصر التي تحتاجوها في شجرة المشاهد. ففهم مسارات العقد هو الأساس الذي يمكننا البناء عليه لتفادي العديد من الأخطاء الشائعة ورسائل الخطأ مثل null instance والإشارة لأي عقدة نحتاجها بالطريقة الصحيحة. ترجمة -وبتصرّف- للقسمين Understanding tree order و Understanding node paths من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: إنشاء شخصيات ثلاثية الأبعاد في جودو Godot كتابة سكربتات GDScript وإرفاقها بالعقد في جودو استخدام الإشارات Signals في جودو Godot لغات البرمجة المتاحة في جودو Godot
  2. تنشر العديد من قواعد البيانات المعلومات على جداول مختلفة بحسب معناها وسياقها، وبالتالي قد نحتاج إلى الإشارة إلى أكثر من جدول واحد عند استرجاع معلومات حول البيانات الموجودة في قاعدة البيانات، لذا توفّر لغة الاستعلام البنيوية Structured Query Language -أو SQL اختصارًا- طرقًا متعددة لاسترجاع البيانات من جداول مختلفة مثل عمليات المجموعة Set Operations، وخاصةً معامل المجموعة UNION الذي تدعمه معظم أنظمة قواعد البيانات العلاقية Relational Database Systems والذي يأخذ نتائج استعلامين لأعمدة متطابقة ويدمجهما في استعلام واحد. سنشرح في هذا المقال طريقة استخدام المعامل UNION لاسترجاع ودمج البيانات من أكثر من جدول، ونتعلم كيفية دمجه مع عملية الترشيح Filtering لترتيب النتائج. مستلزمات العمل يجب أن يكون لديك حاسوب يشغّل نظام إدارة قواعد البيانات العلاقية Relational Database Management System -أو RDBMS اختصارًا- المستند إلى لغة SQL. اختبرنا التعليمات والأمثلة الواردة في هذا المقال باستخدام البيئة التالية: خادم عامل على توزيعة أوبنتو Ubuntu مع مستخدم بصلاحيات مسؤول مختلف عن المستخدم الجذر وجدار حماية مضبوط باستخدام أداة UFW كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو، ومقال كيفية تثبيت توزيعة أوبنتو من لينكس بأبسط طريقة نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضّح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا خطوات هذا المقال باستخدام مستخدم MySQL مختلف عن المستخدم الجذر وفق الطريقة الموضحة في الفقرة التالية من المقال. معرفة أساسية بتنفيذ استعلامات SELECT لاختيار البيانات من قاعدة البيانات كما هو موضّح في مقال كيفية الاستعلام عن السجلات من الجداول في SQL ملاحظة: تجدر الإشارة إلى أنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، إذ ستعمل الأوامر المُقدّمة في هذا المقال بنجاح مع معظم هذه الأنظمة وهي جزء من صيغة لغة SQL المعيارية، ولكن قد تجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. سنحتاج أيضًا إلى قاعدة بيانات تحتوي على بعض الجداول المُحمَّلة ببيانات تجريبية نموذجية لنتمكّن من التدرب على استخدام عمليات UNION. وسنوضح في القسم التالي تفاصيل حول الاتصال بخادم MySQL وإنشاء قاعدة بيانات تجريبية، والتي سنستخدمها في أمثلة هذا المقال. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية سنتصل في هذا القسم بخادم MySQL وسننشئ قاعدة بيانات تجريبية لنتمكّن من اتباع الأمثلة الواردة في هذا المقال. إذا كانت قاعدة بيانات SQL الخاصة بنا تعمل على خادم بعيد، علينا الاتصال بالخادم مُستخدمين بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بنا مكان user: $ mysql -u user -p ننشئ قاعدة بيانات بالاسم bookstore: mysql> CREATE DATABASE bookstore; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر الخرج التالي: الخرج Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات bookstore من خلال تنفيذ تعليمة USE التالية: mysql> USE bookstore; وسيظهر الخرج التالي: الخرج Database changed اخترنا قاعدة البيانات، وسننشئ عدة جداول تجريبية ضمنها، حيث سنستخدم في هذا المقال مكتبة افتراضية تقدّم خدمة شراء الكتب وتأجيرها، وتُدار كل خدمة من هاتين الخدمتين على حدة، وبالتالي ستُخزَّن البيانات المتعلقة بشراء الكتب وتأجيرها في جداول منفصلة. ملاحظة: بسّطنا مخطط قاعدة البيانات لهذا المثال للتوضيح، إذ تكون هياكل الجداول أكثر تعقيدًا في الحياة الواقعية مع تضمين مفاتيح رئيسية Primary Keys ومفاتيح خارجية Foreign Keys ضمنها. ويمكن مطالعة مقال فهم قواعد البيانات العلاقية لمزيد من المعلومات حول كيفية تنظيم البيانات. يحتوي الجدول الأول book_purchases على بيانات حول الكتب المُشتراة والعملاء الذين أجروا عمليات الشراء، إذ سيحتوي على أربعة أعمدة هي: purchase_id: يحتوي هذا العمود على معرّف عملية الشراء ونمثّله بنوع البيانات int، وسيكون هذا العمود هو المفتاح الرئيسي للجدول، حيث تصبح كل قيمة معرّفًا فريدًا للصف الخاص بها customer_name: يحتوي هذا العمود على اسم العميل، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 30 محرفًا book_title: يحتوي هذا العمود على عنوان الكتاب الذي جرى شراؤه، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 200 محرف date: يستخدم هذا العمود نوع البيانات date، ويحتوي على تاريخ كل عملية شراء ننشئ هذا الجدول التجريبي باستخدام الأمر التالي: mysql> CREATE TABLE book_purchases ( mysql> purchase_id int, mysql> customer_name varchar(30), mysql> book_title varchar(40), mysql> date date, mysql> PRIMARY KEY (purchase_id) mysql> ); إذا كان الخرج كما يلي، فهذا يعني إنشاء الجدول الأول بنجاح: الخرج Query OK, 0 rows affected (0.00 sec) يحزّن الجدول الثاني book_leases معلومات حول الكتب المُستعارة، وتكون بنيته مشابهة للجدول السابق، ولكن نميّز عملية تأجير الكتاب بتاريخين مختلفين هما تاريخ التأجير ومدته، وبالتالي سيحتوي هذا الجدول على خمسة أعمدة هي: lease_id: يحتوي هذا العمود على معرّف عملية التأجير، ونمثّله بنوع البيانات int، ويكون هذا العمود هو المفتاح الرئيسي للجدول، حيث تصبح كل قيمة معرّفًا فريدًا للصف الخاص بها customer_name: يحتوي هذا العمود على اسم العميل، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 30 محرفًا book_title: يحتوي هذا العمود على عنوان الكتاب المُستعار، ونمثّله باستخدام نوع البيانات varchar بحد أقصى 200 محرف date_from: يستخدم نوع البيانات date، ويحتوي هذا العمود على تاريخ بدء عملية التأجير date_to: يستخدم نوع البيانات date، ويحتوي هذا العمود على تاريخ انتهاء عملية التأجير لننشئ الجدول الثاني باستخدام الأمر التالي: mysql> CREATE TABLE book_leases ( mysql> lease_id int, mysql> customer_name varchar(30), mysql> book_title varchar(40), mysql> date_from date, mysql> date_to date, mysql> PRIMARY KEY (lease_id) ); يؤكّد الخرج التالي إنشاء الجدول الثاني: الخرج Query OK, 0 rows affected (0.00 sec) لنحمّل الآن الجدول book_purchases ببعض البيانات التجريبية من خلال تشغيل عملية INSERT INTO التالية: mysql> INSERT INTO book_purchases mysql> VALUES mysql> (1, 'sammy', 'The Picture of Dorian Gray', '2023-10-01'), mysql> (2, 'sammy', 'Pride and Prejudice', '2023-10-04'), mysql> (3, 'sammy', 'The Time Machine', '2023-09-23'), mysql> (4, 'bill', 'Frankenstein', '2023-07-23'), mysql> (5, 'bill', 'The Adventures of Huckleberry Finn', '2023-10-01'), mysql> (6, 'walt', 'The Picture of Dorian Gray', '2023-04-15'), mysql> (7, 'walt', 'Frankenstein', '2023-10-13'), mysql> (8, 'walt', 'Pride and Prejudice', '2023-10-19'); ستضيف عملية INSERT INTO السابقة 8 عمليات شراء مع قيمها المحدَّدة إلى جدول book_purchases، حيث يشير الخرج التالي إلى إضافة جميع الصفوف الثمانية: الخرج Query OK, 8 rows affected (0.00 sec) Records: 8 Duplicates: 0 Warnings: 0 ثم ندخِل بعض البيانات التجريبية في الجدول book_leases كما يلي: mysql> INSERT INTO book_leases mysql> VALUES mysql> (1, 'sammy', 'Frankenstein', '2023-09-14', '2023-11-14'), mysql> (2, 'sammy', 'Pride and Prejudice', '2023-10-01', '2023-12-31'), mysql> (3, 'sammy', 'The Adventures of Huckleberry Finn', '2023-10-01', '2023-12-01'), mysql> (4, 'bill', 'The Picture of Dorian Gray', '2023-09-03', '2023-09-18'), mysql> (5, 'bill', 'Crime and Punishment', '2023-09-27', '2023-12-05'), mysql> (6, 'kim', 'The Picture of Dorian Gray', '2023-10-01', '2023-11-15'), mysql> (7, 'kim', 'Pride and Prejudice', '2023-09-08', '2023-11-17'), mysql> (8, 'kim', 'The Time Machine', '2023-09-04', '2023-10-23'); وسيظهر الخرج التالي الذي يؤكّد إضافة هذه البيانات: الخرج Query OK, 8 rows affected (0.00 sec) Records: 8 Duplicates: 0 Warnings: 0 تتعلق عمليات التأجير والشراء بعملاء وعناوين كتب متماثلة، مما سيكون مفيدًا لتوضيح سلوك المعامل UNION، وبذلك سنكون جاهزين لمتابعة هذا المقال والبدء في استخدام عمليات UNION في لغة SQL. فهم صيغة المعامل UNION يطلب المعامل UNION في لغة SQL من قاعدة البيانات أخذ مجموعتي نتائج منفصلتين ومسترجعتين من استعلامات SELECT فردية ودمجهما في مجموعة نتائج واحدة تحتوي على صفوف مُعادة من كلا الاستعلامين. ملاحظة: يمكن أن تكون استعلامات SELECT المستخدمة مع العملية UNION معقدة وتتطلب استخدام تعليمات JOIN أو دوال تجميعية Aggregations أو استعلامات الفرعية Subqueries. إذ يمكننا استخدام عمليات UNION لدمج نتائج الاستعلامات مهما كانت معقدة، ولكن للسهولة سنستخدم في أمثلتنا التالية استعلامات SELECT بسيطة للتركيز على كيفية تصرف المعامل UNION. يوضّح المثال التالي الصيغة العامة لتعليمة SQL التي تتضمن المعامل UNION: mysql> SELECT column1, column2 FROM table1 mysql> UNION mysql> SELECT column1, column2 FROM table2; تبدأ تعليمة SQL السابقة بتعليمة SELECT التي تعيد عمودين من الجدول table1، ونتبعها بالمعامل UNION وتعليمة SELECT ثانية، ويعيد استعلام SELECT الثاني أيضًا عمودين من الجدول table2. تخبر الكلمة المفتاحية UNION قاعدة البيانات بأخذ الاستعلامات السابقة واللاحقة، وتنفيذها تنفيذًا منفصلًا، ثم ضم مجموعات النتائج الخاصة بها ضمن مجموعة واحدة. يُعَد جزء الشيفرة البرمجية السابق بأكمله -بما في ذلك استعلامات SELECT والكلمة المفتاحية UNION بينهما- تعليمة SQL واحدة، لذلك لا ينتهي استعلام SELECT الأول بفاصلة منقوطة، والتي تظهر بعد اكتمال التعليمة فقط. لنفترض مثلًا أننا نريد إدخال جميع العملاء الذين اشتروا كتابًا أو استأجروه، حيث يحتفظ الجدول book_purchases بسجلات عمليات الشراء، بينما يخزّن الجدول book_leases عمليات تأجير الكتب، لذا لنشغّل الاستعلام التالي: mysql> SELECT customer_name FROM book_purchases mysql> UNION mysql> SELECT customer_name FROM book_leases; وتكون مجموعة نتائج هذا الاستعلام كما يلي: الخرج +---------------+ | customer_name | +---------------+ | sammy | | bill | | walt | | kim | +---------------+ 4 rows in set (0.000 sec) يشير هذا الخرج إلى أن Sammy و Bill و Walt و Kim اشتروا كتبًا أو استأجروها. دعونا نجرّب تنفيذ تعليمتي SELECT تنفيذًا منفصلًا، مرةً لعمليات الشراء ومرةً لعمليات التأجير لفهم كيفية إنشاء مجموعة هذه النتائج. لنشغّل أولًا الاستعلام التالي لإعادة العملاء الذين اشتروا كتبًا فقط: mysql> SELECT customer_name FROM book_purchases; وسيكون الخرج كما يلي: الخرج +---------------+ | customer_name | +---------------+ | sammy | | sammy | | sammy | | bill | | bill | | walt | | walt | | walt | +---------------+ 8 rows in set (0.000 sec) اشترى Sammy و Bill و Walt كتبًا، لكن Kim لم يفعل ذلك. ثم لنشغّل الاستعلام التالي لإعادة العملاء الذين استأجروا كتبًا فقط: mysql> SELECT customer_name FROM book_leases; وسيكون الخرج كما يلي: الخرج +---------------+ | customer_name | +---------------+ | sammy | | sammy | | sammy | | bill | | bill | | kim | | kim | | kim | +---------------+ 8 rows in set (0.000 sec) يشير الجدول book_leases إلى أن Sammy و Bill و Kim يستعيرون كتبًا، ولكن Walt لا يستعير كتبًا أبدًا. إذا جمعنا بين الإجابتين، فيمكننا الحصول على البيانات لكل من عمليات الشراء والتأجير. الفرق المهم بين استخدام عملية UNION وتنفيذ استعلامين تنفيذًا منفصلًا هو أن عملية UNION تزيل القيم المكررة، حيث تُدمَج النتائج دون تكرار أسماء العملاء في النتيجة. يجب أن يعيد كلا الاستعلامين النتائج بالتنسيق نفسه لاستخدام عملية UNION لدمج نتائج استعلامين منفصلين بطريقة صحيحة، إذ ستؤدي التعارضات إلى حدوث أخطاء في محرّك قاعدة البيانات أو إلى ظهور نتائج لا تتطابق مع الهدف من الاستعلام كما سنوضّح في المثالين التاليين. تنفيذ عملية UNION مع عدد غير متطابق من الأعمدة لنجرّب تنفيذ عملية UNION مع تعليمة SELECT تعيد عمودًا واحدًا وتعليمة SELECT أخرى تعيد عمودين: mysql> SELECT purchase_id, customer_name FROM book_purchases mysql> UNION mysql> SELECT customer_name FROM book_leases; سيستجيب خادم قاعدة البيانات بالخطأ التالي: الخرج The used SELECT statements have a different number of columns لا يمكن إجراء عمليات UNION على مجموعات النتائج التي لها أعداد أعمدة مختلفة. تنفيذ عملية UNION مع ترتيب غير متطابق للأعمدة لنجرّب الآن تنفيذ عملية UNION مع تعلميتي SELECT تعيدان القيم نفسها ولكن بترتيب مختلف كما يلي: mysql> SELECT customer_name, book_title FROM book_purchases mysql> UNION mysql> SELECT book_title, customer_name FROM book_leases; لن يعيد خادم قاعدة البيانات خطأ، ولكن لن تكون مجموعة النتائج صحيحة كما يلي: الخرج +------------------------------------+------------------------------------+ | customer_name | book_title | +------------------------------------+------------------------------------+ | sammy | The Picture of Dorian Gray | | sammy | Pride and Prejudice | | sammy | The Time Machine | | bill | Frankenstein | | bill | The Adventures of Huckleberry Finn | | walt | The Picture of Dorian Gray | | walt | Frankenstein | | walt | Pride and Prejudice | | Frankenstein | sammy | | Pride and Prejudice | sammy | | The Adventures of Huckleberry Finn | sammy | | The Picture of Dorian Gray | bill | | Crime and Punishment | bill | | The Picture of Dorian Gray | kim | | Pride and Prejudice | kim | | The Time Machine | kim | +------------------------------------+------------------------------------+ 16 rows in set (0.000 sec) تدمج عملية UNION في هذا المثال العمود الأول من الاستعلام الأول مع العمود الأول من الاستعلام الثاني وتفعل الشيء نفسه بالنسبة للعمود الثاني، وبالتالي ستخلط أسماء العملاء وعناوين الكتب مع بعضها بعضًا. استخدام الشروط وترتيب النتائج باستخدام UNION دمجنا في المثال السابق مجموعات النتائج التي تمثل جميع الصفوف في جدولين متطابقين، ولكن ستحتاج في كثير من الأحيان إلى ترشيح الصفوف وفق شروط محددة قبل دمج النتائج، حيث يمكن لتعليمات SELECT المدموجة باستخدام المعامل UNION استخدام تعليمة WHERE لتطبيق ذلك. لنفترض مثلًا أننا نريد معرفة الكتب التي يقرأها Sammy من خلال شرائها أو استئجارها، لذا نشغّل الاستعلام التالي: mysql> SELECT book_title FROM book_purchases mysql> WHERE customer_name = 'Sammy' mysql> UNION mysql> SELECT book_title FROM book_leases mysql> WHERE customer_name = 'Sammy'; يتضمن كلا الاستعلامين تعليمة WHERE، مما يؤدي إلى ترشيح الصفوف من الجدولين المنفصلين لتضمين عمليات الشراء والتأجير التي أجراها Sammy فقط، وستكون نتائج هذا الاستعلام كما يلي: الخرج +------------------------------------+ | book_title | +------------------------------------+ | The Picture of Dorian Gray | | Pride and Prejudice | | The Time Machine | | Frankenstein | | The Adventures of Huckleberry Finn | +------------------------------------+ 5 rows in set (0.000 sec) تضمن عملية UNION عدم وجود تكرارات في قائمة النتائج. يمكننا استخدام تعليمات WHERE أيضًا لتحديد الصفوف المُعادة في استعلامي SELECT أو أحدهما فقط، ويمكن أن تشير تعليمة WHERE إلى أعمدة وشروط مختلفة في كل من الاستعلامين. إضافةً لذلك، لا تتبع النتائج التي تعيدها عملية UNION ترتيبًا محدَّدًا، ولكن يمكننا تغيير ذلك من خلال استخدام تعليمة ORDER BY، حيث ينفَّذ الترتيب على النتائج النهائية المدموجة وليس على الاستعلامات الفردية. يمكننا فرز عناوين الكتب أبجديًا بعد استرجاع قائمة بجميع الكتب التي اشتراها أو استأجرها Sammy من خلال تنفيذ الاستعلام التالي: mysql> SELECT book_title FROM book_purchases mysql> WHERE customer_name = 'Sammy' mysql> UNION mysql> SELECT book_title FROM book_leases mysql> WHERE customer_name = 'Sammy' mysql> ORDER BY book_title; وسيكون الخرج كما يلي: الخرج +------------------------------------+ | book_title | +------------------------------------+ | Frankenstein | | Pride and Prejudice | | The Adventures of Huckleberry Finn | | The Picture of Dorian Gray | | The Time Machine | +------------------------------------+ 5 rows in set (0.001 sec) نلاحظ أن إعادة النتائج بالترتيب يعتمد على العمود book_title الذي يحتوي على النتائج الناتجة عن دمج استعلامَي SELECT. استخدام العملية UNION ALL للاحتفاظ بالنسخ المكررة يزيل المعامل UNION تلقائيًا الصفوف المكررة من النتائج كما أظهرت الأمثلة السابقة، ولكن قد لا يكون هذا السلوك هو ما نريد تحقيقه باستخدام الاستعلام، فمثلًا لنفترض أننا نريد معرفة الكتب المُشتراة أو المستأجرة في 1 من شهر 11 من عام 2023، حيث يمكننا استرجاع هذه العناوين من خلال اتباع المثال التالي المشابه لما سبق: mysql> SELECT book_title FROM book_purchases mysql> WHERE date = '2022-10-01' mysql> UNION mysql> SELECT book_title FROM book_leases mysql> WHERE date_from = '2022-10-01' mysql> ORDER BY book_title; وسنحصل على النتائج التالية: الخرج +------------------------------------+ | book_title | +------------------------------------+ | Pride and Prejudice | | The Adventures of Huckleberry Finn | | The Picture of Dorian Gray | +------------------------------------+ 3 rows in set (0.001 sec) عناوين الكتب المُعادة هنا صحيحة، ولكن لن تخبرنا النتائج فيما إذا كانت هذه الكتب مشتراة فقط أم مستأجرة فقط أم كليهما، حيث ستظهر عناوين الكتب في الجدولين book_purchases و book_leases في الحالات التي جرى فيها شراء بعض الكتب واستئجارها، ولكن ستُفقَد هذه المعلومات في النتيجة، لأن المعامل UNION يزيل الصفوف المكررة. تمتلك لغة SQL طريقة لتغيير هذا السلوك والاحتفاظ بالصفوف المكررة، حيث يمكننا استخدام المعامل UNION ALL لدمج النتائج من استعلامين دون إزالة الصفوف المكررة، ويعمل المعامل UNION ALL بطريقة مشابهة للمعامل UNION، ولكن إذا وُجِدت تكرارات متعددة للقيم نفسها، فستكون جميعها موجودة في النتيجة. لنشغّل الاستعلام السابق نفسه مع وضع UNION ALL مكان UNION كما يلي: mysql> SELECT book_title FROM book_purchases mysql> WHERE date = '2022-10-01' mysql> UNION ALL mysql> SELECT book_title FROM book_leases mysql> WHERE date_from = '2022-10-01' mysql> ORDER BY book_title; ستكون القائمة الناتجة أطول هذه المرة كما يلي: الخرج +------------------------------------+ | book_title | +------------------------------------+ | Pride and Prejudice | | The Adventures of Huckleberry Finn | | The Adventures of Huckleberry Finn | | The Picture of Dorian Gray | | The Picture of Dorian Gray | +------------------------------------+ 5 rows in set (0.000 sec) يظهَر الكتابان The Adventures of Huckleberry Finn و The Picture of Dorian Gray مرتين في مجموعة النتائج، وهذا يعني أنهما ظهرا في الجدولين book_purchases و book_leases، وبالتالي يمكننا افتراض أنه جرى تأجيرهما وشراؤهما في ذلك اليوم. يمكن الاختيار بين المعاملين UNION و UNION ALL والتبديل بينهما اعتمادًا على ما إذا أردنا إزالة التكرارات أو الاحتفاظ بها. ملاحظة: تنفيذ المعامل UNION ALL أسرع من تنفيذ المعامل UNION، إذ لا تحتاج قاعدة البيانات إلى فحص مجموعة النتائج للعثور على التكرارات. فإذا أردنا دمج نتائج استعلامات SELECT التي نعرف أنها لن تحتوي على أي صفوف مكررة، فيمكن أن يحقق استخدام المعامل UNION ALL أداءً أفضل مع مجموعات البيانات الأكبر حجمًا. الخلاصة تعرّفنا في هذا المقال على كيفية استرجاع البيانات من جداول متعددة باستخدام عمليات UNION و UNION ALL، واستخدمنا أيضًا تعليمات WHERE لترشيح النتائج وتعليمات ORDER BY لترتيبها، وتعلّمنا الأخطاء المحتمَلة والسلوكيات غير المتوقعة إذا أنتجت تعليمات SELECT تنسيقات بيانات مختلفة. يجب أن تعمل الأوامر الواردة في هذا المقال على معظم قواعد البيانات العلاقية، ولكن يجب أن نتذكر أن كل قاعدة بيانات SQL تستخدم تقديمها الفريد للغة، لذا ننصح بالاطلاع على مقال مقارنة بين أنظمة إدارة قواعد البيانات العلاقية: SQLite مع MySQL مع PostgreSQL لمزيد من المعلومات، كما ننصح بالرجوع إلى توثيق RDBMS الرسمي للحصول على شرح مفصّل لكل أمر ومجموعة خياراته الكاملة. ترجمة -وبتصرف- للمقال How To Use Unions in SQL لصاحبَيه Mateusz Papiernik و Rachel Lee. اقرأ أيضًا المقال السابق: استخدام الاستعلامات المتداخلة Nested Queries في SQL الدمج بين الجداول في SQL أنواع قواعد البيانات وأهم مميزاتها واستخداماتها معالجة الأخطاء والتعديل على قواعد البيانات في SQL التعابير الجدولية الشائعة Common Table Expressions في SQL
  3. نوضح في هذا المقال بعض المشكلات الشائعة التي قد يواجهها المطورون أثناء تدريب أو استخدام نماذج مكتبة المحولات Transformers، ونشرح كيفية إيجاد حلول فعالة لها. مشكلة تشغيل المكتبة في بيئات محمية بجدار حماية قد تكون بعض الخوادم أو الأجهزة التي تحتوي على وحدات معالجة الرسوميات GPU وتعمل في بيئات سحابية أو ضمن شبكات داخلية محمية بجدار حماية Firewall لا تسمح لها بالاتصال بالإنترنت، مما يؤدي إلى حدوث خطأ في الاتصال، عندها إذا حاول السكربت تنزيل أوزان النموذج أو مجموعات البيانات من الإنترنت عبر مكتبة Transformers، فسوف تتوقف عملية التنزيل وتظهر رسالة خطأ مشابهة للرسالة التالية: ValueError: Connection error, and we cannot find the requested files in the cached path. Please try again or make sure your Internet connection is on. لحل هذه المشكلة، نحتاج لتشغيل مكتبة المحولات Transformers في وضع عدم الاتصال بالإنترنت لتجنب حدوث هذا الخطأ. نفاد ذاكرة CUDA هو خطأ شائع يحدث عندما نحاول تشغيل نموذج تعلم عميق كبير على وحدة معالجة الرسوميات GPU لكن الذاكرة المتاحة لا تكفي لتحميل النموذج أو البيانات المطلوبة. فتدريب النماذج الكبيرة التي تحتوي على ملايين المعاملات أمر صعب بدون استخدام العتاد المناسب، وفي حال نفاد ذاكرة وحدة معالجة الرسوميات GPU سنحصل على رسالة خطأ كالتالي: CUDA out of memory. Tried to allocate 256.00 MiB (GPU 0; 11.17 GiB total capacity; 9.70 GiB already allocated; 179.81 MiB free; 9.85 GiB reserved in total by PyTorch) وفيما يلي بعض الحلول المحتملة التي يمكننا تجربتها لتقليل استخدام الذاكرة: تقليل حجم الدفعة المتمثل بالقيمة per_device_train_batch_size في الصنف TrainingArguments استخدام تقنية gradient_accumulation_steps في الصنف TrainingArguments لزيادة حجم الدفعة الإجمالي بفعالية، حيث تسمح لنا هذه التقنية باستخدام دفعات أصغر أثناء التدريب مع تراكم التدرجات عبر عدة دفعات ملاحظة: اطلع على دليل الأداء على منصة Huggingface لمزيد من التفاصيل حول تقنيات توفير الذاكرة. تعذر تحميل نموذج تنسرفلو المحفوظ يحفظ التابع model.save في إطار عمل تنسرفلو TensorFlow النموذج بالكامل متضمنًا البنية والأوزان وضبط التدريب في ملف واحد، ولكن قد نواجه خطأ عند محاولة تحميل ملف النموذج مرة أخرى لأن مكتبة المحولات Transformers قد لا تحمّل جميع العناصر المرتبطة بإطار عمل تنسرفلو TensorFlow في ملف النموذج. يُوصَى باتباع الخطوات التالية لتجنب المشكلات المتعلقة بحفظ وتحميل نماذج TensorFlow: حفظ أوزان النموذج مع لاحقة الملف h5 باستخدام model.save_weights، ثم إعادة تحميل النموذج باستخدام التابع from_pretrained()‎ كما يلي: >>> from transformers import TFPreTrainedModel >>> from tensorflow import keras >>> model.save_weights("some_folder/tf_model.h5") >>> model = TFPreTrainedModel.from_pretrained("some_folder") حفظ النموذج باستخدام ‎~TFPretrainedModel.save_pretrained وتحميله مرة أخرى باستخدام التابع from_pretrained()‎: >>> from transformers import TFPreTrainedModel >>> model.save_pretrained("path_to/model") >>> model = TFPreTrainedModel.from_pretrained("path_to/model") هذا يجعل عملية الحفظ أكثر توافقًا مع مكتبة المحولات عند تحميل النماذج مرة أخرى. خطأ الاستيراد ImportError يوجد خطأ شائع آخر قد نواجهه وهو خطأ الاستيراد ImportError الذي يحدث عند محاولة استيراد مكتبة أو كائن معين، ولكن النظام لا يستطيع العثور عليه في المكان المحدد، ويقع هذا الخطأ خاصةً عند إصدار نموذج حديث ولكننا لا نزال نستخدم إصدار قديم من المكتبة لا يدعم ميزاته الجديدة. على سبيل المثال، إذا حاولنا استيراد الصنف ImageGPTImageProcessor من المكتبة transformers ولم يستطع النظام العثور عليه ستظهر رسالة خطأ كالتالي: ImportError: cannot import name 'ImageGPTImageProcessor' from 'transformers' (unknown location) لحل هذا النوع من الأخطاء، علينا التأكد من تثبيت أحدث إصدار من مكتبة Transformers للوصول إلى أحدث النماذج من خلال الأمر التالي: pip install transformers --upgrade خطأ CUDA يحدث هذا الخطأ عادةً أثناء تنفيذ العمليات الحسابية على وحدة معالجة الرسوميات GPU، وهو يتعلق بخطأ عام في الشيفرة البرمجية للجهاز ويعرض رسالة كالتالي: RuntimeError: CUDA error: device-side assert triggered لحل هذا الخطأ علينا محاولة تشغيل الشيفرة البرمجية على وحدة المعالجة المركزية CPU أولًا للحصول على رسالة خطأ توصيفية واضحة، لذا نضيف متغير البيئة التالي لبداية شيفرتنا البرمجية للتبديل إلى وحدة المعالجة المركزية: >>> import os >>> os.environ["CUDA_VISIBLE_DEVICES"] = "" كما يوجد خيار آخر يساعدنا على تشخيص الحل، وهو الحصول على تعقّب أفضل أثناء استخدام وحدة معالجة الرسوميات GPU، لذا نضيف متغير البيئة التالي إلى بداية الشيفرة البرمجية لجعل التعقّب يشير بوضوح إلى مصدر الخطأ كالتالي: >>> import os >>> os.environ["CUDA_LAUNCH_BLOCKING"] = "1" سيتسبب هذا الأمر في تمكين الوضع المتزامن، حيث تُنفذ العمليات على GPU بشكل متسلسل خطوة بخطوة، بدلاً من العمل على التوازي وبهذا يمكننا تحديد موقع الخطأ بشكل أفضل. خرج خاطئ بسبب خطأ بالتعامل مع رموز الحشو Padding Tokens عند تدريب النماذج على نصوص بأطوال مختلفة قد نحتاج لاستخدام رموز الحشو Padding Tokens وهي رموز ليس لها معنى في البيانات المدخلة ولكنها تُستخدم فقط لتعبئة السلاسل النصية بحيث يكون طولها متساوي، وتكون قيمة هذه الرموز عادةً صفرًا أو أي قيمة أخرى تحددها أثناء التدريب. في حال لم نتعامل مع هذه الرموز بشكل صحيح باستخدام ما يسمى قناع الانتباه attention mask والذي يحدد ما هي الرموز التي يجب على النموذج تجاهلها فقد نحصل على خرج غير صحيح، وبما أن مكتبة Transformers قد لا تقوم تلقائيًا بإنشاء قناع لبعض النماذج، لذا يجب إضافته يدويًا لتجنب أخطاء كهذه. على سبيل المثال قد يكون التمثيل الرقمي لدخل النموذج hidden_state غير صحيح إذا كانت معرّفات الدخل input_ids تتضمن رموز الحشو ولا تتجاهلها بشكل صحيح. لتوضيح ذلك، لنحمّل نموذجًا Model ومرمِّزًا Tokenizer، حيث يمكن الوصول إلى معرّف pad_token_id الخاص بالنموذج لمعرفة قيمته، قد تكون قيمة معرّف pad_token_id هي None لبعض النماذج وهذا يعني أن النموذج لا يستخدم رموز حشو، ولكن يمكن ضبطها يدويًا. لنستورد نموذج BERT المدرب مسبقًا والذي يحدد معرّف الحشو pad_token_id لتكون صفر : >>> from transformers import AutoModelForSequenceClassification >>> import torch >>> model = AutoModelForSequenceClassification.from_pretrained("google-bert/bert-base-uncased") >>> model.config.pad_token_id 0 سيكون الخرج الذي نحصل عليه للتسلسل الأول بدون تقنيع رموز الحشو كالتالي: >>> input_ids = torch.tensor([[7592, 2057, 2097, 2393, 9611, 2115], [7592, 0, 0, 0, 0, 0]]) >>> output = model(input_ids) >>> print(output.logits) tensor([[ 0.0082, -0.2307], [ 0.1317, -0.1683]], grad_fn=<AddmmBackward0>) ويكون الخرج الفعلي للتسلسل الثاني : >>> input_ids = torch.tensor([[7592]]) >>> output = model(input_ids) >>> print(output.logits) tensor([[-0.1008, -0.4061]], grad_fn=<AddmmBackward0>) يجب توفير قناع انتباه attention_mask لنموذجنا لتجاهل رموز الحشو وتجنب هذا الخطأ الخفي، فهو لا يعطينا رسالة خطأ صريحة، سيتطابق الآن خرج التسلسل الثاني مع الخرج الفعلي: >>> attention_mask = torch.tensor([[1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0]]) >>> output = model(input_ids, attention_mask=attention_mask) >>> print(output.logits) tensor([[ 0.0082, -0.2307], [-0.1008, -0.4061]], grad_fn=<AddmmBackward0>) لا تنشئ مكتبة المحولات Transformers قناع انتباه attention_mask تلقائيًا لرمز الحشو دائمًا وذلك لأن: بعض النماذج لا تحتوي على رمز حشو في بعض الحالات، نحتاج للاهتمام برموز الحشو وأخذها بعين الاعتبار أثناء المعالجة خطأ استخدام نموذج غير مناسب للمهمة من المهم التأكد من استخدام النموذج المناسب لكل مهمة محددة، فإذا جربنا تحميل نموذج غير متوافق مع المهمة ستظهر رسالة خطأ مشابهة للتالي: ValueError: Unrecognized configuration class XYZ for this kind of AutoModel يوصى باستخدام الصنف AutoModel لتحميل نسخ مدربة مسبقًا من النماذج، حيث يساعد هذا الصنف في تحديد وتحميل البنية الصحيحة تلقائيًا من نقطة تحقق معينة بناءً على الضبط، فإذا ظهر الخطأ عند تحميل نموذج من نقطة تحقق، فهذا يعني أن الصنف التلقائي لم يتمكن من العثور على ربط صحيح بين الضبط ونوع النموذج الذي نحاول تحميله، ولا يدعم النموذج الذي نحاول تحميله المهمة المطلوبة. على سبيل المثال، سنرى هذا الخطأ إذا حاولنا استخدام نموذج GPT2 للإجابة على الأسئلة، لأن GPT2 ليس مخصصًا لهذه المهمة. >>> from transformers import AutoProcessor, AutoModelForQuestionAnswering >>> processor = AutoProcessor.from_pretrained("openai-community/gpt2-medium") >>> model = AutoModelForQuestionAnswering.from_pretrained("openai-community/gpt2-medium") ValueError: Unrecognized configuration class <class 'transformers.models.gpt2.configuration_gpt2.GPT2Config'> for this kind of AutoModel: AutoModelForQuestionAnswering. Model type should be one of AlbertConfig, BartConfig, BertConfig, BigBirdConfig, BigBirdPegasusConfig, BloomConfig, ... الخاتمة حاولنا في هذا المقال تسليط الضوء على أبرز المشكلات التي قد نواجهها عند التعامل مع مكتبة المحوِّلات Transformers، قد لا يحتوي المقال على جميع المشكلات لكن في حال واجهتك مشكلة ما وصعب عليك حلها لا تتردد في كتابة مشكلتك في قسم الأسئلة والأجوبة في أكاديمية حسوب حيث سيجيبك عدد من المختصين عليها بالتفصيل، كما يمكنك أيضًا طلب المساعدة في منتديات منصة Huggingface التي تتضمن فئات محددة يمكنك نشر سؤالك فيها مثل فئة المبتدئين أو Transformers، وتأكّد من كتابة وصف جيد لمشكلتك مع توفير بعض الأكواد البرمجية. وعند وجود خطأ يتعلق بالمكتبة Transformers بلغ عنها في مستودع المكتبة، وحاول تضمين أكبر قدر ممكن من المعلومات التي تصف الخطأ للمساعدة على معرفته بصورة أفضل وإصلاحه بسرعة وسهولة. ترجمة -وبتصرّف- للقسم Troubleshoot من توثيقات Hugging Face. اقرأ أيضًا المقال السابق: قياس أداء نماذج المحولات Transformers تثبيت مكتبة المحوّلات Transformers قياس أداء نماذج المحولات Transformers تعرف على منصة تنسرفلو TensorFlow للذكاء الاصطناعي
  4. تعرّفنا في المقال السابق على طريقة استيراد كائنات ثلاثية الأبعاد لمحرك الألعاب جودو وكيفية ترتيبها في مشهد اللعبة، وسنضيف في هذا المقال مزيدًا من الكائنات إلى مشهد اللعبة، وسنشرح طريقة إنشاء شخصية ثلاثية الأبعاد يتحكم فيها المستخدم. بناء المشهد سنستمر في هذا المقال استخدام مجموعة الملحقات assets التي توفرها منصة Kenney على هذا الرابط والتي شرحنا طريقة تنزيلها واستيرادها في مقال سابق. نفتح مشروع اللعبة ثلاثية الأبعاد التي بدأناها، ونحدّد الآن جميع ملفات block*.glb، وننتقل إلى التبويب استيراد Import ونضبط نوع الجذر Root Type الخاص بهذه الملفات على StaticBody3D ، بعدها ننقر على زر إعادة الاستيراد Reimport كما في الصورة أدناه. بعدها، نحدّد الكائن block-grass-large.glb بزر الفأرة الأيمن ونختار إنشاء مشهد موروث جديد، ستظهر عقدة جديدة باسم block-grass-large في المشهد، وعقدة ابن لها تُمثّل المجسم Mesh، نحدد العقدة الابن وننقر فوق خيار مجسم Mesh في القائمة العلوية الظاهرة في محرر جودو، ثم نحدد خيار إنشاء شكل تصادم Create Collision Shape ونعيّن قيمة الحقل Collision Shape Placement لتكون Sibling وقيمة الحقل Collision Shape Type لتكون Trimmesh كما فعلنا بالضبط في المقال السابق. عند الضغط على زر الإنشاء سيضيف جودو تلقائيًا العقدة CollionShape3D مع شكل تصادم يطابق المجسم Mesh. يمكننا الآن حفظ المشهد باسم BlockLarge، ويفضل إنشاء مجلد منفصل وليكن platform_objects لحفظ المشهد وكافة المشاهد الأخرى التي تمثل أجزاء منصة اللعبة بأشكالها المختلفة. نفتح مشهد الأرضية Ground مع الصناديق Crates الذي عملنا عليه في المقال السابق ونحذف كافة الصناديق المضافة ونستبدلها بالمشهد الموروث الذي حفظناه للتو، بهذا سنتمكّن من وضع عدة كتل بجانب بعضها البعض بحيث تكون في صف واحد وتشكل منصة اللعبة أو عالم اللعبة. بعدها نحدّد العقدة BlockLarge وننقر خيار تعديل المحاذاة Configure Snap من القائمة العلوية التحوّل Transform الظاهرة أعلى نافذة العرض كالتالي: نضبط خيار ترجمة المحاذاة Translate Snap على القيمة 0.5، ثم ننقر على زر استخدام المحاذاة Snap Mode أو نضغط على مفتاح Y، ثم نكرّر الكتلة عدة مرات ونسحبها لترتيبها ضمن المشهد بالشكل المناسب. يمكن أيضًا إضافة مشاهد لبعض كتل المنصة الأخرى وترتيبها في أيّ شكل نريده. إضافة شخصية Character سننشئ الآن شخصية يمكنها التجول على المنصة التي أنشأناها، لذا نفتح مشهدًا جديدًا ونبدأ باستخدام العقدة CharacterBody3D بالاسم Character، حيث تتصرف عقدة PhysicsBody بطريقة مشابهة جدًا لنظيرتها ثنائية الأبعاد، وتحتوي على التابع move_and_slide()‎ الذي سنستخدمه لإجراء الحركة وكشف التصادم. نضيف عقدة MeshInstance3D على شكل كبسولة، وعقدة CollionShape3D مطابقة لها، ولنتذكّر أن بإمكاننا إضافة عقدة StandardMaterial3D إلى المجسم Mesh وضبط خاصية اللون Color في القسم Albedo. شكل الكبسولة جميل ومناسب للشخصية، ولكن سيكون من الصعب معرفة الاتجاه الذي يمثل الوجه، لذا لنضيف مجسم Mesh آخر على شكل مخروطي CylinderMesh3D، ونضبط نصف قطره العلوي Top Radius على القيمة 0.2 ونصف قطره السفلي Bottom Radius على القيمة 0.001 وارتفاعه Height على القيمة 0.5، ثم نضبط دورانه حول المحور x على ‎-90 درجة. أصبح لدينا الآن شكل مخروطي جميل، لذا نرتّبه بحيث يشير لخارج الجسم على طول محور z السالب، إذ يمكن بسهولة معرفة الاتجاه السالب لأن أسهم أداة Gizmo تشير إلى الاتجاه الموجب. ملاحظة: أضفنا في هذه الشخصية أيضًا مجسمين كرويين بنفس الطريقة لتمثيل عيني الشخصية، ويمكن إضافة أية تفاصيل نريدها. لنضف الآن عقدة كاميرا Camera3D إلى المشهد لتتبع الشخصية. نضع الكاميرا خلف الشخصية وفوقها مع توجيهها للأسفل قليلًا، ثم ننقر على زر معاينة Preview للتحقق من عرض الكاميرا وظهورالشخصية بالشكل المناسب. كود التحكم في الشخصية قبل إضافة سكربت برمجي للتحكم في الشخصية، سوف نفتح إعدادات المشروع Project Settings، ونضيف المدخلات التالية في تبويب خريطة الإدخال Input Map: إجراء الإدخال Input Action المفتاح Key التحرك للأمام move_forward المفتاح W التحرك للخلف move_back المفتاح S الانعطاف لليمين strafe_right المفتاح D الانعطاف لليسار strafe_left المفتاح A القفز jump المسافة Space يمكننا الآن كتابة سكربت لتحريك شخصيتنا ثلاثية الأبعاد كما يلي: extends CharacterBody3D var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") var speed = 4.0 # سرعة الحركة var jump_speed = 6.0 # تحديد ارتفاع القفزة var mouse_sensitivity = 0.002 # سرعة الدوران func get_input(): var input = Input.get_vector("strafe_left", "strafe_right", "move_forward", "move_back") velocity.x = input.x * speed velocity.z = input.y * speed func _physics_process(delta): velocity.y += -gravity * delta get_input() move_and_slide() نلاحظ أن الشيفرة البرمجية في الدالة ‎_physics_process()‎ بسيطة، حيث نضيف الجاذبية للتسارع في الاتجاه الموجب للمحور Y إلى الأسفل، ثم نستدعي الدالة get_input()‎ للتحقق من الإدخال أي معرفة المفتاح الذي ضغط المستخدم عليه، ثم نستخدم التابع move_and_slide()‎ للتحرك في اتجاه متجه السرعة. نشغّل اللعبة لاختبارها، وسنرى النتيجة التالية: هذا يبدو جيدًا، ولكن يجب أن نكون قادرين على تدوير الشخصية باستخدام الفأرة، لذا سنضيف الشيفرة البرمجية التالية إلى سكربت الشخصية: func _unhandled_input(event): if event is InputEventMouseMotion: rotate_y(-event.relative.x * mouse_sensitivity) عندما نحرك الفأرة أفقيًا في اتجاه المحور x سيعمل السكربت بتدوير الشخصية حول المحور الرأسي Y، وبالتالي إذا حركنا الفأرة إلى اليمين، سيؤدي ذلك إلى تدوير الشخصية إلى اليسار. وإذا حركنا الفأرة إلى اليسار ستدور الشخصية إلى اليمين. لدينا هنا مشكلة، إذ يحرّكنا الضغط على مفتاح W على طول المحور Z لعالم اللعبة بغض النظر عن الاتجاه الذي تتجه له الشخصية، لأن اللعبة تستخدم هنا الإحداثيات العالمية Global Coordinates، ولكننا بحاجة إلى التحرك بناء على اتجاه الكائن، ويمكن حل هذه المشكلة باستخدام التحويلات. الاستفادة من التحويلات Transforms التحويل هو مصفوفة رياضية تحتوي على معلومات حول انتقال الكائن ودورانه وتغيير حجمه في آن واحد، ويُخزَّنه جودو في نوع البيانات Transform، حيث تُسمَّى معلومات الموضع بالاسم transform.origin وتكون معلومات الاتجاه في transform.basis. تشير محاور X و Y و Z الخاصة بأداة Gizmo على طول محاور الكائن نفسه عندما تكون في وضع الحيّز المحلي Local Space Mode، ويشابه ذلك الأساس basis الخاص بالتحويل، حيث يحتوي هذا الأساس على ثلاثة كائنات Vector3 تسمى x و y و z تمثل هذه الاتجاهات، ويمكننا استخدامها لضمان أن الضغط على مفتاح W سيؤدي إلى التحرك في الاتجاه الأمامي للكائن دائمًا. نعدّل الدالة get_input()‎ كما يلي: func get_input(): var input = Input.get_vector("strafe_left", "strafe_right", "move_forward", "move_back") var movement_dir = transform.basis * Vector3(input.x, 0, input.y) velocity.x = movement_dir.x * speed velocity.z = movement_dir.z * speed نطبّق هذا التحويل على المتجه من خلال ضرب متجه الإدخال في أساس التحويل transform.basis. يمثّل الأساس دوران الكائن، لذا لنحوّل الآن اتجاه الأمام والخلف للإشارة على طول المحور Z للكائن، ونحوّل مفاتيح التحريك لليمين واليسار للإشارة على طول المحور X الخاص بالكائن. القفز Jumping سنضيف الآن حركة أخرى إلى اللاعب وهي القفز، لتحقيق ذلك نضيف الأسطر التالية إلى نهاية الدالة ‎_unhandled_input()‎: if event.is_action_pressed("jump") and is_on_floor(): velocity.y = jump_speed تحسين الكاميرا إذا وقفت الشخصية بالقرب من عائق ما، فيمكن للكاميرا الالتصاق بالكائن بطريقة غير لطيفة. وبالرغم من أن برمجة كاميرا ثلاثية الأبعاد قد تكون أمرًا معقدًا بحد ذاته، ولكن يمكننا استخدام عقد جودو المضمنة للحصول على حل جيد لذلك. نحذف العقدة Camera3D من مشهد الشخصية ونضيف العقدة SpringArm3D التي تعمل كذراع متحركة تحمل الكاميرا أثناء كشف التصادمات، وستقرّب الكاميرا عند وجود عقبة، ثم نضبط خاصية طول الذراع Spring Length على القيمة 5، ونضبط خاصية الموضع Position على القيم ‎(0, 1, 0)‎. نلاحظ ظهور خط بلون أصفر يشير إلى طول النابض Spring Length، حيث ستتحرك الكاميرا على طول هذا الخط، ولكنها ستقترب عند وجود عقبة حتى نهايته كلما أمكن ذلك. نعيد إضافة العقدة Camera3D كابن للعقدة SpringArm3D، ونحاول تشغيل اللعبة مرة أخرى، ويمكن تجربة تدوير ذراع النابض حول محوره X ليشير إلى الأسفل قليلًا حتى الوصول لنتيجة مرضية. الخلاصة شرحنا في هذا المقال كيفية بناء مشهد ثلاثي الأبعاد أكثر تعقيدًا وكتابة الشيفرة البرمجية لحركة شخصية يتحكم فيها المستخدم، وتعلمنا أيضًا مفهوم التحويلات Transforms التي تعد مفهومًا مهمًا جدًا في الألعاب ثلاثية الأبعاد، والتي ستستخدمها بكثرة في المقالات القادمة. ترجمة -وبتصرّف- للقسم Creating a 3D Character من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: استيراد الكائنات ثلاثية الأبعاد في جودو تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو استخدام محرر جودو ثلاثي الأبعاد كتابة سكربتات GDScript وإرفاقها بالعقد في جودو
  5. تفيدنا لغة الاستعلام البنيوية Structured Query Language -أو SQL اختصارًا- في إدارة البيانات المخزنة في نظام إدارة قواعد بيانات علاقية Relational Database Management System -أو RDBMS اختصارًا- ومن أبرز الوظائف المفيدة في SQL هي إنشاء استعلام ضمن استعلام آخر، والذي يُعرَف باسم الاستعلام الفرعي Subquery أو الاستعلام المتداخل Nested وهو موضوع مقالنا اليوم. متطلبات العمل يجب توفر حاسوب يشغّل أحد أنواع أنظمة إدارة قواعد البيانات العلاقية RDBMS التي تستخدم لغة SQL. وبالنسبة لهذا المقال فقد اختبرنا التعليمات والأمثلة الواردة فيه باستخدام البيئة التالية: خادم عامل على أحدث إصدار من توزيعة أوبنتو Ubuntu مع مستخدم ذو صلاحيات مسؤول مختلف عن المستخدم الجذر، وجدار حماية مُفعَّل كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضّح في مقال كيفية تثبيت MySQL على أوبنتو وقد نفذنا خطوات هذا المقال باستخدام مستخدم MySQL مختلف عن المستخدم الجذر وفق الطريقة الموضحة في الخطوة 3 من المقال. ملاحظة: تجدر الإشارة إلى أنّ الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، إذ ستعمل الأوامر المُقدمة في هذا المقال بنجاح مع معظم هذه الأنظمة، ولكن قد نجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. سنحتاج أيضًا إلى قاعدة بيانات تحتوي على جدول يحتوي بيانات تجريبية نموذجية لنتمكّن من التدرب على استخدام الاستعلامات المتداخلة، وفي حال لم تكن متوفرة فيمكن مطالعة القسم التالي لمعرفة كيفية إنشاء قاعدة البيانات والجدول المستخدَمَين في أمثلة هذا المقال. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية إذا كانت قاعدة بيانات SQL الخاصة بنا تعمل على خادم بعيد، علينا الاتصال بالخادم باستخدام بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بنا مكان user: $ mysql -u user -p ننشئ قاعدة بيانات بالاسم zooDB: mysql> CREATE DATABASE zooDB; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر الخرج التالي: الخرج Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات zooDB من خلال تنفيذ تعليمة USE التالية: mysql> USE zooDB; الخرج Database changed اخترنا قاعدة البيانات، وسننشئ جدولًا ضمنها، حيث سننشئ جدولًا يخزّن المعلومات حول الزوار الذين يزورون حديقة الحيوان، وسيحتوي هذا الجدول على الأعمدة السبعة التالية: guest_id: يخزّن قيمًا للزوار الذين يزورون حديقة الحيوان ويستخدم نوع البيانات int، ويمثّل المفتاح الرئيسي Primary Key للجدول، ممّا يعني أن كل قيمة في هذا العمود ستمثّل معرّفًا فريدًا للصف الخاص بها first_name: الاسم الأول لكل زائر ويستخدم نوع البيانات varchar بحد أقصى 30 محرف last_name: الاسم الأخير لكل زائر ويستخدم نوع البيانات varchar بحد أقصى 30 محرف guest_type: يحدد هل الزائر بالغ أو طفل ويستخدم نوع البيانات varchar بحد أقصى 15 محرف membership_type: نوع العضوية لكل زائر ويستخدم نوع بيانات varchar بحد أقصى 30 محرف membership_cost: تكلفة أنواع العضوية المختلفة، ويستخدم نوع البيانات decimal بدقة 5 ومقياس 2، أي أن القيم الموجودة في هذا العمود يمكن أن تحتوي على خمسة أرقام مع وجود رقمين على يمين الفاصلة العشرية total_visits: يسجل إجمالي عدد الزيارات لكل زائر ويستخدم نوع البيانات int سننشئ جدولًا باسم guests يحتوي على الأعمدة السابقة من خلال تشغيل الأمر CREATE TABLE التالي: mysql> CREATE TABLE guests ( mysql> guest_id int, mysql> first_name varchar(30), mysql> last_name varchar(30), mysql> guest_type varchar(15), mysql> membership_type varchar(30), mysql> membership_cost decimal(5,2), mysql> total_visits int, mysql> PRIMARY KEY (guest_id) mysql> ); ثم ندخل بعض البيانات التجريبية في هذا الجدول الفارغ كما يلي: mysql> INSERT INTO guests mysql> (guest_id, first_name, last_name, guest_type, membership_type, membership_cost, total_visits) mysql> VALUES mysql> (1, 'Judy', 'Hopps', 'Adult', 'Resident Premium Pass', 110.0, 168), mysql> (2, 'Nick', 'Wilde', 'Adult', 'Day Pass', 62.0, 1), mysql> (3, 'Duke', 'Weaselton', 'Adult', 'Resident Pass', 85.0, 4), mysql> (4, 'Tommy', 'Yax', 'Child', 'Youth Pass', 67.0, 30), mysql> (5, 'Lizzie', 'Yax', 'Adult', 'Guardian Pass', 209.0, 30), mysql> (6, 'Jenny', 'Bellwether', 'Adult', 'Resident Premium Pass', 110.0, 20), mysql> (7, 'Idris', 'Bogo', 'Child', 'Youth Pass', 67.0, 79), mysql> (8, 'Gideon', 'Grey', 'Child', 'Youth Pass', 67.0, 100), mysql> (9, 'Nangi', 'Reddy', 'Adult', 'Guardian Champion', 400.0, 241), mysql> (10, 'Octavia', 'Otterton', 'Adult', 'Resident Pass', 85.0, 11), mysql> (11, 'Calvin', 'Roo', 'Adult', 'Resident Premium Pass', 110.0, 173), mysql> (12, 'Maurice', 'Big', 'Adult', 'Guardian Champion', 400.0, 2), mysql> (13, 'J.K.', 'Lionheart', 'Child', 'Day Pass', 52.0, 1), mysql> (14, 'Priscilla', 'Bell', 'Child', 'Day Pass', 104.0, 2), mysql> (15, 'Tommy', 'Finnick', 'Adult', 'Day Pass', 62.0, 1); الخرج Query OK, 15 rows affected (0.01 sec) Records: 15 Duplicates: 0 Warnings: 0 نحن الآن جاهزون لبدء استخدام الاستعلامات المتداخلة في لغة SQL. ما هو الاستعلام المتداخل الاستعلام في لغة SQL هو عملية تسترجع البيانات من جدول في قاعدة بيانات وتتضمن تعليمة SELECT دائمًا، أما الاستعلام المتداخل nested query فهو استعلام داخل استعلام آخر، بمعنى آخر الاستعلام المتداخل هو تعليمة SELECT توضع بين قوسين عادة، وتُضمَّن في عملية SELECT أو INSERT أو DELETE رئيسية، ويُعَد الاستعلام المتداخل مفيدًا في الحالات التي نريد فيها تنفيذ أوامر متعددة في تعليمة استعلام واحدة بدلًا من كتابة عدة أوامر لإعادة النتيجة المطلوبة، ويمكننا من خلال الاستعلامات المتداخلة إتمام عمليات معقدة على البيانات بطريقة أسهل. سنستخدم في هذا المقال الاستعلامات المتداخلة مع تعليمات SELECT و INSERT و DELETE، كما سنستخدم الدوال التجميعية Aggregate Functions ضمن استعلام متداخل لمقارنة قيم البيانات بقيم البيانات المفروزة التي حدّدناها باستخدام تعلميتي WHERE و LIKE. استخدام الاستعلامات المتداخلة مع تعليمة SELECT لنوضّح فائدة الاستعلامات المتداخلة عمليًا باستخدام البيانات التجريبية التي أضفناها في خطوة سابقة، ولنفترض مثلًا أننا نريد البحث عن جميع الزوار في الجدول guests الذين زاروا حديقة الحيوان بعدد مرات أكبر من متوسط الزيارات. قد نفترض أن بإمكاننا العثور على هذه المعلومات من خلال الاستعلام التالي: mysql> SELECT first_name, last_name, total_visits mysql> FROM guests mysql> WHERE total_visits > AVG(total_visits); ولكن سيعيد الاستعلام الذي يستخدم الصيغة السابقة خطأ كما يلي: الخرج ERROR 1111 (HY000): Invalid use of group function سبب هذا الخطأ هو أن الدوال التجميعية مثل الدالة AVG()‎ لا تعمل إلا إذا كانت مُنفَّذة ضمن تعليمة SELECT. أحد الخيارات لاسترجاع هذه المعلومات هو تشغيل استعلام للعثور على متوسط عدد زيارات الزوار أولًا، ثم تشغيل استعلام آخر للعثور على النتائج بناءً على تلك القيمة كما هو الحال في المثالين التاليين: mysql> SELECT AVG(total_visits) FROM guests; الخرج +-----------------+ | avg(total_visits) | +-----------------+ | 57.5333 | +-----------------+ 1 row in set (0.00 sec) mysql> SELECT first_name, last_name, total_visits mysql> FROM guests mysql> WHERE total_visits > 57.5333; الخرج +----------+---------+------------+ | first_name | last_name | total_visits | +----------+---------+------------+ | Judy | Hopps | 168 | | Idris | Bogo | 79 | | Gideon | Grey | 100 | | Nangi | Reddy | 241 | | Calvin | Roo | 173 | +----------+---------+------------+ 5 rows in set (0.00 sec) ولكن يمكننا الحصول على مجموعة النتائج نفسها باستخدام استعلام واحد من خلال تداخل الاستعلام الأول (SELECT AVG(total_visits) FROM guests;‎) مع الاستعلام الثاني. ينبغي أن نضع في الحسبان أن استخدام العدد المناسب من الأقواس مع الاستعلامات المتداخلة أمر ضروري لإكمال العملية التي نريد تنفيذها، لأن الاستعلام المتداخل هو أول عملية تُنفَّذ: mysql> SELECT first_name, last_name, total_visits mysql> FROM guests mysql> WHERE total_visits > mysql> (SELECT AVG(total_visits) FROM guests); الخرج +------------+-----------+--------------+ | first_name | last_name | total_visits | +------------+-----------+--------------+ | Judy | Hopps | 168 | | Idris | Bogo | 79 | | Gideon | Grey | 100 | | Nangi | Reddy | 241 | | Calvin | Roo | 173 | +------------+-----------+--------------+ 5 rows in set (0.00 sec) يوضّح المثال السابق أهمية استخدام استعلام متداخل في تعليمة واحدة كاملة للحصول على النتائج المطلوبة بدلًا من الاضطرار إلى تشغيل استعلامين منفصلين، وفق الخرج الذي حصلنا عليه فقد زار خمسة زوار حديقة الحيوان بعدد مرات أكبر من متوسط الزيارات، ويمكن أن تقدّم هذه المعلومات نظرة مفيدة للتفكير في طرق إبداعية لضمان استمرار الأعضاء الحاليين في زيارة حديقة الحيوان وتجديد عضويتهم كل سنة. استخدام الاستعلامات المتداخلة مع تعليمة INSERT لا يقتصر الأمر على تضمين الاستعلام المتداخل في تعليمات SELECT أخرى فقط، إذ يمكننا أيضًا استخدام الاستعلامات المتداخلة لإدخال البيانات في جدول موجود مسبقًا من خلال تضمين الاستعلام المتداخل في عملية INSERT. لنفترض وجود حديقة حيوانات ما تطلب بعض المعلومات عن زوار حديقة الحيوان خاصة بنا وتود تقديم خصم بنسبة 15% للزوار الذين يشترون العضوية الدائمة في موقعها، لذا سنستخدم تعليمة CREATE TABLE لإنشاء جدول جديد بالاسم upgrade_guests والذي يحتوي على ستة أعمدة، وننتبه جيدًا لأنواع البيانات مثل int و varchar والحد الأقصى من المحارف التي يمكن الاحتفاظ بها، فإن لم تكن متماشية مع أنواع البيانات الأصلية من الجدول guests الذي أنشأناه في قسم إعداد قاعدة بيانات نموذجية، فسنتلقى خطأً عند محاولة إدخال بيانات من الجدول guests باستخدام استعلام متداخل ولن تُنقَل البيانات بطريقة صحيحة. لننشئ هذا الجدول مع المعلومات التالية: mysql> CREATE TABLE upgrade_guests ( mysql> guest_id int, mysql> first_name varchar(30), mysql> last_name varchar(30), mysql> membership_type varchar(30), mysql> membership_cost decimal(5,2), mysql> total_visits int, mysql> PRIMARY KEY (guest_id) mysql> ); احتفظنا بمعظم معلومات نوع البيانات في هذا الجدول كما كانت في الجدول guests من أجل التناسق والدقة، وحذفنا أي أعمدة إضافية لا نريدها في الجدول الجديد. أصبح هذا الجدول الفارغ جاهزًا للبدء، والخطوة التالية هي إدخال قيم البيانات المطلوبة في الجدول. نكتب التعليمة INSERT INTO مع الجدول upgrade_guests الجديد مع وجود اتجاه واضح لمكان إدخال البيانات، ثم نكتب الاستعلام المتداخل باستخدام تعليمة SELECT لاسترجاع قيم البيانات ذات الصلة وتعليمة FROM للتأكد من أن البيانات تأتي من الجدول guests. سيطبّق خصم بقيمة 15% على الأعضاء الدائمين من خلال تضمين عملية الضرب الرياضية * للضرب بالعدد 0.85 ضمن الاستعلام المتداخل (membership_cost * 0.85)، وتفيدنا تعليمة WHERE في فرز القيم الموجودة في العمود membership_type. يمكننا تضييق نطاق النتائج لتشمل فقط نتائج الأعضاء الدائمين باستخدام تعليمة LIKE ووضع رمز النسبة المئوية % قبل وبعد الكلمة "Resident" بين علامتي اقتباس مفردتين لتحديد نوع العضوية التي تتبع النمط أو المفردات نفسها، وسيُكتَب هذا الاستعلام كما يلي: mysql> INSERT INTO upgrade_guests mysql> SELECT guest_id, first_name, last_name, membership_type, mysql> (membership_cost * 0.85), total_visits mysql> FROM guests mysql> WHERE membership_type LIKE '%resident%'; الخرج Query OK, 5 rows affected, 5 warnings (0.01 sec) Records: 5 Duplicates: 0 Warnings: 5 يشير الخرج السابق إلى إضافة خمسة سجلات إلى الجدول upgrade_guests الجديد. يمكن التأكد من نقل البيانات التي طلبتها بنجاح من الجدول guests إلى الجدول upgrade_guests الفارغ الذي أنشأناه من خلال تشغيل الأمر التالي مع الشروط التي حدّدناها مع الاستعلام المتداخل وتعليمة WHERE: mysql> SELECT * FROM upgrade_guests; الخرج +----------+------------+------------+-----------------------+-----------------+--------------+ | guest_id | first_name | last_name | membership_type | membership_cost | total_visits | +----------+------------+------------+-----------------------+-----------------+--------------+ | 1 | Judy | Hopps | Resident Premium Pass | 93.50 | 168 | | 3 | Duke | Weaselton | Resident Pass | 72.25 | 4 | | 6 | Jenny | Bellwether | Resident Premium Pass | 93.50 | 20 | | 10 | Octavia | Otterton | Resident Pass | 72.25 | 11 | | 11 | Calvin | Roo | Resident Premium Pass | 93.50 | 173 | +----------+------------+------------+-----------------------+-----------------+--------------+ 5 rows in set (0.01 sec) نلاحظ الإدراج الصحيح لمعلومات عضوية الزائر الدائم "Resident" ذات الصلة من الجدول guest في الجدول upgrade_guests، مع إعادة حساب التكلفة membership_cost الجديدة مع تطبيق خصم 15%، وبذلك ساعدت هذه العملية في تقسيم الجمهور المناسب واستهدافه، وأصبحت الأسعار المخفَّضة متاحة بسهولة لمشاركتها مع الأعضاء الجدد المحتملين. استخدام الاستعلامات المتداخلة مع تعليمة DELETE لنفترض أننا نريد إزالة الزوار المعتادين والتركيز فقط على الترويج لخصم البطاقة المميزة للأعضاء الذين لا يزورون حديقة الحيوان كثيرًا حاليًا. نبدأ هذه العملية باستخدام تعليمة DELETE FROM بحيث يكون من الواضح المكان الذي ستحذف البيانات منه، وهو الجدول upgrade_guests في حالتنا، ثم نستخدم تعليمة WHERE لفرز أي قيمة من العمود total_visits تزيد عن الكمية المحدَّدة في الاستعلام المتداخل. نستخدم تعليمة SELECT في استعلامنا المتداخل المُضمَّن للعثور على المتوسط الحسابي AVG للعمود total_visits بحيث تحتوي تعليمة WHERE السابقة على قيم البيانات المناسبة للمقارنة معها. وأخيرًا، نستخدم تعليمة FROM لاسترجاع تلك المعلومات من الجدول guests، وسيكون الاستعلام الكامل كما يلي: mysql> DELETE FROM upgrade_guests mysql> WHERE total_visits > mysql> (SELECT AVG(total_visits) FROM guests); الخرج Query OK, 2 rows affected (0.00 sec) لنتأكّد من حذف هذه السجلات بنجاح من الجدول upgrade_guests، ونستخدم تعليمة ORDER BY لتنظيم النتائج وفق العمود total_visits بترتيب رقمي تصاعدي. ملاحظة: لن يؤدي استخدام تعليمة DELETE لحذف السجلات من جدولنا الجديد إلى حذفها من الجدول الأصلي، حيث يمكننا تشغيل التعليمة SELECT * FROM original_table للتأكد من وجود جميع السجلات الأصلية، حتى إن كانت محذوفة من الجدول الجديد. mysql> SELECT * FROM upgrade_guests ORDER BY total_visits; الخرج +----------+------------+------------+-----------------------+-----------------+--------------+ | guest_id | first_name | last_name | membership_type | membership_cost | total_visits | +----------+------------+------------+-----------------------+-----------------+--------------+ | 3 | Duke | Weaselton | Resident Pass | 72.25 | 4 | | 10 | Octavia | Otterton | Resident Pass | 72.25 | 11 | | 6 | Jenny | Bellwether | Resident Premium Pass | 93.50 | 20 | +----------+------------+------------+-----------------------+-----------------+--------------+ 3 rows in set (0.00 sec) يشير هذا الخرج إلى نجاح تعليمة DELETE والاستعلام المتداخل في حذف قيم البيانات المُحدَّدة، لذا سيحتوي هذا الجدول الآن على معلومات الزوار الثلاثة الذين عدد زياراتهم أقل من متوسط عدد الزيارات، وهو ما يُعَد نقطة انطلاق لموظف حديقة الحيوان للتواصل مع هؤلاء الزوار بشأن الترقية إلى التذاكر المميزة بسعر مخفَّض لتشجيعهم على الذهاب أكثر إلى حديقة الحيوان. الخلاصة شرحنا في هذا المقال الاستعلامات المتداخلة ووضحنا فائدتها في الحصول على نتائج دقيقة لم نكن سنتمكن من الحصول عليها إلا من خلال تشغيل استعلامات منفصلة، وعرضنا أمثلة عملية لاستخدام تعليمات SELECT وINSERT و DELETE مع الاستعلامات المتداخلة لتوفير طريقة أخرى لإدخال البيانات أو حذفها في خطوة واحدة. ترجمة -وبتصرف- للمقال How To Use Nested Queries in SQL لصاحبته Jeanelle Horcasitas. اقرأ أيضًا المقال السابق: استخدام تعليمتي GROUP BY و ORDER BY في SQL الاستعلامات الفرعية Subqueries في SQL مفاهيم نموذج البيانات العلائقية RDM الأساسية بعض الدوال المساعدة في SQL
  6. نشرح في هذا المقال طرق قياس أداء نماذج مكتبة المحولات Transformer التي توفرها منصة Hugging Face باستخدام مكتبات قياس الأداء الخارجية المخصصة لقياس سرعة وتعقيد الذاكرة في هذه النماذج، ونوضح أفضل الممارسات التي علينا اتباعها لقياس وتقييم أداء النماذج عند استخدام هذه المكتبات. ملاحظة: قد تصبح أدوات قياس الأداء الخاصة بمنصة Hugging Face مُهمَلة، ومن المفيد التحقق دومًا من هذه الملاحظات التي تشرح بالتفصيل كيفية قياس أداء نماذج Transformers. قياس أداء نماذج المحولات Transformers يسمح الصنفان PyTorchBenchmark و TensorFlowBenchmark بقياس أداء نماذج Transformers بمرونة كبيرة، حيث تسمح لنا أصناف قياس الأداء بقياس ذروة استخدام الذاكرة Peak Memory Usage، ومعرفة الوقت المطلوب Required Time لكل من الاستدلال Inference والتدريب Training. ملاحظة: تستخدم عملية الاستدلال Inference نموذجًا مدربًا لإجراء تنبؤات أو قرارات جديدة بناءً على بيانات جديدة. ويتطلب الاستدلال إجراء تمرير أمامي واحد أي تمرير البيانات عبر النموذج للحصول على النتيجة دون تعديل أو تحديث للأوزان، أما التدريب فهو يُحسّن أداء النموذج من خلال تعديل الأوزان داخل الشبكة العصبية باستخدام بيانات التدريب، وينفذ تمرير أمامي واحد وتمرير خلفي واحد من أجل حساب الخطأ في المخرجات التي جرى التنبؤ بها ثم يُعدّل الأوزان وفقًا لذلك. يحتاج الصنفان PyTorchBenchmark و TensorFlowBenchmark لتمرير كائن من نوع PyTorchBenchmarkArguments أو TensorFlowBenchmarkArguments لإنشاء نسخ منها، حيث يحتوي كل كائن من هذه الكائنات على جميع عمليات الضبط Configurations ذات الصلة بصنف قياس الأداء المقابل. يوضّح المثال التالي كيفية قياس أداء نموذج BERT من نوع bert-base-cased، في حال استخدام إطار عمل بايتورش Pytorch، فسنكتب ما يلي: >>> from transformers import PyTorchBenchmark, PyTorchBenchmarkArguments >>> args = PyTorchBenchmarkArguments(models=["google-bert/bert-base-uncased"], batch_sizes=[8], sequence_lengths=[8, 32, 128, 512]) >>> benchmark = PyTorchBenchmark(args) في حال استخدام إطار عمل تنسرفلو TensorFlow، فسنكتب ما يلي: >>> from transformers import TensorFlowBenchmark, TensorFlowBenchmarkArguments >>> args = TensorFlowBenchmarkArguments( … models=["google-bert/bert-base-uncased"], batch_sizes=[8], sequence_lengths=[8, 32, 128, 512] … ) >>> benchmark = TensorFlowBenchmark(args) تحتاج أصناف قياس الأداء لثلاثة وسطاء هي: models و batch_sizes و sequence_lengths، حيث يكون الوسيط models مطلوبًا ويمثل قائمة list من معرّفات النماذج المطلوب قياسها من مستودع النماذج. والوسيط batch_sizes اختياري ويستخدم لتحديد حجم الدفعات batch size أثناء قياس الأداء، والوسيط sequence_lengths اختياري لتحديد حجم معرّفات الدخل input_ids التي سيُقاس أداء النموذج عليها. هنالك العديد من المعاملات الأخرى التي يمكننا ضبطها باستخدام أصناف قياس الأداء، لذا ننصح بمطالعة الملفات التالية لإطار عمل بايتورش PyTorch: src/transformers/benchmark/benchmark_args_utils.py src/transformers/benchmark/benchmark_args.py والملف التالي لإطار عمل تنسرفلو Tensorflow: src/transformers/benchmark/benchmark_args_tf.py كما يمكن تشغيل أوامر الصدفة Shell التالية من المجلد الجذر لطباعة قائمة وصفية بجميع المعاملات القابلة للضبط لإطار عمل PyTorch و Tensorflow على التوالي. سنستخدم الأمر التالي في إطار عمل PyTorch: python examples/pytorch/benchmarking/run_benchmark.py --help بعدها، يمكننا إنشاء كائن من صنف معين مخصص لقياس الأداء من خلال استدعاء التابع benchmark.run()‎: >>> results = benchmark.run() >>> print(results) ==================== INFERENCE - SPEED - RESULT ==================== -------------------------------------------------------------------------------- Model Name Batch Size Seq Length Time in s -------------------------------------------------------------------------------- google-bert/bert-base-uncased 8 8 0.006 google-bert/bert-base-uncased 8 32 0.006 google-bert/bert-base-uncased 8 128 0.018 google-bert/bert-base-uncased 8 512 0.088 -------------------------------------------------------------------------------- ==================== INFERENCE - MEMORY - RESULT ==================== -------------------------------------------------------------------------------- Model Name Batch Size Seq Length Memory in MB -------------------------------------------------------------------------------- google-bert/bert-base-uncased 8 8 1227 google-bert/bert-base-uncased 8 32 1281 google-bert/bert-base-uncased 8 128 1307 google-bert/bert-base-uncased 8 512 1539 -------------------------------------------------------------------------------- ==================== ENVIRONMENT INFORMATION ==================== - transformers_version: 2.11.0 - framework: PyTorch - use_torchscript: False - framework_version: 1.4.0 - python_version: 3.6.10 - system: Linux - cpu: x86_64 - architecture: 64bit - date: 2020-06-29 - time: 08:58:43.371351 - fp16: False - use_multiprocessing: True - only_pretrain_model: False - cpu_ram_mb: 32088 - use_gpu: True - num_gpus: 1 - gpu: TITAN RTX - gpu_ram_mb: 24217 - gpu_power_watts: 280.0 - gpu_performance_state: 2 - use_tpu: False وسنستخدم الأمر التالي في إطار عمل TensorFlow: python examples/tensorflow/benchmarking/run_benchmark_tf.py --help بعدها، يمكننا إنشاء كائن من صنف معين لقياس الأداء من خلال استدعاء التابع benchmark.run()‎: >>> results = benchmark.run() >>> print(results) >>> results = benchmark.run() >>> print(results) ==================== INFERENCE - SPEED - RESULT ==================== -------------------------------------------------------------------------------- Model Name Batch Size Seq Length Time in s -------------------------------------------------------------------------------- google-bert/bert-base-uncased 8 8 0.005 google-bert/bert-base-uncased 8 32 0.008 google-bert/bert-base-uncased 8 128 0.022 google-bert/bert-base-uncased 8 512 0.105 -------------------------------------------------------------------------------- ==================== INFERENCE - MEMORY - RESULT ==================== -------------------------------------------------------------------------------- Model Name Batch Size Seq Length Memory in MB -------------------------------------------------------------------------------- google-bert/bert-base-uncased 8 8 1330 google-bert/bert-base-uncased 8 32 1330 google-bert/bert-base-uncased 8 128 1330 google-bert/bert-base-uncased 8 512 1770 -------------------------------------------------------------------------------- ==================== ENVIRONMENT INFORMATION ==================== - transformers_version: 2.11.0 - framework: Tensorflow - use_xla: False - framework_version: 2.2.0 - python_version: 3.6.10 - system: Linux - cpu: x86_64 - architecture: 64bit - date: 2020-06-29 - time: 09:26:35.617317 - fp16: False - use_multiprocessing: True - only_pretrain_model: False - cpu_ram_mb: 32088 - use_gpu: True - num_gpus: 1 - gpu: TITAN RTX - gpu_ram_mb: 24217 - gpu_power_watts: 280.0 - gpu_performance_state: 2 - use_tpu: False يُقاس أداء الوقت المطلوب للاستدلال، ومقدار الذاكرة التي يحتاجها النموذج تلقائيًا دون الحاجة إلى تدخل يدوي من المستخدم. حيث يعرض القسم الأول والثاني من خرج المثال السابق النتيجة المقابلة لوقت الاستدلال وذاكرته، وتُطبَع جميع المعلومات ذات الصلة ببيئة الحوسبة ENVIRONMENT INFORMATIONمثل نوع وحدة معالجة الرسوميات GPU والنظام وإصدارات المكتبة وغير ذلك. يمكننا حفظ هذه المعلومات اختياريًا في ملف ‎.csv عند إضافة الوسيط save_to_csv=True إلى الصنفين PyTorchBenchmarkArguments و TensorFlowBenchmarkArguments على التوالي، حيث يُحفَظ كل قسم في ملف منفصل، كما يمكننا تحديد مسار كل ملف ‎.csv اختياريًا. قياس أداء النموذج BERT باستخدام إعدادات عشوائية يمكننا قياس أداء نموذج model ما باستخدام إعدادات عشوائية بدلاً من قياس أداء نموذج مدرَّب مسبقًا باستخدام معرّف هذا النموذج، على سبيل المثال يمكن قياس أداء نموذج BERT باستخدام المعرف google-bert/bert-base-uncased، لنلقِ نظرة على المثال التالي الذي يستخدم إطار عمل بايتورش PyTorch لقياس أداء نماذج متعددة مستخدمًا إعدادات عشوائية: >>> from transformers import PyTorchBenchmark, PyTorchBenchmarkArguments, BertConfig >>> args = PyTorchBenchmarkArguments( … models=["bert-base", "bert-384-hid", "bert-6-lay"], batch_sizes=[8], sequence_lengths=[8, 32, 128, 512] … ) >>> config_base = BertConfig() >>> config_384_hid = BertConfig(hidden_size=384) >>> config_6_lay = BertConfig(num_hidden_layers=6) >>> benchmark = PyTorchBenchmark(args, configs=[config_base, config_384_hid, config_6_lay]) >>> benchmark.run() ==================== INFERENCE - SPEED - RESULT ==================== -------------------------------------------------------------------------------- Model Name Batch Size Seq Length Time in s -------------------------------------------------------------------------------- bert-base 8 128 0.006 bert-base 8 512 0.006 bert-base 8 128 0.018 bert-base 8 512 0.088 bert-384-hid 8 8 0.006 bert-384-hid 8 32 0.006 bert-384-hid 8 128 0.011 bert-384-hid 8 512 0.054 bert-6-lay 8 8 0.003 bert-6-lay 8 32 0.004 bert-6-lay 8 128 0.009 bert-6-lay 8 512 0.044 -------------------------------------------------------------------------------- ==================== INFERENCE - MEMORY - RESULT ==================== -------------------------------------------------------------------------------- Model Name Batch Size Seq Length Memory in MB -------------------------------------------------------------------------------- bert-base 8 8 1277 bert-base 8 32 1281 bert-base 8 128 1307 bert-base 8 512 1539 bert-384-hid 8 8 1005 bert-384-hid 8 32 1027 bert-384-hid 8 128 1035 bert-384-hid 8 512 1255 bert-6-lay 8 8 1097 bert-6-lay 8 32 1101 bert-6-lay 8 128 1127 bert-6-lay 8 512 1359 -------------------------------------------------------------------------------- ==================== ENVIRONMENT INFORMATION ==================== - transformers_version: 2.11.0 - framework: PyTorch - use_torchscript: False - framework_version: 1.4.0 - python_version: 3.6.10 - system: Linux - cpu: x86_64 - architecture: 64bit - date: 2020-06-29 - time: 09:35:25.143267 - fp16: False - use_multiprocessing: True - only_pretrain_model: False - cpu_ram_mb: 32088 - use_gpu: True - num_gpus: 1 - gpu: TITAN RTX - gpu_ram_mb: 24217 - gpu_power_watts: 280.0 - gpu_performance_state: 2 - use_tpu: False وفي المثال التالي نقيس أداء النماذج باستخدام إطار عمل تنسرفلو TensorFlow مع ضبط عشوائي للنماذج، حيث يمكننا اختيار النماذج المختلفة و إعدادات الضبط المناسبة للاختبار مع توفير الوسائط المناسبة كما يلي: >>> from transformers import TensorFlowBenchmark, TensorFlowBenchmarkArguments, BertConfig >>> args = TensorFlowBenchmarkArguments( … models=["bert-base", "bert-384-hid", "bert-6-lay"], batch_sizes=[8], sequence_lengths=[8, 32, 128, 512] … ) >>> config_base = BertConfig() >>> config_384_hid = BertConfig(hidden_size=384) >>> config_6_lay = BertConfig(num_hidden_layers=6) >>> benchmark = TensorFlowBenchmark(args, configs=[config_base, config_384_hid, config_6_lay]) >>> benchmark.run() ==================== INFERENCE - SPEED - RESULT ==================== -------------------------------------------------------------------------------- Model Name Batch Size Seq Length Time in s -------------------------------------------------------------------------------- bert-base 8 8 0.005 bert-base 8 32 0.008 bert-base 8 128 0.022 bert-base 8 512 0.106 bert-384-hid 8 8 0.005 bert-384-hid 8 32 0.007 bert-384-hid 8 128 0.018 bert-384-hid 8 512 0.064 bert-6-lay 8 8 0.002 bert-6-lay 8 32 0.003 bert-6-lay 8 128 0.0011 bert-6-lay 8 512 0.074 -------------------------------------------------------------------------------- ==================== INFERENCE - MEMORY - RESULT ==================== -------------------------------------------------------------------------------- Model Name Batch Size Seq Length Memory in MB -------------------------------------------------------------------------------- bert-base 8 8 1330 bert-base 8 32 1330 bert-base 8 128 1330 bert-base 8 512 1770 bert-384-hid 8 8 1330 bert-384-hid 8 32 1330 bert-384-hid 8 128 1330 bert-384-hid 8 512 1540 bert-6-lay 8 8 1330 bert-6-lay 8 32 1330 bert-6-lay 8 128 1330 bert-6-lay 8 512 1540 -------------------------------------------------------------------------------- ==================== ENVIRONMENT INFORMATION ==================== - transformers_version: 2.11.0 - framework: Tensorflow - use_xla: False - framework_version: 2.2.0 - python_version: 3.6.10 - system: Linux - cpu: x86_64 - architecture: 64bit - date: 2020-06-29 - time: 09:38:15.487125 - fp16: False - use_multiprocessing: True - only_pretrain_model: False - cpu_ram_mb: 32088 - use_gpu: True - num_gpus: 1 - gpu: TITAN RTX - gpu_ram_mb: 24217 - gpu_power_watts: 280.0 - gpu_performance_state: 2 - use_tpu: False يُقاس الوقت والذاكرة المطلوبة للاستدلال للضبط المخصَّص الخاص بالصنف BertModel هذه المرة، وهذه الميزة مفيدة خاصة عند تحديد الضبط الذي يجب تدريب النموذج عليه. أفضل ممارسات قياس الأداء فيما يلي قائمة موجزة بأفضل الممارسات التي يجب علينا الانتباه لها عند قياس أداء نموذج model: قياس الأداء مدعوم حاليًا لجهاز واحد فقط، ويوصى عند قياسه على وحدة معالجة الرسوميات GPU تحديد الجهاز الذي سيُشغّل الشيفرة البرمجية عليه عن طريق ضبط متغير البيئة CUDA_VISIBLE_DEVICES بقيمة محددة قبل تشغيل هذه الشيفرة علينا ضبط الخيار no_multi_processing على القيمة True للاختبار وتنقيح الأخطاء فقط، ويوصى بتشغيل كل قياس ذاكرة في عملية منفصلة لضمان قياسها بدقة يجب دائمًا ذكر معلومات البيئة عند مشاركة نتائج قياس أداء النموذج، فقد تختلف النتائج بين أجهزة GPU المختلفة وإصدارات المكتبة المختلفة مشاركة قياس الأداء تمكنا من إجراء قياسات أداء لجميع النماذج الأساسية المتاحة لوقت الاستدلال على العديد من الإعدادات المختلفة مثل إطار عمل PyTorch مع استخدام TorchScript وبدونها، وإطار عمل TensorFlow مع استخدام XLA وبدونها. ونُفذّت جميع هذه الاختبارات -باستثناء TensorFlow XLA- عبر وحدات المعالجة المركزية CPU ووحدات معالجة الرسوميات GPU. يمكن مطالعة طريقة قياس أداء Transformers ونتائجها بمزيد من التفصيل. وكما نلاحظ فقد أصبحت مشاركة نتائج قياس الأداء مع المجتمع أسهل من أي وقت مضى باستخدام أدوات قياس الأداء الجديدة مثل نتائج قياس أداء TensorFlow. الخاتمة وصلنا لختام مقالنا الذي شرحنا فيه كيفية استخدام مكتبات قياس أداء نماذج المحولات Transformers بسهولة، مع توفير أكواد يمكن استخدامها لضبط عملية قياس الأداء وتشغيله، وعرضنا النتائج التي حصلنا عليها مثل وقت الاستدلال و استخدام الذاكرة وقارنا بينها، كما وضحنا أفضل الممارسات التي يجب اتباعها لقياس أداء النماذج بكفاءة. ترجمة -وبتصرّف- للقسم Benchmarks من توثيقات Hugging Face. اقرأ أيضًا المقال السابق: تصدير نماذج المحولات Transformers إلى صيغة TorchScript جولة سريعة للبدء مع مكتبة المحوّلات Transformers تثبيت مكتبة المحوّلات Transformers تقييم واختيار نماذج تعلم الآلة
  7. نشرح في هذا المقال كيفية استيراد كائنات ثلاثية الأبعاد موجودة مسبقًا أنشأناها أو نزّلناها من مصدر خارجي إلى داخل محرك ألعاب جودو Godot، ونوضح المزيد حول كيفية استخدام العقد ثلاثية الأبعاد في جودو. استيراد الكائنات ثلاثية الأبعاد في حال كنا على دراية ببرامج النمذجة ثلاثية الأبعاد مثل بلندر Blender، فيمكننا إنشاء نماذجنا الخاصة لاستخدامها في لعبتنا. وفي حال لم نكن كذلك، فهناك العديد من المصادر التي توفر لنا تنزيل الكائنات لأنواع معينة من الألعاب، ومن بينها منصة Kenney التي توفر الكثير من الموارد والملحقات assets المجانية عالية الجودة لصنّاع الألعاب. سنستخدم في أمثلتنا التالية مجموعة ملحقات Platformer Kit للعبة منصات من Kenney، والتي يمكن تنزيلها من هذا الرابط ، حيث تحتوي مجموعة واسعة من الكائنات ثلاثية الأبعاد 3D. وفيما يلي عينة توضّح هذه الملحقات: سنجد بعد التنزيل مجلدًا باسم Models يتضمن مجموعة متنوعة من الصيغ التي يمكن التعامل معها في محرك ألعاب جودو، ولكن صيغة GLTF مفضلة عن الصيغ الأخرى. صيغ الملفات ثلاثية الأبعاد يجب أن نحفظ نماذجنا ثلاثية الأبعاد بصيغة يمكن لجودو استخدامها سواءً أردنا إنشاء نماذجنا الخاصة أو تنزيلها، حيث يدعم جودو صيغ الملفات التالية للنماذج ثلاثية الأبعاد، ولكل منها ميزاته وقيوده: GlTF: صيغ نماذج ثلاثية الأبعاد مدعومة في كل من الإصدارات النصية ‎.gltf والثنائية ‎.glb DAE Collada‎: صيغة أقدم لكنها لا تزال مدعومة OBJ Wavefront‎: صيغة أقدم مدعومة، ولكنها محدودة مقارنة بالخيارات الحديثة FBX: صيغة تجارية لها دعم محدود تُعَد صيغة GlTF هي الصيغة الموصى بها كما وضحنا سابقًا، لكونها تحتوي على معظم الميزات ودعمها جيد في جودو، سنضع المجلد GLB format في الموجود ضمن المجلد Models في مجلد مشروع جودو الخاص بنا ونعيد تسميته إلى platformer_kit. نرجع إلى نافذة محرك ألعاب جودو، يجب أن نرى شريط التقدم كما في الصورة التالية أثناء فحص جودو للمجلد platformer_kit واستيراد جميع الكائنات ضمنه. لننقر نقرًا مزدوجًا على أحد هذه الكائنات وليكن crate.glb في تبويب نظام الملفات FileSystem والذي يمثل صندوق ثلاثي الأبعاد كالتالي: خطوات استيراد كائن ثلاثي الأبعاد وتعديل عقدة الجذر عند النقر على كائن ثلاثي الأبعاد يمكننا رؤية خيارات استيراد هذا الكائن في التبويب استيراد Import بجانب تبويب المشهد Scene مع إمكانية ضبط الخاصية نوع الجذر Root Type وتعديل اسم الجذر Root Name، دعونا نضبط نوع الجذر على RigidBody3D ونطلق عليه اسم Crate للتعبير عن كونه يمثل صندوق ثلاثي الأبعاد، ثم ننقر على زر إعادة الاستيراد Reimport. ننقر بعدها بزر الفأرة الأيمن على crate.glb ونحدّد خيار مشهد موروث جديد New Inherited Scene. أصبح لدينا كائن لعبة كلاسيكي هو الصندوق Crate، وعقدة جذر للمشهد هي RigidBody3D بالاسم Crate كما أردنا تمامًا، لكن سنلاحظ ظهور تحذير يشير بأن هذه العقدة لا تحتوي مكون التصادم Collision الضروري للتفاعل مع الكائنات الأخرى. لذا فإن الخطوة التالية التي علينا القيام بها هي إضافة شكل تصادم إلى الكائن ثلاثي الأبعاد، ويمكننا تطبيق ذلك من خلال إضافة عقدة من نوع CollionShape3D كما نفعل عادة في الألعاب ثنائية الأبعاد 2D، ولكن سننجزها هنا بطريقة أسرع. نحدّد العقدة crate، وسنلاحظ ظهور شريط قوائم في الجزء العلوي من نافذة العرض، ننقر على أيقونة المجسم ونحدّد إنشاء شكل تصادم Create Collision shape ونحدد قيمة الحقل Collision Shape Placement لتكون Sibling أي أن أن شكل التصادم سيضاف كعقدة أخ للعقدة الحالية، وقيمة الحقل Collision Shape Type لتكون Trimmesh أي التصادم سيتم بناءً على المجسم ثلاثي الأبعاد للكائن، وعند الضغط على زر الإنشاء سيضيف جودو تلقائيًا العقدة CollionShape3D مع شكل تصادم يطابق Mesh. انتهينا الآن من إعداد كائن الصندوق Crate، لنحفظ المشهد الخاص به ونتعرّف كيف يمكننا استخدامه في اللعبة أو المشروع. بناء مشهد ثلاثي الأبعاد ننشئ مشهدًا جديدًا باستخدام عقدة الجذر Node3D، وأول ابن سنضيفه هو الأرضية لوضع بعض كائنات الصناديق عليها، لذا نضيف عقدة StaticBody3D ونسميها Ground، ونضيف إليها عقدة ابن من نوع MeshInstance3D. ونحدّد خيار BoxMesh جديدة في الخاصية Mesh ضمن قسم الفاحص. ثم نضبط حجمها على القيم التالية ‎(10,0.1,10)‎ بحيث يكون لدينا أرضية كبيرة، ستظهر الأرضية باللون الأبيض بشكل افتراضي وسيبدو شكلها أفضل إن غيرناها للون البني أو الأخضر، وللقيام بذلك ننتقل للخاصية Material الموجودة في قسم خاصيات Mesh فهي تساعدنا على تحديد مظهر الكائن. سنحدّد الخيار StandardMaterial3D جديدة كقيمة للخاصية Mesh، ثم ننقر عليها لنستعرض قائمة كبيرة من الخاصيات، ما يهمنا هو خاصية اللون Color ضمن قسم Albedo لضبط الأرضية باللون الأخضر الداكن. الآن إذا أضفنا صندوقًا، فسيسقط عبر الأرضية، لذا يجب إعطاؤها شكل تصادم من خلال إضافة عقدة التصادم CollisionShape3D كعقدة ابن للأرضية Ground ونحدد قيمة الخاصية Shape لتكون BoxShape3 جديدة، ثم نضبط حجم صندوق التصادم Size ليكون بنفس حجم Mesh. عند إضافة صندوق إلى المشهد، سيسقط تلقائياً عبر الأرضية Ground نظراً لغياب خصائص التصادم الفيزيائي. لحل هذه المشكلة، نحتاج إلى إضافة عقدة CollisionShape3D كعقدة فرعي للأرضية Ground. بمجرد إضافة العقدة، علينا تعيين خاصية Shape لها باختيار BoxShape3D جديدة ، ثم نضبط أبعاد صندوق التصادم ليتطابق مع حجم الشبكة. بهذه الطريقة، ستصبح الأرضية قادرة على منع الأجسام من السقوط عبرها. ننشئ الآن عددًا من الصناديق في المشهد ونرتبها في كومة تقريبية. ونضيف كاميرا Camera ونضعها في مكان يوفر لنا رؤية جيدة للصناديق، ونشغّل المشهد ونشاهد الصناديق تتدحرج. سنلاحظ أن المشهد مظلم بسبب عدم وجود ضوء فيه، إذ لا يضيف جودو افتراضيًا أي إضاءة أو بيئة إلى المشاهد كما يفعل في نافذة عرض المحرّر، ويُعَد ذلك مناسبًا عندما نريد إعداد الإضاءة الخاصة بنا، ولكن يوجد طريقة مختصرة لنضيء مشهدنا البسيط كما سنوضح في القسم التالي. الإضاءة تتوفر عقد إضاءة متعددة في المشاهد ثلاثية الأبعاد يمكننا استخدامها لإنشاء مجموعة متنوعة من تأثيرات الإضاءة. سنبدأ بالعقدة DirectionalLight3D أولًا، ولكن سنجعل جودو يستخدم العقدة نفسها التي يستخدمها في نافذة المحرر بدلًا من إضافتها يدويًا. نلاحظ وجود أيقونتين في الجزء العلوي فوق نافذة العرض يتحكمان في إضاءة المعاينة Preview Lighting وبيئة المعاينة Preview Environment. إذا نقرنا على النقاط الثلاث بجوارهما، فيمكنك رؤية إعداداتهما. ننقر على زر إضافة الشمس إلى المشهد Add Sun to Scene، وبهذا سيضيف جودو العقدة DirectionalLight3D إلى المشهد مباشرة. ننقر على زر إضافة بيئة إلى المشهد Add Environment to Scene، وسيفعل جودو الشيء نفسه مع معاينة السماء من خلال إضافة عقدة WorldEnvironment. نشغّل المشهد مرة أخرى، وسنتمكن من رؤية الصناديق تتساقط بشكل أفضل. تدوير الكاميرا لنجعل الآن الكاميرا تدور ببطء حول المشهد، لذا نحدّد العقدة الجذر ونضيف عقدة Node3D، والتي ستكون موجودة عند النقطة ‎(0,0,0)‎ ونطلق على هذه العقدة اسم CameraHub. نسحب الكاميرا في شجرة المشهد لجعلها ابنًا لهذه العقدة الجديدة، حيث إذا دارت عقدة CameraHub حول المحور y، فسنسحب الكاميرا معها. نضيف سكربتًا إلى العقدة الجذر ونضع فيه ما يلي: extends Node3D func _process(delta): $CameraHub.rotate_y(0.6 * delta) يعمل السكربت أعلاه على تدوير العقدة CameraHub حول المحور Y بشكل مستمر أثناء اللعبة. وتعتمد سرعة التدوير على الزمن بين الإطارات delta ما يجعل الحركة أكثر سلاسة بغض النظر عن أداء الجهاز. الخلاصة تعلّمنا في هذا المقال كيفية استيراد كائنات ثلاثية الأبعاد من مصادر خارجية وكيفية دمجها في مشهد بسيط في جودو، وتعرفنا أيضًا على الإضاءة والكاميرات المتحركة، وسنوضح في المقال التالي كيفية بناء مشهد أكثر تعقيدًا وتضمين شخصية يتحكم فيها اللاعب. ترجمة -وبتصرّف- للقسم Importing 3D Objects من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: استخدام محرر جودو ثلاثي الأبعاد تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو مطور الألعاب: من هو وما هي مهامه تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو
  8. يمكن لقواعد بيانات لغة الاستعلام البنيوية Structured Query Language -أو SQL اختصارًا- تخزين وإدارة بيانات كثيرة لعدد من الجداول، لذا يجب فهم كيفية فرز البيانات مع مجموعات البيانات الكبيرة لتحليل مجموعات النتائج أو تنظيم البيانات للتقارير أو الاتصالات الخارجية. توجد تعليمتان شائعتان في لغة SQL تساعدان في فرز البيانات هما GROUP BY و ORDER BY، حيث تفرز التعليمة GROUP BY البيانات من خلال تجميعها بناء على العمود أو الأعمدة التي نحددها في الاستعلام، وتُستخدَم هذه التعليمة مع دوال التجميع Aggregate Functions، وتسمح التعليمة ORDER BY بتنظيم مجموعات النتائج أبجديًا أو رقميًا وبترتيب تصاعدي أو تنازلي. سنفرز في هذا المقال نتائج استعلام لغة SQL باستخدام تعليمتي GROUP BY و ORDER BY، وسنتدرب على تنفيذ الدوال التجميعية وتعليمة WHERE في استعلاماتنا لفرز النتائج. مستلزمات العمل يجب توفر حاسوب لتشغيل أحد أنواع أنظمة إدارة قواعد البيانات العلاقية Relational Database Management System -أو RDBMS اختصارًا- التي تستخدم لغة SQL. وفي مقالنا الحالي اختبرنا التعليمات والأمثلة الواردة باستخدام البيئة التالية: خادم عامل على أحدث إصدار من توزيعة أوبنتو Ubuntu مع مستخدم ذو صلاحيات مسؤول مختلف عن المستخدم الجذر وجدار حماية مُفعَّل كما هو موضح في دليل الإعداد الأولي للخادم مع الإصدار 20.04 من أوبنتو نظام MySQL مُثبَّت ومؤمَّن على الخادم كما هو موضّح في مقال كيفية تثبيت MySQL على أوبنتو، وقد نفذنا الخطوات باستخدام مستخدم MySQL مختلف عن المستخدم الجذر وفق الطريقة الموضحة في الخطوة التالية من المقال ملاحظة: تجدر الإشارة إلى أن الكثير من أنظمة إدارة قواعد البيانات العلاقية RDBMS لها تقديماتها الفريدة من لغة SQL، إذ ستعمل الأوامر المُقدّمة في هذا المقال بنجاح مع معظم هذه الأنظمة، ولكن قد توجد بعض الاختلافات في الصيغة أو الناتج عند اختبارها على أنظمة مختلفة عن MySQL. سنحتاج أيضًا إلى قاعدة بيانات تحتوي على جدول يتضمن بيانات تجريبية نموذجية لنتمكّن من التدرب على فرز نتائج البيانات، ولكن إن لم يكن لدينا قاعدة بيانات جاهزة، فيمكن مطالعة القسم التالي لمعرفة كيفية إنشاء قاعدة البيانات والجداول المستخدمة في أمثلتنا. الاتصال بخادم MySQL وإعداد قاعدة بيانات تجريبية نموذجية إذا كانت قاعدة بيانات SQL الخاصة بنا تعمل على خادم بعيد، فعلينا الاتصال بالخادم باستخدام بروتوكول SSH من جهازنا المحلي كما يلي: $ ssh user@your_server_ip ثم نفتح واجهة سطر أوامر خادم MySQL مع وضع اسم حساب مستخدم MySQL الخاص بنا مكان user: $ mysql -u user -p ننشئ قاعدة بيانات بالاسم movieDB: mysql> CREATE DATABASE movieDB; إذا أُنشئِت قاعدة البيانات بنجاح، فسيظهر خرج كما يلي: الخرج Query OK, 1 row affected (0.01 sec) يمكن اختيار قاعدة البيانات movieDB من خلال تنفيذ تعليمة USE التالية: mysql> USE movieDB; الخرج Database changed بعد أن اخترنا قاعدة البيانات، سننشئ جدولًا ضمنها كي يخزّن معلومات عروض دور السينما المحلية، وسيحتوي هذا الجدول على الأعمدة السبعة التالية: theater_id: يخزّن قيمًا من نوع int لكل من قاعات عرض الأفلام، ويمثّل المفتاح الرئيسي Primary Key للجدول date: يستخدم نوع البيانات DATE لتخزين التاريخ المُحدَّد حسب السنة والشهر واليوم لعرض الفيلم time: يمثّل العرض المُجدوَل للفيلم، ويستخدم نوع البيانات TIME للساعات والدقائق والثواني movie_name: يخزّن اسم الفيلم باستخدام نوع البيانات varchar بحد أقصى 40 محرف movie_genre: يستخدم نوع البيانات varchar بحد أقصى 30 محرفًا للاحتفاظ بمعلومات حول النوع الخاص بكل فيلم guest_total: يستخدم نوع البيانات int لعرض العدد الإجمالي لروّاد السينما الذين حضروا عرض الفيلم ticket_cost: يستخدم نوع البيانات decimal ويمثّل تكلفة تذكرة عرض الفيلم لننشئ جدولًا بالاسم movie_theater يحتوي على الأعمدة السابقة من خلال تشغيل أمر CREATE TABLE التالي: mysql> CREATE TABLE movie_theater ( mysql> theater_id int, mysql> date DATE, mysql> time TIME, mysql> movie_name varchar(40), mysql> movie_genre varchar(30), mysql> guest_total int, mysql> ticket_cost decimal(4,2), mysql> PRIMARY KEY (theater_id) mysql> ); ثم ندخل بعض البيانات التجريبية في هذا الجدول الفارغ كما يلي: mysql> INSERT INTO movie_theater mysql> (theater_id, date, time, movie_name, movie_genre, guest_total, ticket_cost) mysql> VALUES mysql> (1, '2022-05-27', '10:00:00', 'Top Gun Maverick', 'Action', 131, 18.00), mysql> (2, '2022-05-27', '10:00:00', 'Downton Abbey A New Era', 'Drama', 90, 18.00), mysql> (3, '2022-05-27', '10:00:00', 'Men', 'Horror', 100, 18.00), mysql> (4, '2022-05-27', '10:00:00', 'The Bad Guys', 'Animation', 83, 18.00), mysql> (5, '2022-05-28', '09:00:00', 'Top Gun Maverick', 'Action', 112, 8.00), mysql> (6, '2022-05-28', '09:00:00', 'Downton Abbey A New Era', 'Drama', 137, 8.00), mysql> (7, '2022-05-28', '09:00:00', 'Men', 'Horror', 25, 8.00), mysql> (8, '2022-05-28', '09:00:00', 'The Bad Guys', 'Animation', 142, 8.00), mysql> (9, '2022-05-28', '05:00:00', 'Top Gun Maverick', 'Action', 150, 13.00), mysql> (10, '2022-05-28', '05:00:00', 'Downton Abbey A New Era', 'Drama', 118, 13.00), mysql> (11, '2022-05-28', '05:00:00', 'Men', 'Horror', 88, 13.00), mysql> (12, '2022-05-28', '05:00:00', 'The Bad Guys', 'Animation', 130, 13.00); الخرج Query OK, 12 rows affected (0.00 sec) Records: 12 Duplicates: 0 Warnings: 0 أصبحت الجداول جاهزة ومعبأة بالبيانات، ونحن جاهزون الآن لبدء فرز نتائج الاستعلام باستخدام تعليمات لغة SQL. استخدام التعليمة GROUP BY وظيفة التعليمة GROUP BY هي تجميع السجلات ذات القيم المشتركة، وتُستخدَم دائمًا مع دالة تجميعية، حيث تلخّص الدالة التجميعية المعلومات وتعيد نتيجة واحدة، فمثلًا يمكننا الاستعلام عن العدد الإجمالي أو مجموع قيم العمود وستنتج قيمة واحدة. ويمكن باستخدام التعليمة GROUP BY تطبيق الدالة التجميعية للحصول على قيمة نتيجة واحدة لكل مجموعة نريدها. تُعَد التعليمة GROUP BY مفيدة لإعادة النتائج المرغوبة المتعددة مرتبة حسب مجموعة محددة أو عدة مجموعات بدلًا من عمود واحد فقط. ويجب أن تأتي التعليمة GROUP BY دائمًا بعد التعليمة FROM والتعليمة WHERE إذا اخترنا استخدام إحداهما. فيما يلي مثال يوضّح بنية الاستعلام باستخدام التعليمة GROUP BY والدالة التجميعية: mysql> GROUP BY syntax mysql> SELECT column_1, AGGREGATE_FUNCTION(column_2) FROM table GROUP BY mysql> column_1; لنوضّح كيفية استخدام التعليمة GROUP BY من خلال مثال عملي، لنفترض أننا نقود حملة لتسويق عدة إصدارات من الأفلام، ونريد تقييم نجاح جهودنا التسويقية من خلال الطلب من دار السينما المحلية مشاركة البيانات التي جمعتها من روّاد السينما يومي الجمعة والسبت. سنبدأ بمطالعة البيانات من خلال تشغيل التعليمة SELECT مع الرمز * لتحديد جميع الأعمدة في الجدول movie_theater: mysql> SELECT * FROM movie_theater; الخرج +------------+------------+----------+-------------------------+-------------+-------------+-------------+ | theater_id | date | time | movie_name | movie_genre | guest_total | ticket_cost | +------------+------------+----------+-------------------------+-------------+-------------+-------------+ | 1 | 2022-05-27 | 10:00:00 | Top Gun Maverick | Action | 131 | 18.00 | | 2 | 2022-05-27 | 10:00:00 | Downton Abbey A New Era | Drama | 90 | 18.00 | | 3 | 2022-05-27 | 10:00:00 | Men | Horror | 100 | 18.00 | | 4 | 2022-05-27 | 10:00:00 | The Bad Guys | Animation | 83 | 18.00 | | 5 | 2022-05-28 | 09:00:00 | Top Gun Maverick | Action | 112 | 8.00 | | 6 | 2022-05-28 | 09:00:00 | Downton Abbey A New Era | Drama | 137 | 8.00 | | 7 | 2022-05-28 | 09:00:00 | Men | Horror | 25 | 8.00 | | 8 | 2022-05-28 | 09:00:00 | The Bad Guys | Animation | 142 | 8.00 | | 9 | 2022-05-28 | 05:00:00 | Top Gun Maverick | Action | 150 | 13.00 | | 10 | 2022-05-28 | 05:00:00 | Downton Abbey A New Era | Drama | 118 | 13.00 | | 11 | 2022-05-28 | 05:00:00 | Men | Horror | 88 | 13.00 | | 12 | 2022-05-28 | 05:00:00 | The Bad Guys | Animation | 130 | 13.00 | +------------+------------+----------+-------------------------+-------------+-------------+-------------+ 12 rows in set (0.00 sec) هذه البيانات مفيدة، ولكننا نريد إجراء تقييم أعمق وفرز النتائج لبعض الأعمدة المُحدَّدة، فقد نكون مهتمين بمعرفة مدى تقبّل رواد السينما لهذه الأفلام ذات الأنواع المختلفة مثل معرفة متوسط عدد الأشخاص الذين شاهدوا كل نوع من الأفلام. سنستخدم التعليمة SELECT لاسترجاع أنواع الأفلام المختلفة من العمود movie_genre، ثم نطبّق الدالة التجميعية AVG على العمود guest_total، ونستخدم التعليمة AS لإنشاء اسم بديل للعمود بالاسم average، ونضمّن التعليمة GROUP BY لتجميع النتائج وفق العمود movie_genre، إذ سيوفّر تجميعها بهذه الطريقة متوسط النتائج لكل نوع من الأفلام كما يلي: mysql> SELECT movie_genre, AVG(guest_total) AS average mysql> FROM movie_theater mysql> GROUP BY movie_genre; الخرج +-------------+----------+ | movie_genre | average | +-------------+----------+ | Action | 131.0000 | | Drama | 115.0000 | | Horror | 71.0000 | | Animation | 118.3333 | +-------------+----------+ 4 rows in set (0.00 sec) يعطي الخرج السابق المتوسطات الأربعة لكل نوع من الأفلام ضمن المجموعة movie_genre، حيث جذبت أفلام الحركة Action أعلى متوسط لعدد المشاهدين لكل عرض بناء على هذه المعلومات. لنفترض أننا نريد قياس الإيرادات على مدى يومين منفصلين، حيث يعيد الاستعلام التالي القيم من العمود date والقيم التي تعيدها الدالة التجميعية SUM التي تحتوي على معادلة رياضية بين قوسين لضرب عدد إجمالي روّاد السينما في تكلفة التذكرة، والتي نمثّلها على النحو التالي: SUM(guest_total * ticket_cost) يتضمن الاستعلام التالي التعليمةَ AS لتوفير الاسم البديل total_revenue للعمود الذي تعيده الدالة التجميعية، ونكمل الاستعلام باستخدام التعليمة GROUP BY لتجميع نتائج الاستعلام وفق العمود date: mysql> SELECT date, SUM(guest_total * ticket_cost) mysql> AS total_revenue mysql> FROM movie_theater mysql> GROUP BY date; الخرج +------------+---------------+ | date | total_revenue | +------------+---------------+ | 2022-05-27 | 7272.00 | | 2022-05-28 | 9646.00 | +------------+---------------+ 2 rows in set (0.00 sec) استخدمنا هنا التعليمة GROUP BY لتجميع العمود date، لذا يمثّل الخرج نتائج إجمالي الإيرادات في مبيعات التذاكر لكل يوم، والتي هي 7272 دولارًا أمريكيًا ليوم الجمعة 27 من الشهر الخامس، و 9646 دولارًا أمريكيًا ليوم السبت 28 من الشهر الخامس. لنفترض الآن أننا نريد التركيز على فيلم واحد وتحليله وليكن فيلم الرسوم المتحركة The Bad Guys حيث نريد معرفة كيفية تأثير التوقيت والأسعار على اختيار الأسرة لمشاهدة فيلم رسوم متحركة. سنستخدم في هذا الاستعلام الدالة التجميعية MAX لاسترجاع الحد الأقصى لتكلفة التذكرة ticket_cost، مع التأكّد من تضمين التعليمة AS لإنشاء الاسم البديل للعمود وهو price_data، ثم نستخدم التعليمة WHERE لتضييق نطاق النتائج وفق العمود movie_name للحصول على اسم الفيلم فقط، ونستخدم التعليمة AND أيضًا لتحديد أوقات الأفلام الأكثر شيوعًا اعتمادًا على أرقام العمود guest_total التي كانت أكثر من 100 باستخدام معامل المقارنة ‎>‎، ثم نكمل الاستعلام باستخدام التعليمة GROUP BY ونجمّع النتائج وفق العمود time: mysql> SELECT time, MAX(ticket_cost) AS price_data mysql> FROM movie_theater mysql> WHERE movie_name = "The Bad Guys" mysql> AND guest_total > 100 mysql> GROUP BY time; الخرج +----------+------------+ | time | price_data | +----------+------------+ | 09:00:00 | 8.00 | | 05:00:00 | 13.00 | +----------+------------+ 2 rows in set (0.00 sec) نلاحظ وفقًا لهذا الخرج حضور عدد أكبر من روّاد السينما لها الفيلم في وقت مبكر من العرض الصباحي في الساعة 9:00 صباحًا، حين كان سعره أقل تكلفة وهو 8.00 دولارات أمريكية لكل تذكرة، ولكن تظهر هذه النتائج أيضًا أن روّاد الفيلم دفعوا سعر التذكرة الأعلى وهو 13.00 دولارًا أمريكيًا في الساعة 5:00 مساءً، مما يشير إلى أن العائلات تفضل العروض التي تكون وقت مبكر من المساء وستدفع سعرًا أكبر قليلًا مقابل التذكرة. يمكن لهذه المعلومات أن تفيد مدير دار السينما وتشير له لأن فتح المزيد من الفترات في المساء الباكر يمكن أن يزيد عدد العائلات التي تختار الحضور بناء على الوقت المفضل والسعر. تُستخدَم التعليمة GROUP BY دائمًا مع دالة تجميعية، ولكن قد تكون هناك استثناءات، لذا إذا أردنا تجميع النتائج دون استخدام دالة تجميعية، فيمكن استخدام التعليمة DISTINCT لتحقيق النتيجة نفسها. حيث تزيل التعليمة DISTINCT أيّ تكرارات في مجموعة النتائج من خلال إعادة القيم الفريدة في العمود، ولا يمكن استخدامها إلّا مع التعليمة SELECT، فمثلًا إذا أردنا تجميع جميع الأفلام وفق الاسم، فيمكننا ذلك باستخدام الاستعلام التالي: mysql> SELECT DISTINCT movie_name FROM movie_theater; الخرج +-------------------------+ | movie_name | +-------------------------+ | Top Gun Maverick | | Downton Abbey A New Era | | Men | | The Bad Guys | +-------------------------+ 4 rows in set (0.00 sec) هناك نسخ مكررة لأسماء الأفلام نظرًا لوجود عروض متعددة كما وضحنا عند عرض كافة البيانات الموجودة في الجدول، لذا أزالت التعليمة DISTINCT تلك التكرارات وجمّعت لنا القيم الفريدة بفعالية ضمن عمود واحد هو movie_name، ويُعَد هذا مطابقًا فعليًا للاستعلام التالي الذي يتضمن التعليمة GROUP BY: mysql> SELECT movie_name FROM movie_theater GROUP BY movie_name; بعد أن شرحنا بالتفصيل طريقة استخدام التعليمة GROUP BY مع الدوال التجميعية، لنتعلّم الآن كيفية فرز نتائج الاستعلام باستخدام التعليمة ORDER BY. استخدام التعليمة ORDER BY تتمثل وظيفة التعليمة ORDER BY في فرز النتائج بترتيب تصاعدي أو تنازلي اعتمادًا على العمود أو الأعمدة التي نحدّدها في الاستعلام، حيث ستنظّم هذه التعليمة البيانات بترتيب أبجدي أو رقمي اعتمادًا على نوع البيانات التي يخزّنها العمود الذي نحدّده بعدها. تفرز التعليمة ORDER BY النتائج بترتيب تصاعدي افتراضيًا، ولكن إذا أردنا استخدام الترتيب التنازلي، فيجب تضمين الكلمة المفتاحية DESC في استعلامنا. يمكن أيضًا استخدام التعليمة ORDER BY مع التعليمة GROUP BY، ولكن يجب أن تأتي بعدها لكي تعمل بنجاح، ويجب أن تأتي التعليمة ORDER BY بعد التعليمة FROM والتعليمة WHERE كما هو الحال مع التعليمة GROUP BY. تكون الصيغة العامة لاستخدام التعليمة ORDER BY كما يلي: mysql> SELECT column_1, column_2 FROM table ORDER BY column_1; لنستخدم البيانات التجريبية النموذجية الخاصة بالسينما ونتدرب على فرز النتائج باستخدام التعليمة ORDER BY، ولنبدأ بالاستعلام التالي الذي يسترجع القيم من العمود guest_total وينظّم تلك القيم العددية باستخدام التعليمة ORDER BY: mysql> SELECT guest_total FROM movie_theater mysql> ORDER BY guest_total; الخرج +-------------+ | guest_total | +-------------+ | 25 | | 83 | | 88 | | 90 | | 100 | | 112 | | 118 | | 130 | | 131 | | 137 | | 142 | | 150 | +-------------+ 12 rows in set (0.00 sec) حدّد الاستعلام السابق عمودًا يحتوي على قيم عددية، لذا فقد نظّمت التعليمة ORDER BY النتائج حسب الترتيب الرقمي والتصاعدي بدءًا من القيمة 25 ضمن العمود guest_total. إذا أردنا ترتيب العمود تنازليًا، فيمكننا إضافة الكلمة المفتاحية DESC في نهاية الاستعلام، وإذا أردنا ترتيب البيانات حسب قيم المحارف ضمن العمود movie_name، فيمكن تحديد ذلك في الاستعلام الذي نكتبه. لنطبّق هذا النوع من الاستعلامات باستخدام التعليمة ORDER BY لترتيب العمود movie_name حسب قيم المحارف تنازليًا، ونفرز النتائج أيضًا من خلال تضمين التعليمة WHERE لاسترجاع البيانات الخاصة بالأفلام المعروضة في الساعة 10:00 مساء من العمود time: mysql> SELECT movie_name FROM movie_theater mysql> WHERE time = '10:00:00' mysql> ORDER BY movie_name DESC; الخرج +-------------------------+ | movie_name | +-------------------------+ | Top Gun Maverick | | The Bad Guys | | Men | | Downton Abbey A New Era | +-------------------------+ 4 rows in set (0.01 sec) توضّح هذه المجموعة من النتائج عروض الأفلام الأربعة المختلفة في الساعة 10:00 مساءً بترتيب أبجدي تنازليًا بدءًا من الفيلم Top Gun Maverick إلى الفيلم Downtown Abbey A New Era. لندمج الآن تعليمتي ORDER BY و GROUP BY مع الدالة التجميعية SUM لتوليد نتائج حول إجمالي الإيرادات المُستلَمة لكل فيلم، ولكن لنفترض أن دار السينما أخطأت في حساب إجمالي روّاد السينما ونسيت تضمين الحفلات الخاصة المدفوعة مسبقًا وحجز التذاكر لمجموعة مكونة من 12 شخصًا في كل عرض. سنستخدم في هذا الاستعلام الدالة SUM ونضمّن 12 زائرًا إضافيًا لكل فيلم معروض من خلال تطبيق معامل الجمع + ثم نجمع القيمة 12 مع العمود guest_total، ونتأكّد من وضع ذلك بين قوسين، ثم نضرب المجموع بالعمود ticket_cost، ونكمل المعادلة الرياضية بإغلاق القوسين في النهاية. نضيف التعليمة AS لإنشاء الاسم البديل للعمود الجديد بعنوان total_revenue، ثم نستخدم التعليمة GROUP BY لتجميع نتائج العمود total_revenue لكل فيلم بناءً على البيانات المسترجَعة من العمود movie_name. وأخيرًا، نستخدم التعليمة ORDER BY لتنظيم النتائج ضمن العمود الجديد total_revenue بترتيب تصاعدي: mysql> SELECT movie_name, SUM((guest_total + 12) * ticket_cost) mysql> AS total_revenue mysql> FROM movie_theater mysql> GROUP BY movie_name mysql> ORDER BY total_revenue; الخرج +-------------------------+---------------+ | movie_name | total_revenue | +-------------------------+---------------+ | Men | 3612.00 | | Downton Abbey A New Era | 4718.00 | | The Bad Guys | 4788.00 | | Top Gun Maverick | 5672.00 | +-------------------------+---------------+ 4 rows in set (0.00 sec) تعطينا هذه المجموعة من النتائج إجمالي الإيرادات لكل فيلم مع مبيعات تذاكر الزوار الإضافية التي يبلغ عددها 12 تذكرة، وتنظّم إجمالي مبيعات التذاكر بترتيب تصاعدي من الأقل إلى الأعلى، وبالتالي حصل الفيلم Top Gun Maverick على أكبر عدد من مبيعات التذاكر، بينما حصل فيلم Men على المبيعات الأقل، وكانت أفلام The Bad Guys و Downton Abbey A New Era متقاربة جدًا في إجمالي مبيعات التذاكر. إلى هنا نكون قد وصلنا لنهاية هذا القسم الذي تعرفنا فيه على طرق مختلفة لتطبيق تعليمة ORDER BY وكيفية تحديد الترتيب الذي نفضّله مثل الترتيب التصاعدي والتنازلي لكل من قيم البيانات المحرفية والرقمية، وتعلّمنا أيضًا كيفية تضمين التعليمة WHERE لتضييق نطاق النتائج، وأجرينا استعلامًا باستخدام كلّ من تعليمات GROUP BY و ORDER BY مع دالة تجميعية ومعادلة رياضية. الخلاصة يُعَد فهم كيفية استخدام تعليمتي GROUP BY و ORDER BY أمرًا مهمًا لفرز النتائج والبيانات، حيث يمكننا من خلالهما تنظيم نتائج متعددة ضمن مجموعة واحدة أو تنظيم أحد الأعمدة بترتيب أبجدي وتنازلي أو تطبيق الأمرين معًا في وقت واحد، وتعلّمنا أيضًا طرقًا أخرى لفرز النتائج باستخدام التعليمة WHERE. ولمعرفة المزيد ننصح بمطالعة مقال كيفية استخدام محارف البدل Wildcards في لغة SQL للتدرب على ترشيح النتائج باستخدام التعليمة LIKE، كما ننصح بالاطلاع على سلسلة تعلم SQL في أكاديمية حسوب للمزيد حول كيفية التعامل مع لغة SQL. ترجمة -وبتصرف- للمقال How To Use GROUP BY and ORDER BY in SQL لصاحبته Jeanelle Horcasitas. اقرأ أيضًا المقال السابق: استخدام العروض Views في لغة SQL دوال التعامل مع البيانات في SQL التجميع والترتيب في SQL كيفية التعامل مع التواريخ والأوقات في SQL
  9. إذا كنا نطور نماذج ذكاء اصطناعي ونرغب في تحسين أدائها أو نشرها في بيئات مختلفة، فإن TorchScript أداة قوية توفر لنا طريقة لإنشاء نماذج قابلة للتسلسل والتحسين من شيفرة باي تورش PyTorch البرمجية وفقًا لتوثيق TorchScript. لا تزال التجارب جارية لاختبار استخدام TorchScript مع النماذج التي يكون حجم مدخلاتها متغيرًا. وفي الإصدارات القادمة، سيجري تقديم أمثلة برمجية أكثر، وتحسين مرونة التنفيذ، بالإضافة إلى مقارنة أداء الكود المكتوب بلغة بايثون Python مع الكود المحوَّل إلى TorchScript لمعرفة الفرق في السرعة والكفاءة. توجد وحدتان من وحدات باي تورش PyTorch هما الوحدة JIT لترجمة نموذج باي تورش إلى كود يمكن تنفيذه مباشرة على الآلة، والوحدة TRACE لتسريع الكود فهما تسمحان للمطورين بتصدير نماذجهم لإعادة استخدامها في برامج أخرى مثل برامج C++‎ التي تركز على الفعالية وتحسين الأداء والموارد المستخدمة. سنوفر فيما يلي واجهة تتيح لنا تصدير نماذج مكتبة المحوِّلات Transformers إلى صيغة TorchScript حتى نتمكّن من إعادة استخدامها في بيئة مختلفة عن برامج بايثون Python المستندة إلى إطار عمل باي تورش PyTorch، وسنوضّح كيفية تصدير واستخدام النماذج باستخدام صيغة TorchScript. يتطلب تصدير النموذج شيئين هما: إنشاء نسخة خاصة من النموذج متوافقة مع TorchScript باستخدام الراية torchscript تمرير بيانات تجريبية أو دخل وهمي إلى النموذج ليتمكن من تتبع العمليات الحسابية وتسجيلها بشكل صحيح يتضمن هذان الأمران الضروريان عدة أمور يجب على المطورين توخي الحذر بشأنها كما سنوضّح فيما يلي. راية TorchScript والأوزان المرتبطة Tied Weights لا يدعم TorchScript تصدير النماذج التي تحتوي على أوزان مرتبطة، لذا يجب فصل هذه الأوزان ونسخها مسبقًا قبل التصدير. لذا تُعَد الراية torchscript ضرورية لأن معظم النماذج اللغوية في مكتبة المحولات Transformers لها أوزان تربط بين طبقة التضمين Embedding وطبقة فك الترميز Decoding. وبما أن صيغة TorchScript لا تسمح بتصدير النماذج التي لها أوزان مرتبطة، لذا من الضروري فك الارتباط ونسخ الأوزان مسبقًا. تُفصَل طبقة التضمين Embedding عن طبقة فك الترميز Decoding للنماذج التي تنسخها الراية torchscript، مما يعني أنه لا ينبغي تدريبها لاحقًا، إذ سيؤدي التدريب إلى عدم مزامنة الطبقتين، وسيعطي نتائج غير متوقعة. لا ينطبق ذلك على النماذج التي لا تحتوي على رأس نموذج لغوي Language Model Head، فهذه النماذج لا تحتوي على أوزان مرتبطة، وبالتالي يمكن تصديرها بأمان بدون الراية torchscript. الدخل الوهمي والأطوال المعيارية عند تصدير نموذج باستخدام TorchScript، يجب تنفيذ تمرير أمامي على دخل وهمي. هذا الدخل هو بيانات افتراضية تُمرَّر عبر النموذج لمساعدة باي تورش PyTorch على تسجيل العمليات التي يجري تنفيذها على كل موتر Tensor أثناء انتقال القيم بين الطبقات. لماذا نحتاج إلى الدخل الوهمي يعتمد PyTorch على هذه العمليات المسجلة لإنشاء تعقّب Trace للنموذج، وهو ما يسمح بتحويله إلى صيغة TorchScript. لكن هذا التعقب يكون مرتبطًا بأبعاد الدخل الوهمي المستخدمة أثناء التصدير، مما يعني أن النموذج الناتج لن يدعم أطوال تسلسل أو أحجام دفعات مختلفة عن التي استُخدمت عند التتبع. إذا حاولنا تمرير بيانات بحجم مختلف عن الحجم المستخدم أثناء التصدير، فسيظهر خطأ، لأن النموذج لم يجري تعقّبه إلا لأبعاد محددة، ولا يستطيع التعامل مع أطوال مختلفة تلقائيًا، سيكون الخطأ على النحو التالي: `The expanded size of the tensor (3) must match the existing size (7) at non-singleton dimension 2` يُوصَى بتعقّب النموذج باستخدام حجم دخل وهمي لا يقل عن أكبر حجم دخل للنموذج أثناء الاستدلال Inference. يمكن أن يساعد الحشو Padding في ملء القيم المفقودة، ولكن ستكون أبعاد المصفوفة كبيرة أيضًا بسبب تعقّب النموذج باستخدام حجم دخل أكبر، مما يؤدي لإجراء مزيد من العمليات الحسابية. انتبه إلى العدد الإجمالي للعمليات التي تجري على كل دخل وراقب الأداء عند تصدير نماذج ذات أطوال تسلسلٍ مختلفة. استخدام صيغة TorchScript في بايثون Python يوضّح هذا القسم كيفية حفظ النماذج وتحميلها وكيفية استخدام التعقّب للاستدلال. حفظ النموذج يمكن تصدير نموذج BertModel باستخدام صيغة TorchScript من خلال إنشاء نسخة BertModel من الصنف BertConfig ثم حفظها على القرص الصلب باسم الملف traced_bert.pt كما يلي: from transformers import BertModel, BertTokenizer, BertConfig import torch enc = BertTokenizer.from_pretrained("google-bert/bert-base-uncased") # ‫ترميز Tokenizing النص المُدخل text = "[CLS] Who was Jim Henson ? [SEP] Jim Henson was a puppeteer [SEP]" tokenized_text = enc.tokenize(text) # تقنيع‫ Masking أحد رموز Tokens الدخل masked_index = 8 tokenized_text[masked_index] = "[MASK]" indexed_tokens = enc.convert_tokens_to_ids(tokenized_text) segments_ids = [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1] # إنشاء دخل وهمي tokens_tensor = torch.tensor([indexed_tokens]) segments_tensors = torch.tensor([segments_ids]) dummy_input = [tokens_tensor, segments_tensors] # ‫تهيئة النموذج باستخدام راية torchscript # ضبط الراية على القيمة‫ True بالرغم من أن ذلك غير ضروري لأن هذا النموذج لا يحتوي على رأس النموذج اللغوي LM. config = BertConfig( vocab_size_or_config_json_file=32000, hidden_size=768, num_hidden_layers=12, num_attention_heads=12, intermediate_size=3072, torchscript=True, ) # إنشاء نسخة من النموذج model = BertModel(config) # يجب أن يكون النموذج في وضع التقييم model.eval() # إذا أردنا إنشاء نسخة من النموذج باستخدام‫ *from_pretrained*، فيمكن أيضًا ضبط راية TorchScript بسهولة model = BertModel.from_pretrained("google-bert/bert-base-uncased", torchscript=True) # إنشاء التعقّب traced_model = torch.jit.trace(model, [tokens_tensor, segments_tensors]) torch.jit.save(traced_model, "traced_bert.pt") تحميل النموذج يمكننا الآن تحميل النموذج BertModel الذي حفظناه بالاسم traced_bert.pt من القرص الصلب واستخدامه مع الدخل الوهمي dummy_input المُهيَّأ مسبقًا كما يلي: loaded_model = torch.jit.load("traced_bert.pt") loaded_model.eval() all_encoder_layers, pooled_output = loaded_model(*dummy_input) استخدام نموذج مُتعقَّب للاستدلال سنستخدم النموذج المتعقَّب للاستدلال باستخدام التابع السحري __call__ الخاص به كما يلي: traced_model(tokens_tensor, segments_tensors) نشر نماذج TorchScript من منصة Hugging Face على خدمة AWS قدمت خدمة AWS عائلة من نسخ Amazon EC2 Inf1 لاستدلال التعلم الآلي منخفض التكلفة وعالي الأداء في السحابة، حيث تعمل نسخ Inf1 باستخدام شريحة AWS Inferentia، والتي هي مسرّع للعتاد المُخصّص ومتخصصة في أحمال استدلال التعلم العميق. و AWS Inferentia هي أداة SDK لشريحة Inferentia تدعم تعقّب نماذج المحوِّلات Transformers وتحسينها للنشر على Inf1، حيث توفر أداة Neuron SDK ما يلي: واجهة برمجة تطبيقات API سهلة الاستخدام مع تغيير سطر واحد من الشيفرة البرمجية لتعقّب نموذج TorchScript وتحسينه للاستدلال في السحابة تحسينات الأداء الجاهزة لتحسين تكلفة الأداء دعم نماذج المحولات Transformers من منصة Hugging Face المبنية باستخدام إطار عمل PyTorch أو TensorFlow النتائج تعمل نماذج المحولات Transformers المستندة إلى بنية BERT أي تمثيلات المشفّر ثنائية الاتجاه من مكتبة المحوِّلات أو Bidirectional Encoder Representations from Transformers أو أنواعها المختلفة مثل distilBERT و roBERTa بنجاح على Inf1 للمهام غير التوليدية مثل الإجابة على الأسئلة الاستخراجية وتصنيف التسلسلات وتصنيف الرموز، ولكن يمكن أيضًا تكييف مهام توليد النصوص لتشغيلها على Inf1. ويمكن العثور على مزيد من المعلومات حول النماذج التي يمكن تحويلها على Inferentia في قسم ملاءمة بنية النموذج في توثيق Neuron. الاعتماديات Dependencies يتطلب استخدام AWS Neuron لتحويل النماذج بيئةَ Neuron SDK التي تكون مضبوطة مسبقًا على AWS Deep Learning AMI. تحويل النموذج لأداة AWS Neuron سنحوّل النموذج لأداة AWS NEURON باستخدام الشيفرة البرمجية نفسها من قسم استخدام صيغة TorchScript في بايثون التي شرحناها سابقًا في هذا المقال لتعقّب النموذج BertModel، ونستورد امتداد إطار عمل torch.neuron للوصول إلى مكونات Neuron SDK من خلال واجهة برمجة تطبيقات بايثون كما يلي: from transformers import BertModel, BertTokenizer, BertConfig import torch import torch.neuron وكل ما علينا فعله هو تعديل السطر التالي: - torch.jit.trace(model, [tokens_tensor, segments_tensors]) + torch.neuron.trace(model, [token_tensor, segments_tensors]) هذا يتيح لأداة Neuron SDK تعقّب النموذج وتحسينه لنسخ Inf1. وأخيرًا ننصح بمطالعة توثيق AWS NeuronSDK لمعرفة المزيد عن ميزات AWS Neuron SDK وأدواتها وبرامجها التعليمية وتحديثاتها الأخيرة. الخلاصة شرحنا في هذا المقال كيفية تصدير نماذج المحولات Transformers إلى صيغة TorchScript لاستخدامها في بيئات غير Python. يتطلب الأمر تطبيق راية torchscript على النموذج وفصل الأوزان المرتبطة بين الطبقات. كما وضحنا طريقة تمرير دخل وهمي لتسجيل العمليات الحسابية وتحويل النموذج إلى صيغة قابلة للتسلسل. وعرضنا كيفية استخدام هذا النموذج في Python بعد تصديره وحفظه. وأخيرًا شرحنا كيفية تحسين النماذج واستخدامها على خدمات AWS باستخدام Neuron SDK. ترجمة -وبتصرّف- للقسم Export to TorchScript من توثيقات Hugging Face. اقرأ أيضًا نظرة عامة على الصنف Trainer في مكتبة المحولات Transformers جولة سريعة للبدء مع مكتبة المحوّلات Transformers تثبيت مكتبة المحوّلات Transformers بناء نماذج مخصصة باستخدام مكتبة Transformers في Hugging Face
  10. سنلقي نظرة في هذا المقال على كيفية البدء في استخدام محرّر جودو Godot ثلاثي الأبعاد، حيث ستتعلم كيفية التنقل في هذا المحرّر، وكيفية إنشاء الكائنات ثلاثية الأبعاد ومعالجتها، وكيفية العمل مع بعض العقد الأساسية ثلاثية الأبعاد في جودو مثل الكاميرات والإضاءة. قد يكون تطوير الألعاب ثلاثية الأبعاد أكثر تعقيدًا من تطوير الألعاب ثنائية الأبعاد، حيث يمكن تطبيق العديد من المبادئ نفسها مثل العمل مع العقد، وكتابة السكربتات والتعامل مع المنطق البرمجي، ولكن يتطلب التطوير ثلاثي الأبعاد عدد من الاعتبارات الأخرى، لذا يُفضَّل تعلم تطوير الألعاب ثنائية الأبعاد للمبتدئين، والانتقال إلى تعلم تطوير الألعاب ثلاثية الأبعاد بعد امتلاك الفهم الجيد لمبادئ تطوير الألعاب، يفترض هذا المقال وجود معرفة جيدة بإنشاء لعبة متكاملة ثنائية الأبعاد في جودو Godot 2D على الأقل. البدء في تطوير الألعاب ثلاثية الأبعاد تتمثل إحدى نقاط قوة جودو في قدرته على التعامل مع الألعاب ثنائية الأبعاد وثلاثية الأبعاد بكفاءة. يمكننا تطبيق الكثير مما نتعلّمته من العمل على المشاريع ثنائية الأبعاد كالعقد nodes والمشاهد scenes والإشارات signals وما إلى ذلك على الألعاب ثلاثية الأبعاد، ولكن سنجد بعض التعقيدات والمميزات الجديدة في التطوير ثلاثي الأبعاد، وسنوضح في فقراتنا التالية أهم الميزات الإضافية المتوفرة في نافذة محرر جودو ثلاثي الأبعاد. التوجيه Orienting في الفضاء ثلاثي الأبعاد عندما نفتح مشروعًا جديدًا في جودو لأول مرة فسنرى عرض المشروع ثلاثي الأبعاد وسنلاحظ وجود أدوات وخصائص مُعدة خصيصًا للتطوير ثلاثي الأبعاد 3D كما في الصورة التالي: أول شيء سنلاحظه هو الخطوط الثلاثة الملونة في المنتصف، والتي هي المحور X باللون الأحمر، والمحور Y باللون الأخضر، والمحور Z باللون الأزرق، وتسمى النقطة التي تلتقي فيها هذه المحاور بنقطة الأصل Origin والتي لها الإحداثيات ‎(0,0,0)‎. وسنلاحظ أن مخطط الألوان هذا يُطبَّق أيضًا في أماكن أخرى من الفاحص Inspector. ملاحظة: قد تستخدم برامج تطوير الألعاب ثلاثية الأبعاد اصطلاحات مختلفة للتوجيه Orienting. ففي محرك جودو، يستخدم المحور Y ليحدد الاتجاه للأعلى، أي يشير المحور Y للأعلى والأسفل، ويشير المحور X إلى اليسار واليمين، ويشير المحور Z للأمام والخلف. بينما في بعض البرامج ثلاثية الأبعاد الأخرى، قد يُستخدم المحور Z ليشير للاتجاه نحو الأعلى. لذا، يجب أن نأخذ هذا الأمر في الاعتبار عند الانتقال لتطبيقات مختلفة. يمكننا التنقل في المشروع ثلاثي الأبعاد باستخدام الفأرة ولوحة المفاتيح، وفيما يلي عناصر التحكم الأساسية لكاميرا العرض: نحرك عجلة الفأرة للأعلى أو للأسفل لتكبير أو تصغير المشهد نستخدم الزر الفأرة الأوسط مع السحب لتدوير الكاميرا حول الهدف الحالي نستخدم مفتاح Shift مع الزر الأوسط للفأرة مع السحب لتحريك الكاميرا حول المشهد ننقر بزر الفأرة الأيمن مع السحب لتدوير الكاميرا حول محورها دون تغيير موقعها في المشهد ملاحظة: في بعض الألعاب ثلاثية الأبعاد الشهيرة، يوجد وضع يسمى Freelook يسمح لنا بالتنقل بحرية في المشهد دون قيود. يمكننا تفعيل هذا الوضع أو إيقافه في جودو باستخدام الاختصار Shift + F. عند تفعيل هذا الوضع يمكننا استخدام مفاتيح WASD أي مفتاح W للتقدم للأمام، و A للتحرك لليسار،و S للتحرك للخلف، و D للتحرك لليمين من أجل التحرك حول المشهد بحرية كما نفعل في الألعاب عادة. وفي هذا الوضع، ستتيح لنا الفأرة التوجيه وتحريك الكاميرا حول المشهد أو الهدف كما نريد. بالنسبة لتغيير عرض الكاميرا، سنجد في الزاوية العلوية اليسرى من الشاشة، اسم توضيحي منظوري Perspective. عند النقر عليه، يمكننا تغيير زاوية رؤية الكاميرا، أي يمكننا جعل الكاميرا تتجه إلى اتجاه معين كجعلها تعرض المشهد من الأعلى أو الجوانب أو بأي زاوية نرغب بها. إضافة كائنات ثلاثية الأبعاد لنضف الآن أول عقدة ثلاثية الأبعاد. ترث العقد ثلاثية الأبعاد في جودو من العقدة الأساسية Node3D، وهي توفر نفس الخصائص التي ترثها العقدة Node2D في المشاريع الثنائية الأبعاد، مثل خاصية الموضع position لتحديد مكان الكائن في المشهد، وخاصية الدوران rotation لتحديد زاوية دوران الكائن. عند إضافة عقدة ثلاثية الأبعاد Node3D إلى المشهد، سنرى الكائن أن يظهر عند نقطة الأصل (0,0,0) في الفضاء ثلاثي الأبعاد كما في الصورة التالية: في جودو، عندما نضيف كائن ثلاثي الأبعاد إلى المشهد، لا يُعدّ هذا الكائن عقدة، بل يُسمى Gizmo ثلاثي الأبعاد. تُعدّ Gizmo أداة تتيح لنا التحكم في الكائنات الثلاثية الأبعاد، مثل تحريكها أو تدويرها في الفضاء. تستخدم هذه الأداة ثلاث حلقات كي تتحكم في الدوران، وتستخدم ثلاثة أسهم لتحرك الكائن على طول المحاور الثلاثة، وتكون الحلقات والأسهم ملونة لتتناسب مع ألوان المحاور. يمكن تجربة استخدام هذه الأداة والتعرف عليها، واستخدام زر التراجع Undo لاستعادة الوضع السابق. ملاحظة: قد نجد أن أدوات Gizmo المستخدمة لتحريك الكائنات وتدويرها وتغيير حجمها مزعجة أو متداخلة مع بعضها أثناء العمل. ويمكننا حل هذه المشكلة بسهولة واختيار نوع واحد فقط من التعديلات بالنقر على أيقونات الوضع للتقيد بنوع واحد فقط من التحويلات أي يمكن أن نختار إما وضع التحريك أو وضع التدوير أو وضع التحجيم، فباختيارنا لأحد هذه الأوضاع، سنتمكن من التركيز على تعديل واحد في كل مرة وسيسهل علينا التعامل مع الكائنات في المشهد. الحيز العالمي والحيز المحلي الحيز العالمي والحيز المحلي هما طريقتان للتحكم في تحريك الكائنات داخل جودو حيث يعتمد الحيز العالمي Global Space على محاور ثابتة لا تتغير مهما دورنا الكائن أوحركناه وتشير الأسهم دائمًا إلى هذه المحاور الثابتة، أما في الحيز المحلي Local Space فتتحرك المحاور مع حركة الكائن نفسه وتُعدّل بناء على تحريك الكائن أو تدويره. تعمل عناصر التحكم في أداة Gizmo في الحيز العالمي Global Space افتراضيًا، إذ تبقى أسهم أداة Gizmo تشير على طول المحاور عند تدوير الكائن، ولكن إذا نقرنا على زر استخدام الحيز المحلي Use Local Space، فستتحوّل الأداة إلى تحريك الجسم في حيز محلي. تشير أسهم أداة Gizmo الآن إلى محاور الكائن نفسه بدلاً من محاور العالم عند تدويره. فيمكن أن يساعدنا التبديل بين الحيز المحلي والعالمي في تحديد موقع الكائن بدقة وفقًا لاحتياجاتنا. التحويلات Transforms لنبحث عن العقدة Node3D في الفاحص Inspector، حيث سنرى خاصيات الموضع Position والدوران Rotation والتحجيم Scale في قسم التحويل Transform، لذا نسحب الكائن باستخدام أداة Gizmo ونلاحظ كيف تتغير هذه القيم. تكون هذه الخاصيات متعلقة بأب هذه العقدة كما هو الحال في العقد ثنائية الأبعاد 2D. تشكّل هذه الخاصيات مع بعضها البعض ما يسمى بتحويل العقدة. سنتمكن من الوصول إلى خاصية التحويل transform التي هي كائن جودو Transform3D عند تغيير الخاصيات المكانية للعقدة في الشيفرة البرمجية، ويكون لهذا الكائن خاصيتان هما origin و basis. تمثّل الخاصية origin موضع الجسم، بينما تحتوي الخاصية basis على ثلاثة متجهات تحدّد محاور إحداثيات الجسم المحلية، حيث يمكننا التفكير في أسهم المحاور الثلاثة في أداة Gizmo عندما تكون في وضع الحيز المحلي Local Space، وسنوضّح كيفية استخدام هذه الخاصيات لاحقًا. الشبكات Meshes لا تتمتع عقدة Node3D بحجم أو مظهر خاص بها مثل عقدة Node2D، لذا يمكننا استخدام العقدة Sprite2D لإضافة خامة Texture إلى العقدة في الألعاب ثنائية الأبعاد 2D، ولكننا سنحتاج إلى إضافة شبكة Mesh في الألعاب ثلاثية الأبعاد 3D. الشبكة هي وصف رياضي لشكل ما، وتتكون من مجموعة من النقاط تسمى الرؤوس Vertices، وترتبط هذه الرؤوس بخطوط تسمى الأضلاع Edges، وتشكل الأضلاع المتعددة مع بعضها البعض وجهًا Face، فمثلًا يتكون المكعب من 8 رؤوس و 12 ضلعًا و 6 أوجه. إضافة الشبكات يمكن إنشاء الشبكات باستخدام برامج النمذجة ثلاثية الأبعاد مثل بلندر Blender، ويمكننا أيضًا العثور على العديد من مجموعات النماذج ثلاثية الأبعاد المتاحة للتنزيل إن لم نتمكّن من إنشاء نموذجنا الخاص. قد نحتاج في بعض الأحيان إلى شكل هندسي أساسي فقط مثل المكعب أو الكرة، ويوفر جودو في هذه الحالة طريقة لإنشاء شبكات بسيطة تسمى الأشكال الأولية Primitives. لنضف عقدة MeshInstance3D كعقدة ابن للعقدة Node3D، وننقر على الخاصية Mesh الخاصة بها في الفاحص Inspector: يمكننا الآن رؤية قائمة الأشكال الأولية المتاحة، والتي تمثل مجموعة مفيدة من الأشكال الشائعة المفيدة. سنحدّد خيار BoxMesh جديدة، سنرى مكعبًا يظهر على الشاشة. الكاميرات سنحاول تشغيل المشهد مع كائن المكعب، ولكنا لن نرى أي شيء في نافذة عرض اللعبة ثلاثية الأبعاد دون إضافة العقدة Camera3D، لذا لنضفها إلى العقدة الجذر ونستخدم أداة Gizmo الخاصة بالكاميرا لوضعها في اتجاه المكعب كما يلي: يسمى الشكل الهرمي الوردي الأرجواني على الكاميرا Fustrum وهو يمثل مجال نظر الكاميرا، ونلاحظ السهم الذي له شكل المثلث الصغير ويمثل اتجاه الكاميرا للأعلى. نضغط على زر المعاينة Preview في الجزء العلوي الأيسر أثناء تحريك الكاميرا لمعرفة ما تراه الكاميرا، ونشغّل المشهد للتأكد من أن كل شيء يعمل كما هو متوقع. الخلاصة تعلمنا في هذا المقال كيفية استخدام محرر جودو ثلاثي الأبعاد، وكيفية إضافة عقد ثلاثية الأبعاد مثل Node3D و MeshInstance3D و Camera3D، وكيفية استخدام أدوات Gizmo لوضع الكائنات الخاصة بنا في مكانها، بالإضافة إلى مجموعة من المصطلحات الجديدة. سنوضح في المقال التالي كيفية بناء مشهد ثلاثي الأبعاد من خلال استيراد ملفات الأصول ثلاثية الأبعاد 3D Assets وكيفية استخدام مزيد من عقد جودو ثلاثية الأبعاد. ترجمة -وبتصرّف- للقسم The 3D Editor من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: كتابة سكربتات GDScript وإرفاقها بالعقد في جودو تعرف على واجهة محرك الألعاب جودو تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو العقد Nodes والمشاهد Scenes في جودو Godot
  11. يتطلب نشر نماذج مكتبة المحوِّلات Transformers التي توفرها منصة Huggingface في بيئات الإنتاج تصدير النماذج إلى صيغة متسلسلة Serialized أي إلى صيغة يمكن تحميلها وتنفيذها في أوقات تشغيل وعلى عتاد متخصص. سنشرح في مقال اليوم كيفية تصدير نماذج Transformers باستخدام مكتبة Optimum وهي امتداد لمكتبة المحولات Transformers تتيح تصدير النماذج من إطار عمل باي تورش PyTorch أو تنسر فلو TensorFlow إلى صيغ متسلسلة مثل ONNX و TFLite عبر وحدة exporters، وتوفر المكتبة أيضًا مجموعة من أدوات تحسين الأداء لتدريب النماذج وتشغيلها بكفاءة. تصدير نماذج Transformers إلى صيغة ONNX صيغة ONNX هي اختصار لعبارة Open Neural Network eXchange وهي معيار مفتوح يحدّد مجموعة من المعاملات وصيغة ملفات مشتركة لتمثيل نماذج التعلم العميق Deep Leaning في مجموعة متنوعة من أطر العمل كإطار عمل باي تورش PyTorch وتنسرفلو TensorFlow، وتُستخدَم هذه المعاملات لإنشاء رسم بياني حسابي يُطلق عليه اسم التمثيل الوسيط Intermediate Representation يمثّل تدفق البيانات عبر الشبكة العصبونية عند تصدير النموذج إلى صيغة ONNX. الهدف من صيغة ONNX هو تمكين تبادل النماذج بين أطر العمل المختلفة، وتسهيل نقل النماذج بين بيئات تطوير متعددة مع الحفاظ على تكاملها وأدائها، وتسهّل هذه الصيغة التبديل بين أطر العمل من خلال عرض رسم بياني باستخدام معاملات وأنواع بيانات معيارية، فمثلًا يمكن تصدير نموذج مُدرَّب في إطار عمل باي تورش PyTorch إلى صيغة ONNX، ثم استيراده في إطار عمل تنسرفلو TensorFlow والعكس صحيح. بعد تصدير النموذج إلى صيغة ONNX سنتمكن من تنفيذ الأمور التالية: تحسين الاستدلال والتنبؤ من النموذج باستخدام تقنيات مثل تحسين الرسم البياني Graph Optimization والتكميم Quantization تشغيل مسرّع ONNX Runtime باستخدام أصناف ORTModelForXXX التي تتبع واجهة برمجة تطبيقات AutoModel المستخدمة أيضًا في مكتبة المحولات Transformers تشغيل خطوط أنابيب الاستدلال المُحسَّنة التي تملك نفس واجهة برمجة التطبيقات الخاصة بالدالة pipeline()‎ في مكتبة المحولات Transformers توفّر مكتبة Optimum دعمًا لتصدير صيغة ONNX من خلال الاستفادة من كائنات الضبط Configuration التي تكون جاهزة لعدد من بنى النماذج، وهي مصمَّمة لتكون قابلة للتوسيع بسهولة إلى بنى أخرى، ويمكنك مطالعة توثيق Optimum للحصول على قائمة بعمليات الضبط الجاهزة. هنالك طريقتان لتصدير نموذج Transformers إلى صيغة ONNX هما: التصدير باستخدام مكتبة Optimum عبر واجهة سطر الأوامر CLI التصدير باستخدام مكتبة Optimum عبر optimum.onnxruntime تصدير النموذج إلى صيغة ONNX باستخدام واجهة سطر الأوامر يمكن تصدير نموذج Transformers إلى صيغة ONNX عبر واجهة سطر الأوامر CLI من خلال تثبيت اعتمادية إضافية كما يلي: pip install optimum[exporters] يمكن مطالعة توثيق Optimum للتعرف على جميع الوسطاء المتاحة، أو استخدم الأمر التالي للمساعدة: optimum-cli export onnx --help يمكننا تصدير نقطة التحقق Checkpoint الخاصة بالنموذج من مستودع Hub على سبيل المثال النموذج ،distilbert/distilbert-base-uncased-distilled-squad هو نسخة من نموذج DistilBERT جرى تحسينه للعمل مع مجموعة بيانات الأسئلة والأجوبة SQuAD. ويمكننا تصدير هذا النموذج أو تحميله من خلال تشغيل الأمر التالي: optimum-cli export onnx --model distilbert/distilbert-base-uncased-distilled-squad distilbert_base_uncased_squad_onnx/ يجب أن نرى السجلات التي تعرض التقدم ومكان حفظ ملف model.onnx الناتج بالشكل التالي: Validating ONNX model distilbert_base_uncased_squad_onnx/model.onnx... -[✓] ONNX model output names match reference model (start_logits, end_logits) - Validating ONNX Model output "start_logits": -[✓] (2, 16) matches (2, 16) -[✓] all values close (atol: 0.0001) - Validating ONNX Model output "end_logits": -[✓] (2, 16) matches (2, 16) -[✓] all values close (atol: 0.0001) The ONNX export succeeded and the exported model was saved at: distilbert_base_uncased_squad_onnx يوضّح المثال السابق تصدير نقطة تحقق من مستودع Hub. علينا أن نتأكد أولًا من حفظ أوزان النموذج وملفات Tokenizer في نفس المجلد المُسمَّى local_path عند تصدير نموذج محلي. إذا استخدمنا واجهة سطر الأوامر CLI، فعلينا تمرير اسم المجلد local_path إلى الوسيط model بدلًا من اسم نقطة التحقق على مستودع Hub واستخدم الوسيط ‎--task الذي يمثل المهمة. ويمكن مطالعة المزيد حول قائمة المهام المدعومة في توثيق Optimum. optimum-cli export onnx --model local_path --task question-answering distilbert_base_uncased_squad_onnx/ بعدها يمكننا تشغيل ملف النموذج المُصدّر model.onnx الناتج على أحد المسرّعات Accelerators المتعددة التي تدعم معيار ONNX، فمثلًا يمكن تحميل النموذج وتشغيله باستخدام المسرّع ONNX Runtime كما يلي: >>> from transformers import AutoTokenizer >>> from optimum.onnxruntime import ORTModelForQuestionAnswering >>> tokenizer = AutoTokenizer.from_pretrained("distilbert_base_uncased_squad_onnx") >>> model = ORTModelForQuestionAnswering.from_pretrained("distilbert_base_uncased_squad_onnx") >>> inputs = tokenizer("What am I using?", "Using DistilBERT with ONNX Runtime!", return_tensors="pt") >>> outputs = model(**inputs) ينطبق الأمر نفسه على نقاط تحقق تنسرفلو TensorFlow على مستودع Hub، فمثلًا يمكننا تصدير نقطة تحقق TensorFlow خام غير مُعدّلة من مجموعة Keras كما يلي: optimum-cli export onnx --model keras-io/transformers-qa distilbert_base_cased_squad_onnx/ تصدير النموذج إلى صيغة ONNX باستخدام optimum.onnxruntime يمكننا تصدير نموذج Transformers إلى ONNX برمجيًا بدلًا من استخدام واجهة سطر الأوامر CLI كما يلي: >>> from optimum.onnxruntime import ORTModelForSequenceClassification >>> from transformers import AutoTokenizer >>> model_checkpoint = "distilbert_base_uncased_squad" >>> save_directory = "onnx/" >>> # ‫تحميل نموذج من مكتبة Transformers وتصديره إلى صيغة ONNX >>> ort_model = ORTModelForSequenceClassification.from_pretrained(model_checkpoint, export=True) >>> tokenizer = AutoTokenizer.from_pretrained(model_checkpoint) >>> # ‫حفظ نموذج onnx والمرمِّز >>> ort_model.save_pretrained(save_directory) >>> tokenizer.save_pretrained(save_directory) تصدير النموذج إلى بنية غير مدعومة إذا أردنا المساهمة بإضافة دعم لنموذج لا يمكن تصديره حاليًا، فيجب أن نتحقق أولًا فيما إذا كان مدعومًا في optimum.exporters.onnx، وإن كان غير مدعوم، فيمكن أن نساهم في Optimum مباشرةً. تصدير نموذج باستخدام الحزمة transformers.onnx ملاحظة: لم تَعُد هناك صيانة للحزمة tranformers.onnx، لذا علينا تصدير النماذج باستخدام المكتبة Optimum كما هو موضح في الأقسام السابقة، إذ سيُزال هذا القسم في الإصدارات المستقبلية. يمكن تصدير نموذج Transformers إلى صيغة ONNX باستخدام الحزمة tranformers.onnx من خلال تثبيت اعتماديات إضافية كما يلي: pip install transformers[onnx] نستخدم حزمة transformers.onnx كما نستخدم وحدة بايثون Python لتصدير نقطة تحقق باستخدام ضبط Configuration جاهز كما يلي: python -m transformers.onnx --model=distilbert/distilbert-base-uncased onnx/ سيؤدي هذا إلى تصدير رسم ONNX البياني لنقطة التحقق التي يحدّدها الوسيط ‎--model. علينا تمرير نقطة تحقق على مستودع Hub أو أي نقطة تحقق مُخزَّنة محليًا، ثم يمكننا تشغيل ملف model.onnx الناتج على أحد المسرِّعات التي تدعم معيار ONNX، فمثلًا يمكننا تحميل النموذج وتشغيله باستخدام المُسرّع ONNX Runtime كما يلي: >>> from transformers import AutoTokenizer >>> from onnxruntime import InferenceSession >>> tokenizer = AutoTokenizer.from_pretrained("distilbert/distilbert-base-uncased") >>> session = InferenceSession("onnx/model.onnx") >>> # ‫يتوقع المسرّع ONNX Runtime وجود مصفوفات NumPy كدخل >>> inputs = tokenizer("Using DistilBERT with ONNX Runtime!", return_tensors="np") >>> outputs = session.run(output_names=["last_hidden_state"], input_feed=dict(inputs)) يمكن الحصول على أسماء المخرجات المطلوبة مثل ‎["last_hidden_state"]‎ من خلال الاطّلاع على ضبط ONNX لكل نموذج، فمثلًا يكون لدينا ما يلي بالنسبة لنموذج DistilBERT: >>> from transformers.models.distilbert import DistilBertConfig, DistilBertOnnxConfig >>> config = DistilBertConfig() >>> onnx_config = DistilBertOnnxConfig(config) >>> print(list(onnx_config.outputs.keys())) ["last_hidden_state"] ينطبق الأمر نفسه على نقاط تحقق تنسرفلو TensorFlow على مستودع Hub، حيث نصدّر نقطة تحقق تنسرفلو TensorFlow خام غير مُدرّبة كما يلي: python -m transformers.onnx --model=keras-io/transformers-qa onnx/ يمكننا تصدير نموذج مُخزَّن محليًا من خلال حفظ أوزان النموذج وملفات Tokenizer الخاصة به في المجلد نفسه مثل local-pt-checkpoint، ثم نصدّره إلى صيغة ONNX من خلال توجيه الوسيط ‎--model الخاص بحزمة transformers.onnx إلى المجلد المطلوب كما يلي: python -m transformers.onnx --model=local-pt-checkpoint onnx/ تصدير النماذج إلى صيغة TFLite باستخدام المكتبة Optimum يُعدّ TensorFlow Lite أو TFLite اختصارًا إطار عمل خفيف الوزن لنشر نماذج تعلم الآلة على الأجهزة ذات الموارد المحدودة مثل الهواتف المحمولة والأنظمة المُضمَّنة وأجهزة إنترنت الأشياء IoT، فقد صُمِّم إطار عمل TFLite لتحسين النماذج وتشغيلها بكفاءة على هذه الأجهزة ذات القدرة الحاسوبية والذاكرة المحدودة. يُمثَّل نموذج TFLite بصيغة فعالة خاصة يمكن نقلها وتحدّدها لاحقة الملفات ‎.tflite. كما توفر المكتبة Optimum دالة لتصدير نماذج المحولات Transformers إلى صيغة TFLite عبر وحدة exporters.tflite، وللحصول على قائمة ببنى النماذج المدعومة يمكن مطالعة توثيق Optimum. يمكننا تصدير نموذج إلى صيغة TFLite من خلال تثبيت الاعتماديات المطلوبة كما يلي: pip install optimum[exporters-tf] ويمكن مطالعة توثيق Optimum للتعرف على جميع الوسطاء المتاحة، أو استخدم الأمر التالي للمساعدة: optimum-cli export tflite --help يمكن تصدير نقطة التحقق الخاصة بالنموذج من مستودع Hub مثل google-bert/bert-base-uncased من خلال تشغيل الأمر التالي: optimum-cli export tflite --model google-bert/bert-base-uncased --sequence_length 128 bert_tflite/ يجب أن نرى السجلات التي تشير إلى التقدم ، ويظهر مكان حفظ ملف model.tflite الناتج كما يلي: Validating TFLite model... -[✓] TFLite model output names match reference model (logits) - Validating TFLite Model output "logits": -[✓] (1, 128, 30522) matches (1, 128, 30522) -[x] values not close enough, max diff: 5.817413330078125e-05 (atol: 1e-05) The TensorFlow Lite export succeeded with the warning: The maximum absolute difference between the output of the reference model and the TFLite exported model is not within the set tolerance 1e-05: - logits: max diff = 5.817413330078125e-05. The exported model was saved at: bert_tflite يوضح المثال السابق تصدير نقطة تحقق من مستودع Hub. يجب أن نتأكد أولًا من حفظ أوزان النموذج وملفات Tokenizer في المجلد نفسه local_path عند تصدير نموذج محلي. وإذا استخدمنا واجهة سطر الأوامر CLI، فعلينا تمرير اسم المجلد local_path إلى الوسيط model-- بدلًا من اسم نقطة التحقق على مستودع Hub. الخاتمة شرحنا في هذا المقال كيفية تصدير نماذج التعلم العميق التي تستخدم مكتبة Transformers من منصة Hugging Face إلى صيغ يمكن تشغيلها في بيئات إنتاج حقيقية مثل تطبيقات الهاتف أو الويب أو أنظمة الذكاء الاصطناعي كالصيغة ONNX والصيغة TFLite باستخدام مكتبة Optimum. الفائدة الأساسية من تصدير النماذج إلى هذه الصيغ هي تحسين أدائها عند التشغيل على أجهزة متخصصة، مثل المعالجات أو الأجهزة التي تملك قدرة حاسوبية محدودة، وتسهيل تشغيلها في تطبيقات عملية بسرعة وكفاءة. ترجمة -وبتصرّف- للقسمين Export to ONNX و Export to TFLite من توثيقات Hugging Face. اقرأ أيضًا ما هي منصة Hugging Face للذكاء الاصطناعي استخدام مكتبة المرمزات Tokenizers في منصة Hugging Face إنشاء بنية مخصصة لنماذج Transformers في Hugging Face بناء نماذج مخصصة باستخدام مكتبة Transformers في Hugging Face
  12. تُعَد كتابة السكربتات البرمجية وربطها بالعقد والكائنات الأخرى الأسلوب الأساسي لبناء سلوك الألعاب في محرك جودو. على سبيل المثال، تعرض العقدة Sprite2D صورة تلقائيًا، ولكن يمكننا من خلال إضافة سكربت مخصص لهذه العقدة تحريك هذه الصورة عبر الشاشة، وتحديد سرعة واتجاه الحركة والعديد من الخصائص الأخرى. ما هي لغة GDScript لغة جي دي سكربت GDScript هي لغة مُضمَّنة في جودو لكتابة السكربتات البرمجية والتفاعل مع العقد. وتشير عدة مراجع لأن لغة GDScript تعتمد على لغة بايثون Python، إذ تستخدم لغة GDScript صياغة مبنية على لغة بايثون، ولكنها لغة مميزة مُحسَّنة ومدمجة مع محرك جودو، لذا إذا كنا على دراية باستخدام لغة بايثون، فستكون هذه اللغة مألوفة وسهلة الفهم بالنسبة لنا. ملاحظة: يُفترَض امتلاك بعض الخبرة في أساسيات البرمجة كي نتمكن من تعلّم محرّك الألعاب جودو، فإن لم نكن على دراية بالبرمجة على الإطلاق فسيكون لديك عبء إضافي لتعلم البرمجة. فإذا وجدتم صعوبة في فهم الشيفرة البرمجية في هذا المقال، فننصح قبل البدء بمطالعة مقال أساسيات لغة بايثون ومقال تعلم أساسيات البرمجة. بنية ملف GDScript يجب أن يكون السطر الأول من أي ملف GDScript هو extends <Class>‎ حيث أن <Class> هو إما صنف مُضمَّن موجود مسبقًا، أو صنف يعرّفه المستخدم، فمثلًا إذا أردنا إرفاق سكربت معين بعقدة CharacterBody2D، فسيبدأ السكربت بالعبارة extends CharacterBody2D، وهذا يعني أن السكربت يأخذ جميع وظائف كائن CharacterBody2D المُضمَّن ويوسّعه باستخدام الوظائف الإضافية التي أنشأناها بنفسنا. يمكننا في بقية السكربت تعريفُ أيّ عدد من المتغيرات المعروفة أيضًا باسم خاصيات الصنف والدوال المعروفة أيضًا باسم توابع الصنف. إنشاء سكربت GDScript لننشئ السكربت الأول، لنتذكّر أن بإمكاننا إرفاق سكربت بأيّ عقدة. لذا سنفتح محرّر جودو ونتقل للتبويب مشهد Scene، ونضيف عقدة من نوع Sprite2D إلى المشهد، ثم ننقر بزر الفأرة الأيمن على العقدة الجديدة، ونحدّد خيار إلحاق نص برمجي Attach Script، ويمكننا أيضًا النقر على الأيقونة الموجودة يسار مربع البحث. يجب بعد ذلك أن نختار المكان الذي تريد حفظ السكربت البرمجي فيه ونحدد اسمه، وفي حال سمّينا العقدة باسم ما، فسيُسمَّى السكربت تلقائيًا بذلك الاسم، لذا إن لم نغيِّر أي شيء، فسوف يسمى هذا السكربت sprite2d.gd. نفتح الآن نافذة محرّر السكربت البرمجي سنجد بعض الكود البرمجي فيها، سيكون هذا السكربت مرتبطًا بالشخصية الرسومية Sprite الجديدة الفارغة حتى الآن، فقد وضع جودو تلقائيًا بعض أسطر الشيفرة البرمجية بالإضافة إلى بعض التعليقات التي تشرح الشيفرة. extends Sprite2D # Called when the node enters the scene tree for the first time. func _ready() -> void: pass # Replace with function body. # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta: float) -> void: pass أُضيف هذا السكربت إلى العقدة Sprite2D، لذا يكون السطر الأول تلقائيًا هو extends Sprite2D كي يوسّع هذا السكربت الصنف Sprite2D ويكون قادرًا على الوصول إلى جميع الخاصيات والتوابع التي توفرها العقدة Sprite2D ومعالجتها. ملاحظة: إن الخاصيات Properties والتوابع Methods هي المتغيرات والدوال المُعرَّفة في الكائن، قد يخلط المبرمجون المبتدئون في استخدام هذه المصطلحات. سنعرّف بعد ذلك جميع المتغيرات التي سنستخدمها في السكربت، والتي تُسمَّى المتغيرات الأعضاء Member Variables، ولتعريف المتغيرات نستخدم الكلمة المفتاحية var. سنتجاوز التعليقات الموجودة في السكربت ونتحدث عن الجزء التالي، حيث سنرى في السكربت أعلاه الدالة ‎_ready()‎ حيث يبدأ تعريف الدالة في لغة GDScript بالكلمة المفتاحية func. والدالة ‎_ready()‎ هنا هي دالة خاصة ينفّذها جودو بشكل تلقائي عند إضافة عقدة إلى الشجرة، أو عندما يبدأ المشهد بالعمل بالضغط على تشغيل Play. لنفترض أننا نريد نقل الشخصية الرسومية إلى موضع محدد عند بدء اللعبة، عندها يجب علينا ضبط خاصية الموضع Position في الفاحص Inspector، توجد هذه الخاصية في القسم Node2D، وبالتالي ستتوفر هذه الخاصية في أي عقدة من نوع Node2D، وليس فقط في العقد من نوع Sprite2D. لنضبط الآن هذه الخاصية عن طريق الشيفرة البرمجية، إحدى الطرق المتبعة للعثور على اسم الخاصية هي التمرير فوقها في الفاحص Inspector كما يلي: يحتوي جودو على أداة بحث مساعد مدمجة رائعة تساعدنا في التعرف على تفاصيل الأصناف التي نتعامل معها، لبدء استخدام هذه الأداة ننقر على التبويب Classes في الجزء العلوي من نافذة السكربت، ثم نبحث عن اسم الصنف Node2D، ستظهر لنا صفحة المساعدة التي تعرض جميع الخاصيات والتوابع المتاحة بالصنف. إذا مررنا لأسفل قليلاً، سنلاحظ وجود الخاصية position ضمن قسم Member Variables، والتي تُعد من المتغيرات الأعضاء للصنف. ونلاحظ أيضًا أن هذه الخاصية من النوع Vector2، مما يعني أنها تُستخدَم لتخزين الإحداثيات على المحورين X و Y. لنرجع إلى السكربت البرمجي ونستخدم هذه الخاصية كما يلي: func _ready(): position = Vector2(100, 150) نلاحظ كيف يعرض لنا المحرّر اقتراحات أثناء الكتابة فعندما نكتب مثلًا Vector2، سيخبرنا التلميح بوضع عددين عشريين x و y. لدينا الآن سكربت يمثّل ضبط موضع الشخصية الرسومية على القيم ‎(100,150)‎ عند بدء التشغيل، ويمكننا تجربة ذلك بالضغط على زر تشغيل المشهد Play Scene. ملاحظة: يستخدم جودو المتجهات أو الأشعة Vectors للعديد من الأشياء، وسنتحدث عنها بمزيد من التفصيل لاحقًا. وأخيرًا قد يتساءل المبتدئون في برمجة الألعاب عن كيفية حفظ جميع هذه الأوامر البرمجية، والجواب هو مثل أي مهارة أخرى تمامًا، إذ لا يتعلق الأمر بالحفظ بل بالممارسة والتطبيق، فعندما نكرر تنفيذ الأشياء مرارًا وتكرارًا ستصبح بديهية بالنسبة لنا. ومن الجيد الاحتفاظ بمستندات مرجعية في متناول اليد مبدئيًا، واستخدام البحث كلما رأينا شيئًا لا نعرفه، وإذا كان لدينا شاشات متعددة، فلنحتفظ بنسخة مفتوحة من صفحات التوثيق للرجوع إليها بسرعة. الخاتمة أنشأنا في مقال اليوم أول نص برمجي باستخدام GDScript، من الضروري تطبيق وفهم كل ما فعلناه في هذه الخطوة قبل الانتقال إلى الخطوة التالية، حيث سنضيف مزيدًا من الشيفرة البرمجية لتحريك الشخصيات الرسومية حول الشاشة في المقال التالي. ترجمة -وبتصرّف- للقسم Introduction to GDScript: Getting started من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: تعرف على العقد في محرك الألعاب Godot العقد Nodes والمشاهد Scenes في جودو Godot لغات البرمجة المتاحة في جودو Godot كتابة برنامجك الأول باستخدام جودو Godot مدخل إلى محرك الألعاب جودو Godot
  13. سنتعرف في هذا المقال على الصنف Trainer في مكتبة المحولات Transformers ضمن منصة Hugging Face، ونوضح طريقة الاستفادة منه في تدريب النماذج اللغوية الكبيرة، كما سنتعرف أيضًا على أصناف عديدة تفيدنا في تحسين وتسريع عملية التدريب. ما هو الصنف Trainer الصنف Trainer هو أداة تُسهّل تدريب النماذج المبنية باستخدام باي تورش PyTorch في مكتبة المحولات Transformers. فبدلًا من أن نضطر لكتابة الشيفرة الخاصة بتدريب النموذج من البداية، يتيح لنا هذا الصنف تمرير العناصر الأساسية مثل النموذج Model، والمُرمِّز Tokenizer، ومجموعة البيانات Dataset، ودالة التقييم Evaluation Function، ومعاملات التدريب الفائقة Hyperparameters، والبدء بعدها بالتدريب بسرعة دون الحاجة للتعامل مع الكثير من التفاصيل والإعدادات يدويًا، كما أن الصنف Trainer قابل للتخصيص وبإمكاننا تعديل إعداداته بما يوافق احتياجاتنا. توفر مكتبة Transformers أيضًا الصنف Seq2SeqTrainer الذي يساعدنا في مهام تتطلب تحويل سلسلة نصية إلى سلسلة نصية أخرى مثل الترجمة أو التلخيص. يوجد أيضًا الصنف SFTTrainer من مكتبة TRL التي تغلِّف الصنف Trainer وهي مُحسَّنة لتدريب النماذج اللغوية مثل Llama-2 و Mistral باستخدام تقنيات الانحدار التلقائي Autoregressive التي تُمكِّن النموذج اللغوي من توليد النصوص بناءً على تسلسل الكلمات السابقة، مما يُحسن أداءه، ويدعم الصنف SFTTrainer أيضًا ميزات أخرى مثل Sequence Packing و LoRA وQuantization و DeepSpeed للتوسّع بكفاءة إلى أيّ حجم نموذج نحتاجه، مما يجعله مثاليًا لتدريب النماذج اللغوية المتقدمة. ملاحظة: يُستحسن الاطلاع على توثيق Trainer لمعرفة المزيد حول الأصناف المختلفة ومتى نستخدم كل منها، فالصنف Trainer هو الخيار الأكثر تنوعًا ويناسب مجموعة واسعة من المهام، والصنف Seq2SeqTrainer مناسب لمهام تتطلب حويل تسلسل نصي إلى تسلسل نصي آخر كالترجمة والتلخيص، أما الصنف SFTTrainer فهو مناسب لتدريب النماذج اللغوية المتقدمة. قبل البدء باستخدام الصنف Trainer، لنتأكد من تثبيت مكتبة Accelerate المفيدة لتفعيل وتشغيل تدريب نماذج باي تورش PyTorch في البيئات الموزعة بسرعة من خلال الأمر التالي: pip install accelerate # للترقية pip install accelerate --upgrade سنوفر في الفقرات التالية نظرة عامة على الصنف Trainer وطريقة استخدامه في مكتبة المحوّلات Transformers. الاستخدام الأساسي للصنف Trainer يتضمن الصنف Trainer الشيفرة البرمجية الموجودة في حلقة تدريب أساسية، والتي تتضمن ما يلي: إجراء خطوة تدريب لحساب الخسارة حساب التدرجات Gradients باستخدام التابع backward تحديث الأوزان بناءً على التدرجات تكرار هذه العملية حتى الوصول إلى عدد محدَّد مسبقًا من دورات التدريب Epochs يجرّد الصنف Trainer هذه الشيفرة حتى لا نضطر إلى القلق بشأن كتابة حلقة تدريب يدويًا في كل مرة، أو إذا كنا مبتدئين في استخدام إطار عمل PyTorch وإجراء عملية التدريب من خلاله، فما علينا سوى توفير المكوّنات الأساسية المطلوبة للتدريب كالنموذج ومجموعة البيانات، وسيتولى الصنف Trainer التعامل مع الأمور الأخرى نيابة عنا. في حال أردنا تحديد خيارات التدريب فيمكننا العثور عليها في الصنف TrainingArguments. لنحدّد مثلًا مكان حفظ النموذج في المعامل output_dir ودفع النموذج إلى مستودع Hub بعد التدريب باستخدام المعامل push_to_hub=True كما يلي: from transformers import TrainingArguments training_args = TrainingArguments( output_dir="your-model", learning_rate=2e-5, per_device_train_batch_size=16, per_device_eval_batch_size=16, num_train_epochs=2, weight_decay=0.01, eval_strategy="epoch", save_strategy="epoch", load_best_model_at_end=True, push_to_hub=True, ) بعد إنشاء training_args علينا تمريره للصنف Trainer إلى جانب النموذج، ومجموعة البيانات، والمُعالِج المسبق لمجموعة البيانات إذ يمكن أن يكون هذا المُعالِج مرمِّزًا أو مستخرج ميزات أو معالج صور حسب نوع بياناتنا، كذلك علينا تمرير مجمِّع بيانات ودالة لحساب المقاييس التي نريد تتبعها أثناء التدريب، ثم نستدعي التابع train()‎ لبدء التدريب كما يلي: from transformers import Trainer trainer = Trainer( model=model, args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], tokenizer=tokenizer, data_collator=data_collator, compute_metrics=compute_metrics, ) trainer.train() نقاط التحقق Checkpoints يحفظ الصنف Trainer نقاط التحقق Checkpoints الخاصة بنموذجنا في المجلد المحدد في المعامل output_dir الخاص بالصنف TrainingArguments، حيث سنجد نقاط التحقق محفوظة في المجلد الفرعي checkpoint-000 وتقابل الأرقام في النهاية خطوة التدريب، ويُعَد حفظ نقاط التحقق مفيدًا لاستئناف التدريب لاحقًا. # الاستئناف من نقطة التحقق الأخيرة trainer.train(resume_from_checkpoint=True) # الاستئناف من نقطة تحقق مُحدَّدة محفوظة في مجلد الخرج trainer.train(resume_from_checkpoint="your-model/checkpoint-1000") يمكننا حفظ نقاط التحقق الخاصة بنموذجنا، ولكن حالة المُحسِّن لن تُحفَظ افتراضيًا في مستودع Hub، فكي نحفظها يجب علينا ضبط المعامل push_to_hub=True في الصنف TrainingArguments لإيداعها Commit ودفعها إلى المستودع. فيما يلي بعض الخيارات الأخرى التي يمكننا استخدامها لتحديد كيفية حفظ نقاط التحقق وإعدادها من خلال المعامل hub_strategy. يدفع الخيار hub_strategy="checkpoint"‎ أحدث نقطة تحقق إلى مجلد فرعي باسم last-checkpoint ، والذي يمكننا استئناف التدريب منه يدفع الخيار hub_strategy="all_checkpoints"‎ جميع نقاط التحقق إلى المجلد المحدَّد في المعامل output_dir، حيث سنرى نقطة تحقق واحدة لكل مجلد في مستودع النموذج عند استئناف التدريب من نقطة تحقق محفوظة سابقًا، يحاول الصنف Trainer أن يحتفظ بنفس حالة الأرقام العشوائية RNG التي كانت في وقت الحفظ، سواء في Python أو NumPy أو PyTorch. ولكن بسبب بعض الإعدادات الافتراضية في باي تورش PyTorch، قد لا تكون الأرقام العشوائية هي نفسها عند استئناف التدريب. فإذا كنا نرغب في جعل كل شيء يحدث بنفس الطريقة تمامًا في كل مرة، يمكن تعديل بعض الإعدادات لجعل التدريب ينفذ دومًا بنفس الطريقة، لكن يجب أن نضع بالحسبان أن هذا قد يجعل التدريب أبطأ قليلاً، ويمكن الرجوع إلى دليل التحكم في العشوائية في PyTorch لمطالعة هذه الإعدادات التي يتوجب تفعيلها لتحقيق ذلك. تخصيص الصنف Trainer صُمِّم الصنف Trainer ليكون سهل الاستخدام والوصول، وهو يتميز أيضًا بسهولة التخصيص، إذ يمكننا إنشاء أصناف فرعية للعديد من توابع الصنف وتعديلها لدعم الوظيفة التي نريدها دون الحاجة إلى إعادة كتابة حلقة التدريب بالكامل من الصفر كي تتوافق مع هذه الوظيفة، وتتضمن هذه التوابع ما يلي: ينشئ التابع get_train_dataloader()‎ صنف DataLoader للتدريب ينشئ التابع get_eval_dataloader()‎ صنف DataLoader للتقييم ينشئ التابع get_test_dataloader()‎ صنف DataLoader للاختبار يسجّل التابع log()‎ معلومات حول الكائنات المختلفة التي تراقب التدريب يستخدم التابع create_optimizer_and_scheduler()‎ لإعداد المحسّن optimizer ومُجَدوِل معدل التعلم learning rate scheduler، ويمكن التخصيص أيضًا باستخدام التابعين create_optimizer()‎ و create_scheduler()‎ يحسب التابع compute_loss()‎ الخسارة على دفعة من دخل التدريب يجري التابع training_step()‎ خطوة التدريب يجري التابع prediction_step()‎ خطوة التنبؤ والاختبار يقيّم التابع evaluate()‎ النموذج ويعيد مقاييس التقييم يجري التابع predict()‎ تنبؤات على مجموعة الاختبار باستخدام المقاييس إذا كانت التسميات Labels متاحة يمكننا مثلًا تخصيص التابع compute_loss()‎ لاستخدام خسارة موزونة weighted loss كما يلي: from torch import nn from transformers import Trainer class CustomTrainer(Trainer): def compute_loss(self, model, inputs, return_outputs=False): labels = inputs.pop("labels") # تمرير أمامي outputs = model(**inputs) logits = outputs.get("logits") # حساب الخسارة المخصَّصة لثلاثة تسميات توضيحية بأوزان مختلفة loss_fct = nn.CrossEntropyLoss(weight=torch.tensor([1.0, 2.0, 3.0], device=model.device)) loss = loss_fct(logits.view(-1, self.model.config.num_labels), labels.view(-1)) return (loss, outputs) if return_outputs else loss دوال رد النداء Callbacks يوجد خيار آخر لتخصيص الصنف Trainer وهو استخدام دوال رد النداء Callbacks، لا تُغيّر هذه الدوال في حلقة التدريب نفسها، بل تراقب حالة الحلقة وتنفذ بعض الإجراءات مثل التوقف المبكر عن التنفيذ، أو تسجيل النتائج استنادًا إلى الحالة الحالية، وبالتالي لا يمكن استخدام دالة رد نداء لتنفيذ شيءٍ مخصص كتعريف دالة خسارة مخصَّصة باستخدامها، بل يجب في هذه الحالة إنشاء صنف فرعي وتعديل التابع compute_loss()‎ ضمنه. على سبيل المثال، يمكننا إضافة دالة رد نداء للتوقف المبكر بعد 10 خطوات في حلقة التدريب على النحو التالي: from transformers import TrainerCallback class EarlyStoppingCallback(TrainerCallback): def __init__(self, num_steps=10): self.num_steps = num_steps def on_step_end(self, args, state, control, **kwargs): if state.global_step >= self.num_steps: return {"should_training_stop": True} else: return {} ثم نمرّرها إلى المعامل callback الخاص بالصنف Trainer كما يلي: from transformers import Trainer trainer = Trainer( model=model, args=training_args, train_dataset=dataset["train"], eval_dataset=dataset["test"], tokenizer=tokenizer, data_collator=data_collator, compute_metrics=compute_metrics, callback=[EarlyStoppingCallback()], ) التسجيل Logging يمكننا من خلال ضبط مستوى التسجيل Logging التحكم في كمية وتفاصيل المعلومات المسجلة أثناء تنفيذ الشيفرات البرمجية، وهذا يساعدنا في تتبع الأداء واكتشاف الأخطاء بشكل فعال حسب الحاجة. يُضبَط الصنف Trainer على المستوى logging.INFO افتراضيًا والذي يبلّغ عن الأخطاء والتحذيرات والمعلومات الأساسية الأخرى، وتُضبَط نسخة الصنف Trainer في البيئات الموزعة على المستوى logging.WARNING الذي يبلّغ عن الأخطاء والتحذيرات فقط. يمكننا تغيير مستوى التسجيل باستخدام المعاملات log_level و log_level_replica في الصنف TrainingArguments. كما يمكن ضبط إعداد مستوى السجل لكل جهاز من خلال استخدام المعامل log_on_each_node لتحديد استخدام مستوى السجل على كل جهاز أو على الجهاز الرئيسي فقط إذا كنا نعمل في بيئة تحتوي على أجهزة متعددة. ملاحظة1: من المفيد مطالعة توثيق واجهة برمجة التطبيقات الخاصة بالتسجيل Logging للحصول على مزيد من المعلومات حول مستويات التسجيل المختلفة ودور كل منها. ملاحظة2: يحدّد الصنف Trainer مستوى السجل بطريقة منفصلة لكل جهاز في التابع Trainer.__init__()‎ المسؤول عن تهيئة كائن التدريب، لذا قد نرغب في التفكير في ضبط مستوى السجل مبكرًا في حال استخدمنا دوال مكتبة المحوّلات Transformers الأخرى قبل إنشاء الكائن. يمكن مثلًا ضبط الشيفرة البرمجية الرئيسية لاستخدام مستوى السجل نفسه وفقًا لكل جهاز كما يلي: logger = logging.getLogger(__name__) logging.basicConfig( format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", datefmt="%m/%d/%Y %H:%M:%S", handlers=[logging.StreamHandler(sys.stdout)], ) log_level = training_args.get_process_log_level() logger.setLevel(log_level) datasets.utils.logging.set_verbosity(log_level) transformers.utils.logging.set_verbosity(log_level) trainer = Trainer(...) استخدمنا مجموعات مختلفة من المعاملات log_level و log_level_replica لضبط ما يُسجَّل على كل جهاز، حيث إذا كان لدينا جهاز واحد، فنستخدم ما يلي: my_app.py ... --log_level warning --log_level_replica error ونضيف المعامل log_on_each_node 0 للبيئات متعددة الأجهزة كما يلي: my_app.py ... --log_level warning --log_level_replica error --log_on_each_node 0 # الضبط للإبلاغ عن الأخطاء فقط my_app.py ... --log_level error --log_level_replica error --log_on_each_node 0 تقنية NEFTune تقنية NEFTune هي طريقة لتحسين أداء النموذج أثناء التدريب عن طريق إضافة ضجيج Noise إلى البيانات المستخدمة لتدريب النموذج. وما نعنيه بالضجيج هنا إضافة بعض التغييرات العشوائية إلى البيانات للمساعدة في تحسين قدرة النموذج على التعميم وعدم حفظ التفاصيل الزائدة التي قد تؤدي إلى مشكلات في الأداء، فعند استخدام NEFTune، يجري تعديل طبقة التضمين أي التمثيل الرياضي للبيانات داخل النموذج بإضافة الضجيج إليها، مما يساعد في تدريب النموذج بمرونة أكبر. يمكننا تفعيل هذه التقنية في الصنف Trainer من خلال ضبط المعامل neftune_noise_alpha داخل الصنف TrainingArguments، فهذا المعامل يحدد مقدار الضجيج الذي سيُضاف إلى طبقة التضمين أثناء التدريب كما في المثال التالي: from transformers import TrainingArguments, Trainer training_args = TrainingArguments(..., neftune_noise_alpha=0.1) trainer = Trainer(..., args=training_args) بعد الانتهاء من التدريب، تُعطَّل تقنية NEFTune لاستعادة الطبقة الأصلية للتضمين بدون أي تعديلات عشوائية وتجنب أيّ سلوك غير متوقّع. استراتيجية التدريب GaLore إسقاط التدرج منخفض الرتبة Gradient Low-Rank Projection -أو GaLore اختصارًا- هي استراتيجية تدريب منخفضة الرتبة وتتميز بكفاءة في استخدام الذاكرة وتسمح بالتعلم الكامل للمعاملات، ولكنها أكثر كفاءة في استخدام الذاكرة من طرق التكيف منخفضة الرتبة التقليدية مثل LoRA. توفر هذه الاستراتيجية أداءً جيدًا باستخدامها ذاكرة أقل، مما يجعلها خيارًا ممتازًا للاستخدام في تدريب النماذج الكبيرة، قبل البدء باستخدام GaLore، علينا التأكد من تثبيت المستودع الرسمي الخاص بها باستخدام الأمر التالي: pip install galore-torch نحتاج بعد ذلك إلى تحديد كيفية تحسين النموذج بشكل دقيق وإضافة بعض الخيارات التي تحدد طريقة التحسين وأيضًا تحديد الوحدات المستهدفة التي نريد تعديلها، حيث نضيف الخيارات ["galore_adamw", "galore_adafactor", "galore_adamw_8bit"] في المعامل optim مع المعامل optim_target_modules الذي يمكن أن يكون قائمة من السلاسل النصية أو التعابير النمطية Regex أو مسارًا كاملًا مطابقًا لأسماء وحدات النموذج المستهدفة التي نريد تكييفها أثناء عملية التدريب. فيما يلي مثال لسكربت لتدريب نموذج باستخدام مكتبة GaLore، لكن علينا التأكّد من استخدام الأمر التالي أولًا قبل تشغيل السكربت pip install trl datasets وفيما يلي السكربت المطلوب: import torch import datasets import trl from transformers import TrainingArguments, AutoConfig, AutoTokenizer, AutoModelForCausalLM train_dataset = datasets.load_dataset('imdb', split='train') args = TrainingArguments( output_dir="./test-galore", max_steps=100, per_device_train_batch_size=2, optim="galore_adamw", optim_target_modules=["attn", "mlp"] ) model_id = "google/gemma-2b" config = AutoConfig.from_pretrained(model_id) tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_config(config).to(0) trainer = trl.SFTTrainer( model=model, args=args, train_dataset=train_dataset, dataset_text_field='text', max_seq_length=512, ) trainer.train() يمكننا تمرير الوسطاء الإضافية التي تدعمها GaLore من خلال تمرير المعامل optim_args كما يلي: import torch import datasets import trl from transformers import TrainingArguments, AutoConfig, AutoTokenizer, AutoModelForCausalLM train_dataset = datasets.load_dataset('imdb', split='train') args = TrainingArguments( output_dir="./test-galore", max_steps=100, per_device_train_batch_size=2, optim="galore_adamw", optim_target_modules=["attn", "mlp"], optim_args="rank=64, update_proj_gap=100, scale=0.10", ) model_id = "google/gemma-2b" config = AutoConfig.from_pretrained(model_id) tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_config(config).to(0) trainer = trl.SFTTrainer( model=model, args=args, train_dataset=train_dataset, dataset_text_field='text', max_seq_length=512, ) trainer.train() يمكن مطالعة المزيد عن هذه طريقة GaLore في المستودع الأصلي. يمكننا حاليًا تدريب الطبقات الخطية فقط باستخدام طريقة GaLore التي تستخدم طريقة التفكك منخفض الرتبة Low-Rank Decomposition، بينما ستظل الطبقات المتبقية تًدرّب وتُحسَّن بالطريقة التقليدية. وسنلاحظ أن عملية التحضير لبدء التدريب تستغرق بعض الوقت مثلًا 3 دقائق لنموذج 2B على NVIDIA A100 ولكن يجب أن يعمل التدريب بسلاسة بعد ذلك. يمكننا أيضًا إجراء تحسين على مستوى الطبقة من خلال إضافة الكلمة layerwise إلى نهاية اسم المحسِّن كما يلي: import torch import datasets import trl from transformers import TrainingArguments, AutoConfig, AutoTokenizer, AutoModelForCausalLM train_dataset = datasets.load_dataset('imdb', split='train') args = TrainingArguments( output_dir="./test-galore", max_steps=100, per_device_train_batch_size=2, optim="galore_adamw_layerwise", optim_target_modules=["attn", "mlp"] ) model_id = "google/gemma-2b" config = AutoConfig.from_pretrained(model_id) tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_config(config).to(0) trainer = trl.SFTTrainer( model=model, args=args, train_dataset=train_dataset, dataset_text_field='text', max_seq_length=512, ) trainer.train() نلاحظ أن التحسين على مستوى الطبقة Layer-wise Optimization تجريبي بعض الشيء ولا يدعم توزيع البيانات Distributed Data Parallel أو DDP اختصارًا، وبالتالي عند استخدام هذا النوع من التحسين فقد تتمكن من تشغيل التدريب على وحدة معالجة رسومية GPU واحدة فقط. محسّن LOMO محسّن LOMO هو أداة تُستخدم لتحسين النماذج اللغوية الكبيرة عندما تكون الموارد محدودة، وهو يساعد على تقليل استخدام الذاكرة أثناء التدريب. يُعد AdaLomo نوعًا من محسنات LOMO، وهو يعتمد على تقنية التعلم التكيفي Adaptive Learning لتحديث المعاملات بكفاءة، مما يعزز الأداء مع الحفاظ على استهلاك منخفض للذاكرة. يعمل محسّن LOMO عن طريق دمج حساب التدرج وتحديث المعاملات في خطوة واحدة، مما يساعد على تسريع عملية التدريب وتقليل الحاجة إلى الذاكرة. المحسنات المتاحة في LOMO هي lomo و adalomo . ملاحظة: يُوصَى باستخدام محسّن AdaLomo بدون تفعيل خيار حساب تدرجات النموذج grad_norm للحصول على أداء أفضل وإنتاجية أعلى. نثبّت أولًا LOMO من مستودع Pypi باستخدام الأمر التالي: pip install lomo-optim أو نثبّته من المصدر باستخدام الأمر التالي: pip install git+https://github.com/OpenLMLab/LOMO.git. فيما يلي سكربت بسيط يوضح كيفية صقل نموذج google/gemma-2b مع مجموعة بيانات IMDB بدقة كاملة: import torch import datasets from transformers import TrainingArguments, AutoTokenizer, AutoModelForCausalLM import trl train_dataset = datasets.load_dataset('imdb', split='train') args = TrainingArguments( output_dir="./test-lomo", max_steps=1000, per_device_train_batch_size=4, optim="adalomo", gradient_checkpointing=True, logging_strategy="steps", logging_steps=1, learning_rate=2e-6, save_strategy="no", run_name="lomo-imdb", ) model_id = "google/gemma-2b" tokenizer = AutoTokenizer.from_pretrained(model_id) model = AutoModelForCausalLM.from_pretrained(model_id, low_cpu_mem_usage=True).to(0) trainer = trl.SFTTrainer( model=model, args=args, train_dataset=train_dataset, dataset_text_field='text', max_seq_length=1024, ) trainer.train() مكتبة Accelerate والصنف Trainer يعتمد الصنف Trainer على مكتبة Accelerate، والتي هي مكتبة لتدريب نماذج PyTorch بسهولة في البيئات الموزعة مع دعم التكاملات Integrations مثل FullyShardedDataParallel -أو FSDP اختصارًا- و DeepSpeed. ملاحظة: يمكن مطالعة المزيد حول استراتيجيات تجزئة FSDP أو FSDP Sharding وتفريغ وحدة المعالجة المركزية CPU Offloading وغير ذلك باستخدام الصنف Trainer في دليل FSDP. يمكننا استخدام مكتبة Accelerate مع الصنف Trainer من خلال تشغيل أمر accelerate.config لإعداد التدريب لبيئة التدريب الخاصة بنا، حيث ينشئ هذا الأمر ملف config_file.yaml الذي سيُستخدَم عند تشغيل سكربت التدريب. فيما يلي مثلًا بعض عمليات الضبط النموذجية التي يمكننا إعدادها: عند استخدام DistributedDataParallel: compute_environment: LOCAL_MACHINE distributed_type: MULTI_GPU downcast_bf16: 'no' gpu_ids: all machine_rank: 0 # تغيير الرتبة حسب الجهاز main_process_ip: 192.168.20.1 main_process_port: 9898 main_training_function: main mixed_precision: fp16 num_machines: 2 num_processes: 8 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false عند استخدام FSDP: compute_environment: LOCAL_MACHINE distributed_type: FSDP downcast_bf16: 'no' fsdp_config: fsdp_auto_wrap_policy: TRANSFORMER_BASED_WRAP fsdp_backward_prefetch_policy: BACKWARD_PRE fsdp_forward_prefetch: true fsdp_offload_params: false fsdp_sharding_strategy: 1 fsdp_state_dict_type: FULL_STATE_DICT fsdp_sync_module_states: true fsdp_transformer_layer_cls_to_wrap: BertLayer fsdp_use_orig_params: true machine_rank: 0 main_training_function: main mixed_precision: bf16 num_machines: 1 num_processes: 2 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false عند استخدام DeepSpeed: compute_environment: LOCAL_MACHINE deepspeed_config: deepspeed_config_file: /home/user/configs/ds_zero3_config.json zero3_init_flag: true distributed_type: DEEPSPEED downcast_bf16: 'no' machine_rank: 0 main_training_function: main num_machines: 1 num_processes: 4 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false عند استخدام DeepSpeed مع إضافة Accelerate: compute_environment: LOCAL_MACHINE deepspeed_config: gradient_accumulation_steps: 1 gradient_clipping: 0.7 offload_optimizer_device: cpu offload_param_device: cpu zero3_init_flag: true zero_stage: 2 distributed_type: DEEPSPEED downcast_bf16: 'no' machine_rank: 0 main_training_function: main mixed_precision: bf16 num_machines: 1 num_processes: 4 rdzv_backend: static same_network: true tpu_env: [] tpu_use_cluster: false tpu_use_sudo: false use_cpu: false يُعَد أمر accelerate_launch الطريقة الموصَى بها لتشغيل سكربت التدريب على نظام موزع باستخدام مكتبة Accelerate والصنف Trainer مع المعاملات المُحدَّدة في الملف config_file.yaml الذي يُحفَظ في مجلد ذاكرة التخزين المؤقت للمكتبة Accelerate ويُحمَّل تلقائيًا عند تشغيل أمر accelerate_launch. يمكننا مثلًا تشغيل سكربت التدريب run_glue.py مع ضبط FSDP كما يلي: accelerate launch \ ./examples/pytorch/text-classification/run_glue.py \ --model_name_or_path google-bert/bert-base-cased \ --task_name $TASK_NAME \ --do_train \ --do_eval \ --max_seq_length 128 \ --per_device_train_batch_size 16 \ --learning_rate 5e-5 \ --num_train_epochs 3 \ --output_dir /tmp/$TASK_NAME/ \ --overwrite_output_dir ويمكن تحديد المعاملات من الملف config_file.yaml في سطر الأوامر كما يلي: accelerate launch --num_processes=2 \ --use_fsdp \ --mixed_precision=bf16 \ --fsdp_auto_wrap_policy=TRANSFORMER_BASED_WRAP \ --fsdp_transformer_layer_cls_to_wrap="BertLayer" \ --fsdp_sharding_strategy=1 \ --fsdp_state_dict_type=FULL_STATE_DICT \ ./examples/pytorch/text-classification/run_glue.py --model_name_or_path google-bert/bert-base-cased \ --task_name $TASK_NAME \ --do_train \ --do_eval \ --max_seq_length 128 \ --per_device_train_batch_size 16 \ --learning_rate 5e-5 \ --num_train_epochs 3 \ --output_dir /tmp/$TASK_NAME/ \ --overwrite_output_dir ويمكن مطالعة المقال الخاص بتشغيل سكربتات Accelerate على منصة Huggingface لمعرفة المزيد حول أمر accelerate_launch وعمليات الضبط المخصَّصة. الخاتمة وصلنا لختام مقالنا الذي وضحنا فيه استخدام صنف التدريب Trainer من مكتبة Transformers وتعرفنا على طريقة استخدامه وإعداد البيانات والنماذج ومعاملات التدريب التي سيستخدمها، كما ناقشنا أهم المزايا التي يقدمها هذا الصنف والتي تجعله خيارًا مثاليًا لتسريع تدريب النماذج وتحسين أدائها. ننصحكم بتجربة التقنيات التي شرحناها في هذا المقال والاستفادة منها في تحسين مشاريع الذكاء الاصطناعي. ترجمة -وبتصرّف- للقسم Trainer من توثيقات Hugging Face. اقرأ أيضًا المقال السابق: استخدام قوالب الدردشة Chat Templates للنماذج اللغوية الكبيرة LLMs إنشاء بنية مخصصة لنماذج Transformers في Hugging Face تعرف على مكتبة المحوّلات Transformers من منصة Hugging Face مشاركة نموذج ذكاء اصطناعي على منصة Hugging Face
  14. العقد Nodes هي البنى الأساسية لإنشاء الألعاب في محرك جودو Godot، فالعقدة هي كائن يمكنه تمثيل نوع معين من وظائف اللعبة، فقد تعرض بعض أنواع العقد رسومات graphics أو تشغّل رسومًا متحركة animation أو تمثّل نموذجًا ثلاثي الأبعاد 3D model لكائن. كما تحتوي العقدة أيضًا على مجموعة من الخاصيات properties التي تسمح بتخصيص سلوكها، ويعتمد اختيار أي عقدة على الوظيفة التي نحتاجها، ويمكن أن كل تكون عقدة كوحدة مستقلة تؤدي وظيفة معينة، كما يمكننا دمج العديد من هذه الوحدات أو العقد لتشكيل كائنات اللعبة بطريقة مرنة ومتكاملة. العمل مع العقد برمجيًا، العقد عبارة عن كائنات objects فهي تغلّف البيانات data والسلوك behavior، ويمكنها أن ترث خاصيات properties من عقد أخرى. لننقر الآن على زر + أو زر إضافة/إنشاء عقدة جديدة Add/Create a New Node في تبويب المشهد Scene بدلًا من استخدام أحد الاقتراحات الافتراضية للعقد التي يقترحها علينا جودو وذلك كما يلي: بعد النقر على زر إنشاء عقدة جديدة سنشاهد الآن تسلسل هرمي يتضمن كافة أنواع العقد المتاحة في محرك الألعاب جودو كما يلي: تندرج جميع العقد ذات الأيقونات الزرقاء على سبيل المثال ضمن الفئة Node2D، مما يعني أن هذه العقد سيكون لها خاصيات العقد ثنائية الأبعاد Node2D التي سنتحدث عنها لاحقًا بمزيد من التفصيل. نلاحظ أن القائمة طويلة جدًا، وسيكون صعبًا التمرير عبرها للعثور على العقدة التي نحتاجها في كل مرة، لذا يمكننا استخدام وظيفة البحث للعثور على العقدة المطلوبة باستخدام عدد صغير من الأحرف. مثلًا يمكننا العثور على عقدة Sprite2D بسرعة بكتابة الحرفين sp فقط في حقل البحث وستنتقل إليها مباشرة، بعدها ننقر على زر أنشئ Create لإضافة العقدة للمشهد. أصبح لدينا الآن العقدة Sprite2D في تبويب المشهد Scene، لذا نتأكد من تحديدها، ثم ننظر إلى تبويب الفاحص Inspector على الجانب الأيسر من شاشة محرر جودو حيث سنرى فيها جميع خاصيات العقدة التي حدّدناها. نلاحظ أن الخاصيات الظاهرة للعقدة ليست فقط التي تخص عقدة Sprite2D نفسها، بل تشمل أيضًا الخصائص التي ورثتها من العقد الأخرى التي جاءت قبلها في سلسلة من العقد وهي منظَّمة حسب مصدرها، حيث ترث العقدة Sprite2D العقدة Node2D التي ترث بدورها العقدة CanvasItem التي ترث بدورها العقدة Node الأساسية البسيطة. بعد إضافة هذه العقدة للعبتنا سنلاحظ أن الشخصية الرسومية Sprite لا تظهر كما هو متوقع في واجهة المستخدم. فالغرض من هذه العقدة هو عرض صورة أو خامة Texture. وبما أن الخاصية Texture في حاوية الفاحص Inspector فارغة حاليًا فلهذا السبب لم تظهر في نافذة العرض. لحسن الحظ يأتي كل مشروع جديد من جودو مع صورة باسم icon.svg يمكننا استخدامها حاليًا، وهذه الصورة هي أيقونة محرك الألعاب جودو، لذا سنسحبها من التبويب نظام الملفات Filesystem في الجانب الأيمن ونفلتها في الحقل الخاص بالخاصية Texture. ننقر لتوسيع قسم التحويل Transform في حاوية الفاحص Inspector، ونكتب ضمن خاصية الموضع Position القيمة 50 للمحور الأفقي x والقيمة 50 للمحور العمودي y لنحدد مكان ظهور عقدتنا داخل المشهد. كما يمكننا أيضًا النقر على الشخصية الرسومية Sprite وسحبها ضمن نافذة العرض، وسنرى أن قيم الموضع Position تتغير أثناء تحريكنا لها. إحدى الخصائص المهمة للعقد هي إمكانية ترتيبها في تسلسل هرمي من العقدة الأم إلى العقدة الابن، مما يتيح لنا تنظيم الكائنات داخل المشهد، على سبيل المثال يمكننا إنشاء عقدة رئيسية للاعب Player تحتوي على عقد فرعية متعددة مثل عقدة Sprite2D لتمثيل الشكل المرئي للاعب وعقدة فرعية أخرى AnimationPlayer لتحريك اللاعب. لنجرب إنشاء تسلسل عقد هرمي دعونا نحدد أولاً عقدتنا Sprite2D، ثم نضغط على زر الإضافة مرة أخرى لإضافة عقدة Sprite2D جديدة. بعد ذلك، لنسحب الأيقونة نفسها لليسار قليلًا، ونعين خاصية الخامة Texture الخاصة بهذه العقدة الجديدة. نلاحظ أن قيم الموضع Position للعقدة الأب تتغير أثناء تحريكها. لكن في حال فحص قيم الموضع Position للعقدة الابن سنجد أنها ما تزال (50,50). فقيمة خاصية التحويل Transform لها نسبية وتعتمد على عقدة الكائن الأب. إذا حركنا العقدة الأب، فإن جميع العناصر المرتبطة به أي كافة العقد الأبناء له ستتحرك معها تلقائيًا، لأنها مرتبطة به من حيث الموقع أو الدوران، أو أي تغييرات أخرى تجري عليه بينما إذا حركنا العقدة الابن فقط، فإن العقدة الأب لها لن تتأثر بحركتها المشاهد Scenes يمكن تشبيه المشهد Scenes في جودو على أنه حاوية تتضمن جميع كائنات أو عقد لعبتنا. يمكن أن تمثل هذه العقد شخصيات أو صور خلفية أو حتى الأكواد البرمجية التي تتحكم في كيفية تصرف الأشياء. ويمكن أن يكون المشهد بسيطًا ومكونًا كائن واحد أو معقدًا مثل مستوى كامل في اللعبة. فتجميع العقد مع بعضها البعض ضمن محرك ألعاب جودو يوفر لنا أداة قوية، ويمكّننا من إنشاء كائنات معقدة من وحدات البناء التي تمثلها العقد، فمثلًا قد تحتوي عقدة اللاعب Player في لعبتنا على العديد من العقد الأبناء المرتبطة بها مثل Sprite2D للعرض و AnimationPlayer لتحريكها و Camera2D لمتابعتها وغير ذلك. تُسمى مجموعة العقد المرتبة في بنية شجرية بالمشهد Scene، وسنوضح في المقالات اللاحقة من هذه السلسلة بشكل عملي كيفية استخدام المشاهد في تنظيم كائنات لعبتنا في أجزاء مستقلة تعمل جميعها مع بعضها البعض ونتعرف على الطرق الأمثل لإنشاء وتنظيم مشاهد لعبة جودو. الخاتمة وصلنا لختام مقالنا الذي تعرفنا فيه على مفهوم العقد Nodes في محرك الألعاب جودو Godot والتي تمثل اللبنات الأساسية التي يمكن من خلالها تطوير الألعاب بسهولة ومرونة. تسمح العقد بترتيب الكائنات داخل المشهد بشكل هرمي، مما يتيح للمطورين تنظيم الكائنات وإعطائها خصائص وسلوكيات متنوعة، من الرسومات إلى الرسوم المتحركة والنماذج ثلاثية الأبعاد. بالإضافة إلى ذلك، تعرفنا على مفهوم المشهد الذي يمثل مجموعة من العقد المرتبطة هرميًا ببعضها والذي يمكننا تصميم ألعاب جودو بطريقة منظمة. ترجمة -وبتصرّف- للقسم Nodes: Godot's building blocks من توثيقات Kidscancode. اقرأ أيضًا الرؤية التصميمية لمحرك اﻷلعاب جودو Godot طبقات الحاوية في جودو إنشاء وبرمجة مشاهد لعبة ثنائية الألعاب في محرك جودو إنشاء نسخ من المشاهد والعقد في جودو
  15. نشرح في هذا المقال قوالب الدردشة Chat Templates واستخدامها في النماذج اللغوية الكبيرة LLMs، فالدردشة تعتمد على محادثات مكونة من سلسلة رسائل متعددة بدلاً من النصوص المنفصلة كما هو الحال في النماذج اللغوية التقليدية، كما تحتوي كل رسالة في المحادثة على دور محدد مثل المستخدم User أو المساعد Assistant، بالإضافة إلى محتوى نص الرسالة، وبالتالي نحتاج إلى طريقة منظمة لتحويلها لصيغة تُمكّن النموذج Model من فهمها ومعالجتها بشكل صحيح وإنتاج استجابات ملائمة لها. تتمثل وظيفة القوالب Chat Templates في تحويل هذه المحادثات إلى تنسيق يمكن للنموذج Model تحليله وفهمه بطريقة مناسبة تمامًا كما هو الحال في عملية الترميز Tokenization. وتُعدّ القوالب جزءًا أساسيًا من عملية التحضير للنموذج فهي تعمل كآلية تنظم تحويل المحادثات المكونة من قوائم من الرسائل مثل رسائل المستخدم وردود المساعد، إلى سلسلة نصية واحدة متكاملة ومُهيَّأة بطريقة تناسب متطلبات النموذج، مما يضمن فهم النموذج للمدخلات بشكل صحيح ومعالجتها بكفاءة. مثال على قالب الدردشة باستخدام نموذج BlenderBot لنوضّح مثال على استخدام قالب دردشة باستخدام النموذج BlenderBot الذي يحتوي على قالب افتراضي بسيط مهمته إضافة مسافة أو فراغ بين الرسائل المتبادلة والمنفصلة عن بعضها لتصبح عبارة عن سلسلة نصية واحدة يستطيع النموذج معالجتها بسهولة، بدلاً من التعامل مع كل رسالة بشكل منفصل: from transformers import AutoTokenizer # تحميل المحول tokenizer = AutoTokenizer.from_pretrained("facebook/blenderbot-400M-distill") # إعداد المحادثة chat = [ {"role": "user", "content": "مرحبًا، كيف حالك؟"}, {"role": "assistant", "content": "أنا بخير. كيف يمكنني مساعدتك اليوم؟"}, {"role": "user", "content": "أريد أن أظهر كيفية عمل قوالب الدردشة!"}, ] # تطبيق قالب الدردشة tokenizer.apply_chat_template(chat, tokenize=False) سنلاحظ بعدها اختصار المحادثة بأكملها في سلسلة نصية واحدة. وإذا استخدمنا الإعداد الافتراضي tokenize=True، فستخضع هذه السلسلة النصية لتحويل وستُرمَّز هذه السلسلة النصية بشكل وحدات ترميز tokens. ملاحظة: وحدات الترميز tokens هي الأجزاء الأصغر من النص التي يمكن للنموذج معالجتها. مثال على قالب الدردشة باستخدام نموذج Mistral-7B-Instruct لنستخدم الآن مثالًا أكثر تعقيدًا وهو النموذج mistralai/Mistral-7B-Instruct-v0.1 للمحادثة ونوضح كيفية استخدام قالب دردشة مختلف يختلف عن النموذج البسيط السابق BlenderBot. from transformers import AutoTokenizer # تحميل المحول tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.1") # إعداد المحادثة chat = [ {"role": "user", "content": "مرحبًا، كيف حالك؟"}, {"role": "assistant", "content": "أنا بخير. كيف يمكنني مساعدتك اليوم؟"}, {"role": "user", "content": "أريد أن أشرح كيفية عمل قالب الدردشة!"}, ] # تطبيق قالب الدردشة بدون ترميز بشكل tokens formatted_chat = tokenizer.apply_chat_template(chat, tokenize=False) # طباعة النتيجة print(formatted_chat) نلاحظ هنا أن المرمِّز أضاف رموز التحكم [INST] و ‎[/INST]‎ هذه المرة للإشارة إلى بداية ونهاية رسائل المستخدم دون استخدامها مع رسائل المساعد، تساعد هذه الرموز النموذج على فهم الرسائل بشكل أفضل لأنها تتماشى مع الطريقة التي تدرب عليها، حيث دُرِّب النموذج الحالي باستخدام هذه الرموز على عكس النموذج BlenderBot. طريقة استخدام قوالب الدردشة قوالب الدردشة سهلة الاستخدام، فلاستخدامها ليس علينا سوى إنشاء قائمة بالرسائل مع مفاتيح الدور role والمحتوى content، ثم تمريرها إلى التابع apply_chat_template()‎، وسنحصل على خرج جاهز للاستخدام. يُفضَّل أيضًا استخدام add_generation_prompt=True لإضافة موجِّه توليد Generation Prompt عند استخدام قوالب الدردشة كدخل لتوليد النموذج. فيما يلي مثال لتجهيز الدخل للتابع model.generate()‎ باستخدام النموذج المساعد Zephyr: from transformers import AutoModelForCausalLM, AutoTokenizer # تعيين نقطة التحقق checkpoint = "HuggingFaceH4/zephyr-7b-beta" tokenizer = AutoTokenizer.from_pretrained(checkpoint) model = AutoModelForCausalLM.from_pretrained(checkpoint) # قد ترغب باستخدام تنسيق bfloat16 أو الانتقال إلى وحدة معالجة الرسومات GPU هنا # إعداد الرسائل messages = [ { "role": "system", "content": "أنت روبوت دردشة ودود ترد دائمًا بأسلوب القراصنة", }, {"role": "user", "content": "كم عدد الطائرات المروحية التي يمكن للإنسان تناولها في وجبة واحدة؟"}, ] # تطبيق قالب الدردشة وتحويله لشكل ملائم للنموذج tokenized_chat = tokenizer.apply_chat_template(messages, tokenize=True, add_generation_prompt=True, return_tensors="pt") # طباعة النص المستخرج بعد فك الترميز print(tokenizer.decode(tokenized_chat[0])) سيؤدي ذلك إلى الحصول على سلسلة نصية بتنسيق الدخل الذي يتوقعه النموذج Zephyr كما يلي: <|system|> أنت روبوت دردشة ودود يرد دائمًا بأسلوب القراصنة </s> <|user|> كم عدد الطائرات المروحية التي يمكن للإنسان تناولها في جلسة واحدة؟ </s> <|assistant|> نسّقنا الدخل ليناسب النموذج Zephyr وذلك على النحو التالي: <|system|>: يحدد أن المساعد هو روبوت دردشة ودود ويجب أن يجيب دائمًا بأسلوب القراصنة <|user|>: يحدد أن المستخدم يطرح السؤال <|assistant|>: يحدد الموضع الذي سيظهر فيه رد المساعد يمكننا الآن استخدام النموذج لتوليد استجابة لسؤال المستخدم كما يلي: outputs = model.generate(tokenized_chat, max_new_tokens=128) print(tokenizer.decode(outputs[0])) وسنحصل على النتيجة التالية: <|system|> أنت روبوت دردشة ودود يرد دائمًا بأسلوب القراصنة </s> <|user|> كم عدد الطائرات المروحية التي يمكن للإنسان تناولها في جلسة واحدة؟ </s> <|assistant|> يا رفيقي، يؤسفني أن أخبرك أن البشر لا يستطيعون تناول الطائرات المروحية. الطائرات المروحية ليست طعامًا، بل هي آلات طائرة. الطعام مخصص للأكل، مثل طبق غني من الخمر، أو وعاء شهي من الحساء، أو رغيف لذيذ من الخبز. أما الطائرات المروحية، فهي للنقل والحركة، وليس للأكل. لذلك، أقول لا شيء، يا أصدقائي. لا شيء على الإطلاق. استخدام خط أنابيب Pipeline آلي للدردشة خطوط الأنابيب Pipelines هي طريقة تلقائية ومبسطة لاستخدام النماذج اللغوية للدردشة، فهي عبارة عن واجهات جاهزة توفرها مكتبة Transformers من Hugging Face لتسهيل استخدام النماذج المختلفة دون الحاجة إلى كتابة الكثير من الكود. تدعم خطوط أنابيب توليد نصوص مدخلات الدردشة، مما يسهّل علينا استخدام نماذج الدردشة. وقد اعتدنا سابقًا على استخدام الصنف ConversationalPipeline المخصَّص، ولكنه أُهمِل الآن ودُمِجت وظائفه مع الصنف TextGenerationPipeline. لنستخدم مثال Zephyr مرة أخرى، ولكن باستخدام خط أنابيب هذه المرة كما يلي: from transformers import pipeline # إنشاء أنبوب لتوليد النصوص باستخدام النموذج HuggingFaceH4/zephyr-7b-beta pipe = pipeline("text-generation", "HuggingFaceH4/zephyr-7b-beta") # تعريف الرسائل بين المستخدم والنظام messages = [ { "role": "system", # دور النظام: تحديد سلوك المساعد "content": "أنت روبوت دردشة ودود يرد دائمًا بأسلوب القراصنة", # محتوى النظام }, {"role": "user", "content": "كم عدد طائرات الهليكوبتر التي يمكن للإنسان أن يأكلها في جلسة واحدة؟"}, # سؤال المستخدم ] # توليد النص بناءً على الرسائل print(pipe(messages, max_new_tokens=128)[0]['generated_text'][-1]) # طباعة آخر حرف من رد المساعد وهذه هي استجابة النموذج: {'role': 'assistant', 'content': "يا صديقي، أخشى أنني يجب أن أخبرك أن البشر لا يستطيعون أكل الطائرات الهليكوبتر. الطائرات الهليكوبتر ليست طعامًا، إنها آلات طيران. الطعام يجب أن يُؤكل، مثل طبق كبير من الخمر، أو وعاء شهي من الحساء، أو رغيف لذيذ من الخبز. ولكن الطائرات الهليكوبتر، هي للنقل والتحرك، وليست للأكل. لذلك، أقول لا شيء، يا أصدقائي. لا شيء على الإطلاق."} سيتولى خط الأنابيب جميع تفاصيل الترميز واستدعاء دالة apply_chat_template نيابة عنا. كل ما علينا فعله هو تهيئة خط الأنابيب وتمرير قائمة الرسائل إليه بعد تزويد النموذج بقالب الدردشة. ما هي موجّهات التوليد Generation Prompts مُوجّهات التوليد Generation Prompts هي تعليمات أو إشارات تضاف إلى المحادثة أو المدخلات التي تقدمها إلى النموذج لتحفيز استجابة معينة. وفي سياق النماذج اللغوية مثل ChatGPT، نستخدم هذه الموجّهات لتحديد كيفية بدء الاستجابة أو توجيه النموذج للاستجابة بطريقة معينة. فعندما نتفاعل مع نموذج دردشة مثل بوت الدردشة، علينا إرسال سلسلة من الرسائل تتضمن ما يقوله المستخدم مثل "مرحبًا" وما يرد به المساعد مثل "أهلاً، تشرفت بلقائك" وموجّه التوليد يوفر تعليمات إضافية تُضاف للمحادثة لتوجيه النموذج حول كيفية التفاعل. فيمكن لموجّهات التوليد أن تحدد مثلاً أين يجب أن يبدأ النموذج في الرد، أو كيف يجب أن يبدو الرد. نلاحظ أن التابع apply_chat_template يحتوي على الوسيط add_generation_prompt، حيث يخبر هذا الوسيط القالب Template بإضافة رموز tokens تشير إلى بداية استجابة البوت Bot، فمثلًا ليكن لدينا الدردشة التالية: messages = [ {"role": "user", "content": "مرحبًا"}, {"role": "assistant", "content": "أهلًا تشرفت بلقائك"}, {"role": "user", "content": "هل يمكنني طرح سؤال؟"} ] وستكون النتيجة كما يلي بدون موجّه التوليد وباستخدام قالب ChatML الذي رأيناه في مثال نموذج Zephyr: tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=False) """<|im_start|>user مرحبًا!<|im_end|> <|im_start|>assistant تشرفت بلقائك!<|im_end|> <|im_start|>user هل يمكنني طرح سؤال؟<|im_end|> """ وستكون النتيجة كما يلي عند استخدام موجّه التوليد: tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) """<|im_start|>user مرحبًا!<|im_end|> <|im_start|>assistant تشرفت بلقائك!<|im_end|> <|im_start|>user هل يمكنني طرح سؤال؟<|im_end|> <|im_start|>assistant """ إذًا، عندما نستخدم نماذج الدردشة، سنحتاج إلى إخبار النموذج Model بما يجب عليه فعله بالضبط، خصوصًا فيما يتعلق بكيفية الرد على المستخدم. على سبيل المثال، لن يعرف النموذج تلقائيًا أين يبدأ في كتابة استجابة البوت المساعد، لذا نضيف رموز أو موجهات توليد خاصة في المكان الذي نريد أن تبدأ فيه استجابة البوت. فالموجه الذي نضيفه هنا مثل <|im_start|>assistant، سيخبر النموذج بأن هناك بداية لاستجابة البوت في هذه النقطة، وبدونه، قد يخطئ النموذج في فهم السياق ويبدأ في كتابة استجابة في مكان غير مناسب، مثل متابعة رسالة المستخدم. ملاحظة: لا تحتاج كل النماذج إلى هذه الموجهات، فبعض النماذج مثل BlenderBot و LlaMA لا تستخدم أي رموز خاصة، لأن هذه النماذج تعرف تلقائيًا أين تبدأ استجابة البوت. وبالتالي، لن نحتاج لإضافة موجّهات توليد في هذه الحالة. هل يمكن استخدام قوالب الدردشة في التدريب يمكن استخدام قوالب الدردشة في التدريب، وهي طريقة جيدة للتأكّد من أن قالب الدردشة يطابق الرموز التي يراها النموذج أثناء التدريب، لذا يُوصَى بتطبيق قالب الدردشة كخطوة معالجة مسبقَة لمجموعة بياناتك، ويمكننا بعد ذلك ببساطة المتابعة مثل أي مهمة لتدريب نموذج لغوي آخر. يجب عند التدريب ضبط add_generation_prompt=False لأن الرموز المضافة لموجّه استجابة المساعد لن تكون مفيدة أثناء التدريب. ليكن لدينا المثال التالي: from transformers import AutoTokenizer from datasets import Dataset tokenizer = AutoTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-beta") chat1 = [ {"role": "user", "content": "أيّهما أكبر، القمر أم الشمس؟"}, {"role": "assistant", "content": "الشمس."} ] chat2 = [ {"role": "user", "content": "أيّهما أكبر، الفيروس أم البكتيريا؟"}, {"role": "assistant", "content": "البكتيريا."} ] dataset = Dataset.from_dict({"chat": [chat1, chat2]}) dataset = dataset.map(lambda x: {"formatted_chat": tokenizer.apply_chat_template(x["chat"], tokenize=False, add_generation_prompt=False)}) print(dataset['formatted_chat'][0]) وسنحصل على النتيجة التالية: <|user|> أيّهما أكبر، القمر أم الشمس؟</s> <|assistant|> الشمس.</s> بعد تطبيق قالب الدردشة وتنسيق المحادثات، نتابع التدريب على النموذج بنفس الطريقة التي نتبعها في تدريب نماذج اللغة الأخرى باستخدام العمود formatted_chat الذي يحتوي على المحادثات بتنسيق يتوافق مع طريقة التدريب التي يتوقعها النموذج. ملاحظة: إذا نسّقنا النص باستخدام apply_chat_template(tokenize=False)‎ ثم رمّزناه في خطوة منفصلة، فيجب أن نضبط الوسيط add_special_tokens=False، وإذا استخدمنا apply_chat_template(tokenize=True)‎، فلا داعي للقلق بشأن ذلك. تضيف بعض المرمِّزات رموزًا خاصة مثل <bos> و <eos> إلى النص الذي نرمّزه افتراضيًا، لذا يجب أن تتضمن قوالب الدردشة دائمًا جميع الرموز الخاصة التي نحتاجها، وبالتالي يمكن أن تؤدي إضافة رموز خاصة إضافية باستخدام add_special_tokens=True الافتراضي إلى ظهور رموز خاصة غير صحيحة أو مكرَّرة، مما سيضر بأداء النموذج. متقدم: دخل إضافي لقوالب الدردشة الوسيط الوحيد الذي يتطلبه apply_chat_template هو messages، ولكن يمكننا تمرير أيّ وسيط كلمات مفتاحية Keyword Argument إلى apply_chat_template وسيكون متاحًا ضمن القالب، مما يمنحنا قدرة على استخدام قوالب الدردشة للعديد من الأغراض. لا توجد قيود على أسماء أو تنسيق هذه الوسطاء، حيث يمكن تمرير السلاسل النصية أو القوائم أو القواميس أو أيّ شيء آخر تريده. توجد بعض حالات الاستخدام الشائعة لهذه الوسطاء الإضافية مثل تمرير أدوات لاستدعاء الدوال أو مستندات للتوليد باستخدام الاسترجاع المعزَّز Retrieval-augmented، حيث توجد بعض التوصيات حول ما يجب أن تكون عليه أسماء وتنسيقات هذه الوسطاء والتي سنوضحها لاحقًا، لذا نشجّع مطوري النماذج على جعل قوالب الدردشة الخاصة بهم متوافقة مع هذا التنسيق لتسهيل نقل الشيفرة البرمجية لاستدعاء الأدوات فيما بين النماذج. متقدم: استخدام الأدوات واستدعاء الدوال يمكن للنماذج اللغوية الكبيرة LLMs الخاصة باستخدام الأدوات اختيار استدعاء الدوال كأدوات خارجية قبل توليد إجابة، حيث يمكن ببساطة تمرير قائمة من الدوال إلى الوسيط tools عند تمرير الأدوات إلى نموذج استخدام الأدوات Tool-use كما يلي: import datetime def current_time(): """احصل على الوقت المحلي الحالي كسلسلة نصية.""" return str(datetime.now()) def multiply(a: float, b: float): """ دالة ضرب رقمين المعاملات: a: الرقم الأول b: الرقم الثاني """ return a * b tools = [current_time, multiply] model_input = tokenizer.apply_chat_template( messages, tools=tools ) علينا الدوال بالتنسيق السابق لكي تعمل بالطريقة الصحيحة، وبالتالي يمكن تحليلها تحليلًا صحيحًا بوصفها أدوات، لذا يجب علينا اتباع القواعد التالية: يجب أن يكون للدالة اسم يصف عملها يجب أن يكون لكل وسيط تلميح لنوعه Type Hint يجب أن يكون للدالة سلسلة نصية توثيقية Docstring وفق نمط جوجل Google المعياري أو وصف أولي للدالة، وتتبعه كتلة Args:‎ التي تصف الوسطاء، إلّا في حالة عدم احتواء الدالة على وسطاء لا تضمّن الأنواع في كتلة Args:‎ فمثلًا نكتب a:الرقم الأول وليس a(int):الرقم الأول حيث يتوجب علينا وضع تلميحات الأنواع في ترويسة الدالة بدلًا من ذلك يمكن أن يكون للدالة نوع للقيمة المعادة وكتلة Returns:‎ في السلسلة النصية التوثيقية، ولكنها اختيارية لأن معظم نماذج استخدام الأدوات تتجاهلها تمرير نتائج الأدوات إلى النموذج تكفي الشيفرة البرمجية التجريبية السابقة لسرد الأدوات المتاحة لنموذجنا، ولكن إذا أردنا استخدام أداة فعليًا، فيجب أن: نحلّل خرج النموذج للحصول على اسم أو أسماء الأدوات ووسطائها نضيف استدعاء أو استدعاءات أداة النموذج إلى المحادثة نستدعي الدالة أو الدوال المقابلة مع تلك الوسطاء نضيف النتيجة أو النتائج إلى المحادثة مثال كامل لاستخدام أداة سنستخدم في هذا المثال نموذج Hermes-2-Pro بحجم 8B لأنه أحد أعلى نماذج استخدام الأدوات أداءً في فئته الحجمية حاليًا. إذا كان لدينا ذاكرة كافية، فيمكن التفكير في استخدام نموذج أكبر بدلًا من ذلك مثل النموذجين Command-R أو Mixtral-8x22B، ويدعم كلاهما استخدام الأدوات ويقدّمان أداءً أقوى. لنحمّل أولًا النموذج والمرمِّز كما يلي: import torch from transformers import AutoModelForCausalLM, AutoTokenizer checkpoint = "NousResearch/Hermes-2-Pro-Llama-3-8B" # تحميل المرمِّز Tokenizer باستخدام نقطة التحقق checkpoint وتحديد النسخة المناسبة tokenizer = AutoTokenizer.from_pretrained(checkpoint, revision="pr/13") # تحميل النموذج Model باستخدام نقطة التحقق checkpoint، مع تحديد نوع البيانات bfloat16 وتوزيع النموذج على الأجهزة device_map model = AutoModelForCausalLM.from_pretrained(checkpoint, torch_dtype=torch.bfloat16, device_map="auto") ثم نعرّف قائمة الأدوات كما يلي: def get_current_temperature(location: str, unit: str) -> float: """ الحصول على درجة الحرارة الحالية في موقع معين. Args: location: الموقع الذي سيتم الحصول على درجة حرارته، بصيغة "المدينة، البلد". unit: الوحدة التي سيتم إرجاع درجة الحرارة بها. (الخيارات: ["مئوية"، "فهرنهايت"]). Returns: درجة الحرارة الحالية في الموقع المحدد بوحدات القياس المحددة، كعدد عشري (float). """ return 22. # يُحتمل أن تقوم الدالة الحقيقية بالحصول على درجة الحرارة الفعلية def get_current_wind_speed(location: str) -> float: """ الحصول على سرعة الرياح الحالية بالكيلومتر في الساعة (km/h) في موقع معين. Args: location: الموقع الذي سيتم الحصول على سرعة الرياح فيه، بصيغة "المدينة، البلد". Returns: سرعة الرياح الحالية في الموقع المحدد بالكيلومتر في الساعة (km/h)، كعدد عشري (float). """ return 6. # يُحتمل أن تقوم الدالة الحقيقية بالحصول على سرعة الرياح الفعلية tools = [get_current_temperature, get_current_wind_speed] لنُعِدّ الآن محادثة البوت كما يلي: messages = [ {"role": "system", "content": "أنت بوت يجيب على استفسارات الطقس. يجب أن ترد بوحدة القياس المستخدمة في الموقع الذي تم الاستفسار عنه."}, {"role": "user", "content": "مرحبًا، ما هي درجة الحرارة في باريس الآن؟"} ] ونطبّق قالب الدردشة ونولّد استجابةً كما يلي: inputs = tokenizer.apply_chat_template(messages, chat_template="tool_use", tools=tools, add_generation_prompt=True, return_dict=True, return_tensors="pt") inputs = {k: v.to(model.device) for k, v in inputs.items()} out = model.generate(**inputs, max_new_tokens=128) print(tokenizer.decode(out[0][len(inputs["input_ids"][0]):])) سنحصل على النتيجة التالية: <tool_call> {"arguments": {"location": "باريس، فرنسا", "unit": "مئوية"}, "name": "get_current_temperature"} </tool_call><|im_end|> استدعى النموذج الدالة مع وسطاء صالحة وبالتنسيق الذي تطلبه السلسلة النصية التوثيقية للدالة، واستدل النموذج أننا نشير إلى باريس في فرنسا، وتذكّرَ أنه يجب بالتأكيد عرض درجة الحرارة في فرنسا بالدرجة المئوية باعتبارها موطن نظام الوحدات الدولي. سنضيف الآن استدعاء الأداة الخاصة بالنموذج إلى المحادثة، حيث نولّد معرّف tool_call_id عشوائي. لا تستخدم جميع النماذج هذه المعرّفات، ولكنها تسمح للنماذج بإنشاء استدعاءات أدوات متعددة في وقتٍ واحد وتتبع الاستجابة المقابلة لكلّ استدعاء. يمكنك توليد هذه المعرّفات بأيّ طريقة تريدها، ولكن يجب أن تكون فريدة في كل دردشة. tool_call_id = "vAHdf3" # معرّف عشوائي، ويجب أن يكون فريدًا لكل استدعاء أداة tool_call = {"name": "get_current_temperature", "arguments": {"location": "باريس، فرنسا", "unit": "مئوية"}} messages.append({"role": "assistant", "tool_calls": [{"id": tool_call_id, "type": "function", "function": tool_call}]}) أضفنا استدعاء الأداة إلى المحادثة، ويمكننا الآن استدعاء الدالة وإضافة النتيجة إلى المحادثة، حيث نستخدم في هذا المثال دالة وهمية تعيد القيمة 22.0 دائمًا، لذا يمكننا إضافة هذه النتيجة مباشرةً. يجب أن يتطابق المعرّف tool_call_id مع المعرّف المستخدَم في استدعاء الأداة السابق. messages.append({"role": "tool", "tool_call_id": tool_call_id, "name": "get_current_temperature", "content": "22.0"}) أخيرًا، سندع المساعد يقرأ خرج الدالة ويتابع الدردشة مع المستخدم كما يلي: inputs = tokenizer.apply_chat_template(messages, chat_template="tool_use", tools=tools, add_generation_prompt=True, return_dict=True, return_tensors="pt") inputs = {k: v.to(model.device) for k, v in inputs.items()} out = model.generate(**inputs, max_new_tokens=128) print(tokenizer.decode(out[0][len(inputs["input_ids"][0]):])) وسنحصل على النتيجة التالية: درجة الحرارة الحالية في باريس، فرنسا هي 22.0 درجة مئوية وضّحنا مثالًا بسيطًا باستخدام أدوات وهمية واستدعاء واحد، ولكن ستعمل التقنية نفسها مع أدوات حقيقية متعددة ومحادثات أطول، ويمكن أن تكون هذه الطريقة فعّالة لتوسيع قدرات وكلاء المحادثة باستخدام معلومات في الوقت الحقيقي أو أدوات حسابية مثل الآلات الحاسبة أو الوصول إلى قواعد بيانات كبيرة. ملاحظة: لا تستخدم كل النماذج جميع ميزات استدعاء الأدوات السابقة، إذ تستخدم بعض النماذج معرّفات استدعاء الأدوات، ويستخدم البعض الآخر ببساطة اسم الدالة ويطابق استدعاءات الأدوات مع النتائج باستخدام الترتيب، وتوجد عدة نماذج لا تستخدم أي من هاتين الطريقتين وتنشئ استدعاء أداة واحد فقط في كل مرة لتجنب الالتباس. إذا أردنا أن تكون شيفرتنا البرمجية متوافقة مع أكبر عدد ممكن من النماذج، فيُوصَى ببناء استدعاءات الأدوات الخاصة بنا كما وضّحنا سابقًا، وإعادة نتائج الأدوات بالترتيب الذي أنشأه النموذج، ويجب أن نتعامل قوالب الدردشة في كل نموذج مع المهام المتبقية. فهم مخططات الأدوات Tool Schemas تُحوَّل كل دالة تمرّرها إلى الوسيط tools الخاص بالدالة apply_chat_template إلى مخطط JSON، ثم تُمرَّر هذه المخططات إلى قالب دردشة النموذج، حيث لا ترى نماذج استخدام الأدوات دوالك مباشرةً، ولا ترى الشيفرة البرمجية الفعلية التي بداخلها أبدًا، فما يهمها هو تعريفات الدوال والوسطاء التي تحتاج إلى تمريرها إليها، إذ تهتم بما تفعله الأدوات وكيفية استخدامها، وليس بكيفية عملها. الأمر متروك لك لقراءة خرج هذه الدوال واكتشاف طلبها لاستخدام أداة وتمرير وسطائها إلى دالة الأداة وإعادة الاستجابة في الدردشة. يجب أن يكون توليد مخططات JSON لتمريرها إلى القالب تلقائيًا وغير مرئي بما أن دوالك تتبع المواصفات السابقة، ولكن إذا واجهنا مشكلات، أو أردنا مزيدًا من التحكم في التحويل، فيمكن التعامل مع التحويل يدويًا. فيما يلي مثال على تحويل مخطط يدويًا: from transformers.utils import get_json_schema def multiply(a: float, b: float): """ دالة تقوم بضرب عددين الوسائط: a: العدد الأول الذي سيتم ضربه b: العدد الثاني الذي سيتم ضربه """ return a * b schema = get_json_schema(multiply) print(schema) وسنحصل على النتيجة التالية: { "type": "function", "function": { "name": "multiply", "description": "دالة تقوم بضرب عددين", "parameters": { "type": "object", "properties": { "a": { "type": "number", "description": "العدد الأول الذي سيتم ضربه" }, "b": { "type": "number", "description": "العدد الثاني الذي سيتم ضربه" } }, "required": ["a", "b"] } } } يمكن أيضًا تعديل هذه المخططات أو كتابتها من الصفر بنفسنا بدون استخدام get_json_schema على الإطلاق، حيث يمكن تمرير مخططات JSON مباشرةً إلى الوسيط tools في apply_chat_template، مما يمنحنا قدرًا كبيرًا من القوة لتعريف مخططات دقيقة لدوال أكثر تعقيدًا، ولكن يجب توخي الحذر، فكلما كانت المخططات أكثر تعقيدًا، كلما زاد احتمال ارتباك النموذج عند التعامل معها. يُوصى باستخدام توقيعات دوال بسيطة simple function signatures إن أمكن ذلك مع إبقاء الحد الأدنى من الوسطاء وخاصة الوسطاء المعقدة والمتداخلة. فيما يلي مثال لتعريف المخططات يدويًا وتمريرها إلى apply_chat_template مباشرة: # دالة بسيطة لا تأخذ أي وسائط current_time = { "type": "function", "function": { "name": "current_time", "description": "الحصول على الوقت المحلي الحالي كسلسلة نصية.", "parameters": { 'type': 'object', 'properties': {} } } } # دالة أكثر اكتمالًا تأخذ وسيطين عدديين multiply = { 'type': 'function', 'function': { 'name': 'multiply', 'description': 'دالة لضرب عددين', 'parameters': { 'type': 'object', 'properties': { 'a': { 'type': 'number', 'description': 'العدد الأول الذي سيتم ضربه' }, 'b': { 'type': 'number', 'description': 'العدد الثاني الذي سيتم ضربه' } }, 'required': ['a', 'b'] } } } model_input = tokenizer.apply_chat_template( messages, tools=[current_time, multiply] ) متقدم: التوليد المعزز بالاسترجاع Retrieval-augmented Generation يمكن للتوليد المعزز بالاسترجاع Retrieval-augmented Generation -أو RAG اختصارًا- الخاص بالنماذج اللغوية الكبيرة LLM البحث في مجموعة من المستندات للحصول على معلومات قبل الرد على استعلام، مما يسمح للنماذج بتوسيع قاعدة المعرفة الخاصة بها بما يتجاوز حجم السياق المحدود. يجب أن يأخذ قالب نماذج RAG الوسيط documents، والذي يجب أن يمثّل قائمة من المستندات، حيث يكون كل مستند قاموسًا واحدًا مع مفتاحي العنوان title والمحتويات contents، وهما سلاسل نصية. لا توجد دوال مساعدة ضرورية لأن هذا التنسيق أبسط بكثير من مخططات JSON المُستخدَمة مع الأدوات. فيما يلي مثال لقالب RAG: document1 = { "title": "القمر: عدونا القديم", "contents": "لطالما حلم الإنسان بتدمير القمر. في هذه المقالة، سأناقش..." } document2 = { "title": "الشمس: صديقنا القديم", "contents": "على الرغم من قلة تقديرها في كثير من الأحيان، إلا أن الشمس تقدم العديد من الفوائد الملحوظة..." } model_input = tokenizer.apply_chat_template( messages, documents=[document1, document2] ) متقدم: كيف تعمل قوالب الدردشة يُخزَّن قالب الدردشة الخاص بالنموذج في السمة tokenizer.chat_template، ويُستخدَم القالب الافتراضي لصنف هذا النموذج عند عدم ضبط قالب دردشة. لنلقِ نظرة أولًا على قالب BlenderBot: >>> from transformers import AutoTokenizer >>> tokenizer = AutoTokenizer.from_pretrained("facebook/blenderbot-400M-distill") >>> tokenizer.default_chat_template "{% for message in messages %}{% if message['role'] == 'user' %}{{ ' ' }}{% endif %}{{ message['content'] }}{% if not loop.last %}{{ ' ' }}{% endif %}{% endfor %}{{ eos_token }}" قد يكون ذلك معقدًا بعض الشيء، لذا لننظّفه قليلًا لجعله مقروءًا أكثر، ونتأكّد في هذه العملية أيضًا من أن الأسطر الجديدة والمسافات البادئة التي نضيفها غير مُضمَّنة في خرج القالب كما سنوضّح في قسم إزالة المسافات البيضاء من السلاسل النصية Trimming Whitespace لاحقًا. {%- for message in messages %} {%- if message['role'] == 'user' %} {{- ' ' }} {%- endif %} {{- message['content'] }} {%- if not loop.last %} {{- ' ' }} {%- endif %} {%- endfor %} {{- eos_token }} يُعَد ذلك محرك قوالب Jinja، والتي هي لغة قوالب تسمح بكتابة شيفرة برمجية بسيطة تولّد نصًا، حيث تشبه شيفرتها البرمجية وصياغتها لغة بايثون Python، إذ سيبدو القالب السابق في لغة بايثون كما يلي: for idx, message in enumerate(messages): if message['role'] == 'user': print(' ') print(message['content']) if not idx == len(messages) - 1: # التحقق من الرسالة الأخيرة في المحادثة print(' ') print(eos_token) ينجز هذا القالب ثلاثة أشياء هي: إذا كانت الرسالة رسالة مستخدم، فسيضيف مسافة فارغة قبلها بالنسبة لكل رسالة، وإلا فلن يطبع شيئًا يضيف محتوى الرسالة إن لم تكن الرسالة هي الرسالة الأخيرة، فسيضيف مسافتَين بعدها، ويطبع رمز EOS بعد الرسالة الأخيرة هذا القالب بسيط جدًا، فهو لا يضيف رموز تحكم ولا يدعم رسائل النظام، ويُعَد طريقة شائعة لإعطاء النموذج توجيهات حول كيفية التصرّف في المحادثة اللاحقة. تمنحك لغة القوالب Jinja الكثير من المرونة لهذه الأشياء، لذا لنوضّح فيما يلي قالب Jinja الذي يمكنه تنسيق الدخل بطريقة مماثلة للطريقة التي ينسقّ قالب LLaMA بها هذا الدخل، حيث يتضمن قالب LLaMA الحقيقي معالجة رسائل النظام الافتراضية ورسائل النظام المختلفة قليلًا، ولكن لا نستخدم هذا القالب في شيفرتنا البرمجية الفعلية. {%- for message in messages %} {%- if message['role'] == 'user' %} {{- bos_token + '[INST] ' + message['content'] + ' [/INST]' }} {%- elif message['role'] == 'system' %} {{- '<<SYS>>\\n' + message['content'] + '\\n<</SYS>>\\n\\n' }} {%- elif message['role'] == 'assistant' %} {{- ' ' + message['content'] + ' ' + eos_token }} {%- endif %} {%- endfor %} يضيف هذا القالب رموزًا Tokens محددة بناءً على دور كل رسالة، والتي تمثّل مَن أرسلها، إذ يمكن أن يميّز النموذج بين رسائل المستخدم والمساعد والنظام بوضوح بسبب الرموز الموجودة ضمنها. متقدم: إضافة وتعديل قوالب الدردشة سنوضّح فيما يلي كيفية إضافة وتعديل قوالب الدردشة. كيف ننشئ قالب دردشة Chat Templates إنشاء قالب دردشة أمر بسيط، حيث نكتب قالب Jinja ونضبط السمة tokenizer.chat_template، ولكن قد نجد أن من الأسهل البدء بقالب موجود مسبقًا من نموذج آخر وتعديله لتلبية احتياجاتنا، فمثلًا يمكن أخذ قالب LLaMA السابق وإضافة الرموز "[ASST]" و"[‎/ASST]" إلى رسائل المساعد كما يلي: {%- for message in messages %} {%- if message['role'] == 'user' %} {{- bos_token + '[INST] ' + message['content'].strip() + ' [/INST]' }} {%- elif message['role'] == 'system' %} {{- '<<SYS>>\\n' + message['content'].strip() + '\\n<</SYS>>\\n\\n' }} {%- elif message['role'] == 'assistant' %} {{- '[ASST] ' + message['content'] + ' [/ASST]' + eos_token }} {%- endif %} {%- endfor %} وما علينا الآن سوى ضبط السمة tokenizer.chat_template، وسيستخدم التابع apply_chat_template()‎ قالبنا الجديد في المرة التالية التي تستخدمه فيها. تُحفَظ هذه السمة في الملف tokenizer_config.json، لذا يمكننا استخدام التابع push_to_hub()‎ لتحميل القالب الجديد إلى مستودع FacHugging e Hub والتأكد من أن الجميع يستخدمون القالب الصحيح لنموذجنا. template = tokenizer.chat_template template = template.replace("SYS", "SYSTEM") # تغيير رمز النظام tokenizer.chat_template = template # ضبط القالب الجديد tokenizer.push_to_hub("model_name") # رفع قالبك الجديد إلى مستودع‫ Hub يستدعي الصنف TextGenerationPipeline التابع apply_chat_template()‎ الذي يستخدم قالب الدردشة الخاص بك، لذا سيصبح النموذج متوافقًا تلقائيًا مع هذا الصنف بعد ضبط قالب الدردشة الصحيح. ملاحظة: عند صقل Fine-tune نموذج دردشة باستخدام قالب الدردشة، يجب إضافة رموز تحكم جديدة إلى المرمِّز بوصفها رموزًا خاصة. هذه الرموز لا تُقسَم أبدًا، مما يعني أنها ستظل دائمًا تعامل كرموز فردية بدلاً من أن تُقسَّم إلى أجزاء أثناء عملية الترميز. بالإضافة إلى ذلك، يجب تعيين سمة أداة المرمِّز eos_token إلى الرمز الذي يمثل نهاية عمليات التوليد الخاصة بالبوت في قالبنا. هذا يضمن أن أدوات توليد النص تتعرف بشكل صحيح على اللحظة التي يجب فيها التوقف عن توليد النص. لماذا تحتوي بعض النماذج قوالب متعددة تستخدم بعض النماذج قوالب مختلفة لحالات استخدام مختلفة، فمثلًا قد نستخدم قالبًا للدردشة العادية وقالبًا آخر لاستخدام الأداة أو التوليد المعزز بالاسترجاع، حيث تكون السمة tokenizer.chat_template قاموسًا في هذه الحالات. قد يؤدي ذلك إلى بعض الارتباك، لذا يُوصَى باستخدام قالب واحد لجميع حالات الاستخدام إن أمكننا ذلك. يمكننا استخدام تعليمات Jinja مثل if tools is defined وتعريفات ‎{% macro %}‎ لتغليف مسارات الشيفرة البرمجية المتعددة في قالب واحد بسهولة. إذا كان للمرمِّز قوالب متعددة، فستكون السمة tokenizer.chat_template قاموسًا dict، حيث يكون كل مفتاح هو اسم القالب. يمتلك التابع apply_chat_template معالجة خاصة لأسماء قوالب معينة، حيث يبحث عن قالب باسم default في معظم الحالات، ويعطي خطأ إن لم يتمكن من العثور عليه، ولكن إذا كان القالب tool_use موجودًا عندما يمرّر المستخدم الوسيط tools، فسيستخدمه بدلًا من ذلك. يمكن الوصول إلى القوالب ذات الأسماء الأخرى من خلال تمرير اسم القالب الذي نريده إلى الوسيط chat_template الخاص بالتابع apply_chat_template()‎. قد يكون ذلك مربكًا بعض الشيء للمستخدمين، لذا إذا أدرنا كتابة قالب بنفسنا، فيُوصى باستخدام قالب واحد إن أمكن ذلك. ما القالب الذي يجب استخدامه يجب أن نتأكد من أن القالب يتطابق مع تنسيق الرسالة الذي شاهده النموذج أثناء التدريب عند ضبط هذا القالب للنموذج المُدرَّب مسبقًا للدردشة، وإلّا فقد ينخفض الأداء. يحدث الشيء نفسه حتى إن درّبنا النموذج أكثر، فمن المحتمل أن نحصل على الأداء الأفضل إذا أبقينا رموز الدردشة ثابتة، ويُعَد ذلك مشابهًا جدًا للترميز، حيث نحصل على الأداء الأفضل للاستدلال Inference أو الصقل Fine-tuning عندما نطابق بدقة الترميز المُستخدَم أثناء التدريب. إذا درّبنا نموذجًا من الصفر أو صقلنا نموذج لغوي أساسي للدردشة، فلدينا الحرية لاختيار قالب مناسب، حيث تتمتع النماذج اللغوية الكبيرة LLM بالذكاء الكافي لتعلّم كيفية التعامل مع الكثير من تنسيقات الدخل المختلفة، وأحد الخيارات الشائعة هو تنسيق ChatML الذي يُعَد خيارًا جيدًا ومرنًا للعديد من حالات الاستخدام، والذي يبدو كما يلي: {%- for message in messages %} {{- '<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n' }} {%- endfor %} إذا أعجبنا التنسيق السابق، فلدينا صيغة مؤلفةً من سطر واحد وجاهزةً للنسخ في شيفرتنا البرمجية، حيث تتضمن هذه الصيغة دعمًا لموجّهات التوليد Generation Prompts، ولكنها لا تضيف رموز BOS أو EOS. إذا توقّع نموذجنا هذه الرموز، فلن يضيفها التابع apply_chat_template تلقائيًا، حيث سيُرمَّز النص باستخدام add_special_tokens=False لتجنب التعارضات المحتملة بين القالب والمنطق البرمجي add_special_tokens. إذا توقّع نموذجنا رموزًا خاصة، فعلينا التأكّد من إضافتها إلى القالب. tokenizer.chat_template = "{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}" يغلّف هذا القالب كل رسالة برمزَي ‎<|im_start|>‎ و ‎<|im_end|>‎، ويكتب الدور كسلسلة نصية، مما يسمح بالمرونة في الأدوار التي تتدرب بها، وسيبدو الخرج كما يلي: <|im_start|>system أنت روبوت محادثة مفيد يبذل قصارى جهده لعدم قول شيء سخيف يجذب الانتباه على تويتر.<|im_end|> <|im_start|>user كيف حالك؟<|im_end|> <|im_start|>assistant أنا بأفضل حال!<|im_end|> تُعَد أدوار المستخدم والنظام والمساعد هي الأدوار المعيارية للدردشة، ويُوصَى باستخدامها عندما يكون ذلك منطقيًا، وخاصةً إذا أردنا أن يعمل نموذجنا بنجاح مع الصنف TextGenerationPipeline، ولكن لا يقتصر الأمر على هذه الأدوار، إذ تُعَد عملية إنشاء القوالب مرنة ويمكن أن تمثّل أيّ سلسلة نصية دورًا. كيف نبدأ بإضافة قوالب الدردشة إذا كان لدينا نموذج دردشة، فيجب ضبط السمة tokenizer.chat_template الخاصة به واختباره باستخدام التابع apply_chat_template()‎، ثم دفع المُرمِّز المحدَّث إلى مستودع Hugging Face Hub. يُطبَّق الشيء نفسه حتى إن لم نكن مالكي النموذج، فإذا استخدمنا نموذجًا مع قالب دردشة فارغ أو نموذجًا لا يزال يستخدم قالب الصنف الافتراضي، فيجب فتح طلب سحب Pull Request إلى مستودع النموذج حتى نتمكّن من ضبط هذه السمة بطريقة صحيحة. نصبح جاهزين بعد ضبط هذه السمة، إذ سيعمل التابع tokenizer.apply_chat_template الآن بنجاح مع هذا النموذج، مما يعني أنه مدعوم تلقائيًا أيضًا في أماكن أخرى مثل الصنف TextGenerationPipeline. يمكننا ضمان استفادة المجتمع بالكامل من الإمكانيات الكاملة للنماذج مفتوحة المصدر من خلال التأكد من تضمين هذه السمة في النماذج. فقد كان عدم تطابق التنسيق مشكلة قائمة في هذا المجال، مما أثر سلبًا على الأداء لفترة طويلة، ولذلك حان الوقت لوضع حد لهذه المشكلة. متقدم: نصائح لكتابة القوالب إن لم نكن على دراية بلغة القوالب Jinja، فسنجد أن أسهل طريقة لكتابة قالب دردشة هي أولًا كتابة كود قصير بلغة بايثون ينسّق الرسائل بالطريقة التي نريدها ثم تحويل هذا السكربت إلى قالب. لنتذكّر أن معالج القالب سيستقبل سجل المحادثة كمتغير بالاسم messages، وسنتمكّن من الوصول إلى هذا المتغير في قالبنا كما تفعل في لغة بايثون، حيث يمكننا تكراره ضمن حلقة باستخدام التعليمة {% for message in messages %} أو الوصول إلى الرسائل الفردية باستخدام التعليمة ‎{{ messages[0] }}‎ مثلًا. يمكن أيضًا استخدام النصائح التالية لتحويل شيفرتنا البرمجية إلى لغة القوالب Jinja. إزالة المسافات البيضاء من السلاسل النصية ستطبع لغة القوالب Jinja افتراضيًا أي مسافات Whitespace مثل الفراغات والسطور الجديدة تأتي قبل أو بعد كتلة ما، والذي قد يشكّل مشكلة بالنسبة لقوالب الدردشة التي نريد أن تكون دقيقة مع الفراغات، حيث يمكن تجنب ذلك من خلال كتابة قوالبك كما يلي: {%- for message in messages %} {{- message['role'] + message['content'] }} {%- endfor %} بدلًا من الطريقة التالية: {% for message in messages %} {{ message['role'] + message['content'] }} {% endfor %} ستؤدي إضافة الرمز - إلى إزالة أي مسافة بيضاء تأتي قبل الكتلة. قد يبدو أن المثال الثاني لا يسبب مشكلات، ولكنه قد يتضمّن الخرجُ السطرَ الجديد والمسافة البادئة، وهو الشيء الذي نريد تجنبّه. حلقات For تبدو حلقات For في لغة Jinja كما يلي: {%- for message in messages %} {{- message['content'] }} {%- endfor %} نلاحظ أن كل ما يوجد ضمن {{ كتلة التعبير }} سيُطبَع في الخرج، ويمكنك استخدام معاملات مثل + لدمج السلاسل النصية ضمن كتل التعبير. تعليمات If تبدو تعليمات If في لغة Jinja كما يلي: {%- if message['role'] == 'user' %} {{- message['content'] }} {%- endif %} تستخدم لغة بايثون المسافات البيضاء لتمييز بدايات ونهايات كتل for و if، وتطلب لغة Jinja إنهاءها صراحةً باستخدام {% endfor %} و {% endif %}. المتغيرات الخاصة يمكن الوصول إلى قائمة الرسائل messages داخل قالبنا، ولكن يمكنك أيضًا الوصول إلى العديد من المتغيرات الخاصة الأخرى، والتي تتضمّن الرموز الخاصة مثل bos_token و eos_token، بالإضافة إلى المتغير add_generation_prompt الذي ناقشناه سابقًا. يمكن أيضًا استخدام المتغير loop للوصول إلى معلومات حول تكرار الحلقة الحالي مثل استخدام ‎{% if loop.last %}‎ للتحقق مما إذا كانت الرسالة الحالية هي الرسالة الأخيرة في المحادثة. فيما يلي مثال يجمع بين هذه الأفكار لإضافة موجّه توليد في نهاية المحادثة إذا كانت قيمة add_generation_prompt هي True: {%- if loop.last and add_generation_prompt %} {{- bos_token + 'المساعد:\n' }} {%- endif %} التوافق مع لغة قوالب Jinja التي لا تستخدم لغة بايثون توجد تطبيقات متعددة للغة قوالب Jinja التي تستخدم لغات مختلفة، ويكون لها عادةً الصياغة نفسها، ولكن الاختلاف الرئيسي هو إمكانية استخدام توابع بايثون عند كتابة قالب باستخدام بايثون مثل استخدام التابع ‎.lower()‎ مع السلاسل النصية أو التابع ‎.items()‎ مع القواميس. سيتوقف ذلك إذا حاول شخص ما استخدام قالبك على تطبيق للغة قوالب Jinja التي لا تستخدم لغة بايثون، فالتطبيقات التي لا تستخدم لغة بايثون شائعة الاستخدام وخاصةً في بيئات النشر حيث تحظى لغة جافاسكربت JS ورَست Rust بشعبية كبيرة. فيما يلي بعض التغييرات السهلة التي يمكن إجراؤها على قوالبنا لضمان توافقها مع جميع تطبيقات Jinja: نستخدم مرشّحات Jinja بدل توابع بايثون، حيث يكون لها الاسم نفسه عادة، فمثلًا يتحوّل string.lower()‎ إلى string|lower، ويتحوّل dict.items()‎ إلى dict|items. أحد التغييرات الملحوظة هو أن string.strip()‎ يصبح string|trim. ويمكن مطالعة قائمة المرشحات المضمنة في توثيق Jinja لمزيد من المعلومات نضع true و false و none بدل True و False و None الخاصة بلغة بايثون قد يؤدي عرض القاموس أو القائمة مباشرةً إلى نتائج مختلفة في تطبيقات أخرى، فمثلًا قد تتغير إدخالات السلسلة النصية من علامتي اقتباس مفردتين إلى علامتي اقتباس مزدوجتين، لذا يمكن أن تساعدنا إضافة مرشّح tojson في ضمان التناسق الخاتمة بهذا نكون وصلنا لختام مقالنا الشامل الذي شرحنا فيه كيفية تحويل الدردشات إلى تنسيق قابل للاستخدام في نماذج Hugging Face باستخدام قوالب الدردشة Chat Templates، وتعرفنا على أمثلة مختلفة توضح طريقة تطبيقها لتحسين التفاعل بين النظام والمستخدم والتأكد من أن البيانات المدخلة تتماشى مع طريقة تدريب النموذج. ترجمة -وبتصرّف- للقسم Templates for Chat Models من توثيقات Hugging Face. اقرأ أيضًا كيف تبدأ في مجال الذكاء الاصطناعي مشاركة نموذج ذكاء اصطناعي على منصة Hugging Face تدريب نموذج ذكاء اصطناعي على تلخيص النصوص باستخدام المكتبة Transformers أسئلة شائعة حول الذكاء الاصطناعي
×
×
  • أضف...