التعامل مع مشاكل النص

تُعرف هذه المشكلات أيضًا بمشكلات معالجة اللغة الطبيعية (NLP). تشبه مشكلات معالجة اللغة الطبيعية أيضًا الصور بمعنى أنها مختلفة تمامًا. تحتاج إلى إنشاء خطوط إنتاج كما تحتاج إلى فهم الجانب التجاري لبناء نموذج جيد.

 سيأخذك بناء النماذج  إلى مستوى معين ، ولكن لتحسين المشاريع التجارية التي تبني النموذج من أجلها والمساهمة فيها ، يجب أن تفهم كيف تؤثر على الجانب التجاري.

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

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

تعريف المشكلة

نبدأ بمجموعة بيانات مراجعة الأفلام من IMDB (هنا) التي تتكون من 25000 مراجعة للمشاعر الإيجابية و 25000 مراجعة للمشاعر السلبية . يمكن تطبيق المفاهيم التي سأناقشها هنا على أي مجموعة بيانات لتصنيف النص تقريبًا. مجموعة البيانات هذه سهلة الفهم. 

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

 كيف ستبدأ بمثل هذه المشكلة؟ 

معاجم المشاعر

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

def find_sentiment(sentence, pos, neg):
    """
    هذه الوظيفة ترجع شعور الجملة
  : param pos:  الجملة ، سلسلة
  : param pos: مجموعة من الكلمات الإيجابية
  : param neg: مجموعة من الكلمات السلبية
  : return: إرجاع المشاعر الإيجابية أو السلبية أو المحايدة
    
    """
    # تقسيم الجملة بمسافة
    # تصبح "this is a sentence!"
    # ["this", "is" "a", "sentence!"]
    
    sentence = sentence.split()
    
    # اجعل الجملة  مجموعة
    
    sentence = set(sentence)
    
    # التحقق من الكلامات الإيجابية المشتركة
    
    num_common_pos = len(sentence.intersection(pos))
    
    # التحقق من الكلامات السلبية المشتركة
    
    num_common_neg = len(sentence.intersection(neg))
    
    if num_common_pos > num_common_neg:
        return "positive"
    
    if num_common_pos < num_common_neg:
        return "negative"
    return "neutral"

ومع ذلك ، فإن هذا النوع من النهج لا يأخذ الكثير في الاعتبار. وكما ترى ، فإن split() ليس مثاليًا أيضًا. إذا كنت تستخدم Split () ، فستجد جملة مثل:

“hi, how are you?”

تتحول إلى

[“hi,”, “how”, “are”, “you?”]

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

التعميل 

يُعرف تقسيم سلسلة إلى قائمة كلمات باسم التعميل (tokenization). أحد أكثر المعملات شيوعًا تاتي من NLTK (مجموعة أدوات اللغة الطبيعية).

In [X]: from nltk.tokenize import word_tokenize
In [X]: sentence = "hi, how are you?"

In [X]: sentence.split()
Out[X]: ['hi,', 'how', 'are', 'you?']

In [X]: word_tokenize(sentence)
Out[X]: ['hi', ',', 'how', 'are', 'you', '?']

كما ترى ، باستخدام تعميل  الكلمة الخاص بـ NLTK ، يتم تقسيم الجملة نفسها بطريقة أفضل بكثير. ستعمل المقارنة باستخدام قائمة الكلمات بشكل أفضل الآن! هذا ما سنطبقه على نموذجنا الأول لاكتشاف المشاعر. 

حقيبة الكلمات

أحد النماذج الأساسية التي يجب أن تجربها دائمًا مع مشكلة التصنيف في معالجة اللغة الطبيعية هو حقيبة الكلمات (bag of words). في حقيبة الكلمات ، نقوم بإنشاء مصفوفة متناثرة (sparse matrix) ضخمة تخزن تعداد جميع الكلمات الموجودة في مجموعة الكلمات (corpus = جميع المستندات = جميع الجمل). لهذا ، سوف نستخدم CountVectorizer من scikit-Learn. دعونا نرى كيف يعمل.

from sklearn.feature_extraction.text import CountVectorizer

# إنشاء مجموعة بيانات 
corpus = [
 "hello, how are you?",
 "im getting bored at home. And you? What do you think?",
 "did you know about counts",
 "let's see if this works!",
 "YES!!!!"
]

#CountVectorizer تهيئة 
ctv = CountVectorizer()

ctv.fit(corpus)
corpus_transformed = ctv.transform(corpus)

إذا قمنا بطباعة corpus_transformed ، فسنحصل على شيء مشابه لما يلي:

 (0, 2) 1
 (0, 9) 1
 (0, 11) 1
 (0, 22) 1
 (1, 1) 1
 (1, 3) 1
 (1, 4) 1
 (1, 7) 1
 (1, 8) 1
 (1, 10) 1
 (1, 13) 1
 (1, 17) 1
 (1, 19) 1
 (1, 22) 2
 (2, 0) 1
 (2, 5) 1
 (2, 6) 1
 (2, 14) 1
 (2, 22) 1
 (3, 12) 1
 (3, 15) 1
 (3, 16) 1
 (3, 18) 1
 (3, 20) 1
 (4, 21) 1

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

 الطريقة التي يعمل بها CountVectorizer هي أولاً تعميل (tokenizing)  الجملة ثم يقوم بتعيين قيمة لكل عملة (token). لذلك ، يتم تمثيل كل عملة بواسطة فهرس فريد. هذه المؤشرات الفريدة هي الأعمدة التي نراها. يقوم CountVectorizer بتخزين هذه المعلومات.

print(ctv.vocabulary_)
{'hello': 9, 'how': 11, 'are': 2, 'you': 22, 'im': 13, 'getting': 8,
'bored': 4, 'at': 3, 'home': 10, 'and': 1, 'what': 19, 'do': 7, 'think':
17, 'did': 6, 'know': 14, 'about': 0, 'counts': 5, 'let': 15, 'see': 16,
'if': 12, 'this': 18, 'works': 20, 'yes': 21}

نرى أن الفهرس 22 يخص ““you”” وفي الجملة الثانية ، استخدمنا “you” مرتين. وبالتالي ، العدد هو 2. آمل أن يكون واضحًا الآن ما هي حقيبة الكلمات. لكننا نفتقد بعض الأحرف الخاصة. في بعض الأحيان يمكن أن تكون هذه الأحرف الخاصة مفيدة أيضًا. فمثلا، “؟” يشير إلى سؤال في معظم الجمل. دعنا ندمج word_tokenize من scikit-Learn في CountVectorizer ونرى ما سيحدث.

from sklearn.feature_extraction.text import CountVectorizer
from nltk.tokenize import word_tokenize

# إنشاء مجموعة بيانات 
corpus = [
 "hello, how are you?",
 "im getting bored at home. And you? What do you think?",
 "did you know about counts",
 "let's see if this works!",
 "YES!!!!"
]

#word_tokenize بواسطة  CountVectorizer تهيئة 
ctv = CountVectorizer(tokenizer=word_tokenize, token_pattern=None)

ctv.fit(corpus)
corpus_transformed = ctv.transform(corpus)
print(ctv.vocabulary_)

هذا يغير مفرداتنا إلى:

{'hello': 14, ',': 2, 'how': 16, 'are': 7, 'you': 27, '?': 4, 'im': 18, 'getting': 13, 'bored': 9, 'at': 8, 'home': 15, '.': 3, 'and': 6, 'what': 24, 'do': 12, 'think': 22, 'did': 11, 'know': 19, 'about': 5, 'counts': 10, 'let': 20, "'s": 1, 'see': 21, 'if': 17, 'this': 23, 'works': 25, '!': 0, 'yes': 26}

الآن ، لدينا المزيد من الكلمات في المفردات. وبالتالي ، يمكننا الآن إنشاء مصفوفة متناثرة باستخدام جميع الجمل في مجموعة بيانات IMDB ويمكننا بناء نموذج. 

 الانحدار اللوجستي

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

import pandas as pd
from nltk.tokenize import word_tokenize
from sklearn import linear_model
from sklearn import metrics
from sklearn import model_selection
from sklearn.feature_extraction.text import TfidfVectorizer

