تحليل الآراء باستخدام التعلّم العميــــق

Develop a Deep Convolutional Neural Network for Sentiment Analysis (Text Classification)

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

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

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

1- كيفية تحضير بيانات نصية لمراجعات الأفلام لتصنيفها باستخدام التعلّم العميق.

2- كيفية تعليم تضمين الكلمات كجزء من تركيب نموذج التعلم العميق.

3- كيفية بناء تضمين الكلمات بشكل مستقل وكيفية استخدام تضمين الكلمات المدرَّب مسبقًا في نموذج الشبكة العصبونية.

بيئة بايثون

قبل أن نشرع في مقالنا يتوجب علينا تحضير بيئة العمل المناسبة من خلال تنصيب الأدوات والبرمجيات وتحضير المكتبات التي نحتاجها.    

نفترض أنه لديك بيئة للغة بايثون 3 Python 3.

يجب أن يكون لديك أيضاً كيراس 2.2 Keras 2.2 أو نسخة أعلى مثبتة إما مع الواجهة الخلفية تنسر فلوTensorFlow أو ثيانو Theano. كما يجب أن تثبّت المكتبات التالية مكتبة تحليل البيانات في بايثون (بانداس) Pandas، مكتبة بايثون العددية Numpy، مكتبة سكيت للتعليم Skit-Learn مكتبة الرسم في بايثون matplot.

بإمكانك العمل على معالج الواجهات الرسومية GPU أو وحدة المعالجة المركزية CPU.

مجموعة بيانات مراجعات الأفلام

إن مجموعة بيانات مراجعات الأفلام هي عبارة عن مجموعة من مراجعات الأفلام التي تم استخلاصها من الموقع imdb.com الذي يحوي قاعدة بيانات تتضمن معلومات خاصة بالأفلام و المسلسلات في أوائل العقد الأول من القرن العشرين بواسطة بو بانج Bo Pang و ليليان لي Lillian Lee. حيث تم جمع هذه المراجعات وإتاحتها كجزء من أبحاثهم حول معالجة اللغة الطبيعية.

تم إصدار المراجعات في الأصل في عام 2002 ، ولكن تم إصدار نسخة محدّثة ومعالجتها في عام 2004وتعرف باسم “الإصدار 2.0”.

تتألف مجموعة البيانات من 1000 مراجعة إيجابية و 1000 مراجعة سلبية للأفلام مأخوذة من أرشيف rec.arts.movies.reviews مجموعة الأخبار المستضافة على الموقع نفسه . 

يشير المؤلفون إلى مجموعة البيانات هذه باسم “مجموعة البيانات المتناقضة”

تمت كتابة مجموعة البيانات هذه قبل عام 2002 ، بحد أقصى 20 مراجعة لكل مؤلف (إجمالي 312 مؤلف) لكل فئة [1].

تم تنقية البيانات  كما يلي:

  • احتواء مجموعة البيانات على مراجعات مكتوبة باللغة الإنجليزية فقط.
  • تم تحويل الأحرف في النصوص إلى أحرف صغيرة.
  • ترك مسافة فراغ قبل وبعد علامات التنقيط كالفاصلة والنقطة والأقواس. 
  • تم تقسيم النص إلى جمل، ووضع كل جملة في سطر واحد.

 استُخدمت هذه البيانات لمهام معالجة اللغات الطبيعية ذات الصلة، فمن أجل مهمة التصنيف حققت نماذج تعلم الآلة مثل تعلم القرار الآلي SVM على هذه البيانات أداءً بين 70٪ إلى 80٪ (على سبيل المثال 78٪ -82٪).

قد يشهد إعداد البيانات الأكثر تطوراً نتائج تصل إلى 86٪ بتقسيم البيانات بطريقة التقسيم 10  10 fold وهذا يجعل الدقة تتراوح من الثمانينات إلى منتصف الثمانينات باستخدام تجارب الطرق الحديثة. وسيكون لاختيار المصنّف دور في الحصول على الأداء العالي في الدقة.

عزيزي القارئ بإمكانك الحصول على مجموعة البيانات من الموقع المشار إليه في المرجع [2].

بعد فك ضغط الملف ، ستحصل على مجلد باسم “txt_sentoken” وبداخله مجلدين “neg” و “pos” يحتوي مجلد  “neg” المراجعات السلبية ويحتوي مجلد “pos” المراجعات الإيجابية. يتم تخزين المراجعات بحيث تكون كل واحدة في ملف مع اصطلاح تسمية  cv000 إلى cv999 لكل مراجعة سلبية أو إيجابية. 

