إنشاء مجيــب آلي باستخدام مكتبة اللغات الطبيعية في بايثون nltk

A Chatbot in Python using nltk

هل فكّرت يوماً أن تتحدث مع الآلة؟

يا له من سؤال مثير وهل جُنِنا حتى نتكلّم ونتحدث  مع آلة؟ وهل ستستطيع الآلة إجابتنا بشكل صحيح ومقنع؟

ليس من الجنون أن نتحادث مع الآلة بل إنّ هناك مهاماً ستزداد فعاليتها ويَحسُن أداؤها إن اعتمدت على مجيب آلي لتساؤلاتنا.

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

بناء المجيب الآلي

 سنقوم ببناء المجيب الآلي باستخدام مكتبة اللغات الطبيعية NLTK  [1] والتي تُعتبرمنصة رائدة لبناء برامج بايثون التي تتعامل مع بيانات اللغات الطبيعية.

والآن دعونا نجهز تضمين المكتبات الضرورية وذلك بلغة البايثون:

import pandas as pd
import nltk
import numpy as np
import re
from nltk.stem import wordnet
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
from nltk import pos_tag
from sklearn.metrics import pairwise_distances
from nltk import word_tokenize
from nltk.corpus import stopwords

لنقرأ مجموعة البيانات المخزنة بصيغة xlsx باستخدام مكتبة تحليل البيانات في بايثون (بانداس) pandas  كما يلي:

df=pd.read_excel('dialog_talk_agent.xlsx')
df.head()

الشكل 1: جزء من البيانات
df.shape[0]

وبتنفيذ  التعليمة أعلاه  نجد أن البيانات التي تم تحميلها تحوي 1592 سطراً وكل سطر يتألف من عمودين عمود للاستعلام  يأخذ العنوان “السياق-Context” وعمود للاستجابة ويأخذ العنوان “استجابة النص-text Response”، وهذا النمط من البيانات يسمى البيانات المتوازية Parallel Data، كما نلاحظ وجود قيم خالية Null في مجموعة البيانات لا سيّما إذا فتحنا البيانات في برنامج الجداول الإلكترونية إكسل سنجد أن البيانات تندرج ضمن مجموعات مختلفة، وترد كل مجموعة من الأسئلة بشكل متتال.

 تشير القيم الخالية إلى أن استجابة السؤال الخالية مماثلة لاستجابة السؤال السابق الذي يكون سؤال من نفس النمط وضمن نفس المجموعة من الأسئلة. حيث أن مجموعة البيانات هذه صُممت بأن تُعطي الاستجابة لأول سؤال من الأسئلة ذات النمط الواحد والمجموعة الواحدة. لذا يمكننا استخدام التابع ffill() الذي يضع قيمة الاستجابة السابقة في القيم الخالية كما هو موضح:

df.ffill(axis=0,inplace=True)
df.head()
الشكل 2 جزء من البيانات بعد تطبيق تابع ffill()

خطوات العمل:

1- تقييس النص Normalization

2-تمثيل الكلمات كأشعة Vectorization

3- حساب تشابه جيب التمام ( cosine)

1- تقييس النص2-  تمثيل الكلمات كأشعة
– رد الحروف إلى شكلها الصغير
– حذف المحارف الخاصة
– الحصول على أصل الكلمات
حقيبة الكلمات

 تردد الحد- تردد المستند العكسي
  
3- حساب تشابه جيب التمام
الجدول-1 خطوات العمل

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

نسمي هذا التابع step1  كما يلي:

def step1(x):
    for i in x:
        a=str(i).lower()
        p=re.sub(r'[a-z0-9]',' ',a)
        print(p)

وبتنفيذ هذا التابع وتمرير النص إلبه، سنجد أن عملية تنقية النصوص تمّت. وعلينا الآن تقسيم الجمل النصية إلى الوحدات اللغوية بتطبيق عملية الحصول على الوحدات اللغوية tokenization.

تقوم عملية الحصول على الكلمات word tokenizer بتحويل سلسلة النص إلى قائمة من الكلمات [2].

لنأخذ المثال التالي ونطبق عليه عملية الحصول على الكلمات كما يلي:

s='tell me about your personality'
words=word_tokenize(s)
print(words)

