تنبيه : هذه المقالة مُترجمة و بالإمكانكم الإطلاع على المقاله الأصلية في مدونة فكتور جو و عنوانها
An Introduction to Neural Networks
نسمع كثيرا عن مصطلح الـ Neural Network أو الشبكات العصبية بالعربية . و هذا المصطلح قد يعطي الإنطباع بالصعوبة و التقيد و لكن الأمر أسهل بكثير مما قد يتخيلة الكثيرين. و من خلال هذا المقال سنحاول أن نفهم ما هي الشبكات العصبية و سنقاوم كذلك ببنائها من الصفر بإستخدام البايثون.
1- حجر الأساس : العصبونات (Neurons)
الوحدة الأساسية للشبكات العصبية ( Neural Network ) تعرف بأسم العصبونه (Neurons ) و مهمتها هي أن تأخذ بعض المدخلات و تجري عليها عدة عمليات حسابية و تنتج لنا نتيجة أو مخرج .
هناك ثلاثة أمور تحدث داخل العصبونه (Neuron ) في الصورة السابقة :
أولاً : كل مدخل تم ضربه بـ وَزن (Weight):
ثانياً : كل المدخلات الموزونه تم جمعها مع بعضها و إضافة إنحياز (Bias ) لها :
ثالثاً : المجموع تم تمريره بدالة تنشيط (Activation Function) :
الهدف و الحكمة من دالة التنشيط ( Activation Function ) هو تحويل مدخل عير مُقيد إلى مخرج مقيد ذا بشكل يمكن التبؤ به. و أحد أكثر دوال التنشيط رواجاً هي دالة السيجمويد (Sigmoid Function )
مخرجات دالة السيجمويد تتمثل في أرقام بين 0 و 1 حيث تقوم الدالة بتحويل أي رقم سلبي كبير إلى ~0 و أي رقم موجب كبير إلى ~1 فبالتالي يمكن تصور هذه الدالة بأنها تقوم بضعط أو عصر (-∞ , ∞ ) إلى ( 0 , 1 ) .
مثال للتوضيح
فلنفترض أن لدينا عصبونه بمدخلين و كذلك نستخدم دالة السيجمويد مع المعطيات التالية
هي طريقة أخرى لكتابة في هيئة المتجهات (Vector Form) ، و الأن لنفترض أن مدخلات العصبونه لديها هذه القيم [X=[2,3 سنستخدم الضرب النقطي (Dot product ) لتبسيط كتابة المعادلات و سهولة قرائتها
و بالتالي مُخرج العصبونه سيكون 0.999 في حال كانت قيم المدخلات X=[2,3]
و تسمى هذه العملية بالتغذية الأمامية (Feed Forward ) .
برمجة العصبونه ( Neurons) :
في هذه الجزئية نستخدم نمباي ( NumPY ) و هي مكتبة حسابية في البايثون :
import numpy as np def sigmoid(x): # دالة التفعيل return 1 / (1 + np.exp(-x)) class Neuron: def __init__(self, weights, bias): self.weights = weights self.bias = bias def feedforward(self, inputs): # ضرب الأوزان مع المدخلات ثم جمعها مع الإنحياز total = np.dot(self.weights, inputs) + self.bias # إستخدام دالة السيجمويد return sigmoid(total) # إستهلال الأوزان و الإنحياز weights = np.array([0, 1]) # w1 = 0, w2 = 1 bias = 4 # b = 4 n = Neuron(weights, bias) # قيم مدخلات العصبونه x = np.array([2, 3]) # x1 = 2, x2 = 3 # ناتج الشبكة print(n.feedforward(x)) # 0.9990889488055994
و كما نرى فالنتيجة مشابة للنيجة التي حصلنا عليها سابقا.
2- ضم العصبونات لتكوين شبكة عصبية :
الشبكة العصيبة (Neural Network ) ليست سوى مجموعة من العصبونات الموصولة ببعضها البعض . و هنا مثال لشبكة عصبية بسيطة :
هذه الشبكة تتألف من مُدخلين ثم طبقة مخفية ( Hidden Layer ) تتألف من عصبونين ( h1 و h2 ) بالإضافة إلى طبقة المخرجات و التي تحتوي على عصبون (o1) ، من الجدير بالملاحظة أن مُدخلات O1 هي مُخرجات كٍلا العصبونين h1 و h2 و هذا ما يشكل شبكة.
الطبقة الخفية : هي أي طبقة تقع بين طبقة المدخلات ( الطبقة الأولى) و طبقة المخرجات ( الطبقة الأخيرة ).
مثال : التغذية الأمامية (Feed Forward) :
دعونا نستخدم الشبكة أعلاه و لنتفرض أن كل العصبونات ( Neurons ) لديها نفس الأوزان w=[0,1] و نفس الإنحياز b = 0 و نستخدم دالة السيجمويد في جميعها . و لتمثل h1 , h2 ,o1 مخرجات العصبونات التي تمثلها .
ماذا سيحدث لو مررنا المدخلين x = [2,3] على هذه الشبكة ؟
وبالتالي نجد أن الشبكة العصبية ستعطينا نتيجة 0.7216 إذا ما كانت مدخلاتها x=[2,3]
الجدير بالذكر بأن الشبكة العصبية قد تتألف من أي عدد من الطبقات و أي عدد من العصبونات في كل طبقة و لكن تبقى الفكرة نفسها نغذي الشبكة بالمُدخلات من المقدمة عن طريق العصبونات من أجل الحصول على مُخرجات في النهاية . من أجل التبسيط سنستخدم نفس الشبكة في الصورة أعلاه.
برمجة الشبكة العصبية : التعذية الأمامية
فلتقم الأن ببرمجة التقذية الأمامية للشبكة العصبية ، هذه صورة الشبكة العصبية كمرجع.
import numpy as np # كود من الجزئية السابقة class OurNeuralNetwork: ''' :الشبكة العصبية لديها - مدخلين - (h1, h2) طبقة خفية تحتوي على عصبونين - (o1) طبقة مخرجات تحتوي على عصبون واحد :كل عصبون لديه نفس الأوزان و الإنحياز - w = [0, 1] - b = 0 ''' def __init__(self): # إستهلال قيم الأوزان و الإنحياز weights = np.array([0, 1]) bias = 0 # تم إنشاءه في الجزئية السابقة Neuron class self.h1 = Neuron(weights, bias) self.h2 = Neuron(weights, bias) self.o1 = Neuron(weights, bias) def feedforward(self, x): out_h1 = self.h1.feedforward(x) out_h2 = self.h2.feedforward(x) # h2 , h1 هي مخرجات o1 مدخلات out_o1 = self.o1.feedforward(np.array([out_h1, out_h2])) return out_o1 network = OurNeuralNetwork() x = np.array([2, 3]) print(network.feedforward(x)) # 0.7216325609518421
و حصلنا على نفس النتيجة 0.7216 .
3- تدريب الشبكة العصبية: الجزء الأول
لدينا القياسات التالية :
الأسم | الوزن ( كج) | الطول (سم) | الجنس |
إيمان | 70 | 165 | أنثى |
محمد | 97 | 172 | ذكر |
عبد الفتاح | 89 | 170 | ذكر |
أمال | 57 | 160 | أنثى |
نرغب في تدريب الشبكة للتتنبأ بجنس الشخص بناءً على وزنه و طوله
سنمثل الذكر بـ 0 و الأنثى بـ 1 و كما سنقوم تحريك البيانات لنجعل قرائتها أسهل .
الأسم | الوزن (ناقص 72) | الطول ( ناقص 166) | الجنس |
إيمان | -2 | -1 | 1 |
محمد | 25 | 6 | 0 |
عبد الفتاح | 17 | 4 | 0 |
أمال | -15 | -6 | 1 |
هنا قمنا بإختيار قيم التحريك ( 72و 166) و هي أرقام قمت بإختيارها من عندي لجعل البيانات أسهل للقراءة و التوضيح لكن في العادة نقوم بتحريك البيانات بناءً على المتوسط (mean ) .
الخسارة (Loss) :
قبل أن ندرب الشبكة العصبية نحن بحاجة لطريقة نخبر بها الشبكة العصبية عن جودة أدائها و إذا ما كان هناك إمكانية لتقوم بعمل أفضل . و هذه وظيفة دالة الخسارة ( Loss function )
هنالك العديد من دوال الخسارة و هنا سنستخدم دالة تعرف بـ متوسط خطأ التربيع ( Mean Square Error MSE )
فلنقم بتفصيل هذه المعادلة
- n هو عدد الأمثلة ، و هنا لدينا أربعة
- y تمثل المتغير الذي نرغب بالتنبؤ به
- Ytrue تمثل القيمة الحقيقية للمتغير ( الجواب الصحيح ) كمثال Ytrue بالنسبة لإيمان تعتبر 1 (أنثى)
- Ypred تمثل القيمة المتنبأ بها للمتغير و هي قيمة المخرجات من الشبكة
تعرف بـ خطا التربيع ( Squared error ) و ببساطة كل ما تقوم به دالة الخسارة هي حساب المتوسط من ناتج كل الأخطاء التربيعية و لذلك تسمى بـ متوسط خطأ التربيع ( Mean square error ) و كلما كانت تنبؤات الشبكة أفضل كلما كانت قيمة الخسارة قليلة .
و بالتالي : تبؤات أفضل = خسارة أقل .
مثال على حساب الخسارة:
فلنفترض أن مخرجات شبكتنا كانت 0 بمعنى أن الشبكة واثقة بأن كل البشر هم ذكور ، في هذه الحالة ماذا ستكون الخسارة
الأسم | ytrue | ypred | ytrue−ypred2 |
إيمان | 1 | 0 | 1 |
مدثر | 0 | 0 | 0 |
عبد الفتاح | 0 | 0 | 0 |
أمال | 1 | 0 | 1 |
و هذه الخسارة تعتبر كبيرة جداً
برمجة دالة الخسارة :
هنا سنقوم ببرمجة دالة الخسارة :
import numpy as np def mse_loss(y_true, y_pred): return ((y_true - y_pred) ** 2).mean() y_true = np.array([1, 0, 0, 1]) y_pred = np.array([0, 0, 0, 0]) print(mse_loss(y_true, y_pred)) # 0.5
و النتيجة هي 0.5 .
4- تدريب الشبكة العصبية: الجزء الثاني
الأن لدينا هدف واضح كالشمس و هو تقليل خسارة الشبكة العصبية . نعلم بأنه بمقدورنا تغيير أوزان الشبكة و الإنحياز من أجل التأثير على التنبؤات . ولكن السؤال هو كيف نفعل ذلك بطريقة تقلل من الخسارة.
الأسم | الوزن (ناقص 72) | الطول ( ناقص 166) | الجنس |
إيمان | -2 | -1 | 1 |
و بالتالي متوسط الخطأ التربيعي ( mean square error ) سيصبح الخطأ التربيعي لإيمان سيصبح
طريقة أخرى للتفكير في دالة الخسارة هي بإعتبارها دالة الأوزان و الإنحيازان . و بالتالي دعونا نميز كل الأوزان و الإنحيازات في شبكتنا :
و الأن يمكننا أن نكتب الخسارة (Loss) كدالة متعددة المتغيرات ( Multivariable Function ) :
فلنفترض أننا نرغب بتعديل w1 فكيف سيأثر ذلك على دالة الخسارة L ؟ جواب هذا السؤال يمكن الوصول إليه بإستخدام المشتقات الجزئية لــ
حتى نبدأ بالحساب علينا أولا أن نقوم بإعادة ترتيب المشتقة الجزئية نسبة إلى
هنا قمنا بإستخدام قاعدة التسلسل ( Chain rule )
بإمكاننا حساب المشتقة لأنه سبق و قمنا و بحساب
الأن نحن بحاجة لأن نتعامل مع و كما فعلنا سابقاً فلنفرض بأن h1 , h2 ,o1 هم مخرجات العصبونات اللاتي يمثلُهن ، و بالتالي :
F هنا هي دالة السيجمويد للتنشيط و هي التنبؤ المطلوب و المعروف أيضاً بالمُخرج o1
بما أن w1 تؤثر فقط على h1 يمكننا كتابة الأتي :
و سنقوم بالمثل مع :
و بالتالي
x1 هنا تمثل الوزن و x2 تمثل الطول . هذه المرة الثانية التي نرى فيها f'(x) ( والتي تمثل المشتقة لدالة السيجمويد ) ، لذلك هيا نشتقها :
سنستخدم هذه الصيغة لـ f'(x) لاحقا.
و بذلك نكون قد إنتهينا من جميع المعادلات الرياضية و حساباتها بحيث تمكنا من تقسيم لعدة أجزاء بإمكاننا بسهولة حسابتها :
ملاحظة :نظام حساب المشتقات الجزئية عكسيا بمعنى البدء من النهاية و الرجوع للوراء يعرف بـ الإنتشار العكسي (BackPropagation) .
مثال لحساب المشتقات الجزئية :
فلنفترض أن لدينا إيمان فقط في بياناتنا
الأسم | الوزن (ناقص 72) | الطول ( ناقص 166) | الجنس |
إيمان | -2 | -1 | 1 |
فلنقم أولا بإستهلال قيم الأوزان بـ 1 و قيم الإنحيازات بـ 0 . و من ثم نقوم بعملية التغذية الأمامية خلال الشبكة كاملة و بالتالي سنحصل على
و هنا نجد بأن الشبكة أنتجت لنا وهذه النتيجة في المنتصف فهي لا تفضل الذكر (1) أو الأنثى (0)
و الأن نقوم بحساب المشتقة :
للتذكير فقد قمنا سابقاً بإشتقاق دالة السيجمويد بهذه الطريقة .
و هذا يخبرنا بأنه لو زدنا قيمة w1 فستزيد قيمة الخسارة بشكل طفيف
5- تدريب النزول الإشتقاقي العشوائي ( Stochastic Gradient Descent ) :
الأن أصبح بحوزتنا جميع الأدوات المطلوبة لنقوم بتمرين شبكة عصبية ، و سنقوم بإستخدام خوارزمية تحسين (Optimization Algorthim ) تسمى النزول الإشتقاقي العشوائي ( Stochastic Gradient Descent ) أو SGD بإختصار ، و هذه الخوارزمية تقوم بإخبارنا كيف نغير قيم الوزن و الإنحياز حتى نقلل الخسارة وهذا هو أهم شيئ بالنسبة لنا . و ببساطة هي معادلة التحديث التالية :
هنا تمثل عدد ثابت يسمى معدل التعلم (Learning rate) و وظيفته التحكم في سرعة التدريب ، و كل ما نقوم به هو طرح قيمة من
- لو كانت موجبة ، ستنقص ، و هذا سيودي إلى إنقاص الخسارة L
- لو كانت سالبة ، ستزيد ، و هذا سيودي إلى إنقاص الخسارة L
و إذا طبقنا هذا الأمر على كل وزن و على كل إنحياز في الشبكة ، فتدريجياً ستنقص الخسارة و الشبكة العصبية ستتحسن .
و الطريقة التي نتبعها في عملية التدريب كالتالي :
- نقوم بإختيار عينة واحدة من بياناتنا ، و لهذا السبب تسمى الخوارزمية بالعشوائية ( Stochastic ) لأننا نعمل على عينه واحده.
- نقوم بحساب المشتقات الجزئية للخسارة نسبةً إلى الأوزان و الإنحياز . كمثال ( ……إلخ )
- بعد ذلك نقوم بإستخدام معادلة التحديث من أجل تحديث كل وزن و إنحياز.
- نكرر الخطوات من البداية.
برمجة شبكة عصبية متكاملة :
و أخير حان الوقت لبرمجة شبكة عصبية من البداية حتى النهاية ، و للتذكير فبحوذتنا هذه البيانات :
الأسم | الوزن (ناقص 72) | الطول ( ناقص 166) | الجنس |
إيمان | -2 | -1 | 1 |
مدثر | 25 | 6 | 0 |
عبد الفتاح | 17 | 4 | 0 |
أمال | -15 | -6 | 1 |
import numpy as np def sigmoid(x): # f(x) = 1 / (1 + e^(-x)) دالة السيجمويد return 1 / (1 + np.exp(-x)) def deriv_sigmoid(x): # f'(x) = f(x) * (1 - f(x)) : مشتقة السيجمويد fx = sigmoid(x) return fx * (1 - fx) def mse_loss(y_true, y_pred): # بنفس الطول arrays عبارة عن y_true و y_pred return ((y_true - y_pred) ** 2).mean() class OurNeuralNetwork: ''' :الشبكة العصبية لديها - مدخلين - (h1, h2) طبقة خفية تحتوي على عصبونين - (o1) طبقة مخرجات تحتوي على عصبون واحد ''' def __init__(self): # الأوزان self.w1 = np.random.normal() self.w2 = np.random.normal() self.w3 = np.random.normal() self.w4 = np.random.normal() self.w5 = np.random.normal() self.w6 = np.random.normal() # الإنحياز self.b1 = np.random.normal() self.b2 = np.random.normal() self.b3 = np.random.normal() def feedforward(self, x): # لها عنصرين array هي x h1 = sigmoid(self.w1 * x[0] + self.w2 * x[1] + self.b1) h2 = sigmoid(self.w3 * x[0] + self.w4 * x[1] + self.b2) o1 = sigmoid(self.w5 * h1 + self.w6 * h2 + self.b3) return o1 def train(self, data, all_y_trues): ''' تساوي عدد الأمثلة n و (n x 2) حجمها numpy array البيانات هي عناصر n لها numpy array هي all_y_trues تمثل البيانات all_y_trues العناصر في ''' learn_rate = 0.1 epochs = 1000 # عدد الدورات خلال البيانات for epoch in range(epochs): for x, y_true in zip(data, all_y_trues): # --- التفذية الأمامية و التي سنحتاجها لاحقاً sum_h1 = self.w1 * x[0] + self.w2 * x[1] + self.b1 h1 = sigmoid(sum_h1) sum_h2 = self.w3 * x[0] + self.w4 * x[1] + self.b2 h2 = sigmoid(sum_h2) sum_o1 = self.w5 * h1 + self.w6 * h2 + self.b3 o1 = sigmoid(sum_o1) y_pred = o1 # --- .حساب المشتقات الجزئيى d_L_d_ypred = -2 * (y_true - y_pred) # o1 العصبون d_ypred_d_w5 = h1 * deriv_sigmoid(sum_o1) d_ypred_d_w6 = h2 * deriv_sigmoid(sum_o1) d_ypred_d_b3 = deriv_sigmoid(sum_o1) d_ypred_d_h1 = self.w5 * deriv_sigmoid(sum_o1) d_ypred_d_h2 = self.w6 * deriv_sigmoid(sum_o1) # h1 العصبون d_h1_d_w1 = x[0] * deriv_sigmoid(sum_h1) d_h1_d_w2 = x[1] * deriv_sigmoid(sum_h1) d_h1_d_b1 = deriv_sigmoid(sum_h1) # h2 العصبون d_h2_d_w3 = x[0] * deriv_sigmoid(sum_h2) d_h2_d_w4 = x[1] * deriv_sigmoid(sum_h2) d_h2_d_b2 = deriv_sigmoid(sum_h2) # --- تحديث الأوزان و الإنحياز # h1 العصبون self.w1 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_w1 self.w2 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_w2 self.b1 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_b1 # h2 العصبون self.w3 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_w3 self.w4 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_w4 self.b2 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_b2 # O1 العصبون self.w5 -= learn_rate * d_L_d_ypred * d_ypred_d_w5 self.w6 -= learn_rate * d_L_d_ypred * d_ypred_d_w6 self.b3 -= learn_rate * d_L_d_ypred * d_ypred_d_b3 # --- epoch حساب الخسارة بعد كل if epoch % 10 == 0: y_preds = np.apply_along_axis(self.feedforward, 1, data) loss = mse_loss(all_y_trues, y_preds) print("Epoch %d loss: %.3f" % (epoch, loss)) # التغريف بالبيانات data = np.array([ [-2, -1], # إيمان [25, 6], # مدثر [17, 4], # عبد الفتاح [-15, -6], # أمال ]) all_y_trues = np.array([ 1, # إيمان 0, # مدثر 0, # عبد الفتاح 1, # أمال ]) # تدريب الشبكة العصبية network = OurNeuralNetwork() network.train(data, all_y_trues)
بوسعكم تجريب الكود هنا و كما يتوفر الكود على github
نلاحظ أن الخسارة تتناقص بشكل ثابت و تدرجي بينما تتعلم الشبكة :
الأن بمقدورنا إستخدام الشبكة للتنبؤ بالجنس :
# القيام بالتنبؤ Hala = np.array([-7, -3]) # 65 kg, 163 cm Khaled = np.array([20, 2]) # 92 kg, 168 cm print("Hala: %.3f" % network.feedforward(Hala)) # 0.951 - F print("Khaled: %.3f" % network.feedforward(Khaled)) # 0.039 - M
و إلى هنا نكون قد وصلنا إلى نهاية هذه المقالة و التي تعرقنا فيها على أساسيات الشبكات العصبية. و بمقدوركم اللعب بالشبكات العصبية هنا.
إضافة تعليق