دليلك للاستفادة من Machine Learning لبناء Chatbot (الجزء الثاني)

193

لنكمل التطبيق العملي لبناء chatbot

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

def evaluate_recall(y, y_test, k=1):
num_examples = float(len(y))
num_correct = 0
for predictions, label in zip(y, y_test):
if label in predictions[:k]:
num_correct += 1
return num_correct/num_examples

y هي قائمة من التوقعات مصنفة حسب الترتيب بترتيب تنازلي ، و y_test هي التسمية الفعلية. على سبيل المثال، ان كانت y قائمة من [0،3،1،2،5،6،4،7،8،9] أن العنصر 0 قد حصل على أعلى درجة، وحصل العنصر 9 على أدنى درجة. تذكر أن لدينا 10 عناصر لكل مثال في مجموعة الاختبار، والأول (رقم 0 في فهرس القائمة) هو الصحيح دائمًا لأن عمود النطق يأتي قبل أعمدة التشتيت في بياناتنا.

منطقيا، يجب أن يحصل المتنبئ العشوائي تمامًا على 10٪ من recall@1، و20٪ من recall@2، وما إلى ذلك. دعونا نرى ما إذا كان هذا هو الحال.

# Random Predictor
def predict_random(context, utterances):
return np.random.choice(len(utterances), 10, replace=False)
# Evaluate Random predictor
y_random = [predict_random(test_df.Context[x], test_df.iloc[x,1:].values) for x in range(len(test_df))]
y_test = np.zeros(len(y_random))
for n in [1, 2, 5, 10]:
print(“Recall @ ({}, 10): {:g}”.format(n, evaluate_recall(y_random, y_test, n)))

النتيجة.

Recall @ (1, 10): 0.0937632
Recall @ (2, 10): 0.194503
Recall @ (5, 10): 0.49297
Recall @ (10, 10): 1

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

class TFIDFPredictor:
def __init__(self):
self.vectorizer = TfidfVectorizer()
def train(self, data):
self.vectorizer.fit(np.append(data.Context.values,data.Utterance.values))
def predict(self, context, utterances):
# Convert context and utterances into tfidf vector
vector_context = self.vectorizer.transform([context])
vector_doc = self.vectorizer.transform(utterances)
# The dot product measures the similarity of the resulting vectors
result = np.dot(vector_doc, vector_context.T).todense()
result = np.asarray(result).flatten()
# Sort by top results and return the indices in descending order
return np.argsort(result, axis=0)[::-1]
# Evaluate TFIDF predictor
pred = TFIDFPredictor()
pred.train(train_df)
y = [pred.predict(test_df.Context[x], test_df.iloc[x,1:].values) for x in range(len(test_df))]
for n in [1, 2, 5, 10]:
print(“Recall @ ({}, 10): {:g}”.format(n, evaluate_recall(y, y_test, n)))

النتيجة.

Recall @ (1, 10): 0.495032
Recall @ (2, 10): 0.596882
Recall @ (5, 10): 0.766121
Recall @ (10, 10): 1

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

نموذج شبكة ذاكرة المصطلحات طويلة-قصيرة LSTM ثنائي التشفير

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

نموذج شبكة LSTM ثنائي التشفير سنبني مثل هذا الشكل:

يعمل تقريبا على النحو التالي:

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

يتم تغذية كل من السياق والاستجابة في نفس الشبكة العصبية كلمة تلو الأخرى. تولد الشبكة متجهاً يحمل، بشكل فضفاض “المعنى” لكل من السياق والاستجابة (c وr في الصورة). يمكننا اختيار مدى حجم هذه المتجهات، لنفترض أننا اخترنا 256 بعداً.

نقوم بضرب قيمة c مع مصفوفة M للتنبؤ برد r. إذا كانت c عبارة عن متجه 256 بعداً، فإن M هي مصفوفة الأبعاد 256 × 256، والنتيجة هي متجه آخر 256 بعداً، وهو ما يمكن أن يفسر على أنها استجابة تم إنشاؤها. يتم تعليم المصفوفة M أثناء التدريب.

نحن نقيس التشابه مع الاستجابة المتوقعة r’ والإجابة الفعلية r عن طريق أخذ حاصل ضرب كلا المتجهين. عند الحصول على قيمة كبيرة ذلك يعني أن المتجهات متشابهة وأن الاستجابة يجب أن تحصل على درجة عالية. ثم نقوم بتطبيق وظيفة sigmoid لتحويل تلك الدرجة إلى احتمالية. لاحظ أنه يتم دمج الخطوتين 3 و4 في الشكل.

لتدريب النموذج، نحتاج أيضًا إلى وظيفة لحساب التكلفة. سنستخدم الخسارة الثنائية المشتركة للأنتروبيا المشتركة لمشاكل التصنيف binary cross-entropy loss common for classification problems. دعنا الاحتمالات التي ستعمل عليها هذه الوظيفة. لكل استجابة منهم احتمالين فقط، يمكن أن تكون 1 (استجابة فعلية) أو 0 (استجابة غير صحيحة). يتم حساب خسارة الانتروبيا عبر

 L = −y * Ln (y’) - (1 - y) * Ln (1 − y’).

 المغزى وراء هذه الصيغة بسيط. إذا تم اختيار y = 1، فسيتم تركنا مع

L = -ln (y')

، والذي يمنع ان يكون التنبؤ بعيدًا عن 1.

وإذا y = 0 يتم تركنا مع

L = −Ln (1 − y')،

 والذي يمنع ان يكون التنبؤ بعيدا عن الصفر.

لتنفيذنا ذلك، سنستخدم مزيجًا من numpy وpandas وTensorflow وTF Learn (مجموعة من وظائف عالية المستوى ل Tensorflow).

تحضير البيانات

تأتي مجموعة البيانات في الأصل بصيغة CSV. يمكننا العمل مباشرة مع ملفات CSV، ولكن من الأفضل تحويل بياناتنا إلى تنسيق الصيغة TFRecord الخاصة ب Tensorflow. وتتمثل الميزة الرئيسية لهذا التنسيق في أنه يسمح لنا بتحميل الموترات tensors مباشرةً من ملفات الإدخال والسماح لـ Tensorflow بالتعامل مع جميع عمليات الخلط والتجميع والاصطفاف في المدخلات. كجزء من المعالجة المسبقة، نقوم أيضًا بإنشاء قائمة بالمفردات. هذا يعني أننا نقوم بتعيين كل كلمة إلى عدد صحيح، على سبيل المثال، قد تصبح “قطة” 2631. ستقوم ملفات TFRecord التي سنولدها لاحقاً بتخزين هذه الأرقام الصحيحة بدلاً من الكلمات. كما سنحفظ المفردات حتى نتمكن من إعادة تعيين الأعداد الصحيحة إلى الكلمات في أي وقت.

يحتوي كل مثال على:

  • السياق context: سلسلة من معرفات الكلمات “في هيئة اعداد صحيحة” التي تمثل نص السياق، على سبيل المثال، [231، 2190، 737، 0، 912].
  • طول السياق context_len: عدد كلمات الحوار على سبيل المثال 5 على سبيل المثال أعلاه.
  • الاستجابة utterance: سلسلة من معرفات الكلمات التي تمثل الكلام (استجابة).
  • طول الكلام utterance_len: عدد كلمات الاستجابة.
  • التسمية label: فقط في بيانات التدريب. 0 أو 1.
  • المشتت distractor_ [N]: فقط في بيانات الاختبار / التحقق من الصحة. N يتراوح من 0 إلى 8. سلسلة من معرفات الكلمات التي تمثل نص الكلام(المشتتات).
  • طول المشتت distractor_ [N] _len: فقط في بيانات الاختبار / التحقق من الصحة. N يتراوح من 0 إلى 8. طول الكلام(المشتتات).

تتم المعالجة المسبقة بواسطة البرنامج prep_data.py، الذي يقوم بإنشاء 3 ملفات: train.tfrecords وvalidation.tfrecords و test. tfrecords. يمكنك تشغيل البرنامج بنفسك أو تنزيل ملفات البيانات هنا.

إنشاء وظيفة مدخلات

من أجل استخدام الدعم المدمج الخاص ب Tensorflow للتدريب والتقييم، نحتاج إلى إنشاء وظيفة إدخال – وظيفة تقوم بإرجاع دفعات من بيانات الإدخال الخاصة بنا بالشكل المطلوب. في الواقع، نظرًا لأن بيانات التدريب والاختبار الخاصة بنا تحتوي على تنسيقات مختلفة، فنحن بحاجة إلى وظائف إدخال مختلفة لكل منهم. يجب أن تقوم وظيفة الإدخال بإرجاع مجموعة من الخصائص والتسميات (إذا كانت متوفرة). شيء على غرار ما يلي:

def create_input_fn(mode, input_files, batch_size, num_epochs=None):
def input_fn():
# TODO Load and preprocess data here
return batched_features, labels
return input_fn

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

يمكن العثور على البرنامج كاملاً في udc_inputs.py، تقوم الوظيفة عموماً بما يلي:

  1. إنشاء تعريف للخصائص الموجودة في ملف المثال الخاص بنا.
  2. قراءة السجلات من ملفات الإدخال عن طريق tf.TFRecordReader.
  3. تحليل السجلات وفقا لتعريف الخصائص.
  4. استخراج مسميات التدريب.
  5. جمع الأمثلة ومسميات التدريب.
  6. إرجاع الأمثلة والمسميات التدريبية.
  7. تحديد مقاييس التقييم.

إنشاء مقاييس التقييم

لقد ذكرنا من قبل أننا نريد استخدام مقياس recall@k لتقييم نموذجنا. لحسن الحظ، يأتي Tensorflow بالفعل مع العديد من مقاييس التقييم التي يمكننا استخدامها، بما في ذلك recall@k. لاستخدام هذه المقاييس، نحتاج إلى إنشاء قاموس يقوم بالربط بين نتيجة هذا المقاس وتمريرها إلى وظيفة تأخذ التنبؤات والمسميات كمدخلات لها:

def create_evaluation_metrics():
eval_metrics = {}
for k in [1, 2, 5, 10]:
eval_metrics[“recall_at_%d” % k] = functools.partial(
tf.contrib.metrics.streaming_sparse_recall_at_k,
k=k)
return eval_metrics

أعلاه، نستخدم functools.partial لتحويل وظيفة تأخذ 3 مدخلات إلى وظيفة تأخذ مدخلين فقط. لا تدع اسمًا مثل streaming_sparse_recall_at_k يربكك. streaming هنا يعني أن نتيجة هذا المقياس تراكمية نسبة الي بيانات الادخال، بينما sparse تشير إلى تنسيق المسميات.

وهذا يجلب نقطة مهمة: ما هو بالضبط شكل تنبؤاتنا أثناء التقييم؟ أثناء التدريب، نتوقع احتمال أن يكون المثال صحيحًا. ولكن أثناء التقييم، هدفنا هو تقيم كل من الاستجابة و9 من المشتتات واختيار أفضلهم – فنحن لا نتنبأ فقط بالصحيح / الخطأ. وهذا يعني أنه خلال التقييم، يجب أن يحصل كل مثال على متجه مكون من 10 نتائج، على سبيل المثال. [0.34، 0.11، 0.22، 0.45، 0.01، 0.02، 0.03، 0.08، 0.33، 0.11]، حيث تتوافق الدرجات مع الاستجابة الحقيقية و9 مشتتات على التوالي. يتم تسجيل كل واحد منهم بشكل مستقل، لذلك لا تحتاج الاحتمالات ان يكون حاصل جمعها 1. نظرًا لأن الاستجابة الحقيقية هي دومًا العنصر 0 في القائمة. سيتم حساب المثال أعلاه على أنه تم تصنيفه بشكل غير صحيح عن طريق recall@1 لأن المشتت الثالث حصل على احتمال 0.45 في حين أن الاستجابة الحقيقية ” العنصر 0″ حصل فقط على 0.34.

مرحلة التدريب

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

estimator = tf.contrib.learn.Estimator(
model_fn=model_fn,
model_dir=MODEL_DIR,
config=tf.contrib.learn.RunConfig())
input_fn_train = udc_inputs.create_input_fn(
mode=tf.contrib.learn.ModeKeys.TRAIN,
input_files=[TRAIN_FILE],
batch_size=hparams.batch_size)
input_fn_eval = udc_inputs.create_input_fn(
mode=tf.contrib.learn.ModeKeys.EVAL,
input_files=[VALIDATION_FILE],
batch_size=hparams.eval_batch_size,
num_epochs=1)
eval_metrics = udc_metrics.create_evaluation_metrics()
# We need to subclass theis manually for now. The next TF version will
# have support ValidationMonitors with metrics built-in.
# It’s already on the master branch.
class EvaluationMonitor(tf.contrib.learn.monitors.EveryN):
def every_n_step_end(self, step, outputs):
self._estimator.evaluate(
input_fn=input_fn_eval,
metrics=eval_metrics,
steps=None)
eval_monitor = EvaluationMonitor(every_n_steps=FLAGS.eval_every)
estimator.fit(input_fn=input_fn_train, steps=None, monitors=[eval_monitor])

هنا نقوم بإنشاء model_fn، على هيئة وظيفتين لإدخال لبيانات التدريب والتقييم، وقاموس لمقاييس التقييم لدينا. ووظيفة اخري تقوم بمتابعة تقييم النموذج عند كل خطوة من FLAGS.eval_every “أي عند كل اكتمال لاي عملية تقييم” أثناء التدريب. أخيرا، نقوم بتدريب النموذج. يعمل التدريب إلى أجل غير مسمى، ولكن Tensorflow يقوم تلقائيًا بحفظ ملفات نقاط التفتيش في MODEL_DIR “أي المكان يحث يحفظ النموذج”، بحيث يمكنك إيقاف التدريب في أي وقت. هناك طريقة أفضل وهي استخدام التوقف المبكر، مما يعني أنك تتوقف عن التدريب تلقائيًا عندما يتوقف مقياس التحقق من الصحة عن التحسن (بمعنى أنك وصلت لمرحلة الملائمة الزائدة over fitting). يمكنك رؤية البرنامج بالكامل في udc_train.py.

إنشاء النموذج

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

def dual_encoder_model(
hparams,
mode,
context,
context_len,
utterance,
utterance_len,
targets):
# Initialize embedidngs randomly or with pre-trained vectors if available
embeddings_W = get_embeddings(hparams)
# Embed the context and the utterance
context_embedded = tf.nn.embedding_lookup(
embeddings_W, context, name=”embed_context”)
utterance_embedded = tf.nn.embedding_lookup(
embeddings_W, utterance, name=”embed_utterance”)
# Build the RNN
with tf.variable_scope(“rnn”) as vs:
# We use an LSTM Cell
cell = tf.nn.rnn_cell.LSTMCell(
hparams.rnn_dim,
forget_bias=2.0,
use_peepholes=True,
state_is_tuple=True)
# Run the utterance and context through the RNN
rnn_outputs, rnn_states = tf.nn.dynamic_rnn(
cell,
tf.concat(0, [context_embedded, utterance_embedded]),
sequence_length=tf.concat(0, [context_len, utterance_len]),
dtype=tf.float32)
encoding_context, encoding_utterance = tf.split(0, 2, rnn_states.h)
with tf.variable_scope(“prediction”) as vs:
M = tf.get_variable(“M”,
shape=[hparams.rnn_dim, hparams.rnn_dim],
initializer=tf.truncated_normal_initializer())
# “Predict” a response: c * M
generated_response = tf.matmul(encoding_context, M)
generated_response = tf.expand_dims(generated_response, 2)
encoding_utterance = tf.expand_dims(encoding_utterance, 2)
# Dot product between generated response and actual response
# (c * M) * r
logits = tf.batch_matmul(generated_response, encoding_utterance, True)
logits = tf.squeeze(logits, [2])
# Apply sigmoid to convert logits to probabilities
probs = tf.sigmoid(logits)
# Calculate the binary cross-entropy loss
losses = tf.nn.sigmoid_cross_entropy_with_logits(logits, tf.to_float(targets))
# Mean loss across the batch of examples
mean_loss = tf.reduce_mean(losses, name=”mean_loss”)
return probs, mean_loss

البرنامج بالكامل موجود في الملف double_encoder.py. يمكننا الآن إنشاء وظيفة النموذج لدينا في udc_train.py الذي حددناه سابقًا.

model_fn = udc_model.create_model_fn(
hparams=hparams,
model_impl=dual_encoder_model)

النتيجة.

INFO:tensorflow:training step 20200, loss = 0.36895 (0.330 sec/batch).
INFO:tensorflow:Step 20201: mean_loss:0 = 0.385877
INFO:tensorflow:training step 20300, loss = 0.25251 (0.338 sec/batch).
INFO:tensorflow:Step 20301: mean_loss:0 = 0.405653
INFO:tensorflow:Results after 270 steps (0.248 sec/batch): 
			recall_at_1 = 0.507581018519, recall_at_2 = 0.689699074074, recall_at_5 = 0.913020833333, recall_at_10 = 1.0, loss = 0.5383

تقيم النموذج

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

 python udc_test.py - model_dir = $ MODEL_DIR_FROM_TRAINING ،

على سبيل المثال.

 python udc_test.py - model_dir = ~ / github / chatbot-retrieval / runs / 1467389151.

سيؤدي هذا إلى تشغيل مقاييس التقييم recall@k على مجموعة الاختبار بدلاً من مجموعة التحقق من الصحة. لاحظ أنه يجب عليك استدعاء بـ udc_test.py بنفس المدخلات التي استخدمتها أثناء التدريب. لذا، إذا كنت قد تدربت مع – embedding_size = 128 فأنت بحاجة إلى استدعاء البرنامج الاختبار بنفس الطريقة.

بعد التدريب لحوالي 20000 خطوة (حوالي ساعة على بطاقة معالجة رسومات GPU سريعة) يحصل نموذجنا على النتائج التالية من مجموعة الاختبار:

recall_at_1 = 0.507581018519
recall_at_2 = 0.689699074074
recall_at_5 = 0.913020833333

في حين أن recall@1 قريب من نموذج TFIDF، فإن recall@2 وrecall@5 أفضل بكثير، مما يشير إلى أن شبكتنا العصبية تعيّن أعلى الدرجات للإجابات الصحيحة. ذكرت الصحيفة الأصلية 0.55 و0.72 و0.92 لكل من recall@1, recall@2, and recall@5 على التوالي، لكنني لم أتمكن من إعادة إنتاج درجات عالية. ربما تؤدي المعالجة الإضافية للبيانات إلى زيادة النقاط قليلاً.

القيام بالتنبؤات

يمكنك تعديل وتشغيل udc_predict.py للحصول على نتائج الاحتمالات للبيانات لغير المرئية. على سبيل المثال.

 python udc_predict.py - model_dir =. / runs / 1467576365 / outputs:

النتيجة.

Context: Example context
Response 1: 0.44806
Response 2: 0.481638

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

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

المصدر:(Ultimate Guide to Leveraging NLP & Machine Learning for your Chatbot)

تعليقات