إن تابع  pos_tag المزوَّد ضمن مكتبة صندوق اللغات الطبيعية nltk يعيد أقسام الكلام لكل كلمة، لذلك تابع الحصول على أصل الكلمة يحدد أقسام الكلام لكل كلمة ويحولها إلى جذرها المناسب.

pos-tag(nltk.word_tokenize(s), tagset=None) 
lema=wordnet.wordNetLemmatizer()
lema.lematize('absorbed',pos='V')

يكون الخرج كما يلي:

[(‘tell’, ‘VB’),

 (‘me’, ‘PRP’),

 (‘about’, ‘IN’),

 (‘your’, ‘PRP’),

 (‘personality’, ‘NN’)]

 حيث تشير أقسام الكلام في الخرج “VB” إلى الفعل و “PRP” إلى الضميرو “IN”  إلى الحروف أما “NN” فتشير إلى الأسماء. 

أما  الخرج ‘absorb’ فيشير إلى أصل الكلمة.

والآن عزيزي القارئ يمكننا جمع كل الخطوات التفصيلية في مرحلة التقييس في تابع واحد text_Normalization

def text_normalization(text):
    text=str(text).lower() # text to lower case
    spl_char_text=re.sub(r'[^ a-z]','',text) # removing special characters
    tokens=nltk.word_tokenize(spl_char_text) # word tokenizing
    lema=wordnet.WordNetLemmatizer() # intializing lemmatization
    tags_list=pos_tag(tokens,tagset=None) # parts of speech
    lema_words=[]   # empty list 
    for token,pos_token in tags_list:
        if pos_token.startswith('V'):  # Verb
            pos_val='v'
        elif pos_token.startswith('J'): # Adjective
            pos_val='a'
        elif pos_token.startswith('R'): # Adverb
            pos_val='r'
        else:
            pos_val='n' # Noun
        lema_token=lema.lemmatize(token,pos_val) # performing lemmatization
        lema_words.append(lema_token) # appending the lemmatized token into a list
    
    return " ".join(lema_words) # returns the lemmatized tokens as a sentence 

ولنختبر أداء التابع على هذه الجملة:

text_normalization('telling you some stuff about me')

يكون الخرج كما يلي:

‘tell you some stuff about me’

لنطبق تابع التقييس على مجموعة البيانات المدروسة، وذلك بتشكيل عمود جديد للبيانات يحتوي نص الاستعلام بعد تطبيق التقييس عليه.

df['lemmatized_text']=
df['Context'].apply(text_normalization) # applying the function to the dataset to get clean text
df.tail(15)

إنّ خطوتنا التالية هي إجراء التمثيل الشعاعي للكلمات،  سنطبق في هذه المقالة طريقتين من طرائق تمثيل الكلمات كأشعة  أحدهما حقيبة الكلمات bag of words (BOW) والآخر تردد الحد- تردد المستند العكسي TF-IDF Term Frequency-Inverse Document Frequency.

استخدام حقيبة الكلمات

إن حقيبة الكلمات هي تمثيل للنص بحيث تصف ورود الكلمات داخل المستند النصي [3].

إذا افترضنا أن القاموس يحتوي الكلمات التالية {Playing, is, love} ونريد تحويل الجملة التالية Playing football is love إلى شعاع، سنمر على كل كلمة من الجملة ونفحص وجودها في القاموس فإذا كانت موجودة أخذت القيمة ضمن الشعاع “1”وإلا فإن القيمة “0”، وبالتالي سنحصل على الشعاع التالي الممثل لهذه الجملة

(1, 0, 1, 1)

cv = CountVectorizer()
X =cv.fit_transform(df['lemmatized_text']).toarray()

features = cv.get_feature_names()
df_bow = pd.DataFrame(X, columns = features)
df_bow.head()

كل استعلام يتمثل بشعاع طوله 505 قيمة، لأن عدد الكلمات غير المكررة في مجموعة البيانات هو 505، وبالتالي يمكننا القول أنّه لدينا 505 ميزة feature ومن أجل كل استعلام إن كان يحتوي هذه الميزة (الكلمة) نضع القيمة “1” في مكان الميزة ونضع القيمة “0” إن لم تكن الكلمة موجودة في الاستعلام.

 إن كلمات التوقف هي كلمات شائعة ولا تمثل المعنى المطلوب للمستخدم. لذلك سنقوم بإزالة كلمات التوقف من معجم الكلمات.

