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

الوحدة 3 — الواجهة الوظيفية: مدخلات متعدّدة وتفرّعات

توقّفت الوحدة السابقة عند أربعة حدود للواجهة التتابعية. تُزيل الواجهة الوظيفية هذه الحدود جميعها، مقابل فكرة واحدة جديدة: الطبقة كائن قابل للاستدعاء على تنسور.

تحوّل زاوية النظر

في النمط التتابعي نُصرّح بلائحة من الطبقات. أمّا في النمط الوظيفي فنُوصِّل التنسورات بعضها ببعض، ثمّ نُشير في النهاية إلى طرفَي الرسم.

from tensorflow import keras
from tensorflow.keras import layers

inputs = keras.Input(shape=(784,))
h = layers.Dense(128, activation="relu")(inputs)
h = layers.Dropout(0.2)(h)
outputs = layers.Dense(10, activation="softmax")(h)

model = keras.Model(inputs=inputs, outputs=outputs)

layers.Dense(128) يُنشئ الطبقة؛ و(inputs) الذي يليه يُطبّقها. هاتان الخطوتان، اللتان تنصهران في النمط التتابعي، تنفصلان هنا، وهذا الفصل بالذات هو ما يجعل كلّ ما يأتي بعده ممكنًا.

التنسور المُتعامَل معه رمزيّ: لا يحمل أيّ بيان، بل شكلًا ونوعًا فقط. يستعمله Keras لفحص التوافق بين الطبقات لحظة البناء، قبل أيّ تدريب.

مدخلان مختلفان في طبيعتهما

لنأخذ حالة ملموسة: التنبّؤ بسعر مسكن انطلاقًا من صورته وخصائصه العدديّة. طبيعتان من البيانات، فمعالجتان قبل الدمج.

image = keras.Input(shape=(128, 128, 3), name="image")
tabular = keras.Input(shape=(12,), name="features")

v = layers.Conv2D(32, 3, activation="relu")(image)
v = layers.GlobalAveragePooling2D()(v)

t = layers.Dense(32, activation="relu")(tabular)

merged = layers.Concatenate()([v, t])
h = layers.Dense(64, activation="relu")(merged)
price = layers.Dense(1, name="price")(h)

model = keras.Model(inputs=[image, tabular], outputs=price)

تسمية المدخلات ليست زخرفة: بل تُتيح تغذية fit بقاموس، أكثر وضوحًا وأقلّ هشاشة بكثير من الترتيب الوضعيّ.

model.fit({"image": images, "features": features}, prices, epochs=10)
Concatenate يشترط أبعادًا متوافقة

تُدمج Concatenate على المحور الأخير افتراضيًا: وعليه أن تتطابق جميع الأبعاد الأخرى. دمج تنسور شكله (None, 8, 8, 32) مع آخر شكله (None, 32) يفشل. ولهذا فإنّ GlobalAveragePooling2D أعلاه لا غنى عنه: فهو يُقلّص مخرج الالتفاف إلى شعاع يصير حينئذٍ متوافقًا مع التفرّع الجدولي.

اختيار طريقة الدمج

Concatenate ليست الخيار الوحيد، وللاختيار عواقب.

طبقة الدمجالأثرمتى تُستعمَل
Concatenateيُلصق، فيتراكم البُعدتفرّعات تحمل معلومات مستقلّة
Addجمع حدًّا حدًّا، بأبعاد متطابقةالاتّصالات المتبقّية
Multiplyجداء حدًّا حدًّاآليّات البوابات والانتباه
Averageمتوسّطتجميع نماذج متكافئة

الفرق بين Concatenate وAdd جوهريّ. فالتراصف يحفظ كامل معلومات التفرّعَين ويترك للطبقة التالية أن تقرّر ما تفعل بها، مقابل بُعد أكبر. أمّا الجمع فيُطبق الإشارتين ولا يزيد شيئًا في البُعد، لكنّه يفترض أنّ التفرّعَين يعيشان في فضاء التمثيل نفسه. وهذا بالضبط ميكانيك الاتّصال المتبقّي في الوحدة 5 من الدورة 07.

مخرجان، خسارتان، وموازنة

