سنرى في هذا المنشور كيفية إنشاء أسئلة الاختيار من متعدد (MCQs) تلقائيًا من أي قصة أو مقالة. هذه خطوة واحدة نحو إنشاء تلك MCQs التي تراها تلقائيًا في المدرسة الإعدادية / المدرسة الثانوية.
الإدخال: ستكون المدخلات إلى برنامجنا مقالة (مقالة كمثال يتم توفيرها في مستودع github أدناه) مثل ما يلي :
The Greek historian knew what he was talking about. The Nile River fed Egyptian civilization for hundreds of years. The Longest River the Nile is 4,160 miles long — the world’s longest river. It begins near the equator in Africa and flows north to the Mediterranean Sea ..............
الإخراج: سيكون الإخراج عبارة عن مجموعة من MCQs التي يتم إنشاؤها تلقائيًا مع عوامل تشتيت فعالة (اختيارات إجابة خاطئة) :
1) The Nile provided so well for _______ that sometimes they had surpluses, or more goods than they needed.
a ) Angolan
b ) Algerian
c ) Egyptians
d ) Bantu
More options: ['Basotho', 'Beninese', 'Berber', 'Black African', 'Burundian', 'Cameroonian', 'Carthaginian', 'Chadian', 'Chewa', 'Congolese', 'Djiboutian', 'Egyptian', 'Ethiopian', 'Eurafrican', 'Ewe', 'Fulani']
2) As in many ancient societies, much of the knowledge of _______ came about as priests studied the world to find ways to please the gods.
a ) Malawi
b ) East Africa
c ) Somalia
d ) Egypt
More options: ['Togo', 'Zimbabwe', 'Gabon', 'Ghana', 'Lake Tanganyika', 'Ottoman Empire', 'Mozambique', 'Iran', 'Israel', 'Saudi Arabia', 'Lebanon', 'Turkey', 'Iraq', 'Levant', 'Syria', 'Jordan']
3) The _______ provided so well for Egyptians that sometimes they had surpluses, or more goods than they needed.
a ) Nyala
b ) Omdurman
c ) Nile
d ) Port Sudan
More options: ['Khartoum', 'Nubian Desert', 'Darfur', 'Libyan Desert', 'Kordofan', 'Gulu', 'Buganda', 'Entebbe', 'Jinja', 'Lake Edward', 'entebbe', 'gulu', 'kayunga', 'Upper Egypt', 'Suez Canal', 'Aswan High Dam']
دعنا نبدأ في معرفة كيف يمكنك إنشاء منشئ MCQ الخاص بك بالاستفادة من أحدث تقنيات معالجة اللغة الطبيعية NLP.
(إذا كنت ترغب في تجربة مولد MCQ المتقدم مباشرة، فيرجى زيارة https://questgen.ai/)
قم أولاً بتثبيت المكتبات الضرورية في jupyter notebook. إذا واجهت أي خطأ، فما عليك سوى التلاعب بالإصدارات والتحقق من حالات عدم التوافق الأخرى:
!pip install gensim
!pip install git+https://github.com/boudinfl/pke.git
!python -m spacy download en
!pip install bert-extractive-summarizer --upgrade --force-reinstall
!pip install spacy==2.1.3 --upgrade --force-reinstall
!pip install -U nltk
!pip install -U pywsd
import nltk
nltk.download('stopwords')
nltk.download('popular')
BERT Extractive Summarizer
هنا نستخدم مكتبة بسيطة bert-extractive-summarizer والذي تقوم بهذه المهمة بالنسبة لنا. نقوم بتحميل النص الإجمالي من ملف egypt.txt الموجود في Github repo ونطلب من المكتبة أن تعطينا نصًا موجزًا. يمكنك اللعب مع معلمات النسبة، الحد الأقصى والحد الأدنى لطول الجمل التي يجب الاحتفاظ بها للتلخيص وما إلى ذلك.
from summarizer import Summarizer
f = open("egypt.txt","r")
full_text = f.read()
model = Summarizer()
result = model(full_text, min_length=60, max_length = 500 , ratio = 0.4)
summarized_text = ''.join(result)
print (summarized_text)
نحصل على جزء ملخص من النص الأصلي كناتج:
The Nile River fed Egyptian civilization for hundreds of years. It begins near the equator in Africa and flows north to the Mediterranean Sea. A delta is an area near a river’s mouth where the water deposits fine soil called silt.......................
استخراج الكلمات الرئيسية
نحن نستخدم مكتبة python keyword extractor (PKE) ونستخرج جميع الكلمات الرئيسية keywords المهمة من النص الأصلي. ثم احتفظ بالكلمات الرئيسية الموجودة في النص الملخص فقط. هنا أقوم فقط باستخراج الأسماء لأنها ستكون أكثر ملاءمة لـ MCQs. كما أنني أقوم باستخراج 20 كلمة رئيسية فقط ولكن يمكنك اللعب بهذه المعلمة (get_n_best).
import pprint
import itertools
import re
import pke
import string
from nltk.corpus import stopwords
def get_nouns_multipartite(text):
out=[]
extractor = pke.unsupervised.MultipartiteRank()
extractor.load_document(input=text)
# not contain punctuation marks or stopwords as candidates.
pos = {'PROPN'}
#pos = {'VERB', 'ADJ', 'NOUN'}
stoplist = list(string.punctuation)
stoplist += ['-lrb-', '-rrb-', '-lcb-', '-rcb-', '-lsb-', '-rsb-']
stoplist += stopwords.words('english')
extractor.candidate_selection(pos=pos, stoplist=stoplist)
# 4. build the Multipartite graph and rank candidates using random walk,
# alpha controls the weight adjustment mechanism, see TopicRank for
# threshold/method parameters.
extractor.candidate_weighting(alpha=1.1,
threshold=0.75,
method='average')
keyphrases = extractor.get_n_best(n=20)
for key in keyphrases:
out.append(key[0])
return out
keywords = get_nouns_multipartite(full_text)
print (keywords)
filtered_keys=[]
for keyword in keywords:
if keyword.lower() in summarized_text.lower():
filtered_keys.append(keyword)
print (filtered_keys)
الإخراج هو:
Original text keywords: ['egyptians', 'nile river', 'egypt', 'nile', 'euphrates', 'tigris', 'old kingdom', 'red land', 'crown', 'upper', 'lower egypt', 'narmer', 'longest river', 'africa', 'mediterranean sea', 'hyksos', 'new kingdom', 'black land', 'ethiopia', 'middle kingdom']
Keywords present in summarized text: ['egyptians', 'nile river', 'egypt', 'nile', 'old kingdom', 'red land', 'crown', 'upper', 'lower egypt', 'narmer', 'africa', 'mediterranean sea', 'new kingdom', 'middle kingdom']
تعيين الجملة
لكل كلمة رئيسية سنستخرج الجمل المقابلة التي تحتوي على الكلمة من النص الملخص.
from nltk.tokenize import sent_tokenize
from flashtext import KeywordProcessor
def tokenize_sentences(text):
sentences = [sent_tokenize(text)]
sentences = [y for x in sentences for y in x]
# Remove any short sentences less than 20 letters.
sentences = [sentence.strip() for sentence in sentences if len(sentence) > 20]
return sentences
def get_sentences_for_keyword(keywords, sentences):
keyword_processor = KeywordProcessor()
keyword_sentences = {}
for word in keywords:
keyword_sentences[word] = []
keyword_processor.add_keyword(word)
for sentence in sentences:
keywords_found = keyword_processor.extract_keywords(sentence)
for key in keywords_found:
keyword_sentences[key].append(sentence)
for key in keyword_sentences.keys():
values = keyword_sentences[key]
values = sorted(values, key=len, reverse=True)
keyword_sentences[key] = values
return keyword_sentences
sentences = tokenize_sentences(summarized_text)
keyword_sentence_mapping = get_sentences_for_keyword(filtered_keys, sentences)
print (keyword_sentence_mapping)
الإخراج هو:
{'egyptians': ['The Nile provided so well for Egyptians that sometimes they had surpluses, or more goods than they needed.', 'For example, some ancient Egyptians learned to be scribes, people whose job was to write and keep records.', 'Egyptians believed that if a tomb was robbed, the person buried there could not have a happy afterlife.',
'nile river': ['The Nile River fed Egyptian civilization for hundreds of years.'],
'old kingdom': ['Historians divide ancient Egyptian dynasties into the Old Kingdom, the Middle Kingdom, and the New Kingdom.', 'The Old Kingdom started about 2575 B.C., when the Egyptian empire was gaining strength.'], } .....................
توليد MCQ
هنا نحصل على عوامل التشتيت (اختيارات إجابات خاطئة) من Wordnet و Conceptnet لتوليد أسئلة MCQ النهائية.
ما هي عوامل التشتيت (اختيارات الإجابة الخاطئة)؟
لنفترض أن “المؤرخين يقسمون السلالات المصرية القديمة إلى المملكة القديمة، والمملكة الوسطى، والمملكة الحديثة Historians divide ancient Egyptian dynasties into the Old Kingdom, the Middle Kingdom, and the New Kingdom”. هي جملتنا ونريد أن نعطي كلمة “Egyptian” كملء للكلمة الرئيسية الفارغة، فإن اختيارات الإجابة الخاطئة الأخرى يجب أن تكون مشابهة للكلمة المصرية وليس مرادفة للمصرية.
Eg: Historians divide ancient __________ dynasties into the Old Kingdom, the Middle Kingdom, and the New Kingdom
a)Egyptian
b)Ethiopian
c)Angolian
d)Algerian
في الأسئلة أعلاه، الخيارات b)، c)و d) مشتتات.
كيف نولد هذه المشتتات تلقائيًا؟
نتبع طريقتين، أحدهما مع wordnet والآخر باستخدام conceptnet للحصول على المشتتات.
نهج Wordnet المستخدم في الكود الخاص بنا:
بالنظر إلى المدخلات كجملة وكلمة رئيسية، نحصل على ” sense” الكلمة أولاً.
اسمحوا لي أن أشرح بـ “sense” بمثال. إذا كانت لدينا جملة “طار الخفاش في الغابة وهبط على شجرة The bat flew into the jungle and landed on a tree” وكلمة رئيسية “خفاش bat “، فإننا نعلم تلقائيًا أننا هنا نتحدث عن الخفاش الثديي الذي يحتوي على أجنحة وليس مضرب كريكيت cricket bat أو مضرب بيسبول baseball bat. على الرغم من أننا بشر جيدون في ذلك، إلا أن الخوارزميات ليست جيدة جدًا في التمييز بين أحدهما والآخر. وهذا ما يسمى توضيح معنى الكلمة word sense disambiguation (WSD). في wordnet، قد يكون لكلمة “bat” عدة معانٍ، إحداها لمضرب الكريكيت، وواحدة للثدييات الطائرة وما إلى ذلك. لذا فإن الدالة get_wordsense تحاول الحصول على المعنى الصحيح للكلمة التي تُعطى جملة معها. هذا ليس مثاليًا في جميع الأوقات، ومن ثم نحصل على بعض الأخطاء إذا كانت الخوارزمية تضيق إحساسًا خاطئًا. عندئذٍ ستكون عوامل التشتيت (اختيارات الإجابة الخاطئة) خاطئة أيضًا.
بمجرد تحديد المعنى نسميه دالة get_distractors_wordnet للحصول على المشتتات. ما يحدث هنا هو أنه لنفترض أننا حصلنا على كلمة مثل “الفهد cheetah ” وحدد معناها، ثم ننتقل إلى Hypernym. كلمة Hypernym هي فئة ذات مستوى أعلى لكلمة معينة. في مثالنا، القطط هي hypernym للفهد.
ثم نذهب للعثور على جميع hyponyms (الفئات الفرعية) للقطط والتي قد تكون Leopard وTiger وLion وكلها تنتمي إلى مجموعة القطط. لذلك يمكننا استخدام Leopard وTiger وLion كمشتتات (اختيارات إجابة خاطئة) لـ MCQ المحدد.
نهج Conceptnet المستخدم في الكود الخاص بنا:
لا تتوفر كل الكلمات في wordnet ولا تحتوي جميعها على hypernyms. ومن ثم، إذا فشلنا في استخدام wordnet، فإننا نبحث في Conceptnet أيضًا عن عوامل التشتيت. get_distractors_conceptnet هي الدالة الرئيسية المستخدمة للحصول على المشتتات من Conceptnet. لا تملك Conceptnet شرطًا لإزالة الغموض بين معاني الكلمات المختلفة كما ناقشنا أعلاه مع مثال الخفافيش. ومن ثم فإننا نحتاج إلى المضي قدمًا في أي مفهوم تعطينا مفهوم المعنى عندما نستفسر بكلمة معينة.
دعونا نرى كيف نستخدم Conceptnet في حالة الاستخدام الخاصة بنا. لا نقوم بتثبيت أي شيء لأننا نستخدم Conceptnet API مباشرة. لاحظ أن هناك حدًا لمعدل API لكل ساعة، لذا احذر منه.
بالنظر إلى كلمة مثل “كاليفورنيا California ” ، فإننا نستعلم عن Conceptnet واسترجاع علاقة “Partof”. في مثالنا “كاليفورنيا” جزء من “الولايات المتحدة”.
نذهب الآن إلى “الولايات المتحدة” ونرى الأشياء الأخرى التي تشترك معها في “جزء” من العلاقة. ستكون هذه ولايات أخرى مثل “تكساس” و “أريزونا” و “سياتل” وما إلى ذلك. ومن ثم بالنسبة إلى كلمة “كاليفورنيا” الخاصة بنا، قمنا بإحضار عوامل التشتيت “تكساس” و “أريزونا” إلخ.
import requests
import json
import re
import random
from pywsd.similarity import max_similarity
from pywsd.lesk import adapted_lesk
from pywsd.lesk import simple_lesk
from pywsd.lesk import cosine_lesk
from nltk.corpus import wordnet as wn
# Distractors from Wordnet
def get_distractors_wordnet(syn,word):
distractors=[]
word= word.lower()
orig_word = word
if len(word.split())>0:
word = word.replace(" ","_")
hypernym = syn.hypernyms()
if len(hypernym) == 0:
return distractors
for item in hypernym[0].hyponyms():
name = item.lemmas()[0].name()
#print ("name ",name, " word",orig_word)
if name == orig_word:
continue
name = name.replace("_"," ")
name = " ".join(w.capitalize() for w in name.split())
if name is not None and name not in distractors:
distractors.append(name)
return distractors
def get_wordsense(sent,word):
word= word.lower()
if len(word.split())>0:
word = word.replace(" ","_")
synsets = wn.synsets(word,'n')
if synsets:
wup = max_similarity(sent, word, 'wup', pos='n')
adapted_lesk_output = adapted_lesk(sent, word, pos='n')
lowest_index = min (synsets.index(wup),synsets.index(adapted_lesk_output))
return synsets[lowest_index]
else:
return None
# Distractors from http://conceptnet.io/
def get_distractors_conceptnet(word):
word = word.lower()
original_word= word
if (len(word.split())>0):
word = word.replace(" ","_")
distractor_list = []
url = "http://api.conceptnet.io/query?node=/c/en/%s/n&rel=/r/PartOf&start=/c/en/%s&limit=5"%(word,word)
obj = requests.get(url).json()
for edge in obj['edges']:
link = edge['end']['term']
url2 = "http://api.conceptnet.io/query?node=%s&rel=/r/PartOf&end=%s&limit=10"%(link,link)
obj2 = requests.get(url2).json()
for edge in obj2['edges']:
word2 = edge['start']['label']
if word2 not in distractor_list and original_word.lower() not in word2.lower():
distractor_list.append(word2)
return distractor_list
key_distractor_list = {}
for keyword in keyword_sentence_mapping:
wordsense = get_wordsense(keyword_sentence_mapping[keyword][0],keyword)
if wordsense:
distractors = get_distractors_wordnet(wordsense,keyword)
if len(distractors) ==0:
distractors = get_distractors_conceptnet(keyword)
if len(distractors) != 0:
key_distractor_list[keyword] = distractors
else:
distractors = get_distractors_conceptnet(keyword)
if len(distractors) != 0:
key_distractor_list[keyword] = distractors
index = 1
print ("#############################################################################")
print ("NOTE:::::::: Since the algorithm might have errors along the way, wrong answer choices generated might not be correct for some questions. ")
print ("#############################################################################\n\n")
for each in key_distractor_list:
sentence = keyword_sentence_mapping[each][0]
pattern = re.compile(each, re.IGNORECASE)
output = pattern.sub( " _______ ", sentence)
print ("%s)"%(index),output)
choices = [each.capitalize()] + key_distractor_list[each]
top4choices = choices[:4]
random.shuffle(top4choices)
optionchoices = ['a','b','c','d']
for idx,choice in enumerate(top4choices):
print ("\t",optionchoices[idx],")"," ",choice)
print ("\nMore options: ", choices[4:20],"\n\n")
index = index + 1
أخيرًا ، نقوم بإنشاء المخرجات على النحو التالي:
#############################################################################
NOTE:::::::: Since the algorithm might have errors along the way, wrong answer choices generated might not be correct for some questions.
#############################################################################
1) The Nile provided so well for _______ that sometimes they had surpluses, or more goods than they needed.
a ) Angolan
b ) Algerian
c ) Egyptians
d ) Bantu
More options: ['Basotho', 'Beninese', 'Berber', 'Black African', 'Burundian', 'Cameroonian', 'Carthaginian', 'Chadian', 'Chewa', 'Congolese', 'Djiboutian', 'Egyptian', 'Ethiopian', 'Eurafrican', 'Ewe', 'Fulani']
2) As in many ancient societies, much of the knowledge of _______ came about as priests studied the world to find ways to please the gods.
a ) Malawi
b ) East Africa
c ) Somalia
d ) Egypt
More options: ['Togo', 'Zimbabwe', 'Gabon', 'Ghana', 'Lake Tanganyika', 'Ottoman Empire', 'Mozambique', 'Iran', 'Israel', 'Saudi Arabia', 'Lebanon', 'Turkey', 'Iraq', 'Levant', 'Syria', 'Jordan']
3) The _______ provided so well for Egyptians that sometimes they had surpluses, or more goods than they needed.
a ) Nyala
b ) Omdurman
c ) Nile
d ) Port Sudan
More options: ['Khartoum', 'Nubian Desert', 'Darfur', 'Libyan Desert', 'Kordofan', 'Gulu', 'Buganda', 'Entebbe', 'Jinja', 'Lake Edward', 'entebbe', 'gulu', 'kayunga', 'Upper Egypt', 'Suez Canal', 'Aswan High Dam']
4) It combined the red _______ of Lower Egypt with the white _______ of Upper Egypt.
a ) Capital
b ) Crown
c ) Masthead
d ) Head
More options: []
5) It combined the red Crown of Lower Egypt with the white Crown of _______ Egypt.
a ) Upper Berth
b ) Lower Berth
c ) Upper
More options: []
لاحظ أنه قد يكون هناك العديد من الأخطاء إما بسبب المعنى الخاطئ للكلمة المستخرجة من خوارزمية إزالة الغموض عن معنى الكلمة (WSD) في Wordnet أو تحديدها بمعنى مختلف بسبب قيودها.
الأشياء التي يمكن تحسينها؟
- تمامًا مثل علاقة “partof”، توجد علاقة “IsA” في Conceptnet والتي يمكن استكشافها بشكل أكبر للحصول على المشتتات.
- نظرًا لأن لدينا جمل متعددة لكل كلمة رئيسية، استخدمها جميعًا للقيام بإزالة الغموض عن معنى الكلمة (WSD) واختيار المعنى الذي يحتوي على أعلى عدد.
- يمكن بدلاً من ذلك استخدام موجهات الكلمات (word2vec ، glove) كطريقة لتشتيت كلمة معينة.
- يمكنك استخدام دقة الضمير pronoun resolution (neural coreference resolution) على النص الكامل قبل تمريره إلى ملخّص BERT. ثم يتم حل أي جمل بها ضمائر بحيث تبدو كاملة ومستقلة عند تقديمها كـ MCQ.