معاينة مختبر آمنة
Generative TF
هذي معاينة منقّحة للقراءة فقط؛ ما فيه أي شيء يشتغل داخل الصفحة.
قراءة فقط
معاينة الدفتر
Generative TF
> **ملاحظة بيئة التشغيل المدمجة:** هالمعاينة تستخدم عيّنة صغيرة وثابتة وآمنة من ناحية الحقوق عشان تكون النتايج قابلة للتكرار. النتايج بالحجم الكامل تحتاج مجموعة البيانات أو النموذج الموثّق بالدرس داخل بيئة خارجية معتمدة.
# الشبكات التوليدية
وفّرت الشبكات العصبية المتكررة (RNNs)، ومعها الخلايا ذات البوابات مثل خلايا الذاكرة الطويلة قصيرة المدى (LSTMs) والوحدات المتكررة ذات البوابات (GRUs)، طريقةً لنمذجة اللغة؛ يعني إنها تتعلّم ترتيب الكلمات وتتنبأ بالكلمة اللي بعدها في التسلسل. وهالشي يخلّينا نستخدم RNNs في **المهام التوليدية**، مثل توليد النصوص، والترجمة الآلية، وحتى كتابة وصف للصور.
في بنية RNN اللي أخذناها بالوحدة السابقة، كل وحدة RNN تطلع الحالة المخفية التالية. ونقدر بعد نضيف مخرجًا ثانيًا لكل وحدة متكررة، وبكذا تطلع لنا **تسلسلًا** طوله يساوي طول التسلسل الأصلي. ونقدر نستخدم وحدات RNN ما تستقبل مدخلًا في كل خطوة؛ تأخذ متجه حالة ابتدائي، وبعدها تنتج تسلسلًا من المخرجات.
بهالدفتر بنركّز على نماذج توليدية بسيطة تساعدنا نولّد نصوصًا. وللتبسيط، بنبني **شبكة على مستوى المحارف** تولّد النص محرفًا محرفًا. وقت التدريب نأخذ متنًا نصيًا ونقسّمه إلى تسلسلات من المحارف.
import tensorflow as tf
from tensorflow import keras
import numpy as np
# course-edition deterministic TensorFlow news fixture v1
_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)## بناء مفردات الحروف
تدعم طبقة `TextVectorization` الحالية التقسيم على مستوى الحرف مباشرةً باستخدام `split="character"`. نخلي المعالجة بدون تحويل حالة الأحرف، ونتعلم المفردات من عناوين مجموعة التدريب.
titles = ds_train.map(lambda item: item["title"])
vectorizer = keras.layers.TextVectorization(
standardize=None,
split="character",
output_mode="int",
ragged=True,
)
vectorizer.adapt(titles.batch(256))ونبي رمزًا خاصًا يدل على **نهاية التسلسل**، وبنسمّيه `<eos>`. خلونا نضيفه يدويًا إلى المفردات:
vocabulary = vectorizer.get_vocabulary()
eos_token = len(vocabulary)
vocab_size = eos_token + 1
reverse_map = {index: token for index, token in enumerate(vocabulary)}
reverse_map[eos_token] = "<eos>"
def encode(text):
return vectorizer(tf.constant([text])).values.numpy().tolist()والحين نقدر نرمّز النص إلى تسلسلات من الأرقام كذا:
encode("Hello, world!")## تدريب RNN توليدية على كتابة العناوين
بندرّب RNN على توليد عناوين الأخبار بهالطريقة: في كل خطوة نمرر عنوانًا واحدًا إلى RNN، ونطلب من الشبكة تولّد المحرف التالي لكل محرف دخل لها:
> **وصف الشكل:** صورة تبيّن مثالًا على توليد RNN لكلمة "HELLO".
وعند آخر محرف في التسلسل، نطلب من الشبكة تولّد الرمز `<eos>`.
الفرق الأساسي في RNN التوليدية هنا إننا نأخذ مخرج RNN من كل خطوة، مب بس من الخلية الأخيرة. نسوي هالشي بتحديد المعامل `return_sequences` في خلية RNN.
أثناء التدريب يكون مدخل الشبكة تسلسلًا من المحارف المرمّزة بطول معيّن، ويكون المخرج تسلسلًا بالطول نفسه لكنه مزاح عنصرًا واحدًا وينتهي بالرمز `<eos>`. تضم الدفعة المصغّرة عدة تسلسلات من هالنوع، ونستخدم **الحشو** عشان نوحّد أطوالها.
خلونا نكتب دوال تحوّل مجموعة البيانات. بما إننا نبي نحشو التسلسلات على مستوى الدفعة المصغّرة، نجمع مجموعة البيانات أولًا باستدعاء `.batch`، ثم نستخدم `map` للتحويل. يعني دالة التحويل تستقبل دفعة مصغّرة كاملة:
def title_batch_fn(batch):
encoded = vectorizer(batch["title"]).to_tensor(default_value=0)
eos_column = tf.fill([tf.shape(encoded)[0], 1], tf.cast(eos_token, encoded.dtype))
targets = tf.concat([encoded[:, 1:], eos_column], axis=1)
return tf.one_hot(encoded, vocab_size), tf.one_hot(targets, vocab_size)تعالج الدالة الدفعة داخل مسار `tf.data`: تحوّل العناوين لأرقام، وتضيف الحشو، وتزيح الهدف رمزًا واحدًا، ثم تضيف رمز نهاية التسلسل.
training_ds = (
ds_train.shuffle(10_000, seed=2026)
.batch(8)
.map(title_batch_fn, num_parallel_calls=tf.data.AUTOTUNE)
.prefetch(tf.data.AUTOTUNE)
)> **ملاحظة:** ممكن يبان الفرق بين دوال التحويل في Python ودوال TensorFlow معقدًا شوي، ويمكن تسألون: ليه ما نحوّل مجموعة البيانات بدوال Python العادية قبل تمريرها إلى `fit`؟ نقدر نسوي كذا، لكن `Dataset.map` له ميزة كبيرة: ينفّذ مسار تحويل البيانات داخل الرسم البياني الحاسوبي لـ TensorFlow، فيستفيد من حسابات GPU ويقلّل نقل البيانات بين CPU وGPU.
الحين نقدر نبني شبكة التوليد ونبدأ التدريب. نقدر نعتمد على أي خلية متكررة أخذناها بالوحدة السابقة: بسيطة، أو LSTM، أو GRU. في مثالنا بنستخدم LSTM.
بما إن الشبكة تستقبل محارف وحجم المفردات صغير، ما نحتاج طبقة تضمين؛ نقدر نمرر مدخلات بترميز one-hot مباشرة إلى خلية LSTM. وطبقة المخرج هي مصنّف `Dense` يحوّل مخرج LSTM إلى أرقام رموز بترميز one-hot.
وبما إن أطوال التسلسلات تختلف، نقدر نستخدم طبقة `Masking` عشان تنشئ قناعًا يتجاهل الجزء المحشو من السلسلة. هالطبقة مب ضرورية تمامًا؛ لأن اللي بعد رمز `<eos>` ما يهمنا كثير، لكن بنستخدمها عشان نجرّب هالنوع من الطبقات. شكل `input_shape` هو `(None, vocab_size)`، و`None` هنا يعني إن طول التسلسل متغيّر. وشكل المخرج بعد هو `(None,vocab_size)`، مثل ما يبيّن `summary`:
model = keras.models.Sequential([
keras.Input(shape=(None, vocab_size)),
keras.layers.Masking(mask_value=0.0),
keras.layers.LSTM(128, return_sequences=True),
keras.layers.Dense(vocab_size, activation="softmax"),
])
model.summary()
model.compile(optimizer=keras.optimizers.Adam(), loss="categorical_crossentropy")
model.fit(training_ds, epochs=1)## توليد المخرج
بعد ما درّبنا النموذج، نبي نستخدمه لتوليد مخرج. أول شي نحتاج طريقة تفك ترميز النص الممثّل بتسلسل أرقام الرموز. نقدر نستخدم `tokenizer.sequences_to_texts`، لكنها ما تضبط زين مع التجزئة على مستوى المحارف. لذلك نأخذ قاموس الرموز من tokenizer واسمه `word_index`، ونبني خريطة عكسية، ونكتب دالة فك ترميز خاصة فينا:
def decode(token_ids):
return "".join(reverse_map.get(int(token_id), "�") for token_id in token_ids if int(token_id) != 0)الحين نبدأ التوليد. نبدأ بالسلسلة `start` ونرمّزها إلى التسلسل `inp`، وبعدها نستدعي شبكتنا في كل خطوة عشان تستدل على المحرف التالي.
مخرج الشبكة `out` متجه فيه `vocab_size` عنصرًا تمثّل احتمالات الرموز. نستخدم `argmax` عشان نجيب رقم الرمز الأعلى احتمالًا، ثم نضيف محرفه إلى قائمة الرموز الناتجة ونكمل. نكرر هالعملية `size` مرة عشان نولّد عدد المحارف المطلوب، ونتوقف بدري إذا وصلنا إلى `eos_token`.
def generate(model, size=100, start="Today "):
tokens = encode(start)
generated = list(tokens)
for _ in range(size):
inputs = tf.one_hot(tokens, vocab_size)[None, ...]
probabilities = model(inputs, training=False)[0, -1]
next_token = int(tf.argmax(probabilities))
if next_token == eos_token:
break
generated.append(next_token)
tokens.append(next_token)
return decode(generated)
generate(model)## أخذ عيّنات من المخرج أثناء التدريب
ما عندنا مقياس مفيد مثل *الدقة*، لذلك أفضل طريقة نشوف فيها هل النموذج يتحسّن هي **أخذ عيّنات** من النص الناتج أثناء التدريب. بنستخدم **دوال الاستدعاء**؛ نمررها إلى الدالة `fit`، وهي تستدعيها دوريًا وقت التدريب.
sampling_callback = keras.callbacks.LambdaCallback(
on_epoch_end=lambda _epoch, _logs: print(generate(model))
)
model.fit(training_ds, callbacks=[sampling_callback], epochs=3)هالمثال يولّد نصًا زينًا إلى حد معقول، ونقدر نحسّنه بعدة طرق:
* **نص أكثر**. استخدمنا العناوين بس، لكن تقدرون تجرّبون النص كاملًا. تذكّروا إن RNNs ما تتعامل زين مرة مع التسلسلات الطويلة؛ لذلك يا تقسّمونها إلى جمل أقصر، أو تدرّبون دائمًا على طول ثابت تحددونه في `num_chars`، مثل 256. جرّبوا تعدّلون المثال لهالبنية بالاستفادة من [درس Keras الرسمي](https://keras.io/examples/generative/lstm_character_level_text_generation/).
* **LSTM متعدد الطبقات**. جرّبوا 2 أو 3 طبقات من خلايا LSTM. مثل ما ذكرنا بالوحدة السابقة، كل طبقة من LSTM تستخرج أنماطًا معيّنة من النص. وفي مولّد المحارف نتوقع إن طبقة LSTM السفلى تستخرج المقاطع، والطبقات الأعلى تستخرج الكلمات وتراكيبها. يكفي تمرير معامل عدد الطبقات إلى مُنشئ LSTM.
* وتقدرون تجرّبون **وحدات GRU** وتقارنون أداءها، وتجرّبون **أحجامًا مختلفة للطبقة المخفية**. الطبقة الكبيرة مرة ممكن تسبب فرط التكيّف، فتتعلم الشبكة النص نفسه، والحجم الصغير ممكن ما يعطي نتيجة زينة.
## التوليد المرن للنص ودرجة الحرارة
في تعريف `generate` السابق، كنا نأخذ دائمًا المحرف صاحب أعلى احتمال على أنه المحرف التالي. والنتيجة إن النص غالبًا يدخل في حلقة ويكرر تسلسلات المحارف نفسها، مثل هالمثال:
```
today of the second the company and a second the company ...
```
لكن إذا طالعنا توزيع احتمالات المحرف التالي، ممكن يكون الفرق بين أعلى الاحتمالات بسيطًا؛ مثلًا احتمال محرف يساوي 0.2 واحتمال الثاني 0.19. وبعد التسلسل '*play*'، ممكن تكون المسافة و**e** (مثل كلمة *player*) خيارين متقاربين.
يعني مو دايم من المناسب نختار صاحب أعلى احتمال؛ لأن ثاني أعلى احتمال ممكن بعد يعطينا نصًا له معنى. الأفضل إننا **نأخذ عيّنات** من المحارف بحسب توزيع الاحتمالات اللي طلعته الشبكة.
نسوي هالعيّنة بدالة `np.multinomial`، وهي تطبّق **التوزيع متعدد الحدود**. وتحت بنعرّف دالة تنفّذ هالتوليد **المرن** للنص:
def generate_soft(model, size=100, start="Today ", temperature=1.0):
tokens = encode(start)
generated = list(tokens)
for _ in range(size):
inputs = tf.one_hot(tokens, vocab_size)[None, ...]
logits = tf.math.log(tf.clip_by_value(model(inputs, training=False)[0, -1], 1e-8, 1.0))
next_token = int(tf.random.categorical(logits[None, :] / temperature, 1, seed=2026)[0, 0])
if next_token == eos_token:
break
generated.append(next_token)
tokens.append(next_token)
return decode(generated)
prompts = ["Today ", "On Sunday ", "Riyadh, ", "President ", "Once upon a time "]
for temperature in [0.3, 0.8, 1.0, 1.3]:
print(f"\n--- Temperature = {temperature}")
for prompt in prompts:
print(generate_soft(model, size=160, start=prompt, temperature=temperature))أضفنا معاملًا ثانيًا اسمه **درجة الحرارة** يحدد مدى تمسّكنا بأعلى احتمال. إذا كانت قيمته 1.0، نأخذ عيّنة عادلة من التوزيع متعدد الحدود. وكل ما اتجهت القيمة إلى اللانهاية تقاربت الاحتمالات، وصار اختيار المحرف التالي عشوائيًا. بالمثال تحت، نلاحظ إن النص يفقد معناه إذا رفعنا الحرارة كثير، ويصير قريبًا من النص المتكرر الناتج بالطريقة الحاسمة إذا قرّبناها من 0.
حذفنا المخرجات وعدّادات التشغيل والودجات والمحتوى النشط وقت الاستيراد. شغّل الدفاتر بس في بيئة خارجية تثق فيها.
سجّل تطبيقك
التسجيل اختياري، يفيدك تتذكر وش طبّقت، ولا يمنع إكمال الدورة.