بعد ذلك نقوم بتحميل وإعداد البيانات النصية.

إعداد البيانات

في هذا القسم ، سنتناول ثلاثة أمور:

1- فصل البيانات إلى مجموعات تدريب واختبار.

2- تحميل وتنقية البيانات لإزالة علامات التنقيط والأرقام.

3- تحديد معجم الكلمات المستخدمة.

فصل البيانات إلى مجموعات تدريب واختبار

من المفترض أن يقوم النظام الذي نبنيه بالتنبؤ عن الآراء لأي مراجعة فيلم نصية فيما إن كانت سلبية أم إيجابية.

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

لذلك يا عزيزي القارئ سنقوم معاً بتقسيم البيانات النصية إلى بيانات تدريب وأخرى للاختبار.

 سوف نستخدم آخر 100 مراجعة إيجابية وآخر 100 مراجعة سلبية كمجموعة اختبار، والمراجعات المتبقية 1800 كـمجموعة بيانات التدريب.

 وهذا يعني أنه 90% من البيانات للتدريب و10% من البيانات للاختبار. سيكون تقسيم البيانات سهلاً بسبب تسمية الملفات فالملفات من 000 إلى 899 للتدريب والملفات من 900 إلى 999 للاختبار.

تحميل وتنقية البيانات لإزالة علامات التنقيط والأرقام

إن البيانات النصية تم تنقيتها ولا تحتاج الكثير من الإعداد.

سنقوم بإعداد البيانات بالطريقة التالية:

1- تقسيم الجمل إلى كلمات حسب الفراغات.

2- إزالة علامات التنقيط.

3- إزالة المحارف والكلمات المؤلفة من حروف غير الأبجدية.

4- حذف الكلمات المعروفة بكلمات توقف stop words.

5- حذف الكلمات التي لا يزيد طولها عن المحرف الواحد.

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

عزيزي القارئ، لنأخذ هذا المثال الذي يقوم بتنقية أول مراجعة إيجابية من مجموعة البيانات النصية:

import nltk
from nltk.corpus import stopwords
import string
 
# load doc into memory
def load_doc(filename):
  # open the file as read only
  file = open(filename, 'r')
  # read all text
  text = file.read()
  # close the file
  file.close()
  return text
nltk.download('stopwords')
# turn a doc into clean tokens
def clean_doc(doc):
  import nltk
  from nltk.corpus import stopwords
  # split into tokens by white space
  tokens = doc.split()
  # remove punctuation from each token
  table = str.maketrans('', '', string.punctuation)
  tokens = [w.translate(table) for w in tokens]
  # remove remaining tokens that are not alphabetic
  tokens = [word for word in tokens if word.isalpha()]
  # filter out stop words
  stop_words = set(stopwords.words('english'))
  tokens = [w for w in tokens if not w in stop_words]
  # filter out short tokens
  tokens = [word for word in tokens if len(word) > 1]
  return tokens

بعد بناء توابع التحميل والتنقية نقوم باستدعائها من أجل ملف المراجعة الأولى الإيجابية كما يلي:

# load the document
filename = '/cv000_29590.txt'
text = load_doc(filename)
tokens=clean_doc(text)
print(tokens)

يؤدي تنفيذ المثال إلى طباعة قائمة طويلة من الكلمات المنقّاة.

[‘films’, ‘adapted’, ‘comic’, ‘books’, ‘plenty’, ‘success’, ‘whether’, ‘theyre’, ‘superheroes’, ‘batman’, ‘superman’, ‘spawn’, ‘geared’, ‘toward’, ‘kids’, ‘casper’, ‘arthouse’, ‘crowd’, ‘ghost’, ‘world’, ‘theres’, ‘never’, ‘really’, ‘comic’, ‘book’, ‘like’, ‘hell’, ‘starters’, ‘created’, ‘alan’, ‘moore’, ‘eddie’, ‘campbell’, ‘brought’, ‘medium’, ‘whole’, ‘new’, ‘level’, ‘mid’, ‘series’, ‘called’, ‘watchmen’, ‘say’, ‘moore’, ‘campbell’, ‘thoroughly’, ‘researched’, ‘subject’, ‘jack’, ‘ripper’, ‘would’,…………………………]

تحديد معجم الكلمات المستخدمة

من الضروري تحديد المفردات المستخدمة عند بناء نموذج حقيبة الكلمات bag-of-words أونموذج تضمين الكلمات.  