if __name__=="__main__":
    # قراءة مجموعة البيانات 
    df = pd.read_csv("NLP\input\IMDB Dataset.csv")

    # تعيين الموجب 1 و السلبي 0
    df.sentiment = df.sentiment.apply(
        lambda x:1 if x == "positive" else 0
    )

    # -1 له القيم  kfold ننشى عامود جديد نسميه 
    df["kfold"] = -1

    # الخطوة التالية هي ترتيب صفوف البيانات بشكل عشوائي
    df = df.sample(frac=1).reset_index(drop=True)

    # الحصول على التسميات 
    y = df.sentiment.values

    # kfold تهيئة 
    kf = model_selection.StratifiedKFold(n_splits=5)

    # تعبئة العامود الجديد
    for f, (t_, v_) in enumerate(kf.split(X=df, y=y)):
        df.loc[v_, 'kfold'] = f
    
    # نمر على الطيات التي أنشئناها
    for fold_ in range(5):
        # إطارات بيانات مؤقتة للتدريب والاختبار
        train_df = df[df.kfold != fold_].reset_index(drop=True)
        test_df  = df[df.kfold == fold_].reset_index(drop=True)
        
        #word_tokenize بواسطة  CountVectorizer تهيئة 
        tfidf_vec = TfidfVectorizer(
            tokenizer= word_tokenize,
            token_pattern=None
        )

        tfidf_vec.fit(train_df.review)

        # تحويل مراجعات بيانات التدريب والتحقق
        xtrain = tfidf_vec.transform(train_df.review)
        xtest  = tfidf_vec.transform(test_df.review)

        # تهيئة نموذج الانحدار اللوجستي
        model = linear_model.LogisticRegression()

        # مناسبة النموذج
        model.fit(xtrain, train_df.sentiment)

        # عمل تنبؤات على بيانات الاختبار
        # عتبة التنبؤات هي 0.5
        preds = model.predict(xtest)

        # حساب الدقة
        accuracy = metrics.accuracy_score(test_df.sentiment, preds)

        print(f"Fold : {fold_}")
        print(f"Accuracy = {accuracy}")
        print(f"")

يستغرق تشغيل هذا الكود  وقتًا ولكن يجب أن يمنحك الإخراج التالي:

 python ctv_logres.py
Fold: 0
Accuracy = 0.8903
Fold: 1
Accuracy = 0.897
Fold: 2
Accuracy = 0.891
Fold: 3
Accuracy = 0.8914
Fold: 4
Accuracy = 0.8931

واو ، حصلنا على دقة 89٪ ، وكل ما فعلناه هو استخدام حقيبة  الكلمات مع الانحدار اللوجستي! هذا رائع جدا! ومع ذلك ، فقد استغرق هذا النموذج الكثير من الوقت للتدريب ، دعنا نرى ما إذا كان بإمكاننا تحسين الوقت باستخدام مصنف نايف بايز (naïve bayes classifier). يحظى مصنف نايف بايز بشعبية كبيرة في مهام معالجة اللغة الطبيعية حيث أن المصفوفات المتناثرة ضخمة و  نموذج نايف بايز بسيط. لاستخدام هذا النموذج ، نحتاج إلى تغيير استيراد واحد  و السطر الخاص بالنموذج. دعونا نرى كيف يعمل هذا النموذج. سوف نستخدم MultinomialNB من scikit-Learn.

import pandas as pd
from nltk.tokenize import word_tokenize
from sklearn import naive_bayes
from sklearn import metrics
from sklearn import model_selection
from sklearn.feature_extraction.text import CountVectorizer

if __name__=="__main__":
    # قراءة مجموعة البيانات 
    df = pd.read_csv("NLP\input\IMDB Dataset.csv")

    # تعيين الموجب 1 و السلبي 0
    df.sentiment = df.sentiment.apply(
        lambda x:1 if x == "positive" else 0
    )

    # -1 له القيم  kfold ننشى عامود جديد نسميه 
    df["kfold"] = -1

    # الخطوة التالية هي ترتيب صفوف البيانات بشكل عشوائي
    df = df.sample(frac=1).reset_index(drop=True)

    # الحصول على التسميات 
    y = df.sentiment.values

    # kfold تهيئة 
    kf = model_selection.StratifiedKFold(n_splits=5)

    # تعبئة العامود الجديد
    for f, (t_, v_) in enumerate(kf.split(X=df, y=y)):
        df.loc[v_, 'kfold'] = f
    
    # نمر على الطيات التي أنشئناها
    for fold_ in range(5):
        # إطارات بيانات مؤقتة للتدريب والاختبار
        train_df = df[df.kfold != fold_].reset_index(drop=True)
        test_df  = df[df.kfold == fold_].reset_index(drop=True)
        
        #word_tokenize بواسطة  CountVectorizer تهيئة 
        count_vec = CountVectorizer(
            tokenizer= word_tokenize,
            token_pattern=None
        )

        count_vec.fit(train_df.review)

        # تحويل مراجعات بيانات التدريب والتحقق
        xtrain = count_vec.transform(train_df.review)
        xtest  = count_vec.transform(test_df.review)

        # تهيئة نموذج الانحدار اللوجستي
        model = naive_bayes.LogisticRegression()

        # مناسبة النموذج
        model.fit(xtrain, train_df.sentiment)

        # عمل تنبؤات على بيانات الاختبار
        # عتبة التنبؤات هي 0.5
        preds = model.predict(xtest)

        # حساب الدقة
        accuracy = metrics.accuracy_score(test_df.sentiment, preds)

        print(f"Fold : {fold_}")
        print(f"Accuracy = {accuracy}")
        print(f"")

النتائج كالتالي:

❯ python ctv_nb.py
Fold: 0
Accuracy = 0.8444
Fold: 1
Accuracy = 0.8499
Fold: 2
Accuracy = 0.8422
Fold: 3
Accuracy = 0.8443
Fold: 4
Accuracy = 0.8455

نرى أن هذه النتيجة منخفضة. لكن نموذج نايف بايز فائق السرعة. 

مصطلح الترددات-تردد المستند المعكوس

طريقة أخرى في معالجة اللغة الطبيعية يميل معظم الناس هذه الأيام إلى تجاهلها أو عدم الاهتمام بمعرفتها تسمى TF-IDF. بحيث أن TF  هو مصطلح الترددات (term frequencies,) ، و IDF هو تردد المستند المعكوس (inverse document frequency). قد يبدو الأمر صعبًا من خلال هذه المصطلحات ، لكن الأمور ستتضح معادلاتهم 

على غرار CountVectorizer في scikit-Learn ، لدينا TfidfVectorizer. دعونا نحاول استخدامه بنفس الطريقة التي استخدمنا بها CountVectorizer.

from sklearn.feature_extraction.text import TfidfVectorizer
from nltk.tokenize import word_tokenize

# إنشاء مجموعة بيانات 
corpus = [
 "hello, how are you?",
 "im getting bored at home. And you? What do you think?",
 "did you know about counts",
 "let's see if this works!",
 "YES!!!!"
]

#word_tokenize بواسطة  TfidfVectorizer تهيئة 
ctv = TfidfVectorizer(tokenizer=word_tokenize, token_pattern=None)

ctv.fit(corpus)
corpus_transformed = ctv.transform(corpus)
print(corpus_transformed)

هذا يعطي الناتج التالي:

(0, 27)	0.2965698850220162
  (0, 16)	0.4428321995085722
  (0, 14)	0.4428321995085722
  (0, 7)	0.4428321995085722
  (0, 4)	0.35727423026525224
  (0, 2)	0.4428321995085722
  (1, 27)	0.35299699146792735
  (1, 24)	0.2635440111190765
.
.
.
.

نرى أنه بدلاً من القيم الصحيحة ، نحصل هذه المرة على عدد عائم. استبدال CountVectorizer بـ TfidfVectorizer هو أيضًا بسيط. تقدم Scikit-Learn أيضًا TfidfTransformer. إذا كان لديك قيم عدد ، يمكنك استخدام TfidfTransformer والحصول على نفس السلوك مثل TfidfVectorizer.

import pandas as pd
from nltk.tokenize import word_tokenize
from sklearn import linear_model
from sklearn import metrics
from sklearn import model_selection
from sklearn.feature_extraction.text import TfidfVectorizer

