انتقل إلى المحتوى الرئيسي

الوحدة 5 — tf.data: القراءة والتحويل والتحميل المسبق

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

المشكلة: تجويع المسرّع

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

يُنشئ tf.data خطًّا تتراكب فيه القراءة والحساب. فيصير الزمن الإجمالي أكبرَهما لا مجموعَهما. وعلى تدريبٍ تستغرق فيه تهيئة البيانات 40% من الوقت، يكون الربح فوريًّا ومعتبرًا.

تركيب خطّ بيانات

import tensorflow as tf

AUTO = tf.data.AUTOTUNE

jeu = (
tf.data.Dataset.from_tensor_slices((chemins, etiquettes))
.shuffle(10_000)
.map(charger_et_redimensionner, num_parallel_calls=AUTO)
.batch(32)
.prefetch(AUTO)
)

modele.fit(jeu, epochs=20)

لكلّ حلقة دورٌ محدَّد:

العمليةالدورملاحظة
from_tensor_slicesتُقطّع إلى عناصراحفظ المسارات لا الصور
shuffle(n)يخلط في متّسع من nمتّسع صغير جدًّا لا يخلط شيئًا
map(f)يُطبّق تحويلًاوازِ باستعمال num_parallel_calls
batch(k)يجمع في حزم من kبعد الخلط، لا قبله أبدًا
prefetchيُهيّئ الحزمة التاليةدائمًا في المقام الأخير

يُتيح AUTOTUNE لـTensorFlow اختيار درجة التوازي وعمق المتّسع بالقياس في وقت التنفيذ. وهو خيار أفضل من قيمة ثابتة يدويًّا في أغلب الحالات، ويتكيّف مع الآلة.

ترتيب العمليات يُغيّر النتيجة

المسألة ليست ذوقيّة: فعدّة ترتيبات تُنتج نتائج خاطئة أو بطيئة.

shuffle قبل batch. فالخلط بعد التجميع لا يُعيد ترتيب سوى الحزم، ويبقى محتواها ثابتًا من حقبة إلى أخرى. فيرى النموذج الجواراتِ نفسها دائمًا، ويختفي الأثر التنظيمي للخلط.

batch قبل map حين يكون التحويل قابلًا للتشعيع. فتطبيق تطبيعٍ عنصرًا عنصرًا يُكلّف استدعاء دالة لكلّ مثال. أمّا على حزمة فهو استدعاء واحد على تنسور. ولعملية حسابية بحتة يُسرّع عكس الترتيب الأداء إسراعًا واضحًا. أمّا فكّ ترميز صورة فينبغي أن يبقى عنصرًا عنصرًا، إذ يكون كلّ ملفّ متميّزًا.

prefetch في الأخير، دائمًا. فإن وُضع في الوسط لم يُغطّ سوى نهاية الخطّ، وترك المسرّع منتظرًا للخطوات السابقة.

متّسع الخلط ليس مجموعة البيانات

تملأ shuffle(1000) متّسعًا من ألف عنصر وتسحب منه. فعلى مجموعة من مئة ألف مثال مرتّبة حسب الصنف، لا يحتوي هذا المتّسع إلّا على صنفٍ واحد في كلّ لحظة: فالخلط وهميّ وكلّ حزمة أُحاديّة الصنف. ويتباعد النموذج بلا سببٍ ظاهر. اجعل المتّسع في حجم مجموعة البيانات، أو اخلط مسارات الملفّات في المنبع بـshuffle على قائمة بايثون، وذلك لا يُكلّف شيئًا في الذاكرة.

أين نضع الذاكرة الوسيطة

يحفظ cache نتيجة الخطوات السابقة له، وموضعه يُحدّد ما يُعاد استعماله.

jeu = (
tf.data.Dataset.from_tensor_slices((chemins, etiquettes))
.map(decoder_image, num_parallel_calls=AUTO)
.cache() # بعد فكّ الترميز، وقبل التعزيز
.shuffle(10_000)
.map(augmenter, num_parallel_calls=AUTO)
.batch(32)
.prefetch(AUTO)
)

