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

الوحدة 2 — التحويل إلى TensorFlow Lite

الميزانيّة معلَنة. الآن نحوِّل نموذجنا: مصنِّف أوراق النباتات الذي دُرِّب بـMobileNetV2 مُعاد ضبطه على PlantVillage. ما نتلقّاه من الوحدة 08 (TensorFlow وKeras) هو مجلَّد SavedModel أو ملفّ .keras؛ ما ينتظره الهاتف هو ملفّ ثنائيّ .tflite واحد. الفارق ليس بسيطًا كتغيير امتداد: صيغة .tflite مختلفة تمامًا (FlatBuffers لا Protobuf)، والقائمة العلنيّة للعوامل ليست هي نفسها، والبيانات الوصفيّة تعيش داخل الملفّ نفسه لا في مجلَّد بجانبه.

ما الذي يحدث فعلًا أثناء التحويل

TFLiteConverter يقرأ رسم الحساب من نموذج Keras أو من SavedModel، ثمّ يقوم بثلاث خطوات متتالية:

أوّلًا، يُحوِّل عوامل TensorFlow إلى عوامل TFLite. ليست كلّ عمليّة موجودة في TFLite. القائمة الرسميّة تحوي نحو مئة وثلاثين عاملًا فقط، مقابل ألف وخمسمئة في TensorFlow الكامل. عمليّات نادرة (مثل tf.io.decode_jpeg داخل الرسم، أو أعداد عشوائيّة داخل طبقة مخصَّصة) تُرفَض.

ثانيًا، يُحسِّن الرسم. يدمج الالتفافات مع التفعيلات (Conv + BN + ReLU → عقدة واحدة)، ويُسقط الفروع الميتة، ويُوحِّد أنماط ثابتة. النتيجة رسم أصغر وأسرع.

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

أوّل تحويل: النموذج الخام

نبدأ من نموذج Keras المدرَّب. لتبسيط التنفيذ، نستعمل هنا MobileNetV2 مُعاد ضبطه سابقًا وحفظه بصيغة .keras:

import tensorflow as tf

# 1) تحميل النموذج من قرص المكتب. جاء من الدورة 08.
model = tf.keras.models.load_model("plantvillage_mobilenetv2.keras")

# 2) تجهيز المحوّل من Keras. مسار مباشر يعمل في التسعين بالمئة من الحالات.
converter = tf.lite.TFLiteConverter.from_keras_model(model)

# 3) تحويل خام، بلا أيّ تحسين. يعطي ملفًّا كبيرًا لكنّه مطابق للنموذج الأصليّ.
tflite_model = converter.convert()

with open("plantvillage_raw.tflite", "wb") as f:
f.write(tflite_model)

print(f"حجم النموذج .tflite : {len(tflite_model) / 1024 / 1024:.2f} Mo")

نموذج MobileNetV2 كامل (float32) يُنتج عادة ملفًّا بحجم 14 مِيغا. رقم أعلى بكثير من ميزانيّتنا (8 مِيغا)، وسنحلّه بالتكميم في الوحدة 3.

المسار البديل: من SavedModel لا من Keras

يوجد مسار ثانٍ أكثر عموميّة، مفيد حين لا نمتلك كائن Keras، بل مجرَّد مجلَّد SavedModel (نتيجة model.export("plantvillage_saved") مثلًا، أو نموذج يأتي من فريق آخر):

# البديل : من مجلَّد SavedModel. يعمل مع أيّ نموذج TensorFlow، ليس Keras فقط.
converter = tf.lite.TFLiteConverter.from_saved_model("plantvillage_saved/")
tflite_model = converter.convert()

المسار من SavedModel يدعم أنواعًا أوسع من النماذج، ويحفظ التوقيعات المتعدّدة إن وُجدت. أمّا المسار من Keras فأبسط ويكفي حين يكون النموذج مبنيًّا بواجهة keras.

العوامل غير المدعومة والعوامل المُحدَّدة

إن استعمل نموذجك عاملًا نادرًا، سترى رسالة من نوع:

Some ops are not supported by the native TFLite runtime, you can enable
TF Select ops... : RandomStandardNormal, Bincount, ...

هنا خياران:

الخيار المفضَّل: أعِد كتابة الجزء المسيء بعوامل مدعومة. tf.image.resize مثلًا مدعومة، فلماذا تستعمل tf.py_function تُطلق كود بايثون؟ هذا يعيدك في الغالب إلى مراجعة معماريّة النموذج، وهو ما يجب فعله باكرًا.

الخيار الاحتياطيّ: تفعيل « العوامل المُحدَّدة » (TF Select) التي تُدمج جزءًا من TensorFlow الكامل داخل مكتبة TFLite. ثمن هذا: حجم المكتبة على الهاتف يقفز من مِيغا واحد إلى ما بين 10 و15 مِيغا، وبعض المفوَّضين (GPU خصوصًا) يتوقّفون عن العمل على العوامل المُحدَّدة. لا تستعمل هذا الحلّ إلاّ حين يفشل الحلّ الأوّل.

converter = tf.lite.TFLiteConverter.from_keras_model(model)
# تفعيل العوامل المُحدَّدة بحذر : تضخّم المكتبة وتُعطِّل GPU
converter.target_spec.supported_ops = [
tf.lite.OpsSet.TFLITE_BUILTINS,
tf.lite.OpsSet.SELECT_TF_OPS,
]
tflite_model = converter.convert()

التوقيعات: أسماء المدخلات والمخرجات

نموذج Keras لديه اسم افتراضيّ للمدخل («input_1») وللمخرج، وTFLite يحتفظ بهذه الأسماء داخل الملفّ ضمن ما يُسمّى التوقيع (signature). التطبيق على الهاتف سيحتاج هذه الأسماء ليعرف أين يضع بايتات الصورة، وأين يقرأ الاحتمالات.

