الوحدة 5 — التقليم وتقليل الحجم
التكميم يُبدِّل نوع الأوزان (float32 → int8)، لكنّه يُبقيها كلّها. التقليم (pruning) يعمل بمنطق مختلف: يُصفِّر جزءًا من الأوزان، ويترك الباقي كما هو. الفكرة قديمة (LeCun في التسعينيّات) وبسيطة: كثير من الأوزان في شبكة عصبيّة قيمها قريبة من الصفر، وحذفها لا يُغيِّر المخرَج تقريبًا. ما يميّز TFLite هو أنّه يعرف كيف يستفيد من هذا التبعثر لضغط الملفّ.
التقليم بالمقدار: أبسط استراتيجيّة
الاستراتيجيّة الأكثر شيوعًا هي التقليم بالمقدار (magnitude pruning). في نهاية كلّ خطوة تدريب، نأخذ كلّ طبقة، نفرز أوزانها بالقيمة المطلقة، ونُصفِّر أصغرها حتّى نصل إلى نسبة التبعثر المستهدفة (مثلًا 50 %). الشبكة تُدرَّب عدّة عصور مع هذه الآليّة، فتتعلّم الأوزان الباقية أن تُعوِّض الأوزان المحذوفة.
النقطة المهمّة: التقليم يجب أن يجري أثناء التدريب، لا بعده. إن قلّمت نموذجًا مدرَّبًا دفعة واحدة، ستفقد نقاطًا كثيرة، وحتّى إعادة تدريب لا تسترجعها. الحلّ هو الجدولة التدريجيّة: تبدأ بتبعثر 0 % ثمّ ترتفع خطوة خطوة إلى الهدف، والتدريب يتكيّف مع الحذف الذي يجري.
التطبيق: TensorFlow Model Optimization
نفس الحزمة المستعمَلة في الوحدة 4 تقدِّم أدوات التقليم:
import tensorflow as tf
import tensorflow_model_optimization as tfmot
base_model = tf.keras.models.load_model("plantvillage_mobilenetv2.keras")
# 1) جدولة التبعثر : من 0 % إلى 50 % على 5 عصور
pruning_schedule = tfmot.sparsity.keras.PolynomialDecay(
initial_sparsity=0.0,
final_sparsity=0.5,
begin_step=0,
end_step=1000, # يجب أن يوافق عدد الخطوات في 5 عصور
power=3, # قوّة التقعّر : 3 تعني بطء البداية وسرعة النهاية
)
# 2) تغليف النموذج بعوامل التقليم
pruned_model = tfmot.sparsity.keras.prune_low_magnitude(
base_model,
pruning_schedule=pruning_schedule,
)
pruned_model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),
loss="sparse_categorical_crossentropy",
metrics=["accuracy"],
)
قوّة التقعّر power=3 تعني أنّ التبعثر يبقى قريبًا من الصفر في البداية، ثمّ يتسارع. هذا يسمح للنموذج بالتكيّف تدريجيًّا، لا صدمة واحدة.
الاستدعاءات اللازمة أثناء fit
التقليم يحتاج استدعاء خاصًّا يُحدِّث نسبة التبعثر عند كلّ خطوة:
train_ds = tf.keras.utils.image_dataset_from_directory(
"plantvillage/train", image_size=(224, 224), batch_size=32,
).map(lambda x, y: ((tf.cast(x, tf.float32) / 127.5) - 1.0, y))
val_ds = tf.keras.utils.image_dataset_from_directory(
"plantvillage/val", image_size=(224, 224), batch_size=32,
).map(lambda x, y: ((tf.cast(x, tf.float32) / 127.5) - 1.0, y))
callbacks = [
tfmot.sparsity.keras.UpdatePruningStep(), # إلزاميّ
tfmot.sparsity.keras.PruningSummaries(log_dir="./logs/pruning"),
]
pruned_model.fit(
train_ds,
epochs=5,
validation_data=val_ds,
callbacks=callbacks,
)
بعد التدريب، يجب إزالة عوامل التقليم لتحصل على نموذج نظيف يمكن تحويله إلى TFLite:
final_model = tfmot.sparsity.keras.strip_pruning(pruned_model)
final_model.save("plantvillage_pruned.keras")
المفاجأة: الملفّ لم يتقلَّص
نحوِّل النموذج إلى .tflite ونتفاجأ:
converter = tf.lite.TFLiteConverter.from_keras_model(final_model)
tflite_pruned = converter.convert()
print(f"الحجم بعد التقليم : {len(tflite_pruned) / 1024 / 1024:.2f} Mo")
# النتيجة : 14.0 Mo، أي نفس الحجم قبل التقليم !
لماذا؟ لأنّ الأوزان المُصفَّرة تُخزَّن كأصفار float32، وليست محذوفة. حجم الملفّ لا يتغيّر. التبعثر بحدّ ذاته لا يقلِّل حجم القرص. ما يقلِّله هو الضغط: صيغة التخزين التي تعرف كيف تُمثِّل الأصفار بشكل مختصر.
الضغط: هنا يظهر المكسب
نضغط الملفّ بـgzip، الذي يتعرَّف على تكرارات الأصفار ويُختصِرها:
import gzip
# تحويل خام إلى tflite ثمّ ضغط
open("pruned.tflite", "wb").write(tflite_pruned)
with open("pruned.tflite", "rb") as f_in:
with gzip.open("pruned.tflite.gz", "wb", compresslevel=9) as f_out:
f_out.writelines(f_in)
import os
raw = os.path.getsize("pruned.tflite")
gz = os.path.getsize("pruned.tflite.gz")
print(f"خام : {raw / 1024 / 1024:.2f} Mo | مضغوط : {gz / 1024 / 1024:.2f} Mo")
على نموذج مُقلَّم بنسبة 50 %: الخام 14 مِيغا، المضغوط 7 مِيغا. الضغط قسّم الحجم إلى النصف. هذا يهمّ لتحميل التطبيق (الحجم المُنزَّل من متجر Google Play أو App Store)، لكنّه لا يهمّ للاستدلال (النموذج يُفكَّك في الذاكرة قبل التشغيل).