كيف تعمل مصنفات بايز الساذجة (Naive Bayes Classifiers)؟ شرح شامل مع أمثلة كود بايثون

دقائق القراءة: 13

كيف تعمل مصنفات بايز الساذجة (Naive Bayes Classifiers)؟ شرح شامل مع أمثلة كود بايثون

تُعد مصنفات بايز الساذجة (Naive Bayes Classifiers - NBC) من خوارزميات التعلم الآلي البسيطة لكنها قوية وفعالة. تعتمد هذه المصنفات بشكل أساسي على مبادئ الاحتمال الشرطي ونظرية بايز. في هذا المقال، سنكشف الستار عن المفهوم الجوهري وراء مصنفات بايز الساذجة، وسنقدم مثالاً عملياً يوضح كيفية استخدامها لحل مشكلة تصنيف. سنتعمق في الأقسام التالية في الجوانب الرياضية لهذه المصنفات، ولكن لا تتردد في تجاوز هذه الأقسام والانتقال مباشرة إلى جزء التطبيق العملي إذا لم تكن مهتماً بالتفاصيل الرياضية. في قسم التطبيق، سنعرض خوارزمية NBC بسيطة، ثم نستخدمها لحل مشكلة تصنيف كلاسيكية: تحديد ما إذا كان راكب معين على متن سفينة تايتانيك قد نجا من الحادث أم لا.

فهم الاحتمال الشرطي: حجر الزاوية

قبل الخوض في تفاصيل الخوارزمية نفسها، دعنا نلقي نظرة على الأساس الرياضي البسيط الذي تستند إليه. نحتاج أولاً إلى فهم ماهية الاحتمال الشرطي وكيف يمكننا استخدام نظرية بايز لحسابه.

لنتخيل نردًا عادلاً بستة أوجه. ما هو احتمال الحصول على الرقم ستة عند رمي النرد؟ الأمر بسيط، إنه 1/6. لدينا ست نتائج محتملة ومتساوية الاحتمال، ونحن مهتمون بواحدة فقط منها. إذن، الاحتمال هو 1/6.

ولكن ماذا لو أخبرتك أنني رميت النرد بالفعل وكانت النتيجة رقمًا زوجيًا؟ ما هو احتمال أننا حصلنا على الرقم ستة الآن؟ هذه المرة، النتائج المحتملة هي ثلاثة فقط، لأنه لا توجد سوى ثلاثة أرقام زوجية على النرد (2، 4، 6). نحن ما زلنا مهتمين بنتيجة واحدة فقط من هذه النتائج، لذا فإن الاحتمال الآن أكبر: 1/3.

ما الفرق بين الحالتين؟ في الحالة الأولى، لم يكن لدينا أي معلومات مسبقة عن النتيجة. وبالتالي، احتجنا إلى الأخذ في الاعتبار كل نتيجة ممكنة. في الحالة الثانية، أُخبرنا أن النتيجة كانت رقمًا زوجيًا، لذا تمكنا من تقليص مساحة النتائج المحتملة إلى الأرقام الزوجية الثلاثة فقط التي تظهر في النرد العادي ذي الستة أوجه.

بشكل عام، عندما نحسب احتمال وقوع حدث A، بشرط وقوع حدث آخر B، فإننا نقول إننا نحسب الاحتمال الشرطي لـ A بشرط B، أو ببساطة احتمال A بشرط B. نرمز له بـ P(A|B). على سبيل المثال، احتمال الحصول على الرقم ستة بشرط أن يكون الرقم الذي حصلنا عليه زوجيًا هو: P(Six|Even) = 1/3. هنا، رمزنا لحدث الحصول على الرقم ستة بـ Six، ولحدث الحصول على رقم زوجي بـ Even.

ولكن، كيف نحسب الاحتمالات الشرطية؟ هل هناك صيغة محددة لذلك؟

صيغ حساب الاحتمالات الشرطية ونظرية بايز

