عن الذكاء
الصناعي ورسم مجموعة ماندلبروت باستخدام العمليات الفرعية في سمول فيجوال بيزيك!
سألني أحد الأصدقاء أن أشرح له مجموعة
ماندلبروت Mandelbrot
Set، ولم أكن على علم بها فبحثت عنها، فاكتشفت أنها إحدى الأنماط
الجزئية (الكسيريات) Fractals وهي منمنمات رياضية متكررة تصنع أشكالا
مبهرة.. فمثلا، لو رسمت مجموعة ماندلبروت بدقة عالية جدا،
ثم كبرت كل جزء منها لتغوص في تفاصيلها أكثر وأكثر، فستجد كل نفس الشكل النمطي
يتكرر داخل نفسه إلى ما لا نهاية (شاهد الصورة المتحركة في صفحة ويكيبيديا(.
استهواني الموضوع، فطلبت من صديقي الذكاء الصناعي "كوبايلت"
أن يعطيني مثالا عليها بكود سمول بيزيك (لأنه لم يتعلم
سمول فيجوال بيزيك بعد) فلم يتأخر، ومن ثم
حولت أنا الكود إلى سمول فيجوال بيزيك ببساطة، لكن المشكلة أنه كان بطيئا جدا، لأنه يرسم نقطة بنقطة باستخدام
الدالة GW.SetPixel، ومساحة نافذة الرسم 800×600 أي
480 ألف نقطة.
لهذا كان أول تعديل أدخلته هو محاولة
التخلص من رسم ألوان الخلفية السوداء لأنها تمثل معظم مساحة الشكل.. لكن نظرا لأن
النقاط ملونة بدرجات مختلفة من الأسود، فقد استبعدت الألوان التي تميل إلى السواد،
ثم قررت أن أختبر كوبايلت، فسألته:
- كيف نجعل رسم الصورة أسرع؟
فاقترح تصغير مساحة الرسم، لكني
أخبرته أنني لا أريد تصغير المساحة.
فأعطاني كودا بديلا كل ما فعله فيه هو
العبث بحلقة التكرار دون أن يفعل شيئا فعليا!
وحينما أخبرته بهذا أراد أن يبيع
القضية، وتعلل بأن لغة سمول بيزيك لا تملك القدرات اللازمة للرسم بأفضل كفاءة وأن
عليّ استخدام لغة أخرى!
فقلت له:
- التحدي البرمجي هو أداء العمل المطلوب بالإمكانيات
المتاحة.. هناك طريقة لحل هذه المشكلة في سمول بيزيك بدون تقليل حجم الشاشة.. هل
تستطيع اكتشافها؟
فأجابني بطريقة شخص حافظ لا فاهم:
- بالطبع، يمكن تحسين أداء الرسم في Small Basic دون الحاجة إلى تقليل حجم الشاشة.
إليك بعض النصائح التي قد تساعدك.
وأخذ يسرد علي نصائح لا تعنيني ولا
تحل المشكلة!
فأدركت أن هذه آخر حدوده, وأنه لا
يستطيع اكتشاف الحل بدون توجيه، فسألته:
- هل تستطيع معرفة إن كان اللون الذي سترسمه أقرب إلى
السواد فتتجاهله؟
وهنا فوجئت به يقول:
- لتحديد ما إذا كان اللون الذي سترسمه قريبًا من السواد
وبالتالي تجاهله، يمكنك استخدام قيمة التكرار iteration التي تحصل عليها من الحلقة التكرارية. إذا كانت قيمة
iteration قريبة من الحد الأقصى maxIteration، فهذا يعني أن النقطة تقع داخل
مجموعة ماندلبروت ويجب أن تكون سوداء أو قريبة جدًا من السواد. في هذه الحالة،
يمكنك تجاهل رسم هذه النقطة لتسريع العملية. إليك كيف يمكن تعديل الكود لتحقيق ذلك.
وأعطاني الكود المعدل، وكان بالفعل
نفس الكود الذي استخدمته أنا لحل المشكلة!.. وأضاف:
- في هذا الكود، أضفت المتغير threshold الذي يحدد عتبة لتجاهل النقاط
التي تكون قيمة iteration
لها قريبة من maxIteration. بهذه الطريقة، سيتم رسم النقاط التي لها قيم ألوان
أكثر إشراقًا فقط، والتي تمثل النقاط خارج مجموعة ماندلبروت، وهو ما يسرع العملية
بشكل كبير.. أتمنى أن يكون هذا الحل مفيدًا لك!
مع ملاحظة أنه أخطأ في الشرح هنا،
فالكود كان يفعل العكس، فهو يلغي رسم النقاط الأقل من العتبة.. لكن ما علينا،
المهم أن الكود نفسه صحيح.. لهذا قلت له:
- نعم، هذا هو ما أتكلم عنه.
وستجدون المشروع في مجلد الأمثلة
المرفق بلغة سمول فيجوال بيزيك.
ورغم أن الرسوم
تبدو معقدة،
لكن المدهش أن الكود البرمجي نفسه قصير جدا وسهل جدا ولا
يحتوي إلا على عمليات الجمع والطرح والتربيع.. وربما يكون هذا بسبب استخدام
الأعداد المركبة Complex numbers في تمثيل الشكل (لا
يظهر هذا في البرمجة) حيث نعتبر الجزء الحقيقي هو الإحداثي السيني (الأفقي)،
والجزء التخيلي هو الإحداثي الصادي الرأسي.
لكن الأمر لم يتوقف هنا، فما زالت عملية الرسم بطيئة حتى
بعد تسريعها بحذف النقاط السوداء، وقد قررت مناقشة ذلك مع أعضاء مجموعة سمول
بيزيك، فخرجت من النقاش بفكرتين:
الأولى:
هي استخدام مكتبة المطور الصغير LitDev لرسم النقاط، فهي ترسم على صورة
ثم تعرضها على الشاشة، وهذا يجعلها أسرع بكثير، لكن لن يظهر شيء على الشاشة قبل
اكتمال الرسم، وهو أمر يمكن التعايش معه.. هذا حل جميل، وأجمل ما فيه أنه كشف لي
أن مطور مكتبة LitDev
وهي مصممة أساسة للغة سمول بيزيك قد أنشأ نسخة منها للعمل مع لغة سمول فيجوال
بيزيك، وقد استأذنته في أن أضيف هذه النسخة ضمن اللغة مباشرة بدلا من أن يتعب
المبرمج في البحث عنها، فسمح بهذا، وهي الآن مضمنة في مجلد المكتبات الخارجية
للغة، وجاهزة للعمل بمجرد إعداد سمول فيجوال بيزيك على جهازك، وسترى أسماء
الكائنات الخاصة بها في قائمة الإكمال التلقائي في محرر الكود، وكلها تبدأ
بالحرفين LD.
والثانية:
هي استخدام العمليات الفرعية Threads لتسريع رسم النقاط والاستفادة من
قدرات معالج الحاسب.. وفي نقاش مع مطور LitDev أشار إلى أنه يستخدم المنبه Timer في سمول بيزيك حينما يريد تنفيذ عمليات غير متزامنة،
لكن هذا الأمر محدود لأن كل المتغيرات في سمول بيزيك عامة ولا توجد متغيرات محلية Local Variables.. بينما سمول فيجوال بيزيك لا
تعاني من هذه المشكلة لأنها تسمح بالمتغيرات المحلية.. وقد أعجبتني الفكرة، لكن
حينما حاولت تجربتها اكتشفت أن المنبه الخاص بالنماذج يعطل تنفيذ البرنامج إلى أن
ينتهي الكود الذي ينفذه وهذا يمنعني من استخدامه كعملية فرعية، بينما المنبه العام
الخاص بسمول بيزيك يحل هذه المشكلة، لكن مشكلته أنه منبه واحد فقط ويجب استخدامه
لتشغيل كل العمليات الفرعية وهذا يعقد الكود!
وهنا سألت نفسي: ولماذا لا أسمح بإنشاء عمليات فرعية مباشرة في لغة سمول فيجوال
بيزيك؟
كل المطلوب هو إرسال الإجراء الفرعي subroutine الذي تريد تشغيله في عملية جديدة
إلى دالة في مكتبة تؤدي هذا الغرض.. لكن المشكلة، كيف أرسل الإجراء (وليكن اسمه task1)؟.. الطريقة الوحيدة المتاحة هي
إرساله اسمه كنص "task1"
واستخدام الانعكاس Reflection
لاستدعاء هذا الإجراء من اسمه، وهو أمر ليس معقدا جدا ويمكن فعله، لكن كتابة اسم
الإجراء كنص سيحتمل الخطأ، وسيحتاج لتعديلات في محرر الكود لأدعم الإكمال التلقائي
للاسم، وتعديلا في مترجم الكود لأعطي خطأ لو كان الاسم خاطئا، وأنا لا أريد أن
أقوم بكل هذا المجهود!
لهذا ظللت أفكر في حل أفضل، فهداني
الله سبحانه إلى فكرة بسيطة لكنها مدهشة، فالصيغة الوحيدة التي تسمح فيها سمول
بيزيك بكتابة اسم الإجراء (دون استدعائه) هي عند استخدامه كمعالج للحدث Event handler مثل:
Button1.OnClick =
Button_OnClick
إذن فكل ما أريده هو طريقة لأستخدم
هذه الصيغة لإنشاء عملية فرعية.. وهنا فكرت فيما يلي:
Thread.SubToRun = Task1
حيث SubToRun هو حدث Event سيستقبل الإجراء الفرعي (مثل Task1)، ولكني هنا لن أطلق الحدث أبدا، لكني سأستخدم الجزء
AddHandler الموجود في تعريف الحدث لإطلاق
عملية فرعية جديدة وجعلها تنفذ الإجراء الفرعي:
Public Shared Custom Event SubToRun
AddHandler(handler As SmallVisualBasicCallback)
Dim t As New Threading.Thread(Sub() handler())
t.Start()
End AddHandler
End Event
وبهذا استطعت بسطري كود فقط أن أمنح
سمول فيجوال بيزيك القدرة إلى إنشاء برامج قادرة على العمل في عمليات فرعية
متعددة!
لكن المشكلة التي تواجه العمليات
الفرعية بشكل عام في فيجوال بيزيك وسي شارب، هو أنها تستطيع فقط تشغيل إجراء بدون
معاملات، وهذا يعني أنك ستضطر لجعل العمليات الفرعية تأخذ بياناتها من متغيرات
عامة، وهذا أمر يجب تجنبه خاصة إذا كانت هذه المتغيرات العامة تتغير.. لحل هذه
المشكلة في فيجوال بيزيك وسي شارب نستطيع وضع الإجراء الفرعي في فئة ومعه مجموعة
متغيرات معرفة على مستوى الفئة، وعندما نرغب في إطلاق الإجراء الفرعي، نعرف نسخة
من الفئة ونمرر إلى متغيراتها البيانات المطلوبة، ثم نجعل العملية الفرعية تنفذ
الإجراء.. لكن هذا غير ممكن في سمول فيجوال بيزيك فهي لا تستطيع تعريف الفئات!
وبسبب هذا، حينما جربت رسم أجزاء
مجموعة ماندلبروت بعمليات فرعية، كانت بعض العمليات الفرعية تقرأ قيمة نقطة
البداية بشكل خاطئ، لأنها تتأخر في العمل قليلا بينما يكون المتغير العام الذي
يحمل القيمة قد تغير!
ولحل هذه المشكلة، أضفت سطرا من الكود
لتعطيل العملية الفرعية الرئيسية ل 10 ملي ثانية بعد إطلاق كل عملية فرعية،
لأعطيها الفرصة لبدء التشغيل وقراءة المتغير العام:
AddHandler(handler As SmallVisualBasicCallback)
Dim t As New Threading.Thread(Sub() handler())
t.Start()
Program.Delay(10)
End AddHandler
وقد نجحت هذه الطريقة في حل المشكلة،
مع الأخذ في الاعتبار أنك لو كنت تتعامل مع عدد كبير من المتغيرات العامة وتجري
عليها بعض الحسابات التي تحتاج لبعض الوقت، فقد تحتاج أن تستدعي الدالة Program.Delay بنفسك لتعطيل البرنامج الرئيسي
لفترة أطول تسمح لكل عملية فرعية ببدء العمل بشكل صحيح.. وأفكر عامة في أن أضيف
خاصية إلى المكتبة Thread
اسمها InitialDelay ستكون قيمتها الافتراضية 10،
وأعدل الكود إلى
Program.Delay(InitialDelay)
وبهذا أسمح لك بتحديد القيمة المناسبة
لانتظار انطلاق كل عملية فرعية والتأكد من أنها قرأت البيانات بشكل صحيح، وبهذا لا
تحتاج إلى تعطيل البرنامج بنفسك، كما يمكنك أن تلغي الانتظار نهائيا بجعله صفرا لو
لم تكن تحتاجه.
وبتقسيم الرسم على عدة عمليات فرعية،
وفرت حوالي 35% من الوقت!