نتحقّق من التوقيع بعد التحويل مباشرة:

interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()

# التوقيع الافتراضيّ
signatures = interpreter.get_signature_list()
print("التوقيعات المُعلَنة :", signatures)

runner = interpreter.get_signature_runner()
print("المدخلات :", runner.get_input_details())
print("المخرجات :", runner.get_output_details())

إن رأيت أسماء تلقائيّة مثل serving_default_input_1، فهي كافية للاستدلال، لكنّها تُصعّب القراءة. الأنظف أن تُثبّت أسماء واضحة عند التصدير من Keras:

# تحديد أسماء واضحة قبل التحويل يُيسِّر عمل المطوّرين على الهاتف
class LeafClassifier(tf.Module):
def __init__(self, keras_model):
super().__init__()
self.model = keras_model

@tf.function(input_signature=[
tf.TensorSpec(shape=[1, 224, 224, 3], dtype=tf.float32, name="image")
])
def classify(self, image):
return {"probabilities": self.model(image, training=False)}

wrapper = LeafClassifier(model)
tf.saved_model.save(wrapper, "plantvillage_named/", signatures={"classify": wrapper.classify})

converter = tf.lite.TFLiteConverter.from_saved_model("plantvillage_named/")
tflite_model = converter.convert()

هذا التوقيع الصريح («image» → «probabilities») سيسمح لتطبيق Android في الوحدة 7 باستدعاء interpreter.getInputTensor("image") بدل رقم مبهم.

البيانات الوصفيّة: من دون كودك، النموذج لغز

ملفّ .tflite بذاته لا يعرف: ما هي الأصناف الـ38؟ ما التطبيع المستعمَل؟ ما حجم الإدخال؟ من دون هذه المعلومات، مطوِّر يفتح الملفّ ولا يعرف كيف يستعمله. البيانات الوصفيّة (Metadata) تحلّ هذه المسألة بحقن كائنات وصفيّة داخل الملفّ نفسه:

from tflite_support import metadata_writers
from tflite_support.metadata_writers import image_classifier
from tflite_support.metadata_writers import writer_utils

writer = image_classifier.MetadataWriter.create_for_inference(
writer_utils.load_file("plantvillage_raw.tflite"),
input_norm_mean=[127.5],
input_norm_std=[127.5], # تطبيع [-1, 1] كما في MobileNetV2
label_file_paths=["labels.txt"], # ملفّ نصّيّ بسطر لكلّ صنف
)

writer_utils.save_file(writer.populate(), "plantvillage_with_meta.tflite")

بعد ذلك، مكتبة Task Library على Android تكتشف تلقائيًّا حجم الإدخال، والتطبيع، والأصناف. هذه بضع أسطر تُوفِّر ساعات على من سيستعمل النموذج بعدك.

البيانات الوصفيّة قبل التسليم

لا تُسلِّم أبدًا ملفّ .tflite بلا labels.txt مضمَّن. مطوِّر التطبيق سيكتب قائمة الأصناف يدويًّا، سيخطئ في ترتيب صنفَين، سيُدرَّس النموذج « متلَف » وهو سليم. البيانات الوصفيّة عقد بين من يصنع النموذج ومن يستعمله.

اختبار سريع بعد التحويل

قبل الانتقال إلى التكميم، نتحقّق أنّ النموذج المحوَّل يُعطي نتائج مطابقة تقريبًا لنموذج Keras الأصليّ على صورة اختبار:

import numpy as np

# صورة اختبار : تنسور عشوائيّ بحجم الإدخال
sample = np.random.uniform(-1, 1, (1, 224, 224, 3)).astype(np.float32)

# استدلال Keras
keras_out = model.predict(sample, verbose=0)

# استدلال TFLite عبر التوقيع
runner = interpreter.get_signature_runner()
tflite_out = runner(image=sample)["probabilities"]

# الاختلاف يجب أن يكون شبه صفر (اختلافات نقطة عائمة صغرى)
diff = np.abs(keras_out - tflite_out).max()
print(f"أقصى اختلاف بين Keras و TFLite : {diff:.6f}")

إن كان الاختلاف بحدود 1e-5، فالتحويل نظيف. إن كان أكبر، فثمّة عامل تُرجم بتقريب مختلف، ويجب التحقّق قبل الاستمرار.

الخلاصة

  • TFLite ليس امتدادًا لـTensorFlow: صيغة مختلفة (FlatBuffers)، ومكتبة عوامل أصغر بكثير، وأدوات تحسين خاصّة. التحويل ليس تصديرًا بل تحويل هيكل.
  • مساران للتحويل: من Keras (أبسط، أشمل استخدامًا) أو من SavedModel (أكثر عموميّة، يدعم توقيعات متعدّدة). ابدأ بالأوّل واسقط على الثاني عند الحاجة.
  • العوامل غير المدعومة تعالَج بإعادة كتابة النموذج، لا بتفعيل SELECT_TF_OPS الذي يُضخِّم المكتبة ويُعطِّل GPU. اجعل هذا الخيار آخر ملجأ.
  • التوقيعات والبيانات الوصفيّة عقد: أسماء مدخلات ومخرجات واضحة، وطبقة تطبيع مُصرَّح بها، وقائمة أصناف مضمَّنة، هي ما يجعل النموذج قابلًا للاستعمال بلا وثيقة جانبيّة.

الوحدة التالية تأخذ هذا الملفّ الخام بحجم 14 مِيغا وتقلّصه إلى الربع بالتكميم بعد التدريب، مع مقارنة ثلاث استراتيجيّات.