if __name__=="__main__":
    # قراءة مجموعة البيانات 
    df = pd.read_csv("NLP\input\IMDB Dataset.csv")

    # تعيين الموجب 1 و السلبي 0
    df.sentiment = df.sentiment.apply(
        lambda x:1 if x == "positive" else 0
    )

    # -1 له القيم  kfold ننشى عامود جديد نسميه 
    df["kfold"] = -1

    # الخطوة التالية هي ترتيب صفوف البيانات بشكل عشوائي
    df = df.sample(frac=1).reset_index(drop=True)

    # الحصول على التسميات 
    y = df.sentiment.values

    # kfold تهيئة 
    kf = model_selection.StratifiedKFold(n_splits=5)

    # تعبئة العامود الجديد
    for f, (t_, v_) in enumerate(kf.split(X=df, y=y)):
        df.loc[v_, 'kfold'] = f
    
    # نمر على الطيات التي أنشئناها
    for fold_ in range(5):
        # إطارات بيانات مؤقتة للتدريب والاختبار
        train_df = df[df.kfold != fold_].reset_index(drop=True)
        test_df  = df[df.kfold == fold_].reset_index(drop=True)
        
        #word_tokenize بواسطة  CountVectorizer تهيئة 
        tfidf_vec = TfidfVectorizer(
            tokenizer= word_tokenize,
            token_pattern=None
        )

        tfidf_vec.fit(train_df.review)

        # تحويل مراجعات بيانات التدريب والتحقق
        xtrain = tfidf_vec.transform(train_df.review)
        xtest  = tfidf_vec.transform(test_df.review)

        # تهيئة نموذج الانحدار اللوجستي
        model = linear_model.LogisticRegression()

        # مناسبة النموذج
        model.fit(xtrain, train_df.sentiment)

        # عمل تنبؤات على بيانات الاختبار
        # عتبة التنبؤات هي 0.5
        preds = model.predict(xtest)

        # حساب الدقة
        accuracy = metrics.accuracy_score(test_df.sentiment, preds)

        print(f"Fold : {fold_}")
        print(f"Accuracy = {accuracy}")
        print(f"")

سيكون من المثير للاهتمام معرفة كيفية أداء TF-IDF مع نموذج الانحدار اللوجستي القديم الخاص بنا على مجموعة بيانات المشاعر.

❯ python tfv_logres.py
Fold: 0
Accuracy = 0.8976
Fold: 1
Accuracy = 0.8998
Fold: 2
Accuracy = 0.8948
Fold: 3
Accuracy = 0.8912
Fold: 4
Accuracy = 0.8995

نرى أن هذه الدرجات أعلى قليلاً من CountVectorizer ، وبالتالي ، تصبح المعيار الجديد الذي نريد التغلب عليه. مفهوم آخر مثير للاهتمام فيمعالجة اللغة الطبيعية هو n-grams

النغرامات N-grams 

N-grams عبارة عن مجموعات من الكلمات بالترتيب. من السهل إنشاء N-grams. تحتاج فقط إلى الاهتمام بالترتيب. لجعل الأمور أكثر إنسيابية، يمكننا استخدام تطبيق n-gram من NLTK.

from nltk import ngrams 
from nltk.tokenize import word_tokenize 

N = 3

# الحملة المدخلة
sentence = "hi, how are you?"

# الحملة بعد التعميل 
tokenized_sentence = word_tokenize(sentence)

# n_grams توليد 
n_grams = list(ngrams(tokenized_sentence,N))
print(n_grams)

الذي يعطي:

[('hi', ',', 'how')
, (',', 'how', 'are')
, ('how', 'are', 'you')
, ('are', 'you', '?')]

وبالمثل ، يمكننا أيضًا إنشاء 2-grams ، أو 4-grams, ، إلخ. الآن ، تصبح هذه النغرامات جزءًا من مفرداتنا ، وعندما نحسب الأعداد أو tf-idf ، فإننا نعتبر كل n-gram  كعملة جديدة  تمامًا.و بالتالي نقوم بدمج السياق إلى حد ما. 

تقدم كل من تطبيقات CountVectorizer و TfidfVectorizer لـ scikit-Learn نجرامات بواسطة معييارngram_range ، والتي لها حد أدنى وحد أقصى.

 بشكل افتراضي ،فهي (1 ، 1). عندما نغيرها إلى (1 ، 3) ، فإننا ننظر إلى  غرام أحادي (unigrams)، و غرام ثنائي (bigrams) ، و غرام ثلاثي (trigrams) .

 تغيير الكود السابق بسيط. نظرًا لأننا حصلنا على أفضل نتيجة حتى الآن مع tf-idf ، فلنرى ما إذا كان تضمين n-grams سيحسن النموذج. التغيير الوحيد المطلوب هو في تهيئة TfidfVectorizer

 tfidf_vec = TfidfVectorizer(
 tokenizer=word_tokenize,
 token_pattern=None,
 ngram_range=(1, 3)
 )

دعونا نرى ما إذا كان لدينا أي نوع من التحسينات.

 python tfv_logres_trigram.py
Fold: 0
Accuracy = 0.8931
Fold: 1
Accuracy = 0.8941
Fold: 2
Accuracy = 0.897
Fold: 3
Accuracy = 0.8922
Fold: 4
Accuracy = 0.8847

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

التشذيب و اللملة (Stemming and lemmatization)

هناك الكثير من الأشياء في أساسيات معالجة اللغة الطبيعية. مصطلح واحد يجب أن تكون على دراية به هو التشذيب (Stemming). آخر هو اللمات (lemmatization). يختزل االتشذيب و اللملة الكلمة إلى أصغر أشكالها.

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

يأتي كل من التشذيب و اللملة  من علم اللغة. وتحتاج إلى معرفة متعمقة بلغة معينة إذا  كنت تخطط لإنشاء لتشذيب و لملمة تلك اللغة. يمكن إجراء كل من التشذيب و اللملة بسهولة باستخدام حزمة NLTK. دعونا نلقي نظرة على بعض الأمثلة لكليهما. هناك العديد من الأنواع المختلفة من التشذيب و اللملة . سأعرض مثالاً باستخدام أكثر أنواع شيوعاً  Snowball Stemmer و WordNet Lemmatizer.

from nltk.stem import WordNetLemmatizer
from nltk.stem.snowball import SnowballStemmer

# تهيئة اللملة
lemmatizer  = WordNetLemmatizer()

# تهيئة التشذيب
stemmer = SnowballStemmer("english")

words = ["fishing", "fishes", "fished"]

for word in words :
    print(f"word={word}")
    print(f"stemmed_word= {stemmer.stem(word)}")
    print(f"lemma = {lemmatizer.lemmatize(word)}")
    print("")

الذي يعطي:

word=fishing
stemmed_word= fish
lemma = fishing

word=fishes
stemmed_word= fish
lemma = fish

word=fished
stemmed_word= fish
lemma = fished

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

إستخراج الموضوع (topic extraction)

هناك موضوع آخر يجب أن تكون على دراية به وهو استخراج الموضوع. يمكن إجراء استخراج الموضوع باستخدام non-negative matrix factorization (NMF)  أو latent semantic analysis (LSA) ، والذي يُعرف أيضًا باسم تحليل القيمة المفردة أو SVD. هذه هي تقنيات التحلل التي تقلل البيانات إلى عدد معين من المكونات. يمكنك وضع أي من هذه في مصفوفة متناثرة التي تم الحصول عليها من CountVectorizer أو TfidfVectorizer.

 دعونا نطبقه على TfidfVetorizer الذي استخدمناه من قبل.

import pandas as pd 
from nltk.tokenize import word_tokenize
from sklearn import decomposition
from sklearn.feature_extraction.text import TfidfVectorizer

# قراءة مجموعة البيانات 
# نحتاج إلى 10 ألاف عينة فقط
corpus = pd.read_csv("../input/IMDB Dataset.csv", nrows=10000)
corpus = corpus.review.values

# word_tokenize بإستخدام TfidfVectorizer تهيئة

tfv = TfidfVectorizer(tokenizer=word_tokenize, token_pattern=None)

# مناسبة التعميل مع مجموعة البيانات
tfv.fit(corpus)

# tfidf تحويل
corpus_transformed = tfv.transform(corpus)

# SVD تهيئة
svd = decomposition.TruncatedSVD(n_components=10)

# SVD  مناسبة
corpus_svd = svd.fit(corpus_transformed)

