لوحة المتصدرين
المحتوى الأكثر حصولًا على سمعة جيدة
المحتوى الأعلى تقييمًا في 11/21/20 في كل الموقع
-
هذا المقال جزء من سلسلة «مدخل إلى الذكاء الاصطناعي»: الذكاء الاصطناعي: أهم الإنجازات والاختراعات وكيف أثرت في حياتنا اليومية الذكاء الاصطناعي: مراحل البدء والتطور والأسس التي نشأ عليها المفاهيم الأساسية لتعلم الآلة تعلم الآلة: التحديات الرئيسية وكيفية التوسع في المجال يمكنك قراءة السلسلة على شكل كتاب إلكتروني بالانتقال إلى صفحة الكتاب، مدخل إلى الذكاء الاصطناعي وتعلم الآلة. إن سبق وحاولت قراءة أي مقالٍ على الإنترنت يتحدث عن تعلّم الآلة، فلا بدّ من أنك عثرت على نوعين من المقالات؛ فإما أن تكون سميكة وأكاديمية ومليئة بالنظريات (عن نفسي لم أستطع حتى تجاوز نصف مقال)، أو عن القصص الخيالية المريبة حول سيطرة الذكاء الصنعي على البشر، أو منهم من يتحدث عن البيانات وتأثيرها الساحر على مجالات العلوم الأخرى أو البعض الآخر من المقالات يتحدث عن كيفية تغيّر وظائف المستقبل. لم يكن هنالك أي كتاب يضعني على بداية الطريق لكي أتعلم أبسط الأساسيات لأنتقل بعدها إلى المفاهيم المعقدة واثق الخطى. لذا كان لا بدّ لي من كتابة مقالٍ بسيط ومفهوم وواقعي لطالما تمنيت وجوده. سيكون هذا المقال مقدمة بسيطة لأولئك الّذين أرادوا دومًا فهم طريقة عمل مجال تعلّم الآلة. بتطبيق أفكاره على أمثلة من مشاكل العالم الحقيقي، والحلول العملية المطبقة، بلغة بسيطة ومفهومة، وتجنب النظريات الصعبة قدر الإمكان. ليستطيع الجميع فهم ماهيّة هذا العِلم سواءً أكانوا مبرمجين أو مدراء بل وحتى لأي شخص كان. فهرس المحتويات لماذا نريد من الآلات أن تتعلم؟ المكونات الرئيسية لتعلم الآلة 1. البيانات (Data) 2. الميّزات (Features) 3. الخوارزميات (Algorithms) الفرق بين التعلم (Learning) والذكاء (Intelligence) تعلم الآلة التقليدي التعلم الموجه (Supervised Learning) التصنيف (Classification) أشجار القرار (Decision Trees) الانحدار (Regression) التعلم غير الموجه (Unsupervised learning) التجميع (Clustering) خوارزمية K-mean خوارزمية DBSCAN تقليل الأبعاد (Dimensionality Reduction) تعلم قواعد الربط (Association Rule Learning) التعلم المعزز (Reinforcement Learning) طريقة المجموعات 1. طريقة التكديس (Stacking) 2. طريقة التعبئة (Bagging) 3. طريقة التعزيز (Boosting) الشبكات العصبية (Neural Networks) والتعلم العميق (Deep Leaning) الشبكات العصبية (Neural Networks) الشبكات العصبية التلافيفية (Convolutional Neural Networks) الشبكات العصبية المتكررة (Recurrent Neural Networks) التعلم العميق (Deep Learning) الفرق بين الشبكات العصبية والتعلم العميق الخلاصة المراجع وإليك المخطط الرئيسي للمواضيع التي سنتناولها في هذه المقالة ملخصة بالصورة التالية: لماذا نريد من الآلات أن تتعلم؟ هذا صديقنا أحمد يريد شراء سيارة ويحاول حساب مقدار مبلغ المال الّذي سيحتاجُ لتوفيره شهريًا. واستعرض عشرات الإعلانات على الإنترنت وعلم بأن السيارات الجديدة يبلغ سعرها حوالي 20000 دولار، والسيارات المستعملة لعام واحد يبلغ سعرها 19000 دولار، والمستعملة لعامين يبلغ سعرها 18000 دولار وهكذا دواليك. يبدأ أحمد -محللنا الرائع- بملاحظة نمط معين لسعر السيارات؛ إذ يعتمد سعر السيارة على مدة استخدامها، وينخفض سعرها بمقدار 1000 دولار مقابل كلّ عام من عمرها، لكن سعرها لن ينخفض أقل من 10000 دولار. في هذه الحالة وطبقًا لمصطلحات تعلّم الآلة يكون أحمد قد ابتكر ما يُعرفُ بالانحدار (Regression): وهي طريقة لتوقع قيمة (أو سعر) معيّن على أساس بيانات قديمة معروفة. أغلب الناس تؤدي هذا الأمر طوال الوقت دون أن تشعر به، فمثلًا عند محاولتنا لتقدير السعر المعقول لجهاز أيفون مستعمل على موقع eBay، أو أثناء محاولتنا معرفة وزن اللحوم المناسب والكافي لبلوغ حدّ الشبع لكلّ شخص من المدعوين على عزومة الغداء. فعندها سنبدأ بتقدير الأمر ونسأل أنفسنا، هل 200 غرام كافي للشخص؟ أم 500 غرام أفضل؟ سواءً اعترفنا بذلك أم لا، أغلبنا يؤدي هذه المهمة لا شعوريًا. دورة الذكاء الاصطناعي احترف برمجة الذكاء الاصطناعي AI وتحليل البيانات وتعلم كافة المعلومات التي تحتاجها لبناء نماذج ذكاء اصطناعي متخصصة. اشترك الآن سيكون من الجميل أن يكون لدينا صيغة بسيطة مثل هذه لحلّ كلّ مشكلة في العالم. وخاصة بالنسبة لعزومة الغداء. ولكن لسوء الحظ هذا مستحيل. لنعود إلى مثال السيارات. المشكلة الحقيقية في هذا المثال هو وجود تواريخ تصنيع مختلفة، وعشرات الأنواع من السيارات، والحالة الفنية للسيارة، بالإضافة لارتفاع الطلب الموسمي على سيارة معينة، والكثير من العوامل المخفية الأخرى الّتي تؤثر بسعر السيارة. وبالتأكيد لن يستطيع صديقنا أحمد الاحتفاظ بكلّ هذه البيانات في رأسه أثناء حسابه للسعر. معظم الناس كسالى بطبعهم ولذلك سنحتاج حتمًا لآلات لتأدية العمليات الرياضية. لذا لنجاري الوضع الحاصل ولِنتجه باتجاه توفير آلة تؤدي هذه المهمة الحسابية الّتي واجهناها. ولنُوفر لها بعض البيانات اللازمة وسنطلبُ منها العثور على جميع الأنماط المخفية المتعلقة بالسعر. الجميل في الأمر أن هذه الآلة ستُؤدي هذه المهمة بطريقة أفضل بكثير مما سيُؤديه بعض الناس عند تحليلهم بعناية لجميع التبعيات المتعلقة بالسعر في أذهانهم. في الحقيقة كان هذا النوع من المشاكل المحفز الأساسي لولادة تعلّم الآلة. المكونات الرئيسية لتعلم الآلة لو أردنا اختصار جميع الأهداف الكامنة وراء مجال تعلّم الآلة فسيكون الهدف الوحيد هو توقع النتائج معينة بناءً على البيانات المدخلة (أي التعلّم من البيانات المدخلة). وهذا خلاصة الأمر. إذ يمكن تمثيل جميع مهام تعلّم الآلة بهذه الطريقة. كلّما زاد تنوع البيانات (تسمى في بعض الأحيان بالعينات) المجمعة لديك، كلّما كان مهمة العثور على الأنماط ذات الصلة والتنبؤ بالنتيجة أسهل نسبيًا. لذلك، فإن أي نظام يستخدم تعلّم الآلة سيحتاجُ لثلاثة مكونات رئيسية وهي: 1. البيانات (Data) هل تريد الكشف عن رسائل البريد الإلكتروني المزعجة؟ احصل على عينات من الرسائل هذه الرسائل المزعجة. هل تريد التنبؤ بالتغيرات الّتي تطرأ على أسعار الأسهم؟ ابحث عن سجلات أسعار الأسهم. هل تريد معرفة ما هي تفضيلات المستخدم؟ حللّ أنشطته على الفيسبوك، واعتقد بأن مارك زوكربيرج ماهرٌ جدًا في ذلك. كلما كانت البيانات أكثر تنوعًا، كانت النتيجة أفضل. في بعض الأحيان تكون عشرات الآلاف من سجلات البيانات هي الحد الأدنى لاستنتاج معلومة معينة. وفي البعض الآخر نحتاج إلى ملايين العينات. هناك طريقتين رئيسيتين للحصول على البيانات: الطريقة اليدوية. الطريقة الآلية. تتميز البيانات المجمّعة يدويًا باحتوائها على أخطاء أقل بكثير بالموازنة مع نظيرتها الآلية، ولكنها بالمقابل تستغرق وقتًا أطول في التجميع مما يجعلها أكثر تكلفة عمومًا. أما الطريقة الآلية فتكون أرخص إذ كلّ ما سنفعله هو جمع كلّ ما يمكننا العثور عليه على أمل أن تكون جودة هذه البيانات مقبولة. تستخدم بعض الشركات مثل غوغل عملائها لتصنيف البيانات لهم مجانًا. هل تعلم لماذا طريقة التحقق البشري ReCaptcha (المستخدمة في أغلب المواقع) تجبرك على "تحديد جميع لافتات الشوارع الموجودة في صورة معينة"؟ في الحقيقة إن هذه الطريقة ما هي إلا وسيلة لتصنيف البيانات وتعظيم الاستفاد منها. إذ يستغلون حاجتك للتسجيل في الموقع معين ويسخّرونك مجبرًا للعمل لديهم وبالمجان. مدعين بأنهم بهذه الطريقة يختبرونك بأنك بشري! نعم هذا بالضبط ما يفعلونه! تبًا لهم الأشرار! أراهن لو أنك بمكانهم فستُظهر رمز التحقق البشري أكثر منهم بكثير. أليس كذلك؟ بيد أن من الصعوبة بمكان الحصول على مجموعة جيدة من البيانات -والتي تسمى عادةً مجموعة بيانات (Dataset). وهذه المجموعات مُهمّة للغاية بل إن مجموعة البيانات ذات الجودة العالية هي في الواقع كنز حقيقي لصاحبها لدرجة أن الشركات يمكن أن تكشف أحيانًا عن خوارزمياتها، إلا أنها نادرًا ما تكشف مجموعات البيانات الخاصة بها. 2. الميزات (Features) تُعرف أيضًا باسم المعاملات (Parameters) أو المتغيّرات (Variables). والتي يمكن أن تعبر عن المسافة المقطوعة بالسيارات، أو جنس المستخدم، أو سعر السهم، أو تكرار كلمة معينة في النص. بعبارة أخرى، هذه هي الميزات الّتي يجب أن تنظرَ لها الآلة. عندما تكون البيانات مخزنة في الجداول، يكون الأمر بسيطًا - فالميّزات هي أسماء الأعمدة. ولكن ماذا لو كان لديك 100 غيفابايت من صور القطط؟ بكلّ تأكيد لا يمكننا اعتبار كلّ بكسل ميزّة. هذا هو السبب بكون اختيار الميّزات الصحيحة يستغرق عادة وقتًا أطول من أي خطوة أخرى في بناء نظام يعتمد على تعلّم الآلة. وهذا أيضًا هو المصدر الرئيسي للأخطاء. ولذلك دائمًا ما تكون الاختيارات البشرية غير موضوعية. إذ يختارون فقط الميّزات الّتي يحبونها أو تلك الّتي يجدونها "أكثر أهمية". ولذا من فضلك تجنب أن تكون بشريًا! 3. الخوارزميات (Algorithms) وهو الجزء الأسهل والأكثر وضوحًا. إذ يمكن حلّ أي مشكلة بطرق مختلفة. بيد أن الطريقة الّتي تختارها ستُؤثر على دقة النموذج النهائي وأدائه وحجمه. هناك فارق بسيط واحد مهم: إذا كانت البيانات سيئة فلن تساعدك حتى أفضل خوارزمية موجودة. في بعض الأحيان يشار إليها بمصطلح "الدخل السيئ سيؤدي إلى نتائج سيئة". لذلك لا تهتم كثيرًا لنسبة الدقة، وحاول الحصول على المزيد من البيانات كبداية. من الجدير بالذكر أن مصطلح نموذج (Model) يشير إلى ما خلاصة ما تعلمته من البيانات، ويكمننا في بعض الأحيان استخدام نموذج جاهز وتمرير البيانات له أو تحسين نموذج حالي. الفرق بين التعلم (Learning) والذكاء (Intelligence) إن سبق ورأيت مقالًا بعنوان "هل ستحلّ الشبكات العصبية محل تعلم الآلة؟" أو على شاكلته من العناوين الّتي تنشرها بعض المواقع التابعة لوسائل إعلامية على الإنترنت. دائمًا ما يسمي رجال الإعلام هؤلاء أي انحدار خطي (Linear Regression) على أنه ذكاء اصطناعي، بل إن بعض وسائل الإعلام تضخم الأمور لدرجة يصعب تصديقها حتى أصبحنا نخاف من الذكاء الصنعي كما خاف أبطال فيلم Terminator من الروبوت SkyNet. وإليك صورة توضح المفاهيم وتفض الالتباس الموجود: علوم الحاسب (Computer Science): عمومًا هو دراسة أجهزة الحاسب بما فيها من أسس نظرية و حسابية، كما تشتمل على دراسة الخوارزميات، وبُنى المعطيات وأساسيات تصميم الشبكات ونمذجة البيانات ..وغيرها. الذكاء الصناعي (Artificial Intelligence): وهو فرع من فروع علوم الحاسب يهدف إلى تعزيز قدرة الآلات والحواسيب على أداء مهام مُعينة تُحاكي وتُشابه تلك الّتي تقوم بها الكائنات الذكيّة؛ كالقدرة على التفكير، أو التعلُم من التجارب السابقة، أو غيرها من العمليات الأُخرى الّتي تتطلب عمليات ذهنية. تعلم الآلة (Machine Learning): وهو جزء مهم من الذكاء الاصطناعي، وهو أحد فروع الذكاء الاصطناعي الّذي يُعنى بجعل الحاسوب قادرًا على التعلُم من تلقاء نفسه من أيّ خبرات أو تجارب سابقة، مما يجعله قادرًا على التنبؤ واتخاذ القرار المُناسب بصورة أسرع، ولكن تعلم الآلة ليس الفرع الوحيد الّذي يؤدي هذه المهمة. الشبكات العصبية الاصطناعية (Artificial Neural Networks): وهي من أحد أشهر الطرق الشعبية في مجال تعلّم الآلة، ولكن هناك طرق أخرى جيدة أيضًا. التعلم العميق (Deep Learning): هو طريقة حديثة لبناء وتدريب واستخدام الشبكات العصبية. وهي بالأساس هيكلية جديدة للشبكات العصبية. وحاليًا لا أحد يفصل التعلم العميق عن "الشبكات العصبية العادية". حتى أننا نستخدم نفس المكتبات لهم. من الأفضل دومًا تسمية نوع الشبكة وتجنب استخدام الكلمات الرنانة. من المهم دائمًا تذكر بأنه لا توجد طريقة واحدة أبدًا لحل مشكلة معينة في مجال تعلّم الآلة. بل هناك دائمًا العديد من الخوارزميات الّتي يمكنها حل نفس المشكلة، وتبقى مهمة اختيار الطريقة أو الخوارزمية الأنسب عائدة إليك. إذ يمكنك حلّ أيّ شيء باستخدام شبكة عصبية اصطناعية، ولكن من الّذي سيدفع لك ثمن استئجار (أو حتى شراء) بطاقة معالجة الرسوميات (GPU) من نوع GeForces؟ لنبدأ بنظرة عامة أساسية على الاتجاهات الأربعة السائدة حاليًا في مجال تعلّم الآلة. تعلم الآلة التقليدي جاءت الطرق الأولى لتعلّم الآلة من مجال الإحصاء البحت في خمسينات القرن الماضي. إذ اعتمد العلماء على حلّ معظم المهام الرياضية الرسمية من خلال البحث عن الأنماط في الأرقام، وتقييم قرب نقاط البيانات، وحساب اتجاه المتجهات. يعمل حاليًا نصف الإنترنت على هذه الخوارزميات. فعندما ترى قائمة بالمقالات المرشحة لك قراءتها في أحد المواقع، أو عندما يحظر البنك الّذي تتعامل معه بطاقتك عند تمريرك إياها على الآلة في محطة وقود عشوائية في مكان مجهول بعيدة عن سكنك، فعلى الأرجح هذه الأفعال ناتجة عن خوارزميات تعلّم الآلة. تعد الشركات التكنولوجيا الكبرى من أكبر المعجبين بالشبكات العصبية، إذ أنها تستطيع الاستفادة منها بقدر أكبر من الشركات الناشئة. فمثلًا يمكن لدقة صغيرة ولتكن 2٪ لإحدى خوارزمياتها الحساسة أن تعود بالنفع على إيرادات الشركة بمبلغ مالي ضخم يبلغ 2 مليار دولار. ولكن عندما تكون شركتك صغيرة وناشئة، فهذه النسبة ليست ذات فائدة كبيرة. فإذا أمضى المهندسين في فريقك البرمجي سنة كاملة يعملون على تطوير خوارزمية توصية جديدة لموقع التجارة الإلكترونية الخاص بك، مع معرفتهم بأن 99٪ من الزيارات تأتي من محركات البحث. عندها ستكون فائدة الخوارزمية قليل جدًا إذ لم عديمة الفائدة تمامًا وخصيصًا أن معظم المستخدمين لم يفتحوا الصفحة الرئيسية. فهذا سيكون أكبر هدر لطاقة فريقك البرمجي وبذلك سيكون أسوء استثمار لهذه العقول خلال هذه السنة. بصرف النظر عن ما يقال عن هذه الطرق إلا أنها سهلة سهولة كبيرة. بل إنها مثل أساسيات الرياضيات وأغلبنا يستخدمها يوميًا بدون أن يفكر بها. ينقسم تعلّم الآلة الكلاسيكي إلى فئتين وهما التعلّم الموجّه (ويسمى أيضًا التعلّم الخاضع للإشراف) و التعلّم غير الموجّه (ويسمى أيضًا التعلّم غير الخاضع للإشراف). التعلم الموجه (Supervised Learning) تحتوي الآلة على "مشرف" أو "مُعلّم" يزود الآلة بجميع الإجابات الصحيحة والدقيقة، مثل تحديد فيما إذا كان الشكل في الصورة لقطة أو كلب. قسّم (أو صنّف) المعلم بهذا الطريقة فعليًا البيانات إلى قطط وكلاب، ويستخدم الجهاز هذه الأمثلة الصحيحة للتعلم منها. واحدًا تلو الآخر. أما التعلم غير الموجّه فأن سيترك الآلة بمفردها مع كومة كبيرة من صور الحيوانات ومهمتها ستكون تصنيف هذه الصور. وذلك لأن البيانات غير مصنفة، ولا يوجد معلّم يحدد لنا ما الشكل الموجود في هذه الصور، ولذلك ستحاول الآلة بمفردها العثور على أي أنماط في الصور لتحديد الفوارق ومعرفة ما الموجود في الصور. سنتحدث عن هذه الطرق في التعرف على الأنماط لاحقًا. من الواضح أن الآلة ستتعلم أسرع بكثير مع معلّم، لذلك فالتعلّم الموجّه مستخدمٌ بكثرة في المهام الواقعية. هناك نوعين رئيسيين لطريقة التعلم الموجّه وهما: التصنيف (Classification): التنبؤ بصنف كائن معين. الانحدار (Regression): التنبؤ بنقطة معينة على محور رقمي. التصنيف (Classification) تقسيم الكائنات أو العناصر بناءً على إحدى السمات المعروفة مسبقًا. افصل الجوارب بحسب اللون، والمستندات بحسب اللغة، والموسيقى بحسب الأسلوب. وعمومًا يستخدم التصنيف من أجل: تصفية البريد الإلكتروني من الرسائل المزعجة. كشف عن اللغة المستخدمة. البحث عن وثائق مماثلة. تحليل المشاعر. التعرف على الحروف والأرقام المكتوبة بخط اليد. الكشف عن الغش. ومن بعض الخوارزميات الشائعة المستخدمة للتصنيف: خوارزمية بايز أو المصنّف المعتمد على قانون بايز في الاحتمالات Naive Bayes. خوارزمية شجرة القرار Decision Tree. خوارزمية الانحدار اللوجستي Logistic Regression. خوارزمية الجار الأقرب K-Nearest Neighbours. خوارزمية الدعم الآلي للمتجه Support Vector Machine. ويوجد أيضًا العديد من الخوارزميات الأخرى. غالبًا ما يعتمد مجال تعلّم الآلة على تصنيف الأشياء. إذ تكون الآلة في هذه الحالة مثل طفل يتعلم كيفية فرز الألعاب: فها هي الدمية، وهذه هي السيارة، وهذه هي الشاحنة …إلخ ولكن مهلًا. هل هذا الأمر صحيح؟ هل سيعرف الطفل المعنى الحقيقي للدمية أو للسيارة؟ ليستطيع بعدها تمييز أي دمية مهما اختلف شكلها ولونها وطريقة صنعها؟ أي بعبارة أخرى، هل سيستطيع أن يعمم ما تعلمه؟ في التصنيف سنحتاج دائمًا لمعلّم. ويجب تصنيف البيانات بميّزات (Features) حتى تتمكن الآلة من تعيين الأصناف المناسبة بناءً على هذه الميّزات. في الحقيقة يمكننا تصنيف كلّ شيئ تقريبًا ابتداءً من تصنيف المستخدمين بناءً على اهتماماتهم (كما تفعل خوارزمية فيسبوك)، والمقالات المستندة إلى اللغة أو الموضوع (وهذا أمر مهم لمحركات البحث)، والموسيقى المبنية على الأسلوب سواءً أكانت موسيقى جاز أو هيب هوب …إلخ (هذا الأمر نشاهده في قوائم تشغيل الخاصة بتطبيق Spotify)، وحتى تصنيف رسائل البريد الإلكتروني الخاصة بك. في تصفية الرسائل المزعجة وغير المرغوب بها، تستخدم خوارزمية بايز Naive Bayes على نطاق واسع. إذ تحسبُ الآلة عدد الكلمات الجيدة في الرسالة وعدد الكلمات الاحتيالية أيضًا بناءً على تصنيف سابق للكلمات موجود في قاعدة بيانات أو مجموعة بيانات (Dataset)، ومن ثمّ تضرب الاحتمالات باستخدام معادلة بايز، وتجمعُ النتائج النهائية وبهذه البساطة أصبح لدينا جهاز يستفيد من طرق تعلّم الآلة من أجل أن يزيد ذكائه ومعرفته. بعدها بفترة وجيزة تعلم مرسلو البريد العشوائي كيفية التعامل مع هذه المرشحات -إن صح التعبير- والّتي تعتمد على خوارزمية بايز فعكفوا على إضافة الكثير من الكلمات المصنّفة على أنها "جيدة" في نهاية البريد الإلكتروني لتُتضاف هذه الكلمات إلى العملية الحسابية الخاصة بحساب احتمالات كون الرسالة مزعجة أم لا. ومن المفارقة أن هذه الثغرة سمّيت لاحقًا بتسمم بايز. دخلت خوارزمية بايز التاريخ باعتبارها من أوائل الخوارزميات الأنيقة والمفيدة عمليًا في ترشيح رسائل البريد الإلكتروني، ولكن في وقتنا الحالي لا تستخدم هذه الخوارزمية وإنما تستخدم خوارزميات أكثر قوة وذكاءً معتمدًا على الشبكات العصبية الاصطناعية لتصفية رسائل البريد العشوائي المزعج. إليك مثال عملي آخر على تطبيقات خوارزميات التصنيف. لنفترض أنك بحاجة لاقتراض بعض المال عن طريق بطاقتك الائتمانية. كيف سيعرف البنك إذا كنت تريد فعلًا أن تسدد هذا القرض أم لا؟ وبالتأكيد لا توجد طريقة مباشرة لمعرفة ذلك، مثل أن يسألك مثلًا. ولكن لدى البنك الكثير من الملفات الشخصية لأشخاص اقترضوا مالًا في الماضي. في الواقع لدى البنك جميع البيانات المهمة حول أعمار الأشخاص المقترضين ومستوى تعليمهم ومهنهم ورواتبهم -والأهم من ذلك- حقيقة أن هل هؤلاء المقترضين سددوا القرض أم لا. وهذه البيانات مهمة جدًا إذ يمكننا تمريرها للنظام الداخلي للبنك الّذي يعتمد على تعلّم الآلة للعثور على الأنماط المحددة الموجودة في الأشخاص الّذين يسددون القروض، وبذلك يمكننا الحصول على الإجابة المبنية على هذه البيانات السابقة. في الواقع لا توجد مشكلة حقيقية في الحصول على إجابة من هذه البيانات. وإنما تكمن المشكلة في أنه لا يستطيع البنك أن يثق في إجابة الآلة ثقةً عمياء. فماذا لو حدث فشل في النظام أو في جزء منه مثل تعطل أحد الاقراص الصلبة المخزن عليها قواعد البيانات المطلوبة، أو أن أحد قراصنة الإنترنت هجم على الخادم وتلاعب بالخوارزميات أو البيانات. فما الّذي سيحدث في هذه الحالة؟ للتعامل مع هذه الحالة لدينا خوارزمية شجرة القرار. والتي ستقسّم جميع البيانات تلقائيًا إلى أسئلة أجوبتها نعم أو لا. قد يبدو الأمر غريبًا بعض الشيئ من منظور بشري، فمثلًا ما المشكلة إذا كان الدائن يكسب أكثر من 128.12 دولارًا أمريكيًا؟ بالرغم من ذلك تضع الآلة مثل هذه الأسئلة لتقسيم البيانات بشكل أفضل في كلّ خطوة. وهكذا تُصنعُ شجرة القرار. كلما كان الفرع أعلى كلما كان السؤال أعم. يمكن لأي محلل أن يأخذ ناتج الخوارزمية ويعلّم تمامًا ما هو القرار المناسب. يمكن ألا يشعر بأن كلّ تفاصيلها منطقية إلا أنه يستطيع أن يبني عليها قراره. أشجار القرار (Decision Trees) تستخدم خوارزمية شجرة القرار على نطاق واسع في المجالات ذات المسؤولية العالية مثل: التشخيص والطب وفي الأمور المالية. من أكثر الخوارزمات شيوعًا لتشكيل الأشجار هما خوارزمية CART وخوارزمية C4.5. ونادرًا ما تستخدم طريقة بناء أشجار القرار الأساسية الصرفة في وقتنا الحالي. إلا أنها غالبًا ما تضع حجر الأساس للأنظمة الكبيرة، بل إن المُجمّعات (Ensembles) المعتمدة على أشجار القرار تعمل بطريقة أفضل من المجمعات المعتمدة على الشبكات العصبية الاصطناعية (سنتحدث لاحقًا في هذا المقال عن كلّ جزء منهم بالتفصيل). عندما تبحث عن شيء ما في غوغل، فما يحدث بالضبط هو أن مجموعة من الأشجار ستبحث عن إجابة أو مجموعة من الإجابات المناسبة لك. وهذه الأشجار سريعة جدًا، ولذلك تحبها محركات البحث. تعدّ خوارزمية الدعم الآلي للمتجه (Support Vector Machines) والتي يشار إليها اختصارًا (SVM) هي الطريقة الأكثر شيوعًا للتصنيف الكلاسيكي. والمستخدمة لتصنيف كلّ شيئ موجود تقربيًا مثل: النباتات حسب مظهرها في الصور، والوثائق بحسب الفئات …إلخ. الفكرة وراء خوارزمية الدعم الآلي للمتجه بسيطة جدًا إذ تحاول رسم خطين بين نقاط البيانات الخاصة بك مع أكبر هامش بينهما. هناك جانب مفيد جدًا من خوارزميات التصنيف وهو الكشف عن البيانات الشاذة. فعندما لا تتناسب الميزة مع أيّ من الفئات، فإننا نبرزها. وتستخدم هذه الطريقة حاليًا في الطب وتحديدًا في أجهزة التصوير بالرنين المغناطيسي، تبرز الحواسيب جميع المناطق المشبوهة أو انحرافات الاختبار. كما تستخدم أيضًا في أسواق الأسهم للكشف عن السلوك غير الطبيعي للتجار لمعرفة ما يحدث وراء الكواليس. الجميل في الأمر أنه عندما نعلّم الحاسب الأشياء الصحيحة فنكون علمناه تلقائيًا ما هي الأشياء الخاطئة. القاعدة الأساسية هي كلّما زاد تعقيد البيانات، زاد تعقيد الخوارزمية. بالنسبة للنصوص والأرقام والجداول سنختار النهج الكلاسيكي. إذ النماذج الناتجة ستكون أصغر، وتتعلم أسرع وتعمل بوضوح أكبر. أما بالنسبة للصور ومقاطع الفيديو وجميع أنواع البيانات المعقدة الأخرى، سنتجه بالتأكيد نحو الشبكات العصبية. منذ خمس سنوات فقط كان بإمكانك العثور على مصنف للوجه مبني على خوارزمية الدعم الآلي للمتجه (SVM). حاليًا أصبح من السهل الاختيار من بين مئات الخوارزميات المعتمدة على الشبكات العصبية المدربة مسبقًا. أما بالنسبة لمرشحات البريد العشوائي فلم يتغير شيئ. فلا تزال بعض الأنظمة مكتوبة بخوارزمية الدعم الآلي للمتجه (SVM). الانحدار (Regression) وهو طريقة لرسم خط بين مجموعة نقاط. نعم، هذا هو التعلّم الآلة! يستخدم الانحدار حاليًا في تطبيقات متعددة، مثل: توقعات أسعار الأسهم. تحليل حجم الطلب والمبيعات. التشخيص الطبي. أي ارتباطات عددية. ومن الخوارزميات الشائعة نذكر: خوارزمية الانحدار الخطي Linear. خوارزمية االانحدار متعدد الحواف Polynomial. الانحدار هو في الأساس آلية للتصنيف ولكن هنا نتوقع رقمًا بدلًا من فئة. ومن الأمثلة على ذلك توقع سعر السيارة من خلال المسافة المقطوعة، وتوقع حركة المرور بحسب وقت محدد من اليوم، وتوقع حجم الطلب من خلال نمو الشركة، وما إلى ذلك. ويكون من الواجب استخدام الانحدار عندما تعتمد مشكلة معينة على الوقت. كلّ من يعمل في مجالات التمويل والتحليل المالي يحب خوارزميات الانحدار. حتى أن معظمها مدمج في برنامج مايكروسوفت إكسل (Excel). وطريقة استخدامها سلسة جدًا من الداخل إذ تحاول الآلة ببساطة رسم خط يشير إلى متوسط الارتباط (Average Correlation). على عكس الشخص الّذي يحاول رسم شكل الانحدار يدويًا على السبورة، فإن الآلة ترسم الشكل بدقة رياضية عالية جدًا، بحساب متوسط الفاصل الزمني لكل نقطة. عندما يكون خط الانحدار مستقيمًا فيكون هذا الانحدار خطيًا، أما عندما يكون خط الانحدار منحنيًا فيكون الانحدار متعدد الحواف (Polynomial). وهذه الأنواع الرئيسية من الانحدار. والبعض الآخر أكثر غرابة مثل الانحدار اللوجستي (Logistic Regression) وسيكون شكله مميز كتميز الخروف الأسود في قطيع غنم. ولكن لا تدعه يخدعك، لأنه مجرد طريقة تصنيف وليس انحدارًا. لا بأس في الخلط بين الانحدار والتصنيف. إذ يتحول العديد من المصنّفات لتنفيذ عملية انحدار بقليل من الضبط والإعداد. وعمومًا تستخدم طرق الانحدار عندما لا يمكننا تحديد فئة الكائن، وإنما يمكننا تحديد ومعرفة مدى قربه من هذه الفئة، وهنا بالضبط تأتي مهمته. في حال أردت التعمق أكثر في التعلم الموجّه، فراجع هذه السلسلة: Machine Learning for Humans. التعلم غير الموجه (Unsupervised learning) ظهر التعلّم غير الموجّه بعد ظهور التعلّم الموجّه بقليل، وتحديدًا في التسعينيات. ويستخدم أقل من التعلّم الموجّه، ولكن في بعض الأحيان لن يكون لدينا خيار آخر سوى استخدامه. تعدّ البيانات المصنّفة نوع فاخر من البيانات. ولكن ماذا لو كنت رغبت في إنشاء تصنيف مخصص للحافلات (الباصات)؟ هل يجب عليك التقاط صور يدويًا لمليون حافلة في الشوارع وتصنيف كلّ واحدةٍ منها؟ مستحيل، سيستغرق هذا الأمر عمرًا بأكمله. في هذا النوع من التعلم من منظورنا نحن نعطيها أومر معينة للتجميع أو تقليل الأبعاد وهي تؤدي هذه المهمة من خلال تحليلها لقيم الميّزات (Features) ومحاولة الربط بينها ومعرفة العلاقات أو الارتباط بين هذه البيانات. كما يمكن أن تكون البيانات معقدة للغاية، ولذلك لا يمكن لهذه الخوارزميات التكهن بالنتيجة المطلوبة بصورة صحيحة. في تلك الحالات نحاول تنظيم بياناتنا أو إعادة هيكلتها في صيغة منطقية أكثر من السابق من أجل معرفة فيّما إذا استطاعت هذه الخوارزميات استنتاج شيئ ما، وبذلك الأمر له عدة جوانب للحلّ ومتعلّق بنوعية البيانات وطريقة تنظيمها وجودتها. ولكن هناك بعض الأمل إذ لدينا الملايين من المنصات الّتي توفر خدمات رخيصة نسبيًا تبدأ من 5 دولار أمريكي، وغالبًا نعتمد عليها لمساعدتنا في تصنيف البيانات وهذه الطريقة المعتمدة الّتي تجري وفقها أمور تطوير البيانات في هذا المجال. بعض الأنواع للتعلّم غير الموجّه: التجميع (Clustering). تقليل الأبعاد أو التعميم (Dimensionality Reduction). تعلم قواعد الربط (Association rule learning). التجميع (Clustering) تُقسّم عملية التجميع الكائنات على أساس ميّزات غير معروفة. إذ تختار الآلة أفضل طريقة لفرز الميّزات الّتي تراها مناسبة. هذه بعض التطبيقات لعملية التجميع في وقتنا الحالي: تقسيم السوق (أو تقسيم أنواع العملاء، أو طريقة ولائهم للعلامة التجارية). دمج نقاط قريبة على الخريطة. ضغط الصورة. تحليل وتسمية البيانات الجديدة. الكشف عن السلوك غير الطبيعي. ومن بعض خوارزميات الشائعة للتجميع: خوارزمية K-mean_clustering. خوارزمية Mean-Shift. خوارزمية DBSCAN. تعدّ عملية التجميع فعليًا عملية تصنيف ولكن المفارقة هنا أنها لا تحتوي على فئات محددة مسبقًا. مشابهة جدًا لعملية تقسيم الجوارب في الدرج بحسب ألوانهم، وذلك عندما لا تتذكر كلّ الألوان الّتي لديك فعندها ستجلب الجورب الأول ذو اللون الأسود وتضعه جانبًا وتأخذ الجورب الثاني ..وهكذا. تحاول خوارزميات التجميع العثور على كائنات متشابهة (بحسب بعض الميزات) ودمجها في مجموعة. تُجمّع الكائنات الّتي لديها الكثير من الميزات المماثلة في فئة واحدة. وتسمح لنا بعض الخوارزميات حتى تحديد العدد الدقيق للمجموعات الّتي نريدها. من أحد أشهر الأمثلة على التجميع هي تجميع العلامات (أو المؤشرات) على خرائط الوِب. فمثلًا عندما تبحث عن جميع المطاعم النباتية المحيطة بك، سيجمّعُ محرك البحث الخاص بالخريطة جميع المطاعم على شكل أرقام. ولو أنه لم يجمّعها لك فحتمًا سيتجمد متصفحك بعد عملية البحث لأنه سيحاول رسم جميع المطاعم النباتية الموجودة على سطح الكرة الأرضية وعددهم سيكون كبير بكلّ تأكيد. ومن بعض الاستخدامات الأخرى لخوارزميات التجميع تطبيقات الهواتف المحمولة مثل: Apple Photos وGoogle Photos إذ كِلاهما يستخدمان خوارزميات تجميع معقدة أكثر من المثال السابق، وذلك لإنهم يبحثان عن الوجوه المميزة في الصور بهدف إنشاء ألبومات خاصة لأصدقائك. لا يعرف التطبيق عدد أصدقاؤك ولا حتى كيف تبدوا أشكالهم، ولكنه مع ذلك يحاول العثور على ميزات صريحة في وجوههم، والموازنة بينها لمعرفة عددهم، وعرض الألبومات وفقًا لذلك. ومن بعض الاستخدامات الأخرى هي ضغط الصورة فعند حفظ الصورة بلاحقة PNG، يمكنك ضبط مجموعة الألوان وليكن عددها 32 لونًا. هذا يعني أن آلية التجميع ستأخذ جميع البكسلات "المحمرة" وتحسب قيمة "المتوسط الأحمر" وتضبطه على جميع البكسلات الحمراء. وبذلك يكون لدينا ألوان أقل، مما يؤدي في نهاية المطاف إلى حجم ملف أقل، وبذلك تنخفضُ مصاريف التخزين المحلي أو السحابي. ومع ذلك يمكن أن نواجه بعض مشاكل في الألوان مثل الألوان القريبة من لونين بنفس الوقت مثل لون الأزرق السمائي (Cyan). إذ لا يمكننا تصنيفه فيما إذا كان أخضرًا أم أزرق؟ هنا يأتي دور خوارزمية K-Means. خوارزمية K-mean إذ تعيّن خوارزمية K-Means مجموعة من النقاط اللونية والبالغ عددها 32 نقطة لونية بطريقة عشوائية في مجموعة الألوان. وتسمى هذه النقاط (أو الألوان) بالنقاط المركزية (Centroids). وتُحدد النقاط المتبقية على أنها مخصصة لأقرب نقطة (لون) مركزي. وبعدها سنُلاحظ أننا حصلنا نوعًا ما على ما يشبه المجرات حول هذه الألوان 32. ثم ننقل النقطة المركزية إلى وسط مجرتنا، ونكرر ذلك حتى تتوقف النقطة المركزية عن التحرك. نفذنا جميع المهام بنجاح. ولدينا 32 مجموعة محددة ومستقرة. وإليك شرحًا كرتونيًا وتفصيليًا لما جرى: البحث عن الألوان المركزية مريح. إلا أن التجميعات في الحياة الواقعية ليست دائمًا على شكل دوائر. لنفترض أنك عالم جيولوجيا. وتحتاج للعثور على بعض المعادن المتماثلة على الخريطة. في هذه الحالة، يمكن تشكيل تجميعات بطريقة غريبة وحتى متشعبة. ولا يمكنك أيضًا أن تعرف عددهم فهل هم 10؟ أم 100؟ بكل تأكيد أن خوارزمية K-means لن تتناسب مع هذه الحالة، وإنما ستكون خوارزمية DBSCAN مفيدةً أكثر. خوارزمية DBSCAN لنفترض أن النقاط لدينا هم أناس في ساحة البلدة. اَبحث عن أي ثلاثة أشخاص يقفون بالقرب من بعضهم البعض واطلب منهم أن يمسكوا أيديهم. ثم اطلب منهم البدء في الإمساك بأولئك الجيران الّذين يمكنهم الوصول إليهم. وهكذا دواليك. إلى أن نصل لشخص لا يستطيع الامساك بأي شخص آخر. هذه هي مجموعتنا الأولى. كرر هذه العملية ليُجمعُ كلّ الناس بمجموعات. ملاحظة: الشخص الّذي ليس لديه من يمسك يده - هو فعليًا مجرد بيانات شاذة. إليك رسم توضيحي يبين لك كيف سيبدو الحل: وإن كنت مهتم بخوارزميات التجميع؟ يمكنك الاطلاع على هذه المقالة The 5 Clustering Algorithms Data Scientists Need to Know. نلاحظ أن التجميع مشابه تمامًا للتصنيف، إذ يمكن استخدام المجموعات للكشف عن الحالات الشاذة. هل لاحظت بأن المستخدم يتصرف بطريقة غير طبيعية بعد اشتراكه بموقعكK أو بخدمتك؟ دع الآلة تحجبه مؤقتًا، وتنشئ تذكرة للدعم الفني لفحص هذا النشاط المريب لاتخاذ القرار المناسب. فربما يكون روبوت آلي يحاول إشغال الخادم الّذي تحجزه لموقعك. في الحقيقة لن نحتاج حتى لمعرفة ماهيّة "السلوك الطبيعي" للمُستخدم وإنما سنأخذ جميع أفعال ونشاطات المستخدم ونحملها إلى نموذجنا ونترك الآلة تقرر ما إذا كان هذا المستخدم "نموذجيًا" أم لا. يمكن أن لا يعمل هذا النهج بطريقة جيد بالموازنة مع التصنيف، ولكن القرار النهائي سيُبنى على المحاولة والتجربة. تقليل الأبعاد (Dimensionality Reduction) وتعرف أيضًا بالتعميم (Generalization) وهي عملية تجميع ميّزات محددة بداخل ميّزات ذات مستوى أعم وأعلى. ومن بعض التطبيقات العملية لهذه الطريقة نجد: أنظمة التوصية. التصورات (المحاكاة) الجميلة. نمذجة الموضوعات والبحث عن وثائق مماثلة. تحليل الصور المزيفة. إدارة المخاطر. ومن بعض الخوارزميات الشائعة لتطبيقها: خوارزمية تحليل المكونات الرئيسية Principal Component Analysis ويشار لها اختصارًا (PCA). خوارزمية تحليل القيمة المفردة Singular Value Decomposition ويشار لها اختصارًا (SVD). خوارزمية Latent Dirichlet allocation. خوارزمية التحليل الدلالي الكامن Latent Semantic Analysis ويشار إليها اختصارًا (LSA أو pLSA أو GLSA). خوارزمية t-SNE (التي تستخدم في مجال الرؤية الحاسوبية). استخدم علماء البيانات المتعصبون سابقًا هذه الأساليب، وكان عليهم العثور على "شيئ مثير للاهتمام" في أكوام ضخمة من الأرقام. وعندما لم تساعدهم مخططات إكسل بهذه المهمة أجبروا الآلات على العثور على الأنماط. حتى حصلوا على طريقة تقليل الأبعاد أو ميّزة تعلّم كيفية تقليل البعد. من الأفضل دائمًا استخدام التلخيص أو التجريد (Abstractions)، عوضًا عن مجموعة من الميزات المجزأة. فمثلًا، يمكننا دمج كلّ الكلاب ذات الآذان مثلثية الشكل والأنوف الطويلة والذيل الكبير ليصبح لدينا تلخيص لشكل كلب لطيف وهو كلب "شبيرد". نعم فقدنا بعض المعلومات حول الصفات المميزة الخاصة بالكلب شبيرد، إلا أن التلخيص الجديد يعدّ أكثر فائدة لتسمية الأغراض وتوضيحها. بالإضافة إلى ذلك، إن النماذج المُلخAصة تتعلّم بطريقة أسرع، ولا تظهر لديها مشكلة "فرض التخصيص" (Overfitting) -الّتي سنتحدث عنها بالتفصيل لاحقًا- بكثرة وهي تستخدم عددًا أقل من الميّزات. أصبحت هذه الخوارزميات أداة مذهلة "لنمذجة المواضيع". إذ يمكننا تلخيص مواضيع من كلمات محددة لمعانيها. هذا ما تفعله خوارزمية التحليل الدلالي الكامن. تعتمد على عدد مرات تكرار كلمة معينة في موضوع محدد. مثل: استخدام كلمة "تقنية" بكثرة في المقالات التقنية، وبالتأكيد سنعثر على أسماء الأشخاص السياسيين بكثرة في الأخبار السياسية وهكذا. كما يمكننا بكل تأكيد إنشاء مجموعات من جميع الكلمات في المقالات، ولكننا سنفقد جميع الروابط المهمة بين معاني الكلمات خصيصًا العلاقة بين الكلمات ذات المعنى نفسه مثل البطارية (Battery) والبطارية المقصود بها المدخرات الكهربائية - (Accumulator) الموجودة في مستندات مختلفة. إلا أن خوارزمية التحليل الدلالي الكامن ستتعامل معها بالطريقة الصحيحة، ولهذا السبب تحديدًا سمّيت "بخوارزمية التحليل الدلالي الكامن". لذلك نحن بحاجة إلى ربط الكلمات والمستندات في ميزة واحدة للحفاظ على هذه الاتصالات الكامنة واتضح لنا بأن خوارزمية التفكيك المفرد (Singular decomposition) تؤدي هذه المهمة بقوة، مما يشفُ عن فائدة المجموعات المجمعة بحسب الموضوع الّتي تحدثنا عنها سابقًا. من الاستخدامات الشائعة الأخرى هي أنظمة التوصية (Recommender Systems) والتصفية التعاونية (Collaborative Filtering) من أجل تقليل الأبعاد. مما يبدو أنه إذا كنت تستخدمه في تلخيص تقييمات المستخدمين، فستحصل على نظام رائع للتوصية بالأفلام والموسيقى والألعاب بل وحتى أي شيئ تريده. للمزيد من المعلومات حول هذا الموضوع نوصيك بالكتاب الرائع "برمجة الذكاء الجمعي" Programming Collective Intelligence. بالكاد سنتمكن من فهم فهمًا كاملًا لفكرة التلخيص (أو التجريد) الآلي، ولكن من الممكن رؤية بعض الارتباطات عن قرب. إذ يرتبط بعضها بعمر المستخدم فمثلًا يلعب الأطفال لعبة ماين كرافت (Minecraft) ويشاهدون معها الرسوم المتحركة بكثرة، ويرتبط بعض المستخدمين الآخرين بنوعية فيلم معينة أو بهوايات مخصصة وهكذا. تستطيع الآلات الحصول على هذه المفاهيم التجريدية عالية المستوى من دون حتى فهم ماهيتها، بناءً فقط على معرفة تقييمات المستخدم. تعلم قواعد الربط (Association Rule Learning) وهي طريقة للبحث عن الأنماط في تدفق الطلبات. حاليا تستخدم في عدد من المجالات مثل: التنبؤ بالمبيعات والخصومات. تحليل البضائع المشتراة معًا. معرفة كيفية وضع المنتجات على الرفوف. تحليل أنماط تصفح الإنترنت. الخوارزميات الشائعة لها هي: خوارزمية Apriori. خوارزمية Eclat. خوارزمية FP-growth. وتستخدم هذه الطريقة لتحليل عربات (سلّات) التسوق الإلكترونية أو الواقعية، كما تستخدم أيضًا لأتمتة استراتيجية التسويق، والمهام الأخرى المتعلقة بمثل هذه الأحداث. وتحديدًا عندما يكون لديك تسلسل لشيئ معين وترغب في إيجاد أنماط فيه - جرب هذه الأشياء. لنفترض أن العميل سيأخذ ستة عبوات من العصائر ويذهب إلى طاولة المحاسبة ثم إلى باب الخروج. هل يجب أن نضع الفول السوداني بجانب الطريق المؤدي إلى طاولة المحاسبة؟ وفي حال وضعناها، كم مرة سيشتريها الناس بالمجمل؟ لربما تتماشى العصائر مع الفول السوداني، ولكن ما هي التسلسلات الأخرى الّتي يمكننا التنبؤ بها اعتمادًا على البيانات؟ هل يمكن لتغييرات بسيطة في ترتيب البضائع أن تؤدي إلى زيادة كبيرة في الأرباح؟ وينطبق نفس الشيء على التجارة الإلكترونية. إذ المهمة هنا أكثر حماسية وإثارة للاهتمام، فما الّذي سيشتريه العميل في المرة القادمة؟ هل سيشتري المنتجات النباتية؟ أم الحيوانية؟ تعتمد الأساليب الكلاسيكية لتعلم الآلة على نظرة مباشرة على جميع السلع المشتراة باستخدام الأشجار أو المجموعات. يمكن للخوارزميات البحث عن الأنماط فقط، ولكن لا يمكنها تعميمها أو إعادة إنتاجها بما يتوافق مع الأمثلة الجديدة. أما في العالم الحقيقي فإن كلّ متجر تجزئة كبير يبني حلًا خاصًا ومناسبًا له، لذلك لا نرى تطورات كبيرة في هذا المجال. وعلى المستوى التقني فإن أعلى مستوى من التقنيات المستخدمة هي أنظمة التوصية (أو تسمى أحيانًا الأنظمة الناصحة). التعلم المعزز (Reinforcement Learning) وهو عملية رمي روبوت في متاهة وتركه بمفرده ليجد طريق الخروج بنفسه. من بعض التطبيقات العملية المستخدمة حاليًا: السيارات ذاتية القيادة. روبوت تنظيف الأرضية. الألعاب. أتمتة التداول. إدارة موارد المؤسسة. من أبرز الخوارزميات الشائعة لها: خوارزمية التعلم المعزز وفق النموذج الحر Q-Learning. خوارزمية خطة ماركوف للتعلّم المعزز لاتخاذ القرار SARSA. خوارزمية التعلم المعزز العميق وفق النموذج الحر DQN. خوارزمية الناقد المميز غير المتزامن A3C. الخوارزمية الجينية Genetic algorithm. أخيرًا وليس آخرًا، نصل إلى شيئ يشبه الذكاء الحقيقي. في كثير من المقالات نرى خطأ شائعًا بأن يصنف التعلّم المعزز تحت قسم التعلّم الموجّه أو أحيانًا في قسم التعلّم غير الموجّه. لذا وجب التنويه إلى كونه طريقة تعلّم منفصلة. يستخدم التعلّم المعزز في الحالات الّتي لا تتعلق فيها مشكلتك بالبيانات على الإطلاق، وإنما لديك بيئة افتراضية تتعامل معها. مثل عالم ألعاب الفيديو أو مدينة افتراضية للسيارات ذاتية القيادة. إن معرفة جميع قواعد الطرقات الجوية في العالم لن تعلّم الطيار الآلي كيفية القيادة على بأحد الطرق الجوية. بغض النظر عن مقدار البيانات الّتي نجمعها، لا يزال يتعذر علينا توقع جميع المواقف المحتملة. وهذا هو السبب الأساسي لهدف التعلم المعزز وهو ** تقليل الخطأ، وليس التنبؤ بجميع التحركات المحتملة**. إن البقاء على قيد الحياة في البيئة الافتراضية هي الفكرة الأساسية للتعلم المعزز. إذ سنعتمد على ترك الروبوت الصغير الفقير يتجول في الحياة الافتراضية ونُعاقبه على الأخطاء ونُكافؤه على الأفعال الصحيحة. بنفس الطريقة الّتي نعلم بها أطفالنا، أليس كذلك؟ الطريقة أكثر فعالية لتدريب الروبوت هي بناء مدينة افتراضية والسماح للسيارة ذاتية القيادة بتعلم كلّ طرق القيادة وحيلها فيها أولًا. في الحقيقة هذه هي الطريقة المعتمدة في تدريب الروبوت الموجودة في السيارات ذاتية القيادة. إذ ننشئ في البداية مدينة افتراضية استنادًا لخريطة المدينة الحقيقية، ونضيف إليها أناس افتراضيين يمشون في الشوارع (لمحاكاة الواقع) ونترك السيارة تتعلم بمفردها وذلك بوضع هدف نصب أعيننا وهو "تقليل العدد الّذي تقتله من الناس بأقل ما يمكن" وهكذا يستمر الروبوت في التدرب إلى أن يصل لمرحلة لا يقتل بها أحد. عندما يؤدي الروبوت أداءً جيدًا في لعبة GTA عندها سنُحرره ونختبره في الشوارع الحقيقية. قد يكون هناك نهجان مختلفان للتعلم المعزز وهما: نهج قائم على نموذج (Model-Based). نهج غير قائم على نموذج أو النهج الحر (Model-Free). إن النهج القائم على نموذج يعني أن السيارة بحاجة لحفظ كامل الخريطة أو أجزائها. هذا نهج قديم جدًا لأنه من المستحيل بالنسبة للسيارة الفقيرة ذاتية القيادة أن تحفظ الكوكب بأكمله. أما في النهج غير القائم على نموذج فلا تحفظ السيارة كلّ حركة ولكنها تحاول تعميم المواقف، ومحاولة التصرف بعقلانية إلى جانب محاولتها الحصول على أقصى مكافأة. هل تذكر الأخبار المتداولة حول خسار بطل العالم بلعبة Go أمام الذكاء الصنعي؟ هل تعلم بأن عدد التركيبات القانونية المحتملة للعبِ بهذه اللعبة أكبر من عدد الذرات الموجودة في الكون كلّه؟ حتى أن العلماء أثبتوا ذلك لاحقًا. ولكن هل سنطلب من هذا الروبوت المسكين حفظ كلّ ذلك؟ في الواقع أن الآلة لم تتذكر جميع التركيبات المحتملة للعب ومع ذلك فازت بلعبة Go، إذ حاولت تطبيق أفضل حركة في كلّ دور على حدة (تمامًا كما فعلت في لعبة الشطرنج عندما هزمت غاري كاسباروف في المباراة الشهيرة سنة 1997 والّتي سميت بمباراة القرن). هي فعليًا اختارت ببساطة أفضل حركة (من ناحية المكسب) لكلّ حالة، وقد فعلت ما يكفي للتغلب على البشر. يعد هذا النهج مفهومًا أساسيًا أدى لظهور التعلّم المعزز وفق النموذج الحرّ (Q-learning) وهو فرع من فروع التعلّم المعزز بل وظهور الخوارزميات مثل خوارزمية خطة ماركوف للتعلّم المعزز لاتخاذ القرار (SARSA) وخوارزمية التعلّم المعزز العميق وفق النموذج الحرّ (DQN). ومن الجدير بالذكر أن حرف "Q" يشير إلى "الجودة" (Quality) إذ يتعلم الروبوت أداء الفعل الأكثر "نوعية" في كلّ حالة ويحفظ جميع المواقف على أنها سلسلة ماركوفية بسيطة. يمكن للآلة اختبار مليارات المواقف والحالات في البيئة افتراضية، ويمكنها تذكر جميع الحلول الّتي أدت لمكافأة أكبر. ولكن كيف يمكنها أن تميز المواقف الّتي رأتها مسبقًا عن المواقف الجديدة كليًا؟ فمثلًا إذا كانت السيارة ذاتية القيادة في إحدى التقاطعات بين الشوارع وكانت إشارة المرور حمراء وتحولت فجأة الإشارة الخضراء فهل هذا يعني أنها يمكن أن تسير مباشرة؟ ماذا لو كانت هناك سيارة إسعاف تسير في شارع قريب وتطلب من السيارات الأخرى إفساح الطريق لها؟ الإجابة الحالية على هذا السؤال وفق المعطيات المتاحة إلى يومنا هذا هو "لا أحد يعرف ما الّذي ستفعله هذه السيارة ذاتية القيادة". فعليًا لا توجد إجابة سهلة. لطالما استمر الباحثون في المحاولة للعثور على إجابة، ولكن في الوقت نفسه لا يجدون سوى الحلول المؤقتة لبعض الحالات. إذ يعتمد البعض على محاكاة جميع المواقف يدويًا الّتي تنتج حلًا للحالات الاستثنائية، مثل: مشكلة العربة. والبعض الآخر يتعمق أكثر من ذلك ويترك للشبكات العصبية مهمة اكتشافها. وهذا قادنا لتطور التعلم المعزز وفق النموذج الحر (Q-learning) إلى شبكات التعلم المعزز العميق (Deep Q-Network). لكنها ليست بالحل المثالي أيضًا. طريقة المجموعات وهو مجموعة أشجار غبية تتعلّم تصحيح أخطاء بعضها البعض. من بعض تطبيقاتها العملية في وقتنا الحالي: جميع التطبيقات الّتي تعمل على الخوارزميات الكلاسيكية (الفارق هنا أنها تقدم أداء أفضل). أنظمة البحث. الرؤية الحاسوبية. الكشف عن الأغراض. من أبرز الخوارزميات الشائعة لها: خوارزمية الغابات العشوائية (Random Forest). خوارزمية التدرج المعزز (Gradient Boosting). حان الوقت للأساليب الحديثة والكبيرة. تعدّ المجمعات والشبكات العصبية مقاتلان رئيسيان يمهدان طريقنا نحو التفرد في عملية التعلّم. واليوم ينتجون أكثر النتائج دقة ويستخدمون على نطاق واسع في جميع الأحداث. على الرغم من فعاليتها العالية إلا أن الفكرة الكامنة وراءها بسيطة للغاية. إذ تعتمد على أخذ مجموعة من الخوارزميات ذات الفعالية العادية، وتجبرها على تصحيح أخطاء بعضها بعضًا، فستكون الجودة الإجمالية للنظام أفضل من أفضل خوارزميات تعمل بطريقة منفردة. ستحصل على نتائج أفضل إذا أخذت أكثر الخوارزميات تقلبًا في النتائج، والّتي تتوقع نتائج مختلفة تمامًا في حالة حدوث ضوضاء صغيرة على بيانات الدخل. مثل خوارزميات أشجار القرار وأشجار الانحدار. هذه الخوارزميات حساسة للغاية، حتى أنه يمكن لقيمة شاذة واحدة خارجية مطبقة على بيانات الدخل أن تجعل النماذج يجن جنونها. في الحقيقة هذا بالضبط ما نحتاجه. هنالك ثلاث طرق لبناء المجمعات: طريقة التكديس (Stacking). طريقة التعبئة (Bagging). طريقة التعزيز (Boosting). سنشرح كلّ واحدٍ منهم على حدة: 1. طريقة التكديس (Stacking) تُمرر مجموعة من النماذج المتوازية كمدخلات للنموذج الأخير والّذي سيتخذ القرار النهائي. تَنتجُ هذه النماذج من تطبيق خوارزميات مختلفة وكلمة "مختلفة" تعني أي أن خلط تكديس نفس الخوارزميات على نفس البيانات لن يكون له أي معنى أو أهمية. وإن عملية اختيار الخوارزميات أمر متروك لك مطلق الحرية في اختباره. إلا أنه بالنسبة للنموذج المعني باتخاذ القرار النهائي، عادة ما يكون الانحدار خيارًا جيدًا لخوارزميته. 2. طريقة التعبئة (Bagging) وهي معروفة أيضًا باسم Bootstrap Aggregating. في هذه الطريقة نستخدم نفس الخوارزمية، ولكن ندربها على مجموعات فرعية مختلفة من البيانات الأصلية. في النهاية نحسب متوسط الإجابات فقط. يمكن أن تتكرر البيانات في مجموعات فرعية عشوائية. فمثلًا، يمكننا الحصول على مجموعات فرعية من المجموعة "1-2-3" مثل: "2-2-3" و"1-2-2" و"3-1-2" وما إلى ذلك. نستخدم مجموعات البيانات الجديدة هذه لتعليم الخوارزمية نفسها عدة مرات، ثمّ نتوقع الإجابة النهائية عن طريق خوارزمية البسيطة التصويت بالأغلبية. أشهر مثال على استخدام طريقة التعبئة هي خوارزمية الغابات العشوائية (Random Forest) والتي ببساطة تعبأ باستخدام أشجار القرار (الّتي سبق وأن وتحدثنا عنها في الفقرات السابقة). فمثلًا عند فتحك لتطبيق الكاميرا الخاص بهاتفك ورؤيتك لمربعات مرسومة حول وجوه الأشخاص فيجب أن تسأل نفسك، كيف حدث ذلك؟ في الحقيقة من المحتمل أن تكون هذه النتيجة بفضل خوارزمية الغابات العشوائية. وذلك لأن الشبكات العصبية بطيئة جدًا عند تشغيلها في الزمن الحقيقي (Real-time)، وبالمقابل تكون طريقة التعبئة مثالية بهذه الحالات لأنه يمكنها أن تبني أشجار القرار على جميع البطاقات الرسومية الضعيفة والقوية بل وحتى على المعالجات الجديدة الفاخرة الخاصة بتعلّم الآلة! في بعض المهام، تكون الاستراتيجية المتبعة هي التركيز على قدرة الغابة العشوائية على العمل بالتوازي مثلما يحدث عند استخدام الشبكات العصبية الاصطناعية أما طريقة التجميع لا تستطيع العمل بالتوازي، والبعض الآخر من التطبيقات تتطلب السرعة الّتي تستطيع تحققها طرق المجموعات بغض النظر عن أسلوب تطبيقها وتحديدًا في المهام الّتي تتطلب معالجة بالزمن الحقيقي. ولكنها في النهاية مسألة مفاضلة بين خياري الدقة أو السرعة وذلك بحسب كلّ مهمة. 3. طريقة التعزيز (Boosting) وهي الطريقة الّتي تعتمد على تدريب الخوارزميات واحدةً تلو الآخرى. وكلّ خوارزمية لاحقة تولي معظم اهتمامها لنقاط البيانات الّتي أخطأت الخوارزمية السابقة في تفسيرها. وتكرر هذه العملية إلى أن تصبح النتيجة مرضية. كما هو الحال في طريقة التعبئة، تستخدم الخوارزمية مجموعات فرعية متنوعة من بياناتنا ولكن هذه المرة لن نُنشئها بطريقة عشوائية. وإنما في كلّ عينة فرعية، نأخذ جزءًا من البيانات الّتي فشلت الخوارزمية السابقة في معالجتها. وبذلك نُنشئ خوارزمية جديدة لتتعلم كيفية إصلاح الأخطاء الموجودة في الخوارزمية السابقة. الميزة الرئيسية في طرق التجميع هي الدقة الممتازة بالموازنة مع الوقت المأخوذ، وتعد أسرع بكثير من الشبكات العصبية. تقريبًا الأمر أشبه ما يمكن بسباق بين سيارة وشاحنة على المضمار. يمكن للشاحنة أن تؤدي المزيد من الأفعال، ولكن في حال أردت أن تسير بسرعة فحتمًا ستأخذ السيارة. لالقاء نظرة على مثال حقيقي لاستخدام طرق التجميع (وتحديدًا طريقة التعزيز) افتح موقع فيسبوك أو موقع غوغل واكتب أي استعلام في مربع البحث. هل يمكنك سماع جيوش من الأشجار تزأر وتتحطم معًا لفرز النتائج حسب الصلة؟ ذلك بسبب أن هذه الشركات يستخدمون طريقة التجميع باستخدام التعزيز. حاليًا هناك ثلاث أدوات شائعة لتطبيق طريقة التعزيز، يمكنك قراءة هذا التقرير المفصل الّذي يوازن بينها CatBoost مقابل LightGBM مقابل XGBoost. الشبكات العصبية (Neural Networks) والتعلم العميق (Deep Leaning) الشبكات العصبية (Neural Networks) الشبكات العصبية: وهي عبارة عن مجموعة من الخلايا العصبية الاصطناعية الموجودة في طبقاتٍ متوضعةٍ فوق بعضها بعضًا، ولها طبقة أولية، وطبقة النهائية، تتلقى الطبقة الأولية المعلومات الخام، وتعالجها لتُمررها لاحقًا للطبقة الّتي تليها وهكذا إلى أن نحصل على الخرج من الطبقة النهائية. بعض أشهر تطبيقاتها العملية المستخدمة في وقتنا الحالي: تحديد الكائن في الصور ومقاطع الفيديو. التعرف على الكلام والتراكيب اللغوية. معالجة الصور وتحويل التنسيق. الترجمة الآلية. بالإضافة إلى أنه يمكنها أن تعمل عوضًا عن جميع تطبيقات طرق تعلّم الآلة السابقة. من بعض الهيكليات الشائعة للشبكات العصبية: الشبكات العصبية بيرسيبترون (Perceptron). الشبكات العصبية التلافيفية (CNN). الشبكات العصبية المتكررة (RNN). الشبكات العصبية ذات الترميز التلقائي (Autoencoders). إن أي شبكة عصبية اصطناعية هي في الأساس مجموعة من الخلايا العصبية الاصطناعية (Neurons) و الاتصالات (Connections) الّتي بينهم. وإن الخلية العصبية الاصطناعية: هي مجرد تابع لديه مجموعة من المدخلات وخرج وحيد. وتتمثل مهمة الخلية العصبية الاصطناعية في أخذ جميع الأرقام من مدخلاتها، وأداء الوظيفة المنوطة إليها وإرسال النتيجة للخرج. الاتصالات تشبه إلى حدٍ ما القنوات بين الخلايا العصبية الحقيقية. إذ تربط مخرجات خلية عصبية معينة لمدخل خلية عصبية أخرى حتى يتمكنوا من إرسال الأرقام والنتائج لبعضهم بعضًا. وكلّ اتصال له وسيط واحد فقط وهو الوزن (Weight). وهو مشابه لقوة الاتصال للإشارة. فعندما يمر الرقم 10 من خلال اتصال بوزن 0.5 يتحول إلى 5. هذه الأوزان تطلب من الخلية العصبية الاصطناعية أن تستجيب أكثر للدخل ذو الوزن الأكبر، وأقل للدخل ذو الوزن الأقل. تُعدل هذه الأوزان عند التدريب وهكذا تتعلّم الشبكة العصبية الاصطناعية. فيما يلي مثال لخلايا عصبية اصطناعية بسيطة ولكنها مفيدة في الحياة الواقعية: ستجمع جميع الأرقام من مدخلاتها وإذا كان هذا العدد أكبر من N فستُعطي النتيجة 1 وإلا ستعطي النتيجة 0. لمنع حدوث فوضى في الشبكة، ترتبط الخلايا العصبية بطبقات، وليس بطريقة عشوائية. لا ترتبط الخلايا العصبية داخل الطبقة الواحدة، وإنما تتصل بالخلايا العصبية للطبقات التالية والسابقة (الأعلى والأسفل). تتحرك البيانات في الشبكة العصبية الاصطناعية تحركًا صارمًا باتجاه واحد من مدخلات الطبقة الأولى إلى مخرجات الطبقة الأخيرة. إذا وضعت عددًا كافيًا من الطبقات ووضعت الأوزان بطريقة صحيحة، فستحصل على النتيجة المرجوة وإليك مثلًا يوضح الأمر، تريد أن تكتشف ما هو الرقم المكتوب بخط اليد في الصورة الممررة، ستمرر الصورة إلى الشبكة عن طريق مدخلات الطبقة الأولى، وبعدها فإن البكسلات السوداء ستُنشّط الخلايا العصبية المرتبطة بها، وهي بدورها ستُنشّط الطبقات التالية المرتبطة بها، وهكذا حتى يضيء أخيرًا المخرج المسؤول عن الرقم أربعة. إذا هكذا وصلنا للنتيجة المرجوة (سنأخذ هذا المثال بوضوح أكبر وبكلّ تفاصيله الدقيقة في الجزء الثاني من هذه السلسلة). في الواقع عند برمجة الخلايا العصبية على الحاسب لا نكتب عمليًا الخلايا العصبية والوصلات المرتبطة بها. وإنما يمثل كلّ شيء كمصفوفات وتحسب النتيجة بناءً على ضرب المصفوفات ببعضها بعضًا للحصول على أداء أفضل. يبسط هذا الفيديو كيف تحدث عملية التعلّم في الخلايا العصبية الاصطناعية، وكيف تحدد عملية الضرب دقة الشبكة العصبية الّتي لدي (لا تنس أن تغعّل خيار التعليقات التوضيحية لأن الفيديو مترجم إلى اللغة العربية). تحتوي الشبكة على طبقات متعددة لها روابط بين كلّ خلية عصبية تسمى الشبكات العصبية بيرسيبترون المتعددة (Multilayer Perceptron) وتسمى اختصارًا (MLP) وتعد أبسط بنية مناسبة للمبتدئين. بعد إنشاء الشبكة، ستكون مهمتنا هي تعيين الطرق المناسبة لتتفاعل الخلايا العصبية مع الإشارات الواردة بطريقة صحيحة. وسنُعطي الشبكة بيانات الدخل أو "مدخلات الشبكة العصبية" صورة الرقم المكتوب بخط اليد وبيانات الخرج أو "مخرجات الشبكة العصبية" ستكون الرقم الموافق للصورة المُمررة عبر مدخلات الشبكة. أي سنقول للشبكة "عدلي أوزانك بالطريقة الصحيحة حتى تستطيعين معرفة الصورة الممررة لك على أنها صورة للرقم 4". في البداية تُسند جميع الأوزان بطريقة عشوائية. بعد أن نعرض لها رقمًا معينًا، إذ تنبعث منها إجابة عشوائية لأن الأوزان ليست صحيحة حتى الآن، ونوازن مدى اختلاف هذه النتيجة عن النتيجة الصحيحة. ثم نبدأ في بالرجوع للخلف عبر الشبكة من المخرجات إلى المدخلات ونخبر كلّ خلية عصبية، لقد تنشطت هنا وأديت عملًا رهيبًا وهكذا. بعد مئات الآلاف من هذه الدورات "الاستدلال ثمّ التحقق ثمّ التغيير" المتتالية هناك أمل في أن تُصحح الشبكة العصبية أوزانها وتجعلها تعمل على النحو المنشود. الاسم العلمي لهذه المنهجية هي "منهجية الانتشار العكسي" (Backpropagation). يبسط هذا الفيديو كيف تَحدثُ عملية التعلّم بالتفاصيل الدقيقة في الطبقات المخفية وكيف تتعلم من أخطائها (مرة أخرى لا تنسَ أن تغعّل خيار التعليقات التوضيحية لأن الفيديو مترجم إلى اللغة العربية). يمكن للشبكة العصبية المدربة تدريبًا جيدًا أن تنوب عن عمل أي من الخوارزميات الموضحة في هذا الفصل (بل وغالبًا ما يمكنها أن تعمل بدقة أكثر منهم). وهذا ما جعلها شائعة الاستخدام على نطاق واسع. اتضح لاحقًا أن الشبكات الّتي تحتوي على عدد كبير من الطبقات تتطلب قوة حسابية لا يمكن تصورها آنذاك (عند بداية ظهور الشبكات العصبية). أما حاليًا فأي حاسوب مُخصص للألعاب يتفوق بالأداء على أداء مراكز البيانات الضخمة آنذاك. لذلك لم يكُ لدى الناس أي أمل في أن تصبح هذه القدرة الحسابية متوفرة ذلك الحين، وكانت فكرة الشبكات العصبية مزعجة بضخامتها. سنتاول في شرحنا أهم الهياكل المشهورة للشبكات العصبية في الوقت الحاضر. الشبكات العصبية التلافيفية (Convolutional Neural Networks) أحدثت بنية الشبكات العصبية التلافيفية والتي تدعى اختصارًا (CNN) ثورة في عالم الشبكات العصبية حاليًا. إذ تستخدم للبحث عن الكائنات في الصور وفي مقاطع الفيديو، كما تستخدم أيضًا للتعرف على الوجوه، وتحويل التنسيق، وتوليد وتحسين الصور، وإنشاء تأثيرات مثل التصوير البطيء وتحسين جودة الصورة. باختصار تستخدم بنية الشبكات العصبية التلافيفية في جميع الحالات الّتي تتضمن صورًا ومقاطع فيديو. يمكنك ملاحظة كيف استطاعت تقنيات تعلم الآلة الّتي طورتها شركة فيسبوك من تحديد الكائنات الموجودة في الصورة بدقة ممتازة. من أبرز المشاكل الرئيسية الّتي تواجهنا عند التعامل مع الصور هي صعوبة استخراج الميزات منها. على عكس سهولة الّتي نجدها عند التعامل مع النصوص، إذ في النصوص يمكنك ببساطة تقسيم النص بحسب الجمل، والبحث عن الكلمات ذات سمات معينة، وما إلى ذلك. ولكن في الصور الأمر أعقد من ذلك بكثير إذ يجب تصنيف الصور تصنيفًا يدويًا لكي تتمكن برامج تعلّم الآلة من معرفة مكان آذان القطط أو ذيولها في هذه الصورة المحددة والمنصفة. سميت هذه المنهجية لاحقًا باسم "صناعة الميزات يدويًا" وكان يستخدمها الجميع تقريبًا. ولكن الأمر لم يتوقف إلى هذا الحد فحسب وإنما ظهرت العديد من المشاكل مع منهجية صناعة الميزات يدويًا.فمثلًا في البداية إذا كانت تعرفت الشبكة العصبية على أذني القطة وأبعدت هذه القطة عن الكاميرا فنحن في مشكلة لأن الشبكة لن ترى شيئًا (بسبب تغيّر حجم أذن القطة). ثانيًا لنحاول تسمية 10 ميزات مختلفة تميّز القطط عن بقية الحيوانات الأخرى (في الحقيقة أنا أول من فشل في هذه المهمة)، ولكن مع ذلك عندما أرى نقطة سوداء تُسرعُ من جانبي أثناء تجولي في الشارع عند منتصف الليل - حتى لو لمحتها فقط في زاوية عيني - سأستطيع أن أحدد بأنها قطة وليست فأر. والسبب بسيط جدًا إذ لا شعوريًا يصنف دماغنا العديد من الميزات الخاصة بالقطط ولا ينظر إلى شكل الأذن أو عدد الأرجل فقط وذلك بدون أي جهد مني ولا حتى تفكير. وبناءً على ذلك سيصعب الأمر جدًا عند محاولتي لنقل هذه المعرفة إلى الآلة. لذا فهذا يعني أن الآلة ستحتاج إلى تعلّم هذه الميزات بمفردها، وإنشاء هذه الميزات اعتمادًا على الخطوط الأساسية للصورة. سننفذ ما يلي: سنقسم الصورة بأكملها إلى كتل ذات حجم 8×8 بكسل. سنخصص لكل نوع من أنواع الخطوط على الصورة رمزًا معينًا - سواء أكان الخط أفقيًا سيكون الرمز [-] أو رأسيًا سيكون الرمز [|] أو قطريًا سيكون الرمز [/]. يمكن أيضًا أن يكون العديد منها مرئيًا للغاية - وهذا يحدث ولذلك لسنا دائمًا على ثقة تامة. سيكون الناتج عدة جداول من الخطوط الّتي هي في الواقع أبسط الميزات الّتي تمثل حواف الكائنات على الصورة. إنها صور بمفردها ولكنها مبنية من الخطوط. وهكذا نستمر في أخذ كتلة ذات حجم 8×8 ونرى كيف تتطابق معًا. ونعيدها مرارًا وتكرارًا. تسمى هذه العملية بعملية الإلتفاف أو الطيّ (Convolution)، مستمدة هذا الاسم من تابع الطيّ المطبق فيها. يمكن تمثيل عملية الطيّ كطبقة من الشبكة العصبية، لأنه في نهاية الأمر يمكن لكلّ خلية عصبية أن تكون بمثابة تابع يؤدي أي وظيفة أريدها. عندما نغذي ونزود شبكتنا العصبية بالكثير من صور القطط، فإنها ستعيّن تلقائيًا أوزانًا أكبر لمجموعات الخطوط الّتي تتكرر كثيرًا في هذه النوع من الصور. لا تهتم الآلة ما إذا كان ظهر القطة خطًا مستقيمًا أو جسمًا هندسيًا معقدًا مثل وجه القطة، وبالمجمل ستكون بعض مجموعات الخطوط ستكون نشطة دائمًا. كمخرجات ستنظر هذه الشبكة العصبية الاصطناعية ذات البنية التلافيفية لأكثر المجموعات نشاطًا في هذه الصور وستبني عليها قرارها فيما إذا كانت الصور لقطة أو لكلب. يكمن جمال هذه الفكرة في أن الشبكة العصبية ستبحث عن الميزات الأكثر تميزًا للكائنات بمفردها. لسنا بحاجة لاختيارها يدويًا. يمكننا تزويد الشبكة بكمية كبيرة من الصور لأي كائن فقط من خلال البحث في غوغل عن مليارات من الصور المشابهة وهكذا سوف تنشئ شبكتنا خرائط مميزة من الخطوط وتتعلم كيفية تمييز أي كائن بمفردها. الشبكات العصبية المتكررة (Recurrent Neural Networks) تعدّ الشبكات العصبية المتكررة والتي يشار لها اختصارًا (RNN) من أكثر البُنى الهيكلية للشبكات العصبية الاصطناعية شيوعًا في وقتنا الحاضر. وذلك لفوائدها الجمّة إذ أعطتنا الكثير من الأشياء المفيدة مثل: الترجمة الآلية، والتعرف على الكلام، وتركيب صوت مميز للمساعد الشخصي مثل المساعد سيري (Siri). وعمومًا تعدّ هذه البنية من أفضل الخيارات الموجودة للبيانات التسلسلية مثل: الصوت أو النص أو الموسيقى. هل تذكر القارئ الصوتي الخاص الموجود في نظام التشغيل ويندوز إكس بي (Windows XP)؟ هذا الرجل المضحك يبني الكلمات حرفًا بحرف، محاولًا لصقها معًا. الآن وازن بين صوته وصوت المساعد الشخصي أليكسا الخاص بشركة أمازون، أو المساعد الشخصي الخاص بغوغل، فرق كبير بينهم، أليس كذلك؟ إنهم لا ينطقون الكلمات بوضوح فقط وإنما يضيفون لكنة خاصة مناسبة لهم! إليك هذا الفيديو اللطيف لشبكة عصبية تحاول أن تتحدث. كلّ ذلك لأن المساعدين الصوتيين الحديثين مدربون على التحدث على عبارات كاملة دفعة واحدة وليس حرفًا بحرف، يمكننا أخذ مجموعة من النصوص الصوتية وتدريب شبكة عصبية لإنشاء تسلسل صوتي أقرب إلى الكلام الأصلي. بمعنى آخر، سنستخدم النص كمدخل للشبكة العصبية الاصطناعية وصوت الشخص المجرّد كخرج لهذه الشبكة. نطلب من الشبكة العصبية إنشاء بعض الأصوات لنص محدد، ثم موازنته بالصوت الأصلي ومحاولة تصحيح الأخطاء للاقتراب قدر الإمكان من الصوت الأصلي المثالي. تبدو عملية التعلم بسيطة وكلاسيكية أليس كذلك؟ حتى الشبكات العصبية ذات التغذية المُسبقة تستطيع فعل ذلك. ولكن كيف يجب تعريف مخرجات هذه الشبكة؟ هل سيكون بلفظ كلّ عبارة ممكنة موجودة في اللغة الإنكليزية؟ بالتأكيد هذا ليس خيارًا جيدًا. هنا ستساعدنا حقيقة أن النص أو الكلام أو حتى الموسيقى؛ ما هي إلا تسلسلات من المعلومات. تتكون من وحدات متتالية (مثل المقاطع اللفظية للكلمات الإنكليزية). تبدو جميعها فريدة من نوعها ولكنها تعتمد على مقاطع سابقة. ألغِ هذا الاتصال بين هذه المقاطع وستحصل على مقطع موسيقي من نوع دبستيب (Dubstep). يمكننا تدريب الشبكة العصبية بيرسيبترون لتوليد هذه الأصوات الفريدة، ولكن كيف ستتذكر الإجابات السابقة؟ لذا تكمن الفكرة في إضافة ذاكرة خاصة لكلّ خلية عصبية اصطناعية، واستخدامها كمدخل إضافي عند تشغيل المقطع التالي. يمكن للخلايا العصبية أن تدون ملاحظات لنفسها مثل اكتشافها لحرفٍ متحرك، ولذلك يتوجب عليها أن تُظهر المقطع الصوتي التالي بنبرة أعلى (في الحقيقة إنها مجرد مقاربة بسطية للغاية). بهذه الطريقة ظهرت الشبكات المتكررة. كان لهذا النهج مشكلة كبيرة وهي عندما تتذكر جميع الخلايا العصبية نتائجها السابقة، يصبح عدد الاتصالات في الشبكة ضخمًا جدًا لدرجة أنه من المستحيل -من الناحية الفنية- ضبط جميع الأوزان. لذلك عندما لا تستطع الشبكة العصبية نسيان بعض الأشياء غير الهامّة فلن تتمكن من تعلّم الأشياء الجديدة (حتى نحن البشر لدينا نفس المشكلة نسيان بعض المعلومات غير الهامة، أو لعلّها ميزة؟ وخصيصًا إذا كانت هذه الأشياء هي ذكريات مؤلمة!). كان التحسين الأول بسيطًا جدًا وذلك بتحديد حجم معين لذاكرة الخلية العصبية الاصطناعية. لنقل بأن الشبكة ستحفظ آخر 5 نتائج فقط. ولكن أليست هذه الفكرة مناقضة للفكرة الأساسية الّتي انطلقنا منها (وهي تذكر ما تعلمته الشبكة بالكامل)؟ بعد تحديثاتٍ وتطويرات كثيرة جاء لاحقًا نهج أفضل بكثير، والّذي سيستخدم خلايا خاصة، تشبه إلى حدٍ ما ذاكرة الحاسوب. يمكن لكلّ خلية إمكانية تسجيل رقم معين أو قراءته أو إعادة تعيينه. وسميت هذه الخلايا بخلايا الذاكرة طويلة وقصيرة الأجل (LSTM). والآن عندما تحتاج الخلية العصبية إلى تعيين منبه لتذكر هذا المقطع، فإنها ستضع راية (Flag) في تلك الخلية. مثل "كان الحرف ساكنًا في الكلمة، استخدم المرة التالية قواعد نطق مختلفة". عندما لا تستدعي الحاجة لاستخدام الرايات، سيُعاد ضبط الخلية تاركة فقط الاتصالات "طويلة الأجل" للشبكة العصبية. وبعبارة أخرى، ستُتدربُ الشبكة العصبية ليس فقط لكي تتعلّم كيفية ضبط الأوزان وإنما لتتعلّم أيضًا كيفية ضبط الرايات (وهي أشبه ما يمكن بالمنبهات) في الخلايا العصبية. قد يبدو الحل بسيط جدًا ومع ذلك يعمل بكفاءة عالية. لكن ماذا لو دمجنا إمكانية تعديل مقاطع الفيديو باستخدام الشبكات العصبية التلافيفية (CNN) مع إمكانية تعديل الصوت باستخدام الشبكات العصبية المتكررة (RNN) على ماذا سنحصل؟ هل حقًا سنحصل على الرئيس السابق للولايات المتحدة الأمريكية؟ إليك هذا الفيديو لتكتشف الأمر. التعلم العميق (Deep Learning) إذا أردنا أن نختصر التعلم العميق بجملة واحدة وواحدة فقط ستكون حتمًا: "التعلّم العميق هو شبكة عصبية اصطناعية كبيرة". من بعض التطبيقات العملية للتعلم العميق: التعرف على الصور والأصوات. تحليل بيانات الأرصاد الجوية. تحليل بيانات الأبحاث البيولوجية. مجال التسويق واختيار الجمهور المستهدف من الإعلانات. من بعض الهيكليات الشبكة العصبية الاصطناعية المستخدمة بكثرة في التعلّم العميق نجد: شبكات بيرسيبترون متعددة الطبقات (Multilayer Perceptron Networks). الشبكات العصبية التلافيفية (Convolutional Neural Networks). الشبكات العصبية المتكررة ذات الذاكرة قصيرة وطويلة الأمد (Long Short-Term Memory Recurrent Neural Networks). والعديد من البنى الأخرى للشبكة. بعد بناء العلماء والباحثين للعديد من البُنى (المعماريات) الخاصة بالشبكات العصبية في محاولة منهم للعثور على البنية الأنسب لاكتشاف الأنماط في البيانات، ومن بين أبرز هذه البُنى (المعماريات) كانت البنية الخاصة بالشبكة التعلم العميق، ويذكر أن أول مرة ظهر فيها مفهوم التعلم العميق كان في عام 2006، وعرفت في ذلك الوقت على أنها مجال فرعي من مجالات تعلّم الآلة (مع أنها تندرج تحت نفس فئة الشبكات العصبية)، إلا أنها لاقت الاهتمام الواسع عندما طبق جيفري هينتون وزملائه بنية الشبكة الخاصة بالتعلّم العميق في مسابقة ImgNet وحققوا آنذاك نتائج مبهرة، إذ استطاعوا تحقيق دقة أفضل بـ10% من البُنى القديمة في التعرف على الصور. بعد هذا النجاح المدوّي استطاعت بجدارة لفت الأنظار حولها وبدأت بالظهور العديد من التطبيقات والأبحاث الجديدة الخاصة بالتعلم العميق، مما أدى إلى تطورها تطورًا كبيرًا، كما أنها أثببت جودتها بتحقيقها نتائج مذهلة في العديد من التطبيقات، وبذلك أوجدت لنفسها مكانة لا يستهان بها في مجال الذكاء الصنعي عمومًا ومجال تعلم الآلة خصوصًا. يعتمدُ مفهوم التعلم العميق في أساسه على طريقة تعلّم مؤلفة من عدّة طبقات من التمثيلات المقابلة لبنيةٍ هرمية من السمات، ويتم تعريفُ السِّمات والمفاهيم عالية المستوى نزولًا إلى المفاهيم ذات المستوى الأدنى، وهي تعمل أيضًا نمط التعلم الموجه وغير الموجه إلا أنها تعمل عملًا ممتازً مع البيانات المصنفة (أي مع التعلّم الموجّه). إن للتعلم العميق علاقة وطيدة مع البيانات إذ لا بدّ من الحصول على كميات كبيرة من البيانات إذا أردنا استخدام هذا النوع من التعلّم. ومع ازدياد البيانات سوف تتحسن الدقة تحسنًا كبيرًا مما سيؤدي لنتائج أفضل في نهاية المطاف. الفرق بين الشبكات العصبية والتعلم العميق في الحقيقة إن التعلم العميق ما هو إلا بنية مخصصة من الشبكات العصبية، ولكنها سميت بالتعلم العميق نسبة إلى عدد الطبقات الّتي تحتويها هذه البنية الشبكية، وبما أنها أكبر من عدد الطبقات الخاصة بالشبكات العادية آنذاك فلذلك سُميت بهذا الإسم. تكمن قوة التعلم العميق في إمكانيته في تعلّم الميزات (Features) بطريقة هرمية، أي تتعلّم التسلسل الهرمي للميزات انطلاقًا من ميزات من مستوى عالٍ مرورًا بميزات بمستوى أخفض وهكذا إلى أن نصل لآخر الميزات ذات المستوى الأدنى، مما يعطي لأسلوب التعلم هذا مستوًى جديدًا من التجريد للنظام وخصوصًا مع الوظائف والمفاهيم المعقدة من خلال بنائها من المفاهيم الأبسط فالأبسط. من الشكل السابق نلاحظ كيف أن التعلم العميق يستطيع تعلم التمثيلات الهرمية للبيانات. الّتي تربط المدخلات مع المخرجات مباشرة من البيانات دون الاعتماد على الميزات الّتي حددها الإنسان. أي تستطيع استنتاج كميات كبيرة من الميزات ومحاولة تحليلها وبطها بالصورة الكاملة للمشكلة. يطلق على هذا النوع من طريقة تعلّم الميزات الخاصة بالبيانات بتعلم الميزات (Feature learning). ومن بين أبرز الطفرات العلمية الّتي شهدها التعلم العميق كان على يد شركة ديب مايند (والتي استحوذت عليها شركة ألفابت)، إذ استطاعت شركة ديب مايند أن تدمج بين التعلم العميق مع التعلم المعزز من أجل حلّ المشاكل المعقدة مثل لعب الألعاب. أطلقوا لاحقًا على طريقتهم هذه اسم شبكات التعلم المعزز العميق (Deep Q-Network)، بعدها اصبح التعلّم العميق في أغلب البنى الخاصة بالشبكات العصبية. ومن الملاحظ مما سبق أن طريقة التعلم العميق تتطلب أجهزة حاسب قوية جدًا، وذلك لأنها تتعامل مع كميات كبيرة من البيانات. هنالك العديد من البنى الخاصة بالشبكات العصبية لدرجة أننا نحتاج لكتاب كامل لتغطية كافة أنواعها ومميزاتها وسلبياتها وطرق عملها ..إلخ، إلا أنه وبما أنك استطعت تعلم الأساسيات فحتمًا ستستطيع تعلم أصعب البنى الشبكية، ولإعطاء نظرة دقيقة للأمر إليك الصورة التالية: كما يمكنك ايضًا الرجوع إلى هذا المقال لمزيد من المعلومات. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن الخلاصة بعد تعرفنا على أهم الأساسيات الخاصة بتعلّم الآلة، وكيف تختلف عن بعضها بعضًا، وما هي النقاط الّتي يجب علينا التركيز عليها عند اختيارنا لطريقة ما على حساب الأخرى، وتعرفنا أخيرًا على الشبكات العصبية والتعلم العميق، لا بدّ لنا من أن نسأل أنفسنا، ما هي المشاكل الّتي يُواجهنا هذا المجال؟ ما نوع هذه المشاكل؟ وكيف نستطيع تجاوزها؟ في مقالنا القادم سنحاول الإجابة على هذه الأسئلة ونتعلم أيضًا بعض الأمور المهمة والّتي ستساعدنا في المضي قدمًا في هذا المجال. المراجع مقال Machine Learning for Everyone. كتاب Hands on Machine Learning with Scikit Learn Keras and TensorFlow الطبعة الثانية. مقال What is Deep Learning?. اقرأ أيضًا المقال التالي: تعلم الآلة: التحديات الرئيسية وكيفية التوسع في المجال المقال السابق: الذكاء الاصطناعي: مراحل البدء والتطور والأسس التي نشأ عليها النسخة الكاملة من كتاب مدخل إلى الذكاء الاصطناعي وتعلم الآلة1 نقطة
-
ما هو السبب فى أن عداد التكرار لا يعمل ؟ (function mainFunc(){ var i, myMain = document.getElementById("main"); function changeColor(color) { color= ["red","blue","black","yellow","white"]; for(i = 0 ; i<color.length ; i=i+1) myMain.innerHTML=color[i] ; } return changeColor(); }());1 نقطة
-
أظن أن قراءة كتاب خاص بالبرمجة شيء قديم وأمر ممل نسبيا, ففي مجال البرمجة هنالك لكل لغة برمجة موقع خاص بها لشرحها بكل تفاصيلها وهذا سيكون كمرجع لكي في حال نسيتي شيئا ما بعد أن تقومي بتعلم ما تريدين تعلمه وهو ايضا جيد للتعلم في حال قرأتي منه حيث أن مثلا الكود المشروح يمكنكي تطبيقه وتجربته والتعديل عليه في نفس الوقت. صحيح أن الكتب هي المصدر الأساسي للتعلم ولكن هنالك ما هو أسهل من ذلك في هذا الوقت حيث توجد وكما ذكرت أعلاه المواقع المخصصة للغة معينة أو الوثيقة الرسمية للغة البرمجة, الدورات ,المقالات ومجموعات مواقع التواصل الاجتماعي حيث ان هنالك العديد من الصفحات والمجموعات تقدم بعض المعلومات الجميلة والقيمة. وأنا أنصحك بدورة التي بعنوان CS50 المقدمة من جامعة هارفرد فهي تشرح العديد من الأمور اللازم تعلمها في مجال البرمجة ولكن هذه الدورة باللغة الانجليزية ولكن هنالك من قام بشرحها باللغة العربية. وبما أنكِ ذكرتِ أنكِ تعلمتي واجهات الويب يجب عليكي تطبيق بعض المشاريع في هذا المجال ثم يمكنك بعد ذلك الانتقال إلى تطوير الويب حتى تكتمل خبرتك في مجال الويب والتي تحتوي على البرمجة التي تبحثين عنها.1 نقطة
-
مرحبًا @AM0, الشيفرة لديك تعمل جيداً و لعمل ميزة إعادة اللعبة : أولاً داخل الدلة winner() نقوم بتوقيف السطر : Application.Exit(); حتى لا يتوقف البرنامج عند إنتهاء اللعبة . بعد ذلك نقوم بعمل زر جديد و عند الضعط عليه نُعيد اللعبة من جديد بهذه الشيفرة: count = 0; button1.Text = ""; button1.Enabled = true; button2.Text = ""; button2.Enabled = true; button3.Text = ""; button3.Enabled = true; button4.Text = ""; button4.Enabled = true; button5.Text = ""; button5.Enabled = true; button6.Text = ""; button6.Enabled = true; button7.Text = ""; button7.Enabled = true; button8.Text = ""; button8.Enabled = true; button9.Text = ""; button9.Enabled = true; المنطق هو اننا نقوم بإعادة قيمة المتغير count الى الصفر و نص كل الأزرار إلى الوضع الإفتراضي مع عمل ذلك يصبح وكأننا قمنا بإعادة تشغيل البرنامج و يُمكننا اللعب من جديد .1 نقطة
-
1 نقطة
-
اكوادي هذي كنت ابغا احط قائمه فيها العب من جديد او مو شرط قائمه بس ابغا اضغط ويخلي الللعبه تنعاد يعني ابغا شيفره تعيد لي اللعبه هذي الاكواد public partial class Form1 : Form { int count = 0; public Form1() { InitializeComponent(); } private void winner() { if ((button1.Text == button2.Text) && (button1.Text == button3.Text) && button1.Text!="") { if (count % 2 == 0) { MessageBox.Show("Player 1 is the winner"); Application.Exit(); } else { MessageBox.Show("Player 2 is the winner"); Application.Exit(); } } if ((button4.Text == button5.Text) && (button4.Text == button6.Text) && button4.Text != "") { if (count % 2 == 0) { MessageBox.Show("Player 1 is the winner"); Application.Exit(); } else { MessageBox.Show("Player 2 is the winner"); Application.Exit(); } } if ((button7.Text == button8.Text) && (button8.Text == button9.Text) && button7.Text != "") { if (count % 2 == 0) { MessageBox.Show("Player 1 is the winner"); Application.Exit(); } else { MessageBox.Show("Player 2 is the winner"); Application.Exit(); } } if ((button1.Text == button4.Text) && (button4.Text == button7.Text) && button1.Text != "") { if (count % 2 == 0) { MessageBox.Show("Player 1 is the winner"); Application.Exit(); } else { MessageBox.Show("Player 2 is the winner"); Application.Exit(); } } if ((button2.Text == button5.Text) && (button2.Text == button8.Text) && button2.Text != "") { if (count % 2 == 0) { MessageBox.Show("Player 1 is the winner"); Application.Exit(); } else { MessageBox.Show("Player 2 is the winner"); Application.Exit(); } } if ((button3.Text == button6.Text) && (button3.Text == button9.Text) && button3.Text != "") { if (count % 2 == 0) { MessageBox.Show("Player 1 is the winner"); Application.Exit(); } else { MessageBox.Show("Player 2 is the winner"); Application.Exit(); } } if ((button1.Text == button5.Text) && (button1.Text == button9.Text) && button1.Text != "") { if (count % 2 == 0) { MessageBox.Show("Player 1 is the winner"); Application.Exit(); } else { MessageBox.Show("Player 2 is the winner"); Application.Exit(); } } if ((button3.Text == button5.Text) && (button3.Text == button7.Text) && button3.Text != "") { if (count % 2 == 0) { MessageBox.Show("Player 1 is the winner"); Application.Exit(); } else { MessageBox.Show("Player 2 is the winner"); Application.Exit(); } } } private void button1_Click(object sender, EventArgs e) { if (count % 2==0) { button1.Text = "O"; button1.Enabled = false; winner(); count++; } else { button1.Text = "X"; button1.Enabled = false; winner(); count++; } } private void button2_Click(object sender, EventArgs e) { if (count % 2 == 0) { button2.Text = "O"; button2.Enabled = false; winner(); count++; } else { button2.Text = "X"; button2.Enabled = false; winner(); count++; } } private void button3_Click(object sender, EventArgs e) { if (count % 2 == 0) { button3.Text = "O"; button3.Enabled = false; winner(); count++; } else { button3.Text = "X"; button3.Enabled = false; winner(); count++; } } private void button4_Click(object sender, EventArgs e) { if (count % 2 == 0) { button4.Text = "O"; button4.Enabled = false; winner(); count++; } else { button4.Text = "X"; button4.Enabled = false; winner(); count++; } } private void button5_Click(object sender, EventArgs e) { if (count % 2 == 0) { button5.Text = "O"; button5.Enabled = false; winner(); count++; } else { button5.Text = "X"; button5.Enabled = false; winner(); count++; } } private void button6_Click(object sender, EventArgs e) { if (count % 2 == 0) { button6.Text = "O"; button6.Enabled = false; winner(); count++; } else { button6.Text = "X"; button6.Enabled = false; winner(); count++; } } private void button7_Click(object sender, EventArgs e) { if (count % 2 == 0) { button7.Text = "O"; button7.Enabled = false; winner(); count++; } else { button7.Text = "X"; button7.Enabled = false; winner(); count++; } } private void button8_Click(object sender, EventArgs e) { if (count % 2 == 0) { button8.Text = "O"; button8.Enabled = false; winner(); count++; } else { button8.Text = "X"; button8.Enabled = false; winner(); count++; } } private void button9_Click(object sender, EventArgs e) { if (count % 2 == 0) { button9.Text = "O"; button9.Enabled = false; winner(); count++; } else { button9.Text = "X"; button9.Enabled = false; winner(); count++; } } }1 نقطة
-
أن تريد الطباعة في نفس الوسم لذلك يتم أخذ أخر شىء في المصفوفة لكن لو كنت تريد طباعة جميع محتويات المصفوفة عليك إزالة innerHTML من داخل الحلقة وإستخدمها في خارج الحلقة ولكن بعد أن تضع محتويات المصوفة في متغير نقوم ب innerHTML وإدخال القيمة في الوسم (function mainFunc(){ var i, myMain = document.getElementById("main"); var sinr=""; function changeColor(color) { color= ["red","blue","black","yellow","white"]; for(i = 0 ; i<color.length ; i=i+1){ sinr=+","+color[i] } myMain.innerHTML=sinr ; return changeColor(); }());1 نقطة
-
يمكنك أخي عماد أن تعرف السبب من الكود التالي: (function mainFunc(){ var i,c; function changeColor(color) { color= ["red","blue","black","yellow","white"]; for(i = 0 ; i<color.length ; i=i+1) c=color[i] ; } console.log(c); } return changeColor(); }()); هذا الكود سيطبع "white" فقط وذلك ﻷن قيمة c بعد أن تنتهي حلقة التكرار تساوي "white" وفي الكود الخاص بك ما هو داخل العنصر main سيكون "white". ولكن ماذا نفعل لطباعة جميع قيم المصفوفة ؟ المشكلة تكمن في أنك تريد طباعتها داخل عنصر HTML ، إذا أخبرتني بهدفك من ذلك قد نتوصل إلى حل وإلا عليك أن تجد طريقةً أخرى للطباعة. لست متأكداً تماماً مما تريد ولكن لعلك تحصل على ما تريد إن استبدلت هذه التعليمة: myMain.innerHTML=color[i] ; بهذه: myMain.innerHTML+=color[i] + " " ; // المسافة هي لفصل اﻷلوان المراد طباعتها، يمكنك اختيار ما تشاء للفصل بينها مثلاً <br />1 نقطة
-
المعذرة منك أخي عماد ولكن ما الذي لا يعمل فيه بالضبط فقد جربته عندي ولا يبدو أن هناك أي خطأ به ؟1 نقطة
-
نعم يعتبر المفتاح الأساسي index وذلك لأن المفتاح الأساسي هو كائن منطقي. وأقصد بذلك أن هذا ببساطة يحدد مجموعة من الخصائص في عمود واحد أو مجموعة من الأعمدة للمطالبة بأن تكون الأعمدة التي يتكون منها المفتاح الأساسي فريدة (unique) من نوعها وألا يكون أي منها فارغًا ( null). و نظرًا لأنها المفاتيح الأساسية تكون فريدة ولا تكون فارغة ، ويمكن استخدام هذه القيم التي بداخلها لتحديد صف واحد في الجدول في كل مرة. في معظم أنظمة وبهذا سيكون للمفتاح الأساسي فهرس تم إنشاؤه عليه.1 نقطة
-
يُلقِي هذا القسم نظرة على التعبيرات (expressions) المُستخدَمة لتمثيل قيمة أو حسابها. قد يكون التعبير (expression) بهيئة قيمة مُصنَّفة النوع (literal) أو مُتْغيِّر (variable) أو استدعاء دالة (function call) أو مجموعة من تلك الأشياء مُجمَّعة معًا بعوامل (operators) مثل + و >. يُمكِنك أن تُسنِد (assign) قيمة تعبير إلى مُتْغيِّر (variable) أو تُمرِّرها كمُعامل (parameter) أثناء استدعاء برنامج فرعي (subroutine call) أو تَدمِجها مع قيم آخرى ضِمْن تعبير آخر أكثر تعقيدًا. يمكنك أيضًا أن تتجاهلها ببعض الحالات إذا كان هذا ما تريد أن تفعله وهو أمر شائع أكثر مما قد تَظُنّ. تُعدّ التعبيرات في العموم جزءًا أساسيًا من البرمجة، ولقد كان تَعامُلنا معها حتى الآن غير رسمي نوعًا ما، ولكننا سنناقشها تفصيليًا خلال هذا القسم باستثناء بعض العوامل (operators) الأقل استخدامًا. تُعدّ كُلًا من القيم المُجرّدة مُصنَّفة النوع (literals) مثل 674 و 3.14 و true و X، والمُتْغيِّرات، واستدعاءات الدوال من أبسط اللبنات الأساسية بالتعبيرات. ينبغي أن تَتَذكَّر أن أي دالة (function) هي عبارة عن برنامج فرعي (subroutine) يُعيد قيمة، ولقد تَعرَّضنا بالفعل لعدة أمثلة منها برامج (routines) الإِدْخَال المُعرَّفة بالصنف TextIO وكذلك الدوال الرياضية بالصَنْف Math. يَحتوِي الصَنْف Math على مجموعة من الثوابت (constants) الرياضية التي يُمكِن اِستخدَامها ضِمْن أي تعبير رياضي مثل الثابت Math.PI المُمثِل لنسبة محيط أي دائرة إلى طول قُطرها π، وكذلك الثابت Math.E المُمثِل لقيمة أساس اللوغاريتم الطبيعي e. تلك الثوابت (constants) هي عبارة عن مُتْغيِّرات أعضاء (member variables) من النوع double مُعرَّفة بالصَنْف Math، ولأن القيم التي تُمثِلها تلك الثوابت الرياضية تَتَطلَّب عددًا لا نهائيًا من الأرقام لتَخْصِيصها بدقة، اِستخدَمنا أعدادًا تَقْرِيبية لقيم تلك الثوابت. بالإضافة إلى ذلك، يُعرِّف الصَنْف القياسي Integer ثوابتًا (constants) مُتْعلِّقة بالنوع int، فمثلًا، يُمثِل الثابت Integer.MAX_VALUE أكبر عدد يُمكِن للنوع int أن يَحْمله ويُساوِي 2147483647 بينما يُمثِل الثابت Integer.MIN_VALUE أصغر عدد يُمكِن حَمْله ويُساوِي -2147483648. بالمثل، يُعرِّف الصَنْف Double ثوابتًا مُتْعلِّقة بالنوع double فمثلًا يُمثِل الثابت Double.MAX_VALUE أكبر قيمة يُمكِن للنوع double أن يَحملها بينما يُمثِل الثابت Double.MIN_VALUE أصغر قيمة موجبة يُمكِن حَمْلها كما يُعرِّف الصَنْف ثوابتًا آخرى تُمثِل كُلًا من القيمتين اللانهائيتين Double.POSITIVE_INFINITY و Double.NEGATIVE_INFINITY وكذلك القيمة غَيْر المُعرَّفة Double.NaN. فمثلًا، قيمة Math.sqrt(-1) هي Double.NaN. في الواقع، القيم مُصنَّفة النوع (literals) والمُتْغيِّرات (variables) واِستدعاءات الدوال (function calls) هي مُجرّد تعبيرات (expressions) بسيطة يمكنك دَمْجها عن طريق عوامل (operators) لتَحصُل على تعبيرات أكثر تعقيدًا. تَتَضمَّن تلك العوامل كُلًا من العامل + لحساب حاصل مجموع عددين والعامل > لموازنة قيمتين. عندما تَستخدِم أكثر من عامل ضِمْن تعبير (expression)، فعادةً ما تكون الكيفية التي ستُجمَّع بها تلك العوامل -فيما يُعرَف باسم أولوية (precedence) العوامل- لتَحْصِيل قيمها مَحلّ حيرة وتساؤل. فمثلًا، إذا كان لدينا تعبير مثل A + B * C، فإن قيمة B * C تُحسَب أولًا ثم تُضاف إلى A. يَعنِي ذلك أن عامل الضرب * له أولوية عن عامل الجمع + بشكل افتراضي، فإذا لم يَكُن ذلك هو ما تريده، يُمكِنك ببساطة إضافة أقواس تُحدِّد صراحةً الكيفية التي ستُجمَّع بها العوامل، فمثلًا، يمكنك كتابة (A + B) * C إذا أردت أن تَحسِب حاصل جمع A مع B أولًا ثم تضرب النتيجة في C. تُوفِّر الجافا عددًا كبيرًا من العوامل (operators) سنَمُرّ عبر الغالبية الأكثر شيوعًا واستخدامًا ببقية هذا القسم. العوامل الحسابية (arithmetic) تَتَضمَّن العوامل الحسابية (arithmetic operators) كُلًا من عمليات الجمع والطرح والضرب والقسمة، وتُستخدَم الرموز + و - و * و / بالجافا للإشارة إليها على الترتيب. يُمكِنك اِستخدَام تلك العمليات مع أي قيم من النوع العددي: byte و short و int و long و float و double وكذلك مع أي قيم من النوع char حيث تُعامَل كعدد صحيح (integer) ضِمْن ذلك السياق. تحديدًا عندما يُستخدَم محرف من النوع char مع عامل عددي (arithmetic operator)، يُحوِّله الحاسوب إلى عدده الترميزي باليونيكود (unicode code number). في العموم عندما يُحصِّل الحاسوب أيًا من تلك العمليات بشكل فعليّ، فينبغي أن تَكُون القيمتان المُدْمجتان من نفس النوع، فإذا أراد البرنامج أن يَدْمِج قيمتين من نوعين مختلفين، سيُحوِّل الحاسوب إحداهما إلى نوع الآخرى. فمثلًا عند حِسَاب 37.4 + 10، سيُحوِّل الحاسوب العدد الصحيح 10 إلى نَظيره الحقيقي 10.0 ثُمَّ سيَحسِب قيمة 37.4 + 10.0. يُطلَق على ذلك "تَحْوِيل نوع (type conversion)"، ولا تحتاج عادةً لأن تقلق بشأن ذلك عند حُدوثه ضِمْن تعبير (expression) لأن الحاسوب سيُجريه أتوماتيكيًا. عندما تَدمِج قيمتين عدديتين بغض النظر عما إذا كان الحاسوب قد اِضطّر لإِجراء عملية تَحْوِيل نوع (type conversion) لأي منهما أم لا، فإن الناتج سيَكُون من نفس النوع. فمثلًا، إذا حَسبت حاصل ضرب عددين صحيحين من النوع int، ستَحصُل على قيمة من النوع int أما إذا حَسبت حاصل ضرب عددين من النوع double، فستَحصُل على قيمة من النوع double. يُعدّ ذلك أمرًا متوقعًا على أية حال، ولكن عندما تَستخدِم عامل القسمة /، فلابُدّ أن تَكُون أكثر حرصًا، لأنه عند حِسَاب حاصل قسمة عددين صحيحين (integers)، دائمًا ما ستَكُون النتيجة عددًا صحيحًا أي ستُهمَل أية كُسور (fractional) قد يَتَضمَّنها خارج القسمة (quotient)، فمثلًا، قيمة 7/2 تُساوِي 3 لا 3.5. لنَفْترِض مثلًا أن N عبارة عن مُتْغيِّر من النوع العددي الصحيح، ستَكُون إذًا قيمة N/100 عددًا صحيحًا (integer) أما قيمة 1/N فستُساوِي الصفر لأي N أكبر من الواحد. تُعدّ تلك الحقيقة مصدرًا شائعًا للكثير من الأخطاء البرمجية. يُمكِنك مع ذلك أن تُجبِر الحاسوب على أن يَحسِب الناتج بهيئة عدد حقيقي (real number) من خلال جَعل أحد المُعاملات عددًا حقيقيًا، فمثلًا، عندما يُحصِّل الحاسوب قيمة 1.0/N، فإنه سيُحوِّل أولًا N إلى عدد حقيقي لكي يتماشى مع نوع القيمة 1.0، لذلك يَكُون الناتج عددًا حقيقيًا. تُوفِّر الجافا عاملًا (operator) آخر لحِسَاب باقي قسمة (remainder) عددين على بعضهما، ويُستخدَم % للإشارة إليه. إذا كان كُلًا من A و B أعدادًا صحيحة، يُمثِل A % B باقي قسمة A على B. فمثلًا، 7 % 2 تُساوِي 1 بينما 34577 % 100 تُساوِي 77 أما 50 % 8 فتُساوِي 2. لاحِظ أنه في حالة المُعاملات (operands) السالبة، لا يَعمَل % كما قد تَتَوقَّع من أي عامل باقي قسمة عادي، فمثلًا إذا كانت قيمة A أو B سالبة، فستَكُون قيمة A % B سالبة أيضًا. عادةً ما يُستخدَم العامل % لاختبار ما إذا كان عدد صحيح معين سالبًا أم موجبًا: إذا كانت قيمة N % 2 تُساوِي 0، يَكُون N عددًا موجبًا أما إذا كانت قيمته تُساوِي 1، يَكُون عددًا سالبًا. ويُمكِنك عمومًا أن تَفْحَص ما إذا كان عدد صحيح N قابلًا للقسمة على عدد صحيح آخر M بشكل متساوٍ من خلال فَحْص ما إذا كانت قيمة N % M تُساوِي 0. يَعمَل العامل % أيضًا مع الأعداد الحقيقية (real numbers)، ففي العموم، يُمثِل A % B مقدار ما يَتبقَّى بعدما تَحذِف من العدد A أكبر قدر ممكن من B. فمثلًا، 7.52 % 0.5 يساوي 0.02. قد تحتاج أحيانًا لأن تَحسِب القيمة السالبة من عدد معين. في هذه الحالة، بدلًا من اِستخدَام تعبير مثل (-1)*X، تُوفِّر الجافا لهذا الغرض عامل إشارة طرح أُحادي (unary) يُكْتَب على الصورة -X. بالمثل، تُوفِّر الجافا عامل إشارة جمع أُحادي يُكْتَب على الصورة +X، ولكنه لا يُنجز أي شيء فعليّ في الواقع. بالمناسبة، تَذكَّر أنه يُمكِنك أيضًا اِستخدَام العامل + لضمّ (concatenate) قيمة من أي نوع إلى قيمة آخرى عبارة عن سِلسِلة نصية من النوع String. ونظرًا لإمكانية تَحْوِيل أي نوع إلى النوع String أتوماتيكيًا، يُعدّ ذلك مثالًا آخر لعملية تَحْوِيل نوع (type conversion). الزيادة (increment) والنقصان (decrement) ستَجِد أن زيادة قيمة مُتْغيِّر (variable) معين بمقدار يُساوِي الواحد أو إنقاصها عملية شائعة للغاية بالبرمجة. يُمكِنك أن تُجرِي ذلك ضِمْن تَعْليمَة إِسْناد (assignment) كالتالي: counter = counter + 1; goalsScored = goalsScored + 1; تأخذ التَعْليمَة x = x + 1 القيمة السابقة للمُتْغيِّر x وتَحسِب حاصل مجموعها مع العدد واحد ثم تُخزِّن النتيجة كقيمة جديدة للمُتْغيِّر x. في الحقيقة، يُمكِنك تّنْفيذ نفس تلك العملية فقط بكتابة x++ أو ++x حيث يُغيِّر كُلًا منهما قيمة x بنفس تأثير التَعْليمَة x = x + 1. يُمكِنك إذًا كتابة التَعْليمَتين السابقتين على النحو التالي: counter++; goalsScored++; بالمثل، يمكنك كتابة x-- أو --x لطرح العدد واحد من x أي أن x-- لها نفس تأثير التعليمة x = x - 1. تُعرَف إضافة مقدار يُساوِي الواحد إلى مُتْغيِّر باسم "الزيادة (increment)" أما عملية الطرح فتُعرَف باسم "النقصان (decrement)"، وعليه يُطلَق على العاملين ++ و -- اسم عاملي الزيادة (increment operator) والنقصان (decrement operator) على الترتيب. يُمكِنك أن تَستخدِم تلك العوامل مع المُتْغيِّرات (variables) التي تنتمي إلى أي من الأنواع العددية وكذلك مع المُتْغيِّرات من النوع char. إذا كان ch يحتوي على المحرف 'A' فإن ch++ تُغيِّر قيمته إلى 'B'. عادةً ما يُستخدَم العاملان ++ و -- بتَعْليمَات مثل x++; أو x--;، وتَكُون في تلك الحالة بمثابة أوامر تُغيِّر من قيمة x. علاوة على ذلك، يُمكِنك أيضًا أن تَستخدِم x++ أو ++x أو x-- أو --x كتعبير (expression) أو كجزء ضِمْن تعبير أكبر. اُنظر الأمثلة التالية: y = x++; y = ++x; TextIO.putln(--x); z = (++x) * (y--); تُضيِف التَعْليمَة y = x++; مقدارًا يُساوِي الواحد إلى قيمة المُتْغيِّر x كما أنها تُسنِد قيمة معينة إلى y والتي تُعرَّف على أنها القيمة السابقة للمُتْغيِّر x أي قبل إضافة الواحد. فمثلًا، إذا كانت قيمة x هي ٦، فإن التَعْليمَة "y = x++; ستُغيِّر قيمة x إلى ٧. ولأن القيمة السابقة للمُتْغيِّر x تُساوِي ٦، فستُغيِّر التَعْليمَة قيمة y إلى نفس تلك القيمة ٦. من الناحية الآخرى، تُعرَّف قيمة ++x على أنها القيمة الجديدة للمُتْغيِّر x أي بعد إضافة مقدارًا يُساوِي الواحد. فمثلًا، إذا كانت قيمة x تُساوِي ٦، فستُغيِّر التَعْليمَة y = ++x; قيم كلًا من x و y إلى ٧. يَعمَل عامل النقصان (decrement operator) -- بنفس الطريقة. لمّا كان التعبير x = x++ يُسنِد القيمة السابقة للمُتْغيِّر x -أي قبل تّنْفيذ الزيادة- إلى نفس ذات المُتْغيِّر x، فإنه في الواقع لا يُغيِّر قيمته. إذا شئنا الدقة، فإن التعبير السابق يزيد (increment) قيمة x بمقدار الواحد ولكنها سرعان ما تَتَغيَّر إلى القيمة السابقة نتيجة لتَعْليمَة الإِسْناد (assignment). لاحِظ أن التعبير x++ يختلف عن x + 1 فالتعبير الأول يُغيِّر من قيمة x أما الثاني فلا يُغيِّرها. قد يَكُون ذلك مُربكًا نوعًا ما، وفي الحقيقة، يقع الطلبة عادةً بالكثير من الأخطاء البرمجية (bugs) نتيجة لذلك الارتباك، ولكن ليس هناك أي داعٍ لذلك فيُمكِنك مثلًا أن تَستخدِم العاملين ++ و -- كتَعْليمَات مُفردة فقط وليس كتعبيرات (expressions)، وهو ما سنلتزم به خلال الأمثلة التالية. العوامل العلاقية (relational) تُوفِّر الجافا مُتْغيِّرات منطقية وكذلك تعبيرات (expressions) منطقية يُمكِن اِستخدَامها كشُروط (conditions) قد تؤول إلى أي من القيمتين true أو false. عادةً ما يَتَكوَّن التعبير المنطقي من عامل علاقي (relational) يُوازن بين قيمتين، فيَختبِر مثلًا ما إذا كانت قيمتان متساويتين أو ما إذا كانت قيمة إحداهما أكبر من الآخرى وهكذا. يَتَوفَّر بالجافا العوامل العلاقية التالية == و != و < و > و <= و >=: A == B Is A "equal to" B? A != B Is A "not equal to" B? A < B Is A "less than" B? A > B Is A "greater than" B? A <= B Is A "less than or equal to" B? A >= B Is A "greater than or equal to" B? تُستخدَم تلك العوامل (operators) لموازنة قيم من أي أنواع عددية. علاوة على ذلك، يُمكِنها أيضًا أن تُستخدَم لموازنة قيم من النوع char. بالنسبة للمحارف (characters)، فإن العوامل < و > مُعرَّفة لتَعمَل وفقًا لقيمها العددية باليونيكود (unicode) وهو ما يختلف عن الترتيب الأبجدي المُعتاد حيث تأتي الأحرف الأبجدية الكبيرة (upper case) بأكملها قبل الأحرف الأبجدية الصغيرة (lower case). فيما يَتَعلَّق بالتعبيرات المنطقية (boolean expressions)، لا تختلف قيم النوع المنطقي وفقًا للحاسوب عن قيم أي نوع آخر. فيُمكِنك مثلًا أن تُسنِد (assign) تعبيرات منطقية إلى أي مُتْغيِّر منطقي تمامًا مثلما بإِمكانك إِسْناد قيم عددية إلى أي مُتْغيِّر عددي، وكذلك يُمكِن للدوال (functions) أن تُعيد قيم من النوع المنطقي. إلى جانب ذلك، يُمكِنك أن تَستخدِم تلك التعبيرات ضِمْن تَعْليمَتي حَلْقة التَكْرار (loop) والتَفْرِيع (branch)، وهو ما سنُناقشه بالفصل التالي. بالمناسبة، يُمكِنك أن تَستخدِم العاملين == و != لموازنة القيم المنطقية أيضًا، وهو ما قد يَكُون مفيدًا في بعض الأحيان. اُنظر المثال التالي: boolean sameSign; sameSign = ((x > 0) == (y > 0)); فيما يَتَعلَّق بالقيم من النوع String، لا يُمكِنك أن تَستخدِم العوامل العلاقية < و > و <= و >= لموازنة قيم ذلك النوع، ولكن يُسمَح باِستخدَام كُلًا من == و != لموازنتها مع أنها لن تُعطِيك النتيجة التي تَتَوقَّعها بسبب الكيفية التي تَتَصرَّف الكائنات (objects) على أساسها. فمثلًا، يَختبِر العامل == ما إذا كان الكائنان (objects) مُخزَّنين بنفس مَوضِع الذاكرة (memory location) بدلًا من أن يَختبِر ما إذا كانت قيمتهما هي نفسها. قد تحتاج إلى إجراء تلك الموازنة لبعض أنواع الكائنات أحيانًا، ولكن نادرًا ما يَكُون ذلك هو الحال مع السَلاسِل النصية (strings) من النوع String. سنَعُود لمناقشة ذلك فيما بَعْد على أية حال. ينبغي عمومًا أن توازن السَلاسِل النصية باِستخدَام برامج فرعية (subroutines) مثل equals() و compareTo() والتي تَعرَّضنا لها بالقسم الفرعي ٢.٣.٣. إلى جانب ذلك، فإنه عند اِستخدَام كُلًا من العاملين == و != مع الثابت Double.NaN المُمثِل للقيمة غَيْر المُعرَّفة من النوع double، فإنهما لا يَتَصرَّفان على النحو المُتوقَّع. فمثلًا إن كان x مُتْغيِّرًا من النوع double، فإن تعبيرات مثل x == Double.NaN و x != Double.NaN ستُكون مُساوِية للقيمة false بجميع الحالات سواء كانت x تَحتوِي على Double.NaN أم لا. لذا إذا أردت أن تَختبِر ما إذا كان المُتْغيِّر x يحتوي على القيمة غَيْر المُعرَّفة Double.Nan، يُمكِنك أن تَستخدِم الدالة Double.isNan(x) والتي تُعيد قيمة منطقية. العوامل المنطقية (logical/boolean) تَتَركَّب الشُروط (conditions) الأكثر تعقيدًا من عوامل منطقية (boolean operators) هي "and" و "or" و "not" بالإنجليزية مثل الشرط التالي: "If there is a test and you did not study for it…". تُوفِّر الجافا لحسن الحظ تلك العوامل أيضًا كجزء من اللغة. يَدمِج العامل المنطقي "and" قيمتين من النوع المنطقي boolean لتَكُون النتيجة قيمة من النوع boolean. إذا كانت كلتا القيمتين تُساوِي true، فإن النتيجة النهائية ستُساوِي true أما إذا كانت قيمة أي منهما تُساوِي false، فإن النتيجة ستُساوِي false. يَعمَل الترميز && مُمثِلًا للعامل المنطقي "and" بلغة الجافا، فمثلًا، يؤول التعبير (x == 0) && (y == 0) إلى القيمة المنطقية true إذا كانت قيمة كُلًا من x و y تُساوِي صفرًا. في المقابل، يَعمَل الترميز || مُمثِلًا للعامل المنطقي "or" بلغة الجافا. يؤول التعبير A || B إلى true إذا كانت قيمة أي من A أو B أو كليهما تُساوِي true بينما يؤول إلى false إذا كانت قيمة كُلًا من A و B تُساوِي false. يَتبِع كُلًا من العاملين && و || مفهوم الدارة القصيرة (short-circuited) أي لا يَكُون تَحْصِيل قيمة المُعامل الثاني ضروريًا إذا أَمْكَن معرفة النتيجة النهائية قبلها. اُنظر الاختبار التالي: (x != 0) && (y/x > 1) إذا كانت قيمة x تُساوِي صفر، فسيَكُون حاصل قسمة y/x غَيْر مُعرَّف رياضيًا. ولأن الحاسوب سيُحصِّل قيمة المعامل الأيسر (x != 0) أولًا، وسيَجِدها مُساوية للقيمة false، فإنه سيُدرك أن القيمة النهائية للشرط ((x != 0) && anything) ككل لابُدّ وأن تُساوِي false لا محالة، لذا فإنه لا يُحصِّل قيمة المعامل الأيمن، ولا يُنفِّذ عملية القسمة من الأساس. يُعدّ هذا مثالًا على اِستخدَام الدارة القصيرة (short circuit) وهو ما جَنَّبَنا القسمة على صفر. تَستخدِم الجافا المحرف ! لتمثيل العامل المنطقي "not" والذي هو عامل أحادي (unary) يُكْتَب قَبْل مُعامله الوحيد، فمثلًا إذا كان test مُتْغيِّر منطقي من النوع boolean فإن التعبير التالي: test = ! test; سيَعكِس قيمة المُتْغيِّر test ويُغيِّرها من true إلى false ومن false إلى true. العامل الشرطي (conditional) تَملُك أي لغة برمجية جيدة بعض الخاصيات الصغيرة الأنيقة نوعًا ما والتي هي في الواقع ليست ضرورية بشكل فعليّ، فكل ما تَفعَله هي أنها تمنحك شعورًا جيدًا عند اِستخدَامها. تُوفِّر الجافا عاملًا شَّرْطيًا (conditional operator) عبارة عن عامل ثلاثي (ternary) أي لديه ٣ مُعاملات (operands)، ويَتَكوَّن من جزئين هما ? و : يُستخدَمان معًا. يُكْتَب العامل بالصياغة التالية: <boolean-expression> ? <expression1> : <expression2> يَختبر الحاسوب قيمة التعبير المنطقي ** **أولًا، فإذا كانت قيمته تُساوِي true، فسيؤول التعبير ككل إلى قيمة ، أما إذا كانت قيمته تُساوِي false، فسيؤول إلى . اُنظر المثال التالي: next = (N % 2 == 0) ? (N/2) : (3*N+1); بالمثال السابق، إذا كان التعبير N % 2 == 0 يُساوِي true أي إذا كانت قيمة N زوجية، فستُسنَد قيمة N/2 إلى المُتْغيِّر next أما إذا كانت قيمة N فردية، فستُسنَد قيمة (3*N+1) إليه. لاحِظ أن الأقواس ضِمْن هذا المثال ليست مطلوبة ولكنها تُسهِل من قراءة التعبير (expression). عوامل الإسناد (assignment) وتحويل الأنواع (type conversion) لقد تَعرَّضنا بالفعل للترميز = ضِمْن تَعْليمَات الإِسْناد (assignment statement) المسئولة عن إِسْناد قيمة تعبير (expression) معين إلى مُتْغيِّر (variable). يُعدّ ذلك الترميز = بمثابة عامل (operator) بمعنى أنه من المُمكِن اِستخدَامه كتعبير (expression) بحد ذاته أو كجزء ضِمْن تعبير أكثر تعقيدًا. لاحِظ أن قيمة أي عملية إِسْناد (assignment) مثل A=B تَكُون مُساوِية للقيمة ذاتها المُسنَدة إلى A، لذا إذا أردت أن تُسنِد (assign) قيمة مُتْغيِّر B إلى آخر A، وأن تَفْحَص ما إذا كانت قيمته تُساوِي ٠ بنفس ذات الوقت، يُمكِنك أن تَستخدِم ما يلي: if ( (A=B) == 0 )... مع أنه لا يُحبذ القيام بذلك عمومًا. في العموم، لابُدّ أن يَكُون نوع التعبير (expression) على الجانب الأيمن من أي تَعْليمَة إِسْناد (assignment) من نفس نوع المُتْغيِّر (variable) على جانبها الأيسر. في بعض الحالات، يستطيع الحاسوب أن يُحوِّل قيمة التعبير أتوماتيكيًا لكي تتوافق مع نوع المُتْغيِّر (variable). فمثلًا، بالنسبة لقائمة الأنواع العددية: byte و short و int و long و float و double، يستطيع الحاسوب أن يُحوِّل قيمة من نوع مَذكُور بأول تلك القائمة أتوماتيكيًا إلى قيمة من نوع مَذكُور لاحقًا. اُنظر الأمثلة التالية: int A; double X; short B; A = 17; X = A; // ستحوّل A إلى النوع double B = A; // لا يمكن فليس هناك تحويل تلقائي من int إلى short الفكرة باختصار هو أنه يُمكِن دومًا التَحْوِيل بين نوعين أتوماتيكيًا طالما لن يؤدي ذلك التَحْوِيل إلى تَغْيير دلالة القيمة. فمثلًا، يُمكِن لأي قيمة من النوع int أن تُحوَّل دومًا إلى قيمة من النوع double بنفس قيمتها العددية. في المقابل، نظرًا لأن أكبر قيمة يُمكِن للنوع short أن يَحمِلها تُساوِي 32767، فإنه لا يُمكِن لبعض القيم من النوع int التي تَقَع خارج النطاق المسموح به للنوع short مثل 100000 أن تُمثَل باِستخدَام النوع short. قد تَرغَب أحيانًا بأن تُجرِي تَحْوِيلًا لن يَحدُث أتوماتيكيًا. يُمكِنك القيام بذلك عن طريق ما يُعرَف باسم "التَحْوِيل بين الأنواع (type casting)" وذلك بكتابة اسم النوع المطلوب التَحْوِيل إليه بين قوسين أمام القيمة المُراد تَحْوِيلها. اُنظر المثال التالي: int A; short B; A = 17; B = (short)A; // يصح لأن A حولت صراحة إلى قيمة من النوع short يُمكِنك إجراء عملية "التَحْوِيل بين الأنواع (type casting)" من أي نوع عددي إلى أي نوع عددي آخر، ولكنه قد يتسبَّب بتَغْيِير القيمة العددية للعدد. فمثلًا، (short)100000 تُساوِي -31072. أخذنا العدد الصحيح 100000 من النوع int المُكوَّن من ٤ بايت ثُمَّ تركنا ٢ بايت منه حتى نَحصُل على قيمة من النوع short، وعليه فقد خَسرنا المعلومات الموجودة بتلك البايتات المُلغَاة. عندما تُجرِي عملية "التحويل بين الأنواع (type-casting)" من عدد حقيقي (real) إلى عدد صحيح (integer)، يُهمَل الجزء الكسري (fractional) منه تلقائيًا. فمثلًا، (int)7.9453 تُساوِي 7. تُعدّ مسألة الحصول على عدد صحيح عشوائي بين ١ و ٦ بمثابة مثال آخر: تُعيد الدالة Math.random() عددًا حقيقيًا يتراوح بين 0.0 و 0.9999، لذا فإن قيمة التعبير 6*Math.random() تَكُون عددًا يتراوح بين 0.0 و 5.999. يُمكِننا الآن أن نَستخدِم عامل التَحْوِيل بين الأنواع (type-cast operator) (int) لتَحْوِيل قيمة ذلك التعبير إلى عدد صحيح كالتالي (int)(6*Math.random())، والتي تُصبِح نتيجته واحدة من قيم الأعداد الصحيحة ٠ و ١ و ٢ و ٣ و ٤ و ٥. والآن، لنَحصُل على عدد يتراوح بين العددين ١ و ٦، يُمكِننا أن نُضيف مقدارًا يُساوِي الواحد كالتالي (int)(6*Math.random()) + 1. لاحِظ أن الأقواس حول 6*Math.random() ضرورية لأنه وبحسب قواعد الأولوية (precedence rules)، سيُطبَق عامل التحويل بين الأنواع (type cast operator) على العدد ٦ فقط إذا لم نَستخدِم أية أقواس. يُعدّ النوع char بمثابة نوع عددي صحيح (integer) أي يُمكِنك مثلًا أن تُسنِد (assign) قيمة من النوع char إلى مُتْغيِّر من النوع int كما تستطيع أن تُسنِد ثابت (constant) صحيح تتراوح قيمته من ٠ وحتى ٦٥٥٣٥ إلى مُتْغيِّر من النوع char. يُمكِنك حتى أن تُطبِق عملية التَحْوِيل بين الأنواع (type-casting) صراحةً بين النوع char وأي نوع عددي آخر. فمثلًا، (char)97 تُساوِي 'a' أما (int)'+' فتُساوِي 43 بينما (char)('A' + 2) تُساوِي 'C'. أما بالنسبة للنوع String، فلا يُمكِنك أن تَستخدِم عامل التَحْوِيل بين الأنواع (type-casts) لكي تُحوِّل بين النوع String وأي نوع آخر. في المقابل، يُمكِنك أن تَضُمّ (concatenate) قيم من أي نوع إلى سِلسِلة نصية فارغة لتَحْوِيلها إلى النوع String، فمثلًا، قيمة "" + 42 تُساوِي السِلسِلة النصية "42". على نحوٍ أفضل، يُمكِنك أن تَستخدِم الدالة (function) String.valueOf(x) المُعرَّفة كعضو ساكن (static member) بالصَنْف String حيث تُعيد تلك الدالة قيمة x بعد تَحْوِيلها إلى سِلسِلة نصية من النوع String، فمثلًا، يُعيد الاستدعاء String.valueOf(42) القيمة "42". إذا كان ch مُتْغيِّرًا من النوع char، فإن الاستدعاء String.valueOf(ch) يُعيد سِلسِلة نصية (string) طولها يُساوِي ١ ومُكوَّنة من محرف واحد يُمثِل قيمة ch. تستطيع أيضًا تَحْوِيل بعض السَلاسِل النصية (strings) إلى قيم من أنواع آخرى. فمثلًا، يُمكِنك تَحْوِيل سِلسِلة نصية مثل "10" إلى القيمة 10 من النوع int وكذلك تَحْوِيل سِلسِلة نصية مثل "17.42e-2" إلى قيمة من النوع double تُساوي 0.1742. تُوفِّر الجافا دوالًا مبنية مُسْبَقًا (built-in functions) لمُعالجة تلك التَحْوِيلات. يُعرِّف الصَنْف القياسي Integer عضو دالة ساكن (static member function) للتَحْوِيل من النوع String إلى النوع int. لنَفْترِض مثلًا أن str عبارة عن مُتْغيِّر يَتَضمَّن تعبيرًا (expression) من النوع String، سيُحاوِل استدعاء الدالة Integer.parseInt(str) إذًا أن يُحوِّل قيمة ذلك المُتْغيِّر إلى قيمة من النوع int. على سبيل المثال، سيُعيد استدعاء Integer.parseInt("10") القيمة 10 من النوع int. إذا لم تَكُن قيمة المُعامل (parameter) المُمرَّرة للدالة Integer.parseInt تُمثِل قيمة صالحة من النوع int، فسيَحدُث خطأ. بالمثل، يُعرِّف الصَنْف القياسي Double الدالة Double.parseDouble. لنَفْترِض أن str عبارة عن مُتْغيِّر من النوع String، سيُحاوِل إذًا استدعاء الدالة Double.parseDouble(str) أن يُحوِّل قيمة المُتْغيِّر str إلى قيمة من النوع double، وسيَحدُث خطأ إذا لم تَكُن قيمة str تُمثِل قيمة صالحة من النوع double. لنعود الآن إلى تَعْليمَات الإِسْناد (assignment statements)، تُوفِّر الجافا تشكيلة متنوعة من عامل الإِسْناد لتَسهِيل الكتابة. فمثلًا، A += B مُعرَّف ليَعمَل بنفس كيفية عَمَل A = A + B. في العموم، يَتوفَّر عامل إِسْناد مشابه لأي عامل (operator) طالما كان مُطبَقًا على مُعاملين (operands) -باستثناء العوامل العلاقية (relational operators)-. اُنظر الأمثلة التالية: x -= y; // x = x - y; x *= y; // x = x * y; x /= y; // x = x / y; x %= y; // x = x % y; q &&= p; // q = q && p; (for booleans q and p) يُمكِن لعامل تَعْليمَة الإِسْناد المُدْمَج += أن يُطبَق حتى على السَلاسِل النصية من النوع String. لقد طَبَقنا العامل + من قبل على سِلسِلة نصية، وكان ذلك بمثابة عملية ضَمّ (concatenation). لمّا كان التعبير str += x مُكافِئًا تمامًا للتعبير str = str + x، فإنه عند وجود سِلسِلة نصية (string) على الجانب الأيسر من العامل +=، تُلحَق القيمة الموجودة على جانبه الأيمن إلى نهاية السِلسِلة النصية، فمثلًا، إذا كانت قيمة str تُساوِي "tire"، تُغيِّر التَعْليمَة str += 'd' قيمة str إلى "tired". قواعد الأولوية (precedence rules) إذا اِستخدَمت أكثر من عامل (operator) واحد ضِمْن تعبير (expression)، وفي ذات الوقت لم تَستخدِم أي أقواس لتُشير صراحةً إلى الترتيب الذي ينبغي أن تُحصَّل (evaluate) على أساسه قيمة ذلك التعبير، فلابُدّ عندها إذًا من أن تنتبه لما يُعرَف باسم قواعد الأولوية (precedence rules)، والتي تُستخدَم لتَحْدِيد الترتيب الذي سيتبعه الحاسوب لتَحْصِيل قيمة التعبير. يُنصَح عمومًا باِستخدَام الأقواس لتَجنُّب أي ارتباك مُحتمَل لك أو لقارئ البرنامج في العموم. تَستعرِض القائمة التالية العوامل (operators) التي ناقشناها بهذا القسم مُرتَّبة تنازليًا بحسب الأولوية (precedence) من الأعلى (يُحصَّل أولًا) إلى الأقل (يُحصَّل أخيرًا): Unary operators: ++, --, !, unary -, unary +, type-cast Multiplication and division: *, /, % Addition and subtraction: +, - Relational operators: <, >, <=, >= Equality and inequality: ==, != Boolean and: && Boolean or: || Conditional operator: ?: Assignment operators: =, +=, -=, *=, /=, %= تَملُك العوامل (operators) ضِمْن نفس السطر بالقائمة السابقة نفس الأولوية (precedence). إذا اِستخدَمت عواملًا لها نفس الأولوية ضِمْن تعبير بدون تَخْصِيص أي أقواس، تُحصَّل كُلًا من العوامل الأحادية (unary) وعوامل الإسناد (assignment) من اليمين إلى اليسار بينما تُحصَّل قيمة بقية العوامل من اليسار إلى اليمين. فمثلًا، تُحصَّل قيمة التعبير A*B/C كما لو كان مَكْتُوبًا كالتالي (A*B)/C بينما تُحصَّل قيمة التعبير A=B=C على النحو التالي A=(B=C). بفَرْض أن قيمة التعبير B=C هي نفسها قيمة المُتْغيِّر B، هل تَعرِف الغرض من التعبير A=B=C ؟ ترجمة -بتصرّف- للقسم Section 5: Details of Expressions من فصل Chapter 2: Programming in the Small I: Names and Things من كتاب Introduction to Programming Using Java.1 نقطة
-
لقد رأينا مدى سهولة اِستخدَام دوال (functions) مثل System.out.print و System.out.println لعَرْض نص معين للمُستخدِم، ولكن لا يُمثِل ذلك سوى جزءًا صغيرًا من موضوع مُخْرَجات النصوص. تَعتمِد غالبية البرامج عمومًا على البيانات التي يُدخِلها المُستخدِم أثناء زمن تَشْغِيل البرنامج لذا لابُدّ من فهم الكيفية التي ينبغي أن نَتَعامَل على أساسها مع كُلًا من المُدْخَلات والمُخْرَجات. سنشرح خلال هذا القسم كيف يُمكِننا قراءة البيانات المُدْخَلة من قِبَل المُستخدِم كما سنناقش المُخْرَجات على نحو أكثر تفصيلًا، وأخيرًا سنتناول اِستخدَام الملفات كوسيلة للمُدْخَلات والمُخْرَجات. الخرج البسيط والخرج المنسق تُعدّ الدالة System.out.print(x) واحدة من أبسط دوال الخَرْج حيث تَستقبِل مُعاملًا x عبارة عن قيمة أو تعبير (expression) من أي نوع. إذا لم تَكُن قيمة المُعامل x المُمرَّرة سِلسِلةً نصية من النوع String، فإنها ستُحوَّل أولًا إلى قيمة من النوع String ثم ستُرسَل إلى مقصد خَرْج (output destination) يُعرَف باسم "الخَرْج القياسي (standard output)" مما يَعنِي أنها ستُعرَض للمُستخدِم. لاحِظ أنه في حالة برامج واجهة المُستخدِم الرسومية (GUI)، ستُرسَل تلك السِلسِلة إلى مكان لا يُحتمَل للمُستخدِم أن يراه. بالإضافة إلى ذلك، يُمكِننا أيضًا أن نُعيد توجيه الخَرْج القياسي (standard output) لكي يَكْتُب إلى مقصد خَرْج (output destination) مختلف، ولكن فيما هو مُتَعلِّق بالبرنامج التالي، سيتولَّى الكائن System.out مسئولية عَرْض النص للمُستخدِم. تُرسِل الدالتان System.out.println(x) و System.out.print نفس النص إلى الخَرْج، ولكن تُضيف الأولى سطرًا جديدًا (line feed) إلى نهاية النص أي سيُعرَض الخَرْج التالي بسطر جديد. يُمكِنك أيضًا أن تَستخدِم الدالة System.out.println() بدون أية مُعاملات (parameter) لإخراج سطر جديد أي يُكافِئ استدعاء الدالة System.out.println(x) ما يلي: System.out.print(x); System.out.println(); ربما تَكُون قد لاحظت أن System.out.print تُخرِج أعدادًا حقيقية مُكوَّنة من عدد أرقام مناسب بَعْد العلامة العشرية، فمثلًا، تُخرِج العدد π كالتالي "3.141592653589793" أما الأعداد المُمثِلة للنقود فتُخرِجها على الصورة التالية "1050.0" أو "43.575". قد تُفضِّل إِخراج تلك الأعداد بهيئة مختلفة مثل "3.14159" و "1050.00" و "43.58"، وعندها ينبغي أن تَستخدِم ما يُعرَف باسم "الخَرْج المُنسَّق (formatted output)" والذي سيُساعدك على التَحكُّم بكيفية طباعة الأعداد الحقيقية وغيرها من القيم الآخرى. تَتَوفَّر العديد من خيارات التنسيق سنُناقِش هنا أبسطها وأكثرها شيوعًا. يُمكِنك اِستخدَام الدالة System.out.printf للحصول على خَرْج مُنسَّق. يُعدّ الاسم printf اختصارًا للكلمتين "print formatted" أي "اِطبَع بصورة مُنسَّقة"، وهو في الواقع مأخوذ من لغتي البرمجة C و C++. تَستقبِل الدالة System.out.printf مُعاملًا (parameter) واحدًا أو أكثر بحيث يكون مُعاملها الأول عبارة عن سِلسِلة نصية من النوع String تُحدِّد صياغة الخَرْج (output format)، ويُطلَق عليها اسم "صِيْغة سلسلة التنسيق (Format String)" أما بقية المُعاملات فتُخصِّص القيم المطلوب إرسالها للخَرْج. تَطبَع التَعليمَة التالية عددًا بصياغة تتناسب مع قيمة نقدية بالدولار بحيث يُمثِل amount مُتْغيِّرًا من النوع double: System.out.printf( "%1.2f", amount ); تتكوَّن أي صِيْغة سلسلة تنسيق (format string) من "مُحدِّد تنسيق (format specifiers)" واحد أو أكثر بحيث يكون كُلًا منها مسئولًا عن صيغة الخَرْج لقيمة واحدة تُمرَّر عبر مُعامل آخر إضافي. في المثال أعلاه، احتوت صيغة سلسلة التنسيق على مُحدِّد تنسيق واحد هو %1.2f. يبدأ أي مُحدِّد تنسيق عمومًا بعلامة نسبة مئوية % وينتهي بحرف أبجدي يُحدِّد نوع الخَرْج المطلوب كما قد يَتَضمَّن معلومات تنسيق آخرى تُكْتَب بينهما، فمثلًا، ها هي بعض من مُحدِّدات التنسيق المُحتمَلة: %d و %12d و %10s و %1.2f و %1.8g. يُشير الحرف "d" بمُحدِّديّ التنسيق %d و %12d إلى أن القيمة المطلوب كتابتها عبارة عن عدد صحيح (integer) أما "12" بالمُحدِّد الثاني فتُشير إلى أقل عدد من الخانات ينبغي للخَرْج أن يحتلّه، فمثلًا، إذا تَطلَّب إخراج عدد صحيح عدة خانات عددها أقل من ١٢، ستُضَاف خانات فارغة إضافية إلى مُقدمة ذلك العدد لكي يُصبِح مجموع الخانات بالنهاية مساويًا لأقل عدد مُمكِن أي ١٢. يُقال عندها أن الخَرْج "وَاقِع على مُحاذاة اليمين بحقل طوله ١٢" أما إذا تطلَّب إخراجه عددًا أكبر من ١٢ خانة، ستُطبَع جميع الأرقام بدون أي خانات إضافية. لاحِظ أن مُحدِّديّ التنسيق %d و %1d لهما نفس المعنى أي إخراج القيمة بهيئة عدد صحيح بأي عدد ضروري من الخانات. يُعدّ الحرف "d" اختصارًا لكلمة "decimal" كما تستطيع اِستخدَام الحرف "x" بدلًا منه لإِخراج عدد صحيح بصياغة ست عشريّة (hexadecimal). يُمكِنك أيضًا اِستخدَام الحرف "s" بنهاية مُحدِّد تنسيق (format specifier) مع أي قيمة من أي نوع، مما يَعنِي ضرورة إخراج تلك القيمة بصيغتها الافتراضية كما لو أننا لم نَستخدِم خرجًا مُنسَّقًا من الأساس، فمثلًا، يُمكننا أن نُخصِّص مُحدِّد تنسيق مثل %20s بحيث يُمثِل العدد "20" أقل عدد ممكن من المحارف. يُعدّ الحرف "s" اختصارًا لكلمة "string" ويُمكِن استخدامه للقيم من النوع String أو مع أي قيم من أي نوع آخر بحيث تُحوَّل تلك القيم إلى النوع String بالطريقة المُعتادة. تُعدّ مُحدِّدات تنسيق القيم من النوع double أكثر تعقيدًا بقليل. يُستخدَم الحرف "f" بمُحدِّد تنسيق مثل %1.2f لإخراج عدد بصيغة عدد عشري مُكوَّن من فاصلة عائمة (floating-point form) أي يحتوي على عدة أرقام بعد العلامة العشرية. يُخصِّص العدد "2" بذلك المُحدِّد عدد الأرقام المُستخدَمة بَعْد العلامة العشرية أما العدد "1" فيُخصِّص أقل عدد من المحارف يلزم إخراجه. تعني القيمة "1" بذلك المَوضِع إمكانية إخراج أي عدد ضروري من المحارف. كمثال آخر، يُخصِّص المُحدِّد %12.3f صيغة عدد عشري مُكوَّن من ثلاثة أرقام بَعْد العلامة العشرية بحيث يُطبَع على محاذاة اليمين داخل حقل طوله ١٢. بالنسبة لكُلًا من الأعداد الكبيرة جدًا والصغيرة جدًا، فينبغي أن تُكْتَب بصيغة أسية (exponential format) مثل 6.00221415e23 وهو ما يُمثِل حاصل ضرب 6.00221415 في ١٠ مرفوعة للأس ٢٣. يُخصِّص مُحدِّد صيغة مثل %15.8e خَرْجًا بصيغة أُسية بحيث تُشير "8" إلى عدد الأرقام المُستخدَمة بَعْد العلامة العشرية. لاحِظ أنه إذا كنت تَستخدِم "g" بدلًا من "e"، فسيَكوُن الخرج بصيغة أُسية فقط للقيم الصغيرة جدًا والكبيرة جدًا أما القيم الآخرى فستُعرَض بصيغة عدد عشري كما ستُشير القيمة "8" بمُحدِّد صيغة مثل %1.8g إلى عدد الأرقام الكلي بما في ذلك كُلًا من الأرقام قَبْل وبَعْد العلامة العشرية. بالنسبة لخرج القيم العددية، فيُمكِنك أن تُضيف فاصلة , إلى مُحدِّد الصيغة لتقسيم أرقام العدد إلى مجموعات مما يُسهِل من قراءة الأعداد الكبيرة. بالولايات المتحدة الأمريكية، تَتَكوَّن كل مجموعة من ثلاثة أرقام يَفصِل بينها محرف فاصلة، فمثلًا إذا كانت قيمة x تُساوِي واحد بليون، فسيَكُون الخَرْج الناتج عن System.out.printf("%,d",x) هو 1,000,000,000. بدول آخرى، قد يختلف المحرف الفاصل وكذلك عدد الأرقام بكل مجموعة. لابُدّ من كتابة الفاصلة ببداية مُحدِّد الصيغة (format specifier) أي قبل تَخْصِيص عَرْض الحقل (field width) كالتالي %,12.3f. إذا أردت محاذاة الخرج إلى اليسار بدلًا من اليمين، يمكنك إضافة علامة سالبة إلى بداية مُحدِّد الصيغة كالتالي %-20s. إلى جانب مُحدِّدات الصيغة (format specifiers)، قد تَتَضمَّن صيغ سَلاسِل التنسيق (format string) المُمرَّرة إلى الدالة printf محارفًا آخرى عادية تُنسَخ إلى الخَرْج كما هي. يَكُون ذلك مناسبًا لتَضْمِين بعض القيم بمنتصف سِلسِلة نصية. فلنَفْترِض مثلًا أن كُلًا من x و y عبارة عن مُتْغيِّرات (variables) من النوع int، يُمكِننا إذًا كتابة التالي: System.out.printf("The product of %d and %d is %d", x, y, x*y); عندما تُنفَّذ تلك التَعْليمَة، فستَحِلّ قيمة x محلّ أول حُدوث لمُحدِّد الصيغة %d بالسِلسِلة النصية (string) بينما ستَحِلّ قيمة y محلّ الحُدوث الثاني، وأخيرًا ستَحِلّ قيمة التعبير x*y محلّ الحُدوث الثالث. فمثلًا، قد يَكُون الخَرْج كالتالي "The product of 17 and 42 is 714" (علامتي الاقتباس ليست ضِمْن الخَرْج). إذا أردت أن تُخرج علامة نسبة مئوية، فينبغي أن تَكْتُب مُحدِّد الصيغة %% بصيغة سِلسِلة التنسيق (format string) أما إذا كنت تريد أن تُخرِج سطرًا جديدًا، فاستخدِم مُحدِّد الصيغة %n. بالمثل من السَلاسِل النصية العادية، يُمكِنك أيضًا أن تَستخدِم خطًا مائلًا عكسيًا \ لإخراج محارف خاصة كعلامة الاقتباس المزدوجة أو المحرف "tab". مثال لمدخل نصي اعتادت الجافا لسبب غَيْر مفهوم أن تُصعِب من قراءة البيانات المُدْخَلة من قِبَل مُستخدِم البرنامج. كما ذكرنا من قبل، يتَوفَّر الكائن System.out بصورة مُسْبَقة ويُستخَدم لعَرْض الخَرْج للمُستخدِم حيث يَتَضمَّن البرنامج الفرعي (subroutine) System.out.print لذلك الغرض. يَتَوفَّر أيضًا الكائن System.in لأغراض قراءة البيانات المُدْخَلة من قِبَل المُستخدِم، ولكنه يُوفِّر خدمات إِدْخَال بدائية جدًا تَتَطلَّب بعض المهارات البرمجية المُتقدمة بالجافا لاستخدامه بكفاءة. قدمت الجافا منذ إصدارها الخامس الصَنْف Scanner مما سَهَّل قليلًا من عملية قراءة البيانات المُدْخَلة من قِبَل المُستخدِم، ولكنه يَتطلَّب بعض المعرفة بالبرمجة كائنية التوجه (object-oriented programming) لكي تَتَمكَّن من اِستخدَامه، لذلك ربما لا يَكُون الحل الأمثل حاليًا بينما ما نزال نبدأ خطواتنا الأولى. بالإضافة إلى ذلك، قدمت الجافا منذ إصدارها السادس الصَنْف Console لأغراض التواصل مع المُستخدِم، ولكنه يُعاني هو الآخر من بعض المشاكل، فهو أولًا غَيْر متاح دائمًا للاِستخدَام كما أنه قادر على قراءة السَلاسِل النصية (strings) فقط وليس الأعداد. يرى الكاتب أن الصَنْفين Scanner و Console لا يقومان بالأشياء على نحوٍ صحيحٍ تمامًا، لذلك فإنه سيَعتمِد على صَنْف خاص TextIO عَرَّفه بنفسه، ولكن سنتعرَّض أيضًا إلى الصَنْف Scanner بنهاية هذا القسم في حال أردت أن تبدأ باِستخدَامه. يُمكِنك أن تُنشِئ أصنافًا (classes) جديدة تَتَضمَّن برامجًا فرعيةً (subroutines) غَيْر مُتاحة بالجزء القياسي من لغة الجافا. بمُجرّد إِنشائك لصنف جديد، يُمكِنك أن تَستخدِم البرامج الفرعية (subroutines) المُعرَّفة بداخله بنفس الكيفية التي تَستخدِم بها البرامج المبنية مُسْبَقًا (built-in). على سبيل المثال، يَحتوِي الصَنْف TextIO الذي عرَّفه الكاتب على برامج فرعية (subroutines) لقراءة أي مُدْخَلات يَكْتُبها المُستخدِم من خلال كائن الدَخْل القياسي (standard input) System.in وذلك بدون الحاجة لأي مَعرِفة مُتقدمة بالجافا والتي يَتَطلَّبها كُلًا من اِستخدَام الصنف Scanner والاِستخدَام المباشر للكائن System.in. لاحِظ أن الصَنْف TextIO مُعرَّف بـ"حزمة (package)" اسمها textio مما يَعنِي وقوع الملف TextIO.java بمجلد اسمه textio كما يَعنِي أنه من الضروري لأي برنامج يَرغَب باِستخدَام الصَنْف TextIO من أن "يستورد (import)" ذلك الصَنْف من الحزمة textio أولًا بكتابة مُوجِّه الاستيراد import التالي: import textio.TextIO; لابُدّ أن يأتي الموجِّه بالأعلى قَبْل عبارة public class المُستخدَمة لبدء البرنامج. لاحِظ أن غالبية أصناف الجافا القياسية (standard classes) مُعرَّفة ضِمْن حزم (packages) ينبغي اِستيرادها (import) بنفس الطريقة إلى البرامج التي تَستخدِمها. لكي تَتَمكَّن من اِستخدَام الصَنْف TextIO ضِمْن برنامج، لابُدّ أن تَتَأكَّد أولًا من تَوفُّر الصَنْف (class) بذلك البرنامج. يختلف ما يَعنِيه ذلك بحسب بيئة برمجة الجافا (Java programming environment) المُستخدَمة، ولكن ينبغي عمومًا أن تُضيف المجلد textio -الذي يحتوي على الملف TextIO.java- إلى مجلد البرنامج الرئيسي. اُنظر القسم ٢.٦ لمزيد من المعلومات عن كيفية اِستخدَام الصَنْف TextIO. تُعدّ برامج (routines) الإِدْخَال المُعرَّفة بالصَنْف TextIO أعضاء دوال ساكنة (static member functions) ضِمْن ذلك الصنف (ناقشنا ما يعنيه ذلك بالقسم السابق). إذا كنت تَرغَب بكتابة برنامج يَقرأ عددًا صحيحًا مُدْخَلًا من قِبَل المُستخدِم، فيُمكِنك إذًا أن تَستخدِم الصَنْف TextIO حيث يَتَضمَّن عضو دالة ساكن مُعرَّف خصيصًا لذلك الغرض اسمه هو getlnInt. نظرًا لأن تلك الدالة (function) مُعرَّفة ضِمْن الصَنْف TextIO، فلابُدّ إذًا أن تَستخدِم الاسم TextIO.getlnInt للإشارة إليها ضِمْن البرنامج. لا تَستقبِل تلك الدالة أية مُعاملات (parameters)، لذا يَكُون استدعائها على الصورة TextIO.getlnInt(). يُعيد ذلك الاستدعاء قيمة من النوع int تُمثِل القيمة التي أَدْخَلها المُستخدِم، والتي ستَرغَب عادةً في اِستخدَامها بشكل أو بآخر لذا سنُسنِدها إلى مُتْغيِّر (variable). بِفَرض أن userInput عبارة عن مُتْغيِّر (variable) من النوع int صَرَّحنا عنه بتَعْليمَة تّصْرِيح (declaration statement) مثل int userInput;، نستطيع إذًا أن نُسنِد إليه القيمة المُعادة باِستخدَام تَعْليمَة الإِسْناد (assignment statement) التالية: userInput = TextIO.getlnInt(); عندما يُنفِّذ الحاسوب التَعْليمَة بالأعلى، فإنه سينتظر إلى أن يُدْخِل المُستخدِم قيمة من النوع العددي الصحيح أي لابُدّ أولًا أن يُدْخِل المُستخدِم عددًا ثم يَضغَط على مفتاح العودة إلى بداية السطر (return) لكي يُكمِل البرنامج عمله. ستُعيد الدالة (function) المُستدعَاة بالأعلى القيمة التي أَدْخَلها المُستخدِم، والتي ستُخزَّن بالمُتْغيِّر userInput. يَستخدِم البرنامج التالي الدالة TextIO.getlnInt لقراءة العدد المُدْخَل من قِبَل المُستخدِم ثم يَعرِض مربع ذلك العدد (لاحِظ مُوجّه الاستيراد [import] بأول سطر): import textio.TextIO; public class PrintSquare { public static void main(String[] args) { int userInput; // العدد الذي أدخله المستخدم int square; // قيمة مربع العدد الذي أدخله المستخدم System.out.print("Please type a number: "); userInput = TextIO.getlnInt(); square = userInput * userInput; System.out.println(); System.out.println("The number that you entered was " + userInput); System.out.println("The square of that number is " + square); System.out.println(); } // نهاية main() } // نهاية الصنف PrintSquare يَعرِض البرنامج بالأعلى الرسالة النصية "Please type a number:" عند تَشْغِيله ثم يَنتظِر إلى أن يَتَلقَّى إجابة مُكوَّنة من عدد يتبعه محرف العودة إلى بداية السطر (carriage return). لاحِظ أنه من الأفضل دومًا لأي برنامج أن يَطبَع سؤالًا للمُستخدِم قبلما يُحاوِل أن يقرأ أي قيم مُدْخَلة؛ لأنه في حالة غياب ذلك، قد لا يَتَمكَّن المُستخدِم من معرفة نوع القيمة التي ينتظرها الحاسوب بل إنه قد لا يَتَمكَّن حتى من ملاحظة كَوْن البرنامج ينتظره لأن يُدْخِل قيمة في العموم. دوال الإدخال بالصنف TextIO يَتَضمَّن الصَنْف TextIO تشكيلة متنوعة من الدوال (functions) لقراءة الأنواع (types) المختلفة التي يُمكِن للمُستخدِم أن يُدْخِلها. تَستعرِض القائمة التالية عددًا من تلك الدوال: j = TextIO.getlnInt(); // اقرأ قيمة من النوع int y = TextIO.getlnDouble(); // اقرأ قيمة من النوع double a = TextIO.getlnBoolean(); // اقرأ قيمة من النوع boolean c = TextIO.getlnChar(); // اقرأ قيمة من النوع char w = TextIO.getlnWord(); // اقرأ كلمة واحدة كقيمة من النوع String s = TextIO.getln(); // اقرأ سطرًا مدخلًا بالكامل كقيمة من النوع String لكي تُصبِح أي من تَعْليمَات الإِسْناد (assignment) بالأعلى صالحة، ينبغي أولًا أن تُصرِّح (declare) عن المُتْغيِّر الموجود على جانبها الأيسر كما ينبغي للمُتْغيِّر أن يَكُون من نفس النوع المُعاد (return type) من الدالة على الجانب الأيمن. لا تَستقبِل تلك الدوال أية مُعاملات (parameters)، وتُعيد قيم تُمثِل ما أَدْخَله المُستخدِم أثناء تَشْغِيل البرنامج، والتي ينبغي أن نُسنِدها (assign) إلى مُتْغيِّرات (variable) حتى نَتَمكَّن من اِستخدَامها ضِمْن البرنامج، حيث سنَتَمكَّن عندها من اِستخدَام أسماء تلك المُتْغيِّرات للإشارة إلى مُدْخَلات المُستخدِم. عندما تَستدعِي واحدة من تلك الدوال (functions)، فإنها ستُعيد دومًا قيمة صالحة من النوع المُحدَّد، ففي حالة أَدْخَل المُستخدِم قيمة غَيْر صالحة كمُدْخَل أي إذا طلب البرنامج قيمة من النوع int ولكن المُستخدِم أَدْخَل محرفًا (character) غَيْر عددي أو أَدْخَل عددًا يَقَع خارج النطاق المسموح به للقيم التي يُمكِن تَخْزِينها بمُتْغيِّر (variable) من النوع int، فسيَطلُب الحاسوب من المُستخدِم أن يُعيد إِدْخَال القيمة وسيتصرف كما لو أنه لمْ يَر القيمة غَيْر الصالحة نهائيًا. تَسمَح الدالة TextIO.getlnBoolean() للمُستخدِم بكتابة أي من القيم التالية: true و false و t و f و yes و no و y و n و 1 و 0 كما تَسمَح باِستخدَام كُلًا من الأحرف الأبجدية الكبيرة (upper case) والصغيرة (lower case). في جميع الأحوال، يُفسَّر مُدْخَل المُستخدِم على أساس كَوْنه مُكافِئ للقيمة true أو للقيمة false. عادةً ما تُستخدَم الدالة TextIO.getlnBoolean() لقراءة إجابة المُستخدِم على أسئلة "نعم أم لا". يُوفِّر الصَنْف TextIO دالتي إِدْخَال (input functions) تُعيد كلتاهما قيمة من النوع String هما: getlnWord() و getln(). تُعيد الأولى getlnWord() سِلسِلة نصية (string) مُكوَّنة من محارف (characters) فقط بدون أي فراغات (spaces)، حيث تَتَخطَّى عند اِستدعَائها أي فراغ (spaces) أو محرف عودة إلى بداية السطر (carriage return) يُمكِن أن يَكُون المُستخدِم قد أَدْخَله ثم تبدأ بقراءة المحارف إلى أن تَصِل إلى أول فراغ أو أول محرف عودة إلى بداية السطر، وعندها تُعيد سِلسِلة نصية من النوع String مُكوَّنة من المحارف المقروءة. في المقابل، تُعيد دالة الإِدْخَال الثانية getln() سِلسِلة نصية (string) مُكوَّنة من أية محارف أَدْخَلها المُستخدِم بما في ذلك أي فراغات إلى أن تَصِل إلى محرف عودة إلى بداية السطر (carriage return) أي أنها تَقرَأ سطرًا كاملًا من النص المُدْخَل. في حين يقرأ الحاسوب محرف العودة إلى بداية السطر (carriage return) إلا أنه لا يَعُدّه جزءًا من السِلسِلة النصية المُدْخَلة حيث يُهمله الحاسوب بَعْد قراءته. لاحِظ أنه إذا اِكتفَى المُستخدِم بالضغط على محرف العودة إلى بداية السطر بدون أن يَكْتُب أي شيء قبلها، ستُعيد عندها الدالة TextIO.getln() سِلسِلة نصية فارغة "" لا تَحتوِي على أية محارف نهائيًا. لا تتخطَّى دالة الإِدْخَال TextIO.getln() أية فراغات أو محارف نهاية السطر قبل قراءة القيمة المُدْخَلة. يختلف ذلك عن دوال الإِدْخَال الآخرى getlnInt() و getlnDouble() و getlnBoolean() و getlnChar() والتي تُشبه الدالة getlnWord() فيما يَتَعلَّق بتخطّيها لأية فراغات أو محارف عودة إلى بداية السطر (carriage returns) قبل بدئها بقراءة القيمة الفعليّة المُدْخَلة. عندما تَتَخطَّى إحدى تلك الدوال (functions) محرف نهاية السطر، فإنها تَطبَع العلامة '?' كإشارة للمُستخدِم بأن البرنامج ما يزال يَتَوقَّع بعض المُدْخَلات الآخرى. بالإضافة إلى ذلك، إذا أَدْخَل المُستخدِم محارفًا إضافية بنفس سطر القيمة الفعليّة المُدْخَلة، سيُهمِل الحاسوب جميع تلك المحارف الإضافية مع محرف العودة إلى بداية السطر (carriage return) الموجود بنهاية السطر. يترتب على ذلك أنه إذا اِستدعَى برنامج معين دالة إِدْخَال (input function) آخرى، فسيَكُون المُستخدِم مُضطّرًا لأن يُدْخِل سطرًا جديدًا حتى لو كان قد أَدْخَل عدة قيم بالسطر الأول. قد لا يَكُون إهمال الحاسوب لمُدْخَلات المُستخدِم بتلك الطريقة أمرًا جيدًا، ولكنه في الواقع الحل الأسلم بغالبية البرامج. باِستخدَامنا للصَنْف TextIO لكُلًا من المُدْخلات والمخرجات، نستطيع الآن أن نُحسِّن برنامج حساب قيمة الاستثمار من القسم ٢.٢، فيُمكِننا مثلًا أن نَسمَح للمُستخدِم بأن يُدْخِل قيمًا مبدئية لكُلًا من الاستثمار ومُعدّل الفائدة مما سيُعطِينا برنامجًا أكثر فائدة نَرغَب بتَشْغِيله أكثر من مرة. يَستخدِم البرنامج التالي خَرْجًا مُنسَّقًا (formatted output) لطباعة القيم النقدية بالصياغة المناسبة: import textio.TextIO; public class Interest2 { public static void main(String[] args) { double principal; // قيمة الاستثمار double rate; // قيمة معدل الفائدة السنوية double interest; // الفائدة المكتسبة خلال العام System.out.print("Enter the initial investment: "); principal = TextIO.getlnDouble(); System.out.print("Enter the annual interest rate (as a decimal): "); rate = TextIO.getlnDouble(); interest = principal * rate; // إحسب فائدة العام principal = principal + interest; // أضفها إلى المبلغ الأساسي System.out.printf("The amount of interest is $%1.2f%n", interest); System.out.printf("The value after one year is $%1.2f%n", principal); } // نهاية main() } // نهاية الصنف Interest2 قد تتساءل عن سبب وجود برنامج خَرْج (output routine) وحيد System.out.println بإِمكانه طباعة جميع أنواع القيم بينما تَتَوفَّر برامج إِدْخَال (input routine) متعددة لكل نوع على حدى. في الواقع، نظرًا لأن دالة الخَرْج (output function) تَستقبِل القيمة المطلوب طباعتها بهيئة مُعامل، فإن الحاسوب يُمكِنه أن يُحدِّد نوع القيمة بفَحْص قيمة المُعامل. في المقابل، لا تَستقبِل برامج الإِدْخَال (input routines) أية مُعاملات (parameters) لذا كان من الضروري اِستخدَام اسم مختلف لكل برنامج من برامج الإِدْخَال حتى يَتَمكَّن الحاسوب من التمييز بينها. الملفات كمدخلات ومخرجات يُرسِل System.out الخَرْج إلى مقصد خَرْج (output destination) يُعرَف باسم "الخَرْج القياسي (standard output)". يُمثِل الخَرْج القياسي مقصدًا واحدًا فقط ضِمْن عدة مقاصد خَرْج مُحتمَلة، فيُمكِنك مثلًا أن تَكْتُب البيانات إلى ملف مُخزَّن ضِمْن قرص صلب، وهو ما يتميز بعدة أمور منها أن البيانات ستَبقَّى مُخزَّنة بالملف إلى ما بَعْد انتهاء البرنامج كما ستَتَمكَّن من طباعة الملف أو إرساله عبر البريد الالكتروني إلى شخص آخر أو حتى تَعْدِيله باستخدام برنامج آخر. على نحو مُشابه، يُمثِل System.in مصدرًا واحدًا مُحتمَلًا للمُدْخَلات. يستطيع الصَنْف TextIO أن يقرأ من ويَكْتُب إلى الملفات. يَتَضمَّن الصنف TextIO دوال الخَرْج TextIO.put و TextIO.putln و TextIO.putf والتي تَعمَل بنفس طريقة عَمَل الدوال System.out.print و System.out.println و System.out.printf على الترتيب. علاوة على ذلك، يُمكِن أيضًا اِستخدَامها لإرسال نصوص إلى ملفات أو إلى أي مقاصد خَرْج آخرى. عندما تَستخدِم أي من دوال الخَرْج TextIO.put أو TextIO.putln أو TextIO.putf، يُرسَل الخَرْج إلى مقصد الخَرْج الحالي (output destination)، والذي يُمثِل ما يُعرَف باسم "الخَرْج القياسي (standard output)" على نحو افتراضي، ولكن تستطيع تَغْيِيره باِستخدَام برامج فرعية (subroutines) مُعرَّفة بالصَنْف TextIO، فمثلًا، يُمكِنك أن تَستخدِم التَعْليمَة التالية لكي تَكْتُب إلى ملف اسمه "result.txt": TextIO.writeFile("result.txt"); بمُجرّد تّنْفيذ التَعْليمَة السابقة، سيُرسَل الخَرْج الناتج عن أي من تَعْليمَات الصَنْف TextIO إلى ملف اسمه هو "results.txt" بدلًا من أن تُرسَل إلى الخَرْج القياسي (standard output). لاحظ أنه إذا لم يَكُن هنالك أي ملف يَحمِل نفس الاسم، فسيُنشَئ واحدًا جديدًا أما في حالة وجوده، فستُحذَف محتوياته السابقة دون أي تحذير. بَعْد استدعاء TextIO.writeFile، سيَتَذكَّر الصَنْف TextIO ذلك الملف وسيُرسِل أي خَرْج ناتج عن الدالة TextIO.put أو عن أي دوال خَرْج آخرى إلى ذلك الملف أتوماتيكيًا. إذا أردت أن تعود مجددًا إلى الكتابة إلى الخَرْج القياسي (standard output)، يُمكِنك استدعاء ما يلي: TextIO.writeStandardOutput(); يَطلُب البرنامج بالأسفل من المُستخدِم أن يُجيب على بضعة أسئلة ثم يُخرِج تلك الإجابات إلى ملف اسمه "profile.txt". يَستخدِم البرنامج الصَنْف TextIO لإرسال الخَرْج إلى كُلًا من الخَرْج القياسي (standard output) والملف المذكور، ولكن يُمكِن أيضًا اِستخدَام System.out لإرسال الخَرْج إلى الخَرْج القياسي (standard output). import textio.TextIO; public class CreateProfile { public static void main(String[] args) { String name; // اسم المستخدم String email; // البريد الإلكتروني للمستخدم double salary; // المرتب السنوي للمستخدم String favColor; // اللون المفضل للمستخدم TextIO.putln("Good Afternoon! This program will create"); TextIO.putln("your profile file, if you will just answer"); TextIO.putln("a few simple questions."); TextIO.putln(); /* اقرأ إجابات المستخدم */ TextIO.put("What is your name? "); name = TextIO.getln(); TextIO.put("What is your email address? "); email = TextIO.getln(); TextIO.put("What is your yearly income? "); salary = TextIO.getlnDouble(); TextIO.put("What is your favorite color? "); favColor = TextIO.getln(); // اكتب بيانات المستخدم المدخلة إلى الملف profile.txt TextIO.writeFile("profile.txt"); // يرسل الخرج التالي إلى الملف TextIO.putln("Name: " + name); TextIO.putln("Email: " + email); TextIO.putln("Favorite Color: " + favColor); TextIO.putf( "Yearly Income: %,1.2f%n", salary); /* اطبع رسالة أخيرة إلى الخرج القياسي */ TextIO.writeStandardOutput(); TextIO.putln("Thank you. Your profile has been written to profile.txt."); } } إذا أردت أن تَسمَح للمُستخدِم بأن يختار الملف المطلوب اِستخدَامه للخَرْج، يُمكِنك مثلًا أن تَطلُب منه أن يَكْتُب اسم الملف، ولكن يَكُون ذلك عُرضة للخطأ كما أن المُستخدِمين عادةً ما يكونوا مُعتادين على نوافذ اختيار الملفات، لهذا سنَستخدِم التَعْليمَة التالية: TextIO.writeUserSelectedFile(); ستَعرِض التَعْليمَة السابقة واجهة مُستخدِم رسومية (graphical user interface) عبارة عن نافذة اختيار ملف تقليدية. ستَسمَح تلك النافذة للمُستخدِم بأن يختار ملف خَرْج معين أو بأن يُغلِقها دون اختيار أي ملف. تتميز تلك الطريقة بإمكانية تَنبِيه المُستخدِم في حالة كان مُوشِكًا على استبدال ملف موجود بالفعل. تُعيد الدالة TextIO.writeUserSelectedFile قيمة من النوع boolean ستُساوِي true في حالة اختيار ملف وستُساوِي false في حالة غلق النافذة. قد يَفْحَص برنامج معين القيمة المُعادة (return value) من تلك الدالة ليَعرِف ما إذا كان عليه أن يَكْتُب البيانات إلى ملف أم لا. إلى جانب قراءة المُدْخَلات من "الدَخْل القياسي (standard input)"، يستطيع الصَنْف TextIO القراءة من ملف. ستحتاج فقط إلى تَخْصِيص مصدر المُدْخَلات (input source) الخاصة بالدوال المُعرَّفة بالصَنْف TextIO، وهو ما يُمكِنك القيام به إما باِستخدَام التَعْليمَة TextIO.readFile("data.txt") للقراءة من ملف اسمه "data.txt" أو باِستخدَام TextIO.readUserSelectedFile() والتي تُظهِر واجهة مُستخدِم رسومية (GUI) عبارة عن نافذة اختيار ملف تَسمَح للمُستخدِم بأن يختار ملفًا. بََعْد انتهائك من ذلك، سيُقرَأ أي دَخْل بَعْدها من الملف المُختار لا من مُدْخَلات المُستخدِم. يُمكِنك استدعاء الدالة TextIO.readStandardInput() للعودة مرة آخرى إلى الوضع الافتراضي أي قراءة المُدْخَلات المَكْتُوبة من قِبَل المُستخدِم. إذا كان البرنامج يَقَرأ المُدْخَلات من الدَخْل القياسي (standard input)، فسيَتَمكَّن المُستخدِم إذًا من تصحيح أية أخطاء ضِمْن تلك المُدْخَلات أما إذا كان يَقَرأها من ملف، فلن يَكُون ذلك مُمكِنًا لذا إذا عُثِر على بيانات غَيْر صالحة، سيَحدُث خطأ وسينهار (crash) البرنامج. قد تَحدُث بعض الأخطاء أيضًا عند مُحاولة الكتابة إلى ملف ولكنها تُعدّ أقل شيوعًا. يَتَطلَّب الفهم الكامل للمُدْخَلات والمُخْرَجات بلغة الجافا مَعرِفة بمفاهيم البرمجة كائنية التوجه (object oriented programming)، لذا سنعود مجددًا إلى نفس هذا الموضوع بالفصل الحادي عشر بَعْدما نَكُون قد ناقشنا تلك المفاهيم. تُعدّ إمكانيات الصَنْف TextIO فيما يَتَعلَّق بقراءة الملفات وكتابتها بدائية نوعًا ما، ولكنها كافية لغالبية التطبيقات كما أنها ستَمنَحك بعض الخبرة في التَعامُل مع الملفات. خصائص أخرى بالصنف TextIO تَعرَّضنا لمجموعة من دوال الإِدْخَال المُعرَّفة بالصَنْف TextIO والتي بإِمكانها قراءة قيمة واحدة فقط من سطر مُدْخَل. إذا أردت قراءة أكثر من مُجرّد قيمة واحدة من نفس السطر المُدْخَل، فمثلًا، قد تَرغَب بتَمْكِين المُستخدِم من كتابة سطر مثل "42 17" لكي يُدْخِل العددين ٤٢ و ١٧. يُوفِّر الصَنْف TextIO لحسن الحظ دوال إِدْخَال (input functions) آخرى مُعرَّفة خصيصًا لذلك الغرض. اُنظر الأمثلة التالية: j = TextIO.getInt(); // اقرأ قيمة من النوع int y = TextIO.getDouble(); // اقرأ قيمة من النوع double a = TextIO.getBoolean(); // اقرأ قيمة من النوع boolean c = TextIO.getChar(); // اقرأ قيمة من النوع char w = TextIO.getWord(); // اقرأ كلمة واحدة كقيمة من النوع String تبدأ أسماء الدوال بالأعلى بكلمة "get" بدلًا من كلمة "getln" التي تُعدّ اختصارًا للكلمتين "get line" أي "اِجلب سطرًا" وهو ما ينبغي أن يُذكِّرك بأنها تقرأ سطرًا مُدْخَلًا بالكامل. في المقابل، تقرأ الدوال بدون "ln" قيمة مُدْخَلة واحدة بنفس الطريقة لكنها لا تُهمِل بقية السطر المُدْخَل بل تَحتفِظ به كما هو ضِمْن ذاكرة داخلية تُعرَف باسم "مُخزِّن المُدْخَلات المُؤقت (input buffer)". عندما يحتاج الحاسوب إلى قراءة قيمة مُدْخَلة آخرى، فإنه سيَفْحَص أولًا مُخزِّن المُدْخَلات المؤقت قبلما يَطلُب من المُستخدِم إِدْخِال أي قيم جديدة، مما سيَسمَح بقراءة عدة قيم من سطر مُدْخَل واحد. بتعبير أكثر دقة، يقرأ الحاسوب دومًا من مُخزِّن المُدْخَلات المؤقت، ففي المرة الأولى التي سيُحاوِل فيها البرنامج قراءة قيمة مُدْخَلة من المُستخدِم، سينتظر الحاسوب إلى أن ينتهي المُستخدِم من كتابة سطر كامل من المُدْخَلات، ثُمَّ سيَحتفِظ الصَنْف TextIO بذلك السطر ضِمْن مُخزِّن مُدْخَلات مُؤقت (input buffer) إلى أن يُقرأ أو يُهمَل بواسطة إحدى دوال "getln". سيَضطّر المُستخدِم إلى إِدْخَال سطر جديد فقط عندما يُصبِح مُخزِّن المُدْخَلات المُؤقت (buffer) فارغًا. ستَتَخطَّى دوال الإِدْخَال (input functions) المُعرَّفة بالصَنْف TextIO أية فراغات (spaces) أو محارف عودة إلى بداية السطر (carriage returns) عند محاولتها قراءة القيمة المُدْخَلة التالية، ولكنها مع ذلك لن تَتَخطَّى أية محارف آخرى. فمثلًا، إذا أَدْخَل المُستخدِم السطر "42,17" بينما كنت تُحاوِل قراءة قيمتين من النوع int، فسيَتَمكَّن الحاسوب من قراءة العدد الأول بشكل سليم، ولكنه عندما يُحاوِل قراءة العدد الثاني، فستُقابله فاصلة ,، وهو ما يُعدّ خطأ لذا سيَطلُب الحاسوب من المُستخدِم أن يُعيد إدخال العدد الثاني أي إذا أردت أن تَسمَح للمُستخدِم بأن يُدْخِل عدة قيم ضِمْن سطر واحد، فينبغي أن تُخبره بأن عليه أن يَستخدِم فراغًا لا فاصلة بين كل عدد والذي يليه. أما إذا كنت تُفضِّل السماح له باِستخدَام فاصلة (comma)، فاِستخدِم الدالة getChar() لقرائتها أولًا قَبْل أن تُحاوِل قراءة العدد الثاني. يُوفِّر الصَنْف TextIO دالة آخرى لإِدْخَال محرف واحد هي TextIO.getAnyChar() والتي لا تَتَخطَّى أية محارف نهائيًا، فهي ببساطة تقرأ المحرف التالي المُدْخَل من قِبَل المُستخدِم حتى لو كان ذلك المحرف فراغًا أو محرف عودة إلى بداية السطر (carriage return). فمثلًا، إذا أَدْخَل المُستخدِم محرف عودة إلى بداية السطر، فسيَكُون المحرف المُعاد من الدالة getAnyChar() عبارة عن محرف إضافة سطر جديد \n. يُوفِّر الصَنْف TextIO دالة (function) آخرى هي TextIO.peek() تَفْحَص المحرف المُدْخَل التالي دون قرائته بصورة فعلية أي أنه سيَظِلّ متاحًا للقراءة حتى بَعْدما تَفْحَصه باِستخدَام تلك الدالة، وعليه ستَتَمكَّن من مَعرِفة قيمة المُدْخَل التالي وربما حتى اتِّخاذ إِجراءات مختلفة وفقًا لقيمته. يُوفِّر الصَنْف TextIO مجموعة آخرى من الدوال (functions) يُمكِنك الإطلاع عليها بملف الشيفرة المصدرية TextIO.java كما أنها مرفقة بتعليقات (comments) للمساعدة. يتَّضح لنا أن المُدْخَلات أكثر تعقيدًا من المُخْرَجات إلى حد ما، ولكن يُعدّ اِستخدَامها بسيطًا بأغلب التطبيقات من الناحية العملية أما إذا كنت تُحاوِل القيام بشيء مميز، فقد تحتاج إلى المزيد من التفاصيل. يُنصَح عمومًا بالاعتماد على برامج الإِدْخَال (input routines) التي يَحتوِي اسمها على كلمة "getln" وليس كلمة "get" لأنها أبسط بكثير إلا إذا كانت هنالك حاجة لقراءة أكثر من قيمة ضِمْن سطر مُدْخَل واحد. استخدام الصنف Scanner للمدخلات يُسهِل الصَنْف TextIO من قراءة مُدْخَلات المُستخدِم، ولكن ينبغي أن تُتيِح الملف TextIO.java لأي برنامج يَستخدِمه لأنه ليس صَنْفًا قياسيًا (standard class). في المقابل، يُعدّ الصَنْف Scanner جزءًا قياسيًا من الجافا أي سيَكُون متاحًا دائمًا أينما أردت اِستخدَامه لذا قد تُفضِّل اِستخدَامه. يُستخدَم الصَنْف Scanner لقراءة مُدْخَلات المُستخدِم، ويتميز في العموم بسمات رائعة، ولكن ربما يَكُون اِستخدَامه صعبًا نوعًا ما حيث يَتَطلَّب مَعرِفة ببعض قواعد الصيغة (syntax) التي لن نَتَعرَّض لها حتى نَصِل إلى الفصلين الرابع والخامس. سنَذكُر هنا طريقة اِستخدَامه سريعًا دون الخوض بأية تفاصيل، لذا لا تَقْلَق إن لم تفهم كل ما هو مَكْتُوب هنا. سنناقش الصَنْف Scanner تفصيليًا بالقسم الفرعي ١١.١.٥. أولًا، لمّا كان الصنف Scanner مُعرَّف بحزمة java.util، ينبغي أن تُضيف مُوجّه (directive) الاستيراد import التالي إلى بداية ملف الشيفرة بالبرنامج الخاص بك أي قَبْل كلمة public class: import java.util.Scanner; ثم سنُضِيف التَعْليمَة التالية إلى بداية البرنامج main(): Scanner stdin = new Scanner( System.in ); تُنشِي تلك التَعْليمَة مُتْغيِّرًا (variable) اسمه stdin من النوع Scanner. تُعدّ كلمة stdin اختصارًا للكلمتين "standard input" مما يَعنِي "دَخْل قياسي"، ولكن يُمكِنك أن تَستخدِم أي اسم آخر للمُتْغيِّر إن أردت. تستطيع اِستخدَام المُتْغيِّر stdin للوصول إلى تشكيلة من البرامج الفرعية (subroutines) المُختصَّة بقراءة مُدْخَلات المُستخدِم، فمثلًا، تقرأ الدالة stdin.nextInt() قيمة واحدة مُدْخَلة من النوع int ثُم تُعيدها كقيمة للدالة أي تُشبه TextIO.getInt() إلى حد كبير باستثناء أمرين: أولًا، إذا كانت القيمة المُدْخَلة مِن قِبَل المُستخدِم غَيْر صالحة، فلن تَطلُب الدالة stdin.nextInt() من المُستخدِم أن يُعيد إِدْخَال قيمة جديدة وإنما ستَتسبَّب بحُدوث انهيار (crash). ثانيًا، لابُدّ من أن يُدْخِل المُستخدِم فراغًا أو محرف نهاية السطر بَعْد قيمة العدد الصحيح المُدْخَلة. في المقابل، تَتَوقَّف الدالة TextIO.getInt() عن قراءة المُدخلات مع أول محرف لا يُمثِل رقمًا تُقابِله. علاوة على ذلك، تَتَوفَّر توابعًا (methods) لقراءة الأنواع الآخرى مثل stdin.nextDouble() و stdin.nextLong() و stdin.nextBoolean() (تَقبَل الدالة stdin.nextBoolean() القيم true و false فقط). تقرأ تلك البرامج الفرعية (subroutines) أكثر من قيمة واحدة ضِمْن سطر لذا فهي أَشْبه للنُسخ "get" من البرامج الفرعية المُعرَّفة بالصَنْف TextIO وليس النُسخ "getln"، فمثلًا، يُعدّ التابع stdin.nextLine() مكافئًا للتابع TextIO.getln() أما التابع stdin.next() فيُكافِئ TextIO.getWord() حيث يُعيد كلاهما سِلسِلة نصية (string) من المحارف غَيْر الفارغة. يَستعرِض المثال التالي البرنامج Interest2.java مجددًا ولكن باِستخدَام الصَنْف Scanner بدلًا من الصَنْف TextIO: import java.util.Scanner; public class Interest2WithScanner { public static void main(String[] args) { Scanner stdin = new Scanner( System.in ); // أنشئ كائن جديد double principal; // قيمة الاستثمار double rate; // معدل الفائدة السنوي double interest; // الفائدة المكتسبة خلال العام System.out.print("Enter the initial investment: "); principal = stdin.nextDouble(); System.out.print("Enter the annual interest rate (as a decimal): "); rate = stdin.nextDouble(); interest = principal * rate; // إحسب فائدة العام principal = principal + interest; // أضف إلى الأساسي System.out.printf("The amount of interest is $%1.2f%n", interest); System.out.printf("The value after one year is $%1.2f%n", principal); } // نهاية main() } // نهاية الصنف Interest2WithScanner بالبرنامج أعلاه، أضفنا سطرًا لاِستيراد الصَنْف Scanner وآخرًا لإِنشاء الكائن stdin. علاوة على ذلك، بدلًا من التابع TextIO.getlnDouble()، اِستخدَمنا stdin.nextDouble()، والذي يُعدّ في العموم مُكافئًا للتابع TextIO.getDouble() ولكن كل ذلك لا يَعنِينا طالما كان المُستخدِم سيُدْخِل عددًا واحدًا فقط بكل سطر. سنستمر بالاعتماد على الصَنْف TextIO فيما يَتَعلَّق بالمُدْخَلات في العموم، ولكننا سنَستخدِم الصَنْف Scanner مرة آخرى بعدة أمثلة ضِمْن التمارين المُرفقة بنهاية الفصل كما سنُناقِشه تفصيليًا فيما بَعْد. ترجمة -بتصرّف- للقسم Section 4: Text Input and Output من فصل Chapter 2: Programming in the Small I: Names and Things من كتاب Introduction to Programming Using Java.1 نقطة
-
مرحباً عبد العزيز ليس بالضرورة تعلمهما بترتيب معين فكِلاهما مجالين مختلفين فقاعدة البيانات أو المعطيات هي مجموعة من البيانات و المعلومات مخزنة بترتيب معين و بشكل منظم في ملف ضخم حيث يمكن إسترجاع هذه البيانات و التعديل عليها و حتى حذف جزء منها في أي وقت. تنقسم قواعد البيانات من حيث طبيعة التركيب إلى عدة أنواع من بينها قواعد البيانات العلائقية و سُميت بهذا الشكل نظراً لتواجد علاقة بين السجلات و البيانات. حتى نستطيع التعامل مع قواعد البيانات العلائقية نحتاج إلى إستعمال نظام إدارة أو ما يُسمى ب RDBMS و هو إختصار ل ( Relational Database Management System ) أي نظام إدارة قواعد البيانات العلائقية و يوجد العديد من هذه النُظم أذكر منها ( MySQL ، mSQL ، PostgresSQL ، Oracle و غيرها ..) هذا النظام حتى يتعامل مع قواعد البيانات يعتمد على لغة و هذه اللغة إسمها SQL و هي إختصار ل Structured Query Language وهى عبارة عن لغة تُستخدم في معالجة البيانات المخزنة و التعامل معها. هذا بإختصار عن قواعد البيانات العلائقية لأن مجال قواعد البيانات كبير أما بخصوص لغة البرمجة بغض النظر عن ماهية اللغة سواء كانت جافا , Php , بايثون , روبي .. فعند تعلمك لأساسيات اللغة لن تحتاج إلى قواعد البيانات أما إذا أردت إنشاء برنامج و أحتجت فيه إلى التعامل مع قاعدة بيانات ستجد أن مطورين اللغة قد وَفروا لك مكتبات و كلاسات أو حزم جاهزة و عند إستعمالك لهذه المكتبات ستقوم بكتابة إستعلامات و هذه الإستعلامات هي نفسها المكتوبة بلغة SQL ففي هذا الجزء تحتاج معرفة عن كيفية كتابة الإستعلامات أما بخصوص أساسيات اللغة فأثناء تعلمها لا تحتاج معرفة بهذا الجزء بالتوفيق1 نقطة
-
إنّ ما ترغب فيه عندما تحتاج إلى إنشاء منتج برمجي ولكن خبرتك في هندسة البرمجيات محدودة هو التعهيد الخارجي للبرمجيات. إنها ممارسة تجارية ذكية يستخدمها الجميع بدءًا من أصحاب الألف دولار الذين يملكون مواقع الويب الشخصية وانتهاء بأصحاب الثروات الكبيرة؛ وكلهم يفشلون إلى حد ما. في الواقع، من الصعب جدًا أن لا تفشل. لذا أعددت فيما يلي قائمة من التلميحات البسيطة لكل من يقرّر التعهيد الخارجي لتطوير البرمجيات (يوجد أهمها في الأسفل). ولكم تمنيت لو أن شخصًا ما قد أعطاها لي منذ 15 عامًا. لتكن لديك اتفاقية "العمل مقابل أجر" تأكد من أن العقد الذي أبرمته مع الفريق المُتعهِّد لإنشاء البرامج يشتمل على شيء من هذا القبيل: "يجب على الأطراف اعتبار جميع التسليمات التي أنشأها المطور بمثابة أعمال أُنجِزت للتأجير كما هو محدد بموجب قوانين حقوق الطبع والنشر". هذا هو التعريف المختصر والسهل لعبارة "كل ما تنجزه لي هو ملكي". ضع هذا في العقد ولن تستطيع شركة التعهيد الخارجي ادعاء أن البرنامج الذي أنشأته مِلكها، وهو ما يحدث هنا وهناك. احرص على امتلاك مستودعٍ خاصٍّ بك للشيفرة المصدرية تأكد من أن مستودع الشيفرة المصدرية تحت سيطرتك. وأفضل طريقة للقيام بذلك هي إنشاء مستودع GitHub خاصٍّ. سيكون المخزون مرئيًا ولا يمكن الوصول إليه إلّا من خلالك أنت وفريق التعهيد الخارجي. علاوة على ذلك، تأكد من أن الفريق لديه صلاحية القراءة فقط ولا يمكنه تغيير الشيفرة مباشرة إلا من خلال طلبات السحب (pull requests)، لأن Git يتيح إمكانية تدمير السجل بأكمله بضغطة واحدة "قسرية" على الفرع الرئيسي (master branch). لذلك سيكون من الأفضل لك أن تكون الشخص الوحيد الذي لديه صلاحية الكتابة. لجعل الأمور أكثر بساطة، أنصحك باستخدام Rultor كأداة لدمج طلبات السحب هذه بشكل شبه تلقائي. اجمع الإحصائيات بانتظام اطلب من أعضاء فريق التعهيد الخارجي جمع الاحصائيات بانتظام من البرنامج الذي يقومون بإنشائه وإرسالها إليك بطريقة أو بأخرى (عبر البريد الإلكتروني ربما). أنصح باستخدام Hits-of-Code ومدى تغطية اختبارات الوحدة (أو فقط إجمالي عدد اختبارات الوحدة) والتذاكر المفتوحة والمغلقة ومدة الإنشاء. أنا أتحدث هنا عن مقاييس عملية. وهو ما لن تحصل عليه فعليًا باستخدام أدوات أخرى مثل NewRelic. ستحدّد هذه المقاييس أداء الفريق، وليس المنتج قيد التطوير. لا أقول أنه يجب عليك إدارة الفريق من خلال المقاييس، ولكن عليك أن تراقب هذه الأرقام وديناميكيتها. إجراء مراجعات فنية مستقلة تتجلى أهمية هذه المراجعات في صعوبة تضخيمها أو التحايل فيها. إنها حاسمة بشكل استثنائي عندما يتعلق الأمر بالتعهيد الخارجي للبرمجيات. في الواقع، هذه هي الطريقة الأفضل لجمع المعلومات الموضوعية عن البرنامج الذي تحصل عليه من جهة خارجية. لا تعتمد على التقارير والوعود والضمانات والتوثيقات الشاملة. بدلاً من ذلك، استأجر شخصًا آخر بنظام الساعة واطلب منه مراجعة ما يطوّره شريك الخارجي. قم بهذ المراجعات بانتظام وبشكل منهجي. لا تخف من الإساءة إلى المبرمجين، واشرح لهم بصراحة أهمية المراجعة بالنسبة لك. إذا كانوا مهنيين، فسوف يفهمون ويحترمون أهداف عملك. يمكنك أيضا أن تريهم هذه المقالة :-). أتمتة النشر والتحكم فيه اطلب من الفريق المُتعهِّد أتمتة آلية النشر بالكامل وإبقاءها تحت سيطرتك. وأنصح بأن تفعل هذا خلال الأيام الأولى للمشروع. هذا يعني أنه ينبغي تجميع المنتج واختباره وتعبئته وتثبيته ونشره في مستودع الإنتاج (أو على الخادم/الخوادم) بنقرة واحدة. ستحتاج بالطبع إلى إنشاء سكريبت لأتمتة سلسلة العمليات هذه ويجب على شريكك الخارجي أن يكتبه لك. ثم، عند بدء عملية التطوير، يُنفّذ السكريبت تلقائيًا في كل مرة يجري فيها تعديل جديد على المستودع الذي ينشر الإنتاج. المهم هنا هو أن تعرف كيفية عمل هذا السكريبت وكيفية تشغيله حتى تكون قادرًا على بناء منتجك ونشره بنفسك. اطلب إصدارات أسبوعية لبرنامجك لا تنتظر الإصدار النهائي؛ بل اطلب من الفريق المُتعهِّد إصدار نسخة جديدة كل أسبوع. بغض النظر عن مدى كثافة التطوير وعدد الميزات الجاري تنفيذها، من الممكن دائمًا تجميع نسخة جديدة وإصدارها. إذا كان التطوير مكثفًا فعلاً، فاطلب من فريقك استخدام GitFlow أو شيء مشابه لعزل فرع الإنتاج المستقر عن باقي الفروع. لكن احذر من طول الانتظار! احرص على رؤية برنامجك مجمّعًا ومنشورًا كل أسبوع، بدون استثناءات ولا أعذار. إذا لم يتمكن الفريق المُتعهِّد من تقديم ذلك لك، فهذا يدعوك للقلق وإلى تغيير شيء ما. وظِّف مديرًا تقنيًا تنفيذيًا (CTO) مستقلًا تُقدّم هذه النصيحة في الغالب للشركات الصغيرة أو الأفراد الذين يستعينون بفريق خارجي لتطوير البرمجيات ويعتمدون على خبراتهم مع الاستمرار في التركيز على تطوير أعمالهم. هذا ليس من الحكمة في شيء؛ يجب أن يكون لديك مدير تقني تنفيذي مستقل (CTO) يقوم بإبلاغك ويتحكم في كيفية عمل الفريق المُتعهِّد. ينبغي التعاقد مع هذا الشخص وفقًا لجدول دفع مختلف مع أهداف وشروط وأهداف مختلفة. في كثير من الأحيان، يحاول أصحاب الأعمال التذاكي في البرمجيات والتحكم في فريق البرنامج مباشرةً، ويتعلمون أساسيات لغة المصطلحات البرمجية ومبادئ DevOps، لكن هذا يقود حتمًا إلى الفشل. كن ذكيا، فأنت تقوم بتطوير الأعمال، سيكون تعاملك المباشر مع مدير التقنية CTO ويتولى هو توجيه الفريق المُتعهِّد ومراقبته؛ بحيث يرسل CTO لك التقارير عن سير العمل، ويرسل له أعضاء الفريق التقارير التقنية بدورهم. حدّد آليةً للمكافآت والعقوبات لا توجد إدارة بدون هذين المكونين الرئيسيين. ليس من المفترض أن تدير جميع المبرمجين في الفريق المُتعهِّد، بل عليك إدارة الفريق بأكمله كوحدة تحكم واحدة. عليك أن تمنحهم نوعًا من التحفيز. ينبغي أن يعرفوا ماذا سينالون إذا نجحوا وكم سيعانون إذا فشلوا. إذا لم توضح هذه الآلية، فستُضطر للتعامل معها ضمنيًا، إذ أن فرص الرّبح تكون منخفضة جدًا. يفترض معظم الناس أن أفضل حافز والحافز الوحيد لفريق البرمجيات هو استمراره في العمل على المشروع. أنت تدفع لهم وهذا يكفي، أليس كذلك؟ هذا خطأ. لا يمكن أن تكون الإدارة فعالة عندما يكون التحويل المصرفي الشهري بمثابة مكافأة وغيابها يعد عقابًا. إنه أسلوب فجّ، وهذا ما يجعله عديم الفعالية إطلاقًا. ابحث عن آلية أفضل وأكثر لباقة وبالتالي أكثر فعالية. لا تقض أكثر من شهر في المشروع يقول بعض الناس 12 أسبوعًا، وأنا أقول شهرًا، ولست وحدي من يقول ذلك. هل تعلم أن الإصدار الأول من Minecraft (الذي بِيعَ لاحقًا إلى Microsoft مقابل 2.5 مليار دولار) تم إصداره في غضون ستة أيام فقط؟ هناك العديد من الأمثلة الأخرى، بما في ذلك Uber و Dropbox و Buffer وغيرها. لقد عبّر إريك ريس في كتابه Lean Startup عن هذا المفهوم بعبارة "منتج الحد الأدنى القابل للتطبيق" (Minimum Viable Product وتختصر إلى MVP)، وأنا متأكد من أنك سمعت هذا الاختصار من قبل. إذا كان المطورون يعدون بتسليم المنتج في غضون بضعة أشهر، فهناك شيء ليس على ما يرام. ينبغي حتمًا أن تحصل على بعض البرامج العاملة في أقل من شهر وينبغي أن تكون جاهزًا للمستخدمين الذين يدفعون رسومًا حقيقية. لقد أنشأت معظم مشاريعي اللطيفة في أقل من أسبوع لكل منها. لا تدفع الرواتب سيريدون منك بطبيعة الحال أن تدفع شهريًا، بالإضافة إلى التأمين الصحي، وأجهزة الحاسوب، والإجازات، ومكتب مشمس لطيف. وهذا هو ما يمكن أن تضيع فيه مخططاتك، إذ أنك تجعلهم سعداء بينما تخسر أنت. عليك إيجاد طريقة لملاءمة أهدافك (تسليم MVP في أقرب وقت ممكن والبدء في جني الأموال) مع أهدافهم (شراء سيارة جديدة وقضاء الإجازة التالية على الشاطئ). هل تستطيع فعل ذلك؟ إنه أمر صعب. لهذا السبب أنشأنا Zerocracy، مما يجعل الفواتير التدريجية والدفع بالنتائج أمرًا ممكنًا. يمكنك إما نقل مشروعك إلى هذه المنصة وإدارة مطوريك هناك، أو ابتكار شيء ما بمفردك. ولكن تذكر، ليست هناك رواتب شهرية! تدفع فقط مقابل النتائج المقدمة. لا تدعهم يجربون يريد كل مبرمج ذكي استخدام مشروعك الجديد كإجراء اختبار لبعض التقنيات الجديدة. لا يحب الناس أن يكرّروا نفس الشيء الذي كانوا يفعلونه بالأمس، إلا إذا كانوا أغبياء ومملين. ولهذا، سيَنصح أعضاء فريقك غالبا باستخدام شيء جديد، قد يكون إطار عمل جديدًا أو قاعدة بيانات جديدة أو حلّ استضافة سحابية جديدًا أو أدوات نشر جديدة. إنهم يفعلون ذلك لأغراضهم الخاصة، وليس لمساعدة المشروع. لا تسقط في هذا الفخ وكن مصرًّا على استخدام ما لديهم من خبرة، على الأقل في مرحلة MVP. يمكنك أن تعدهم بحرية التجربة، ولكن في وقت لاحق عندما يتم إطلاق النسخة MVP. ترجمة -وبتصرف- للمقال Software Outsourcing Survival Guide لصاحبه Yegor Bugayenko1 نقطة