الآن، سأقدم لك بعض الصيغ لحساب الاحتمالات الشرطية. أعدك بأنها لن تكون صعبة، وهي مهمة إذا كنت ترغب في فهم الجوانب الخفية لخوارزميات التعلم الآلي التي سنتحدث عنها لاحقًا.

يمكن حساب احتمال وقوع حدث A بشرط وقوع حدث آخر B على النحو التالي:

P(A|B) = P(A,B)/P(B)

حيث P(A,B) يرمز إلى احتمال وقوع الحدثين A و B في نفس الوقت، و P(B) يرمز إلى احتمال وقوع الحدث B. لاحظ أننا نحتاج إلى أن يكون P(B) > 0 لأنه لا معنى للحديث عن احتمال A بشرط B إذا كان وقوع B غير ممكن.

يمكننا أيضًا حساب احتمال وقوع حدث A، بشرط وقوع أحداث متعددة B1, B2,..., Bn:

P(A|B1,B2,...,Bn) = P(A,B1,B2,...,Bn)/P(B1,B2,...,Bn)

هناك طريقة أخرى لحساب الاحتمالات الشرطية. هذه الطريقة هي ما يسمى بـ نظرية بايز (Bayes's Theorem):

P(A|B) = P(B|A)P(A)/P(B)

وللحالة العامة مع أحداث متعددة:

P(A|B1,B2,...,Bn) = P(B1,B2,...,Bn|A)P(A)/P(B1,B2,...,Bn)

لاحظ أننا نحسب احتمال الحدث A بشرط الحدث B، عن طريق عكس ترتيب وقوع الأحداث. نفترض الآن أن الحدث A قد وقع ونريد حساب احتمال وقوع الحدث B (أو الأحداث B1,B2,...,Bn في المثال الثاني والأكثر عمومية).

حقيقة مهمة يمكن استنتاجها من هذه النظرية هي صيغة حساب P(B1,B2,...,Bn,A). تُعرف هذه القاعدة باسم قاعدة السلسلة للاحتمالات (chain rule for probabilities):

P(B1,B2,...,Bn,A) = P(B1 | B2, B3, ..., Bn, A)P(B2,B3,...,Bn,A)
= P(B1 | B2, B3, ..., Bn, A)P(B2 | B3, B4, ..., Bn, A)P(B3, B4, ..., Bn, A)
= P(B1 | B2, B3, ..., Bn, A)P(B2 | B3, B4, ..., Bn, A)...P(Bn | A)P(A)

تبدو هذه الصيغة معقدة، أليس كذلك؟ ولكن تحت بعض الشروط، يمكننا إيجاد حل بديل وتجنبها. دعنا نتحدث عن المفهوم الأخير الذي نحتاج إلى معرفته لفهم الخوارزميات.

مفهوم الاستقلالية في الاحتمالات

المفهوم الأخير الذي سنتحدث عنه هو الاستقلالية (Independence). نقول إن الحدثين A و B مستقلان إذا كان:

P(A|B) = P(A)

هذا يعني أن احتمال وقوع الحدث A لا يتأثر بوقوع الحدث B. نتيجة مباشرة لذلك هي أن P(A,B) = P(A)P(B). بعبارة أبسط، هذا يعني أن احتمال وقوع الحدثين A و B في نفس الوقت يساوي حاصل ضرب احتمالات وقوع الحدثين A و B بشكل منفصل.

إذا كان A و B مستقلين، فإنه ينطبق أيضًا ما يلي:

P(A,B|C) = P(A|C)P(B|C)

الآن نحن جاهزون للحديث عن مصنفات بايز الساذجة!

مصنفات بايز الساذجة (Naive Bayes Classifiers)

لنفترض أن لدينا متجه X مكون من n ميزة (features) ونريد تحديد فئة هذا المتجه من مجموعة مكونة من k فئة y1, y2,...,yk. على سبيل المثال، إذا أردنا تحديد ما إذا كان سيمطر اليوم أم لا. لدينا فئتان محتملتان (k = 2): rain (مطر)، not rain (لا مطر)، وقد يكون طول متجه الميزات 3 (n = 3). قد تكون الميزة الأولى هي ما إذا كان الجو غائمًا أو مشمسًا، والميزة الثانية قد تكون ما إذا كانت الرطوبة عالية أو منخفضة، والميزة الثالثة ستكون ما إذا كانت درجة الحرارة عالية أو متوسطة أو منخفضة. إذن، هذه يمكن أن تكون متجهات ميزات محتملة:

<Cloudy, H_High, T_Low>
<Sunny, H_Low, T_Medium>
<Cloudy, H_Low, T_High>

مهمتنا هي تحديد ما إذا كان سيمطر أم لا، بالنظر إلى ميزات الطقس. بعد التعرف على الاحتمالات الشرطية، يبدو من الطبيعي التعامل مع المشكلة بمحاولة حساب احتمال المطر بالنظر إلى الميزات:

R = P(Rain | Cloudy, H_High, T_Low)
NR = P(NotRain | Cloudy, H_High, T_Low)

إذا كان R > NR، فإننا نجيب بأنه سيمطر، وإلا فإننا نقول إنه لن يمطر. بشكل عام، إذا كان لدينا k فئة y1, y2, ..., yk، ومتجه من n ميزة X = <X1, X2,..., Xn>، فإننا نريد إيجاد الفئة yi التي تزيد من:

P(yi | X1, X2, ..., Xn) = P(X1, X2,..., Xn, yi)/P(X1, X2, ..., Xn)

لاحظ أن المقام ثابت ولا يعتمد على الفئة yi. لذا، يمكننا تجاهله والتركيز فقط على البسط. في قسم سابق، رأينا كيفية حساب P(X1, X2,..., Xn, yi) عن طريق تحليله إلى حاصل ضرب احتمالات شرطية (الصيغة المعقدة):

P(X1, X2,..., Xn, yi) = P(X1 | X2,..., Xn, yi)P(X2 | X3,..., Xn, yi)...P(Xn | yi)P(yi)

بافتراض أن جميع الميزات Xi مستقلة وباستخدام نظرية بايز، يمكننا حساب الاحتمال الشرطي على النحو التالي:

P(yi | X1, X2,..., Xn) = P(X1, X2,..., Xn | yi)P(yi)/P(X1, X2, ..., Xn)
= P(X1 | yi)P(X2 | yi)...P(Xn | yi)P(yi)/P(X1, X2, ..., Xn)

ونحن نحتاج فقط إلى التركيز على البسط. من خلال إيجاد الفئة yi التي تزيد من التعبير السابق، فإننا نصنف المتجه المدخل.

كيفية حساب الاحتمالات اللازمة؟

عند حل هذا النوع من المشكلات، نحتاج إلى مجموعة من الأمثلة المصنفة مسبقًا. على سبيل المثال، في مشكلة تخمين ما إذا كان سيمطر أم لا، نحتاج إلى عدة أمثلة لمتجهات الميزات وتصنيفاتها التي تم الحصول عليها من توقعات الطقس السابقة. لذا، سيكون لدينا شيء من هذا القبيل:

...
<Cloudy, H_High, T_Low> -> Rain
<Sunny, H_Low, T_Medium> -> Not Rain
<Cloudy, H_Low, T_High> -> Not Rain
...

لنفترض أننا بحاجة إلى تصنيف متجه جديد <Cloudy, H_Low, T_Low>. نحتاج إلى حساب:

P(Rain | Cloudy, H_Low, T_Low) = P(Cloudy | H_Low, T_Low, Rain)P(H_Low | T_Low, Rain)P(T_Low | Rain)P(Rain)/P(Cloudy, H_Low, T_Low)

نحصل على التعبير السابق بتطبيق تعريف الاحتمال الشرطي وقاعدة السلسلة. تذكر أننا نحتاج فقط إلى التركيز على البسط، لذا يمكننا إسقاط المقام. نحتاج أيضًا إلى حساب الاحتمال لـ NotRain، ولكن يمكننا القيام بذلك بطريقة مماثلة.

يمكننا إيجاد P(Rain) = # Rain/Total. هذا يعني عد الإدخالات في مجموعة البيانات التي تم تصنيفها على أنها Rain وقسمة هذا العدد على حجم مجموعة البيانات. لحساب P(Cloudy | H_Low, T_Low, Rain)، نحتاج إلى عد جميع الإدخالات التي تحتوي على الميزات H_Low, T_Low و Cloudy. يجب أن تكون هذه الإدخالات مصنفة أيضًا على أنها Rain. ثم يتم قسمة هذا العدد على إجمالي كمية البيانات. نحسب بقية عوامل الصيغة بطريقة مماثلة.

إن إجراء هذه الحسابات لكل فئة ممكنة مكلف وبطيء للغاية. لذلك نحتاج إلى وضع افتراضات حول المشكلة لتبسيط الحسابات. تفترض مصنفات بايز الساذجة أن جميع الميزات مستقلة عن بعضها البعض. لذا يمكننا إعادة كتابة صيغتنا بتطبيق نظرية بايز وافتراض الاستقلالية بين كل زوج من الميزات:

P(Rain | Cloudy, H_Low, T_Low) = P(Cloudy | Rain)P(H_Low | Rain)P(T_Low | Rain)P(Rain)/P(Cloudy, H_Low, T_Low)

الآن نحسب P(Cloudy | Rain) عن طريق عد عدد الإدخالات المصنفة على أنها Rain وكانت Cloudy. تُسمى الخوارزمية ساذجة (Naive) بسبب افتراض الاستقلالية هذا. توجد تبعيات بين الميزات في معظم الأوقات؛ لا يمكننا القول في الحياة الواقعية أنه لا توجد تبعية بين الرطوبة ودرجة الحرارة، على سبيل المثال. تُسمى مصنفات بايز الساذجة أيضًا بايز المستقلة (Independence Bayes)، أو بايز البسيطة (Simple Bayes).

الصيغة العامة ستكون:

P(yi | X1, X2, ..., Xn) = P(X1 | yi)P(X2 | yi)...P(Xn | yi)P(yi)/P(X1, X2, ..., Xn)

تذكر أنه يمكنك التخلص من المقام. نحن نحسب فقط البسط ونحدد الفئة التي تزيد قيمته. الآن، دعنا نطبق مصنف NBC الخاص بنا ونستخدمه في مشكلة عملية.

هيا نبرمج: تطبيق مصنف بايز الساذج بلغة بايثون

سأعرض لك تطبيقًا لمصنف NBC بسيط، ثم سنراه عمليًا. المشكلة التي سنحلها هي تحديد ما إذا كان راكب على متن سفينة تايتانيك قد نجا أم لا، بالنظر إلى بعض الميزات مثل جنسه وعمره.

هنا يمكنك رؤية تطبيق لمصنف NBC بسيط للغاية:

class NaiveBayesClassifier:
    def __init__(self, X, y):
        '''
        X and y denotes the features and the target labels respectively
        '''
        self.X, self.y = X, y
        self.N = len(self.X) # Length of the training set
        self.dim = len(self.X[0]) # Dimension of the vector of features
        self.attrs = [[] for _ in range(self.dim)] # Here we'll store the columns of the training set
        self.output_dom = {} # Output classes with the number of ocurrences in the training set. In this case we have only 2 classes
        self.data = [] # To store every row [Xi, yi]

        for i in range(len(self.X)):
            for j in range(self.dim):
                # if we have never seen this value for this attr before,
                # then we add it to the attrs array in the corresponding position
                if not self.X[i][j] in self.attrs[j]:
                    self.attrs[j].append(self.X[i][j])

            # if we have never seen this output class before,
            # then we add it to the output_dom and count one occurrence for now
            if not self.y[i] in self.output_dom.keys():
                self.output_dom[self.y[i]] = 1
            # otherwise, we increment the occurrence of this output in the training set by 1
            else:
                self.output_dom[self.y[i]] += 1

            # store the row
            self.data.append([self.X[i], self.y[i]])

    def classify(self, entry):
        solve = None # Final result
        max_arg = -1 # partial maximum

        for y in self.output_dom.keys():
            prob = self.output_dom[y]/self.N # P(y)

            for i in range(self.dim):
                cases = [x for x in self.data if x[0][i] == entry[i] and x[1] == y] # all rows with Xi = xi
                n = len(cases)
                prob *= n/self.N # P *= P(Xi = xi)

            # if we have a greater prob for this output than the partial maximum...
            if prob > max_arg:
                max_arg = prob
                solve = y

        return solve

هنا، نفترض أن كل ميزة لها نطاق منفصل (discrete domain). هذا يعني أنها تأخذ قيمة من مجموعة محدودة من القيم الممكنة. وينطبق الشيء نفسه على الفئات. لاحظ أننا نخزن بعض البيانات في طريقة __init__ حتى لا نضطر إلى تكرار بعض العمليات. يتم تصنيف إدخال جديد في طريقة classify. هذا مثال بسيط للتطبيق. في تطبيقات العالم الحقيقي، لا تحتاج (ومن الأفضل ألا تقوم) بإنشاء تطبيقك الخاص. على سبيل المثال، تحتوي مكتبة sklearn في بايثون على العديد من تطبيقات NBC الجيدة. لاحظ مدى سهولة تطبيقها!

تطبيق المصنف على بيانات حقيقية (كارثة التايتانيك)

الآن، دعنا نطبق مصنفنا الجديد لحل مشكلة. لدينا مجموعة بيانات تحتوي على وصف لـ 887 راكبًا على متن سفينة تايتانيك. يمكننا أيضًا معرفة ما إذا كان راكب معين قد نجا من المأساة أم لا. لذا فإن مهمتنا هي تحديد ما إذا كان راكب آخر غير مدرج في مجموعة التدريب قد نجا أم لا.

في هذا المثال، سأستخدم مكتبة pandas لقراءة ومعالجة البيانات. لن أستخدم أي أداة أخرى. يتم تخزين البيانات في ملف يسمى titanic.csv، لذا فإن الخطوة الأولى هي قراءة البيانات والحصول على نظرة عامة عليها.

import pandas as pd

data = pd.read_csv('titanic.csv')
print(data.head())

الناتج هو:

   Survived  Pclass                                               Name     Sex   Age  Siblings/Spouses Aboard  Parents/Children Aboard     Fare
0         0       3                            Mr. Owen Harris Braund    male  22.0                        1                        0   7.2500
1         1       1  Mrs. John Bradley (Florence Briggs Thayer) Cum...  female  38.0                        1                        0  71.2833
2         1       3                              Miss. Laina Heikkinen  female  26.0                        0                        0   7.9250
3         1       1                Mrs. Jacques Heath (Lily May Peel) Futrelle  female  35.0                        1                        0  53.1000
4         0       3                                Mr. William Henry Allen    male  35.0                        0                        0   8.0500

لاحظ أن لدينا اسم كل راكب (Name). لن نستخدم هذه الميزة لمصنفنا لأنها ليست ذات أهمية لمشكلتنا. سنتخلص أيضًا من ميزة الأجرة (Fare) لأنها مستمرة (continuous) وميزاتنا يجب أن تكون منفصلة (discrete). توجد مصنفات بايز الساذجة التي تدعم الميزات المستمرة، على سبيل المثال، مصنف بايز الساذج الغاوسي (Gaussian Naive Bayes Classifier).

y = list(map(lambda v: 'yes' if v == 1 else 'no', data['Survived'].values)) # target values as string
# We won't use the 'Name' nor the 'Fare' field
X = data[['Pclass', 'Sex', 'Age', 'Siblings/Spouses Aboard', 'Parents/Children Aboard']].values # features values

بعد ذلك، نحتاج إلى فصل مجموعة بياناتنا إلى مجموعة تدريب ومجموعة تحقق. تُستخدم الأخيرة للتحقق من مدى جودة أداء خوارزميتنا.

print(len(y)) # >> 887
# We'll take 600 examples to train and the rest to the validation process
y_train = y[:600]
y_val = y[600:]
X_train = X[:600]
X_val = X[600:]

نقوم بإنشاء مصنف NBC الخاص بنا باستخدام مجموعة التدريب، ثم نصنف كل إدخال في مجموعة التحقق. نقيس دقة خوارزميتنا عن طريق قسمة عدد الإدخالات التي صنفتها بشكل صحيح على العدد الإجمالي للإدخالات في مجموعة التحقق.

## Creating the Naive Bayes Classifier instance with the training data
nbc = NaiveBayesClassifier(X_train, y_train)

total_cases = len(y_val) # size of validation set

# Well classified examples and bad classified examples
good = 0
bad = 0

for i in range(total_cases):
    predict = nbc.classify(X_val[i])
    # print(y_val[i] + ' --------------- ' + predict)
    if y_val[i] == predict:
        good += 1
    else:
        bad += 1

print('TOTAL EXAMPLES:', total_cases)
print('RIGHT:', good)
print('WRONG:', bad)
print('ACCURACY:', good/total_cases)

الناتج:

TOTAL EXAMPLES: 287
RIGHT : 200
WRONG : 87
ACCURACY : 0.6968641114982579

الدقة ليست رائعة، لكنها بداية جيدة. يمكننا الحصول على تحسين في الدقة بنحو 10% إذا تخلصنا من ميزات أخرى مثل Siblings/Spouses Aboard و Parents/Children Aboard.

الخلاصة التقنية

في عصر تهيمن فيه الشبكات العصبية وخوارزميات التعلم الآلي المعقدة والمكلفة، تظل مصنفات بايز الساذجة (NBCs) خيارًا قيمًا. إنها خوارزميات بسيطة تسمح لنا بتحقيق نتائج جيدة في بعض مشكلات التصنيف دون الحاجة إلى الكثير من الموارد الحاسوبية. كما أنها تتوسع بشكل جيد للغاية، مما يعني أنه يمكننا إضافة المزيد من الميزات وستظل الخوارزمية سريعة وموثوقة.

حتى في الحالات التي لا تكون فيها مصنفات NBCs مناسبة تمامًا للمشكلة التي نحاول حلها، قد تكون مفيدة جدًا كنقطة انطلاق أو خط أساس (baseline). يمكننا أولاً محاولة حل المشكلة باستخدام NBC ببضعة أسطر من التعليمات البرمجية وبجهد قليل. بعد ذلك، يمكننا محاولة تحقيق نتائج أفضل باستخدام خوارزميات أكثر تعقيدًا وتكلفة. هذه العملية يمكن أن توفر لنا الكثير من الوقت وتمنحنا ملاحظات فورية حول ما إذا كانت الخوارزميات المعقدة تستحق العناء حقًا لمهمتنا.

في هذا المقال، قرأت عن الاحتمالات الشرطية، والاستقلالية، ونظرية بايز. هذه هي المفاهيم الرياضية وراء مصنفات بايز الساذجة. بعد ذلك، رأينا تطبيقًا بسيطًا لمصنف NBC وحللنا مشكلة تحديد ما إذا كان راكب على متن سفينة تايتانيك قد نجا من الحادث. نأمل أن تكون قد وجدت هذا المقال مفيدًا.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *