معاينة مختبر آمنة
Transformers TF
هذي معاينة منقّحة للقراءة فقط؛ ما فيه أي شيء يشتغل داخل الصفحة.
قراءة فقط
معاينة الدفتر
Transformers TF
> **ملاحظة بيئة التشغيل المدمجة:** هالمعاينة تستخدم عيّنة صغيرة وثابتة وآمنة من ناحية الحقوق عشان تكون النتايج قابلة للتكرار. النتايج بالحجم الكامل تحتاج مجموعة البيانات أو النموذج الموثّق بالدرس داخل بيئة خارجية معتمدة.
# آليات الانتباه والمحوّلات
من أكبر عيوب الشبكات العصبية المتكررة إن كل كلمات التسلسل يكون لها التأثير نفسه على النتيجة. وهالشي يقلّل أداء نماذج LSTM القياسية ذات المشفّر ومفكّ الترميز في مهام التسلسل إلى التسلسل، مثل **التعرّف على الكيانات المسماة** والترجمة الآلية. بالواقع، كلمات معيّنة في تسلسل الإدخال غالبًا تأثّر على المخرجات المتسلسلة أكثر من غيرها.
خذوا نموذج تسلسل إلى تسلسل مثل الترجمة الآلية. ننفّذه بشبكتين متكررتين: تضغط الأولى، وهي **المشفّر**، تسلسل الإدخال إلى حالة مخفية؛ وتفرد الثانية، وهي **مفكّ الترميز**، هالحالة إلى ناتج مترجم. المشكلة بهالأسلوب إن الحالة النهائية للشبكة يصعب عليها تتذكر بداية الجملة، ولذلك تنخفض جودة النموذج مع الجمل الطويلة.
تعطينا **آليات الانتباه (Attention Mechanisms)** طريقة نوزّن بها أثر سياق كل متجه إدخال على كل تنبؤ تطلعه RNN. ننفّذها بإنشاء وصلات مختصرة بين الحالات الوسيطة في RNN المدخلة وRNN المخرجة. وبكذا، لما نولّد رمز المخرج $y_t$، نحسب حساب كل حالات الإدخال المخفية $h_i$، بمعاملات أوزان مختلفة $\alpha_{t,i}$.
> **وصف الشكل:** صورة تبيّن نموذج مشفّر ومفكّ ترميز معه طبقة انتباه إضافية
*نموذج المشفّر ومفكّ الترميز مع آلية الانتباه الإضافية في [Bahdanau et al., 2015](https://arxiv.org/pdf/1409.0473.pdf)، منقول عن [هالتدوينة](https://lilianweng.github.io/lil-log/2018/06/24/attention-attention.html)*
تمثّل مصفوفة الانتباه $\{\alpha_{i,j}\}$ مقدار مساهمة كلمات معيّنة من الإدخال في توليد كلمة محددة في تسلسل المخرج. وتحت مثال لهالمصفوفة:
> **وصف الشكل:** صورة تبيّن محاذاة نموذج RNNsearch-50، مأخوذة من Bahdanau - arviz.org
*الشكل من [Bahdanau et al., 2015](https://arxiv.org/pdf/1409.0473.pdf) (Fig.3)*
آليات الانتباه صارت أساسًا مهمًا لأنظمة NLP الحديثة، مع إن أفضل بنية تعتمد على المهمة والبيانات وقيود التشغيل. لكن إضافة الانتباه ترفع عدد معاملات النموذج كثير، وهذا سبّب مشاكل في توسيع RNNs. ومن أهم القيود إن طبيعة RNNs المتكررة تصعّب جمع أمثلة التدريب في دفعات وتنفيذها بالتوازي. لازم تعالج RNN كل عنصر من التسلسل بالترتيب، ولذلك مب سهل نوازيها.
أدى استخدام آليات الانتباه مع هالقيد إلى ظهور نماذج **المحوّل (Transformer)**. ومنها عائلات المشفّرات مثل BERT، ونماذج مفكّ الترميز التوليدية، وبنى المشفّر ومفكّ الترميز.
## نماذج المحوّلات
بدل ما نمرر سياق كل تنبؤ سابق إلى خطوة التقييم اللي بعدها، تستخدم **نماذج المحوّلات** **ترميزات الموضع** و**الانتباه** عشان تلتقط سياق المدخل داخل نافذة نصية محددة. والصورة تحت تبيّن كيف يلتقط ترميز الموضع مع الانتباه السياق داخل هالنافذة.
> **وصف الشكل:** صورة متحركة تبيّن كيف تتم عمليات التقييم في نماذج المحوّلات.
بما إن كل موضع في الإدخال يرتبط بكل موضع في المخرج بشكل مستقل، تقدر المحوّلات تنفّذ العمل بالتوازي أفضل من RNNs. وهالشي يسمح بنماذج لغة أكبر وأقدر على التعبير. ويقدر كل رأس انتباه يتعلّم علاقة مختلفة بين الكلمات، وهذا يحسّن مهام معالجة اللغة الطبيعية (NLP) اللاحقة.
## بناء نموذج محوّل بسيط
توفر Keras لبنات انتباه جاهزة، وبهالمثال بنبني كتلة مشفّر صغيرة بأنفسنا عشان نفهم مكوّناتها. ومثل قبل، بنركّز على **التصنيف** للنصوص في **مجموعة البيانات** AG News، مع إن نماذج المحوّل تعطي أفضل نتائجها عادةً في مهام NLP الأصعب.
import tensorflow as tf
from tensorflow import keras
import numpy as np
_course_titles = tf.constant([
"Regional leaders agree on a public health plan",
"The home team wins the basketball final",
"Markets rise after a strong business report",
"Researchers release an efficient intelligence model",
"Diplomats begin a new round of peace talks",
"The tennis champion returns after an injury",
"Retail sales improve with consumer confidence",
"A software update improves privacy",
])
_course_descriptions = tf.constant([
"World policy and international cooperation",
"Sports results and player development",
"Business revenue and economic indicators",
"Science and technology research",
"World diplomacy and humanitarian aid",
"Sports training and competition",
"Business investment and employment",
"Science security and technology",
])
_course_labels = tf.constant([0, 1, 2, 3, 0, 1, 2, 3], dtype=tf.int64)
_course_records = {"title": _course_titles, "description": _course_descriptions, "label": _course_labels}
ds_train = tf.data.Dataset.from_tensor_slices(_course_records).take(6)
ds_test = tf.data.Dataset.from_tensor_slices(_course_records).skip(6)
def extract_text(x):
return x['title'] + ' ' + x['description']
def tupelize(x):
return extract_text(x), x['label']لازم ترث أي طبقة جديدة في Keras من الفئة `Layer` وتنفّذ الدالة `call`. بنبدأ بطبقة **تضمين الموضع**، وبنستفيد من [جزء من الشفرة في توثيق Keras الرسمي](https://keras.io/examples/nlp/text_classification_with_transformer/). وبنفترض إننا نحشو كل تسلسلات الإدخال إلى الطول `maxlen`.
class TokenAndPositionEmbedding(keras.layers.Layer):
def __init__(self, maxlen, vocab_size, embed_dim):
super(TokenAndPositionEmbedding, self).__init__()
self.token_emb = keras.layers.Embedding(input_dim=vocab_size, output_dim=embed_dim)
self.pos_emb = keras.layers.Embedding(input_dim=maxlen, output_dim=embed_dim)
self.maxlen = maxlen
def call(self, x):
maxlen = self.maxlen
positions = tf.range(start=0, limit=maxlen, delta=1)
positions = self.pos_emb(positions)
x = self.token_emb(x)
return x+positionsتتكوّن هالطبقة من طبقتين `Embedding`: وحدة لتضمين الرموز بالطريقة اللي أخذناها قبل، والثانية لتضمين مواضع الرموز. ننشئ المواضع على شكل تسلسل أعداد طبيعية من 0 إلى `maxlen` باستخدام `tf.range`، ثم نمررها على طبقة التضمين. بعدها نجمع متجهي التضمين الناتجين، فيطلع لنا تمثيل للمدخل يجمع الرمز وموضعه، وشكله `maxlen`$\times$`embed_dim`.
> **وصف الشكل:** حُذف الأصل لأن حقوق إعادة استخدامه غير موثّقة.
الحين بننفّذ كتلة المحوّل. بتستقبل مخرج طبقة التضمين اللي عرّفناها قبل:
class TransformerBlock(keras.layers.Layer):
def __init__(self, embed_dim, num_heads, ff_dim, rate=0.1):
super(TransformerBlock, self).__init__()
self.att = keras.layers.MultiHeadAttention(num_heads=num_heads, key_dim=embed_dim, name='attn')
self.ffn = keras.Sequential(
[keras.layers.Dense(ff_dim, activation="relu"), keras.layers.Dense(embed_dim),]
)
self.layernorm1 = keras.layers.LayerNormalization(epsilon=1e-6)
self.layernorm2 = keras.layers.LayerNormalization(epsilon=1e-6)
self.dropout1 = keras.layers.Dropout(rate)
self.dropout2 = keras.layers.Dropout(rate)
def call(self, inputs, training=None):
attn_output = self.att(inputs, inputs)
attn_output = self.dropout1(attn_output, training=training)
out1 = self.layernorm1(inputs + attn_output)
ffn_output = self.ffn(out1)
ffn_output = self.dropout2(ffn_output, training=training)
return self.layernorm2(out1 + ffn_output)يطبّق المحوّل `MultiHeadAttention` على المدخل المرمّز موضعيًا، وينتج متجه انتباه أبعاده `maxlen`$\times$`embed_dim`. بعدها نمزج الناتج مع المدخل ونطبّعه باستخدام `LayerNormalization`.
> **ملاحظة:** تشبه `LayerNormalization` طبقة `BatchNormalization` اللي أخذناها في قسم *الرؤية الحاسوبية (Computer Vision)*، لكنها تطبّع مخرجات الطبقة السابقة لكل عيّنة تدريب بشكل مستقل وتجيبها للنطاق [-1..1].
بعدها نمرر مخرج هالطبقة على شبكة `Dense`، وهي في مثالنا بيرسيبترون بطبقتين. ثم نضيف النتيجة إلى المخرج النهائي، واللي يخضع للتطبيع مرة ثانية.
> **وصف الشكل:** حُذف الأصل لأن حقوق إعادة استخدامه غير موثّقة.
الحين حنا جاهزين نعرّف نموذج المحوّل كاملًا:
embed_dim = 32 # Embedding size for each token
num_heads = 2 # Number of attention heads
ff_dim = 32 # Hidden layer size in feed forward network inside transformer
maxlen = 256
vocab_size = 20000
model = keras.models.Sequential([
keras.Input(shape=(), dtype=tf.string),
keras.layers.TextVectorization(max_tokens=vocab_size, output_sequence_length=maxlen),
TokenAndPositionEmbedding(maxlen, vocab_size, embed_dim),
TransformerBlock(embed_dim, num_heads, ff_dim),
keras.layers.GlobalAveragePooling1D(),
keras.layers.Dropout(0.1),
keras.layers.Dense(20, activation="relu"),
keras.layers.Dropout(0.1),
keras.layers.Dense(4, activation="softmax")
])
model.summary()print('Training tokenizer')
model.layers[0].adapt(ds_train.map(extract_text))
model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')
model.fit(ds_train.map(tupelize).batch(128),validation_data=ds_test.map(tupelize).batch(128))## نماذج BERT المبنية على المحوّلات
**BERT** (Bidirectional Encoder Representations from Transformers) شبكة محوّلات كبيرة متعددة الطبقات؛ فيها 12 طبقة في *BERT-base* و 24 طبقة في *BERT-large*. ندرّب النموذج مسبقًا على متن كبير من البيانات النصية (WikiPedia + الكتب) بتدريب غير خاضع للإشراف، عن طريق توقّع الكلمات المحجوبة في الجملة. وخلال التدريب المسبق يكتسب النموذج فهمًا لغويًا واسعًا، وبعدها نستفيد منه مع مجموعات بيانات ثانية بالضبط الدقيق. وهالعملية اسمها **التعلم بالنقل**.
> **وصف الشكل:** صورة من https://jalammar.github.io/illustrated-bert/
فيه أشكال كثيرة من بُنى المحوّل، منها BERT وDistilBERT وBigBird ونماذج GPT التوليدية وغيرها، ونقدر نضبطها ضبطًا دقيقًا.
خلونا نشوف كيف نستخدم نموذج BERT مدرّبًا مسبقًا لحل مسألة **التصنيف** التقليدية عندنا. بنستفيد من الفكرة وبعض الشفرة في [التوثيق الرسمي](https://www.tensorflow.org/text/tutorials/classify_text_with_bert).
عشان نحمّل النماذج المدرّبة مسبقًا، بنستخدم **TensorFlow Hub**. أول شي نحمّل أداة التحويل إلى متجهات الخاصة بـ BERT:
vectorizer = keras.layers.TextVectorization(max_tokens=2000, output_sequence_length=32)
vectorizer.adapt(ds_train.map(extract_text))vectorizer(['I love transformers'])مهم تستخدمون أداة التحويل نفسها اللي تدربت عليها الشبكة الأصلية. وترجع أداة BERT ثلاثة مكوّنات:
* `input_word_ids`: تسلسل أرقام الرموز في جملة الإدخال
* `input_mask`: يبيّن وش الجزء اللي يحتوي المدخل الفعلي ووش الجزء اللي يمثل الحشو، ويشبه القناع اللي تنتجه طبقة `Masking`
* `input_type_ids`: نستخدمه في مهام نمذجة اللغة، ويسمح بتحديد جملتي إدخال داخل تسلسل واحد
بعدها نقدر ننشئ مستخرج سمات BERT:
bert = keras.Sequential([
keras.layers.Embedding(2000, 32),
keras.layers.GlobalAveragePooling1D(),
], name="course_local_encoder")z = bert(vectorizer(["I love transformers"]))
print(f"Local encoder output shape -> {z.shape}")ترجع طبقة BERT عدة نتائج مفيدة:
* `pooled_output`: ناتج أخذ متوسط كل الرموز في التسلسل. نقدر نعتبره تضمينًا دلاليًا ذكيًا للتسلسل كاملًا، ويكافئ مخرج طبقة `GlobalAveragePooling1D` في نموذجنا السابق
* `sequence_output`: مخرج آخر طبقة محوّل، ويقابل مخرج `TransformerBlock` في نموذجنا فوق
* `encoder_outputs`: مخرجات كل طبقات المحوّل. وبما إننا حمّلنا نموذج BERT من 4 طبقات، مثل ما يوضح اسمه اللي يحتوي `4_H`، فالناتج فيه 4 موترات؛ وآخر واحد منها هو نفسه `sequence_output`
الحين بنعرّف نموذج **التصنيف** كاملًا من المدخل إلى المخرج. بنستخدم *التعريف الدالي للنموذج*: نحدد مدخل النموذج، ثم نعطيه سلسلة تعبيرات تحسب المخرج. وبنثبّت أوزان BERT عشان ما تتدرّب، وندرّب المصنّف الأخير بس:
inp = keras.Input(shape=(), dtype=tf.string)
x = vectorizer(inp)
x = bert(x)
x = keras.layers.Dropout(0.1)(x)
out = keras.layers.Dense(4, activation="softmax")(x)
model = keras.models.Model(inp, out)
bert.trainable = False
model.summary()model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')
model.fit(ds_train.map(tupelize).batch(128),validation_data=ds_test.map(tupelize).batch(128))مع إن عدد المعاملات القابلة للتدريب قليل، العملية بطيئة؛ لأن مستخرج سمات BERT ثقيل حاسوبيًا. ويبدو إننا ما قدرنا نوصل إلى دقة معقولة، يا لأن التدريب ما كان كافيًا، أو لأن معاملات النموذج قليلة.
خلونا نفك تثبيت أوزان BERT وندرّبها بعد. هالخطوة تحتاج معدل تعلم صغير مرة، واستراتيجية تدريب أدق فيها **إحماء**، مع محسّن **AdamW**. بنستخدم الحزمة `tf-models-official` عشان ننشئ المحسّن:
bert.trainable = True
epochs = 2
model.compile(loss="sparse_categorical_crossentropy", metrics=["acc"], optimizer=keras.optimizers.Adam(learning_rate=3e-4))
model.fit(ds_train.map(tupelize).batch(4), validation_data=ds_test.map(tupelize).batch(4), epochs=epochs)مثل ما تشوفون، التدريب بطيء إلى حد كبير. لكن تقدرون تجرّبون تدريب النموذج عدة حقب (5-10)، وتشوفون هل يجيب نتيجة أفضل من الأساليب اللي استخدمناها قبل.
## مكتبة HuggingFace Transformers
من الطرق الشائعة، والأسهل شوي، لاستخدام نماذج المحوّل حزمة [HuggingFace](https://github.com/huggingface/). تعطينا لبنات جاهزة لمهام NLP مختلفة، وهي متاحة لكلٍ من TensorFlow وPyTorch، وهو إطار شائع لبناء **الشبكة العصبية** بعد.
> **ملاحظة:** إذا ما يهمكم تشوفون كيف تشتغل مكتبة Transformers، تقدرون تنتقلون إلى آخر الدفتر؛ لأن الخطوات الجاية ما تختلف جوهريًا عن اللي سويناه فوق. بنكرر تدريب نموذج BERT بمكتبة ثانية ونموذج أكبر بكثير، ولذلك التدريب بياخذ وقتًا طويلًا. يكفي تطالعون الشفرة إذا ما بغيتوا تنتظرون.
خلونا نشوف كيف نحل مسألتنا باستخدام [Huggingface Transformers](https://huggingface.co/).
أول شي نختار النموذج اللي بنستخدمه. إلى جانب بعض النماذج المدمجة، توفر HuggingFace [مستودع نماذج على الإنترنت](https://huggingface.co/models) فيه نماذج كثيرة درّبها المجتمع مسبقًا. نقدر نحمّل أي نموذج ونستخدمه بمجرد تحديد اسمه، وتنزل ملفاته الثنائية المطلوبة تلقائيًا.
وأحيانًا تحتاجون تحمّلون نماذجكم الخاصة. بهالحالة تحددون المجلد اللي فيه الملفات المطلوبة كلها، ومنها معاملات أداة التجزئة، وملف `config.json` اللي فيه معاملات النموذج، والأوزان الثنائية، وغيرها.
نقدر ننشئ النموذج وأداة التجزئة من اسم النموذج نفسه. بنبدأ بأداة التجزئة:
tokenizer = vectorizer
MAX_SEQ_LEN = 32
PAD_INDEX = 0
UNK_INDEX = 1الكائن `tokenizer` فيه الدالة `encode`، ونقدر نستخدمها مباشرةً لترميز النص:
tokenizer(["TensorFlow is a great framework for NLP"])ونقدر بعد نستخدم أداة التجزئة لترميز تسلسل بالشكل المناسب لتمريره إلى النموذج، بما فيه الحقول `token_ids` و`input_mask` وغيرها. ونقدر نطلب موترات TensorFlow بتمرير الوسيط `return_tensors='tf'`:
tokenizer(["Hello, there"])هنا بنستخدم نموذج BERT المدرّب مسبقًا `bert-base-uncased`. وتعني *Uncased* إن النموذج ما يفرّق بين حالة الأحرف.
وقت تدريب النموذج، لازم نعطيه تسلسلًا مجزّأً على شكل رموز. لذلك بنبني مسارًا لمعالجة البيانات. وبما إن `tokenizer.encode` دالة Python، بنستخدم الأسلوب نفسه اللي أخذناه في الوحدة السابقة ونستدعيها عن طريق `py_function`:
def process_fn(x):
text = tf.expand_dims(extract_text(x), axis=0)
return vectorizer(text)[0], x["label"]الحين نقدر نحمّل النموذج الفعلي باستخدام الحزمة `BertForSequenceClassification`. وتضمن لنا إن النموذج فيه بنية **التصنيف** المطلوبة، ومنها المصنّف الأخير. بيطلع لكم تحذير يقول إن أوزان المصنّف الأخير ما تهيّأت وإن النموذج يحتاج تدريبًا مسبقًا؛ وهذا طبيعي تمامًا، لأن هذا اللي بنسويه الحين!
model = keras.Sequential([
keras.layers.Embedding(2000, 32),
keras.layers.GlobalAveragePooling1D(),
keras.layers.Dense(4, activation="softmax"),
], name="course_local_sequence_classifier")model.summary()مثل ما تشوفون في `summary`، يحتوي النموذج قرابة 110 ملايين معاملة! وإذا كانت عندنا مهمة **التصنيف** البسيطة على **مجموعة البيانات** الصغيرة نسبيًا، فغالبًا ما نبي ندرّب طبقة BERT الأساسية:
model.layers[0].trainable = False
model.summary()الحين حنا جاهزين نبدأ التدريب!
> **ملاحظة:** تدريب نموذج BERT بالحجم الكامل ممكن ياخذ وقتًا طويلًا مرة. لذلك بندرّبه على أول 32 دفعة بس، والهدف إننا نوضّح إعداد التدريب. إذا بغيتوا تجرّبون التدريب كاملًا، احذفوا المعاملين `steps_per_epoch` و`validation_steps` واستعدوا للانتظار!
model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["acc"])
model.fit(ds_train.map(process_fn).batch(4), validation_data=ds_test.map(process_fn).batch(4), epochs=2)إذا زدتوا عدد التكرارات وانتظرتوا وقتًا كافيًا، ودرّبتوا النموذج عدة حقب، فتوقّعوا إن **التصنيف** باستخدام BERT بيعطيكم أفضل دقة. والسبب إن BERT فاهم بنية اللغة إلى حد كبير، وما نحتاج إلا نضبط المصنّف الأخير. لكن BERT نموذج كبير، فتدريبه كاملًا ياخذ وقتًا طويلًا وقدرة حوسبة قوية؛ GPU، والأفضل أكثر من وحدة.
> **ملاحظة:** استخدمنا في مثالنا واحدًا من أصغر نماذج BERT المدرّبة مسبقًا. فيه نماذج أكبر غالبًا تعطي نتائج أفضل.
## الزبدة
شفنا بهالوحدة بُنى نماذج حديثة جدًا مبنية على **المحوّلات**، وطبّقناها على مهمة **التصنيف** للنصوص. ونقدر بالطريقة نفسها نستخدم نماذج BERT لاستخراج الكيانات، والإجابة عن الأسئلة، وغيرها من مهام NLP.
تمثّل نماذج المحوّل أحدث التقنيات في NLP، وغالبًا هي أول حل يستاهل تجرّبونه إذا بنيتوا حلول NLP مخصصة. ومع كذا، فهم المبادئ الأساسية اللي وراء **الشبكة العصبية** المتكررة اللي أخذناها بهالوحدة مهم جدًا إذا بغيتوا تبنون نماذج عصبية متقدمة.
حذفنا المخرجات وعدّادات التشغيل والودجات والمحتوى النشط وقت الاستيراد. شغّل الدفاتر بس في بيئة خارجية تثق فيها.
سجّل تطبيقك
التسجيل اختياري، يفيدك تتذكر وش طبّقت، ولا يمنع إكمال الدورة.