# اختر العينة الأولى وقم بإنشاء قاموس
#svd من أسماء الميزات ونتائجها من 
#إلى sample_index يمكنك تغيير متغير  
# الحصول على قاموس لأية عينة أخرى
sample_index = 0
feature_scores = dict(
    zip(
        tfv.get_feature_names(),
        corpus_svd.components_[sample_index]
        )
)

# بمجرد أن نحصل على القاموس ، يمكننا الآن
# بفرزها بترتيب تنازلي واحصل على ملف
#مواضيع N أعلى  
N = 5
print(sorted(feature_scores, key=feature_scores.get, reverse=True)[:N])

يمكنك تشغيله لعينات متعددة باستخدام حلقة.

N = 5 

for sample_index in range(5):
    feature_scores = dict(
        zip(
            tfv.get_feature_names(),
            corpus_svd.components_[sample_index]
        )
    )
    print(
        sorted(
            feature_scores,
            key=feature_scores.get,
            reverse=True
        )[:N]
    )

هذا يعطي الناتج التالي.

['the', ',', '.', 'a', 'and']
['br', '<', '>', '/', '-']
['i', 'movie', '!', 'it', 'was']
[',', '!', "''", '``', 'you']
['!', 'the', "''", '``', '...']

يمكن أن نرى أنه لا معنى له على الإطلاق. لذلك دعونا نحاول التنظيف ومعرفة ما إذا كان ذلك منطقيًا.

 لتنظيف أي بيانات نصية ، خاصةً عندما تكون في pandas dataframe ، يمكنك إنشاء وظيفة.

import re
import string

def clean_text(s):
    """
    هذه الوظيفة تقوم بتنظيف النص
    
    """
    
    # تقسيم النص
    s = s.split()
    
    # ضم العملات بمسافة واحدة
    # سيتخلص هذا من كل المسافات الغربية
    s = " ".join(s)
    
    #regex إزالة جميع علامات الترقيم باستخدام 
    s = re.sub(f'[{re.escape(string.punctuation)}]', '', s)
    
    return s

هذه الوظيفة ستحول سلسلة مثل “hi, how are you????” إلى “hi how are you”. دعنا نطبق هذه الوظيفة على كود SVD القديم ونرى ما إذا كانت تجلب أي قيمة للموضوعات المستخرجة. مع pandas ، يمكنك استخدام وظيفة “apply” لتطبيق كود التنظيف على أي عمود معين.

import pandas as pd 
from nltk.tokenize import word_tokenize
from sklearn import decomposition
from sklearn.feature_extraction.text import TfidfVectorizer

# قراءة مجموعة البيانات 
# نحتاج إلى 10 ألاف عينة فقط
corpus = pd.read_csv("../input/IMDB Dataset.csv", nrows=10000)
corpus.loc[:, "review"] = corpus.review.apply(clean_text)
corpus = corpus.review.values

# word_tokenize بإستخدام TfidfVectorizer تهيئة

tfv = TfidfVectorizer(tokenizer=word_tokenize, token_pattern=None)

# مناسبة التعميل مع مجموعة البيانات
tfv.fit(corpus)

# tfidf تحويل
corpus_transformed = tfv.transform(corpus)

# SVD تهيئة
svd = decomposition.TruncatedSVD(n_components=10)

# SVD  مناسبة
corpus_svd = svd.fit(corpus_transformed)

# اختر العينة الأولى وقم بإنشاء قاموس
#svd من أسماء الميزات ونتائجها من 
#إلى sample_index يمكنك تغيير متغير  
# الحصول على قاموس لأية عينة أخرى
sample_index = 0
feature_scores = dict(
    zip(
        tfv.get_feature_names(),
        corpus_svd.components_[sample_index]
        )
)

# بمجرد أن نحصل على القاموس ، يمكننا الآن
# بفرزها بترتيب تنازلي واحصل على ملف
#مواضيع N أعلى  
N = 5
print(sorted(feature_scores, key=feature_scores.get, reverse=True)[:N])

لاحظ أننا أضفنا سطرًا واحدًا فقط إلى كود SVD الرئيسي لدينا . المواضيع التي تم إنشاؤها هذه المرة تبدو كما يلي.

['the', 'a', 'and', 'of', 'to']
['i', 'movie', 'it', 'was', 'this']
['the', 'was', 'i', 'were', 'of']
['her', 'was', 'she', 'i', 'he']
['br', 'to', 'they', 'he', 'show']

هذا أفضل مما كان لدينا سابقًا. ولكن هل تعلم؟ يمكنك تحسينه عن طريق إزالة كلمات التوقف في وظيفة التنظيف. ما هي كلمات التوقف؟ هذه كلمات تتكرر كثيراً  موجودة في كل لغة. على سبيل المثال ، في اللغة الإنجليزية ، هذه الكلمات هي “a” ، “an” ، “the” ، “for” ، إلخ. إزالة كلمات التوقف ليست دائمًا خيارًا حكيمًا وتعتمد كثيرًا على المشكلة التي نعمل عليها. ستصبح جملة مثل I need a new dog  بعد إزالة كلمات التوقف “need new dog” ، لذلك لا نعرف من يحتاج إلى كلب جديد.

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

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

تضمينات الكلمة (word embeddings)

لقد رأيت أنه حتى الآن قمنا بتحويل العملات إلى أرقام. لذلك ، إذا كان هناك N من العملات في مجموعة معينة ، فيمكن تمثيلها بأعداد صحيحة تتراوح من 0 إلى N-1. الآن سوف نمثل هذه العملات كمتجهات. يُعرف تمثيل الكلمات في المتجهات هذا باسم تضمينات الكلمة أو متجهات الكلمات. 

يعد Word2Vec من Google أحد أقدم الطرق لتحويل الكلمات إلى متجهات. لدينا أيضًا FastText من Facebook و GloVe (المتجهات العالمية لتمثيل الكلمات) من ستانفورد. هذه الأساليب مختلفة تمامًا عن بعضها البعض.

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

يُعرف هذا النهج أيضًا باسم حقيبة الكلمات المستمرة (Continuous Bag of Words) أو نموذج CBoW. يمكنك أيضًا محاولة أخذ كلمة واحدة والتنبؤ بكلمات السياق بدلاً من ذلك. هذا يسمى نموذج التخطي الغرام (skip-gram model). يمكن لـ Word2Vec تعلم التضمين باستخدام هاتين الطريقتين. 

يتعلم FastText التضمينات للحروف n-grams بدلاً من ذلك. تمامًا مثل كلمة n-grams ، إذا استخدمنا أحرفًا ، فإنها تُعرف باسم الحرف n-grams ، وأخيراً ، يتعلم GloVe هذه التضمينات باستخدام مصفوفات التكرار.

الصورة أعلاه توضح أنك إذا طرحت متجهً Germany من متجه Berlin (عاصمة ألمانيا) وأضفت متجهة France إليها ، فستحصل على متجه قريبًا من متجه Paris (عاصمة France ). هذا يدل على أن التضمينات  تعمل أيضًا مع المقارنات. هذا ليس صحيحًا دائمًا ، ولكن مثل هذه الأمثلة مفيدة لفهم  فائدة تضمينات الكلمة. يمكن تمثيل جملة مثل  “hi, how are you ” بمجموعة من المتجهات على النحو التالي.

hi ─> [vector (v1) of size 300]
, ─> [vector (v2) of size 300]
how ─> [vector (v3) of size 300]
are ─> [vector (v4) of size 300]
you ─> [vector (v5) of size 300]
? ─> [vector (v6) of size 300]

هناك طرق متعددة لاستخدام هذه المعلومات. من أبسط الطرق لاستخدام التضمينات هي إستخدامها كما هي. كما ترى في المثال أعلاه ، لدينا متجه تضمين 1×300 لكل كلمة. باستخدام هذه المعلومات ، يمكننا حساب التضمين للجملة بأكملها. هناك طرق متعددة للقيام بذلك. 

يتم عرض الطريقة على اأدناه . في هذه الوظيفة ، نأخذ جميع متجهات الكلمات الفردية في جملة معينة وننشئ متجهة تسوية  للكلمة من جميع متجهات الكلمات في العملات. هذا يوفر لنا متجه الجملة.

import numpy as np 

def sentence_to_vec(s , embedding_dict, stop_words, tokenizer):
    
    # تحويل الجملة إلى سلسلة وحروف صغيرة
    words = str(s).lower()
    
    # تعميل الجملة
    words = tokenizer(words)
    #إزالة كلمات التوقف
    words = [ w for w in words if not w in stop_words]
    
    #alpha-numeric  الإبقاء فقط على عملات
    words = [ w for w in words if w.isalpha()]
    
    # تهيئة  قائمة فارغة لحقظ التضمينات
    M = []
    for w in words:
        # لكل كلمة ، قم بإحضار التضمين من
        # القاموس وإلحاقه بقائمة
        # التضمينات
        if w in embedding_dict:
            M.append( embedding_dict[w])
    # إذا لم يكن لدينا أي متجهات ، فقم بإرجاع الأصفار
    if len(M) ==0:
        return np.zeros(300)
    
    #تحويل القائمة إلى مصفوفة تضمينات
    M = np.array(M)
    
    v = M.sum(axis=0)
    
    return v / np.sqrt((v ** 2).sum())
    

يمكننا استخدام هذه الطريقة لتحويل كل الأمثلة إلى متجه واحد. ربما يمكننا إستخدام متجهات fastText لدينا 300 سمة لكل مراجعة

import io
import numpy as np
import pandas as pd

from nltk.tokenize import word_tokenize
from sklearn import linear_model
from sklearn import metrics
from sklearn import model_selection
from sklearn.feature_extraction.text import TfidfVectorizer

def load_vectors(fname):
    fin = io.open(
             fname,
             'r',
             encoding='utf-8',
             newline='\n',
             errors = 'ignore'
    )

    n, d = map(int, fin.readline().split())
    data = {}
    for line in fin:
        tokens = line.rstrip().split(' ')
        data[tokens[0]] = list(map(float, tokens[1:]))
    return data

def sentence_to_vec(s , embedding_dict, stop_words, tokenizer):
    
    # تحويل الجملة إلى سلسلة وحروف صغيرة
    words = str(s).lower()
    
    # تعميل الجملة
    words = tokenizer(words)
    #إزالة كلمات التوقف
    words = [ w for w in words if not w in stop_words]
    
    #alpha-numeric  الإبقاء فقط على عملات
    words = [ w for w in words if w.isalpha()]
    
    # تهيئة  قائمة فارغة لحقظ التضمينات
    M = []
    for w in words:
        # لكل كلمة ، قم بإحضار التضمين من
        # القاموس وإلحاقه بقائمة
        # التضمينات
        if w in embedding_dict:
            M.append( embedding_dict[w])
    # إذا لم يكن لدينا أي متجهات ، فقم بإرجاع الأصفار
    if len(M) ==0:
        return np.zeros(300)
    
    #تحويل القائمة إلى مصفوفة تضمينات
    M = np.array(M)
    
    v = M.sum(axis=0)
    
    return v / np.sqrt((v ** 2).sum())

if __name__=="__main__":
    # قراءة مجموعة البيانات 
    df = pd.read_csv("NLP\input\IMDB Dataset.csv")

    # تعيين الموجب 1 و السلبي 0
    df.sentiment = df.sentiment.apply(
        lambda x:1 if x == "positive" else 0
    )

    # -1 له القيم  kfold ننشى عامود جديد نسميه 
    #df["kfold"] = -1

    # الخطوة التالية هي ترتيب صفوف البيانات بشكل عشوائي
    df = df.sample(frac=1).reset_index(drop=True)

    # تحميل التضمينات في الذاكرة 
    print("Loading embeddings")
    embeddings = load_vectors("NLP\input\crawl-300d-2m.vec")

    # إنشاء تضمينات الجملة
    print("Creating sentence vectors")
    vectors = []
    for review in df.review.values:
        vectors.append(
            sentence_to_vec(
                s = review,
                embedding_dict = embeddings,
                stop_words= [],
                tokenizer= word_tokenize
            )
        )

    vectors = np.array(vectors)
    # الحصول على التسميات 
    y = df.sentiment.values

    # kfold تهيئة 
    kf = model_selection.StratifiedKFold(n_splits=5)

    # تعبئة العامود الجديد
    for fold_, (t_, v_) in enumerate(kf.split(X=df, y=y)):
        print(f"Training fold: {fold_}")
        # إطارات بيانات مؤقتة للتدريب والاختبار
        xtrain = vectors[t_, :]
        ytrain = y[t_]

        xtest =vectors[v_, :]
        ytest = y[v_]

        # تهيئة نموذج الانحدار اللوجستي
        model = linear_model.LogisticRegression()

        # مناسبة النموذج
        model.fit(xtrain, ytrain)

        # عمل تنبؤات على بيانات الاختبار
        # عتبة التنبؤات هي 0.5
        preds = model.predict(xtest)

        # حساب الدقة
        accuracy = metrics.accuracy_score(ytest, preds)

        print(f"Accuracy = {accuracy}")
        print("")

هذا يعطي النتائج التالية.

❯ python fasttext.py
Loading embeddings
Creating sentence vectors
Training fold: 0
Accuracy = 0.8619
Training fold: 1
Accuracy = 0.8661
Training fold: 2
Accuracy = 0.8544
Training fold: 3
Accuracy = 0.8624
Training fold: 4
Accuracy = 0.8595

حصلنا على نتائج ممتازة ، وكل ما فعلناه هو استخدام تضمينات FastText. 

الشبكات العصبية 

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

هذا يعني أنه يمكننا استخدام النماذج المستخدمة على نطاق واسع لبيانات السلاسل الزمنية مثل الذاكرة طويلة  قصيرة المدى (LSTM) أو الوحدات المتكررة ذات البوابات (GRU) أو حتى الشبكات العصبية التلافيفية (CNNs). 

شبكة الذاكرة طويلة قصيرة المدى 

دعونا نرى كيفية تدريب نموذج LSTM ثنائي الاتجاه بسيط على مجموعة البيانات هذه. في البداية سننشئ مشروعًا. لا تتردد في تسميته كما تريد. وبعد ذلك ستكون خطوتنا الأولى هي تقسيم البيانات للتحقق المتقاطع.

import pandas as pd
from sklearn import model_selection

if __name__ == '__main__':

    # قراءة ملفات التدريب
    df = pd.read_csv("NLP\input\IMDB Dataset.csv")

    # تعيين الموجب 1 و السلبي 0
    df.sentiment = df.sentiment.apply(
        lambda x:1 if x == "positive" else 0
    )

    # -1 و نمله بـ kfold ننشأ عامود جديد نسميه 
    df["kfold"] = -1

    # الخطوة التاليه هي عشوائية صفوف البيانات 
    df = df.sample(frac=1).reset_index(drop=True)

    # الحصول على المسيات
    y = df.review.values

    #  model_selection من  kfold تهيئة كلاس 
    kf = model_selection.StratifiedKFold(n_splits=5)

    # kfold ملء عامود 

    for f , (t_, v_) in enumerate(kf.split(X=df, y=y)):
        df.loc[v_, 'kfold'] = f

    # حفظ ملف الجديد مع عامود الطيات
    df.to_csv("NLP\input\imdb_folds.csv", index=False)

بمجرد تقسيم مجموعة البيانات إلى طيات ، نقوم بإنشاء فئة مجموعة بيانات بسيطة في dataset.py. تُرجع فئة مجموعة البيانات عينة واحدة من بيانات التدريب أو التحقق.

import torch

class IMDBDataset:
    def __init__(self, reviews, targets):

        self.reviews = reviews
        self.targets = targets
    
    def __len__(self):
        return len(self.reviews)
    
    def __getitem__(self , item):
        
        # لأي عنصر معين ، وهو عدد صحيح ،
        # إرجاع المراجعات و الأهداف  كتنسورات
        # هو فهرس العنصر المعني item    
        review = self.reviews[item, :]
        target = self.targets[item]
        return {
            "review": torch.tensor(review, dtype=torch.long),
            "target": torch.tensor(target, dtype=torch.float)
        }

بمجرد الانتهاء من فئة مجموعة البيانات ، يمكننا إنشاء lstm.py الذي يتكون من نموذج LSTM الخاص بنا.

import torch
import torch.nn as nn

class LSTM(nn.Module):
    def __init__(self, embedding_matrix):
        super(LSTM, self).__init__()
        # عدد الكلمات = عدد الصفوف في مصفوفة التضمين
        num_words = embedding_matrix.shape[0]

        # بُعد التضمين هو عدد الأعمدة في المصفوفة
        embed_dim = embedding_matrix.shape[1]

        # نحدد مُدخل طبقة التضمين 
        self.embedding = nn.Embedding(
            num_embeddings=num_words,
            embedding_dim=embed_dim
        )

        # يتم استخدام مصفوفة التضمين كأوزان
        # طبقة التضمين
        self.embedding.weight = nn.Parameter(
            torch.tensor(
                embedding_matrix,
                dtype=torch.float32
            )
        )

        # لا نريد تدريب التضمينات المدربة المسبقة
        self.embedding.weight.requires_grad = False

        # بسيطة ثنائية الإتجاه مع LTSM
        # الحجم المخفي 128
        self.lstm = nn.LTSM(
            embed_dim,
            128,
            bidirectional = True,
            batch_first = True, 
        )

        # طبقة الإخراج وهي طبقة خطية
        # لدينا ناتج واحد فقط
        # المدخل 512 = 128 + 128 بالنسبة للمتوسط و كذلك التجميع الأقصى
        self.out = nn.Linear(512, 1)

    def forward(self, x):
        # تمرير البيانات من خلال طبقة التضمين
        # الإدخال هو مجرد العملات
        x = self.embedding(x)

        # ltsm نقل مخرجات التضمين إلى
        x, _ =self.lstm(x)

        #lstm تطبيق المتوسط و التجميع الأقصى على مخرج
        avg_pool = torch.mean(x, 1 )
        max_pool, _ = torch.max(x, 1)

        # ربط المتوسط و التجميع الأقصى
        # حجم كل منهم 128 لكل إتجاه ، و بالتالي 512 في الإتحاهين
        out = torch.cat ((avg_pool, max_pool), 1)

        # المرور من خلال طبقة الإخراج 
        out = self.out(out)

        return out 

الآن ، نقوم بإنشاء engine.py الذي يتكون من وظائف التدريب والتقييم الخاصة بنا.

import torch
import torch.nn as nn

def train(data_loader, model, optimizer, device):
    """
    هذه الوظيفة الرئيسية التي ستدرب النموذج
    لحزمة واحدة

    """

    # وضع النموذج في وضع التدريب
    model.train()

    # المرور من خلال حزم البيانات 
    for data in data_loader:
        # الحصول على المراجعات و الأهداف
        reviews = data["review"]
        targets = data["target"]

        # نقل البيانات إلى الأجهزة التي نرغب في إستخدامها
        reviews = reviews.to(device, dtype=torch.long)
        targets = targets.to(device, dtype=torch.float)

        # تفريغ الإشتقاقات 
        optimizer.zero_grad()

        # عمل تنبؤات من النموذج
        predictions = model(reviews)

        # حساب الخسارة
        loss = nn.BCEWithLogitsLoss()(
            predictions,
            targets.view(-1, 1)
        )

        loss.backward()
        optimizer.step()

def evaluate(data_loader, model, device):
    # تهيئة القوائم الفارغة لتخزين التوقعات
    # والأهداف
    final_predictions = []
    final_targets = []

    # وضع النموذج في وضع التحقق
    model.eval()

    # تعطيل حساب الإشتقاقات
    with torch.nn_grad():
        for data in data_loader:
            reviews = data["review"]
            targets = data["target"]
            reviews = reviews.to(device, dtype=torch.long)
            targets = targets.to(device, dtype=torch.float)

            # عمل التنبوات
            predictions = model(reviews)

            # نقل التوقعات والأهداف إلى القائمة
            # نحن بحاجة إلى نقل التوقعات والأهداف إلى وحدة المعالجة المركزية أيضًا
            predictions = predictions.cpu().numpy().tolist()
            targets = data["target"].cpu().numpy().tolist()
            final_predictions.extend(predictions)
            final_targets.extend(targets)
    
    return final_predictions, final_targets

            

ستساعدنا هذه الوظائف في train.py الذي يستخدم لتدريب طيات متعددة.

import io
from tensorflow.python.data.ops.optional_ops import Optional
from tensorflow.python.eager.context import device
import torch
import numpy as np
import pandas as pd

import  tensorflow as tf
from sklearn import metrics
import config
import dataset
import engine
import ltsm

def load_vectors(fname):
    fin = io.open(fname, 'r', encoding='utf-8', newline='\n', errors='ignore')
    n, d = map(int, fin.readline().split())
    data = {}
    for line in fin:
        tokens = line.rstrip().split(' ')
        data[tokens[0]] = map(float, tokens[1:])
    return data

def create_embedding_matrix(word_index, embedding_dict):
    # تهيئة المصفوفة بالأصفار
    embedding_matrix = np.zeros((len(word_index) + 1 , 300)) 
    # إنشاء حلقة لكل الكلمات
    for word, i in word_index.item():
        # إذا تم العثور على كلمة في التضمينات المدربة مسبقًا ،
        # تحديث المصفوفة. إذا لم يتم العثور على الكلمة ،
        # المتجه هو الأصفار!
        if word in embedding_dict:
            embedding_matrix[i] = embedding_dict[word]
    return embedding_matrix

def run(df, fold):
    # إحضار إطار بيانات التدريب
    train_df = df[df.kfold != fold].reset_index(drop=True)

    # إحضار إطار بيانات التحقق
    valid_df = df[df.kfold == fold].reset_index(drop=True)

    print("fitting tokenizer")

    # نستخدم كيراس من أجل التعميل 
    tokenizer = tf.keras.preprocessing.text.Tokenizer()
    tokenizer.fit_on_texts(df.review.values.tolist())

    # تحويل بيانات التدريب إلى تسلسلات
    # كمثال
    # [24, 27]  تصبح  "bad movie" 
    #Bad  حيث 24 هو معرف
    #movie بينما 27 هو معرف
    xtrain = tokenizer.texts_to_sequences(train_df.review.values)

    # تحويل بيانات التحقق بالمثل إلى
    # تسلسل
    xtest = tokenizer.texts_to_sequences(valid_df.review.values)

    # إنشاء حشو صفري على بيانات التدريب
    # بعد إعطائها أعلى طول
    xtrain = tf.keras.preprocessing.sequence.pad_sequences(
        xtrain, maxlen=config.MAX_LEN
    )

    xtest = tf.keras.preprocessing.sequence.pad_sequences(
        xtest, maxlen=config.MAX_LEN
    )

    #تهيئة بيانات التدريب 
    train_dataset = dataset.IMDBDataset(
        reviews=xtrain,
        targets = train_df.sentiment.values
    )

    # تحميل البيانات
    train_data_loader = torch.utils.data.DataLoader(
        train_dataset,
        batch_size=config.TRAIN_BATCH_SIZE,
        num_workers=2
    )

    # تهيئة التحقق
    valid_dataset = dataset.IMDBDataset(
        reviews=xtest,
        targets = train_df.sentiment.values
    )

    # إنشاء محمل بيانات للتحقق
    valid_data_loader = torch.utils.data.DataLoader(
        valid_dataset,
        batch_size=config.VALID_BATCH_SIZE,
        num_workers=1
    )

    print("Loading embeddings")
    # تحميل التضمينات 
    embedding_dict = load_vectors("NLP/input/crawl-300d-2M.vec")
    embedding_matrix = create_embedding_matrix(
    tokenizer.word_index, embedding_dict
    )

    #للتدريب GPU سنستخدم الـ
    device = torch.device("cuda")

    #LTSM الحصول على نموذج
    model = ltsm.LTSM(embedding_matrix)

    # Device إرسال النموذج إلى 
    model.to(device)

    # تهيئة محسن أدام
    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

    print("Training Model")
    # وضع أفضل دقة تساوي صفر
    best_accuracy = 0
    # وضع تعدادالتوقف إلى صفر
    early_stopping_counter = 0
    # التدريب و التحقق لكل الحقب
    for epoch in range(config.EPOCHS):
        # تدريب حقبه واحده
        engine.train(train_data_loader, model, optimizer, device)
        #  التحقق
        outputs, targets = engine.evaluate(
            valid_data_loader, model, device
        )

        # استخدم عتبة 0.5
        # يرجى ملاحظة أننا نستخدم طبقة خطية ولا يوجد سيغمويد
        # يجب أن نطبق هذه العتبة بعد السيغمويد
        outputs = np.array(outputs) >=0.5

        # حساب الدقة
        accuracy = metrics.accuracy_score(targets, outputs)
        print(
            f"FOLD:{fold}, Epoch: {epoch}, Accuracy Score = {accuracy}"
        )

        if accuracy > best_accuracy:
            best_accuracy = accuracy
        else:
            early_stopping_counter +=1
        if early_stopping_counter > 2:
            break

