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

الوحدة 3 — التصدير من TensorFlow إلى ONNX

TensorFlow ليس فيه دالّة torch.onnx.export مُدمَجة. الأداة الرسميّة اسمها tf2onnx، وهي حزمة مستقلّة تطوّرها Microsoft. الاستعمال قريب من نظيره في PyTorch، لكنّ ثمّة فارقين حقيقيَّين يجب استيعابهما: ترتيب المحاور والتوقيعات.

تثبيت وسياق سريع

pip install tf2onnx onnx onnxruntime

tf2onnx يقبل ثلاثة مصادر: SavedModel (الصيغة القياسيّة في TensorFlow 2)، وKeras مباشرةً، وdير graph من TensorFlow 1 (نادر اليوم). سنركّز على الأوّلَين، فهما ما تستعمله كلّ المشاريع الحيّة.

من SavedModel: النمط الأمثل

SavedModel هو مجلّد يحتوي على الأوزان والرسم والتوقيعات. تُنتجه Keras بـmodele.save("dossier"). من هذا المجلّد، التصدير سطر واحد في سطر الأوامر:

python -m tf2onnx.convert \
--saved-model resnet18_tf \
--output resnet18.onnx \
--opset 17

أو من داخل بايثون:

import tf2onnx
import tensorflow as tf

modele = tf.keras.models.load_model("resnet18_tf")
spec = (tf.TensorSpec((None, 224, 224, 3), tf.float32, name="image"),)

modele_onnx, _ = tf2onnx.convert.from_keras(
modele, input_signature=spec, opset=17,
output_path="resnet18.onnx",
)

input_signature هو مقابل dynamic_axes عند PyTorch: None في المحور الأوّل يعني حزمة ديناميكيّة. لاحظ أنّه لا حاجة هنا لإدخال مرجعيّ حقيقيّ، فقط وصفٌ للشكل والنوع.

توقيعات SavedModel

SavedModel يحمل واحدًا أو أكثر من التوقيعات، وهي دوالّ Python مُصرَّحة (بـ@tf.function) تُشكّل نقاط الدخول العامّة للنموذج. عند وجود توقيع واحد فقط اسمه serving_default، لا تفعل شيئًا. عند وجود عدّة توقيعات (تدريب، استدلال، تلخيص...)، اختر واحدًا بـ--signature_def:

python -m tf2onnx.convert \
--saved-model modele_multi \
--signature_def serve_inference \
--output modele.onnx --opset 17

نسيان هذا الوسيط يُصدِّر الافتراضيّ، الذي قد لا يكون ما تريد.

Keras مباشرةً

للنماذج التي لم تُحفَظ بعد، يقبل tf2onnx.convert.from_keras نموذجًا في الذاكرة:

import tensorflow as tf
import tf2onnx

modele = tf.keras.Sequential([
tf.keras.layers.Input((224, 224, 3)),
tf.keras.layers.Conv2D(32, 3, activation="relu"),
tf.keras.layers.GlobalAveragePooling2D(),
tf.keras.layers.Dense(10),
])

spec = (tf.TensorSpec((None, 224, 224, 3), tf.float32, name="image"),)
tf2onnx.convert.from_keras(
modele, input_signature=spec, opset=17,
output_path="mini.onnx",
)

هذه الطريقة مثاليّة داخل الدفاتر أو خطوط CI: نُدرِّب، نُصدّر، ننشر، دون ملفّ وسيط.

NHWC مقابل NCHW: الفارق الجوهريّ

TensorFlow يعتمد افتراضيًّا ترتيب NHWC للصور: (حزمة، ارتفاع، عرض، قنوات). PyTorch يعتمد NCHW: (حزمة، قنوات، ارتفاع، عرض). ONNX يقبل الاثنين، لأنّه صيغة عامّة، لكنّ العمليّات مثل Conv تتوقّع افتراضيًّا NCHW في ONNX.

tf2onnx يعالج هذا التحويل تلقائيًّا: يُدرِج عُقد Transpose عند الحاجة لتحويل النسق. النتيجة نموذج ONNX يعمل، لكنّه أثقل ممّا يلزم بسبب هذه العمليّات الإضافيّة. الحلّ: خيار --inputs-as-nchw:

python -m tf2onnx.convert \
--saved-model resnet18_tf \
--output resnet18.onnx \
--opset 17 \
--inputs-as-nchw image

