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

الوحدة 5 — التقليم وتقليل الحجم

التكميم يُبدِّل نوع الأوزان (float32int8)، لكنّه يُبقيها كلّها. التقليم (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)، لكنّه لا يهمّ للاستدلال (النموذج يُفكَّك في الذاكرة قبل التشغيل).

دمج التقليم مع التكميم

القوّة الحقيقيّة تظهر عند دمج التقليم مع التكميم int8:

# النموذج المُقلَّم + تكميم int8 كامل مع جَمْع تمثيليّ
converter = tf.lite.TFLiteConverter.from_keras_model(final_model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset # كما في الوحدة 3
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_pruned_int8 = converter.convert()

open("pruned_int8.tflite", "wb").write(tflite_pruned_int8)
with open("pruned_int8.tflite", "rb") as f_in:
with gzip.open("pruned_int8.tflite.gz", "wb", compresslevel=9) as f_out:
f_out.writelines(f_in)

print(f"int8 خام : {os.path.getsize('pruned_int8.tflite') / 1024 / 1024:.2f} Mo")
print(f"int8 مضغوط : {os.path.getsize('pruned_int8.tflite.gz') / 1024 / 1024:.2f} Mo")

النتيجة على نموذجنا:

النموذجخاممضغوطالدقّة
float3214.0 Mo13.0 Mo0.945
int83.5 Mo3.4 Mo0.936
int8 + تقليم 50 %3.5 Mo2.1 Mo0.933
int8 + تقليم 75 %3.5 Mo1.4 Mo0.918

التقليم بنسبة 50 % يوفّر مِيغا واحدًا على التحميل بلا خسارة تُذكر في الدقّة. التقليم بنسبة 75 % يوفّر مِيغين، لكنّه يبدأ يضرّ الدقّة بجدّيّة.

تبعثر بلا مكسب زمن على العتاد المدنيّ

التبعثر يخفض الحجم بعد الضغط، لكنّه لا يخفض الزمن على CPU العاديّ للهواتف. لتسريع الاستدلال بالتبعثر، يجب عتاد يعرف حسابات على مصفوفات نادرة (بعض NPU الحديثة، Apple Neural Engine). عند غياب هذا العتاد، الفائدة من التقليم كلّها في الحجم المُنزَّل.

تجميع الأوزان: بديل تكميليّ

تجميع الأوزان (weight clustering) استراتيجيّة مقاربة: نجمع أوزان كلّ طبقة إلى عدد قليل من القيم المميَّزة (مثلًا 16 قيمة)، فيصير التنسور قابلًا للضغط بطريقة أكثر فعّاليّة:

cluster_weights = tfmot.clustering.keras.cluster_weights
CentroidInitialization = tfmot.clustering.keras.CentroidInitialization

clustered_model = cluster_weights(
base_model,
number_of_clusters=16,
cluster_centroids_init=CentroidInitialization.KMEANS_PLUS_PLUS,
)

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

ما نختار للخيط الأحمر

لمصنِّف أوراق النباتات، الاختيار المُوصى به:

  • PTQ int8 كامل (الوحدة 3) → 3.5 مِيغا، 0.936
  • إن أردنا خفض التحميل: + تقليم 50 % → 2.1 مِيغا مضغوط، 0.933

هذا يبقى في حدود الميزانيّة (8 مِيغا) بهامش واسع، ويترك مجالًا لبيانات وصفيّة، ولملفّ لغة، ولموارد أخرى.

الخلاصة

  • التقليم يُصفِّر لا يحذف: أوزان قيمتها المطلقة صغيرة تُستبدل بأصفار، والباقي يتعلّم تعويض الفقد. لا بدّ من جدولة تدريجيّة وإعادة تدريب.
  • الملفّ الخام لا يتقلَّص: الأصفار تشغل مكانها في .tflite. الضغط بـgzip هو ما يترجم التبعثر إلى مكسب حقيقيّ على القرص.
  • التقليم يخفض التحميل لا الزمن على العتاد المدنيّ. تسريع الاستدلال بالتبعثر يحتاج NPU يعرف الحسابات على المصفوفات النادرة.
  • دمج التقليم والتكميم: الاستراتيجيّتان لا تتنافيان. int8 + تقليم 50 % يعطي 2.1 مِيغا مضغوط بدقّة تكاد لا تتغيَّر. هذا التركيب هو المُوصى به لنشر واسع.

الوحدة التالية تنتقل من الملفّ إلى ما يُشغِّله: المفسّر والمفوَّضون العتاديّون، وكيف نختار بين CPU وGPU وNNAPI وCore ML وXNNPACK.