if __name__ == "__main__":

    # تحميل البيانات 
    df = pd.read_csv("NLP\input\imdb_folds.csv")

    # تدريب الطيات
    run(df, fold=0)
    run(df, fold=1)
    run(df, fold=2)
    run(df, fold=3)
    run(df, fold=4)

وأخيرًا ، لدينا config.py.

MAX_LEN = 128
TRAIN_BATCH_SIZE = 16
VALID_BATCH_SIZE = 8
EPOCHS = 10

دعونا نرى ما يعطينا هذا.

❯ python train.py
FOLD:0, Epoch: 3, Accuracy Score = 0.9015
FOLD:1, Epoch: 4, Accuracy Score = 0.9007
FOLD:2, Epoch: 3, Accuracy Score = 0.8924
FOLD:3, Epoch: 2, Accuracy Score = 0.9
FOLD:4, Epoch: 1, Accuracy Score = 0.878

هذه إلى حد الأن أفضل نتيجة حصلنا عليها. يرجى ملاحظة أنني عرضت فقط الحقب بأفضل دقة في كل طية. 

لابد أنك لاحظت أننا استخدمنا التضمينات المدربة مسبقًا و LSTM ثنائي الاتجاه. إذا كنت تريد تغيير النموذج ، فيمكنك فقط تغيير النموذج في lstm.py والاحتفاظ بكل شيء كما هو.

 يتطلب هذا النوع من الكود تغييرات طفيفة لإجراء التجارب ويمكن فهمه بسهولة. على سبيل المثال ، يمكنك تدريب تضميناتك بمفردك بدلاً من استخدام التضمينات المدربة مسبقاً ، ويمكنك استخدام بعض التضمينات  الأخرى ، ويمكنك الجمع بين العديد من التضمينات المدربة مسبقاً ، ويمكنك استخدام GRU ، ويمكنك استخدام الإسقاط المكاني (spatial dropout) بعد التضمين ، ويمكنك إضافة طبقة GRU بعد LSTM ، يمكنك إضافة طبقتين LSTM ، يمكنك الحصول على تكوين LSTM-GRU-LSTM ، ويمكنك استبدال LSTM بطبقة لف رياضي، وما إلى ذلك دون إجراء العديد من التغييرات على الكود. 

المحول (Transformer)

الشبكات القائمة على المحولات قادرة على التعامل مع التبعيات طويلة المدى بطبيعتها. تنظر LSTM إلى الكلمة التالية فقط عندما ترى الكلمة السابقة. هذا ليس هو الحال مع المحولات. بحيث يمكن أن ينظر إلى كل الكلمات في كل الجملة في وقت واحد. 

نتيجة لذلك ، هناك ميزة أخرى تتمثل في أنه يمكن موازنتها بسهولة واستخدام وحدات معالجة الرسومات بشكل أكثر كفاءة. تعتبر المحولات موضوعًا واسعًا للغاية ، وهناك العديد من الأنمذجة: BERT و RoBERTa و XLNet و XLM-RoBERTa و T5 وما إلى ذلك.

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

أول شيء نقوم به هو إنشاء ملف التكوين  config.py.

import transformers

# أقصى عدد عملات لكل جملة
MAX_LEN = 512

# عدد الحزم صغير لأن النموذج ضخم
TRAIN_BATCH_SIZE = 8
VALID_BATCH_SIZE = 4

# العدد الأقصى للحقب
EPOCHS = 100

# تحديد مسار النموذج
BERT_PATH = "../input/bert_base_uncased"

# هنا نحفظ النموذج
MODEL_PATH = "model.bin"

#  تدريب الملفات
TRAINING_FILE = "../input/imdb dataset.csv"

# تحديد التعميل
#  سنسخدم التعميل و النموذج من 
# hugginface محولات 
TOKENIZER = transformers.bertTokenizer.from_pretrained(
    BERT_PATH,
    do_lower_case = True
)

ملف التكوين هنا هو المكان الوحيد الذي نحدد فيه التعميل و المعايير الأخرى التي نود تغييرها بشكل متكرر – وبهذه الطريقة يمكننا إجراء العديد من التجارب دون الحاجة إلى الكثير من التغييرات.

 الخطوة التالية هي بناء فئة مجموعة بيانات dataset.py.

import config
import torch

class BERTdatabase:
    def __init__(self, review, target):
        self.review = review
        self.target = target

        #config.py من  TOKENIZER و  MAX_LEN نحصل على 
        self.tokenizer = config.TOKENIZER
        self.max_len = config.MAX_LEN

    def __len__(self):
        return len(self.review)
    
    def __getitem__(self, item):

        # لفهرس عنصر معين ، قم بإرجاع القاموس
        # من المدخلات
        review = str(self.review[item])
        review = " ".join(review.split)

        # hugginface يأتي من محولات  encode_plus  
        # وموجود لجميع التعميلات التي يقدمونها
        # يمكن استخدامه لتحويل سلسلة معينة
        # للمعرفات والقناع ومعرفات نوع العملة التي هي
        # BERT مطلوب لنماذج مثل 
        # هنا ، المراجعة عبارة عن سلسلة

        inputs = self.tokenizer.encode_plus(
            review,
            None,
            add_special_tokens=True,
            max_length = self.max_len,
            pat_to_max_length=True,

        )

        # المعرفات هي معرفات العملات التي تم إنشاؤها
        # بعد تعميل المراجعات 

        ids = inputs["input_ids"]

        # القناع هو 1 حيث لدينا مدخلات
        # و 0 حيث لدينا حشو

        mask = inputs["attention_mask"]

        # معرفات نوع العملة تتصرف بنفس الطريقة مثل
        # قناع في هذه الحالة بالذات
        # في حالة وجود جملتين ، فهذا يساوي 0
        # للجملة الأولى و 1 للجملة الثانية 
        token_type_ids = inputs["token_type_ids"]

        return {
            "ids" : torch.tensor(
                ids, dtype=torch.long
            ),
            
            "mask" : torch.tensor(
                mask, dtype=torch.long
            ),

            "token_type_id" : torch.tensor(
                token_type_ids, dtype=torch.long
            ),
            "targets" : torch.tensor(
                self.target[item], dtype=torch.long
            ), 
        }


والآن نأتي إلى قلب المشروع ، أي النموذج model.py.

import config
import transformers
import torch.nn as nn

class BERTBaseUncased(nn.Module):
    def __init__(self):
        super(BERTBaseUncased, self).__init__()
        #BERT_PATH  نحصل على النموذج من المسار المحدد في
        #config.py داخل 
        self.bert = transformers.BertModel.from_pretrained(
            config.BERT_PATH
        )
        # نستخدم الإسقاط لغرض التنظيم
        self.bert_drop = nn.Dropout(0.5)

        #طبقة إخراج خطية
        self.out = nn.Linear(768,1)
    
    def forward(self,ids, mask, token_type_ids):
        #  في إعداداته الافتراضية يرجع ناتجين BERT 
        # آخر حالة مخفية وإخراج طبقة محمع بيرت
        # نستخدم ناتج المجمّع الذي له الحجم
        # (batch_size، hidden_size)
        # يمكن أن يكون الحجم المخفي 768 أو 1024 حسب
        # إذا كنا نستخدم  بيرت  الأساسي أو الكبير
        # في حالتنا هو 768
        # لاحظ أن هذا النموذج بسيط جدًا
        # قد ترغب في استخدام آخر حالة مخفية
        # أو عدة حالات مخفية

        _, o2 = self.bert(
            ids,
            attention_mask=mask,
            token_type_ids=token_type_ids
            )
        
        # نمر على طبقة الإسقاط
        bo = self.bert_drop(o2)
        # نمر على الطبقو الخطية
        output = self.out(bo)
        
        return output
    
        