هذا يُخبر tf2onnx أنّ الإدخال سيُقدَّم بترتيب NCHW عند الاستدلال، فيُنشئ الرسم مباشرةً بهذا الترتيب دون عمليّات Transpose زائدة. القاعدة: إذا كان الإدخال من مصدر خارجيّ (كاميرا، خطّ معالجة)، وافق ترتيبه؛ إذا كنت أنت من يُنشئ الإدخال، تبنَّ NCHW لأنّه معيار محرّكات الاستدلال.

التحقّق من التكافؤ

كما في PyTorch، اختبار عدديّ فوريّ بعد التصدير:

import numpy as np
import onnxruntime as ort

image_np = np.random.randn(1, 224, 224, 3).astype(np.float32)
sortie_tf = modele(image_np).numpy()

# لاحظ التحويل إن استعملت --inputs-as-nchw
image_nchw = image_np.transpose(0, 3, 1, 2) # إن كان الملفّ nchw
session = ort.InferenceSession("resnet18.onnx", providers=["CPUExecutionProvider"])
sortie_onnx = session.run(None, {"image": image_nchw})[0]

print("أقصى فارق :", np.abs(sortie_tf - sortie_onnx).max())

الفارق يجب أن يكون من مرتبة 1e-5 تقريبًا. أيّ شيء أكبر يستحقّ فتح Netron والبحث عن عُقد غير متوقّعة (Cast، Transpose) قد تُشير إلى مشكلة تحويل نوع خفيّ.

المزالق الشائعة، بترتيب التواتر

عملية غير مدعومة. tf2onnx لا يدعم كلّ عمليّات TensorFlow. رسالة الخطأ صريحة: Tensorflow op [Xxx] is not supported. الحلّ يكون إمّا تحديث tf2onnx (تحسّن الدعم مع كلّ إصدار)، وإمّا إعادة كتابة الجزء الذي يستخدم العمليّة الفريدة بعمليّات أساسيّة. سنعود إلى هذا في الوحدة التاسعة.

نوع float64 صامت. بعض طبقات Keras تحسب داخليًّا في float64 عندما يكون الإدخال مبعثرًا. tf2onnx يحافظ على هذا النوع، وONNX Runtime على CPU يقبله، لكنّ TensorRT يرفضه. الحلّ: tf.cast(x, tf.float32) صراحةً عند بداية النموذج.

تنسورات الشكل الديناميكيّ. استخدام tf.shape(x) وعمليّات على النتيجة تُنتج عمليّات ONNX ديناميكيّة (Shape، Gather، Reshape) قد تُعقّد تحسينات المحرّك. عند الإمكان، ثبِّت الأبعاد يدويًّا.

نموذج غير مُدرَّب. خطأ سخيف لكنّه شائع في السكربتات: تصدير نموذج قبل استدعاء modele.build() أو تدريبه، فتُصدَّر أوزان بلا معنى. تحقّق دائمًا من وزن معلوم قبل التصدير.

صيغة .h5 القديمة

Keras كان يحفظ افتراضيًّا في .h5 (HDF5). هذه الصيغة لا تحفظ التوقيع الذي يحتاجه tf2onnx. عند تحميل .h5، أعد الحفظ في SavedModel أوّلًا:

modele = tf.keras.models.load_model("ancien.h5")
modele.save("modele_saved", save_format="tf") # الآن نقدر نُصدّر

.keras الجديد أفضل، لكن بقيت .h5 منتشرة في المشاريع القديمة. لا تُضيّع ساعة في محاولة تصدير .h5 مباشرةً.

الخلاصة

  • tf2onnx هو الأداة الرسميّة؛ تعمل من SavedModel، من Keras، أو من dير graph قديم.
  • input_signature يُعادل dynamic_axes عند PyTorch: None لحجم حزمة مرن.
  • ترتيب NHWC الخاصّ بـTensorFlow يُحوَّل تلقائيًّا؛ استعمل --inputs-as-nchw لتفادي Transpose زائدة.
  • ركّز على المزالق: عمليّة غير مدعومة، float64 صامت، أبعاد ديناميكيّة، ونماذج غير مُدرَّبة.

الوحدة التالية: كيف نقيس أنّ التصدير حافظ فعلًا على الدقّة، بأدوات علميّة لا بمجرّد نظرة سريعة.