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

Ola Abbas

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

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

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

كل منشورات العضو Ola Abbas

  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 أسئلة شائعة حول الذكاء الاصطناعي
  16. سنتعرّف في هذه السلسلة من المقالات على محرك الألعاب جودو Godot بنسخته الرابعة والميزات التي يوفّرها، وسنبدأ مقال اليوم بشرح واجهة محرك جودو وأهم مكوناتها، ونوضّح التغييرات الرئيسية التي سنلاحظها عند الانتقال لهذا الإصدار من محرك الألعاب. ما هو محرك الألعاب جودو؟ سنتعرّف فيما يلي على محرك الألعاب جودو وسبب استخدامه والميزات التي يوفّرها. محركات الألعاب Game Engines يُعَد تطوير الألعاب أمرًا معقدًا ومثيرًا بنفس الوقت، وهو يتطلب مجموعة متنوعة من المعارف والمهارات، إذ يجب أن تتوفر لدينا الكثير من التقنيات الأساسية لبناء لعبة حديثة قبل أن تتمكّن من إنشائها، تخيل لو كان علينا بناء جهاز الحاسوب الخاص بنا وكتابة نظام التشغيل الخاص به قبل أن نبدأ حتى في البرمجة ألن يكون الأمر غاية في الصعوبة؟ سيكون تطوير الألعاب مشابهًا لذلك في حال أردنا البدء من الصفر وبناء كل شيء نحتاجه بأنفسنا. هنالك أيضًا عدد من الاحتياجات المشتركة لكل لعبة نريد بناءها، فمثلًا سنحتاج أي لعبة إلى رسم أشياء على الشاشة بغض النظر عن نوعها، لذا إذا كانت الشيفرة البرمجية اللازمة للرسم مكتوبة مسبقًا، فسيكون من المنطقي أن نعيد استخدامها بدلًا من إنشائها من جديد لكل لعبة، وهنا يأتي دور محركات الألعاب. فمحرّك الألعاب هو مجموعة من الأدوات والتقنيات المُصمَّمة لمساعدة مطوري الألعاب، مما يسمح لنا بالتركيز أكثر على بناء لعبتنا دون الحاجة لإعادة اختراع العجلة، حيث سنوضّح فيما يلي بعض الميزات التي يوفرها محرك الألعاب الجيد: التصيير أو الإخراج Rendering ثنائي الأبعاد وثلاثي الأبعاد: هو عملية عرض اللعبة على شاشة اللاعب، ويجب أن يعمل محرك التصيير الجيد مع ميزات وحدة معالجة الرسوميات GPU الحديثة، ويدعم الشاشات عالية الدقة، ويحسن المؤثرات مثل الإضاءة ومنظور اللعب Perspective مع الحفاظ في الوقت نفسه على معدّل إطارات مرتفع لضمان تجربة لعب سلسة الفيزياء Physics: يُعَد إنشاء محرك فيزيائي دقيق وقابل للاستخدام مهمة ضخمة، إذ تتطلب معظم الألعاب اكتشاف التصادم والاستجابة له، وتحتاج العديد منها إلى محاكاة فيزيائية مثل الاحتكاك والعطالة أو القصور الذاتي Inertia وغير ذلك، ولن يرغب المطورون بتكبد عناء كتابة الشيفرة البرمجية لهذه المهمة دعم المنصات: قد تحتاج لإصدار لعبتك على منصات متعددة كالهاتف المحمول والويب والحاسوب الشخصي والطرفية Console، فمحرّك الألعاب الجيد يتيح لنا بناء لعبتنا مرة واحدة وتصديرها إلى منصة أو منصات أخرى بيئة التطوير: تُجمَع كل هذه الأدوات في تطبيق واحد، مما يؤدي إلى وجود كل شيء في بيئة واحدة حتى لا تضطر إلى تعلّم سير عمل جديد لكل مشروع تعمل عليه هناك عشرات محركات الألعاب الشهيرة التي يمكننا الاختيار من بينها مثل جودو GoDot ويونتي Unity وآن ريل Unreal وغيرها. وتجدر الإشارة لأن غالبية محرّكات الألعاب الشهيرة هي منتجات تجارية، فقد تكون مجانية للتنزيل وقد لا تكون كذلك، وأنها قد تتطلّب اتفاقية ترخيص أو حقوق ملكية إذا أردنا إصدار ألعابنا وخاصة إذا حقّقت أرباحًا، لذا يجب قراءة وفهم ما نوافق عليه وما يُسمح وما لا يُسمح بفعله عندما ننوي استخدام أي محرك. لماذا نستخدم محرك ألعاب جودو يُعَد محرّك ألعاب جودو مجانيًا ومفتوح المصدر، وهو يصدر وفق ترخيص MIT المرن، فلا توجد رسوم أو تكاليف مخفية أو حقوق ملكية يجب دفعها، ويُعَد جودو محرك ألعاب حديث ومتكامل الميزات. وهو يوفر الكثير من الفوائد لمطوري الألعاب، لأنه غير مثقل بالترخيص التجاري، ويوفر لنا سيطرة كاملة على كيفية ومكان توزيع لعبتنا. كما أن طبيعة جودو مفتوحة المصدر توفر مستوى شفافية أعلى من محركات الألعاب التجارية، فمثلًا إذا وجدنا أن ميزة معينة لا تلبي احتياجاتنا تمامًا، فيمكننا تعديل المحرّك دون الحاجة إلى إذن لذلك. اكتشاف واجهة محرر الألعاب جودو سنكتشف فيما يلي واجهة محرك الألعاب جودو التي ستقضي فيها معظم وقتك عند بناء لعبتك. مدير المشروع Project Manager أول شيء سنراه عند فتح محرك ألعاب جودو هو نافذة مدير المشروع Project Manager، والتي ستبدو كما يلي: سنرى في هذه النافذة قائمة بمشاريع جودو، حيث يمكننا اختيار مشروع موجود مسبقًا والنقر على زر تشغيل Run لتشغيل اللعبة أو النقر على زر تحرير Edit للعمل على اللعبة في محرّر جودو. لنبدأ بالنقر على زر مشروع جديد New Project، في حال لم يكن لدينا أي مشاريع حتى الآن. يمكننا في النافذة السابقة إعطاء اسم للمشروع، وإنشاء مجلد لتخزينه ملفات لعبتنا فيه. ملاحظة: يوجد مشروع جودو في مجلد خاص به، ويمثل ذلك العديد من الفوائد بما في ذلك تسهيل نقل المشاريع ومشاركتها والنسخ الاحتياطي لها، ويجب أيضًا أن تكون جميع ملفات المشروع من صور وملفات صوتية وما إلى ذلك داخل مجلد المشروع. يجب أن نحرص على اختيار اسم يصف عمل مشروع لعبتنا عند تسميته، فمثلًا لا يعد الاسم New Game Project23 جيدًا لكونه لا يساعدنا في تذكر ما يفعله هذا المشروع. يجب أيضًا أن نفكر في التوافق، فقد تكون بعض أنظمة التشغيل حساسة لحالة الأحرف وبعضها الآخر غير حساس لحالة الأحرف، مما يؤدي لحدوث مشكلات إذا نقلنا المشروع أو شاركناه من حاسوب لآخر، لذا يعتمد العديد من المبرمجين قواعد تسمية موحّدة مثل عدم وجود مسافات بين الكلمات واستخدام شرطة سفلية _ بينها. سنسمّي المشروع الجديد getting_started، لذا نكتب هذا الاسم ونتأكد من تفعيل خيار أنشئ مجلد Create Folder، قبل النقر على زر إنشاء وتعديل Create & Edit أسفل النافذة. يمثل الشكل التالي نافذة محرر محرك الألعاب جودو، وهو المكان الذي سنقضي فيه معظم وقتنا عند العمل في جودو، ويكون المحرر مقسمًا إلى أقسام كما يلي: نافذة العرض Viewport: المكان الذي نرى فيه أجزاء لعبتنا أثناء العمل عليها مساحات العمل Workspaces: في الجزء العلوي الأوسط حيث يمكننا التبديل بين العمل في مساحات العمل ثنائية الأبعاد 2D أو ثلاثية الأبعاد 3D أو السكربت، ولكن تكون البداية من مساحة العمل ثلاثية الأبعاد أزرار اختبار اللعب Playtest Buttons: تتيح لنا هذه الأزرار تشغيل اللعبة والتحكم فيها أثناء الاختبار الحاويات Docks أو التبويبات Tabs: توجد على جانبي واجهة جودو عدد من الحاويات Docks والتبويبات حيث يمكننا من خلالها عرض عناصر اللعبة وضبط خاصياتها اللوحة السفلية Bottom Panel: تتضمن هذه اللوحة معلومات خاصة بالسياق لأدوات مختلفة، وأهمها لوحة الخرج Output، حيث سنرى رسائل الأخطاء أو المعلومات عند تشغيل لعبتنا إعدادات المشروع Project Settings تحدثنا عن الأجزاء الرئيسية لواجهة محرر جودو وكيفية عملها، ولنتحدّث الآن عن إعدادات المشروع، فإحدى المهام الأولى عند بدء مشروع جديد هي التأكد من صحة الإعدادات. ننقر على خيار مشروع Project من القائمة ثم نحدّد خيار إعدادات المشروع Project Settings. النافذة السابقة هي نافذة إعدادات المشروع، وتوجد على الجهة اليمنى قائمة من الفئات. تكون الإعدادات الافتراضية مناسبة لمعظم المشاريع، فلا حاجة للقلق بشأن تغييرها إلّا إن كان لدينا خيار محدد يتطلب التغيير. سنلقي نظرة حاليًا على قسم التطبيق Application ثم إعداد Config حيث يمكننا من هنا ضبط عنوان اللعبة، واختيار المشهد الرئيسي الذي سنوضّحه لاحقًا، كما يمكننا من هنا تغيير أيقونة اللعبة. القسم الثاني هو قسم الإظهار Display ثم نافذة Window، وهو المكان الذي يمكننا من تحديد طريقة ظهور لعبتنا وعرضها. إذ يمكننا ضبط العرض width والارتفاع height لضبط حجم نافذة اللعبة، فمثلًا إذا أنشأنا لعبة لهاتف محمول، فيجب ضبطها على دقة وأبعاد الجهاز المستهدف. توجد أيضًا إعدادات لتغيير الحجم Scaling والتمدّد Stretching ووضع ملء الشاشة وغير ذلك. سنترك الحجم الافتراضي كما هو حاليًا، حيث سنتحدث لاحقًا عن كيفية ضبط هذه الإعدادات بدقة لتشغيل اللعبة على أجهزة مختلفة. توجد أيضًا بعض التبويبات في الجزء العلوي من النافذة مثل التبويب عام General الذي تحدّثنا عنه. سنتحدث الآن بإيجاز عن تبويب خريطة الإدخال Input Map، وهو المكان الذي يمكننا فيه تحديد إجراءات إدخال مختلفة للتحكم في إجراءات الإدخال أي كيف نتعامل مع مدخلات لوحة المفاتيح والفأرة وغير ذلك. حيث نهتم في لعبتنا في تحديد المفتاح أو الزر الذي يضغط عليه اللاعب، وتحديد الإجراء الذي سيحدث عند الضغط عليه للتعامل مع مدخلات اللاعب بكفاءة. يوجد أيضًا تبويبات أخرى مثل تبويب التوطين Localization المخصص لدعم لغات متعددة، وتبويب إضافات Plugins التي أنشأ معظمها مجتمع محرك ألعاب جودو، والتي يمكن تنزيلها وإضافتها لتوفير مزيد من الميزات والأدوات المختلفة وغير ذلك وسنتحدث عنها لاحقًا. دليل الانتقال من الإصدار Godot 3.x للإصدار Godot 4.0 سنوضّح فيما يلي التغييرات الرئيسية والمشاكل التي يجب الانتباه إليها إذا أردتَ الانتقال إلى الإصدار 4.0 لمحرك ألعاب جودو. الأسماء الجديدة أكبر تغيير في الإصدار Godot 4 هو توفر مجموعة كاملة من عمليات إعادة التسمية للعقد والدوال وأسماء الخاصيات، حيث تجري معظم هذه العمليات لجعل الأمور متناسقة أو واضحة. إليك فيما يلي بعض التغييرات الكبرى التي يجب الانتباه إليها: العقد ثنائية الأبعاد 2D وثلاثية الأبعاد 3D: حملت العقد ثنائية الأبعاد في الإصدار Godot 3.x اللاحقة 2D، ولكن لم يكن للعقد ثلاثية الأبعاد لاحقة، لذا أصبحت الآن جميع العقد تحمل إما اللاحقة 2D أو اللاحقة 3D لجعل الأمور متناسقة مثل RigidBody2D و RigidBody3D أعيدت تسمية العقدة Spatial إلى Node3D في الفئة ثلاثية الأبعاد لتتناسب معها أعيدت تسمية واحدة من أكثر العقد شهرة وهي KinematicBody إلى CharacterBody2D أو CharacterBody3D، وسنوضّح لاحقًا مزيدًا من تغييرات الواجهة البرمجية API لهذه العقدة. أعيدت تسمية الدالة instance()‎ الخاصة بالعقدة PackedScene إلى instantiate()‎ حلت الخاصيات position و global_position محل الخاصيات translation و global_translation للعقد ثلاثية الأبعاد، مما يجعلها متوافقة مع العقد ثنائية الأبعاد الإشارات Signals والعناصر القابلة للاستدعاء Callables أصبح العمل مع الإشارات منظمًا أكثر في الإصدار 4.0، حيث أصبح النوع Signal نوعًا أصيلًا الآن، لذا سنستخدم عددًا أقل من السلاسل النصية، مما يعني أننا سنحصل على ميزة الإكمال التلقائي والتحقق من الأخطاء. ينطبق الأمر ذاته أيضًا على الدوال، والتي يمكن الآن الرجوع إليها مباشرةً بدلًا من استخدام السلاسل النصية. فيما يلي مثال لتعريف إشارة وتوصيلها وإرسالها: extends Node signal my_signal func _ready(): my_signal.connect(signal_handler) func _input(event): if event.is_action_pressed("ui_select"): my_signal.emit() func signal_handler(): print("signal received") عناصر الانتقال التدريجي Tweens في حال استخدام SceneTreeTween في الإصدار Godot 3.5، فسنكون على دراية باستخدام عناصر الانتقال التدريجي Tween التي تُستخدَم مثلًا لتغيير اللون أو الموقع الخاص بالكائن تدريجيًا في الإصدار Godot 4.0. لم يَعُد Tween عقدة، لذا يمكننا إنشاء كائنات رسوم متحركة للانتقال التدريجي Tween لمرة واحدة كلما احتجنا إليها، وستصبح أكثر قوة وأسهل في الاستخدام من الطريقة القديمة بعد التعود عليها. العقدة AnimatedSprite[2D|3D]‎ يُعَد اختفاء الخاصية playing أكبر تغيير لمستخدمي الإصدار 3‎.x‎ لهذه العقدة، حيث أصبحت أكثر تناسقًا مع استخدام AnimationPlayer، إذ يمكن تبديل التشغيل التلقائي في لوحة SpriteFrames لتشغيل الرسوم المتحركة تلقائيًا. وعلينا استخدام الدالتين play()‎ و stop()‎ في الشيفرة البرمجية للتحكم في التشغيل. العقدة CharacterBody[2D|3D]‎ أكبر تغيير في هذه العقدة هو استخدام الدالة move_and_slide()‎ التي لم تَعُد تستقبل معاملات، حيث فقد أصبحت جميع المعاملات خاصيات مُدمَجة، ويتضمن ذلك الخاصية velocity الأصيلة، لذا فلا حاجة للتصريح عن هذه الخاصيات. يمكن الاطلاع على محارف المنصة ومحارف FPS الأساسية للحصول على أمثلة تفصيلية لاستخدام هذه العقد. عقدة TileMap جُدّدت العقدة TileMap بالكامل في الإصدار 4.0 ابتداءً من كيفية إنشاء موارد TileSet إلى كيفية رسم عناصر الرقعة Tiles والتفاعل معها. مولّد الأعداد العشوائية RNG هناك بعض التغييرات على دوال توليد الأعداد العشوائية المُدمَجة مع لغة البرمجة GDScript، وهذه التغييرات هي: لم نعد بحاجة لاستدعاء الدالة randomize()‎، إذ سيكون الاستدعاء تلقائيًا. إذا أردنا الحصول على عشوائية قابلة للتكرار، نستخدم الدالة seed()‎ لضبطها على قيمة محدَّدة مسبقًا. حلّت الدالة randf_range()‎ للأعداد العشرية أو الدالة randi_range()‎ للأعداد الصحيحة محل الدالة القديمة rand_range()‎. كشف تصادم الأشعة Raycasting توجد واجهة برمجة تطبيقات جديدة عند كشف تصادم الأشعة لاكتشاف التصادم بين الكائنات في الشيفرة البرمجية، حيث تأخذ الدالة PhysicsDirectSpaceState[2D|3D].intersect_ray()‎ كائنًا خاصًا كمعامل، ويحدّد هذا الكائن خاصيات الشعاع، فمثلًا نستخدم ما يلي لكشف تصادم الأشعة في فضاء ثلاثي الأبعاد: var space = get_world_3d().direct_space_state var ray = PhysicsRayQueryParameters3D.create(position, destination) var collision = space.intersect_ray(ray) if collision: print("ray collided") الخاتمة تعرفنا في هذا المقال على محرك الألعاب جودو بنسخته الرابعة، واكتشفنا أهم ميزاته، كما استعرضنا واجهته الرئيسية مثل مدير المشروع ومحرر الألعاب وتعرفنا على كيفية استخدامه لإنشاء مشاريع جديدة وضبط الإعدادات. بالإضافة إلى ذلك، سطنا الضوء على أبرز التغييرات التي سنلاحظها عند الانتقال من الإصدار 3‎.‎x إلى الإصدار 4.0، مثل إعادة تسمية العقد والدوال وتعديل طريقة العمل مع الإشارات والعناصر القابلة للاستدعاء. سنعود لنافذة إعدادات المشروع لاحقًا، لذا لنغلقها الآن ونستعد للانتقال إلى مقالنا التالي من هذه السلسلة والذي سنشرح فيه مفهوم العقد Nodes وطريقة التعامل معها في محرك الألعاب جودو. ترجمة -وبتصرّف- للأقسام What is Godot و The Godot Editor و Migrating from 3.x من توثيقات Kidscancode. اقرأ أيضًا الرؤية التصميمية لمحرك اﻷلعاب جودو Godot كتابة برنامجك الأول باستخدام جودو Godot العقد Nodes والمشاهد Scenes في جودو Godot تعرف على أشهر محركات الألعاب Game Engines
  17. سنضيف في هذا المقال بعض الميزات المتقدمة الاختيارية لموقع مدونة جانغو Django الخاص بنا، والذي أنشأناه في المقالات السابقة من هذه السلسلة، بما في ذلك ميزة ترقيم الصفحات Pagination، والمنشورات ذات الصلة Related Posts، وميزة البحث Search. إنشاء ترقيم الصفحات Pagination في جانغو قد يكون إنشاء مرقّم صفحات فكرة جيدة عند إضافة الكثير من المنشورات إلى مدونتنا، فنحن لا نريد عرض عدد كبير جدًا من المنشورات في صفحة واحدة، لذا يجب إضافة شيفرة برمجية إضافية إلى دوال العرض view functions. لنأخذ على سبيل المثال عرض الصفحة الرئيسية home، حيث يجب أولًا أن نستورد بعض الحزم الضرورية لتفعيل الترقيم كما يلي: from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger نحدّث بعد ذلك العرض home كما يلي: def home(request): site = Site.objects.first() categories = Category.objects.all() tags = Tag.objects.all() featured_post = Post.objects.filter(is_featured=True).first() # إضافة مرقم الصفحات page = request.GET.get("page", "") # الحصول على رقم الصفحة الحالية posts = Post.objects.all().filter(is_published=True) paginator = Paginator(posts, n) # ‫عرض n منشور لكل صفحة try: posts = paginator.page(page) except PageNotAnInteger: posts = paginator.page(1) except EmptyPage: posts = paginator.page(paginator.num_pages) return render( request, "home.html", { "site": site, "posts": posts, "categories": categories, "tags": tags, "featured_post":featured_post }, ) يجب مراعاة ثلاثة شروط مختلفة في الأسطر من 12 إلى 17 في الشيفرة البرمجية السابقة، فإذا كان رقم الصفحة عددًا صحيحًا، فيجب إعادة الصفحة المطلوبة، وإن لم يكن رقم الصفحة عددًا صحيحًا، فيجب إعادة الصفحة 1، وإذا كان رقم الصفحة أكبر من عدد الصفحات، فيجب إعادة الصفحة الأخيرة. بعد أن انتهينا من إعداد الترقيم في الكود البرمجي للعرض، يتوجب علينا إضافة مرقّم الصفحات في القالب Template لكي يظهر في صفحة قائمة المنشورات، أي سنضيفه في الملف templates/vendor/list.html كما يلي: <!-- مرقم الصفحات --> <nav class="isolate inline-flex -space-x-px rounded-md mx-auto my-5 max-h-10" aria-label="Pagination" > {% if posts.has_previous %} <a href="?page={{ posts.previous_page_number }}" class="relative inline-flex items-center rounded-l-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20" > <span class="sr-only">Previous</span> <!-- Heroicon name: mini/chevron-left --> <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" > <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" /> </svg> </a> {% endif %} {% for i in posts.paginator.page_range %} {% if posts.number == i %} <a href="?page={{ i }}" aria-current="page" class="relative z-10 inline-flex items-center border border-blue-500 bg-blue-50 px-4 py-2 text-sm font-medium text-blue-600 focus:z-20" >{{ i }}</a > {% else %} <a href="?page={{ i }}" aria-current="page" class="relative inline-flex items-center border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20" >{{ i }}</a > {% endif %} {% endfor %} {% if posts.has_next %} <a href="?page={{ posts.next_page_number }}" class="relative inline-flex items-center rounded-r-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20" > <span class="sr-only">Next</span> <!-- Heroicon name: mini/chevron-right --> <svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" > <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> </svg> </a> {% endif %} </nav> علينا تطبيق الشيء نفسه لجميع الصفحات التي تعرض قائمة منشورات مثل صفحة الوسوم والفئات وصفحة البحث. عرض المنشورات ذات الصلة Related Posts في جانغو نريد الآن إضافة ميزة عرض المنشورات ذات الصلة بالمنشور الحالي، أي المنشورات التي لها نفس وسوم هذا المنشور ويمكن القيام بذلك من خلال إضافة الشيفرة التالية: def post(request, slug): site = Site.objects.first() requested_post = Post.objects.get(slug=slug) categories = Category.objects.all() tags = Tag.objects.all() # المنشورات ذات الصلة ## الحصول على جميع الوسوم المتعلقة بهذا المقال post_tags = requested_post.tag.all() ## ترشيح جميع المنشورات التي تحتوي على وسوم متعلقة بالمنشور الحالي، واستبعاد المنشور الحالي related_posts_ids = ( Post.objects.all() .filter(tag__in=post_tags) .exclude(id=requested_post.id) .values_list("id") ) related_posts = Post.objects.filter(pk__in=related_posts_ids) return render( request, "post.html", { "site": site, "post": requested_post, "categories": categories, "tags": tags, "related_posts": related_posts, }, ) قد تكون الشيفرة البرمجية السابقة صعبة الفهم بعض الشيء، دعنا نحللها ونشرحها بالتفصيل، حيث يمثل السطر 3 الحصول على المنشور المطلوب باستخدام المتغير slug، ويمثل السطر 9 الحصول على جميع الوسوم Tags التي تعود إلى المنشور المطلوب. تصبح الأمور أعقد في الأسطر من 11 إلى 16، حيث يسترد التابع Post.objects.all()‎ جميع المنشورات من قاعدة البيانات، ثم يسترد التابع filter(tag__in=post_tags)‎ جميع المنشورات التي تحتوي على وسوم مرتبطة بالمنشور الحالي، ولكن لدينا مشكلتان، إذ سيُضمَّن المنشور الحالي في مجموعة الاستعلام، لذا سنستخدم التابع exclude(id=requested_post.id)‎ لاستبعاد المنشور الحالي. لنبسّط الآن المشكلة الثانية، ولنفترض أن لدينا السيناريو التالي مع وجود ثلاث منشورات وثلاثة وسوم: معرّف الوسم Tag ID اسم الوسم Tag Name 1 Tag 1 2 Tag 2 3 Tag 3 معرّف المنشور Post ID اسم المنشور Post Name 1 Post 1 2 Post 2 3 Post 3 وتكون العلاقة بين المنشورات والوسوم علاقة متعدد إلى متعدد Many-to-Many. معرّف الوسم Tag ID معرّف المنشور Post ID 1 2 1 3 1 1 2 1 2 2 2 3 3 2 معرّف المنشور Post ID معرّف الوسم Tag ID 1 1 1 2 2 1 2 2 2 3 3 1 3 2 لنفترض أن المنشور الحالي هو المنشور 2، وبالتالي ستكون الوسوم المتعلقة به هي 1 و 2 و 3، حيث سيذهب جانغو أولًا إلى الوسم 1 عند استخدام التابع filter(tag__in=post_tags)‎، ثم سيجد المنشورات المتعلقة بالوسم 1، والتي هي المنشورات 2 و 3 و 1، ثم يذهب إلى الوسم 2، ويجد المنشورات المتعلقة بالوسم 2، وينتقل أخيرًا إلى الوسم 3. يعيد التابع filter(tag__in=post_tags)‎ بعد ذلك في النهاية القائمة [2,3,1,1,2,3,2]، وستُعاد القائمة [3,1,1,3] بعد تنفيذ التابع exclude()‎، ولا نريد ذلك أيضًا، حيث نحتاج لإيجاد طريقة للتخلص من التكرارات، لذا يجب استخدام التابع values_list('id')‎ لتمرير معرّفات المنشورات إلى المتغير related_posts_ids، ثم نستخدم هذا المتغير لاسترداد المنشورات ذات الصلة، وبذلك نتخلص من التكرار. يمكننا عرض المنشورات ذات الصلة في القالب المقابل كما يلي: <!-- المنشورات ذات الصلة --> <div class="grid grid-cols-3 gap-4 my-5"> {% for post in related_posts %} <!-- المنشور --> <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md"> <a href="{% url 'post' post.slug %}" ><img class="rounded-t-md object-cover h-60 w-full" src="{{ post.featured_image.url }}" alt="..." /></a> <div class="m-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ post.created_at|date:"F j, o" }} </div> <h2 class="text-lg font-bold">{{ post.title }}</h2> <p class="text-base"> {{ post.content|striptags|truncatewords:30 }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{% url 'post' post.slug %}" >Read more →</a > </div> </div> {% endfor %} </div> دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن تطبيق عملية البحث في جانغو سنعمل أخيرًا على إضافة ميزة البحث لمدونتنا، حيث نحتاج إلى استمارة بحث Search Form في الواجهة الأمامية، والتي ترسل استعلام بحث إلى العرض، تسترد دالة العرض السجلات الملائمة من قاعدة البيانات، وتعيد صفحة بحث تعرض النتيجة. استمارة البحث Search Form لنضيف أولًا استمارة بحث إلى الشريط الجانبي Sidebar في الملف templates/vendor/sidebar.html كما يلي: <div class="col-span-1"> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Search</div> <div class="p-4"> <form action="{% url 'search' %}" method="POST" class="grid grid-cols-4 gap-2"> {% csrf_token %} <input type="text" name="q" id="search" class="border rounded-md w-full focus:ring p-2 col-span-3" placeholder="Search something..." /> <button type="submit" class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-full focus:ring col-span-1" > Search </button> </form> </div> </div> . . . </div> لاحظ في الأسطر من 7 إلى 13 السمة name لحقل الإدخال input، حيث سنسميها q، سيُربط دخل المستخدم بالمتغير q ويُرسَل للواجهة الخلفية. إذا نقرنا على الزر في السطر 5، فسيُوجَّه المستخدم إلى عنوان URL بالاسم search، لذا يجب تسجيل نمط عنوان URL المقابل كما يلي: path('search', views.search, name='search'), عرض البحث Search View سيكون عرض البحث Search View كما يلي: def search(request): site = Site.objects.first() categories = Category.objects.all() tags = Tag.objects.all() query = request.POST.get("q", "") if query: posts = Post.objects.filter(is_published=True).filter(title__icontains=query) else: posts = [] return render( request, "search.html", { "site": site, "categories": categories, "tags": tags, "posts": posts, "query": query, }, ) قالب البحث Search Template لإنشاء قالب البحث لنكتب الكود التالي في الملف templates/search.html: {% extends 'layout.html' %} {% block title %} <title>Page Title</title> {% endblock %} {% block content %} <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1"> {% include "vendor/list.html" %} </div> {% include "vendor/sidebar.html" %} </div> {% endblock %} بعدها نحاول البحث عن شيء ما في استمارة البحث، ويجب إعادة المنشورات التي نطلبها فقط. الخلاصة بهذا نكون قد وصلنا لنهاية مقالنا الذي وضحنا فيه طريقة إضافة بعض الميزات المتقدمة الاختيارية لموقع مدونة جانغو Django الخاص بنا بما في ذلك مرقّم الصفحات Paginator والمنشورات ذات الصلة وميزة البحث، لتحسين تجربة المستخدم وتوفير طريقة أكثر تنظيمًا للوصول إلى محتوى المدونة. إذا كان لديكم أي استفسارات أو تعليقات حول هذا المقال أو حول جانغو بشكل عام، فلا تترددوا في طرحها في قسم المناقشة أسفل المقال أو في قسم الأسئلة والأجوبة في الأكاديمية. ترجمة -وبتصرّف- للمقال Django for Beginners #5 - Some Advanced Features لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: إنشاء تطبيق مدونة كامل في جانغو استخدام عمليات CRUD لإدارة مدونة في جانغو مدخل إلى إطار عمل الويب جانغو Django البدء في إنشاء مدونة بسيطة في جانغو
  18. صُمِّمت مكتبة المحوّلات Transformers المتخصصة في بناء نماذج الذكاء الاصطناعي من منصة Huggingface بحيث يمكن توسيعها بسهولة، وتُكتَب النماذج Models بالكامل في مجلد فرعي محدَّد من المستودع بدون تجريد أو إخفاء لأي من تفاصيل العمل، لذا يمكننا بسهولة نسخ أي ملف نموذج وتعديله وفقًا لاحتياجاتنا. وإذا أردنا كتابة نموذج Model جديد خاص بنا، فيمكن البدء بالنموذج من الصفر. سنوضّح في هذا المقال كيفية كتابة نموذج مخصَّص وضبطه Configuration لنتمكّن من استخدامه بشكل يتوافق مع مكتبة المحوّلات Transformers، وسنوضّح كيفية مشاركته مع المجتمع مع شيفرته البرمجية ليتمكن أي شخص من استخدامه، حتى إن لم يكن موجودًا في مكتبة المحوّلات Transformers، حيث سنرى كيفية إضافة أو تعديل الوظائف التي يقدمها إطار العمل الافتراضي في مكتبة Transformers باستخدام أدوات برمجية كالخطافات Hooks والشيفرة البرمجية الخاصة بنا. سنستخدم في هذا المقال نموذج ResNet الذي هو جزء من المكتبة timm ونعدّله ليعمل كجزء من مكتبة Transformers وسنغلفه ضمن النموذج PreTrainedModel الذي يعد أساس جميع النماذج في Transformers. كتابة ضبط Configuration مخصص عند إنشاء نموذج في مكتبة Transformers، يجب علينا أولاً إعداد كائن ضبط النموذج، فضبط النموذج هو كائن يحتوي على جميع المعلومات الضرورية لبناء النموذج، ولا يمكن للنموذج أن يأخذ إلا الكائن config لتهيئته كما سنرى في القسم التالي، لذا يجب أن يكون هذا الكائن مكتملًا قدر الإمكان. ملاحظة: لا حاجة لتمرير كل وسيط بشكل فردي عند إنشاء النموذج، حيث تتبع النماذج في مكتبة المحوّلات Transformers منهجية تمرير كائن واحد config إلى التابع __init__ الخاص بالنموذج. بعد ذلك، يُمرَّر هذا الكائن بالكامل إلى الطبقات الفرعية للنموذج بدلاً من تقسيمه إلى عدة وسطاء. هذا يجعل الشيفرة البرمجية بسيطة ومنظمة من خلال الاحتفاظ بجميع الإعدادات في مكان واحد يسهل الوصول إليه، كما يساهم هذا النهج في تحسين قابلية إعادة استخدام الشيفرة البرمجية مع نماذج أخرى في مكتبة المُحوِّلات. إنشاء كائن ضبط النموذج سنأخذ في المثال التالي، بعض الإعدادات أو الوسطاء من الصنف ResNet والتي نرغب في تعديلها. بعد ذلك، ستوفر عمليات الضبط المختلفة أنواعًا متنوعة من أصناف ResNet المحتملة. ثم سنُخزّن هذه الوسطاء بعد التحقق من صحتها. from transformers import PretrainedConfig from typing import List class ResnetConfig(PretrainedConfig): model_type = "resnet" def __init__( self, block_type="bottleneck", layers: List[int] = [3, 4, 6, 3], num_classes: int = 1000, input_channels: int = 3, cardinality: int = 1, base_width: int = 64, stem_width: int = 64, stem_type: str = "", avg_down: bool = False, **kwargs, ): if block_type not in ["basic", "bottleneck"]: raise ValueError(f"`block_type` must be 'basic' or bottleneck', got {block_type}.") if stem_type not in ["", "deep", "deep-tiered"]: raise ValueError(f"`stem_type` must be '', 'deep' or 'deep-tiered', got {stem_type}.") self.block_type = block_type self.layers = layers self.num_classes = num_classes self.input_channels = input_channels self.cardinality = cardinality self.base_width = base_width self.stem_width = stem_width self.stem_type = stem_type self.avg_down = avg_down super().__init__(**kwargs) الأمور الثلاثة المهمة التي يجب تذكرها عند كتابة الضبط الخاص بنا هي كالتالي: يجب أن يرث الصنف المخصص ResnetConfig من الصنف الأب PretrainedConfig يجب أن يقبل التابع __init__ من الصنف المخصص أي وسطاء kwargs يجب تمرير هذه الوسطاء kwargs إلى الصنف الأب للتابع __init__ تعني الوراثة Inheritance التأكد من الحصول على جميع الوظائف من مكتبة المحوّلات Transformers، ويمثّل القيدان الآخران احتواء الصنف PretrainedConfig على حقول أكثر من الحقول التي نضبطها، ويجب أن يقبل ضبطنا كافة هذه الحقول ثم تُرسَل إلى الصنف الأب عند إعادة تحميل الضبط باستخدام التابع from_pretrained. لا يُعَد تحديد السمة model_type للضبط الخاص بنا بالقيمة model_type="resnet"‎ هنا إلزاميًا، إلا إذا أردنا تسجيل نموذجنا في الأصناف التلقائية Auto Classes كما سنوضح لاحقًا. يمكننا بعد ذلك إنشاء وحفظ الضبط الخاص بنا بسهولة كما نفعل مع أي ضبط نموذج آخر للمكتبة. لاحظ المثال التالي الذي يوضّح كيفية إنشاء الضبط resnet50d وحفظه: resnet50d_config = ResnetConfig(block_type="bottleneck", stem_width=32, stem_type="deep", avg_down=True) resnet50d_config.save_pretrained("custom-resnet") سيؤدي هذا لحفظ ملف بالاسم config.json ضمن المجلد custom-resnet، يمكننا بعدها إعادة تحميل ملف الضبط الخاص باستخدام التابع from_pretrained كما يلي: resnet50d_config = ResnetConfig.from_pretrained("custom-resnet") ويمكننا أيضًا استخدام أي تابع آخر من الصنف PretrainedConfig مثل التابع push_to_hub()‎ لرفع الضبط الخاص بنا إلى المستودع Hub مباشرة. كتابة نموذج مخصص أصبح لدينا ضبط مخصص لنموذجنا ResNet، ويمكننا الآن كتابة النموذج نفسه، حيث سنكتب نموذجين الأول يستخرج الميزات المخفية من مجموعة الصور مثل النموذج BertModel، والثاني لتصنيف الصور وفق الفئات المختلفة مثل النموذج BertForSequenceClassification. لن نكتب نموذج كامل بل سنكتب فقط مغلِّف wrapper بسيط للنموذج للسهولة، سيكون بمثابة هيكل بسيط للنموذج يمكننا تمرير الإعدادات أو الضبط إليه. وقبل أن نكتب الصنف ResNet أو النموذج نفسه، يجب أن نحدد أنواع الكتل في النموذج مثل basic أو bottleneck، ونحدد كيفية بناء هذه الكتل أو الطبقات في النموذج. بمجرد تحديد هذه الأمور، سنستخدم الضبط الذي حددناه سابقًا لتمرير هذه الإعدادات إلى الصنف ResNet لإنشاء النموذج بناءً على هذه الإعدادات. from transformers import PreTrainedModel from timm.models.resnet import BasicBlock, Bottleneck, ResNet from .configuration_resnet import ResnetConfig BLOCK_MAPPING = {"basic": BasicBlock, "bottleneck": Bottleneck} class ResnetModel(PreTrainedModel): config_class = ResnetConfig def __init__(self, config): super().__init__(config) block_layer = BLOCK_MAPPING[config.block_type] self.model = ResNet( block_layer, config.layers, num_classes=config.num_classes, in_chans=config.input_channels, cardinality=config.cardinality, base_width=config.base_width, stem_width=config.stem_width, stem_type=config.stem_type, avg_down=config.avg_down, ) def forward(self, tensor): return self.model.forward_features(tensor) الآن، سنعدّل التابع forward فقط بالنسبة للنموذج ResNet المخصص لتصنيف الصور، فهذا التابع يتعامل مع البيانات المدخلة، ويحدد كيف تتم معالجتها عبر طبقات النموذج للحصول على النتيجة المطلوبة، سنجري التعديل كما يلي: import torch class ResnetModelForImageClassification(PreTrainedModel): config_class = ResnetConfig def __init__(self, config): super().__init__(config) block_layer = BLOCK_MAPPING[config.block_type] self.model = ResNet( block_layer, config.layers, num_classes=config.num_classes, in_chans=config.input_channels, cardinality=config.cardinality, base_width=config.base_width, stem_width=config.stem_width, stem_type=config.stem_type, avg_down=config.avg_down, ) def forward(self, tensor, labels=None): logits = self.model(tensor) if labels is not None: loss = torch.nn.cross_entropy(logits, labels) return {"loss": loss, "logits": logits} return {"logits": logits} نلاحظ في كلتا الحالتين كيف ورثنا الصنف PreTrainedModel واستدعينا تهيئة الصنف الأب باستخدام الضبط config كما يحدث عندما نكتب وحدة torch.nn.Module عادية في PyTorch. ولا يُعَد السطر الذي يضبط config_class إلزاميًا، إلا إذا أردنا تسجيل نموذجنا في الأصناف التلقائية Auto Classes أي عندما نرغب بأن نتيح لمنصة Hugging Face تحديد النموذج تلقائيًا بناءً على الضبط كما سنوضح لاحقًا. ملاحظة: إذا كان نموذجنا مماثلًا لنموذج آخر موجود مسبقًا في المكتبة Transformers، فيمكن إعادة استخدام الضبط الخاص بهذا النموذج نفسه. يمكن جعل نموذجنا يعيد أي مخرجات نريدها، ولكن ستؤدي إعادة قاموس Dictionary كما فعلنا مع الصنف ResnetModelForImageClassification مع تضمين الخسارة عند تمرير التسميات التوضيحية Labels إلى جعل نموذجك قابلًا للاستخدام مباشرة في الصنف Trainer. يُعدّ استخدام تنسيق خرج آخر جيدًا طالما أنك تخطط لاستخدام حلقة تدريب خاصة بك أو أي مكتبة أخرى للتدريب. أصبح لدينا صنف النموذج الخاص بنا، فلننشئ الآن نموذجًا كما يلي: resnet50d = ResnetModelForImageClassification(resnet50d_config) يمكننا استخدام أي تابع من توابع الصنف PreTrainedModel مثل التابع save_pretrained()‎ أو push_to_hub()‎، حيث سنستخدم التابع الثاني في القسم التالي وسنرى كيفية دفع أوزان النموذج باستخدام الشيفرة البرمجية الخاصة بنموذجنا، ولكن لنحمّل أولًا بعض الأوزان المدرَّبة مسبقًا في نموذجنا. يمكن أن ندرّب نموذجنا المخصّص على بياناتنا الخاصة في حالة استخدامه بشكل مخصص، ولكن سنستخدم في هذا المقال النسخة المدرَّبة مسبقًا من الضبط resnet50d، وبما أن نموذجنا يحتوي على مغلِّف فقط، فسيكون من السهل نقل هذه الأوزان كما يلي: import timm pretrained_model = timm.create_model("resnet50d", pretrained=True) resnet50d.model.load_state_dict(pretrained_model.state_dict()) لنوضّح الآن كيفية التأكد من حفظ شيفرة النموذج البرمجية عند تنفيذ التابع save_pretrained()‎ أو push_to_hub()‎. تسجيل النموذج في الأصناف التلقائية Auto Classes إذا أردنا كتابة مكتبة توسّع المكتبة Transformers، فقد نرغب في توسيع الأصناف التلقائية لتضمين نموذجنا الخاص، ويختلف ذلك عن دفع الشيفرة البرمجية إلى المستودع Hub، إذ سيحتاج المستخدمون لاستيراد مكتبتنا هذه للحصول على النموذج المخصَّص على عكس تنزيل شيفرة النموذج البرمجية تلقائيًا من المستودع Hub. إذا احتوى الضبط على السمة model_type التي تختلف عن أنواع النماذج الموجودة مسبقًا واحتوت أصناف نموذجنا على سمات config_class الصحيحة، فيمكن إضافتها إلى الأصناف التلقائية كما يلي: from transformers import AutoConfig, AutoModel, AutoModelForImageClassification AutoConfig.register("resnet", ResnetConfig) AutoModel.register(ResnetConfig, ResnetModel) AutoModelForImageClassification.register(ResnetConfig, ResnetModelForImageClassification) نلاحظ أن الوسيط الأول المُستخدَم عند تسجيل ضبطنا المخصص في الصنف التلقائي AutoConfig يجب أن يتطابق مع السمة model_type لضبطنا المخصص، ويجب أن يتطابق الوسيط الأول المُستخدَم عند تسجيل النماذج المخصَّصة في أي صنف نموذج تلقائي مع السمة config_class لتلك النماذج. إرسال الشيفرة البرمجية للمستودع علينا التأكّد أولًا من تعريف نموذجنا الكامل في ملف بايثون ‎.py، حيث يمكن الاعتماد على الاستيراد النسبي لبعض الملفات الأخرى طالما أن جميع الملفات موجودة في المجلد نفسه، فالوحدات الفرعية لهذه الميزة غير مدعومة حتى الآن. سنعرّف في مثالنا ملف modeling_resnet.py وملف configuration_resnet.py في مجلد ضمن مجلد العمل الحالي resnet_model، ويحتوي ملف الضبط على الشيفرة البرمجية الخاصة بالصنف ResnetConfig، ويحتوي ملف النموذج على الشيفرة البرمجية الخاصة بالصنفين ResnetModel و ResnetModelForImageClassification. . └── resnet_model ├── __init__.py ├── configuration_resnet.py └── modeling_resnet.py يمكن أن يكون الملف ‎__init__.py فارغًا، لكنه موجود لتتمكّن لغة بايثون من اكتشاف إمكانية استخدام resnet_model كوحدة Module مما يعني أنه يمكن استيراد المكونات والملفات من هذا المجلد في برامج بايثون أخرى. ملاحظة1: إذا أردنا نسخ ملفات النموذج من المكتبة إلى مشروعنا الخاص، فيجب استبدال جميع تعليمات الاستيراد النسبية في أعلى الملف واستيرادها مباشرة من حزمة transformers. ملاحظة2: تُعدّ واجهة التطبيقات البرمجية API هذه تجريبية وقد تحتوي على بعض التغييرات في الإصدارات اللاحقة. بإمكاننا إعادة استخدام أو إنشاء صنف فرعي لضبط أو لنموذج موجود مسبقًا، ويمكن مشاركة نموذجنا مع المجتمع من خلال استيراد نموذج وضبط ResNet أولًا من الملفات التي أنشأناها كما يلي: from resnet_model.configuration_resnet import ResnetConfig from resnet_model.modeling_resnet import ResnetModel, ResnetModelForImageClassification بعد ذلك، علينا إخبار المكتبة بأننا نريد نسخ ملفات الشيفرة البرمجية لتلك الكائنات عند استخدام التابع save_pretrained وتسجيلها بطريقة صحيحة في صنف تلقائي محدّد وخاصةً بالنسبة للنماذج، لذا ننفّذ التعليمات التالية: ResnetConfig.register_for_auto_class() ResnetModel.register_for_auto_class("AutoModel") ResnetModelForImageClassification.register_for_auto_class("AutoModelForImageClassification") نلاحظ أنه لا حاجة لتحديد صنف تلقائي للضبط Config، إذ يوجد صنف تلقائي واحد فقط له هو AutoConfig، ولكن يختلف الأمر بالنسبة للنموذج Model، فالنماذج في مكتبة المحولات Transformers قد تُستَخدم في مهام مختلفة مثل توليد النصوص، أو الترجمة أو تصنيف الصور، لذا يتوجب علينا تحديد الصنف التلقائي المناسب بناءً على نوع النموذج والمهمة التي يؤديها. عندما نريد جعل نموذجنا الخاص قابلاً للاستخدام في مكتبة Transformers وتسجيله ضمن النماذج التلقائية مثل AutoModel, AutoConfig، يجب استخدام التابعregister_for_auto_class()‎ لتسجيل النموذج بشكل صحيح، وإذا كنا نفضل استخدام الشيفرة البرمجية الموجودة على المستودع Hub من مستودع آخر، فلن تحتاج لاستدعاء هذا التابع. يمكننا تعديل الملف config.json مباشرة باستخدام البنية التالية في الحالات التي يوجد فيها أكثر من صنف تلقائي: "auto_map": { "AutoConfig": "<your-repo-name>--<config-name>", "AutoModel": "<your-repo-name>--<config-name>", "AutoModelFor<Task>": "<your-repo-name>--<config-name>", }, لننشئ بعد ذلك الضبط والنماذج كما فعلنا سابقًا: resnet50d_config = ResnetConfig(block_type="bottleneck", stem_width=32, stem_type="deep", avg_down=True) resnet50d = ResnetModelForImageClassification(resnet50d_config) pretrained_model = timm.create_model("resnet50d", pretrained=True) resnet50d.model.load_state_dict(pretrained_model.state_dict()) لنتأكّد الآن من تسجيل الدخول لإرسال النموذج إلى المستودع Hub، لذا نشغّل الأمر التالي في الطرفية Terminal: huggingface-cli login أو نكتب من تطبيق المفكرة ما يلي: from huggingface_hub import notebook_login notebook_login() يمكن بعد ذلك رفع النموذج إلى فضاء الأسماء Namespace الخاص بحسابنا على Hugging Face كما يلي: resnet50d.push_to_hub("custom-resnet50d") ترفع التعليمة النموذج resnet50d إلى المستودع Hugging Face Hub باسم custom-resnet50d وتجعل النموذج متاحًا لاستخدامه مباشرة في المنصة Hugging Face. حيث تُنسَخ ملفات ‎.py للنموذج وللضبط بالإضافة إلى أوزان النموذج والضبط بتنسيق json في المجلد custom-resnet50d وستُرفَع النتيجة للمستودع Hub، ويمكننا التحقق من النتيجة في مستودع النماذج على منصة Huggingface. وللمزيد حول طريقة الدفع إلى المستودع Hub ننصح بمطالعة مقال مشاركة نموذج ذكاء اصطناعي على منصة Hugging Face . استخدام نموذج مع شيفرة برمجية مخصصة يمكن استخدام أي ضبط أو نموذج أو مرمِّز Tokenizer مع ملفات الشيفرة البرمجية المخصَّصة في مستودعها باستخدام الأصناف التلقائية والتابع from_pretrained، حيث تُفحَص جميع الملفات والشيفرات البرمجية المرفوعة إلى المستودع Hub بحثًا عن البرامج الضارة، ولمزيد من التفاصيل يُنصَح بمطالعة توثيق أمان Hub، ويجب أيضًا مراجعة شيفرة النموذج والتحقق من كاتبها لتجنّب تنفيذ شيفرة برمجية ضارة. سنضبط القيمة trust_remote_code=True لاستخدام نموذج مع شيفرة برمجية مخصصة كما يلي: from transformers import AutoModelForImageClassification model = AutoModelForImageClassification.from_pretrained("sgugger/custom-resnet50d", trust_remote_code=True) يُفضَّل أيضًا تمرير قيمة تعمية الإيداع Commit Hash إلى سمة المراجعة revision للتأكّد من أن كاتب النماذج لم يُحدّث الشيفرة البرمجية ببعض الأسطر الجديدة الضارة. commit_hash = "ed94a7c6247d8aedce4647f00f20de6875b5b292" model = AutoModelForImageClassification.from_pretrained( "sgugger/custom-resnet50d", trust_remote_code=True, revision=commit_hash ) نلاحظ وجود زر لنسخ قيمة تعمية الإيداع commit hash يمكننا من خلاله نسخ التعديل بسهولةعند تصفح سجل الإيداعات الخاص بمستودع النماذج الموجود على Hugging Face Hub. الخلاصة شرحنا في مقال اليوم كيفية كتابة نموذج مخصَّص وضبطه وطريقة استخدامه في مكتبة المحوّلات Transformers، كما شرحنا كيفية مشاركته مع المجتمع على مستودع Hugging Face Hub ليتمكّن أي شخص من استخدامه. ترجمة -وبتصرّف- للقسم Building custom models من توثيقات Hugging Face. اقرأ أيضًا المقال السابق: استخدام مكتبة المرمزات Tokenizers في منصة Hugging Face استخدام وكلاء مكتبة المحولات Transformers Agents في الذكاء الاصطناعي التوليدي تدريب المًكيَّفات PEFT Adapters بدل تدريب نماذج الذكاء الاصطناعي بالكامل استخدام التدريب الموزع ومكتبة Accelerate لتسريع تدريب نماذج الذكاء الاصطناعي
  19. توفر مكتبة Transformers من منصة Hugging Face العديد من الأدوات المفيدة لبناء وتشغيل النماذج اللغوية الحديثة. ومن بين هذه الأدوات الصنف AutoClass لتحميل النماذج مسبقة التدريب بسهولة، حيث يحمّل AutoClass الإعدادات والأوزان المدربة مسبقًا بما يتناسب مع بنية النموذج، لكن هناك بعض الحالات التي قد نحتاج فيها لتحكم أكبر في معاملات النموذج، وإنشاء نموذج مخصص دون الاعتماد على الصنف AutoClass، وهو ما سنوضحه في هذا المقال. أهمية بناء نموذج مخصص يستدل الصنف AutoClass في مكتبة المحوّلات Transformers على بنية النموذج تلقائيًا ويحمّل الضبط Configuration والأوزان المدربة مسبقًا، حيث يوصى باستخدام هذا الصنف لإنتاج شيفرة برمجية مستقلة عن نقاط التحقق Checkpoint، ولكن يمكن للمستخدمين الذين يريدون مزيدًا من التحكم في معاملات النموذج المحددة إنشاء نموذج مخصص باستخدام مكتبة المحولات Transformers من بعض الأصناف الأساسية فقط. يمكن أن يكون ذلك مفيدًا لأي شخص مهتم بدراسة أو تدريب أو تجربة نموذج من مكتبة Transformers من منصة Huggingface، لذا سنتعمق أكثر في إنشاء نموذج مخصص بدون الصنف AutoClass، حيث سنتعلم كيفية: تحميل ضبط النموذج وتخصيصه إنشاء بنية نموذج إنشاء مرمِّز Tokenizer للنص إنشاء معالج صور للمهام البصرية إنشاء مستخرج ميزات للمهام الصوتية إنشاء معالج مهام متعددة الوسائط الضبط Configuration يمثّل الضبط Configuration السمات Attributes المحدَّدة للنموذج، حيث يكون لكل ضبط خاص بالنموذج سمات مختلفة، فمثلًا تحتوي جميع نماذج معالجة اللغات الطبيعية NLP على السمات hidden_size و num_attention_heads و num_hidden_layers و vocab_size وتحدّد هذه السمات عدد رؤوس الانتباه Attention Heads أو الطبقات المخفية التي سنبني نموذجًا باستخدامها. يمكن مطالعة على سمات النموذج DistilBERT من خلال الوصول إلى صنف الضبط DistilBertConfig كما يلي: >>> from transformers import DistilBertConfig >>> config = DistilBertConfig() >>> print(config) DistilBertConfig { "activation": "gelu", "attention_dropout": 0.1, "dim": 768, "dropout": 0.1, "hidden_dim": 3072, "initializer_range": 0.02, "max_position_embeddings": 512, "model_type": "distilbert", "n_heads": 12, "n_layers": 6, "pad_token_id": 0, "qa_dropout": 0.1, "seq_classif_dropout": 0.2, "sinusoidal_pos_embds": false, "transformers_version": "4.16.2", "vocab_size": 30522 } يعرض الصنف DistilBertConfig جميع السمات الافتراضية المستخدمة لبناء النموذج DistilBertModel الأساسي، وتكون جميع السمات قابلة للتخصيص، مما يعطينا مساحة للتجريب، فمثلًا يمكننا تخصيص نموذج افتراضي بهدف: تجربة دالة تنشيط مختلفة باستخدام المعامل activation استخدام نسبة تسرب Dropout Ratio أعلى لاحتمالات الانتباه باستخدام المعامل attention_dropout >>> my_config = DistilBertConfig(activation="relu", attention_dropout=0.4) >>> print(my_config) DistilBertConfig { "activation": "relu", "attention_dropout": 0.4, "dim": 768, "dropout": 0.1, "hidden_dim": 3072, "initializer_range": 0.02, "max_position_embeddings": 512, "model_type": "distilbert", "n_heads": 12, "n_layers": 6, "pad_token_id": 0, "qa_dropout": 0.1, "seq_classif_dropout": 0.2, "sinusoidal_pos_embds": false, "transformers_version": "4.16.2", "vocab_size": 30522 } ملاحظة: معدل التسرب Dropout Ratio هو تقنية مفيدة في تدريب الشبكات العصبية تساعد على منع الإفراط في التكيّف من خلال تعطيل بعض الخلايا العصبية عشوائيًا أثناء التدريب، وهذا يزيد قدرة النموذج على التعميم ويجعله أكثر قدرة على التعامل مع بيانات جديدة. يمكننا تعديل سمات النموذج المدرَّب مسبقًا في الدالة from_pretrained()‎ كما يلي: >>> my_config = DistilBertConfig.from_pretrained("distilbert/distilbert-base-uncased", activation="relu", attention_dropout=0.4) يمكننا حفظ ضبط النموذج باستخدام الدالة save_pretrained()‎ بعد الانتهاء منه كما يلي، ويُخزَّن ملف الضبط الخاص بنا كملف JSON في مجلد الحفظ المحدَّد: >>> my_config.save_pretrained(save_directory="./your_model_save_path") يمكننا إعادة استخدام ملف الضبط من خلال تحميله باستخدام الدالة from_pretrained()‎ كما يلي: >>> my_config = DistilBertConfig.from_pretrained("./your_model_save_path/config.json") ملاحظة: يمكننا أيضًا حفظ ملف الضبط الخاص بنا على هيئة قاموس Dictionary أو حتى كمجرد فرق بين سمات الضبط المخصصة وسمات الضبط الافتراضية. يمكن الاطلاع على توثيق الضبط على منصة Huggingface لمزيد من التفاصيل. النموذج Model سننشئ الآن نموذجًا، حيث يحدّد النموذج أو كما يشار إليه أحيانًا باسم البنية Architecture ما تفعله كل طبقة وما هي العمليات التي تحدث، وتُستخدَم السمات مثل num_hidden_layers من الضبط لتحديد هذه البنية. تتشارك جميع النماذج في الصنف الأساسي PreTrainedModel وبعض التوابع المشتركة مثل تغيير حجم تضمينات الإدخال وتقليم Pruning رؤوس الانتباه الذاتي Self-attention Heads أو تقليل الأجزاء غير الضرورية أو الفائضة من النموذج لتحسين كفاءته. تكون جميع النماذج أيضًا إما الصنف الفرعي torch.nn.Module أو tf.keras.Model أو flax.linen.Module، وهذا يعني أن النماذج متوافقة مع استخدام كل إطار عمل خاص بها. في حال كنا نستخدم إطار العمل بايتورش Pytorch نحمّل سمات الضبط المخصصة الخاصة بنا في النموذج كما يلي: >>> from transformers import DistilBertModel >>> my_config = DistilBertConfig.from_pretrained("./your_model_save_path/config.json") >>> model = DistilBertModel(my_config) مما يؤدي لإنشاء نموذج مع قيم عشوائية بدل أوزان مُدرَّبة مسبقًا، ولكننا لن نتمكّن من استخدام هذا النموذج استخدامًا مفيدًا حتى ندرّبه. فالتدريب عملية مكلفة وتستغرق وقتًا طويلًا، لذا يُفضَّل استخدام نموذج مدرب مسبقًا للحصول على نتائج أفضل وأسرع مع استخدام جزء بسيط فقط من الموارد المطلوبة للتدريب، لذا سننشئ نموذجًا مدربًا مسبقًا باستخدام الدالة from_pretrained()‎ كما يلي: >>> model = DistilBertModel.from_pretrained("distilbert/distilbert-base-uncased") يُحمَّل ضبط النموذج الافتراضي تلقائيًا عند تحميل الأوزان المُدرَّبة مسبقًا إذا وفرت مكتبة المحوّلات Transformers هذا النموذج، ولكن لا يزال بإمكاننا وضع سماتنا الخاصة مكان بعض أو جميع سمات ضبط النموذج الافتراضي إذا أردنا ذلك كما يلي: >>> model = DistilBertModel.from_pretrained("distilbert/distilbert-base-uncased", config=my_config) وفي حال استخدمنا إطار العمل تنسرفلو TensorFlow، فحمّل سمات الضبط المخصصة في النموذج كما يلي: >>> from transformers import TFDistilBertModel >>> my_config = DistilBertConfig.from_pretrained("./your_model_save_path/my_config.json") >>> tf_model = TFDistilBertModel(my_config) مما يؤدي إلى إنشاء نموذج مع قيم عشوائية بدلًا من أوزان مُدرَّبة مسبقًا، ولكن لن نتمكّن من استخدام هذا النموذج استخدامًا مفيدًا حتى ندربه. إذ يُعَد التدريب عملية مكلفة وتستغرق وقتًا طويلًا، لذا يُفضّل استخدام نموذج مدرّب مسبقًا للحصول على نتائج أفضل وأسرع مع استخدام جزء بسيط فقط من الموارد المطلوبة للتدريب، لذا سننشئ نموذجًا مدربًا مسبقًا باستخدام الدالة from_pretrained()‎ كما يلي: >>> tf_model = TFDistilBertModel.from_pretrained("distilbert/distilbert-base-uncased") يُحمَّل ضبط النموذج الافتراضي تلقائيًا عند تحميل الأوزان المُدرَّبة مسبقًا إذا وفّرت مكتبة المحوّلات Transformers هذا النموذج، ولكن لا يزال بإمكاننا وضع سماتنا الخاصة مكان بعض أو جميع سمات ضبط النموذج الافتراضي إذا أردنا ذلك كما يلي: >>> tf_model = TFDistilBertModel.from_pretrained("distilbert/distilbert-base-uncased", config=my_config) رؤوس النماذج Model heads أصبح لدينا نموذج DistilBERT أساسي يعطي الحالات المخفية Hidden States التي تُمرَّر كدخل إلى رأس النموذج لإنتاج الخرج النهائي. توفر مكتبة المحوّلات Transformers رأس نموذج مختلف لكل مهمة طالما أن النموذج يدعم المهمة، أي لا يمكنك استخدام النموذج DistilBERT لمهمة التحويل من تسلسل إلى آخر Sequence-to-Sequence مثل مهمة الترجمة. في حال استخدمنا إطار العمل Pytorch مع مكتبة Transformers، فإن النموذج DistilBertForSequenceClassification مثلًا هو نموذج DistilBERT أساسي مع رأس لتصنيف التسلسل، وهو بمثابة طبقة خطية فوق الخرج المجمَّع. إذًا سننشئ هذا النموذج كما يلي: >>> from transformers import DistilBertForSequenceClassification >>> model = DistilBertForSequenceClassification.from_pretrained("distilbert/distilbert-base-uncased") يمكننا إعادة استخدام نقطة التحقق السابقة بسهولة لمهمة أخرى من خلال التبديل إلى رأس نموذج مختلف، حيث يمكنك استخدام رأس النموذج DistilBertForQuestionAnswering بالنسبة لمهمة الإجابة على سؤال كما يلي، إذ يشبه رأس الإجابة على سؤال رأس تصنيف التسلسل باستثناء أنه طبقة خطية فوق خرج الحالات المخفية: >>> from transformers import DistilBertForQuestionAnswering >>> model = DistilBertForQuestionAnswering.from_pretrained("distilbert/distilbert-base-uncased") وإذا كنا تستخدم إطار العمل تنسرفلو TensorFlow، فإن النموذج TFDistilBertForSequenceClassification مثلًا هو نموذج DistilBERT أساسي مع رأس لتصنيف التسلسل، والذي يُعَد طبقة خطية فوق الخرج المجمَّع. إذًا لننشئ هذا النموذج كما يلي: >>> from transformers import TFDistilBertForSequenceClassification >>> tf_model = TFDistilBertForSequenceClassification.from_pretrained("distilbert/distilbert-base-uncased") يمكننا إعادة استخدام نقطة التحقق السابقة بسهولة لمهمة أخرى من خلال التبديل إلى رأس نموذج مختلف، حيث يمكننا استخدام رأس النموذج TFDistilBertForQuestionAnswering بالنسبة لمهمة الإجابة على سؤال كما يلي، إذ يشبه رأس الإجابة على سؤال رأس تصنيف التسلسل باستثناء أنه طبقة خطية فوق خرج الحالات المخفية: >>> from transformers import TFDistilBertForQuestionAnswering >>> tf_model = TFDistilBertForQuestionAnswering.from_pretrained("distilbert/distilbert-base-uncased") المرمّز Tokenizer الصنف الأساسي الأخير الذي نحتاجه قبل استخدام نموذج للبيانات النصية هو المرمّز tokenizer لتحويل النص الأولي إلى موترات Tensors، حيث يوجد نوعان من المرمّزات يمكنك استخدامهما مع مكتبة المحولات Transformers هما: PreTrainedTokenizer وهو تنفيذ لغة بايثون Python للمرمّز PreTrainedTokenizerFast: هو مرمّز من مكتبة Tokenizer ويستند إلى لغة رست Rust، وتكون سرعة هذا النوع من المرمّزات ملحوظة وخاصة أثناء الترميز الدفعي Batch Tokenization بسبب تنفيذه باستخدام لغة رست. ويقدّم المرمِّز السريع توابع إضافية مثل ربط الإزاحة Offset Mapping الذي يربط الرموز Tokens بكلماتها أو محارفها الأصلية. يدعم هذان المرمِّزان التوابع الشائعة مثل التشفير وفك التشفير وإضافة رموز جديدة وإدارة الرموز الخاصة. ملاحظة: لا تدعم جميع النماذج المرمِّز السريع، لذا ألقِ نظرة على الجدول الموجود في مقال مكتبة المحوّلات Transformers من منصة Hugging Face للتحقق من دعم النموذج للمرمِّز السريع. يمكنك إنشاء مرمّز من ملف المفردات vocabulary الخاص بنا كما يلي لإنشاء خاص مرمّز بنا: >>> from transformers import DistilBertTokenizer >>> my_tokenizer = DistilBertTokenizer(vocab_file="my_vocab_file.txt", do_lower_case=False, padding_side="left") يجب أن نتذكر أن المفردات القادمة من المرمّز المخصَّص ستكون مختلفة عن المفردات التي يولّدها مرمّز النموذج المُدرَّب مسبقًا، لذا سنحتاج لاستخدام مفردات نموذج مدرب مسبقًا إذا استخدمنا نموذج مُدرَّب مسبقًا، وإلّا لن يكون للدخل أي معنى. لننشئ مرمّز باستخدام مفردات نموذج مدرب مسبقًا باستخدام الصنف DistilBertTokenizer كما يلي: >>> from transformers import DistilBertTokenizer >>> slow_tokenizer = DistilBertTokenizer.from_pretrained("distilbert/distilbert-base-uncased") ولننشئ مرمّز سريع باستخدام الصنف DistilBertTokenizerFast كما يلي: >>> from transformers import DistilBertTokenizerFast >>> fast_tokenizer = DistilBertTokenizerFast.from_pretrained("distilbert/distilbert-base-uncased") ملاحظة: سيحاول الصنف AutoTokenizer افتراضيًا تحميل مرمّز سريع، ولكن يمكنك تعطيل هذا السلوك من خلال ضبط القيمة use_fast=False في الدالة from_pretrained. معالج الصور Image Processor يعالج معالج الصور المدخلات البصرية، وهو يرث الصنف ImageProcessingMixin الأساسي، ويمكن استخدامه من خلال إنشاء معالج صور مرتبط بالنموذج الذي تستخدمه، فمثلًا يمكننا إنشاء صنف ViTImageProcessor افتراضي كما يلي، إذا كنا نستخدم النموذج ViT أو المحوّل البصري Vision Transformer لتصنيف الصور: >>> from transformers import ViTImageProcessor >>> vit_extractor = ViTImageProcessor() >>> print(vit_extractor) ViTImageProcessor { "do_normalize": true, "do_resize": true, "image_processor_type": "ViTImageProcessor", "image_mean": [ 0.5, 0.5, 0.5 ], "image_std": [ 0.5, 0.5, 0.5 ], "resample": 2, "size": 224 } ملاحظة: إن لم نكن نريد تخصيص أيّ شيء، فما علينا سوى استخدام التابع from_pretrained لتحميل معاملات معالج الصور الافتراضية للنموذج. لنعدّل الآن أحد معاملات الصنف ViTImageProcessor لإنشاء معالج الصور المخصَّص كما يلي: >>> from transformers import ViTImageProcessor >>> my_vit_extractor = ViTImageProcessor(resample="PIL.Image.BOX", do_normalize=False, image_mean=[0.3, 0.3, 0.3]) >>> print(my_vit_extractor) ViTImageProcessor { "do_normalize": false, "do_resize": true, "image_processor_type": "ViTImageProcessor", "image_mean": [ 0.3, 0.3, 0.3 ], "image_std": [ 0.5, 0.5, 0.5 ], "resample": "PIL.Image.BOX", "size": 224 } العمود الفقري Backbone تتكون نماذج الرؤية الحاسوبية من العمود الفقري Backbone والعنق Neck والرأس Head، حيث يستخرج العمود الفقري الميزات Features من صورة الدخل، ويجمع العنق الميزات المستخرجة ويحسّنها، ويُستخدم الرأس للمهمة الرئيسية مثل اكتشاف الكائنات. دعنا نبدأ بتهيئة العمود الفقري في ضبط النموذج ونحدد تحميل أوزان مدرَّبة مسبقًا أو تحميل أوزان مُهيَّأة عشوائيًا، ثم يمكننا تمرير ضبط النموذج إلى الرأس. إذا أردنا مثلًا تحميل العمود الفقري ResNet في النموذج MaskFormer باستخدام رأس تقسيم أجزاء الصورة كما يلي: <hfoptions id="backbone"> <hfoption id="pretrained weights"> فيجب ضبط القيمة use_pretrained_backbone=True لتحميل أوزان ResNet المدرَّبة مسبقًا للعمود الفقري كما يلي: from transformers import MaskFormerConfig, MaskFormerForInstanceSegmentation config = MaskFormerConfig(backbone="microsoft/resnet-50", use_pretrained_backbone=True) # ضبط العمود الفقري والعنق model = MaskFormerForInstanceSegmentation(config) # الرأس وإذا أردنا تحميل العمود الفقري ResNet في النموذج MaskFormer باستخدام رأس تقسيم أجزاء الصورة كما يلي: </hfoption> <hfoption id="random weights"> فيجب ضبط القيمة use_pretrained_backbone=False لتهيئة العمود الفقري ResNet عشوائيًا كما يلي: from transformers import MaskFormerConfig, MaskFormerForInstanceSegmentation config = MaskFormerConfig(backbone="microsoft/resnet-50", use_pretrained_backbone=False) # ضبط العمود الفقري والعنق model = MaskFormerForInstanceSegmentation(config) # الرأس يمكن أيضًا تحميل ضبط العمود الفقري بطريقة منفصلة ثم تمريره إلى ضبط النموذج كما يلي: from transformers import MaskFormerConfig, MaskFormerForInstanceSegmentation, ResNetConfig backbone_config = ResNetConfig() config = MaskFormerConfig(backbone_config=backbone_config) model = MaskFormerForInstanceSegmentation(config) تُحمَّل نماذج المكتبة timm ضمن نموذج كما يلي باستخدام القيمة use_timm_backbone=True أو باستخدام الصنف TimmBackbone والصنف TimmBackboneConfig: </hfoption> </hfoptions id="timm backbone"> لذا سنستخدم القيمة use_timm_backbone=True و use_pretrained_backbone=True لتحميل أوزان timm المدرَّبة مسبقًا للعمود الفقري كما يلي: from transformers import MaskFormerConfig, MaskFormerForInstanceSegmentation config = MaskFormerConfig(backbone="resnet50", use_pretrained_backbone=True, use_timm_backbone=True) # ضبط العمود الفقري والعنق model = MaskFormerForInstanceSegmentation(config) # الرأس ولنضبط الآن القيمة use_timm_backbone=True و use_pretrained_backbone=False لتحميل العمود الفقري timm المهيَّأ عشوائيًا كما يلي: from transformers import MaskFormerConfig, MaskFormerForInstanceSegmentation config = MaskFormerConfig(backbone="resnet50", use_pretrained_backbone=False, use_timm_backbone=True) # ضبط العمود الفقري والعنق model = MaskFormerForInstanceSegmentation(config) # الرأس يمكننا أيضًا تحميل ضبط العمود الفقري واستخدامه لإنشاء الصنف TimmBackbone أو تمريره إلى ضبط النموذج، حيث ستحمّل الأعمدة الفقرية Timm الأوزان المدرَّبة مسبقًا افتراضيًا، لذا سنضبط القيمة use_pretrained_backbone=False لتحميل الأوزان المُهيَّأة عشوائيًا كما يلي: from transformers import TimmBackboneConfig, TimmBackbone backbone_config = TimmBackboneConfig("resnet50", use_pretrained_backbone=False) # إنشاء صنف العمود الفقري backbone = TimmBackbone(config=backbone_config) # إنشاء نموذج باستخدام العمود الفقري‫ timm from transformers import MaskFormerConfig, MaskFormerForInstanceSegmentation config = MaskFormerConfig(backbone_config=backbone_config) model = MaskFormerForInstanceSegmentation(config) مستخرج الميزات Feature Extractor يعالج مستخرج الميزات المدخلات الصوتية، وهو يرث الصنف FeatureExtractionMixin الأساسي، ويمكن أن يرث أيضًا الصنف SequenceFeatureExtractor لمعالجة المدخلات الصوتية. لننشئ الآن مستخرج ميزات مرتبط بالنموذج الذي تستخدمه مثل إنشاء صنف Wav2Vec2FeatureExtractor افتراضي كما يلي إذا كنت تستخدم النموذج Wav2Vec2 لتصنيف الأصوات: >>> from transformers import Wav2Vec2FeatureExtractor >>> w2v2_extractor = Wav2Vec2FeatureExtractor() >>> print(w2v2_extractor) Wav2Vec2FeatureExtractor { "do_normalize": true, "feature_extractor_type": "Wav2Vec2FeatureExtractor", "feature_size": 1, "padding_side": "right", "padding_value": 0.0, "return_attention_mask": false, "sampling_rate": 16000 } ملاحظة: إن لم نكن نرغب بتخصيص أيّ شيء، فما علينا سوى استخدام التابع from_pretrained لتحميل معاملات مستخرج الميزات الافتراضية الخاصة بالنموذج. لنعدّل الآن أحد معاملات الصنف Wav2Vec2FeatureExtractor لإنشاء مستخرج الميزات المخصَّص الخاص بنا كما يلي: >>> from transformers import Wav2Vec2FeatureExtractor >>> w2v2_extractor = Wav2Vec2FeatureExtractor(sampling_rate=8000, do_normalize=False) >>> print(w2v2_extractor) Wav2Vec2FeatureExtractor { "do_normalize": false, "feature_extractor_type": "Wav2Vec2FeatureExtractor", "feature_size": 1, "padding_side": "right", "padding_value": 0.0, "return_attention_mask": false, "sampling_rate": 8000 } المعالج Processor تقدم مكتبة المحوِّلات Transformers صنف المعالج الذي يغلِّف أصناف المعالجة مثل مستخرج الميزات والمرمِّز في كائن واحد بالنسبة للنماذج التي تدعم المهام متعددة الوسائط. لنستخدم مثلًا الصنف Wav2Vec2Processor لمهمة التعرّف التلقائي على الكلام Automatic Speech Recognition أو ASR اختصارًا، والتي تحوّل الصوت إلى نص، لذا ستحتاج إلى مستخرج ميزات ومرمّز. لننشئ أولًا مستخرج ميزات للتعامل مع المدخلات الصوتية كما يلي: >>> from transformers import Wav2Vec2FeatureExtractor >>> feature_extractor = Wav2Vec2FeatureExtractor(padding_value=1.0, do_normalize=True) ثم ننشئ مرمّز للتعامل مع المدخلات النصية كما يلي: >>> from transformers import Wav2Vec2CTCTokenizer >>> tokenizer = Wav2Vec2CTCTokenizer(vocab_file="my_vocab_file.txt") ثم ندمج مستخرج الميزات والمرمّز في الصنف Wav2Vec2Processor كما يلي: >>> from transformers import Wav2Vec2Processor >>> processor = Wav2Vec2Processor(feature_extractor=feature_extractor, tokenizer=tokenizer) الخلاصة يمكننا إنشاء أي من النماذج التي تدعمها مكتبة المحولات Transformers من منصة Huggingface باستخدام صنفين أساسيين للضبط والنموذج وصنف إضافي للمعالجة المسبَقة مثل مرمِّز أو معالج صور أو مُستخرج ميزات أو معالج، وتكون هذه الأصناف الأساسية قابلة للضبط، مما يسمح لنا باستخدام السمات المحدَّدة التي نريدها، ويمكن بسهولة إعداد نموذج للتدريب أو تعديل نموذج مُدرَّب مسبقًا لصقله Fine-tune. ترجمة -وبتصرّف- للقسم Create a custom architecture من توثيقات Hugging Face. اقرأ أيضًا تثبيت مكتبة المحوّلات Transformers ما هي منصة Hugging Face للذكاء الاصطناعي مشاركة نموذج ذكاء اصطناعي على منصة Hugging Face بناء روبوتات للعب الألعاب باستخدام طريقة التعلم المعزز ومشتقاتها باستخدام مكتبة TensorFlow بناء شبكة عصبية للتعرف على الأرقام المكتوبة بخط اليد باستخدام مكتبة TensorFlow
  20. نشرح في هذا المقال كيفية استخدام مكتبة ترميز النصوص Tokenizers التي توفرها منصة Hugging Face ونشرح طريقة استخدامها لتقسيم النصوص إلى رموز أو وحدات صغيرة تسمى Tokens، كما نوضح الخطوات المتبعة لإنشاء مقسِّم نصوص باستخدام خوارزمية ترميز زوج البتات Byte Pair Encoding التي توفرها المكتبة ونشرح طريقة استخدامه وتدريبه على بيانات مخصصة. إنشاء مرمز نصوص سنستخدم الصنف PreTrainedTokenizerFast من المكتبة Tokenizers التابعة لمنصة Hugging Face والتي توفر لنا العديد من التوابع لترميز النصوص بسرعة وكفاءة، كما تتيح لنا إمكانية تحميل المُرمِّزات التي أنشأناها بسهولة للعمل داخل مكتبة المحولات Transformers، مما يسهل دمجها مع النماذج اللغوية. لنفهم أساسيات بناء مرمِّز مخصص باستخدام مكتبة Tokenizers من أجل تخصيصه لبيانات محددة أو تطبيقات خاصة، بدلاً من الاعتماد على مرمزات جاهزة قد لا تكون مثالية لجميع الحالات. لنبدأ أولًا بإنشاء مرمِّز تجريبي كما يلي قبل الدخول بالتفاصيل: >>> from tokenizers import Tokenizer >>> from tokenizers.models import BPE >>> from tokenizers.trainers import BpeTrainer >>> from tokenizers.pre_tokenizers import Whitespace >>> tokenizer = Tokenizer(BPE(unk_token="[UNK]")) >>> trainer = BpeTrainer(special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"]) >>> tokenizer.pre_tokenizer = Whitespace() >>> files = [...] >>> tokenizer.train(files, trainer) أصبح لدينا الآن مُرمِّز مُدرَّب على الملفات التي حددناها والتي تحتوي على النصوص المستخدمة لتدريب المرمِّز، وبالتالي يمكننا الاستمرار في استخدامه في وقت التشغيل أو حفظه في ملف JSON لإعادة استخدامه لاحقًا. التحميل المباشر من كائن المرمز لنوضح الآن كيف يمكننا الاستفادة من كائن المرمِّز الذي أنشأناه في الفقرة السابقة داخل مكتبة المحوّلات Transformers المخصصة لمعالجة اللغات الطبيعية NLP، حيث يسمح الصنف PreTrainedTokenizerFast بإنشاء نسخ جديدة بسهولة من خلال قبول نسخة كائن المرمِّز tokenizer كوسيط كما يلي: >>> from transformers import PreTrainedTokenizerFast >>> fast_tokenizer = PreTrainedTokenizerFast(tokenizer_object=tokenizer) ويمكن الآن استخدام هذا الكائن مع جميع التوابع المشتركة بين مرمِّزات مكتبة المحوّلات Transformers والتي تساعد في تحويل النصوص إلى تمثيلات قابلة للاستخدام في النماذج اللغوية، مما يسهل عملية التدريب والتنبؤ باستخدام نماذج المحوّلات المختلفة. تحميل المرمز من ملف JSON يمكن تحميل مرمَّز من ملف JSON من خلال حفظ المرمِّز أولًا كما يلي: >>> tokenizer.save("tokenizer.json") يمكننا بعد ذلك تمرير المسار الذي حفظنا فيه هذا الملف إلى تابع التهيئة الخاص بهذا الصنف PreTrainedTokenizerFast باستخدام المعامل tokenizer_file كما يلي: >>> from transformers import PreTrainedTokenizerFast >>> fast_tokenizer = PreTrainedTokenizerFast(tokenizer_file="tokenizer.json") ويمكن الآن استخدام هذا الكائن مع جميع التوابع المشتركة بين مرمِّزات مكتبة المحوِّلات Transformers. استخدام النماذج متعددة اللغات Multilingual Models للاستدلال توفر مكتبة المحوِّلات Transformers العديد من النماذج متعددة اللغات، وهي نماذج مدربة على بيانات متعددة اللغات ويمكنها التعامل مع نصوص بلغات مختلفة، يختلف استخدام هذه النماذج في الاستدلال والتنبؤ وتحليل النصوص عن النماذج التي تدعم لغة واحدة فقط في بعض الأحيان، ومع ذلك، يمكن استخدام معظم النماذج متعددة اللغات بنفس طريقة استخدام النماذج أحادية اللغة. على سبيل المثال، يمكننا استخدام نموذج مثل bert/bert-base-multilingual-uncased من جوجل بنفس طريقة استخدام النماذج أحادية اللغة، لكننا سنركز في الفقرات التالية على شرح النماذج متعددة اللغات التي يختلف استخدامها عن استخدام النماذج أحادية اللغة لإجراء عمليات الاستدلال. نموذج XLM يحتوي نموذج XLM متعدد اللغات على عشر نقاط تحقق Checkpoints مختلفة تمثل حالات مختلفة من تدريب النموذج، وتكون نقطة واحدة منها فقط أحادية اللغة بينما تتعامل النقاط التسع الأخرى مع لغات متعددة، ويمكن تقسيم هذه النقاط التسع إلى فئتين هما: نقاط التحقق التي تستخدم تضمينات اللغة Language Embeddings للتمييز بين اللغات المختلفة ونقاط التحقق التي لا تستخدم هذه التضمينات. ملاحظة: التضمينات اللغوية Language Embeddings هي طريقة لتحويل الكلمات والجمل إلى أرقام يمكن للنماذج الحاسوبية التعامل معها، الهدف منها هو جعل الحاسوب قادرًا على فهم اللغة البشرية ومعاني الكلمات بناءً على سياقها، فكلما كانت الكلمات ذات معاني مشابهة، ستكون تضميناتها العددية أقرب لبعضها وتستخدم في مجال الترجمة الآلية وتصنيف النصوص وتحليل المشاعر. نماذج XLM التي تستخدم تضمينات اللغة تستخدم نماذج XLM التالية تضمينات اللغة لتحديد اللغة المستخدمة في الاستدلال: FacebookAI/xlm-mlm-ende-1024 لنمذجة اللغة المقنّعة Masked Language Modeling، ويدعم اللغتين الإنجليزية والألمانية FacebookAI/xlm-mlm-enfr-1024 لنمذجة اللغة المقنّعة، ويدعم الإنجليزية والفرنسية FacebookAI/xlm-mlm-enro-1024 لنمذجة اللغة المقنّعة، ويدعم الإنجليزية والرومانية FacebookAI/xlm-mlm-xnli15-1024 لنمذجة اللغة المقنعة، ويعمل مع مجموعة اللغات المدرجة في مجموعة بيانات XNLI FacebookAI/xlm-mlm-tlm-xnli15-1024 لنمذجة اللغة المقنعة والترجمة، ويعمل مع لغات XNLI FacebookAI/xlm-clm-enfr-1024 لنمذجة اللغة السببية Causal Language Modeling ويعمل مع اللغة الإنجليزية والفرنسية FacebookAI/xlm-clm-ende-1024 لنمذجة اللغة السببية ويعمل مع اللغتين الإنجليزية والألمانية يُمثَّل تضمين اللغة على شكل موتر Tensor وهو بنية على شكل مصفوفة متعددة الأبعاد لها نفس حجم بنية input_ids المُمرَّرة إلى النموذج، وتعتمد القيم الموجودة في هذه الموترات على اللغة المستخدمة وتحددها السمات التالية lang2id و id2lang الخاصة بالمرمّز. ملاحظة: نمذجة اللغة السببية Causal Language Modeling هي نوع من نماذج تعلم الآلة تهدف إلى فهم وتوليد النصوص بناء على الترتيب السببي للكلمات، في هذا النوع من النمذجة يتنبأ النموذج بالكلمة التالية في تسلسل الكلمات بناءً على الكلمات التي جاءت قبلها فقط وليس بعدها. أي يتعامل النموذج مع النص بشكل أحادي الاتجاه من اليسار لليمين ويتوقع الكلمة التالية بناءً على الكلمات السابقة لها. على سبيل المثال لترميز الجملة "Wikipedia was used to" باستخدام المرمِّز Tokenizer وتحويلها لتسلسل من الأرقام التعريفية IDs التي يمكن للنموذج معالجتها سنحمّل بداية نقطة تحقق نموذج FacebookAI/xlm-clm-enfr-1024: >>> import torch >>> from transformers import XLMTokenizer, XLMWithLMHeadModel >>> tokenizer = XLMTokenizer.from_pretrained("FacebookAI/xlm-clm-enfr-1024") >>> model = XLMWithLMHeadModel.from_pretrained("FacebookAI/xlm-clm-enfr-1024") تحتوي السمة lang2id الخاصة بالمرمِّز على اللغات المدعومة في النموذج ومعرّفاتها IDs، كما في المثال التالي: >>> print(tokenizer.lang2id) {'en': 0, 'fr': 1} لننشئ بعد ذلك الدخل التالي: >>> input_ids = torch.tensor([tokenizer.encode("Wikipedia was used to")]) # حجم الدفعة هو 1 ثم نضبط معرّف اللغة على القيمة "en" ونستخدمه لتحديد تضمين اللغة وهو موتر tensor يحتوي على القيمة 0 التي تمثل معرّف اللغة الإنجليزية، ويجب أن يكون له نفس حجم البنية input_ids. >>> language_id = tokenizer.lang2id["en"] # 0 >>> langs = torch.tensor([language_id] * input_ids.shape[1]) # torch.tensor([0, 0, 0, ..., 0]) >>> # ‫نعيد تشكيله ليكون بحجم (batch_size, sequence_length) >>> langs = langs.view(1, -1) # ‫أصبح الآن بالشكل ‎[1, sequence_length]‎ (حجم الدفعة هو 1) ويمكنك الآن تمرير البنية input_ids وتضمين اللغة إلى النموذج من أجل فهم النص وتحليله كما يلي: >>> outputs = model(input_ids, langs=langs) سيؤدي تنفيذ السكربت run_generation.py إلى توليد نص مع تضمينات اللغة باستخدام نقاط تحقق xlm-clm. نماذج XLM التي لا تستخدم تضمينات اللغة لا تتطلب نماذج XLM التالية تضمينات اللغة أثناء الاستدلال إذ يستطيع النموذج فهم اللغة التي يتعامل معها بدون الحاجة إلى معرّف اللغة وهي: FacebookAI/xlm-mlm-17-1280 لنمذجة اللغة المقنّعة ويدعم 17 لغة FacebookAI/xlm-mlm-100-1280 لنمذجة اللغة المقنّعة ويدعم 100 لغة تستخدم هذه النماذج تمثيلات الجمل المُعمَّمة Generic Sentence Representations، على عكس نقاط تحقق نموذج XLM السابقة. نموذج BERT يمكن استخدام نماذج BERT التالية للمهام متعددة اللغات: google-bert/bert-base-multilingual-uncased لنمذجة اللغة المقنعة وتوقع الجملة التالية، تدعم 102 لغة google-bert/bert-base-multilingual-cased لنمذجة اللغة المقنعة وتوقع الجملة التالية، وتدعم 104 لغات لا تتطلب هذه النماذج تضمينات اللغة أثناء الاستدلال، فهي تحدِّد اللغة من السياق وتستدل عليها وفقًا لذلك. نموذج XLM-RoBERTa يمكن استخدام نماذج XLM-RoBERTa التالية للمهام متعددة اللغات: FacebookAI/xlm-roberta-base لنمذجة اللغة المقنعة، وتدعم 100 لغة FacebookAI/xlm-roberta-large لنمذجة اللغة المقنعة، وتدعم 100 لغة دُرِّب نموذج XLM-RoBERTa على 2.5 تيرابايت من بيانات CommonCrawl المُنشَأة والمُنظَّفة حديثًا وذلك في 100 لغة، ويحقق هذا النموذج تحسينات كبيرة بالمقارنة مع النماذج متعددة اللغات الصادرة سابقًا مثل mBERT أو XLM في المهام النهائية مثل التصنيف Classification والوسم أو تحديد التسميات تسلسليًا Sequence Labeling والإجابة على الأسئلة Question Answering. نموذج M2M100 يمكن استخدام نماذج M2M100 التالية للترجمة متعددة اللغات: facebook/m2m100_418M للترجمة facebook/m2m100_1.2B للترجمة لنحمّل مثلًا نقطة تحقق النموذج facebook/m2m100_418M للترجمة من الصينية إلى الإنجليزية، حيث يمكنك ضبط لغة المصدر في المرمِّز كالتالي: >>> from transformers import M2M100ForConditionalGeneration, M2M100Tokenizer >>> en_text = "Do not meddle in the affairs of wizards, for they are subtle and quick to anger." >>> chinese_text = "不要插手巫師的事務, 因為他們是微妙的, 很快就會發怒." >>> tokenizer = M2M100Tokenizer.from_pretrained("facebook/m2m100_418M", src_lang="zh") >>> model = M2M100ForConditionalGeneration.from_pretrained("facebook/m2m100_418M") ونرمِّز النص كما يلي: >>> encoded_zh = tokenizer(chinese_text, return_tensors="pt") يفرض النموذج M2M100 أن يكون معرّف اللغة المستهدفة هو أول Token مُولَّد لترجمته إلى اللغة المستهدفة، لذا نضبط المعرّف forced_bos_token_id على القيمة en في التابع generate للترجمة إلى اللغة الإنجليزية كما يلي: >>> generated_tokens = model.generate(**encoded_zh, forced_bos_token_id=tokenizer.get_lang_id("en")) >>> tokenizer.batch_decode(generated_tokens, skip_special_tokens=True) 'Do not interfere with the matters of the witches, because they are delicate and will soon be angry.' النموذج MBart يمكن استخدام نماذج MBart التالية للترجمة متعددة اللغات: facebook/mbart-large-50-one-to-many-mmt للترجمة الآلية متعددة اللغات من لغة إلى عدة لغات، ويدعم 50 لغة facebook/mbart-large-50-many-to-many-mmt للترجمة الآلية متعددة اللغات من عدة لغات إلى عدة لغات، ويدعم 50 لغة facebook/mbart-large-50-many-to-one-mmt للترجمة الآلية متعددة اللغات من عدة لغات إلى لغة واحدة، ويدعم 50 لغة facebook/mbart-large-50 للترجمة متعددة اللغات، ويدعم 50 لغة facebook/mbart-large-cc25 للترجمة الآلية متعددة اللغات، ويعمل مع 25 لغة لنحمّل مثلًا نقطة تحقق النموذج facebook/mbart-large-50-many-to-many-mmt لترجمة اللغة الفنلندية إلى اللغة الإنجليزية، ويمكنك ضبط لغة المصدر في المرمِّز كما يلي: >>> from transformers import AutoTokenizer, AutoModelForSeq2SeqLM >>> en_text = "Do not meddle in the affairs of wizards, for they are subtle and quick to anger." >>> fi_text = "Älä sekaannu velhojen asioihin, sillä ne ovat hienovaraisia ja nopeasti vihaisia." >>> tokenizer = AutoTokenizer.from_pretrained("facebook/mbart-large-50-many-to-many-mmt", src_lang="fi_FI") >>> model = AutoModelForSeq2SeqLM.from_pretrained("facebook/mbart-large-50-many-to-many-mmt") ونرمِّز النص كما يلي: >>> encoded_en = tokenizer(en_text, return_tensors="pt") يفرض النموذج MBart معرّف اللغة المستهدفة بوصفه أول رمز مُولَّد لترجمته إلى اللغة المستهدفة، لذا اضبط المعرّف forced_bos_token_id على القيمة en في التابع generate للترجمة إلى اللغة الإنجليزية كما يلي: >>> generated_tokens = model.generate(**encoded_en, forced_bos_token_id=tokenizer.lang_code_to_id["en_XX"]) >>> tokenizer.batch_decode(generated_tokens, skip_special_tokens=True) "Don't interfere with the wizard's affairs, because they are subtle, will soon get angry." إذا أدرتَ استخدام نقطة تحقق النموذج facebook/mbart-large-50-many-to-one-mmt، فلن تحتاج إلى فرض معرّف اللغة المستهدفة بوصفه أول رمز مُولَّد، وإلّا فسيبقى الاستخدام نفسه مع النماذج الأخرى. الخلاصة تعلمنا في هذا المقال كيفية استخدام مكتبة المرمزات Tokenizers من منصة Hugging Face، والتي تتيح تقسيم النصوص إلى رموز أو وحدات صغيرة Token. كما تناولنا طريقة إنشاء مرمِّز باستخدام أسلوب BPE وتدريبه على بيانات مخصصة. وشرحنا كيفية استخدام المرمِّز المُدرَّب داخل مكتبة المحولات Transformers وكيفية حفظه لإعادة استخدامه. واستعرضنا النماذج مثل XLM وBERT و XLM-RoBERTa التي تدعم تحليل النصوص بلغات متعددة وتستخدم لتطبيقات متنوعة مثل الترجمة والتنبؤ وتحليل النصوص. اقرأ أيضًا جولة سريعة للبدء مع مكتبة المحوّلات Transformers تعرف على مكتبة المحوّلات Transformers من منصة Hugging Face مشاركة نموذج ذكاء اصطناعي على منصة Hugging Face تثبيت مكتبة المحوّلات Transformers
  21. لقد تعرفنا في المقالات السابقة على على كيفية ضرب المصفوفات بمصفوفة عمودية وكذلك كيفية حساب عمليات ضرب المصفوفات لعمل تصاميم ثلاثية الأبعاد 3D وغيرها، وسنتابع في هذا المقال، شرح آخر نوع من عمليات ضرب المصفوفات، إذ يوضّح هذا المقال خاصيات أخرى لعملية ضرب المصفوفات، خاصةً المصفوفة المحايدة Identity Matrix ومعكوس المصفوفة Matrix Inverse. سنوضّح في هذا المقال المواضيع التالية: المصفوفة المحايدة: ‎IA = AI = A‎ المصفوفة المحايدة هي مصفوفة فريدة منقول Transpose المصفوفة المحايدة: ‎IT = I‎ ضرب المصفوفة المحايدة بنفسها: ‎II = I‎ المصفوفة المحايدة الموجودة في المنتصف بين مصفوفتين: ‎AIB = AB‎ معكوس مصفوفة: ‎AA-1 = A-1A = I‎ المصفوفات المفردة Singular وغير المفردة Non-singular معكوس المصفوفة هو مصفوفة فريدة معكوس ضرب مصفوفتين: ‎(AB)-1 = B-1 A-1‎‎ معكوس منقول المصفوفة: ‎‎(A-1)T = (AT)-1‎‎ محدِّد Determinant المصفوفة المفردة ستكون جميع المصفوفات في هذا المقال مصفوفات مربعة؛ فالمصفوفة المربعة هي المصفوفة التي يكون فيها عدد الصفوف مساويًا لعدد الأعمدة. الضرب الملحق Post-multiplication للمصفوفات في المصفوفة المحايدة تستخدم الرسوميات الحاسوبية المصفوفات المربعة لأغراض متعددة؛ إذ تُطبَّق معظم عمليات التحويل Transformations، مثل تغيير مجال الرؤية في العالم ثلاثي الأبعاد أو عرضها على صورة ثنائية الأبعاد، باستخدام المصفوفات المربعة. وكما هو معروف، للمصفوفات المربعة خاصيات لا تتشارك فيها مع المصفوفات الأخرى، لذا سنتعرف أولًا على إحدى هذه الخاصيات من خلال إجراء عملية الضرب التالية: وبالتالي نلاحظ أن الضرب الملحق لمصفوفةٍ ما بالمصفوفة المحايدة يعطي المصفوفة نفسها: ‎AI = A‎‎ الضرب المسبق Pre-multiplication للمصفوفات بالمصفوفة المحايدة لنجرِ الآن عملية الضرب التالية: سنلاحظ أن الضرب المسبق لمصفوفةٍ ما بالمصفوفة المحايدة يعطي المصفوفة نفسها: ‎IA = A‎‎ المصفوفة المحايدة Identity Matrix تُسمَّى المصفوفة I بالمصفوفة المحايدة، لأن ضرب المصفوفة بها، يعطي المصفوفة نفسها: IA = A و ‎AI = A‎، وذلك مهما كانت المصفوفة A؛ وهي مشابهة للعدد الحقيقي 1 الذي يمثل العنصر المحايد لعملية ضرب الأعداد الحقيقية، لأن 1a = a و a1 = a لجميع الأعداد الحقيقية a. لا توجد مصفوفة محايدة مُحدَّدة للمصفوفات بجميع أبعادها؛ إذ تكون المصفوفة ‎IN×N‎ مصفوفةًَ محايدةً للمصفوفات المربعة التي أبعادها N×N فقط، فمثلًا المصفوفة المحايدة للمصفوفات التي أبعادها 3×3 هي: نضع العدد 1 مكان عناصر القطر الرئيسي Main Diagonal للمصفوفة المحايدة ذات N بُعد ‎IN×N‎، والعدد 0 للعناصر الأخرى في المصفوفة؛ إذ يتكون القطر الرئيسي من العناصر التي يتساوى فيها رقم الصف مع رقم العمود، ويجب أن تكون الأرقام من النوع 1 على هذا القطر، وليس على القطر الآخر. المصفوفة المحايدة هي مصفوفة فريدة لا توجد مصفوفة أخرى أبعادها N×N تماثل في عملها المصفوفة المحايدة؛ فبما أن العدد 1 فريدٌ بالنسبة للأعداد الحقيقية، فإن المصفوفة المحايدة هي مصفوفةٌ فريدة بالنسبة للمصفوفات التي أبعادها NxN، وسنبرهن ذلك فيما يلي: لنفترض أن جميع المصفوفات لها الأبعاد الصحيحة لعملية الضرب، وأن هناك مصفوفة Z تحقق ما يلي: ‎ZA = A‎ (1)‎ مهما كانت المصفوفة A التي لها الأبعاد الصحيحة، فستعمل المصفوفة Z مثل عمل المصفوفة I، ولكن نأمل أن تكون مختلفةً عنها؛ أما بالنسبة للمصفوفة B فنعلم أن ضرب مصفوفة B بالمصفوفة المحايدة يعطي المصفوفة B نفسها مهما كانت المصفوفة B التي لها الأبعاد الصحيحة. ‎BI = B‎ (2) لنضع المصفوفة المصفوفة I مكان المصفوفة A في العلاقة رقم (1)، بما أن A يمكن أن تكون أي مصفوفة مناسبة. ‎ZI = I‎ (3) ولنضع الآن المصفوفة Z مكان المصفوفة B في العلاقة رقم (2): ZI = Z (4) لاحظ من العلاقتين (3) و(4) أن Z و I متساويين مع الشيء نفسه، لذا يجب أن يكونا متساويين. Z = I (5) يمكنك البدء مرةً أخرى من الخطوة رقم (1) بالترتيب الآخر ‎AZ = A‎ والوصول إلى النتيجة ذاتها؛ لذا إذا وجدت مصفوفةً تعمل مثل عمل المصفوفة المحايدة، فهي بالتأكيد المصفوفة المحايدة بحد ذاتها. خاصيات أخرى للمصفوفة المحايدة لنوضح الآن بعد الخاصيات الأخرى للمصفوفة المحايدة. على سبيل المثال، يُعَد منقول المصفوفة المحايدة هو المصفوفة المحايدة نفسها: IT = I‎ وهناك خاصية أخرى مفيدة للمصفوفة المحايدة، والتي تُعَد مجرد تطبيقٍ لتعريفها؛ حيث إذا جاءت المصفوفة المحايدة بين مصفوفتين، فلن تؤثر على عملية الضرب: AIB = AB‎ كما أن مربع المصفوفة المحايدة هو المصفوفة المحايدة نفسها: I I = I‎ أو I2 = I تُعَد هذه الخاصيات واضحةً ولا حاجة لحفظها. المصفوفة القطرية Diagonal Matrix تُعَد المصفوفة القطرية (a I‎) مفيدةً أحيانًا في الرسوميات الحاسوبية. لنفترض مثلًا أننا نريد حساب الآتي، بحيث يكون a عدد حقيقي و x مصفوفة عمودية: a Ax‎‎‎ هنا وبحالات كثيرة، سنجد أنه من المفيد أحيانًا التفكير على النحو التالي: ‎(a I) Ax وبهذا تكون قد أصبحت جميع العمليات عبارة عن عمليات ضرب مصفوفات، ويُعَد ذلك ميزة، نظرًا لأنه يمكن إجراء عملية ضرب المصفوفات باستخدام عتاد الرسوميات الحاسوبية دون الحاجة لحسابها. لنحاول الآن إيجاد ناتج ضرب المصفوفتين التاليتين بأبسط طريقة كما يلي: عملية ضرب المصفوفات التي ينتج عنها المصفوفة المحايدة من الواضح أن: ‎(2 I)(0.5 I) = 2 (0.5) I = 1 I = I ولكن جرّب ضرب هاتين المصفوفتين بالطريقة العادية: لاحظ أن الصفوف من المصفوفة الأولى تتلاءم مع أعمدة المصفوفة الثانية، بحيث يساوي حاصل الجداء النقطي لها القيمة 1 لعناصر القطر الرئيسي فقط؛ بينما تساوي جميع عمليات الجداء النقطي الأخرى القيمة 0، وبالتالي فالمصفوفة الناتجة هي المصفوفة المحايدة I. معكوس المصفوفة لنفترض أن ax = b للأعداد الحقيقية a و b، وأن المتغير الحقيقي هو x، ولنوجد قيمة x الآن. إذا كان ax = b، فإن ‎(a-1) ax = (a-1)b، أو x = (a-1)b، إلّا عندما يكون العدد a صفرًا؛ فالقيمة (a-1‎) هي معكوس العدد a، وجميع الأعداد الحقيقية غير الصفرية لها معكوس. توجد فكرة مماثلة خاصة بالمصفوفات المربعة، وليكن لدينا مثلًا: Ap = q‎ لدينا: p و q هي أشعة عمودية، أما A فهي مصفوفة أبعادها n×n. ومع افتراض أن A و q معلومتان: لنجرّب معرفة قيمة المصفوفة العمودية p للوصول إلى النتيجة التالية: يُعَد إيجاد قيمة p بالتخمين أمرًا صعبًا، وسيكون الأمر أسوأ بكثير إذا كانت أبعاد المصفوفة A هي 5 × 5؛ لذا توجد طريقة أفضل لذلك، وهي باستخدام معكوس المصفوفة. إذًا، لتكن لدينا p و q مصفوفات عمودية و A مصفوفة أبعادها N×N، حيث تكون قيمة q و A معلومتين، ونريد معرفة قيمة p: ‎q = Ap‎ (1)‎ لنفترض وجود مصفوفة ‎BN×N‎ تحقق ما يلي: ‎p = Bq‎ (2) إذا وُجِدت مثل هذه المصفوفة، فيمكننا حساب ما نريده (أي المصفوفة p) من المصفوفة q، لذا عوّض ما توصلت إليه من العلاقة رقم (2) في العلاقة رقم (1): ‎q = A (Bq)‎ (3) ‎q = (AB) q‎ (4) إذا كانت العلاقة رقم (4) صحيحة، فإن ‎(AB) = I‎. وعلينا هنا أن نتذكّر أن المصفوفة المحايدة هي مصفوفة فريدة. إذا كانت المصفوفة B موجودةً فعلًا، فهي تُعَد معكوسًا للمصفوفة A، وتُكتَب بالشكل: ‎A-1‎؛ وبالتالي يمكننا الآن إيجاد قيمة p في العلاقة رقم (1) من خلال ضرب كل طرف منها في المعكوس ‎ A-1‎كما يلي: A-1 q = A-1 Ap‎ A-1 q = p‎ المصفوفة غير المفردة Non-singular ليس لجميع المصفوفات المربعة معكوس دائمًا، فالمصفوفة الصفرية مثلًا ليس لها معكوس؛ إذ لا توجد مصفوفة ‎0-1‎ تحقق الخاصية: ‎0 ‎0-1‎ = I‎، ولكن الأمر أسوأ من ذلك، فالعديد من المصفوفات المربعة التي أبعادها N × N ليس لها معكوس. تسمى المصفوفة التي لها معكوس: المصفوفة غير المفردة Non-singular، في حسن تسمى المصفوفة التي ليس لها معكوس: المصفوفة المفردة Singular. إذا كانت المصفوفة A غير مفردة، فهي تحقق ما يلي: AA-1= A-1A = I لدينا هنا مثال عن مصفوفة لها معكوس: وليكن لدينا مثلًا ما يلي: بحيث يمكننا إيجاد قيمة p باستخدام المعكوس ‎A-1‎ كما يلي: معكوس المصفوفة هو مصفوفة فريدة إذا كانت المصفوفة A غير مفردة (أي لها معكوس) وكان ‎Ap = q‎، فإن ‎p = A-1 q‎. يُعَد معكوس المصفوفة المربعة غير المفردة فريدًا، وإحدى الطرق لإثبات ذلك هي وجود مصفوفة عمودية واحدة p فقط تُعَد حلًا للمعادلة ‎Ap = q‎، لذلك يجب أن يكون هناك معكوس ‎A-1‎ واحد فقط للمصفوفة. ويُعَد المعكوس ‎A-1‎ مفيدًا في المناقشات حول المصفوفات وعمليات التحويل، ولكنه ليس مفيدًا جدًا للحسابات الفعلية، لذا لا حاجة لحسابه أبدًا. لنفترض مثلًا أن المصفوفة العمودية p تمثل نقطةً في عالم الرسوميات الحاسوبية، وأنه في حال تغير مجال الرؤية، فستُحوَّل المصفوفة العمودية إلى العلاقة ‎ .q = Ap‎ قد يفكر الكثيرون هنا بعكس التحويل باستخدام ‎A-1 q‎، ولكن هناك دائمًا طريقة أسهل للعودة إلى مجال الرؤية الأصلي بدلًا من حساب المعكوس. لنحاول إيجاد ناتج (‎AB) (B-1 A-1‎) كما يلي: باستخدام خاصية التجميع: ‎(‎AB) (B-1 A-1‎) = A (B B-1) A-1 ‎= A I A-1 = A A-1 = I خاصيات معكوس المصفوفة يساوي معكوس ضرب مصفوفتين نتيجة ضرب معكوس المصفوفة الثانية بمعكوس المصفوفة الأولى؛ فقد عكسنا ترتيب المصفوفات كما يلي: ‎(AB)-1 = B-1 A-1 وتوجد خاصية أخرى مفيدة وهي: ‎(A-1)T = (AT)-1 محدد Determinant المصفوفة المفردة يمكن حساب محدِّد المصفوفة التي أبعادها 2 × 2 على النحو التالي: إليك مثال لحساب محدّد مصفوفة: يُعَد حساب محدد المصفوفات الأكبر أكثر تعقيدًا؛ إذ يندر تطبيقها، حيث يُستخدَم المحدّد عند مناقشة المصفوفات، وليس في العمليات الحسابية. ملاحظة: يساوي محدّد المصفوفة المفردة الصفر. رتبة Rank المصفوفة إليك مثال آخر لحساب محدّد مصفوفة: للمصفوفة في المثال السابق محدّد صفري، وبالتالي فهي مصفوفة مفردة، وليس لها معكوس، وتتكون من صفين متطابقين، وبالتالي يمكن القول بأن صفوفها غير مستقلة؛ فإذا كان أحد الصفوف مضاعفًا لصف آخر، فإننا نسمّيها صفوفًا غير مستقلة؛ إذ يساوي محدّد هذه المصفوفة الصفر. وبالمثل، إذا كان أحد الأعمدة مضاعفًا لعمود آخر، فسيكون هذين العمودين غير مستقلين، وبالتالي يكون محدّد هذه المصفوفة صفرًا. رتبة المصفوفة هي الحد الأقصى لعدد الصفوف المستقلة (أو الحد الأقصى لعدد الأعمدة المستقلة)، وتكون المصفوفة المربعة ‎An×n‎ غير مفردة فقط إذا كانت رتبتها تساوي n. لنجرب إيجاد رتبة المصفوفة التالية: رتبة المصفوفة السابقة هي 3، حيث يُعَد الصف الأخير مضاعفًا للصف الأول. خاتمة بهذا نكون قد تعرفنا على مفهوم كل من المصفوفة المحايدة Identity Matrix ومعكوس المصفوفة وكيفية استخدامهما في عملية ضرب المصفوفات. وبهذا نكون قد وصلنا لنهاية هذه السلسلة التعليمية حول الأشعة وجبر المصفوفات من وجهة نظر رسومات الحاسوب، حيث غطينا عدة مواضيع عن الأشعة والمصفوفات وتعرفنا على عدة حالات للتعامل معها. ترجمة -وبتصرُّف- للفصل Identity Matrix and Matrix Inverse من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: ضرب المصفوفات وحسابها لعمل تصاميم ثلاثية الأبعاد 3D التعرف على النقاط والخطوط في الرسوميات الحاسوبية ثلاثية الأبعاد الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد تعرف على المصفوفات Matrices وعملياتها البسيطة في التصاميم 3D
  22. تُستخدَم عملية ضرب المصفوفات في الرسومات الحاسوبية من أجل إنشاء مصفوفات التحويل Transformation التي تُطبَّق على النقاط والأشعة. وتُصمَّم المناظر في الألعاب ذات نمط منظور الشخص الأول First Person Game مثل لعبة DOOM باستخدام النقاط والأشعة أيضًا، كما ينشأ العرض المتغير الذي نراه أثناء تحرّك اللاعب عبر هذه المناظر بتحويل تلك النقاط والأشعة. ونظرًا لأهمية المصفوفات في مجال النقاط والأشعة التي بدورها تتداخل مباشرةً مع عملية إنشاء تصاميم ثلاثية الأبعاد، فسنفصل في هذا المقال في كيفية التعامل مع المصفوفات وضربها ببعضها. لقد تحدثنا في المقال السابق عن كيفية ضرب المصفوفات بمصفوفة عمودية، ولمعرفة كيفية التعامل مع المصفوفات أكثر، سنناقش بهذا المقال مفهوم ضرب المصفوفات وخاصيتها وكيفية حسابها لعمل تصاميم ثلاثية الأبعاد 3D؛ وسنتحدث عما يلي: متى نضرب المصفوفات ناتج ضرب المصفوفات نتاج كل عنصر من عناصر المصفوفة المحصل عليها بتطبيق عملية جداء نقطي الخاصية التجميعية لضرب المصفوفات الخاصية غير التبديلية لضرب المصفوفات توزيع ضرب المصفوفات على عملية الجمع الضرب بالمصفوفة الصفرية أبعاد نتيجة ضرب المصفوفات يمكن ضرب المصفوفتين ‎A4×3‎ و ‎B3×5‎ عند تطابق البعد الداخلي الذي قيمته 3؛ فإذا رتبنا مصفوفتين مستطيلتين بحيث يكون البعد الداخلي لكل منهما متساويًا، فيمكن ضرب هاتين المصفوفتين، وستكون النتيجة عمومًا عبارة عن مصفوفة مستطيلة كالآتي: AR×N‎ BN×C‎ = ZR×C‎ تحتوي عملية الضرب AB -إذا كان تشكيلها ممكنًا- على نفس عدد صفوف المصفوفة A وعلى نفس عدد أعمدة المصفوفة B؛ وهنا إذا لم نتمكن من تجاهل البعد الداخلي، فلن نتمكن من تشكيل عملية الضرب. فلنطّلع الآن على المثال التالي، مع تجاهل كيفية حساب العناصر مبدئيًا؛ إذ سنضرب مصفوفةً أبعادها 3‎×2 في مصفوفة أخرى أبعادها 2‎×2، والنتيجة ستكون عبارة عن مصفوفة أبعادها 3‎×2: انطلاقًا مما سبق، نستطيع القول أن أبعاد نتيجة ضرب المصفوفتين ‎A4×4‎ B4×2‎ مثلًا، ستكون: A4×4‎ B4×2‎ = C4×2 ملاحظة: تُعَد المصفوفات العمودية والمصفوفات السطرية حالة خاصة من المصفوفات المستطيلة، لذا فإن قاعدة أبعاد نتيجة ضرب المصفوفات تسري عليها أيضًا. لنتدرب الآن على مزيدٍ من الأمثلة في الجدول التالي؛ إذ سنحدّد أبعاد نتيجة ضرب كلّ زوج من المصفوفات: أبعاد المصفوفة الأولى أبعاد المصفوفة الثانية النتيجة 4‎ × 3 ‫ 3‎ × 2 ‫ 4‎ x 2 2‎ × 3 ‫ 2‎ × 3 ‫ غير ممكنة 3‎ × 5 ‫ 5‎ × 1 ‫ 3‎ x 1 3‎ × 2 ‫ 2‎ × 2 ‫ 3‎ x 2 4‎ × 3 ‫ 3‎ × 1 ‫ 4‎ x 1 3‎ × 1 ‫ 1‎ × 4 ‫ 3‎ x 4 ملاحظة: سنحتاج للتحقق دائمًا من أبعاد نتيجة ضرب المصفوفات قبل إجراء العمليات الحسابية؛ فمثلًا، عملية الضرب الأخيرة في الجدول السابق غريبة بعض الشيء، ومن السهل أن يختلط علينا الأمر دون معرفة الشكل الذي يجب أن تبدو عليه النتيجة. يمكننا تشكيل عملية الضرب ‎An×m‎ Bm×p‎، ولكن لا يمكننا تشكيل عملية الضرب ‎Bm×p‎ An×m‎ إذا كانت n ≠ p. وسيتضح لاحقًا أنه حتى لو أمكن تشكيل كلتا عمليتي الضرب، فمن النادر أن يكون ‎AB = BA‎. تشكيل عملية ضرب المصفوفات يُحسَب حاصل ضرب مصفوفتين AB من خلال تطبيق الجداء النقطي على كل صف من المصفوفة A مع كل عمود من المصفوفة B، بحيث يكون عدد الأعمدة في المصفوفة A مساويًا لعدد الصفوف في المصفوفة B. لنجرب قلب أحد أعمدة المصفوفة B بحيث تصبح عناصره محاذيةً لصفوف المصفوفة A، ثم نطبّق عليها الجداء النقطي؛ وهنا سينتج لدينا العنصر الموجود في الصف 1 والعمود 1 من المصفوفة الناتجة. لننتقل بعدها إلى الصف التالي مع تطبيق الجداء النقطي، لينتج لدينا العنصر الموجود في الصف 2 والعمود 1 من المصفوفة الناتجة؛ وسنستمر بهذه العملية حتى الوصول إلى الصف الأخير. لننتقل الآن إلى العمود التالي ونقوم بنفس الشيء عند الانتهاء من العمود الأول من المصفوفة B، ونواصل تطبيق ذات الشيء على كل عمود من المصفوفة B حتى حساب جميع العناصر. ملاحظة: العنصر ij من المصفوفة الناتجة = الجداء النقطي للصف i من المصفوفة A مع العمود j من المصفوفة B. لنفترض أننا نشكّل عملية الضرب التالية: ‎A5×3‎ B3×2 = C5×2 سنستخدم الصف الثالث من المصفوفة A والعمود الثاني من المصفوفة B، لحساب الصف الثالث، والعمود الثاني من المصفوفة الناتجة C. طريقة أخرى لتصور عملية ضرب المصفوفات قد تعرض كتب الرسوميات الحاسوبية طريقةً مختلفةً لضرب المصفوفات؛ إذ أن هناك عدة طرق للتفكير في الأمر بالرغم من أن النتائج هي نفسها. يوضّح الرسم البياني التالي مثلًا طريقةً أخرى لتصور عملية الضرب ‎AB = C‎؛ إذ سنكتب المصفوفة A على يسار المصفوفة الناتجة C والمصفوفة B فوقها، ثم نرسم خطوطًا أفقية وأخرى عمودية لتقسيم المصفوفة C إلى خلايا؛ وسيؤدي ذلك تلقائيًا إلى تشكيل العدد الصحيح من الخلايا. ينتج العدد الذي يجب وضعه في كل خلية من حاصل الجداء النقطي لصف المصفوفة A مع عمود المصفوفة B اللذين يتقاطعان عند هذه الخلية. لنحسب الآن قيمة العنصرين c11‎ و c32‎ كما يلي: c11 = 1×4 + -2×-1 = 6 c32 = -1×1 + 4×2 = 7 سنجرّب الآن حساب حاصل الجداء النقطي الخاص بكل خلية في الرسم البياني التالي: وستكون المصفوفة الناتجة هي المصفوفة التالية: عناصر المصفوفة هي أعداد حقيقية يُعَد كل عنصر في المصفوفة مقدارًا سلميًا Scalar أي عددًا حقيقيًا. لقد استخدمنا في أمثلتنا السابقة أعدادًا صحيحة حتى تكون العملية الحسابية سهلة، ولكن إليك مثال آخر مع قيم عشرية: تشتمل معظم برامج جداول البيانات والآلات الحاسبة الإلكترونية العلمية على دالات رياضية خاصة بالمصفوفات؛ لكن من الأفضل محاولة إجراء الحسابات ذاتيًا عند التعلم من أجل استيعاب العملية. عملية ضرب المصفوفات ليست عملية تبديلية لا تُعَد المساواة ‎AB= BA‎ صحيحةً دائمًا، فحتى إذا تمكنا من تشكيل هاتين العمليتين، فمن النادر أن تكون نتيجتهما متساويتين. لنحسب مثلًا عمليتي الضرب التاليتين: العملية النتيجة الناتج الناتج وهذا يوضّح أن ‎ AB ≠ BA‎بصورة عامة بالنسبة للمصفوفتين A و B، ولكن يمكن مثلًا: A4×4 04×4 = 04×4 A4×4 = 04×4 المصفوفة 0 هي المصفوفة الصفرية ذات الأبعاد الصحيحة لجعل عملية الضرب ممكنة، وبالتالي يمكن أن تكون عملية الضرب عمليةً تبديليةً ‎AB = BA‎ بالنسبة لبعض المصفوفات؛ ولكن لا يمكننا تعميم هذه الخاصية على جميع المصفوفات. تدريب عملي لنوجد ناتج ضرب المصفوفتين التاليتين: لنجرب أيضًا ضرب المصفوفتين التاليتين مثلًا: استخراج عامل عددي من مصفوفة ليكن c(AB) = (cA)B‎ من أجل العدد الحقيقي c، علمًا أن أن c A‎ يعني أن كل عنصر من عناصر المصفوفة A مضروب بالعدد c. سنوضّح هذه القاعدة في المثال التالي: يمكننا تبسيط عملية ضرب المصفوفات في كثير من الأحيان من خلال استخراج عامل عددي من إحدى المصفوفات كما في المثال التالي: عملية ضرب المصفوفات هي عملية تجميعية لنضرب المصفوفتين الأوليتين، ثم نضرب النتيجة بالمصفوفة الثالثة كما يلي: ولنبدأ الآن بالمصفوفتين الأخيرتين أولًا كما يلي: وكما هو واضح، الإجابة النهائية هي نفسها بكلا الطريقتين، وهذا يوضّح أن عملية ضرب المصفوفات هي عملية تجميعية Associative: (AB) ‪C = A (BC) يجب أن تكون الأبعاد الداخلية للمصفوفتين A و B نفسها، كما يجب أن تكون الأبعاد الداخلية للمصفوفتين B و C نفسها. تُكتَب عملية ضرب ثلاث مصفوفات في العادة بالشكل الآتي: ABC. إذًا لنفترض مثلًا: A5×5 B?×? C3×4 = D?×?‪ حيث تكون أبعاد المصفوفتين B و D كما يلي: ‪A5×5 B5×3 C3×4 = D5×4 خاصية توزيع ضرب المصفوفات في جمعها تتعامل خاصية التوزيع مع تعبيرٍ يحتوي على كلٍّ من عمليتي ضرب وجمع المصفوفات؛ وسنتبع الخطوات التالية لذلك. أولًا، نجمع المصفوفتين كما يلي: ثانيًا، نضرب المصفوفتين: ثالثًا، نجري العملية مرةً أخرى بترتيب مختلف؛ إذ سنجري أولًا عملية ضرب المصفوفة اليسرى بكل من المصفوفتين الأخريين كما يلي: في الأخير، نجمع ناتج عمليتي الضرب السابقتين: وكما نلاحظ، كلتا النتيجتين متماثلتان، مما يدل على أن ضرب المصفوفات توزيعيٌّ على جمعها: ‪A (B + C) = AB + AC ويمكن القول أيضًا أن: (X + Y) Z = XZ + YZ ملاحظة: استخدمنا في معظم أمثلتنا أعدادًا صحيحة، ولكن لا يجب أن ننسى أن عناصر المصفوفة قد تكون أعددًا حقيقية أو متغيرات كما يلي: ملخص بالقواعد الخاصة بعملية ضرب المصفوفات سنذكر الآن قائمة بالقواعد الخاصة بعملية ضرب المصفوفات التي ناقشناها في هذا المقال، حيث تفترض كل قاعدة أنه يمكن ضرب المصفوفات؛ أي أن أبعادها مناسبة لعملية الضرب: يمكن تشكيل عملية ضرب المصفوفات في حال: إذا كان ‎AM×K BK×N = C‎، فإن C = CM×N‎ ضرب المصفوفات ليس عملية تبديلية: ‎AB ≠ BA‎، إلّا في حالات ناردة ضرب المصفوفات هو عملية تجميعية: A (BC) = (AB) C = ABC استخراج المعامل العددي من المصفوفات: a(AB) = (a A) B = a AB = A (a B) خاصية التوزيع على الجمع: A ( B + C) = AB + AC (A + B) C = AC + BC الضرب بالمصفوفة الصفرية 0: 0A = 0 بهذا نكون قد وصلنا إلى نهاية هذا المقال التي تعرّفنا من خلاله على عملية ضرب المصفوفات وحسابها، وسنناقش في المقال التالي مزيدًا من خاصيات عملية ضرب المصفوفة بمصفوفة أخرى. ترجمة -وبتصرُّف- للفصل Matrix-Matrix Multiplication من كتاب Vector Math for 3D Computer Graphics لصاحبه Bradley Kjell. اقرأ أيضًا المقال السابق: ضرب المصفوفات بمصفوفة عمودية في التصاميم ثلاثية الأبعاد 3D التعرف على النقاط والخطوط في الرسوميات الحاسوبية ثلاثية الأبعاد الأشعة والنقاط والمصفوفات العمودية في الرسوميات الحاسوبية ثلاثية الأبعاد تعرف على المصفوفات Matrices وعملياتها البسيطة في التصاميم 3D
  23. حان الوقت الآن لإنشاء تطبيق مدونة متكامل باستخدام إطار عمل جانغو Django، فقد تعلمنا في المقال السابق كيف يمكن للنماذج Model والعروض View والقوالب Template أن تعمل معًا لإنشاء تطبيق جانغو لكن هذه العملية صعبة نوعًا ما، إذ يتوجب علينا كتابة 5 إجراءات على الأقل لكل ميزة نريد تحقيقها، وستكون الشيفرة البرمجية مكررةً في معظمها. لذا سنستخدم في هذا المقال واحدة من أفضل ميزات جانغو، وهي لوحة المدير Admin Panel المُضمَّنة، فما عليك سوى كتابة إجراء العرض لمعظم الميزات التي ترغب في إنشائها لتطبيقك، وسيتولى جانغو الباقي نيابة عنك تلقائيًا. إنشاء طبقة النموذج Model سنبدأ أولًا بتصميم بنية قاعدة البيانات. تصميم بنية قاعدة البيانات بالنسبة لأيّ نظام تدوين أساسي، ستحتاج إلى 4 نماذج على الأقل هي User و Category و Tag و Post، وسنضيف في المقال التالي بعض الميزات المتقدمة لاحقًا، ولكن سنكتفي حاليًا بهذه النماذج الأربعة فقط. نموذج المستخدم User المفتاح نوعه معلومات عنه المعرّف id عدد صحيح Integer يتزايد تلقائيًا الاسم name سلسلة نصية String - البريد الإلكتروني email سلسلة نصية فريد كلمة السر password سلسلة نصية - إن النموذج User مُضمَّن مسبقًا في جانغو، إذ يوفّر هذا النموذج المُضمَّن بعض الميزات الأساسية مثل تشفير كلمات المرور Password Hashing واستثياق المستخدمين User Authentication، بالإضافة إلى نظام الأذونات المُضمَّن مع مدير جانغو Django Admin كما سنوضّح لاحقًا. نموذج الفئة Category المفتاح نوعه معلومات عنه المعرّف id عدد صحيح Integer يتزايد تلقائيًا الاسم name سلسلة نصية String - الاسم المختصر slug سلسلة نصية فريد الوصف description نص - نموذج الوسم Tag المفتاح نوعه معلومات عنه المعرّف id عدد صحيح Integer يتزايد تلقائيًا الاسم name سلسلة نصية String - الاسم المختصر slug سلسلة نصية فريد الوصف description نص - نموذج المنشور Post المفتاح نوعه معلومات عنه المعرّف id عدد صحيح Integer يتزايد تلقائيًا العنوان title سلسلة نصية String - الاسم المختصر slug سلسلة نصية فريد المحتوى content نص - featured_image سلسلة نصية String - is_published قيمة منطقية Boolean - is_featured قيمة منطقية - created_at تاريخ Date - نموذج الموقع Site ستحتاج أيضًا إلى جدول آخر لتخزين المعلومات الأساسية للموقع بالكامل مثل الاسم والوصف والشعار. المفتاح نوعه معلومات عنه الاسم name سلسلة نصية String - الوصف description نص text - الشعار logo سلسلة نصية - العلاقات توجد ست علاقات بالنسبة لهذا التطبيق وهي: لكل مستخدم عدة منشورات لكل فئة عدة منشورات ينتمي كل وسم إلى منشورات متعددة ينتمي كل منشور إلى مستخدم واحد ينتمي كل منشور إلى فئة واحدة ينتمي كل منشور إلى وسوم متعددة تطبيق التصميم باستخدام جانغو الآن، بعد أن انتهينا من تصميم هذا النموذج، سنبدأ في تطبيقه كما سنوضح في الخطوات التالية نموذج الموقع Site لنبدأ بنموذج الموقع Site مع توفير خصائص لحفظ بيانات الموقع مثل اسم الموقع ووصفه وصورة الشعار كما يلي: class Site(models.Model): name = models.CharField(max_length=200) description = models.TextField() logo = models.ImageField(upload_to="logo/") class Meta: verbose_name_plural = "site" def __str__(self): return self.name لاحظ أن نوع حقل الصورة ImageField()‎ هو سلسلة نصية string، إذ لا يمكن لقواعد البيانات تخزين الصور، لذا تُخزَّن الصور في نظام ملفات خادمك، وسيحتفظ هذا الحقل بالمسار الذي يشير لموقع الصورة. سنرفع الصور في هذا المثال إلى المجلد ‎mediafiles/logo/‎، وتذكّر إضافة مجلد ملفات الوسائط ‎MEDIA_ROOT = "mediafiles/"‎ في الملف settings.py الذي أعددناه سابقًا. ملاحظة: تحتاج لأن تثبّت مكتبة Pillow على جهازك ليعمل الحقل ImageField()‎ كما يلي: pip install Pillow نموذج الفئة Category بعدها سنصميم نموذج الفئة Category الذي سيُستخدم لتنظيم المحتويات ضمن المدونة ضمن فئات ويحدد الحقول التي سنحتاج إليها لتمثيل الفئات بشكل صحيح في قاعدة البيانات. يتضمن نموذج الفئة Category المحتويات التالية: class Category(models.Model): name = models.CharField(max_length=200) slug = models.SlugField(unique=True) description = models.TextField() class Meta: verbose_name_plural = "categories" def __str__(self): return self.name ينبغي أن يكون النموذج Category مفهومًا بالنسبة لك، لكن دعنا نوضّح الصنف Meta، الذي سنستخدمه لإضافة البيانات الوصفية Metadata إلى النماذج. تمثّل البيانات الوصفية للنموذج أيّ شيء ليس حقلًا مثل خيارات الترتيب واسم جدول قاعدة البيانات وما إلى ذلك، حيث استخدمنا في مثالنا الخيار verbose_name_plural لتحديد صيغة الجمع لكلمة "category"، فجانغو ليس ذكيًا مثل لارافيل Laravel في هذا الجانب، فإن لم نمنح جانغو صيغة الجمع الصحيحة، فسوف يستخدم الجمع "categorys"، وهذا خاطئ. تحدد الدالة ‎__str__(self)‎ الحقل الذي سيستخدمه جانغو عند الإشارة إلى فئة معينة، حيث استخدمنا في مثالنا الحقل name، وسنوضّح أهمية ذلك عندما نصل إلى قسم مدير جانغو. نموذج الوسم Tag يتضمن نموذج الوسم Tag لتخزين الوسوم التي سترتبط بالمنشورات وهو يتضمن اسم الوسم والرابط اللطيف له ووصفه كما توضح الشيفرة التالية: class Tag(models.Model): name = models.CharField(max_length=200) slug = models.SlugField(unique=True) description = models.TextField() def __str__(self): return self.name نموذج المنشور Post لنعرف أخيرًا نموذج المنشور Post الذي يتضمن اسم المنشور والرابط اللطيف والمحتوى الرئيسي والصورة البارزة للمنشور وتاريخ إنشائه وحقلين منطقيين يوضحان هل نشر أم لا وهل هو منشور مميز أما لا كما توضح الشيفرة التالية: from ckeditor.fields import RichTextField . . . class Post(models.Model): title = models.CharField(max_length=200) slug = models.SlugField(unique=True) content = RichTextField() featured_image = models.ImageField(upload_to="images/") is_published = models.BooleanField(default=False) is_featured = models.BooleanField(default=False) created_at = models.DateField(auto_now=True) # تعريف العلاقات category = models.ForeignKey(Category, on_delete=models.CASCADE) tag = models.ManyToManyField(Tag) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) def __str__(self): return self.title لاحظ في السطر 1 أنه إذا نسختَ هذه الشيفرة البرمجية ولصقتها لديك، فسيخبرك محرّر نصوصك بعدم العثور على RichTextField و ckeditor، لأنها حزمة خارجية ولا تُضمَّن في إطار عمل جانغو. تذكّر أنه يمكنك فقط إضافة نص عادي عند إنشاء منشور في المقال السابق، وهذا ليس مثاليًا لمقال في مدونة. إذ تمنحك محرّرات النصوص الغنية أو محرّرات WYSIWYG القدرة على تحرير صفحات HTML مباشرةً دون كتابة الشيفرة البرمجية، حيث استخدمنا المحرّر CKEditor في هذا المقال. لذا يمكنك تثبيت المحرّر CKEditor من خلال تشغيل الأمر التالي: pip install django-ckeditor سجّل بعد ذلك ckeditor في ملف الإعدادات settings.py كما يلي: INSTALLED_APPS = [ "blog", "ckeditor", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", ] تعريف العلاقات أخيرًا، يمكنك إضافة علاقات إلى النماذج، حيث سنضيف الأسطر الثلاثة التالية في النموذج Post للقيام بذلك كما يلي: category = models.ForeignKey(Category, on_delete=models.CASCADE) tag = models.ManyToManyField(Tag) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) بما أننا نستخدم نموذج المستخدم User المُضمَّن (settings.AUTH_USER_MODEL)، فتذكر استيراد الوحدة settings كما يلي: from django.conf import settings ولِّد بعد ذلك ملفات التهجير Migration Files وطبّقها على قاعدة البيانات باستخدام الأمرين التاليين: python manage.py makemigrations python manage.py migrate إعداد لوحة مدير المدونة الخطوة التالية هي إعداد لوحة المدير، حيث يأتي جانغو مع نظام إدارة مضمَّن معه، ويمكنك استخدامه من خلال التسجيل بمستخدم Superuser باستخدام الأمر التالي: python manage.py createsuperuser يمكنك بعد ذلك الوصول إلى لوحة المدير من خلال الانتقال إلى العنوان ‎http://127.0.0.1:8000/admin/‎. لا تزال لوحة المدير فارغة حاليًا، ولا يوجد بها سوى تبويب الاستيثاق Authentication الذي يمكنك استخدامه لإسناد أدوار مختلفة للمستخدمين، وهذا أمر معقد إلى حد ما ويتطلب مقالًا آخر، لذا لن نتحدث عنه الآن، بل سنركز على كيفية ربط تطبيق المدونة blog بنظام المدير، حيث يجب أن تجد ملفًا بالاسم admin.py ضمن التطبيق blog، لذا أضف إليه الشيفرة البرمجية التالية: from django.contrib import admin from .models import Site, Category, Tag, Post # سجّل نماذجك هنا class CategoryAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("name",)} class TagAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("name",)} class PostAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("title",)} admin.site.register(Site) admin.site.register(Category, CategoryAdmin) admin.site.register(Tag, TagAdmin) admin.site.register(Post, PostAdmin) نستورد في السطر 2 النماذج التي أنشأناها، ثم نسجل النماذج المستوردة باستخدام التابع admin.site.register()‎. لاحظ بعد ذلك وجود شيء إضافي عند تسجيل النموذج Category، وهو الصنف CategoryAdmin الذي عرّفناه في السطر 6، وبالتالي يمكنك تمرير بعض المعلومات الإضافية إلى نظام الإدارة في جانغو. يمكنك استخدام الميزة prepopulated_fields لتوليد أسماء لطيفة Slugs لجميع الفئات والوسوم والمنشورات، حيث تعتمد قيمة slug على الاسم name، ولنختبر ذلك من خلال إنشاء فئة جديدة. انتقل إلى العنوان ‎http://127.0.0.1:8000/admin/‎، وانقر على الفئات Categories، وأضف فئة جديدة. تذكّر أننا حددنا صيغة الجمع للكلمة Category في نموذجنا، فإن لم نفعل ذلك، فسيستخدم جانغو الجمع Categorys بدلًا من ذلك كما شرحنا سابقًا. لاحظ توليد الاسم اللطيف تلقائيًا عند كتابة الاسم. حاول الآن إضافة بعض البيانات التجريبية، يجب أن يعمل كل شيء بنجاح. عمليات ضبط اختيارية Optional Configurations لم ينتهي عملنا بعد، لذا افتح لوحة الفئات، ستلاحظ أن بإمكانك الوصول إلى الفئات من صفحة المنشور، ولكن لا توجد طريقة للوصول إلى المنشورات المقابلة من صفحة الفئة. لحل هذه المشكلة، يمكنك استخدام الصنف InlineModelAdmin في الملف blog/admin.py كما يلي: class PostInlineCategory(admin.StackedInline): model = Post max_num = 2 class CategoryAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("name",)} inlines = [ PostInlineCategory ] أنشأنا أولًا الصنف PostInlineCategory، ثم استخدمناه في CategoryAdmin، وتعني عبارة max_num = 2 عرض منشورين فقط في صفحة الفئة التي ستبدو كما يلي: يمكنك بعد ذلك تطبيق الشيء نفسه مع TagAdmin في الملف blog/admin.py: class PostInlineTag(admin.TabularInline): model = Post.tag.through max_num = 5 class TagAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("name",)} inlines = [ PostInlineTag ] لاحظ أن هذه الشيفرة البرمجية مشابهة جدًا لما سبق، ولكن المتغير model ليس Post فقط، بل هو Post.tag.through، لأن العلاقة بين المنشور Post والوسم Tag هي علاقة متعدد إلى متعدد Many-to-Many، وستكون النتيجة النهائية كما يلي: بناء طبقة العروض View ركزنا في الأقسام السابقة على الواجهة الخلفية ولوحة مدير المدونة، وسنركز الآن على جزء الواجهة الأمامية، وهو الجزء الذي يمكن للمستخدمين رؤيته، حيث سنبدأ بدوال العرض View Functions. بما أننا أعددنا لوحة المدير لمدونتنا، فلن نحتاج إلى إنشاء عمليات CRUD الكاملة، وبالتالي وسنهتم فقط بكيفية استرداد المعلومات من قاعدة البيانات. سنحتاج إلى أربع صفحات هي: الصفحة الرئيسية وصفحة الفئة وصفحة الوسم وصفحة المنشور، وسنحتاج إلى دالة عرض واحدة لكلٍّ منها. عرض الصفحة الرئيسية home ضع المحتويات التالية في الملف blog/views.py: from .models import Site, Category, Tag, Post def home(request): site = Site.objects.first() posts = Post.objects.all().filter(is_published=True) categories = Category.objects.all() tags = Tag.objects.all() return render(request, 'home.html', { 'site': site, 'posts': posts, 'categories': categories, 'tags': tags, }) نستورد في السطر 1 النماذج التي أنشأناها في المقال السابق. يحتوي الموقع في السطر 4 على المعلومات الأساسية لموقعنا، حيث نسترد دائمًا السجل الأول من قاعدة البيانات. يضمن التابع filter(is_published=True)‎ في السطر 5 عرض المقالات المنشورة فقط. بعد ذلك علينا تجهيز موجّه إرسال Dispatcher لعنوان URL المقابل لهذه الصفحة في الملف djangoBlog/urls.py كما يلي: path('', views.home, name='home'), عرض الفئة category ضع أيضًا المحتويات التالية في الملف blog/views.py: def category(request, slug): site = Site.objects.first() posts = Post.objects.filter(category__slug=slug).filter(is_published=True) requested_category = Category.objects.get(slug=slug) categories = Category.objects.all() tags = Tag.objects.all() return render(request, 'category.html', { 'site': site, 'posts': posts, 'category': requested_category, 'categories': categories, 'tags': tags, }) وتذكر تجهيز موجّه إرسال عنوان URL المقابل لهذه الصفحة في الملف djangoBlog/urls.py كما يلي: path('category/<slug:slug>', views.category, name='category'), مرّرنا متغيرًا إضافيًا هو slug من عنوان URL إلى دالة العرض، واستخدمنا هذا المتغير في السطرين 3 و 4 من الشيفرة البرمجية السابقة للعثور على الفئة والمنشورات الصحيحة. عرض الوسم tag ضع أيضًا المحتويات التالية في الملف blog/views.py: def tag(request, slug): site = Site.objects.first() posts = Post.objects.filter(tag__slug=slug).filter(is_published=True) requested_tag = Tag.objects.get(slug=slug) categories = Category.objects.all() tags = Tag.objects.all() return render(request, 'tag.html', { 'site': site, 'posts': posts, 'tag': requested_tag, 'categories': categories, 'tags': tags, }) تذكر تجهيز موجّه إرسال عنوان URL المقابل لهذه الصفحة في الملف djangoBlog/urls.py كما يلي: path('tag/<slug:slug>', views.tag, name='tag'), عرض المنشور post ضع أيضًا المحتويات التالية في الملف blog/views.py: def post(request, slug): site = Site.objects.first() requested_post = Post.objects.get(slug=slug) categories = Category.objects.all() tags = Tag.objects.all() return render(request, 'post.html', { 'site': site, 'post': requested_post, 'categories': categories, 'tags': tags, }) وتذكر تجهيز موجّه إرسال عنوان URL المقابل لهذه الصفحة في الملف djangoBlog/urls.py كما يلي: path('post/<slug:slug>', views.post, name='post'), إنشاء طبقة القوالب Template يمكنك استخدام قالب جاهز، فنحن في هذه السلسة لا نركز على لغتي HTML و CSS. وستكون بنية القالب الذي سنستخدمه كما يلي: templates ├── category.html ├── home.html ├── layout.html ├── post.html ├── search.html ├── tag.html └── vendor ├── list.html └── sidebar.html يحتوي الملف layout.html على ترويسة وتذييل الصفحة، وهو المكان الذي نستورد فيه عادة أكواد التنسيقات CSS وأكواد جافا سكريبت JavaScript. وتوجهنا دوال العرض إلى القوالب home و category و tag و post، وتتوسّع جميعها إلى القالب layout الذي يعتبر كأساس لها جميعًا. وتتواجد المكونات التي ستظهر عدة مرات في قوالب مختلفة ضمن المجلد vendor، ويمكنك استيرادها باستخدام الوسم include. قالب التخطيط Layout ضع المحتويات التالية في الملف layout.html: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> {% load static %} <link rel="stylesheet" href="{% static 'style.css' %}" /> {% block title %}{% endblock %} </head> <body class="container mx-auto font-serif"> <nav class="flex flex-row justify-between h-16 items-center border-b-2"> <div class="px-5 text-2xl"> <a href="/"> My Blog </a> </div> <div class="hidden lg:flex content-between space-x-10 px-10 text-lg"> <a href="https://github.com/ericnanhu" class="hover:underline hover:underline-offset-1" >GitHub</a > <a href="#" class="hover:underline hover:underline-offset-1">Link</a> <a href="#" class="hover:underline hover:underline-offset-1">Link</a> </div> </nav> {% block content %}{% endblock %} <footer class="bg-gray-700 text-white"> <div class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10" > <p class="font-serif text-center mb-3 sm:mb-0"> Copyright © <a href="https://www.ericsdevblog.com/" class="hover:underline" >Eric Hu</a > </p> <div class="flex justify-center space-x-4"> . . . </div> </div> </footer> </body> </html> لاحظ في السطرين 7 و 8 الطريقة التي يمكنك بها استيراد الملفات الساكنة ملفات CSS و جافا سكريبت في جانغو، فكما وضحنا لن نتحدث في هذا المقال عن لغة CSS، ولكن ما يهمنا هو معرفة طريقة استيراد ملفات CSS إضافية لتطبيق جانغو. سيبحث جانغو عن الملفات الساكنة Static Files في مجلدات التطبيق الفردية افتراضيًا، حيث سيذهب إلى المجلد ‎/blog‎ في تطبيقنا blog، ويبحث عن المجلد static، ثم يبحث عن الملف style.css ضمن المجلد static كما هو محدد في القالب. blog ├── admin.py ├── apps.py ├── __init__.py ├── migrations ├── models.py ├── static │ ├── input.css │ └── style.css ├── tests.py └── views.py قالب الصفحة الرئيسية Home ضع المحتويات التالية في الملف home.html: {% extends 'layout.html' %} {% block title %} <title>Page Title</title> {% endblock %} {% block content %} <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1"> <!-- المنشور البارز --> <div class="mb-4 ring-1 ring-slate-200 rounded-md hover:shadow-md"> <a href="{% url 'post' featured_post.slug %}" ><img class="float-left mr-4 rounded-l-md object-cover h-full w-1/3" src="{{ featured_post.featured_image.url }}" alt="..." /></a> <div class="my-4 mr-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ featured_post.created_at|date:"F j, o" }} </div> <h2 class="text-lg font-bold">{{ featured_post.title }}</h2> <p class="text-base"> {{ featured_post.content|striptags|truncatewords:80 }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{% url 'post' featured_post.slug %}" >Read more →</a > </div> </div> {% include "vendor/list.html" %} </div> {% include "vendor/sidebar.html" %} </div> {% endblock %} لاحظ أننا فصلنا الشريط الجانبي وقائمة المنشورات ووضعناها في المجلد vendor بدلًا من كتابة شيفرتها البرمجية الثابتة، إذ سنستخدم المكونات نفسها في صفحة الفئة والوسم. قائمة المنشورات ضع أيضًا المحتويات التالية في الملف vendor/list.html: <!-- قائمة المنشورات --> <div class="grid grid-cols-3 gap-4"> {% for post in posts %} <!-- المنشور --> <div class="mb-4 ring-1 ring-slate-200 rounded-md h-fit hover:shadow-md"> <a href="{% url 'post' post.slug %}" ><img class="rounded-t-md object-cover h-60 w-full" src="{{ post.featured_image.url }}" alt="..." /></a> <div class="m-4 grid gap-2"> <div class="text-sm text-gray-500"> {{ post.created_at|date:"F j, o" }} </div> <h2 class="text-lg font-bold">{{ post.title }}</h2> <p class="text-base"> {{ post.content|striptags|truncatewords:30 }} </p> <a class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase text-sm font-semibold font-sans w-fit focus:ring" href="{% url 'post' post.slug %}" >Read more →</a > </div> </div> {% endfor %} </div> لاحظ في الأسطر من 3 إلى 27 كيف مرّرنا المتغير posts من العرض إلى القالب، حيث يحتوي هذا المتغير على مجموعة من المنشورات، وسنقوم بالمرور ضمن القالب على كل عنصر في هذه المجموعة باستخدام حلقة for. تذكّر في السطر 6 أننا أنشأنا موجّه إرسال عنوان URL كما يلي: path('post/<slug:slug>', views.post, name='post'), ستجد التعليمة ‎{% url 'post' post.slug %}‎ في القالب موجّهَ إرسال عنوان URL بالاسم 'posts'، وتسند القيمة post.slug إلى المتغير ‎<slug:slug>‎، والذي سيُمرَّر بعد ذلك إلى دالة العرض المقابلة. ينسّق المرشّح Filter الذي هو date في السطر 14 بيانات التاريخ المُمرَّرة إلى القالب لأن القيمة الافتراضية ليست سهلة الاستخدام، ويمكنك العثور على تنسيقات تواريخ أخرى في توثيق جانغو الرسمي. وضعنا مرشحَين بعد post.content في السطر 18، حيث يزيل المرشّح الأول وسوم HTML، ويأخذ المرشّح الثاني أول 30 كلمة ويقتطع الباقي. قالب الشريط الجانبي Sidebar ضع أيضًا المحتويات التالية في الملف vendor/sidebar.html: <div class="col-span-1"> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Search</div> <div class="p-4"> <form action="" method="get"> <input type="text" name="search" id="search" class="border rounded-md w-44 focus:ring p-2" placeholder="Search something..."> <button type="submit" class="bg-blue-500 hover:bg-blue-700 rounded-md p-2 text-white uppercase font-semibold font-sans w-fit focus:ring">Search</button> </form> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Categories</div> <div class="p-4"> <ul class="list-none list-inside"> {% for category in categories %} <li> <a href="{% url 'category' category.slug %}" class="text-blue-500 hover:underline" >{{ category.name }}</a > </li> {% endfor %} </ul> </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">Tags</div> <div class="p-4"> {% for tag in tags %} <span class="mr-2" ><a href="{% url 'tag' tag.slug %}" class="text-blue-500 hover:underline" >{{ tag.name }}</a ></span > {% endfor %} </div> </div> <div class="border rounded-md mb-4"> <div class="bg-slate-200 p-4">More Card</div> <div class="p-4"> <p> . . . </p> </div> </div> </div> قالب الفئة Category ضع المحتويات التالية في الملف category.html: {% extends 'layout.html' %} {% block title %} <title>Page Title</title> {% endblock %} {% block content %} <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1"> {% include "vendor/list.html" %} </div> {% include "vendor/sidebar.html" %} </div> {% endblock %} قالب الوسم Tag ضع المحتويات التالية في الملف tag.html: {% extends 'layout.html' %} {% block title %} <title>Page Title</title> {% endblock %} {% block content %} <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3 grid grid-cols-1"> {% include "vendor/list.html" %} </div> {% include "vendor/sidebar.html" %} </div> {% endblock %} قالب المنشور Post أخيرًا، يتضمن قالب المنشور المحتويات التالية: {% extends 'layout.html' %} {% block title %} <title>Page Title</title> {% endblock %} {% block content %} <div class="grid grid-cols-4 gap-4 py-10"> <div class="col-span-3"> <img class="rounded-md object-cover h-96 w-full" src="{{ post.featured_image.url }}" alt="..." /> <h2 class="mt-5 mb-2 text-center text-2xl font-bold">{{ post.title }}</h2> <p class="mb-5 text-center text-sm text-slate-500 italic">By {{ post.user|capfirst }} | {{ post.created_at }}</p> <div>{{ post.content|safe }}</div> <div class="my-5"> {% for tag in post.tag.all %} <a href="{% url 'tag' tag.slug %}" class="text-blue-500 hover:underline" mr-3">#{{ tag.name }}</a> {% endfor %} </div> </div> {% include "vendor/sidebar.html" %} </div> {% endblock %} لاحظ في السطر 19 أننا أضفنا المرشح safe، لأن جانغو سيعرض شيفرة HTML كنص عادي افتراضيًا لأسباب أمنية، لذا يجب ان نخبر جانغو بأنه يمكن عرض شيفرات HTML كما هي. أصبح كل شيءجاهزًا، كل ما عليك الأن هو تشغيل خادم التطوير باستخدام الأمر التالي، وعرض تطبيق جانغو الأول الخاص بك: python manage.py runserver ترجمة -وبتصرّف- للمقال Django for Beginners #4 - The Blog App لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: استخدام عمليات CRUD لإدارة مدونة في جانغو إنشاء تطبيق جانغو وتوصيله بقاعدة بيانات إنشاء موقع ويب هيكلي بجانغو رفع مستوى أمان تطبيقات جانغو في بيئة الإنتاج البدء مع إطار العمل جانغو لإنشاء تطبيق ويب
  24. قدمنا في المقالات السابقة من هذه السلسلة العديد من المفاهيم الجديدة في جانغو، وسنوضح في هذا المقال كيفية تفاعل الموجه URL Dispatcher والنماذج Models والعروض Views والقوالب Templates معًا في تطبيق مدونة في جانغو. لن ننشئ في هذا المقال مدونة كاملة الميزات مع الفئات Categories والوسوم Tags وما إلى ذلك، بل سنكتفي فقط بإنشاء صفحة تعرض مقالًا منشورًا، وصفحة رئيسية تعرض قائمةً بجميع المقالات، وصفحة لإنشاء وتعديل وحذف المنشورات. تصميم بنية قاعدة البيانات لنبدأ أولًا بطبقة النموذج Model التي سنصمم فيها بنية قاعدة البيانات، لذا انتقل إلى الملف blog/models.py وأنشئ نموذج Post جديد، وضع فيه الشيفرة البرمجية التالية: from django.db import models class Post(models.Model): title = models.CharField(max_length=100) content = models.TextField() يحتوي نموذج المنشور Post على حقلين فقط هما، العنوان title من النوع CharField بحد أقصى 100 محرف، والمحتوى content من النوع TextField. لتطبيق هذه التغييرات على قاعدة البيانات علينا توليد ملفات التهجير Migration المقابلة باستخدام الأمر التالي: python manage.py makemigrations ثم تطبيق عمليات التهجير باستخدام الأمر التالي: python manage.py migrate عمليات CRUD حان الوقت الآن لنتعمق في التطبيق نفسه، فمن غير المحتمل أن ننشئ جميع المتحكمات Controllers أولًا ثم نصمم القوالب ثم نتقل إلى الموجّهات Routers عند إنشاء تطبيقات واقعية، بل يجب أن نفكر من وجهة نظر المستخدم وفي الإجراءات التي قد يرغب في اتخاذها. يجب أن يمتلك المستخدم القدرة على إجراء أربع عمليات لكل مورد، والذي هو Post في حالتنا، وهذه العمليات هي: الإنشاء Create لإدراج بيانات جديدة في قاعدة البيانات القراءة Read لاسترداد البيانات من قاعدة البيانات التحديث Update لتعديل البيانات الموجودة مسبقًا في قاعدة البيانات الحذف Delete لإزالة البيانات من قاعدة البيانات ويشار إلى هذه العمليات مع بعضها البعض باسم عمليات CRUD. إجراء الإنشاء Create لنبدأ أولًا بعملية الإنشاء، فلا تزال قاعدة البيانات فارغة، لذا يجب على المستخدم إنشاء منشور جديد، ولكنك تحتاج إلى موجّه إرسال عناوين URL لتوجيه نمط عنوان URL الذي هو ‎/post/create/‎ إلى دالة العرض View Function وهي post_create()‎ لإكمال هذا الإجراء. تحتاج دالة العرض post_create()‎ إلى التمييز بين نوع الطلب الوارد إلى السيرفر لذا يجب أن تحتوي على عنصر تحكم في التدفق كتعليمة if لتميز فيما إذا كان تابع الطلب هو GET، عندها ستعيد دالة العرض قالبًا يحتوي على استمارة HTML، لتسمح للمستخدم بتمرير المعلومات إلى الواجهة الخلفية، أما إرسال الاستمارة Form فيجب أن يكون ضمن طلب POST. لذا إذا كان تابع الطلب هو POST، فيجب إنشاء مورد Post جديد وحفظه. إليك مراجعةً مختصرة لتوابع HTTP في حال احتياجك إلى تجديد بعض المعلومات: تابع GET هو تابع طلبات HTTP الأكثر استخدامًا، ويُستخدم لطلب البيانات والموارد من الخادم يُستخدم تابع POST لإرسال البيانات إلى الخادم، ويستعمل عادة لإنشاء أو تحديث المورد يعمل تابع HEAD مثل تابع GET تمامًا، باستثناء أن استجابة HTTP تحتوي على الترويسة فقط دون الجسم، ويستخدم المطورون هذا التابع لأغراض تنقيح الأخطاء Debugging تابع PUT مشابه لتابع POST، مع اختلاف واحد بسيط، فإذا أرسلت مورد باستخدام التابع POST وكان المورد موجودًا مسبقًا على الخادم، فلن يسبب هذا الإجراء أي فرق على الخادم، أما التابع PUT فسيكرر تحديث هذا المورد بالبيانات المرسلة في كل مرة تجري فيها الطلب. يزيل تابع DELETE موردًا من الخادم. لنبدأ بموجّه إرسال عناوين URL، لذا انتقل إلى الملف djangoBlog/urls.py وضع فيه ما يلي: from django.urls import path from blog import views urlpatterns = [ path("post/create/", views.post_create, name="create"), ] ستحتاج بعد ذلك إلى دالة العرض post_create()‎، لذا انتقل إلى الملف blog/views.py وضع فيه الشيفرة البرمجية التالية: from django.shortcuts import redirect, render from .models import Post def post_create(request): if request.method == "GET": return render(request, "post/create.html") elif request.method == "POST": post = Post(title=request.POST["title"], content=request.POST["content"]) post.save() return redirect("home") تفحص الدالة post_create()‎ أولًا تابع طلب HTTP، فإذا كان تابع GET، فيجب إعادة القالب create.html، وإذا كان POST فيجب استخدام المعلومات التي يمرّرها طلب POST لإنشاء نسخة POST جديدة، ثم إعادة التوجيه إلى الصفحة الرئيسية التي سننشئها في الخطوة التالية. سننشئ القالب create.html، ولكن يجب إنشاء القالب templates/layout.html أولًا كما يلي: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://cdn.tailwindcss.com"></script> {% block title %}{% endblock %} </head> <body class="container mx-auto font-serif"> <div class="bg-white text-black font-serif"> <div id="nav"> <nav class="flex flex-row justify-between h-16 items-center shadow-md"> <div class="px-5 text-2xl"> <a href="/"> My Blog </a> </div> <div class="hidden lg:flex content-between space-x-10 px-10 text-lg"> <a href="{% url 'create' %}" class="hover:underline hover:underline-offset-1">New Post</a> <a href="https://github.com/ericnanhu" class="hover:underline hover:underline-offset-1">GitHub</a> </div> </nav> </div> {% block content %}{% endblock %} <footer class="bg-gray-700 text-white"> <div class="flex justify-center items-center sm:justify-between flex-wrap lg:max-w-screen-2xl mx-auto px-4 sm:px-8 py-10"> <p class="font-serif text-center mb-3 sm:mb-0">Copyright © <a href="https://www.ericsdevblog.com/" class="hover:underline">Eric Hu</a></p> <div class="flex justify-center space-x-4"> . . . </div> </div> </footer> </div> </body> </html> لاحظ ‎{% url 'create' %}‎ في السطر 22، فهذه هي الطريقة التي يمكنك بها عكس عناوين URL اعتمادًا على أسمائها، حيث يتطابق الاسم create مع الاسم الذي أعطيته لموجّه الإرسال ‎post/create/‎. أضفنا أيضًا إطار عمل TailwindCSS عبر شبكة CDN في السطر 8 لجعل هذه الصفحة تبدو أفضل، ولكن يجب ألّا تفعل ذلك في بيئة الإنتاج. لننشئ بعد ذلك القالب templates/post/create.html، إذ يتوجب علينا إنشاء المجلد post له لتوضيح أن هذا القالب مخصص لإنشاء منشور: {% extends 'layout.html' %} {% block title %} <title>Create</title> {% endblock %} {% block content %} <div class="w-96 mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-4">Create new post</h2> <form action="{% url 'create' %}" method="POST"> {% csrf_token %} <label for="title">Title:</label><br> <input type="text" id="title" name="title" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300"><br> <br> <label for="content">Content:</label><br> <textarea type="text" id="content" name="content" rows="15" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300"></textarea><br> <br> <button type="submit" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center">Submit</button> </form> </div> {% endblock %} يحدّد السطر 10 الإجراء الذي ستتخذه هذه الاستمارة عند إرسالها، وتابع الطلب الذي ستستخدمه، ويضيف السطر 11 حماية من هجمات CSRF إلى الاستمارة لأغراض أمنية. لاحظ أيضًا الحقل <input> في السطرين 13 و 14، فالسمة Attribute التي هي name في هذا الحقل مهمة جدًا، حيث سيُربَط إدخال المستخدم بهذه السمة عند إرسال الاستمارة، ويمكنك بعد ذلك استرداد هذا الإدخال في دالة العرض كما يلي: title=request.POST["title"] وينطبق الأمر نفسه على العنصر <textarea> في السطرين 17 و 18، ويجب أن يكون الزر من النوع type="submit"‎ ليعمل بنجاح. إجراء القائمة List لننشئ الآن صفحة رئيسية لعرض قائمة بجميع المنشورات، حيث سنبدأ بعنوان URL لهذه الصفحة في الملف djangoBlog/urls.py كما يلي: path("", views.post_list, name="home"), ثم ننتقل إلى دالة العرض في الملف blog/views.py ونضع فيه ما يلي: def post_list(request): posts = Post.objects.all() return render(request, "post/list.html", {"posts": posts}) وسيتضمن القالب list.html المحتويات التالية: {% extends 'layout.html' %} {% block title %} <title>My Blog</title> {% endblock %} {% block content %} <div class="max-w-screen-lg mx-auto my-8"> {% for post in posts %} <h2 class="text-2xl font-semibold underline mb-2"><a href="{% url 'show' post.pk %}">{{ post.title }}</a></h2> <p class="mb-4">{{ post.content | truncatewords:50 }}</p> {% endfor %} </div> {% endblock %} تتكرر التعليمة ‎{% for post in posts %}‎ على جميع المنشورات posts، ويُسنَد كل عنصر إلى المتغير post، وتمرّر التعليمة ‎{% url 'show' post.pk %}‎ المفتاح الرئيسي Primary Key للمنشور post إلى موجّه إرسال عنوان URL للصفحة show التي سننشئها لاحقًا. تستخدم التعليمة ‎{{ post.content | truncatewords:50 }}‎ المرشّح truncatewords لاقتطاع المحتوى بحيث يحتوي على أول 50 كلمة. إجراء العرض Show يجب أن يعرض إجراء العرض محتوى منشور معين، مما يعني أن عنوان URL الخاص به يجب أن يحتوي على شيء فريد يسمح لجانغو بتحديد نسخة واحدة من Post فقط، ويكون هذا الشيء الفريد هو المفتاح الرئيسي Primary Key، لذا ضع ما يلي في الملف djangoBlog/urls.py: path("post/<int:id>", views.post_show, name="show"), سيُسنَد العدد الصحيح الذي يلي ‎post/‎ إلى المتغير id، ويُمرّر إلى دالة العرض في الملف blog/views.py كما يلي: def post_show(request, id): post = Post.objects.get(pk=id) return render(request, "post/show.html", {"post": post}) وسيتضمن القالب المقابل templates/post/show.html المحتويات التالية: {% extends 'layout.html' %} {% block title %} <title>{{ post.title }}</title> {% endblock %} {% block content %} <div class="max-w-screen-lg mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-2">{{ post.title }}</h2> <p class="mb-4">{{ post.content }}</p> <a href="{% url 'update' post.pk %}" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center">Update</a> </div> {% endblock %} إجراء التحديث Update ضع ما يلي في الملف djangoBlog/urls.py لتحديد موجّه إرسال عنوان URL لإجراء التحديث: path("post/update/<int:id>", views.post_update, name="update"), وتكون دالة العرض كما يلي في الملف blog/views.py: def post_update(request, id): if request.method == "GET": post = Post.objects.get(pk=id) return render(request, "post/update.html", {"post": post}) elif request.method == "POST": post = Post.objects.update_or_create( pk=id, defaults={ "title": request.POST["title"], "content": request.POST["content"], }, ) return redirect("home") ملاحظة: أضيف التابع update_or_create()‎ حديثًا إلى الإصدار 4.1 من جانغو. وسيتضمن القالب المقابل templates/post/update.html المحتويات التالية: {% extends 'layout.html' %} {% block title %} <title>Update</title> {% endblock %} {% block content %} <div class="w-96 mx-auto my-8"> <h2 class="text-2xl font-semibold underline mb-4">Update post</h2> <form action="{% url 'update' post.pk %}" method="POST"> {% csrf_token %} <label for="title">Title:</label><br> <input type="text" id="title" name="title" value="{{ post.title }}" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300"><br> <br> <label for="content">Content:</label><br> <textarea type="text" id="content" name="content" rows="15" class="p-2 w-full bg-gray-50 rounded border border-gray-300 focus:ring-3 focus:ring-blue-300">{{ post.content }}</textarea><br> <br> <div class="grid grid-cols-2 gap-x-2"> <button type="submit" class="font-sans text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center">Submit</button> <a href="{% url 'delete' post.pk %}" class="font-sans text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-md text-sm w-full px-5 py-2.5 text-center">Delete</a> </div> </form> </div> {% endblock %} إجراء الحذف Delete ضع ما يلي في الملف djangoBlog/urls.py لتحديد موجّه إرسال عنوان URL لإجراء الحذف: path("post/delete/<int:id>", views.post_delete, name="delete"), وتكون دالة العرض كما يلي في الملف blog/views.py: def post_delete(request, id): post = Post.objects.get(pk=id) post.delete() return redirect("home") لا يتطلب هذا الإجراء قالبًا، لأنه يعيد توجيهك إلى الصفحة الرئيسية بعد اكتمال الإجراء. بدء تشغيل الخادم لنبدأ الآن بتشغيل خادم التطوير ونرى النتيجة كما يلي: python manage.py runserver ستكون الصفحة الرئيسية للمدونة كما يلي: وتكون صفحة إنشاء منشور جديد كما يلي: وتكون صفحة عرض المنشور كما يلي: وتكون صفحة تحديث المنشور كما يلي: الخلاصة بهذا نكون قد أنهينا العمل على تطبيق مدونتنا البسيطة باستخدام Django وأنشأنا نموذج لتمثيل المنشورات في قاعدة البيانات، وتعلمنا كيف ننفيذ عمليات عبر توابع HTTP مثل GET و POST و PUT و DELETE. كما شرحنا خطوات تصميم واجهة المستخدم باستخدام القوالب لعرض المنشورات وإنشاء منشورات جديدة وتحديثها وحذفها. تابع المقال التالي من السلسلة للتعرف على خطوات إكمال المدونة. ترجمة -وبتصرّف- للمقال Django for Beginners #3 - The CRUD Operations لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: استخدام بنية MTV لإنشاء مدونة بسيطة في جانغو إعداد بيئة تطوير تطبيقات جانغو Django بناء تطبيق مهام باستخدام جانغو Django وريآكت React رفع مستوى أمان تطبيقات جانغو في بيئة الإنتاج
  25. نشرح في مقال اليوم بنية نموذج-قالب-عرض Model-Template-View أو MTV اختصارًا التي يعتمد عليها إطار عمل جانغو Django لتطوير الويب حيث يكون النموذج Model مسؤولًا عن التفاعل مع قاعدة البيانات، ويجب أن يقابل كل نموذج جدولًا منها، ويعبر القالب Template عن جزء الواجهة الأمامية من التطبيق، وهو الشيء الذي سيراه المستخدم، أما العرض View فهو يتضمن المنطق البرمجي لواجهة التطبيق الخلفية، ومسؤولً عن استرداد البيانات من قاعدة البيانات عبر النماذج ووضعها في العرض المقابل وإعادة القالب المعروض إلى المستخدم في النهاية. هذه المقالة جزء من سلسلة من المقالات تشرح جانغو للمبتدئين على النحو التالي: الجزء الأول: البدء في إنشاء مدونة بسيطة الجزء الثاني: استخدام بنية MTV لإنشاء مدونة بسيطة الجزء الثالث: استخدام عمليات CRUD لإدارة المدونة الجزء الرابع: تطبيق المدونة الكامل الجزء الخامس: إضافة بعض الميزات المتقدمة إلى تطبيق المدونة مفهوم النماذج Models النموذج من أفضل ميزات جانغو Django، ففي أطر عمل الويب الأخرى ستحتاج إلى إنشاء نموذج وملف تهجير Migration، وملف التهجير هو مخطط Schema لقاعدة البيانات، يصف بنية قاعدة البيانات كأسماء الأعمدة وأنواعها، ويوفر النموذج واجهةً تتعامل مع معالجة البيانات بناءً على هذا المخطط، ولكنك ستحتاج إلى نموذج فقط في جانغو، ويمكن توليد ملفات التهجير المقابلة باستخدام أمر بسيط هو python manage.py makemigrations، مما يوفر عليك كثيرًا من الوقت. يحتوي كل تطبيق جانغو على ملف models.py واحد، ويجب تعريف جميع النماذج المرتبطة بالتطبيق بداخله، ويقابل كل نموذج ملف تهجير، والذي يقابل بدوره جدولًا في قاعدة البيانات. يمكنك التمييز بين الجداول الخاصة بالتطبيقات المختلفة من خلال إسناد بادئة لكل جدول تلقائيًا، حيث سيكون لجدول قاعدة البيانات المقابل لتطبيق blog الخاص بنا البادئة blog_‎. يوضح المثال التالي نموذجًا في الملف blog/models.py: from django.db import models class Person(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) يمكنك بعد ذلك توليد ملف التهجير باستخدام الأمر التالي: python manage.py makemigrations ويجب أن يبدو ملف التهجير الناتج blog/migrations/0001_initial.py كما يلي: # Generated by Django 4.1.2 on 2022-10-19 23:13 from django.db import migrations, models class Migration(migrations.Migration): initial = True dependencies = [] operations = [ migrations.CreateModel( name="Person", fields=[ ( "id", models.BigAutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("first_name", models.CharField(max_length=30)), ("last_name", models.CharField(max_length=30)), ], ), ] يمكنك اعتبار ملف التهجير بمثابة مخطط يوضح كيف يجب أن يبدو جدول قاعدة البيانات، ويمكنك استخدام الأمر التالي لتطبيق هذا المخطط: python manage.py migrate يجب أن تبدو قاعدة بياناتك db.sqlite3 كما يلي: لا تحاول تعديل أو حذف ملفات التهجير إذا كنت مبتدئًا، ودع جانغو يطبّق كل شيء نيابةً عنك إلّا إن كنت تعرف ما تفعله تمامًا. يمكن لجانغو في معظم الحالات اكتشاف التغييرات التي أجريتها في النماذج حتى إذا حذفتَ شيئًا وتوليد ملفات الترحيل وفقًا لذلك. سينشئ النموذج في مثالنا جدول قاعدة بيانات person، وسيحتوي هذا الجدول على ثلاثة أعمدة اسمها id و first_name و last_name، حيث يُنشأ العمود id تلقائيًا كما هو موضح في ملف التهجير، ويُستخدم هذا العمود كمفتاح رئيسي Primary Key للفهرسة Indexing افتراضيًا. يسمَّىCharField()‎ بنوع الحقل ويعرِّف نوع العمود، ويسمَّى max_length بخيار الحقل ويحدّد معلومات إضافية حول هذا العمود. أهم نواع حقول النموذج وخياراتها يوضّح الجدول التالي بعض أنواع الحقول الأكثر استخدامًا وننصحك بالاطلاع على جميع أنواع الحقول وخياراتها من توثيق جانغو الرسمي. نوع الحقل وصف عنه BigAutoField ينشئ عمودًا نوعه عدد صحيح يتزايد تلقائيًا، يُستخدم عادةً مع العمود id. BooleanField ينشئ عمودًا قيمه منطقية True أو False. DateField و DateTimeField يُستخدم لإضافة تواريخ وأوقات كما يوحي اسمه. FileField و ImageField ينشئ عمودًا لتخزين المسار الذي يؤشّر إلى الملف أو الصورة المرفوعة. IntegerField و BigIntegerField تتراوح قيم الأعداد الصحيحة Integer من ‎-2147483648 إلى 2147483647، وتتراوح قيم الأعداد الصحيحة الكبيرة Big Integer من ‎-9223372036854775808 إلى 9223372036854775807 SlugField الاسم المختصر Slug هو نسخة بسيطة من عنوان URL للاسم أو العنوان. CharField و TextField ينشئ كل من CharField و TextField عمودًا لتخزين السلاسل النصية Strings، ولكن يقابل TextField مربع نص أكبر في صفحة مدير جانغو Django Admin التي سنتحدث عنها لاحقًا في هذه السلسلة من المقالات. يوضح الجدول التالي بعض خيارات الحقول الأكثر استخدامًا: خيار الحقل وصف عنه blank يسمح للحقل بأن يحتوي على إدخال فارغ. choices يمنح الحقل خيارات متعددة، حيث سنوضّح ذلك لاحقًا عندما نصل إلى مدير جانغو. default يعطي الحقل قيمةً افتراضية. unique يتأكد من أن كلّ عنصر في العمود فريد، ويُستخدَم عادةً لتحديد الاسم المختصر Slug والحقول الأخرى التي يُفترَض أن تحتوي على قيم فريدة. خيارات الصنف Meta يمكنك أيضًا إضافة الصنف Class الذي هو Meta في صنف النموذج، والذي يحتوي على معلومات إضافية حول هذا النموذج مثل اسم جدول قاعدة البيانات وخيارات الترتيب والأسماء المفردة وجمعها التي يمكن أن يقرأها الإنسان كما يلي: class Category(models.Model): priority = models.IntegerField() class Meta: ordering = ["priority"] verbose_name_plural = "categories" توابع النموذج توابع النموذج هي دوال معرَّفة في صنف النموذج، تسمح بتطبيق إجراءات مخصصة على النسخة الحالية من كائن النموذج كما في المثال التالي: class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) birth_date = models.DateField() def baby_boomer_status(self): "Returns the person's baby-boomer status." import datetime if self.birth_date < datetime.date(1945, 8, 1): return "Pre-boomer" elif self.birth_date < datetime.date(1965, 1, 1): return "Baby boomer" else: return "Post-boomer" في الكود السابق، يفحص جانغو تاريخ ميلاد الشخص ويعيد حالة تمثل إن كانت فترة ولادته قبل زيادة المواليد التي حدثت بعد الحرب العالمية الثانية Pre-boomer، أم خلالها Baby-Boomer أم بعدها Post-boomer للشخص عند استدعاء التابع baby_boomer_status()‎. ملاحظة: الكائنات والتوابع والخاصيات مفاهيم مهمة جدًا في لغات البرمجة، لذا ننصح بمطالعة مقال كائنات وأصناف بايثون لمزيد من المعلومات. وراثة النماذج ستحتاج إلى أكثر من نموذج واحد في معظم تطبيقات الويب، وسيكون لبعضها حقول مشتركة، لذا يمكنك إنشاء نموذج أب Parent Model يحتوي على الحقول المشتركة، ثم تجعل النماذج الأخرى ترث هذا النموذج الأب كما يلي: class CommonInfo(models.Model): name = models.CharField(max_length=100) age = models.PositiveIntegerField() class Meta: abstract = True class Student(CommonInfo): home_group = models.CharField(max_length=5) لاحظ أن النموذج CommonInfo نموذج مجرد Abstract، مما يعني أنه لا يقابل نموذجًا فرديًا فعليًا، بل يستخدم كأب لنماذج أخرى. لنولّد الآن ملف تهجير جديد blog/migrations/0002_student.py باستخدام الأمر التالي للتحقق من ذلك: python manage.py makemigrations سيتولّد ملف التهجير التالي: # Generated by Django 4.1.2 on 2022-10-19 23:28 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("blog", "0001_initial"), ] operations = [ migrations.CreateModel( name="Student", fields=[ ( "id", models.BigAutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("name", models.CharField(max_length=100)), ("age", models.PositiveIntegerField()), ("home_group", models.CharField(max_length=5)), ], options={ "abstract": False, }, ), ] لاحظ إنشاء الجدول Student فقط. علاقات قاعدة البيانات تحدثنا عن كيفية إنشاء جداول فردية، ولكن لا تكون هذه الجداول مستقلة تمامًا في معظم التطبيقات، بل توجد علاقات بينها، فمثلًا قد يكون لديك فئة Category تنتمي إليها منشورات متعددة، ويكون كل منشور في المدونة تابع لمستخدم معين وما إلى ذلك، لذا توجد ثلاثة أنواع أساسية للتعبير عن العلاقات بين جداول قاعدة البيانات وهي: علاقة واحد إلى واحد One-to-one وعلاقة متعدد إلى واحد Many-to-one وعلاقة متعدد إلى متعدد Many-to-many وسنشرح تاليًا المزيد حول هذه العلاقات وطريقة وصفها ضمن نماذج جانغو. علاقة واحد إلى واحد علاقة واحد إلى واحد هي العلاقة الأسهل، فمثلًا يمكن أن يكون لكل شخص هاتف واحد، ويمكن أن يعود كل هاتف إلى شخص واحد، حيث يمكننا وصف هذه العلاقة في النماذج كما يلي: class Person(models.Model): name = models.CharField(max_length=100) class Phone(models.Model): person = models.OneToOneField('Person', on_delete=models.CASCADE) يعمل نوع الحقل OneToOneField مثل أي نوع حقل آخر، ولكنه يتطلب وسيطين على الأقل هما: الوسيط الأول هو اسم النموذج الآخر الذي يرتبط به هذا النموذج بعلاقة، والوسيط الثاني هو on_delete الذي يعرّف الإجراء الذي سيتخذه جانغو عند حذف البيانات، ويتعلق هذا المعامل بلغة SQL أكثر من جانغو، لذا لن نتحدث عنه بالتفصيل، ولكن إن كنت مهتمًا، فاطلع على بعض القيم المتاحة للمعامل on_delete. يمكنك الآن إنشاء عمليات تهجير وتطبيقها لهذه النماذج ومعرفة ما يحدث، وإذا واجهتك مشاكل أثناء تشغيل الأوامر التالية، فاحذف الملف db.sqlite3 وملفات التهجير للبدء من جديد: python manage.py makemigrations python manage.py migrate لاحظ أن نوع الحقل OneToOneField أنشأ العمود person_id في الجدول blog_phone، وسيخزّن هذا العمود معرّف id الشخص الذي يمتلك هذا الهاتف. علاقة متعدد إلى واحد يمكن أن تحتوي كل فئة على منشورات متعددة وينتمي كل منشور إلى فئة واحدة مثلًا، حيث يشار إلى هذه العلاقة باسم علاقة متعدد إلى واحد التي نعرّفها كما يلي: class Category(models.Model): name = models.CharField(max_length=100) class Post(models.Model): category = models.ForeignKey('Category', on_delete=models.CASCADE) ينشئ نوع الحقل ForeignKey العمود category_id في الجدول blog_post، والذي يخزن معرّف id الفئة التي ينتمي إليها هذا المنشور. علاقة متعدد إلى متعدد تكون علاقة متعدد إلى متعدد أكثر تعقيدًا بعض الشيء، فمثلًا يمكن أن يكون لكل مقال وسوم Tags متعددة، ويمكن أن يكون لكل وسم مقالات متعددة: class Tag(models.Model): name = models.CharField(max_length=100) class Post(models.Model): tags = models.ManyToManyField('Tag') ستنشئ الشيفرة البرمجية السابقة جدولًا جديدًا بالاسم post_tags بدلًا من إنشاء عمود جديد، وسيحتوي هذا الجدول الجديد على عمودين هما post_id و tag_id، مما يتيح لك تحديد جميع الوسوم المرتبطة بمنشور معين والعكس صحيح. لنفترض أن لدينا الجدول التالي مثلًا لتوضيح الأمور: post_id tag_id 1 1 2 1 3 2 1 2 2 3 يمتلك المنشور الذي له المعرّف id=1 وسمين tags لهما المعرّف id=1 والمعرّف id=2. إذا أردنا عكس الأمور للعثور على المنشورات باستخدام الوسم، فيمكننا أن نرى منشورين لهما المعرّف id=3 والمعرّف id=1 بالنسبة للوسم ذي المعرّف id=2. طبقة العرض View طبقة العرض مكون مهم في تطبيق جانغو ، فهي المكان الذي نكتب فيه المنطق البرمجي للواجهة الخلفية. يسترد العرض البيانات من قاعدة البيانات من خلال النموذج المقابل ويعالج هذه البيانات المستردة ويضعها في الموقع المقابل في القالب ويعرض هذا القالب ويعيده إلى المستخدم. لا تعمل دالة العرض View Function على استرداد البيانات فقط، إذ توجد أربع عمليات أساسية يمكن تطبيقها على البيانات في معظم تطبيقات الويب، وهي الإنشاء Create والقراءة Read والتحديث Update والحذف Delete، ويشار إلى هذه العمليات مجتمعة بالاسم CRUD، والتي سنوضّحها في مقال لاحق. تحدّثنا عن النماذج في القسم السابق، ولكننا ما زلنا لا نعرف كيفية استرداد البيانات أو تخزينها باستخدام النموذج، حيث يقدم جانغو واجهة برمجة تطبيقات API بسيطة لمساعدتنا في ذلك، تسمى QuerySet. لنفترض أن لدينا النموذج blog/models.py التالي: class Category(models.Model): name = models.CharField(max_length=100) class Tag(models.Model): name = models.CharField(max_length=200) class Post(models.Model): title = models.CharField(max_length=255) content = models.TextField() pub_date = models.DateField() category = models.ForeignKey(Category, on_delete=models.CASCADE) tags = models.ManyToManyField(Tag) يمكنك معالجة البيانات بمساعدة QuerySet من خلال هذا النموذج ضمن دوال العرض التي توجد في الملف blog/views.py. إنشاء البيانات وحفظها لنفترض أنك تريد إنشاء فئة جديدة كما يلي: # استيراد نموذج الفئة‫ Category from blog.models import Category # إنشاء نسخة جديدة من النموذج‫ Category category = Category(name="New Category") # حفظ الفئة التي أنشأناها في قاعدة البيانات category.save() يجب أن يكون ما سبق سهلًا إذا كنت على دراية بمفهوم البرمجة كائنية التوجه، حيث أنشأنا في المثال السابق نسخة جديدة من الكائن Category واستخدمنا التابع save()‎ الذي ينتمي إلى هذا الكائن لحفظ المعلومات في قاعدة البيانات. توجد علاقة متعدد إلى واحد بين الفئة والمنشور، وقد عرفناها باستخدام الحقل ForeignKey()‎، مع افتراض أن لدينا عدد كافي من السجلات في قاعدة البيانات. from blog.models import Category, Post # ‫Post.objects.get(pk=1)‎ هي الطريقة التي نسترجع بها المنشور الذي له المفتاح الرئيسي pk=1، # حيث يرمز‫ pk إلى المفتاح الرئيسي Primary Key الذي يمثّل المعرّف id عادةً إن لم نحدّد خلاف ذلك. post = Post.objects.get(pk=1) # استرداد الفئة التي اسمها‫ "New Category" new_category = Category.objects.get(name="New Category") # إسناد المتغير‫ new_category إلى حقل فئة المنشور وحفظه post.category = new_category post.save() توجد أيضًا علاقة متعدد إلى متعدد بين المنشورات والوسوم كما يلي: from blog.models import Tag, Post post1 = Post.objects.get(pk=1) # استرداد المنشور 1 tag1 = Tag.objects.get(pk=1) # استرداد الوسم 1 tag2 = Tag.objects.get(pk=2) # استرداد الوسم 2 tag3 = Tag.objects.get(pk=3) # استرداد الوسم 3 tag4 = Tag.objects.get(pk=4) # استرداد الوسم 4 tag5 = Tag.objects.get(pk=5) # استرداد الوسم 5 post.tags.add(tag1, tag2, tag3, tag4, tag5) # إضافة الوسوم من 1 إلى 5 إلى المنشور 1 استرداد البيانات استرداد الكائنات أكثر تعقيدًا قليلًا مما رأيناه سابقًا، لذا سنوضّح فيما يلي كيف سنجد سجلًا معينًا في قاعدة بياناتنا التي تحتوي على آلاف السجلات إن لم نكن نعرف المعرّف id، وسنوضّح كيفية الحصول على مجموعة من السجلات التي تناسب معايير معينة بدلًا من الحصول على سجل واحد مثلًا. توابع QuerySet تتيح توابع QuerySet استرداد البيانات بناءً على معايير معينة، ويمكن الوصول إليها باستخدام السمة Attribute التي هي objects، حيث يُستخدَم التابع get()‎ الذي رأيناه سابقًا لاسترداد سجل معين كما يلي: first_tag = Tag.objects.get(pk=1) new_category = Category.objects.get(name="New Category") ويمكن أيضًا استرداد جميع السجلات باستخدام التابع all()‎: Post.objects.all() يعيد التابع all()‎ مجموعة من السجلات والتي نسميها مجموعة الاستعلام QuerySet، ويمكنك تحسين هذه المجموعة من خلال سَلسَلة التابع filter()‎ أو التابع exclude()‎ مع التابع all()‎ كما يلي: Post.objects.all().filter(pub_date__year=2024) سيؤدي ذلك إلى إعادة جميع المنشورات المنشورة في عام 2024، ويُسمَّى pub_date__year بوسيط البحث في الحقول Field Lookup، وسنناقش هذا الموضوع بالتفصيل لاحقًا. يمكننا أيضًا استبعاد المنشورات المنشورة عام 2024 كما يلي: Post.objects.all().exclude(pub_date__year=2024) هناك أيضًا العديد من توابع QuerySet الأخرى بالإضافة إلى get()‎ و all()‎ و filter()‎ و exclude()‎، ولكن لن نتحدث عنها جميعًا، لذا اطّلع على القائمة الكاملة لجميع توابع QuerySet من توثيق جانغو الرسمي. وسطاء عمليات البحث في الحقول Field Lookups وسطاء عمليات البحث في الحقول هي وسطاء الكلمات المفتاحية للتوابع get()‎ و filter()‎ و exclude()‎، والتي تعمل بطريقة مشابهة لتعليمة WHERE في لغة SQL، وتأخذ الصيغة fieldname__lookuptype=value مع وجود شرطة سفلية مزدوجة. Post.objects.all().filter(pub_date__lte='2024-01-01') pub_date هو اسم الحقل و lte هو نوع البحث الذي يرمز إلى أقل من أو يساوي، وستُعاد جميع المنشورات التي يكون فيها تاريخ النشر pub_date أقل من أو يساوي ‎2024-01-01‎. يمكن أيضًا استخدام وسطاء عمليات البحث في الحقول للعثور على السجلات التي لها علاقة بالسجل الحالي كما في المثال التالي، وستُعاد جميع المنشورات التي تنتمي إلى الفئة التي اسمها "Django": Post.objects.filter(category__name='Django') يمكننا تطبيق ذلك عكسيًا مثل إعادة جميع الفئات التي تحتوي على منشورٍ واحد على الأقل الذي يحتوي عنوانه على الكلمة "Django" كما يلي: Category.objects.filter(post__title__contains='Django') يمكننا أيضًا المرور عبر علاقات متعددة كما يلي: Category.objects.filter(post__author__name='Admin') يعيد الاستعلام هنا جميع الفئات التي تحتوي على منشورات ينشرها المستخدم Admin، إذ يمكن وضع سلسلة من العلاقات بالعدد الذي نريده. ويمكنك مطالعة جميع وسطاء عمليات البحث في الحقول التي يمكنك استخدامها من توثيق جانغو الرسمي. حذف الكائنات نستخدم التابع delete()‎ لحذف سجلٍ ما، حيث ستحذف الشيفرة البرمجية التالية المنشور الذي له المفتاح الرئيسي pk=1: post = Post.objects.get(pk=1) post.delete() ويمكننا أيضًا استخدامه لحذف سجلات متعددة معًا كما يلي: Post.objects.filter(pub_date__year=2022).delete() سيؤدي ذلك إلى حذف جميع المنشورات في عام 2022، ولكن قد يتعلّق السجل الذي نحذفه بسجل آخر مثل محاولة حذف فئة تحتوي على منشورات متعددة كما يلي: category = Category.objects.get(pk=1) category.delete() يحاكي جانغو سلوك قيد SQL التالي ON DELETE CASCADE، مما يعني حذف جميع المنشورات التي تنتمي إلى هذه الفئة أيضًا، ولكن إذا أردتَ تغيير ذلك، فيمكنك تغيير خيار on_delete إلى شيء آخر، لذا اطّلع على جميع خيارات on_delete المتاحة في توثيق جانغو الرسمي. دالة العرض View Function وضّحنا ما يمكن فعله داخل دالة العرض، وسنوضّح كيف تبدو دالة العرض الكاملة كما في المثال التالي، حيث تُعرَّف جميع العروض ضمن الملف views.py: from django.shortcuts import render from blog.models import Post # أنشئ عروضك الخاصة هنا def my_view(request): posts = Post.objects.all() return render(request, 'blog/index.html', { 'posts': posts, }) هناك شيئان يجب الانتباه إليهما في هذا المثال، أولهما أن دالة العرض تأخذ المتغير request كدخل، وهذا المتغير هو كائن HttpRequest يُمرَّر تلقائيًا إلى العرض من موجّه إرسال Dispatcher عناوين URL. اطّلع على مقال مدخل إلى HTTP لمزيد من المعلومات حول التواصل بين عميل وخادم التطبيق. يحتوي request على الكثير من المعلومات حول طلب HTTP الحالي، فمثلًا يمكننا الوصول إلى تابع طلب HTTP وكتابة شيفرات برمجية مختلفة لتوابع مختلفة كما يلي: if request.method == 'GET': do_something() elif request.method == 'POST': do_something_else() ملاحظة: اطّلع على جميع المعلومات التي يمكنك الوصول إليها من كائن request في توثيق جانغو الرسمي. والشيء الآخر الذي يجب الانتباه إليه هو استيراد اختصار Shortcut اسمه render()‎، ثم استخدامه لتمرير المتغير posts إلى القالب blog/index.html. يُطلق عليه اختصار لأنه من المفترض تحميل القالب افتراضيًا باستخدام التابع loader()‎ وعرض هذا القالب مع البيانات المُسترَدة وإعادة كائن HttpResponse، ولكن بسّط جانغو هذه العملية باستخدام الاختصار render()‎. لن نتحدث عن الطريقة المعقدة لذلك، لأننا لن نستخدمها في هذا المقال ويمكنك مطالعة جميع دوال الاختصار Shortcut Functions في توثيق جانغو الرسمي. نظام قوالب جانغو طبقة القوالب Template هي جزء الواجهة الأمامية لتطبيق جانغو، لذا تكون ملفات القالب هي شيفرات مكتوبة بلغة HTML لأنها تمثّل ما تراه في المتصفح، ولكن قد تكون الأمور أكثر تعقيدًا قليلًا، فإن احتوى القالب على شيفرات HTML فقط، فسيكون موقع الويب ساكنًا بالكامل، ولا نريد ذلك، لذا يجب أن يخبر القالبُ دالةَ العرض بمكان وضع البيانات المسترَدة. عمليات الضبط Configurations يجب أولًا تغيير شيء ما في الملف settings.py، إذ يجب أن تخبر جانغو بمكان وضع ملفات القوالب، لذا لننشئ المجلد templates، حيث اخترنا وضعه ضمن المجلد الجذر للمشروع، ولكن يمكنك نقله لمكان آخر. . ├── blog ├── db.sqlite3 ├── djangoBlog ├── env ├── manage.py ├── mediafiles ├── staticfiles └── templates انتقل إلى الملف settings.py وابحث عن TEMPLATES. TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ 'templates', ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] لنعدّل الخيار DIRS الذي يؤشر إلى المجلد templates، ولنتحقق الآن من عمل هذا الإعداد، لذا ننشئ نمط عنوان URL جديد يؤشّر إلى العرض test()‎ في الملف djangoBlog/urls.py كما يلي: from django.urls import path from blog import views urlpatterns = [ path('test/', views.test), ] ولننشئ الآن العرض test()‎ في الملف blog/views.py كما يلي: def test(request): return render(request, 'test.html') انتقل إلى المجلد templates وأنشئ القالب test.html كما يلي: <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Test Page</title> </head> <body> <p>This is a test page.</p> </body> </html> شغّل خادم التطوير وانتقل إلى العنوان http://127.0.0.1:8000/‎، وستظهر الصفحة التالية: لغة قوالب جانغو لنناقش الآن محرّك قوالب جانغو بالتفصيل، وتذكّر أنه يمكننا إرسال البيانات من العرض إلى القالب كما يلي: def test(request): return render(request, 'test.html', { 'name': 'Jack' }) تُسنَد السلسلة النصية 'Jack' إلى المتغير name وتمرَّر إلى القالب، ويمكننا عرض المتغير name ضمن القالب باستخدام أقواس معقوصة مزدوجة {{ }} كما يلي: <p>Hello, {{ name }}</p> حدّث المتصفح، وسترى النتيجة التالية: ولكن لا تكون البيانات المُمرَّرة إلى القالب سلسلة نصية بسيطة في أغلب الحالات كما في المثال التالي: def test(request): post = Post.objects.get(pk=1) return render(request, 'test.html', { 'post': post }) المتغير post في المثال السابق هو قاموس Dictionary، حيث يمكنك الوصول إلى العناصر الموجودة في هذا القاموس في القالب كما يلي: {{ post.title }} {{ post.content }} {{ post.pub_date }} المرشحات Filters تحوّل المرشحات قيم المتغيرات، فمثلًا إذا كان لدينا المتغير django الذي قيمته 'the web framework for perfectionists with deadlines' ووضعنا المرشح title مع هذا المتغير كما يلي: {{ django|title }} فسيُحوَّل القالب إلى ما يلي: The Web Framework For Perfectionists With Deadlines اطّلع على جميع المرشحات المُضمَّنة في جانغو من توثيق جانغو الرسمي. الوسوم Tags تضيف الوسوم ميزات لغات البرمجة مثل التحكم في التدفق والحلقات إلى شيفرة HTML، مما يوفر الكثير من الوقت والموارد، إذ لن نضطر إلى كتابة الشيفرة البرمجية نفسها مرارًا وتكرارًا. تُعرَّف جميع الوسوم باستخدام {% %} مثل حلقة for التالية: <ul> {% for athlete in athlete_list %} <li>{{ athlete.name }}</li> {% endfor %} </ul> وتكون تعليمة if كما يلي: {% if somevar == "x" %} This appears if variable somevar equals the string "x" {% endif %} وتكون تعليمة if-else كما في المثال التالي: {% if athlete_list %} Number of athletes: {{ athlete_list|length }} {% elif athlete_in_locker_room_list %} Athletes should be out of the locker room soon! {% else %} No athletes. {% endif %} اطّلع على جميع الوسوم المُضمَّنة في جانغو من توثيق جانغو الرسمي، حيث توجد الكثير من المرشحات والوسوم المفيدة الأخرى في نظام قوالب جانغو، والتي سنتحدث عنها لاحقًا. نظام الوراثة Inheritance الفائدة الأساسية لاستخدام قوالب جانغو هي أنك لست بحاجة إلى كتابة الشيفرة البرمجية نفسها مرارًا وتكرارًا، فمثلًا يوجد شريط تنقل Navigation Bar وتذييل Footer في جميع صفحات تطبيق الويب النموذجي، ومن الصعب تكرار هذه الشيفرة البرمجية في كل صفحة من صيانة هذا التطبيق، لذا يقدم جانغو طريقة سهلة للغاية لحل هذه المشكلة كما سنوضح فيما يلي. لننشئ الملف layout.html في المجلد templates، ويمثّل هذا الملف المكان الذي نعرّف فيه تخطيط القالب الخاص بنا كما يلي، ولكننا لم نضع الشيفرة البرمجية الخاصة بالتذييل وشريط التنقل لتسهيل القراءة: <!DOCTYPE html> <html> <head> {% block meta %} {% endblock %} ‏<-- ‫استورد شيفرة CSS هنا –!> </head> <body> <div class="container"> <!-- ضع شيفرة شريط التنقل هنا --> {% block content %} {% endblock %} <!-- ضع شيفرة تذييل الصفحة هنا --> </div> </body> </html> لاحظ أننا عرّفنا كتلتين في هذا الملف هما meta و content باستخدام الوسم ‎{% block ... %}‎، وعرّفنا القالب home.html التالي لاستخدام هذا التخطيط: {% extends 'layout.html' %} {% block meta %} <title>Page Title</title> <meta charset="UTF-8"> <meta name="description" content="Free Web tutorials"> <meta name="keywords" content="HTML, CSS, JavaScript"> <meta name="author" content="John Doe"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> {% endblock %} {% block content %} <p>This is the content section.</p> {% include 'vendor/sidebar.html' %} {% endblock %} سيجد جانغو أولًا الملف layout.html عند استدعاء القالب السابق، ويملأ الكتلتين meta و content بالمعلومات الموجودة في صفحة home.html. لاحظ وجود شيء آخر في هذا القالب، حيث تخبر التعليمة ‎{% include 'vendor/sidebar.html' %}‎ جانغو بالبحث عن القالب templates/vendor/sidebar.html ووضعه في الصفحة، حيث يتضمن القالب sidebar.html المحتويات التالية مثلًا: <p>This is the sidebar.</p> لا يُعَد ذلك شريطًا جانبيًا، ولكن يمكننا استخدامه لتوضيح عمل نظام الوراثة، وتأكد أيضًا من صحة العرض كما يلي: from django.shortcuts import render def home(request): return render(request, 'home.html') وتأكّد من أن موجّه إرسال عنوان URL الخاص بك يؤشّر إلى هذا العرض كما يلي: path('home/', views.home), افتح متصفحك وانتقل إلى العنوان http://127.0.0.1:8000/home، ويجب أن ترى الصفحة التالية: ترجمة -وبتصرّف- للمقال Django for Beginners #2 - The MTV Structure لصاحبه Eric Hu. اقرأ أيضًا المقال السابق: جانغو للمبتدئين الجزء الأول: البدء في إنشاء مدونة بسيطة إنشاء تطبيق حديث باستخدام Django و Vue | الجزء الأول: الأساسيات العروض والقوالب في Django - الجزء الأول العروض والقوالب في Django - الجزء الثاني مدخل إلى إطار عمل الويب جانغو Django
×
×
  • أضف...