معاينة مختبر آمنة
Transfer Learning TF
هذي معاينة منقّحة للقراءة فقط؛ ما فيه أي شيء يشتغل داخل الصفحة.
قراءة فقط
معاينة الدفتر
Transfer Learning TF
> **ملاحظة بيئة التشغيل المدمجة:** هالمعاينة تستخدم عيّنة صغيرة وثابتة وآمنة من ناحية الحقوق عشان تكون النتايج قابلة للتكرار. النتايج بالحجم الكامل تحتاج مجموعة البيانات أو النموذج الموثّق بالدرس داخل بيئة خارجية معتمدة.
# النماذج المدرّبة مسبقًا والتعلم بالنقل
تدريب الشبكات العصبية الالتفافية (Convolutional Neural Networks أو CNNs) ياخذ وقت طويل ويحتاج بيانات كثيرة. لكن جزء كبير من الوقت يروح في تعلّم أفضل المرشّحات منخفضة المستوى اللي تستخرج الأنماط من الصور. وهنا يجي سؤال طبيعي: نقدر ناخذ شبكة عصبية تدرّبت على مجموعة بيانات ونكيّفها لتصنيف صور ثانية من غير تدريب كامل؟
هالأسلوب اسمه **التعلم بالنقل (Transfer Learning)**، لأننا ننقل جزءًا من المعرفة من نموذج شبكة عصبية إلى نموذج ثاني. عادةً نبدأ بنموذج مدرّب مسبقًا على مجموعة صور كبيرة، مثل **ImageNet**. هالنماذج تعرف تستخرج سمات مختلفة من الصور العامة في مجموعة البيانات، وفي حالات كثيرة يعطينا بناء مصنّف فوق السمات المستخرجة نتيجة زينة.
import tensorflow as tf
from tensorflow import keras
import matplotlib.pyplot as plt
import numpy as np
import os
# Self-contained course computer-vision helpers.
from pathlib import Path
import glob
from PIL import Image
def plot_convolution(data, kernel, title=""):
fig, axes = plt.subplots(2, len(data) + 1, figsize=(10, 4))
fig.suptitle(title, fontsize=16)
tensor_kernel = np.asarray(kernel, dtype=np.float32)[:, :, None, None]
for index, image in enumerate(data):
axes[0, index].imshow(image, cmap="gray")
tensor_image = np.asarray(image, dtype=np.float32)[None, :, :, None]
convolved = tf.nn.conv2d(tensor_image, tensor_kernel, strides=1, padding="SAME")
axes[1, index].imshow(convolved[0, :, :, 0], cmap="gray")
axes[0, index].axis("off")
axes[1, index].axis("off")
axes[0, -1].imshow(kernel, cmap="gray")
axes[0, -1].axis("off")
axes[1, -1].axis("off")
plt.tight_layout()
plt.show()
def plot_results(history):
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].set_title("Accuracy")
axes[1].set_title("Loss")
for key in ("accuracy", "val_accuracy"):
if key in history.history:
axes[0].plot(history.history[key], label=key)
for key in ("loss", "val_loss"):
if key in history.history:
axes[1].plot(history.history[key], label=key)
axes[0].legend()
axes[1].legend()
plt.show()
def display_dataset(dataset, labels=None, n=10, classes=None):
count = min(n, len(dataset))
fig, axes = plt.subplots(1, count, figsize=(1.8 * count, 3))
axes = np.atleast_1d(axes)
for index in range(count):
axes[index].imshow(dataset[index])
axes[index].axis("off")
if classes is not None and labels is not None:
label = int(np.asarray(labels[index]).reshape(-1)[0])
axes[index].set_title(classes[label])
plt.show()
def check_image(filename):
try:
with Image.open(filename) as image:
image.verify()
return True
except (OSError, SyntaxError):
return False
def check_image_dir(pattern):
invalid = [filename for filename in glob.glob(pattern) if not check_image(filename)]
if invalid:
names = ", ".join(Path(filename).name for filename in invalid[:5])
raise ValueError(f"Invalid images detected ({len(invalid)}): {names}")
return 0## مجموعة بيانات تعليمية للقطط والكلاب
ينشئ هالدفتر مجموعة صور صغيرة وثابتة من أشكال هندسية مؤلَّفة لهالنسخة. هدفها تجربة مسار نقل التعلّم بدون تنزيل أرشيف صور خارجي؛ وللتجارب الواقعية استخدموا مجموعة بيانات موثقة الترخيص في بيئتكم.
from pathlib import Path
course_data_dir = Path("data/course-pets")
def course_pet_image(kind, index, size=224):
rng = np.random.default_rng(10_000 + index + (0 if kind == "Cat" else 1_000))
y, x = np.mgrid[0:size, 0:size]
image = np.full((size, size, 3), 0.12, dtype="float32")
image += rng.normal(0, 0.025, image.shape).astype("float32")
cx, cy = size // 2 + rng.integers(-12, 13, size=2)
face = (x - cx) ** 2 + (y - cy) ** 2 < (size * 0.27) ** 2
if kind == "Cat":
ears = ((y < cy - size * 0.18) & (np.abs(x - cx) > size * 0.12) & (np.abs(x - cx) < size * 0.3))
shape = face | ears
color = np.array([0.85, 0.58, 0.25], dtype="float32")
else:
ears = (((x - (cx - size * 0.3)) / (size * 0.14)) ** 2 + ((y - cy) / (size * 0.25)) ** 2 < 1) | (((x - (cx + size * 0.3)) / (size * 0.14)) ** 2 + ((y - cy) / (size * 0.25)) ** 2 < 1)
shape = face | ears
color = np.array([0.35, 0.62, 0.88], dtype="float32")
image[shape] = color
return np.clip(image, 0, 1)
for class_name in ("Cat", "Dog"):
class_dir = course_data_dir / class_name
class_dir.mkdir(parents=True, exist_ok=True)
for index in range(48):
keras.utils.save_img(class_dir / f"{index:03}.png", course_pet_image(class_name, index))
data_dir = str(course_data_dir)print(f"Prepared the deterministic course fixture at {data_dir}.")الصور مولّدة محليًا، ومع هذا نتحقق من سلامة الملفات قبل تحميلها كبيانات تدريب.
check_image_dir(f"{data_dir}/Cat/*.png")
check_image_dir(f"{data_dir}/Dog/*.png")## تحميل مجموعة البيانات
في الأمثلة السابقة حمّلنا مجموعات بيانات مدمجة في Keras. الحين بنتعامل مع مجموعة بياناتنا، ونحتاج نحمّلها من مجلد صور.
في الواقع العملي يمكن تكون مجموعات الصور ضخمة، وما نقدر نفترض إن البيانات كلها تدخل الذاكرة دفعة وحدة. لذلك غالبًا نمثّل مجموعات البيانات على شكل **مولّدات (Generators)** ترجع البيانات بدفعات مصغّرة مناسبة للتدريب.
في **التصنيف**، وتحديدًا **تصنيف الصور**، يوفّر Keras الدالة `image_dataset_from_directory`. تحمل الصور من المجلدات الفرعية اللي تمثّل الفئات، وتهتم بتحجيم الصور، وتقدر بعد تقسّم مجموعة البيانات إلى جزأين للتدريب والاختبار:
batch_size = 16
ds_train = keras.utils.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="training",
seed=13,
image_size=(224, 224),
batch_size=batch_size,
)
ds_test = keras.utils.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="validation",
seed=13,
image_size=(224, 224),
batch_size=batch_size,
)مهم نعيّن قيمة `seed` نفسها في الاستدعاءين؛ لأنها تأثر على تقسيم الصور بين مجموعتي التدريب والاختبار.
تلتقط مجموعة البيانات أسماء الفئات تلقائيًا من المجلدات، وإذا احتجناها نقدر نوصل لها بالاستدعاء هذا:
ds_train.class_namesنقدر نمرّر مجموعات البيانات اللي حصلنا عليها مباشرة إلى دالة `fit` لتدريب النموذج. كل مجموعة فيها الصور وتسمياتها المقابلة، ونقدر نمر عليها بالتركيب الجاي:
for x,y in ds_train:
print(f"Training batch shape: features={x.shape}, labels={y.shape}")
x_sample, y_sample = x,y
break
display_dataset(x_sample.numpy().astype(np.int64),np.expand_dims(y_sample,1),classes=ds_train.class_names)> **ملاحظة**: كل الصور في مجموعة البيانات ممثّلة بموترات فاصلة عائمة ضمن النطاق 0-255. قبل ما نمرّرها إلى الشبكة العصبية لازم نعيد تحجيم القيم إلى 0-1. وعند رسم الصور، إما نسوي الشي نفسه أو نحوّل القيم إلى النوع `int`، مثل الكود اللي فوق، عشان يعرف `matplotlib` إننا نبي نرسم الصورة الأصلية غير المحجّمة.
## النماذج المدرّبة مسبقًا
لمهام كثيرة في **التصنيف**، ومنها **تصنيف الصور**، نلقى نماذج مبنية على **الشبكة العصبية** ومدرّبة مسبقًا. عدد كبير منها متاح في مساحة الأسماء `keras.applications`، وعلى الإنترنت فيه أكثر. خلونا نشوف كيف نحمّل VGG-16، وهو من أبسطها، ونستخدمه:
vgg = keras.applications.VGG16(weights=None)
inp = keras.applications.vgg16.preprocess_input(x_sample[:1])
res = vgg(inp)
predicted_index = int(tf.argmax(res, axis=1)[0])
print(f"Fixture-model output index = {predicted_index}")فيه كم نقطة مهمة هنا:
* قبل ما نمرّر إدخالًا لأي شبكة مدرّبة مسبقًا، لازم نعالجه بالطريقة اللي تتوقعها الشبكة. نسوي هالشي باستدعاء دالة `preprocess_input` المناسبة؛ تستقبل دفعة صور وترجعها بعد المعالجة. مع VGG-16 تُطبّع الصور ويُطرح من كل قناة متوسط محدد مسبقًا، لأن VGG-16 الأصلية تدرّبت بهالمعالجة.
* نطبّق الشبكة العصبية على دفعة الإدخال، وتطلع لنا دفعة موترات كل واحد فيها 1000 عنصر يبيّن احتمال كل فئة. نلقى رقم الفئة الأعلى احتمالًا باستدعاء `argmax` على الموتر.
* النتيجة هي [رقم فئة في `ImageNet`](https://gist.github.com/yrevar/942d3a0ac09ec9e5eb3a). وعشان نفهمها، نقدر نستخدم دالة `decode_predictions` اللي ترجع أعلى n من الفئات مع أسمائها.
خلونا نشوف بعد بنية شبكة VGG-16:
vgg.summary()## الحوسبة باستخدام GPU
الشبكات العصبية العميقة مثل VGG-16 والبنى الأحدث تحتاج قدرة حوسبية كبيرة. لذلك من المنطقي نستفيد من تسريع GPU إذا كان متاحًا. لحسن الحظ، يسرّع Keras الحسابات تلقائيًا على GPU إذا لقاه. بالكود الجاي نتحقق هل TensorFlow يقدر يستخدم GPU:
tf.config.list_physical_devices('GPU')## استخراج سمات VGG
إذا بغينا نستخدم VGG-16 لاستخراج السمات من صورنا، نحتاج النموذج من دون طبقات التصنيف الأخيرة. نقدر ننشئ VGG-16 من غير الطبقات العليا بالكود هذا:
vgg = keras.applications.VGG16(include_top=False, weights=None)
inp = keras.applications.vgg16.preprocess_input(x_sample[:1])
res = vgg(inp)
print(f"Shape after applying VGG-16: {res[0].shape}")
plt.figure(figsize=(15,3))
plt.imshow(res[0].numpy().reshape(-1,512))أبعاد موتر السمات هي 7x7x512، لكن عشان نعرضه بصريًا كان لازم نعيد تشكيله إلى صورة ثنائية الأبعاد.
الحين خلونا نختبر هل تنفع هالسمات لتصنيف الصور. بناخذ يدويًا جزءًا من الصور — 50 دفعة مصغّرة في حالتنا — ونحسب متجهات سماتها مسبقًا. نقدر نسوي كذا باستخدام واجهة **dataset API** في TensorFlow. تاخذ دالة `map` مجموعة بيانات وتطبّق عليها دالة lambda لتحوّلها. نستخدم هالآلية عشان نبني مجموعتي بيانات جديدتين، `ds_features_train` و`ds_features_test`، فيهما السمات المستخرجة من VGG بدل الصور الأصلية.
num = batch_size*50
ds_features_train = ds_train.take(50).map(lambda x,y : (vgg(keras.applications.vgg16.preprocess_input(x)), y))
ds_features_test = ds_test.take(10).map(lambda x,y : (vgg(keras.applications.vgg16.preprocess_input(x)), y))
for x,y in ds_features_train:
print(x.shape,y.shape)
breakاستخدمنا `.take(50)` عشان نحد حجم مجموعة البيانات ونسرّع العرض. وأكيد تقدرون تنفّذون التجربة على مجموعة البيانات كاملة.
بعد ما صار عندنا مجموعة بيانات بسمات مستخرجة، نقدر ندرّب مصنّفًا كثيفًا بسيطًا يميّز القطط من الكلاب. تستقبل هالشبكة متجه سمات شكله (7,7,512)، وتنتج مخرجًا واحدًا يمثّل كلبًا أو قطًا. وبما إن المهمة هي **التصنيف** الثنائي، نستخدم دالة التنشيط `sigmoid` وخسارة `binary_crossentropy`.
model = keras.models.Sequential([
keras.layers.Flatten(input_shape=(7,7,512)),
keras.layers.Dense(1,activation='sigmoid')
])
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
hist = model.fit(ds_features_train, validation_data=ds_features_test)النتيجة ممتازة: نقدر نميّز بين القط والكلب باحتمال يقارب 95٪! لكننا اختبرنا هالأسلوب على جزء من الصور بس، لأن استخراج السمات يدويًا ياخذ وقت طويل.
## التعلم بالنقل باستخدام شبكة VGG كاملة
نقدر نتجنب حساب السمات يدويًا قبل التدريب إذا استخدمنا شبكة VGG-16 الأصلية كاملة أثناء التدريب، وذلك بإضافة مستخرج السمات طبقةً أولى في شبكتنا.
الميزة الحلوة في بنية Keras إن نموذج VGG-16 اللي عرّفناه فوق ينفع بعد طبقةً داخل الشبكة العصبية الثانية. نحتاج بس نبني فوقه مصنّفًا كثيفًا، ثم ندرّب الشبكة كاملة بالانتشار العكسي.
model = keras.models.Sequential()
model.add(keras.applications.VGG16(include_top=False, weights=None, input_shape=(224,224,3)))
model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(1,activation='sigmoid'))
model.layers[0].trainable = False
model.summary()هالنموذج يبان كشبكة متكاملة لمهمة **التصنيف**؛ تستقبل صورة وترجع فئتها. لكن النقطة الدقيقة إننا نبي VGG16 يشتغل مستخرجًا للسمات، مب نعيد تدريبه. لذلك لازم **نجمّد أوزان مستخرج السمات الالتفافي**. نوصل لأول طبقة في الشبكة باستدعاء `model.layers[0]`، ثم نضبط الخاصية `trainable` إلى `False`.
> **ملاحظة**: تجميد أوزان مستخرج السمات ضروري؛ وإلا يمكن طبقة المصنّف غير المدرّبة تخرّب أوزان المستخرج الالتفافي المدرّبة مسبقًا.
لاحظوا إن إجمالي معاملات شبكتنا حول 15 مليونًا، لكننا ندرّب 25k معامل بس. بقية معاملات المرشّحات الالتفافية العليا مدرّبة مسبقًا. هالشي زين؛ لأننا نقدر نضبط عددًا أصغر من المعاملات بعدد أقل من الأمثلة.
الحين بندرّب شبكتنا ونشوف وش نوصل له. توقّعوا وقت تشغيل طويل شوي، ولا تقلقون إذا بان التنفيذ واقفًا فترة.
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
hist = model.fit(ds_train, validation_data=ds_test)واضح إننا حصلنا على مصنّف للقطط والكلاب دقته زينة!
## حفظ النموذج وتحميله
بعد ما ندرّب النموذج، نقدر نحفظ بنيته وأوزانه المدرّبة في ملف للاستخدام بعدين:
model.save("data/cats_dogs.keras")بعدها نقدر نحمّل النموذج من الملف بأي وقت. هالشي يفيد لو خربت التجربة الجاية النموذج؛ ما نضطر نبدأ من الصفر.
model = keras.models.load_model("data/cats_dogs.keras")## الضبط الدقيق في التعلم بالنقل
بالقسم السابق درّبنا طبقة المصنّف الأخيرة عشان تصنّف صور مجموعة البيانات الخاصة بنا، لكننا ما أعدنا تدريب مستخرج السمات. اعتمد نموذجنا على السمات اللي تعلمها من بيانات ImageNet. إذا كانت أجسامكم تختلف بصريًا عن صور ImageNet المعتادة، يمكن ما تكون تركيبة السمات هذي الأفضل؛ لذلك من المنطقي نبدأ ندرّب الطبقات الالتفافية بعد.
عشان نسوي كذا، نفك تجميد معاملات المرشّحات الالتفافية اللي جمّدناها قبل.
> **ملاحظة:** مهم تجمّدون المعاملات أول، وتدرّبون عدة حقب عشان تستقر أوزان طبقة التصنيف. لو بدأتوا فورًا بتدريب الشبكة كاملة ومعاملاتها مفكوكة، فالأخطاء الكبيرة غالبًا بتخرّب الأوزان المدرّبة مسبقًا في الطبقات الالتفافية.
نموذج VGG-16 الالتفافي موجود داخل الطبقة الأولى، وهو بنفسه مكوّن من طبقات كثيرة. نقدر نطالع بنيته:
model.layers[0].summary()نقدر نفك تجميد كل طبقات القاعدة الالتفافية:
model.layers[0].trainable = Trueلكن فكّها كلها دفعة وحدة مب أفضل فكرة. نقدر بالبداية نفك آخر كم طبقة التفافية بس؛ لأنها تحتوي أنماطًا عالية المستوى مرتبطة بصورنا. مثلًا نبدأ بتجميد كل الطبقات إلا آخر 4 طبقات:
for i in range(len(model.layers[0].layers)-4):
model.layers[0].layers[i].trainable = False
model.summary()
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=1e-5),
loss="binary_crossentropy",
metrics=["acc"],
)لاحظوا إن عدد المعاملات القابلة للتدريب زاد كثير، لكنه للحين حول 50% من كل المعاملات.
بعد فك التجميد نقدر ندرّب كم حقبة زيادة — في مثالنا بنسوي حقبة وحدة بس. ونقدر بعد نختار معدل تعلم أقل عشان نقلّل الأثر على الأوزان المدرّبة مسبقًا. حتى مع معدل منخفض، توقّعوا إن الدقة تنزل ببداية التدريب، ثم توصل بالنهاية لمستوى أعلى شوي من حالة الأوزان الثابتة.
> **ملاحظة:** هالتدريب أبطأ بكثير؛ لأننا نحتاج نمرّر التدرجات للخلف عبر طبقات كثيرة من الشبكة!
hist = model.fit(ds_train, validation_data=ds_test)غالبًا بنوصل لدقة تدريب أعلى لأننا نستخدم شبكة أقوى بمعاملات أكثر، لكن دقة التحقق ما بترتفع بالمقدار نفسه.
جرّبوا تفكون تجميد طبقات أكثر وتزيدون التدريب، وشوفوا هل تقدرون توصلون لدقة أعلى.
## نماذج ثانية للرؤية الحاسوبية
VGG-16 من أبسط بنى الرؤية الحاسوبية، ويوفّر Keras شبكات كثيرة مدرّبة مسبقًا. من أشهرها بنى **ResNet** اللي طورتها Microsoft، و**Inception** من Google. خلونا مثلًا نستكشف بنية ResNet-50، وهو أبسط نموذج فيها. ResNet عائلة نماذج بأعماق مختلفة؛ جرّبوا ResNet-152 إذا بغيتوا تشوفون وش يعني نموذج عميق فعلًا:
resnet = keras.applications.ResNet50(weights=None)
resnet.summary()مثل ما تشوفون، النموذج فيه لبنات البناء المألوفة نفسها: طبقات التفاف، وطبقات تجميع، ومصنّف كثيف أخير. نقدر نستخدمه بالطريقة نفسها اللي استخدمنا فيها VGG-16 في التعلم بالنقل. جرّبوا الكود اللي فوق مع نماذج ResNet مختلفة بصفتها النموذج الأساسي، وراقبوا كيف تتغيّر الدقة.
## التطبيع على مستوى الدفعة
في هالشبكة نوع إضافي من الطبقات: **التطبيع على مستوى الدفعة (Batch Normalization)**. فكرته يرجّع القيم اللي تمشي داخل الشبكة العصبية إلى النطاق المناسب. عادةً تشتغل الشبكات أفضل إذا كانت القيم كلها ضمن [-1,1] أو [0,1]، وعشان كذا نعيد تحجيم بيانات الإدخال ونطبّعها. لكن أثناء تدريب شبكة عميقة يمكن تطلع القيم بعيدًا عن هالنطاق ويصير التدريب صعب. تحسب طبقة التطبيع متوسط كل قيم الدفعة المصغّرة الحالية وانحرافها المعياري، ثم تستخدمهما لتطبيع الإشارة قبل تمريرها في طبقة الشبكة العصبية. وهذا يرفع استقرار الشبكات العميقة بشكل واضح.
## الزبدة
بالتعلم بالنقل قدرنا نبني بسرعة مصنّفًا لمهمة **التصنيف** المخصصة ونوصل لدقة مرتفعة. لكن المثال مب عادل تمامًا؛ لأن شبكة VGG-16 الأصلية تدرّبت من قبل على التعرّف على القطط والكلاب، وإحنا أعدنا استخدام أغلب الأنماط الموجودة داخلها. توقّعوا دقة أقل مع أجسام أغرب ومتخصصة في مجال معيّن، مثل تفاصيل خط إنتاج في مصنع أو أوراق أنواع مختلفة من الأشجار.
تقدرون تلاحظون بعد إن المهام الأعقد اللي نحلها الحين تحتاج قدرة حوسبية أعلى، وما يسهل حلها على CPU. بالوحدة الجاية بنجرّب تنفيذًا أخف يدرّب النموذج نفسه بموارد حوسبة أقل، مقابل انخفاض بسيط جدًا في الدقة.
حذفنا المخرجات وعدّادات التشغيل والودجات والمحتوى النشط وقت الاستيراد. شغّل الدفاتر بس في بيئة خارجية تثق فيها.
سجّل تطبيقك
التسجيل اختياري، يفيدك تتذكر وش طبّقت، ولا يمنع إكمال الدورة.