الشبكة الواحدة قد تتعلّم عدّة مهام، وهذا كثيرًا ما يكون مفيدًا: إذ تُنظِّم المهامّ بعضها بعضًا.

inputs = keras.Input(shape=(64,))
trunk = layers.Dense(128, activation="relu")(inputs)

category = layers.Dense(5, activation="softmax", name="category")(trunk)
amount = layers.Dense(1, name="amount")(trunk)

model = keras.Model(inputs=inputs, outputs=[category, amount])

model.compile(
optimizer="adam",
loss={"category": "sparse_categorical_crossentropy", "amount": "mse"},
loss_weights={"category": 1.0, "amount": 0.2},
metrics={"category": ["accuracy"], "amount": ["mae"]},
)

loss_weights هو المُعطى الذي يُنسى ويجب ضبطه. لا سبب لأن يكون للخسارتَين رتبة المقدار نفسها: فإنتروبيا متقاطعة تدور حول 1، وخطأ تربيعي على مبالغ باليورو قد يبلغ 10 000. وبدون موازنة يصرف المُحسِّن كلّ طاقته للثانية ولا يتعلّم التصنيف شيئًا.

ضبط الموازنة بلا تخبّط

شغِّل حقبةً واحدة أوّلًا وسجّل رتبة المقدار لكلّ خسارة. ثمّ اختر الأوزان بحيث تصير الإسهامات متقاربة: إن كانت الانحدار تساوي 500 ضعف التصنيف، فوزن قدره 0,002 على الانحدار يُعيدهما إلى تكافؤ. عدّل بعد ذلك حسب المهمّة الأهمّ لك، عن معرفة لا عن تخمين.

مشاركة الأوزان بين تفرّعين

طبقة تُطبَّق مرّتين تستعمل الأوزان نفسها في الموضعَين. وهذا أساس المعماريات التي تُقارن مدخلَين.

encoder = layers.Dense(64, activation="relu")   # يُنشَأ مرّة واحدة

left = keras.Input(shape=(128,))
right = keras.Input(shape=(128,))

el = encoder(left) # نفس الأوزان
er = encoder(right) # نفس الأوزان

diff = layers.Subtract()([el, er])
similar = layers.Dense(1, activation="sigmoid")(diff)

model = keras.Model(inputs=[left, right], outputs=similar)

يمرّ المدخلان بنفس المُشفِّر، أي في فضاء التمثيل نفسه، فيصير الفارق بينهما قابلًا للمقارنة. أمّا إنشاء طبقتَي Dense(64) مستقلّتَين فيكسر المنطق برمّته: كلّ منهما ستتعلّم إسقاطها الخاصّ ولن يبقى للطرح أيّ معنى.

إعادة استعمال نموذج كطبقة

keras.Model هو نفسه قابل للاستدعاء، وهو ما يُتيح تركيب القوالب.

def residual_block(dim):
e = keras.Input(shape=(dim,))
h = layers.Dense(dim, activation="relu")(e)
h = layers.Dense(dim)(h)
s = layers.Add()([e, h])
return keras.Model(e, s, name=f"residual_{dim}")

block = residual_block(64)
x = block(x) # تُدرَج كطبقة عاديّة

هذا التركيب يجعل المعماريات العميقة قابلة للقراءة، ويعرض model.summary() الكتلة عندئذٍ كسطر وحيد، فيتفادى ملخّصات من ثلاث مئة سطر.

في الخلاصة

  • في النمط الوظيفي، إنشاء الطبقة وتطبيقها فعلان متمايزان؛ وهذا الفصل هو ما يُتيح التفرّعات والدمج والأوزان المشتركة.
  • تسمح المدخلات المسمّاة بتغذية fit بقاموس؛ ويحفظ Concatenate معلومات التفرّعات، بينما يُطبِّقها Add دون زيادة في البُعد.
  • مع عدّة مخارج يصير loss_weights لا غنى عنه: بدونه تسحق الخسارةُ ذات رتبة المقدار الأكبر البقيّة.
  • تطبيق الطبقة نفسها على تنسورَين يُشارك أوزانها، فيضع المدخلَين في فضاء تمثيل مشترك.

الوحدة التالية: الطبقات المخصّصة، للحالات التي لا توجد فيها أيّ طبقة جاهزة تفعل ما نريد.