يقوم هذا النموذج بإرجاع ناتج واحد. يمكننا استخدام خسارة الإنتروبيا المتقاطعة مع اللوغاريتمات التي تطبق أولاً دالة السيغمويد ثم تحسب الخسارة. يتم ذلك في engine.py

import torch
import torch.nn as nn 

def loss_fn(outputs, targets):
    return nn.BCEWithLogitsLoss()(outputs,targets.views(-1,1))

def train_fn(data_loader, model, optimizer, device, scheduler):

    # ضع النموذج في وضع التدريب
    model.train()

    # إنشاء حلقة على كل الحزم
    for d in data_loader:
        # استخراج المعرفات ومعرفات العملات والقناع
        # من الحزمة الحالية
        # أيضا استخراج الأهداف

        ids = d["ids"]
        token_type_ids = d["token_type_ids"]
        mask = d["mask"]
        targets = d["targets"]

        # نقل كل شيئ لجهاز محدد
        ids = ids.to(device, dtype=torch.long)
        token_type_ids = token_type_ids.to(device, dtype=torch.long)
        mask = mask.to(device, dtype=torch.long)
        targets = targets.to(device, dtype=torch.float)

        optimizer.zero_grad()

        # نمر عبر النموذج
        outputs = model(
            ids = ids,
            mask = mask,
            token_type_ids = token_type_ids
        )

        # حساب الخسارة
        loss = loss_fn(outputs, targets)
        loss.backward()
        
        optimizer.step()
        scheduler.step()
    
    def eval_fn(data_loader, model, device):
        """
        هذه  وظيفة التحقق  التي تولد
        تنبؤات بشأن بيانات التحقق
        """
        # ضع النموذج في وضع التحقق
        model.eval()

        #تهيئة القوائم الفارغة
        fin_targets =[]
        fin_outputs = []

        with torch.no_grad():
            
            # هذا الجزء هو نفس وظيفة التدريب
            # باستثناء حقيقة أنه لا يوجد
            # للمحسن ولا توجد حساب zero_grad 
            # scheduler للخسارة أو خطوات .
            for d in data_loader:
                ids = d["ids"]
                token_type_ids = d["token_type_ids"]
                mask = d["mask"]
                targets = d["targets"]

                # نقل كل شيئ لجهاز محدد
                ids = ids.to(device, dtype=torch.long)
                token_type_ids = token_type_ids.to(device, dtype=torch.long)
                mask = mask.to(device, dtype=torch.long)
                targets = targets.to(device, dtype=torch.float)

                outputs = model(
                    ids = ids,
                    mask = mask,
                    token_type_ids=token_type_ids
                )

                # تحويل الأهداف إلى وحدة المعالجة المركزية
                #  وتوسيع القائمة النهائية
                targets = targets.cpu().detach()
                fin_targets.extend(targets.numpy().tolist())
                # تحويل المخرجات إلى وحدة المعالجة المركزية
                #  وتوسيع القائمة النهائية
                outputs = torch.sigmoid(outputs).cpu().detach()
                fin_outputs.extend(outputs.numpy().tolist())
        return fin_outputs, fin_targets






وأخيرًا ، نحن جاهزون للتدريب. دعونا نلقي نظرة على نص التدريب!

from torch._C import device
import config
import dataset
import engine
import torch
import pandas as pd
import torch.nn as nn
import numpy as np
from model import BERTBaseUncased
from sklearn import model_selection
from sklearn import metrics
from transformers import AdamW
from transformers import get_linear_schedule_with_warmup

def train():
    # هذه الوظيفة تدرب النموذج

    #بـ "لا شيء  NaN اقرأ ملف التدريب واملأ قيم  "
    # NaN يمكنك أيضًا اختيار إسقاط قيم  
    # في مجموعة بيانات هذه
    dfx = pd.read_csv(config.TRAINING_FILE).fillna("None")

    # المشاعر = 1 إذا كانت موجبة
    # مشاعر أخرى = 0
    dfx.sentiment = dfx.sentiment.apply(
        lambda x: 1 if x =="positive" else 0
    )

    # نقوم بتقسيم البيانات إلى  طية تدريب و تحقق

    df_train, df_valid = model_selection.train_test_split(
        dfx,
        test_size=0.1,
        random_state=42,
        stratify=dfx.sentiment.values
    )

    # إعادة تعيين الفهرس
    df_train = df_train.reset_index(drop=True)
    df_valid = df_valid.reset_index(drop=True)


    # dataset.py من  BERTDataset تهيئة 
    # لمجموعة التدريب
    train_dataset = dataset.BERTdatabase(
        review=df_train.review.values,
        target=df_train.sentiment.values
    )

    # إنشاء محمل البيانات للتدريب
    train_data_loader = torch.utils.data.DataLoader(
        train_dataset,
        batch_size=config.TRAIN_BATCH_SIZE,
        num_workers=4
    )

    
    # dataset.py من  BERTDataset تهيئة 
    # لمجموعة التحقق
    valid_dataset = dataset.BERTdatabase(
        review=df_valid.review.values,
        target=df_valid.sentiment.values
    )

    # إنشاء محمل البيانات للتحقق
    valid_data_loader = torch.utils.data.DataLoader(
        valid_dataset,
        batch_size=config.VALID_BATCH_SIZE,
        num_workers=1
    )

    # لإستخدام معالج الرسومات device تهيئة 
    device = torch.device("cuda")

    # تحميل النموذج و إرسالة إلى معالج الرسومات
    model = BERTBaseUncased()
    model.to(device)

    # إنشاء المعايير التي نريد تحسينها
    param_optimizer = list(model.named_parameters())
    no_decay = ["bias", "LayerNorm.bias", "LayerNorm.weight"]
    optimizer_parameters = [
        {
            "params" : [
                p for n, p in param_optimizer if
                not any(nd in n for nd in no_decay)
            ],
            "weight_decay" : 0.001,        
        },
        {
            "params" : [
                p for n, p in param_optimizer if
                 any(nd in n for nd in no_decay)
            ],
            "weight_decay" : 0.0
        },
    ]
    # حساب عدد خطوات التدريب
    # هذا يستخدمه المجدول
    num_train_steps = int(
        len(df_train) / config.TRAIN_BATCH_SIZE * config.EPOCHS
    )

    #AdamW محسن 
    #هو المحسن الأكثر استخدامًا
    # للشبكات القائمة على المحولات
    optimizer = AdamW(optimizer_parameters, lr=3e-5)

    # جلب المجدول
    scheduler = get_linear_schedule_with_warmup(
        optimizer,
        num_warmup_step = 0,
        num_training_steps = num_train_steps
    )

    # لو كان لديك عدة معالجات حاسوبية
    model = nn.DataParallel(model)

    # نبدأ تدريب الحزم
    best_accuracy = 0
    for epoch in range(config.EPOCHS):
        engine.train_fn(
            train_data_loader, model, optimizer, device, scheduler
        )
        outputs, targets = engine.eval_fn(
            valid_data_loader, model, device
        )
        outputs = np.array(outputs) >= 0.5
        accuracy = metrics.accuracy_score(targets, outputs)
        print(f"Accuracy Score = {accuracy}")
        if accuracy > best_accuracy:
            torch.save(model.state_dict(), config.MODEL_PATH)
            best_accuracy = accuracy

if __name__ == "__main__":
    train()









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

 هذا النموذج يعطي دقة 93٪ و هو هذا أفضل بكثير من أي نموذج آخر. لكن هل يستحق ذلك؟ 

تمكنا من تحقيق 90٪ باستخدام LSTMs ، وهي أبسط بكثير وأسهل تدريبًا وأسرع عندما يتعلق الأمر بالإستنباط. يمكننا تحسين هذا النموذج على الأرجح بنسبة مئوية باستخدام معالجة بيانات مختلفة أو عن طريق ضبط المعايير مثل الطبقات ، والعقد ، و الإسقاط ، ومعدل التعلم ، وتغيير المُحسِّن ، وما إلى ذلك. ثم سنحصل على فائدة 2٪ تقريبًا من BERT. 

من ناحية أخرى ، استغرق BERT وقتًا أطول للتدريب ، ولديه الكثير من المعايير وهو أيضًا بطيء عندما يتعلق الأمر بالاستدلال. في النهاية ، يجب أن تنظر إلى عملك وتختار بحكمة. لا تختر BERT فقط لأنه “رائع”.

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

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

إضافة تعليق