كلما زاد عدد الكلمات كلما استطعنا تمثيل المستندات بشكل أكبر، فمن المهم أن نعرف ما هي الكلمات التي قد تصادفنا أثناء عملية التنبؤ عن الآراء.

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

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

 عزيزي القارئ إن بناء المعجم ليس معقداً أبداً، فمن أجل كل مستند يمكننا أن نستدعي التابع   add_doc_to_vocab () الذي سنبنيه لاحقاً ، ونستدعيه من أجل إضافة كل مراجعة على حدا، أما التابع process_docs() فسنستدعيه للمرور على كل المراجعات الإيجابية والسلبية وإضافتها إلى المعجم.

يوضح الكود البرمجي التالي بناء المعجم:

from string import punctuation
from os import listdir
from collections import Counter
from nltk.corpus import stopwords
def add_doc_to_vocab(filename, vocab):
  # load doc
  doc = load_doc(filename)
  # clean doc
  tokens = clean_doc(doc)
  # update counts
  vocab.update(tokens)
 
#load all docs in a directory
def process_docs(directory, vocab, is_trian):
  # walk through all files in the folder
  for filename in listdir(directory):
    # skip any reviews in the test set
    if is_trian and filename.startswith('cv9'):
      continue
    if not is_trian and not filename.startswith('cv9'):
      continue
    # create the full path of the file to open
    path = directory + '/' + filename
    # add doc to vocab
    add_doc_to_vocab(path, vocab)
 
#define vocab
vocab = Counter()
# add all docs to vocab
process_docs('txt_sentoken/neg', vocab, True)
process_docs('txt_sentoken/pos', vocab, True)
# print the size of the vocab
print(len(vocab))
# print the top words in the vocab
print(vocab.most_common(50))

بعد تنفيذ الكود نلاحظ أن المعجم يتألف من 44,276 كلمة، وكذلك نلاحظ الكلمات ال 50 الأكثر استخداماً في مراجعات الأفلام.

يتم بناء المعجم وفقاً لكلمات مجموعة البيانات المخصصة للتدريب.

44276

[(‘film’, 7983), (‘one’, 4946), (‘movie’, 4826), (‘like’, 3201), (‘even’, 2262), (‘good’, 2080), (‘time’, 2041), (‘story’, 1907), (‘films’, 1873), (‘would’, 1844), (‘much’, 1824), (‘also’, 1757), (‘characters’, 1735), (‘get’, 1724), (‘character’, 1703), (‘two’, 1643), (‘first’, 1588), (‘see’, 1557), (‘way’, 1515), (‘well’, 1511), (‘make’, 1418), (‘really’, 1407), (‘little’, 1351), (‘life’, 1334), (‘plot’, 1288), (‘people’, 1269), (‘bad’, 1248), (‘could’, 1248), (‘scene’, 1241), (‘movies’, 1238), (‘never’, 1201), (‘best’, 1179), (‘new’, 1140), (‘scenes’, 1135), (‘man’, 1131), (‘many’, 1130), (‘doesnt’, 1118), (‘know’, 1092), (‘dont’, 1086), (‘hes’, 1024), (‘great’, 1014), (‘another’, 992), (‘action’, 985), (‘love’, 977), (‘us’, 967), (‘go’, 952), (‘director’, 948), (‘end’, 946), (‘something’, 945), (‘still’, 936)]

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

يوضح هذا الكود البرمجي حذف الكلمات الواردة مرة واحد فقط.

def save_list(lines, filename):
	# convert lines to a single blob of text
	data = '\n'.join(lines)
	# open file
	file = open(filename, 'w')
	# write text
	file.write(data)
	# close file
	file.close()

# save tokens to a vocabulary file
save_list(tokens, 'vocab.txt')

وبالتالي نحصل على ملف يحتوي معجم  الكلمات الهامّة المأخوذة من المراجعات النصية.

الشكل -1 جزء من معجم الكلمات الهامّة

مبارك، حصلنا على المعجم وأصبحنا جاهزون لاستخلاص ميزات التعلم من المراجعات.

تدريب طبقة تضمين الكلمات

في هذا القسم، سنعلّم طبقة تضمين الكلمات أثناء تدريب الشبكة العصبونية على مشكلة التصنيف.

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

وبالتالي فإن الكلمات المتشابهة في المعنى سيكون لها تمثيل متماثل في الفضاء الشعاعي.

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

يمكننا تعلّم قيم الأشعة للكلمات أثناء تدريب الشبكة العصبونية على مهمة التصنيف وذلك من خلال طبقة التضمين “Embedding Layer” في مكتبة كيراس Keras

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

توضح الشفرة البرمجية أدناه طريقة تحميل المعجم وتخزينه في بنية مجموعة  set، لسهولة التعامل معها برمجياً.

#load doc into memory
def load_doc(filename):
	# open the file as read only
	file = open(filename, 'r')
	# read all text
	text = file.read()
	# close the file
	file.close()
	return text
# load the vocabulary
vocab_filename = 'vocab.txt'
vocab = load_doc(vocab_filename)
vocab = vocab.split()
vocab = set(vocab)

بعد ذلك ، نحتاج إلى تحميل جميع مراجعات أفلام بيانات التدريب. لذلك يمكننا أن نعدّل على التابع process_docs() من القسم السابق لنحمّل كافة مستندات المراجعات المخصصة لتدريب النموذج. ثم نقوم بتنقية هذه المراجعات، ونريد أن تعيد هذه التوابع المراجعات كقائمة من السلاسل النصية strings. وكل سلسلة تعبر عن مستند واحد، وذلك لسهولة تشفيرهم فيما بعد  كتسلسل sequence من الأرقام. 

إن تنقية المستندات تتضمن تقسيم المراجعة بالاعتماد على الفراغات وإزالة علامات التنقيط ثم إزالة الكلمات غير الموجودة في المعجم الذي تم بناؤه في القسم السابق واستدعيناه هنا كملف vocab.txt.

 زادت مهمة إزالة الكلمات غير الموجودة في المعجم على تابع تنقية المستند ولذلك سنكتب التابع المعدّل التالي:

#turn a doc into clean tokens
def clean_doc(doc, vocab):
	# split into tokens by white space
	tokens = doc.split()
	# remove punctuation from each token
	table = str.maketrans('', '', punctuation)
	tokens = [w.translate(table) for w in tokens]
	# filter out tokens not in vocab
	tokens = [w for w in tokens if w in vocab]
	tokens = ' '.join(tokens)
	return tokens

سيقوم تابع process_docs باستدعاء التابع clean_doc من أجل كل مستند في مجموعة بيانات التدريب من مجلد المراجعات الإيجابية وكذلك السلبية.  

 
#load all docs in a directory
def process_docs(directory, vocab, is_trian):
	documents = list()
	# walk through all files in the folder
	for filename in listdir(directory):
		# skip any reviews in the test set
		if is_trian and filename.startswith('cv9'):
			continue
		if not is_trian and not filename.startswith('cv9'):
			continue
		# create the full path of the file to open
		path = directory + '/' + filename
		# load the doc
		doc = load_doc(path)
		# clean doc
		tokens = clean_doc(doc, vocab)
		# add to list
		documents.append(tokens)
	return documents

# load all training reviews
positive_docs = process_docs('~\review_polarity\txt_sentoken\pos', vocab, True)
negative_docs = process_docs('~\review_polarity\txt_sentoken\neg', vocab, True)
train_docs = negative_docs + positive_docs

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

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

نستطيع تشفير النصوص في مستندات التدريب كتسلسل من الأرقام الصحيحة باستخدام الصف المقسِّم class tokenizer  المتوفر في مكتبة كيراس.

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

#create the tokenizer
tokenizer = Tokenizer()
# fit the tokenizer on the documents
tokenizer.fit_on_texts(train_docs)

وبعد أن تم الربط بين الكلمات والأعداد الصحيحة يمكننا استخدام هذا الربط لتشفير المراجعات في مجموعة بيانات التدريب، ويمكننا فعل ذلك من خلال استدعاء التابع texts_to_sequences الموجود في الصف tokenizer.

#sequence encode
encoded_docs = tokenizer.texts_to_sequences(train_docs)

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

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

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

إذاً علينا أن نقوم أولاً بإيجاد أطول مراجعة في مجموعة البيانات باستخدام تابع ()max وتخزين قيمته ثم استدعاء تابع pad_sequences() من توابع مكتبة كيراس لتصبح جميع تسلسلات المراجعات بطول واحد من خلال إضافة حشو الأصفار في نهاية السلسلة.

#pad sequences
max_length = max([len(s.split()) for s in train_docs])
Xtrain = pad_sequences(encoded_docs, maxlen=max_length, padding='post')

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

 #define training labels
ytrain = array([0 for _ in range(900)] + [1 for _ in range(900)])

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

 
#add all test reviews
positive_docs = process_docs('txt_sentoken/pos', vocab, False)
negative_docs = process_docs('txt_sentoken/neg', vocab, False)
test_docs = negative_docs + positive_docs
# sequence encode
encoded_docs = tokenizer.texts_to_sequences(test_docs)
# pad sequences
Xtest = pad_sequences(encoded_docs, maxlen=max_length, padding='post')
# define test labels
ytest = array([0 for _ in range(100)] + [1 for _ in range(100)])

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

سيأخذ النموذج طبقة التضمين كأول طبقة خفية، ويتطلب التضمين حجم المعجم، وبُعد أشعة الفضاء الممثلة للكلمات، وكذلك أطول طول للمستند النصي الدخل [3].

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

 #define vocabulary size (largest integer value)
vocab_size = len(tokenizer.word_index) + 1

 سنستخدم البعد 100 لفضاء الأشعة، لكن يمكننا تجريب قيماً أخرى مثل 50 أو 150.

أما طول أطول مستند فقد حسبناه وخزنّاه في المتحول  max_length  المستخدم خلال عملية الحشو.

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

سيتم بناء شبكة الطّي العصبونية ب 32 فلتر وحجم الفلتر سيكون مساوياً 8 وسنستخدم تابع التفعيل وحدة التصحيح الخطّي RELU، ستكون طبقة الطّي متبوعة بطبقة التجميع التي تقلل حجم طبقة الطّي إلى النصف.

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

سينتهي النموذج بطبقات بيرسبترون متعددة الطبقات multilayers perceptron layers القياسية لتفسر الميزات المستخلصة من شبكة الطّي العصبونية CNN، ستستخدم طبقة الخرج النهائية تابع سيغمويد Sigmoid للتفعيل لتكون قيمة الخرج بين 0 و1 لتعبر عن الآراء الإيجابية والسلبية في المراجعات.

توضح الشفرة البرمجية تعريف النموذج:

#define model
model = Sequential()
model.add(Embedding(vocab_size, 100, input_length=max_length))
model.add(Conv1D(filters=32, kernel_size=8, activation='relu'))
model.add(MaxPooling1D(pool_size=2))
model.add(Flatten())
model.add(Dense(10, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
print(model.summary())

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

الشكل-2 ملخص بنية  النموذج

 وأخيراً حان وقت تدريب النموذج على عينات التدريب، سنستخدم تابع الخسارة الأنتروبيا المتقاطعة الثنائية   binary cross entropy loss لأن مشكلة التصنيف هنا ثنائية فالمراجعة إما إيجابية أو سلبية. كما سنستخدم خوارزمية توقُّع المُلاءمة اللحظية (آدام) Adam الفعالة لتحقيق تناقص المشتق  stochastic gradient descent وسنتتبع الدقة والخسارة أثناء التدريب.

سنمرر عينات التدريب للنموذج 10 مرات أي أن النموذج سيتدرب 10 دورات epochs، وتُضبَ قيم إعداد تدريب النموذج تجريبياً ولقد رأينا هذه القيم الموضحة أدناه في الكود البرمجي هي الأفضل لمشكلتنا [4].[5].

# compile network
model.compile(loss='binary_crossentropy',optimizer='adam', metrics=['accuracy'])
# fit network
model.fit(Xtrain, ytrain, epochs=10, verbose=2)

سنقوم بتقييم النموذج بعد تدريبه، وذلك بتمرير عينات الاختبار،  إنّ عينات الاختبار تحتوي كلمات غير موجودة في المعجم والنموذج لم تمر عليه مراجعات الاختبار مسبقاً.

# evaluate
loss, acc = model.evaluate(Xtest, ytest, verbose=0)
print('Test Accuracy: %f' % (acc*100))

عند تنفيذ الكود البرمجي ستُطبع قيم الدقة والخسارة في نهاية كل دورة على عينات التدريب وسنرى أن الدقة تصل بسرعة إلى 100% على عينات التدريب أما على عينات الاختبار فإن الدقة تبلغ 85 % وتُعتبر دقة عالية في إنجاز هذه المهمة-تصنيف الآراء.

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

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

تدريب تضمين الكلمات word2vec

سنتعلم في هذا القسم تدريب تضمين الكلمات بشكل مستقل باستخدام الخوارزمية الفعالة الشهيرة word2vec.

إن تدريب تضمين الكلمات كجزء من الشبكة العصبونية الأساسية سيكون سلبياً بسبب بطئه خاصة عندما تكون مجموعة البيانات النصية ضخمة.

إن خوارزمية word2vec هي طريقة لتعليم تضمين الكلمات من مجموعة بيانات نصية ضخمة بشكل مستقل.[6].

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

تعالج خوارزمية word2vec المستندات جملةً جملة، وهذا يعني أننا علينا الحفاظ على بنية الجمل الموجودة في المستندات أثناء عملية التنقية.

سنقوم بتحميل المعجم كما فعلنا سابقاً.

#load doc into memory
def load_doc(filename):
	# open the file as read only
	file = open(filename, 'r')
	# read all text
	text = file.read()
	# close the file
	file.close()
	return text

# load the vocabulary
vocab_filename = 'vocab.txt'
vocab = load_doc(vocab_filename)
vocab = vocab.split()
vocab = set(vocab)

وبعدها سنعرّف تابع doc_to_clean_lines() لتنقية المستندات المحملّة وتنظيفها سطراً سطراً، وإعادة  قائمة بالأسطر المنقّاة.

#turn a doc into clean tokens
def doc_to_clean_lines(doc, vocab):
	clean_lines = list()
	lines = doc.splitlines()
	for line in lines:
		# split into tokens by white space
		tokens = line.split()
		# remove punctuation from each token
		table = str.maketrans('', '', punctuation)
		tokens = [w.translate(table) for w in tokens]
		# filter out tokens not in vocab
		tokens = [w for w in tokens if w in vocab]
		clean_lines.append(tokens)
	return clean_lines

ثم سنعدّل التابع process_docs() لتحميل وتنقية جميع المستندات النصية، وإعادة قائمة بجميع أسطر المستندات. إن نتيجة هذا التابع ستشكل بيانات التدريب لنموذج word2vec.

#laad all docs in a directory
def process_docs(directory, vocab, is_trian):
	lines = list()
	# walk through all files in the folder
	for filename in listdir(directory):
		# skip any reviews in the test set
		if is_trian and filename.startswith('cv9'):
			continue
		if not is_trian and not filename.startswith('cv9'):
			continue
		# create the full path of the file to open
		path = directory + '/' + filename
		# load and clean the doc
		doc = load_doc(path)
		doc_lines = doc_to_clean_lines(doc, vocab)
		# add lines to list
		lines += doc_lines
	return lines

يمكننا بعد ذلك تحميل مجموعة البيانات جميعها وتحويلها إلى قائمة طويلة من الجمل (قوائم من الكلمات) الجاهزة لتدريب نموذج word2vec.

#load training data
positive_docs = process_docs('txt_sentoken/pos', vocab, True)
negative_docs = process_docs('txt_sentoken/neg', vocab, True)
sentences = negative_docs + positive_docs
print('Total training sentences: %d' % len(sentences))

الآن سنطبق خوارزمية word2vec باستخدام مكتبة جينسم Gensim في بايثون (خاصة الصف word2vec). يتم تدريب النموذج عند بناء الصف word2vec، نمرر إليه قائمة من الجمل التي تم تنقيتها من مجموعة بيانات التدريب. ثم نحدد حجم فضاء شعاع التضمين (وليكن 100) وعدد الكلمات المجاورة التي تفيدنا عند تعليم التضمين لكل كلمة من جمل مجموعة التدريب (سنضعها 5)، عدد المسالك thread المستخدمة أثناء التدريب (سنستخدم 8 وهذه القيمة تتغير حسب المعالجات المتوفرة لديك)، وكذلك نحدد العدد الأصغري لتكرار ورود الكلمة في مجموعة البيانات (سنختاره 1 كما بنينا ذلك في المعجم في القسم السابق).

ثم نطبع حجم المعجم الذي قمنا بتعليم كلماته وذلك بعد الانتهاء من تدريب النموذج،  وهو مطابق لحجم المعجم vocab.txt الذي يحتوي 25,767 كلمة. 

#train word2vec model
model = Word2Vec(sentences, size=100, window=5, workers=8, min_count=1)
# summarize vocabulary size in model
words = list(model.wv.vocab)
print('Vocabulary size: %d' % len(words))

أخيراً مبارك عليك، حصلت على أشعة التضمين لكلمات المعجم وعليك القيام بحفظها في ملف باستخدام التابع save_word2vec_format () في الخاصّة ‘wv‘ (word vector) الموجودة في النموذج. يتم حفظ التضمين بتنسيق ASCII بوضع كلمة واحدة وشعاع  لكل سطر.

نلاحظ أن تنفيذ المثال كاملاً سيقوم بتحميل 58,109 جملة من بيانات التدريب وإنشاء تضمين للمعجم الذي يحتوي 25,767 كلمة، والحصول على الملف  embedding_word2vec.txt’ الذي يحوي الأشعة المعلّمة المضمنة لكلمات المعجم..

Total training sentences: 58109

Vocabulary size: 2576

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

استخدام تضمين الكلمات المدرب مسبقاً

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

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

يقوم التابع الموضح أدناه بتحميل تضمين الكلمات وإعادة معجم بحيث تشير كل كلمة فيه إلى شعاعها، وذلك بتنسيق مكتبة بايثون العدديّة NumPy.

#load embedding as a dict
def load_embedding(filename):
	# load embedding into memory, skip first line
	file = open(filename,'r')
	lines = file.readlines()[1:]
	file.close()
	# create a map of words to vectors
	embedding = dict()
	for line in lines:
		parts = line.split()
		# key is string word, value is numpy array for vector
		embedding[parts[0]] = asarray(parts[1:], dtype='float32')
	return embedding

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

فمن المهم أن نوضح أن الكلمات المشفرة بأرقام ترتبط بأشعتها المحددة.

سنعرّف التابع get_weight_matrix()  الذي يأخذ بارامترات الدخل التضمين المحمّل ومعجم بأرقام الكلمات المشفرة كأعداد صحيحة، ويعيد مصفوفة تربط كل رقم مشفر للكلمة بشعاعها الصحيح.

#Create a weight matrix for the Embedding layer from a loaded embedding
def get_weight_matrix(embedding, vocab):
	# total vocabulary size plus 0 for unknown words
	vocab_size = len(vocab) + 1
	# define weight matrix dimensions with all 0
	weight_matrix = zeros((vocab_size, 100))
	# step vocab, store vectors using the Tokenizer's integer mapping
	for word, i in vocab.items():
		weight_matrix[i] = embedding.get(word)
	return weight_matrix

والآن بإمكاننا استخدام هذه التوابع لإنشاء طبقة التضمين الجديدة لنموذجنا.

#load embedding from file
raw_embedding = load_embedding('embedding_word2vec.txt')
# get vectors in the right order
embedding_vectors = get_weight_matrix(raw_embedding, tokenizer.word_index)
# create the embedding layer
embedding_layer = Embedding(vocab_size, 100, weights=[embedding_vectors], input_length=max_length, trainable=False)

نلاحظ أن مصفوفة أشعة التضمين والتي تمثل أوزان طبقة التضمين، مُرِّرت إلى طبقة التضمين الجديدة كبارامتر دخل. وتم ضبط بارامتر التدريب ليساوي “false” للتأكد أن الشبكة لا تحاول تدريب الأشعة المدربة مسبقاً كجزء من تدريب الشبكة الأساسية.

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

#define model
model = Sequential()
model.add(embedding_layer)
model.add(Conv1D(filters=128, kernel_size=5, activation='relu'))
model.add(MaxPooling1D(pool_size=2))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
print(model.summary())

وبتنفيذ كامل الشيفرة البرمجية نرى أن الأداء لم يتحسّن. وفي الواقع كان سيئاً فقد تعلمت الشبكة على بيانات التدريب بنجاح لكن التقييم على بيانات الاختبار كان ضعيفاً وصل إلى 57% فقط. 

والسبب يعود إلى بنية word2vec المختارة أو بنية الشبكة العصبونية المختارة. يمكن استخدام أوزان طبقة التضمين كأوزان بداية بدلاً من القيم العشوائية وترك التدريب لطبقة التضمين ممكناً بجعل قيمته “True” وهي الحالة الافتراضية للطبقة. وحينها سيتحسن الأداء قليلاً لتصبح الدقة 64% لكنّه ما زال ضعيفاً لذلك عليك أن تتحرى وتجرب في تعديل بنية الشبكة وطبقة التضمين.

 بإمكاننا أن نستخدم أشعة تضمين الكلمات المدربة مسبقاً على بيانات ضخمة جداً مثل word2vec المقدّمة من شركة Google [7] أو تضمين الأشعة العامّة  Glove المقدّمة من جامعة ستانقورد stanford 8 ]

سنستخدم في هذا المقال التعليمي أشعة تضمين Glove المدربة على بيانات ضخمة من ويكبيديا.

إن ملف تضمين Glove لا يحتوي سطر رأسي لذلك نعدّل على تابع تحميل أشعة التضمين:

#load embedding as a dict
def load_embedding(filename):
	# load embedding into memory, skip first line
	file = open(filename,'r')
	lines = file.readlines()
	file.close()
	# create a map of words to vectors
	embedding = dict()
	for line in lines:
		parts = line.split()
		# key is string word, value is numpy array for vector
		embedding[parts[0]] = asarray(parts[1:], dtype='float32')
	return embedding

سنحتاج أيضاً إلى تجاوز الكلمات غير الموجودة في معجمنا لكنّ ليس لها شعاع مقابل في ملف التضمين Glove. لذلك سنعدّل تابع get_weight_matrix()

# create a weight matrix for the Embedding layer from a loaded embedding
def get_weight_matrix(embedding, vocab):
	# total vocabulary size plus 0 for unknown words
	vocab_size = len(vocab) + 1
	# define weight matrix dimensions with all 0
	weight_matrix = zeros((vocab_size, 100))
	# step vocab, store vectors using the Tokenizer's integer mapping
	for word, i in vocab.items():
		vector = embedding.get(word)
		if vector is not None:
			weight_matrix[i] = vector
	return weight_matrix

 والآن نستطيع تحميل أشعة التضمين Glove وبناء طبقة التضمين في الشفرة البرمجية التالية:

# load embedding from file
raw_embedding = load_embedding('glove.6B.100d.txt')
# get vectors in the right order
embedding_vectors = get_weight_matrix(raw_embedding, tokenizer.word_index)
# create the embedding layer
embedding_layer = Embedding(vocab_size, 100, weights=[embedding_vectors], input_length=max_length, trainable=False)

سنستخدم نفس الكود البرمجي لبناء شبكة الطّي العصبونية.

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

إنّ مهمة التصنيف هنا على نصوص مراجعات الأفلام، بينما أشعة تضمين Glove مدربة على بيانات كبيرة من ويكبيديا.

ففضاء أشعة الكلمات ضمن مواضيع ويكبيديا مختلف، حيث ستختلف معاني ترابط الكلمات مع بعضها ودلالتها [9]، وفي هذه الحالة سيعطي تدريب طبقة التضمين مع نفس المهمة نتائج أفضل في الدقة، بينما في مهمات أخرى مثل تصنيف النصوص إلى مواضيعها [10] أو تحديد النص الزائف [11] فسيكون استخدام أشعة تضمين الكلمات المدربة مسبقاً على بيانات ضخمة ذو أثر فعّال في النتائج.

الخلاصـــة

 تعلمنا في هذا المقال التعليمي كيفية تطوير طبقة التضمين من أجل تصنيف آراء مراجعات الأفلام.

فقد أنجزنا المهام التالية:

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

المراجع

1- A Sentimental Education: Sentiment Analysis Using Subjectivity Summarization Based on Minimum Cuts, 2004.

2- Movie Review Polarity Dataset (review_polarity.tar.gz, 3MB).

3-https://aiinarabic.com/lenet-convolutional-neural-network-in-python/

4-Jason Brownlee, Deep Learning for Natural Language Processing Develop Deep Learning Models for Natural Language in Python, Edition: v1.1, Machine Learning Mastry, 2017.

5-  Jason Brownlee, How to Develop a Deep Convolutional Neural Network for Sentiment Analysis (Text Classification), https://machinelearningmastery.com/develop-word-embedding-model-predicting-movie-review-sentiment/.

6-T. Mikolov, I. Sutskever, K. Chen, G. Corrado, and J. Dean, “Distributed Representations of Words and Phrases and their Compositionality,”

arXiv:1310.4546 [cs.CL], 2013.

7-Tomas Mikolov, Kai Chen, Greg Corrado, Jeffry Dean, “Efficient Estimation of Word Representations in Vector Space”, arXiv:1301.3781v3 [cs.CL] 7 Sep 2013.

8-JeffreyPennington, RichardSocher, ChristopherD.Manning, GloVe: GlobalVectorsforWordRepresentation, Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP), 2014.

9- Francois chollet, “Deep book with python”, Manning publication Co, 2018.

10-D.Sagheer, F.Sukkar, Arabic Sentences Classification via Deep Learning, International Journal of Computer Applications, Vol 182, I ,2018.

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

الكاتب

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

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

0 Shares:
تعليق واحد
اترك تعليقاً

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

You May Also Like