فكّ الترميز يُعطي دائمًا النتيجة نفسها: فوضعه في الذاكرة الوسيطة يُوفّر عملًا مطابقًا في كلّ حقبة. أمّا التعزيز فيجب أن يُنتج تنوّعًا جديدًا في كلّ مرور؛ ووضعه قبل الذاكرة الوسيطة يُثبّت نسخة معزَّزة واحدة لكلّ صورة ويُلغي فائدته كلّها.

ولا تصلح الذاكرة الوسيطة داخل الذاكرة إلّا إن اتّسعت المجموعة المُفكّكة الترميز فيها. وإلّا فإنّ cache("/chemin/fichier") يكتب على القرص، وهو أسرع بكثير من فكّ ترميز مُتكرّر.

القراءة من الملفّات

لصور على القرص، يُغطّي image_dataset_from_directory الحالة الشائعة في سطر واحد، إذ يستنتج الأصناف من أسماء المجلّدات:

jeu = keras.utils.image_dataset_from_directory(
"donnees/entrainement",
image_size=(224, 224),
batch_size=32,
label_mode="int",
)

ولحجم كبير، تصير صيغة TFRecord مُفضّلة. فهي تجمع الأمثلة في ملفّات كبيرة قليلة متتابعة، ممّا يُلغي كُلفة فتح ملايين الملفّات الصغيرة — وهو غالبًا العامل المُهيمن على تخزين شبكيّ.

jeu = (
tf.data.TFRecordDataset(fichiers, num_parallel_reads=AUTO)
.map(analyser_exemple, num_parallel_calls=AUTO)
.batch(64)
.prefetch(AUTO)
)

مصيدة دوالّ بايثون

الدالّة الممرَّرة إلى map تُتَتبَّع، كما في الوحدة الأولى. فيجب أن تُعبَّر بعمليات TensorFlow. ولا مكان لمكتبة بايثون اعتباطية.

# لا يعمل: PIL ليست عملية TensorFlow
def charger(chemin):
return numpy.array(PIL.Image.open(chemin.numpy()))

هناك مخرجان. الحسن يستعمل العمليات الأصلية: tf.io.read_file ثمّ tf.image.decode_jpeg. أمّا الآخر فيُغلّف كود بايثون في tf.py_function، وهذا يعمل لكنّه يُسلسل التنفيذ على القفل الشامل للمفسّر: فيختفي التوازي، ومعه معظم الفائدة.

قِس قبل أن تُحسّن

قارن زمن حقبة بخطّك الكامل، ثمّ زمنَها بـjeu.take(1).repeat() الذي يُعيد تشغيل حزمةٍ في الذاكرة. فإن كان الثاني أسرع بوضوح، فالبيانات هي الاختناق وتنطبق هذه الوحدة. وإن تقاربت المدّتان، فالحساب هو المُهيمن ويجب البحث في مكانٍ آخر — حجم النموذج، أو الدقّة المختلطة، أو التوزيع في الوحدة 9.

في الخلاصة

  • بغير خطّ بيانات يكون الزمن الإجمالي مجموع التهيئة والحساب؛ ومع prefetch يصير أكبرَهما.
  • الترتيب مُلزِم: shuffle قبل batch، وbatch قبل map للتحويلات القابلة للتشعيع، وprefetch دائمًا في الأخير.
  • متّسع الخلط يجب أن يكون قريبًا من حجم المجموعة، وإلّا أنتجت مجموعة مرتّبة حزمًا أُحاديّة الصنف وتدريبًا يتباعد بلا سببٍ ظاهر.
  • يُوضع cache بعد التحويلات الحتميّة وقبل التعزيز، وإلّا انجمد التعزيز على نسخةٍ واحدة لكلّ مثال.

الوحدة التالية: الاستدعاءات، التي تُتيح التصرّف أثناء التدريب دون إعادة كتابته.