stop = stopwords.words('english')
print(stop)

 لنأخذ المثال التالي:

الاستعلام: ‘Will you help me and tell me about yourself more’

أولاً نقوم بإزالة كلمات التوقف كما يلي:

Question ='Will you help me and tell me about yourself more'
Q=[]
a=Question.split()
for i in a:
    if i in stop:
        continue
    else:
        Q.append(i)
    b=" ".join(Q) 

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

والآن للحصول على الاستجابة المرتبطة بهذا الاستعلام سيتم إيجاد تشابه جيب التمام بين الاستعلام والاستعلامات المخزنة في مجموعة البيانات والمطبَّق عليها عملية التقييس.

تشابه جيب التمام

هو مقياس التشابه بين شعاعين، يعطي قيمة الجداء النقطي بين هذين الشعاعين مقسوماً على جداء طويلتي الشعاعين. كما هو موضّح في المعادلة التالية

تشابه جيب التمام(a,b)=الجداء النقطي (a,b) / ااaاا* ااbاا

توضح الشفرة البرمجية التالية حساب تشابه جيب التمام

cosine_value = 1- pairwise_distances(df_bow, Question_bow, metric = 'cosine' )

array([[0.25819889],

       [0.        ],

       [0.        ],

       …,

       [0.        ],

       [0.        ],

       [0.        ]])

وتوضح الشفرة البرمجية  التالية إنشاء عمود لقيم تشابه جيب التمام:

df['similarity_bow']=cosine_value
df_simi = pd.DataFrame(df, columns=['TextResponse','similarity_bow'])

لمعرفة قيمة التشابه الأكبر نقوم بفرز قيم التشابه وعرضهم من خلال الشفرة التالية:

df_simi_sort=df_simi.sort_values(by='similarity_bow', ascending=False) 
df_simi_sort.head()

لنأخذ عتبة معينة للتشابه ولتكن 0.2، ونوجد قيم التشابه الأكبر من هذه العتبة.

نحقق هذه المقارنة مع العتبة من خلال الشفرة البرمجية التالية:

threshold = 0.2
df_threshold = df_simi_sort[df_simi_sort['similarity_bow'] > threshold] 
df_threshold

يمكن أن نعرض قيم التشابه الأكبر من العتبة:

تعطينا الشفرة البرمجية التالية القيمة الأكبر من بين قيم التشابه ومن خلالها نستطيع الحصول على الاستجابة المناسبة :

index_value = cosine_value.argmax() 
df['Text Response'].loc[index_value]

ياللروعة نموذجنا يعمل بشكل جيد، ويجيب علينا بشكل صحيح..

استخدام طريقة  تردد الحد- تردد المستند العكسيTF-IDF

يدل تردد الحد على تكرار الكلمة في المستندات النصية ، أما تردد المستند العكسي فيدل على مدى ندرة (عدم تكرار) الكلمة ضمن المستندات في مجموعة البيانات  [4].

لحساب تردد الحد يتم تقسيم عدد مرات تكرار الكلمة في النص على العدد الكلي لكلمات النص.

أما تردد الحد العكسي فيحسب بتقسيم عدد النصوص الكلية في المجموعة على عدد النصوص التي تحتوي الكلمة ثم يدخل الناتج على تابع اللوغاريتم.

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

وبالتالي تكون قيمة تردد الحد- تردد الحد العكسي للكلمات الشائعة صغيرة أما الكلمات المميزة في النص فتأخذ قيماً كبيرة [5]. 

يمكننا تحقيق ذلك بلغة البايثون بطريقة مشابهة لطريقة حقيبة الكلمات:

tfidf=TfidfVectorizer() 
x_tfidf=tfidf.fit_transform(df['lemmatized_text'])
.toarray()

وأيضاً سنحصل على شعاع لكل استعلام بطول 505 قيمة.

df_tfidf=pd.DataFrame(x_tfidf,columns=tfidf.get_feature_names()) 
df_tfidf.head()

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

كما أنشأنا عموداً للتشابه بطريقة حقيبة الكلمات علينا الآن أن ننشئ عموداً للتشابه بطريقة تردد الحد-تردد المستند العكسي. كما هو موضح في الشفرة البرمجية التالية:

cos=1-pairwise_distances(df_tfidf,Question_tfidf,metric='cosine')  
df['similarity_tfidf']=cos 
df_simi_tfidf = pd.DataFrame(df, columns=['TextResponse','similarity_tfidf']) df_simi_tfidf 

والآن عزيزي القارئ دعنا نحسب قيم تشابه جيب التمام بين الاستعلام الممثل بشعاع بطريقة TF-IDF وبين الاستعلامات المخزنة في مجموعة البيانات والممثلة أيضاً بأشعة بطريقة TF-IDF.

لنأخذ الاستعلام التالي على سبيل المثال ونطبق عليه التقييس ثم نحوّله لشعاع بطريقة TF-IDF والموضح في الشفرة البرمجية التالية:

Question1 ='Tell me about yourself.'
Question_lemma1 = text_normalization(Question1)
Question_tfidf= tfidf.transform([Question_lemma1]).toarray()

وكذلك كما في الطريقة السابقة فإننا نضع عتبة للتشابه 0.2

threshold = 0.2
df_threshold = df_simi_tfidf_sort[df_simi_tfidf_sort['similarity_tfidf'] > threshold] 
df_threshold

ونعرض هذا الجزء من قيم التشابه، علماً أنها القيم  الأكبر من العتبة كما يلي:

وللحصول على قيمة التشابه الأكبر نطبق تابع argmax

index_value1 = cos.argmax()

فنجد أن القيمة الأكبر هي ذات الفهرس 4، لذلك سنحصل على الاستجابة ذات  الفهرس 4 مقابل هذا الاستعلام.

df['Text Response'].loc[index_value1]

“I can help you work smarter instead of harder”

والآن بإمكاننا أن نجمع خطوات الحصول  على الاستجابة من الاستعلام في تابع واحد باستخدام طريقة تردد المستند- تردد المستند العكسي كما في الشفرة التالية:

def chat_tfidf(text):
 lemma=text_normalization(text) 
  tf=tfidf.transform([lemma]).toarray()
  cos=1-pairwise_distances(df_tfidf,tf,metric='cosine') 
 index_value=cos.argmax() 
 return df['Text Response'].loc[index_value]

دعونا نأخذ استجابة بعض الاستعلامات كما هو موضح في الشكل التالي:

الشكل -3 بعض الأمثلة

نلاحظ أن طريقة تردد الحد -تردد المستند العكسي لم تحتاج إلى إزالة كلمات التوقف كما في طريقة حقيبة الكلمات.

الخاتـــمة

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

المراجـــع

1Bhargava Sai Reddy P, A Chatbot in Python using nltk, medium Blog, 2019.

2- دانيا صغير، لمحة عن معالجة اللغات الطبيعية، مدونة الذكاء الاصطناعي باللغة العربية، 2019. 

3- Soumya George K, Shibily Joseph, Text Classification by Augmenting Bag of Words (BOW) Representation with Co-occurrence Feature, IOSR Journal of Computer Engineering, Volume 16, Issue 1, 2014. 

4- Shazad Qaiser, Ramsha Ali, Text Mining: Use of TF-IDF to Examine the   Relevance of Words to Documents, International Journal of Computer Applications, Vol 181, No 1, 2018.

5- دانيا صغير، منظومة لمعالجة النصوص العربية الوصفية باستخدام تقانات الذكاء الصنعي، رسالة أعدت لنيل درجة الدكتوراه في الذكاء الصنعي، جامعة حلب، 2019. 

الكاتب

  • د. م. دانيـــا صـغيــر

    دكتوراه في الذكاء الصنعي، كلية الهندسة المعلوماتية، قسم الذكاء الصنعي واللغات الطبيعية، جامعة حلب، الجمهورية العربية السورية

0 Shares:
5 تعليقات
  1. شكرا ل جهودك هل يوجد ايميل للتواصل هل هناك إمكانية بناء نموذج للتنبؤ بأسعار الأسهم في اسواق المال جربت بعض النماذج التي تستخدم LSTM تنجح في التنبؤ في البيانات التاريخية عند اختبار النموذج وتفشل عند التنبؤ بالأسعار المستقبلية

اترك تعليقاً

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

You May Also Like