معاينة مختبر آمنة
Transfer Learning Py Torch
هذي معاينة منقّحة للقراءة فقط؛ ما فيه أي شيء يشتغل داخل الصفحة.
قراءة فقط
معاينة الدفتر
Transfer Learning Py Torch
> **ملاحظة بيئة التشغيل المدمجة:** هالمعاينة تستخدم عيّنة صغيرة وثابتة وآمنة من ناحية الحقوق عشان تكون النتايج قابلة للتكرار. النتايج بالحجم الكامل تحتاج مجموعة البيانات أو النموذج الموثّق بالدرس داخل بيئة خارجية معتمدة.
# النماذج المدرّبة مسبقًا والتعلم بالنقل
تدريب الشبكات العصبية الالتفافية (Convolutional Neural Networks أو CNNs) ياخذ وقت طويل ويحتاج بيانات كثيرة. لكن جزء كبير من الوقت يروح في تعلّم أفضل المرشّحات منخفضة المستوى اللي تستخرج الأنماط من الصور. وهنا يجي سؤال طبيعي: نقدر ناخذ شبكة عصبية تدرّبت على مجموعة بيانات ونكيّفها لتصنيف صور ثانية من غير تدريب كامل؟
هالأسلوب اسمه **التعلم بالنقل (Transfer Learning)**، لأننا ننقل جزء من المعرفة من نموذج شبكة عصبية إلى نموذج ثاني. عادةً نبدأ بنموذج مدرّب مسبقًا على مجموعة صور كبيرة، مثل **ImageNet**. هالنماذج تعرف تستخرج سمات مختلفة من الصور العامة في مجموعة البيانات، وفي حالات كثيرة يعطينا بناء مصنّف فوق السمات المستخرجة نتيجة زينة.
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
from torchinfo import summary
import numpy as np
import os
# Self-contained course PyTorch computer-vision helpers.
from torchvision.transforms import ToTensor
from sklearn.datasets import load_digits
from PIL import Image
import glob
default_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
def load_mnist(batch_size=64):
global data_train, data_test, train_loader, test_loader
course_digits = load_digits()
images = torch.as_tensor(course_digits.images, dtype=torch.float32).unsqueeze(1) / 16.0
images = torch.nn.functional.interpolate(images, size=(28, 28), mode="bilinear", align_corners=False)
labels = torch.as_tensor(course_digits.target, dtype=torch.long)
split = int(0.8 * len(images))
data_train = torch.utils.data.TensorDataset(images[:split], labels[:split])
data_test = torch.utils.data.TensorDataset(images[split:], labels[split:])
train_loader = torch.utils.data.DataLoader(data_train, batch_size=batch_size, shuffle=True)
test_loader = torch.utils.data.DataLoader(data_test, batch_size=batch_size)
return data_train, data_test
def _validate(net, dataloader, loss_fn):
net.eval()
total_loss, correct, count = 0.0, 0, 0
with torch.no_grad():
for features, labels in dataloader:
features, labels = features.to(default_device), labels.to(default_device)
output = net(features)
total_loss += loss_fn(output, labels).item() * len(labels)
correct += (output.argmax(1) == labels).sum().item()
count += len(labels)
return total_loss / max(count, 1), correct / max(count, 1)
def train(net, train_loader, test_loader, optimizer=None, lr=0.01, epochs=10, loss_fn=None):
net.to(default_device)
loss_fn = loss_fn or nn.CrossEntropyLoss()
optimizer = optimizer or torch.optim.Adam(net.parameters(), lr=lr)
history = {"train_loss": [], "train_acc": [], "val_loss": [], "val_acc": []}
for epoch in range(epochs):
net.train()
total_loss, correct, count = 0.0, 0, 0
for features, labels in train_loader:
features, labels = features.to(default_device), labels.to(default_device)
optimizer.zero_grad()
output = net(features)
loss = loss_fn(output, labels)
loss.backward()
optimizer.step()
total_loss += loss.item() * len(labels)
correct += (output.argmax(1) == labels).sum().item()
count += len(labels)
validation_loss, validation_accuracy = _validate(net, test_loader, loss_fn)
history["train_loss"].append(total_loss / max(count, 1))
history["train_acc"].append(correct / max(count, 1))
history["val_loss"].append(validation_loss)
history["val_acc"].append(validation_accuracy)
print(f"Epoch {epoch + 1}: train_acc={history['train_acc'][-1]:.3f}, val_acc={validation_accuracy:.3f}")
return history
def train_long(net, train_loader, test_loader, **kwargs):
return train(net, train_loader, test_loader, **kwargs)
def plot_results(history):
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(history["train_acc"], label="train")
axes[0].plot(history["val_acc"], label="validation")
axes[1].plot(history["train_loss"], label="train")
axes[1].plot(history["val_loss"], label="validation")
axes[0].set_title("Accuracy")
axes[1].set_title("Loss")
axes[0].legend()
axes[1].legend()
plt.show()
def plot_convolution(kernel, title=""):
convolution = nn.Conv2d(1, 1, kernel_size=3, bias=False)
with torch.no_grad():
convolution.weight.copy_(kernel.reshape(1, 1, 3, 3))
fig, axes = plt.subplots(2, 6, figsize=(10, 4))
fig.suptitle(title)
for index in range(5):
image = data_train[index][0]
axes[0, index].imshow(image[0], cmap="gray")
axes[1, index].imshow(convolution(image.unsqueeze(0))[0, 0], cmap="gray")
axes[0, index].axis("off")
axes[1, index].axis("off")
axes[0, 5].imshow(kernel, cmap="gray")
axes[0, 5].axis("off")
axes[1, 5].axis("off")
plt.show()
def display_dataset(dataset, 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):
image, label = dataset[index]
image = image.detach().cpu()
low, high = image.min(), image.max()
image = (image - low) / (high - low + 1e-7)
axes[index].imshow(np.transpose(image.numpy(), (1, 2, 0)))
axes[index].axis("off")
if classes is not None:
axes[index].set_title(classes[int(label)])
plt.show()
def check_image_dir(pattern):
invalid = []
for filename in glob.glob(pattern):
try:
with Image.open(filename) as image:
image.verify()
except (OSError, SyntaxError):
invalid.append(filename)
if invalid:
raise ValueError(f"Invalid images detected: {invalid[:5]}")
return 0## مجموعة بيانات تعليمية للقطط والكلاب
ينشئ هالدفتر صورًا هندسية ثابتة ومؤلَّفة لهالنسخة. كذا نختبر مسار ImageFolder ونقل التعلّم بدون تنزيل أرشيف صور خارجي؛ وللتجارب الواقعية استخدموا بيانات موثقة الترخيص.
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):
pixels = (course_pet_image(class_name, index) * 255).astype("uint8")
Image.fromarray(pixels).save(class_dir / f"{index:03}.png")
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")بعدها نحمل الصور في مجموعة البيانات داخل PyTorch، ونحوّلها إلى موترات ونطبّعها. بنطبّق التحويل `std_normalize` عشان تدخل الصور ضمن النطاق اللي تتوقعه شبكة VGG المدرّبة مسبقًا:
std_normalize = transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225],
)
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
std_normalize,
])
dataset = torchvision.datasets.ImageFolder(data_dir, transform=transform)
train_size = int(0.8 * len(dataset))
test_size = len(dataset) - train_size
split_generator = torch.Generator().manual_seed(2026)
trainset, testset = torch.utils.data.random_split(
dataset, [train_size, test_size], generator=split_generator
)
display_dataset(dataset, classes=dataset.classes)## النماذج المدرّبة مسبقًا
داخل وحدة `torchvision` نماذج كثيرة مدرّبة مسبقًا، وعلى الإنترنت فيه أكثر. خلونا نشوف كيف نحمّل نموذج VGG-16، وهو من أبسطها، ونستخدمه:
# Keep the release notebook deterministic and offline: pretrained weights
# are intentionally not fetched by the embedded execution profile.
vgg = torchvision.models.vgg16(weights=None)
sample_image = dataset[0][0].unsqueeze(0)
res = vgg(sample_image)
print(res[0].argmax())النتيجة اللي طلعت لنا رقم فئة في `ImageNet`، ونقدر نبحث عنه في [هالجدول](https://gist.github.com/yrevar/942d3a0ac09ec9e5eb3a). الكود الجاي يحمّل جدول الفئات تلقائيًا ويرجع النتيجة:
class_map = {index: (f"course-{index:04d}", f"fixture class {index}") for index in range(1000)}
class_map[res[0].argmax().item()]خلونا نشوف بعد بنية شبكة VGG-16:
summary(vgg,input_size=(1,3,224,224))إضافة للطبقات اللي نعرفها، فيه نوع اسمه **الإسقاط (Dropout)**. هالطبقات تستخدم تقنية **الانتظام (Regularization)**، وهي تعديلات بسيطة على خوارزمية التعلم تساعد النموذج يعمّم أفضل. أثناء التدريب تهمل طبقات الإسقاط نسبة من عصبونات الطبقة السابقة — حول 30% — ويكمل التدريب من دونها. هالشي يساعد التحسين يطلع من النقاط الدنيا المحلية، ويوزّع سلطة القرار على مسارات عصبية مختلفة، فيرفع استقرار الشبكة عمومًا.
## الحوسبة باستخدام GPU
الشبكات العصبية العميقة مثل VGG-16 والبنى الأحدث تحتاج قدرة حوسبية كبيرة. لذلك من المنطقي نستفيد من تسريع GPU إذا كان متاحًا. عشان نسوي كذا، لازم ننقل كل الموترات الداخلة في الحساب صراحةً إلى GPU.
الطريقة المعتادة إن الكود يفحص توفر GPU، ثم يعرّف المتغير `device` بحيث يشير إلى جهاز الحوسبة، سواء كان GPU أو CPU.
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print('Doing computations on device = {}'.format(device))
vgg.to(device)
sample_image = sample_image.to(device)
vgg(sample_image).argmax()## استخراج سمات VGG
إذا بغينا نستخدم VGG-16 لاستخراج السمات من صورنا، نحتاج النموذج من دون طبقات التصنيف الأخيرة. ونقدر نحصل على «مستخرج السمات» هذا باستخدام `vgg.features`:
res = vgg.features(sample_image).cpu()
plt.figure(figsize=(15,3))
plt.imshow(res.detach().view(512,-1).T)
print(res.size())أبعاد موتر السمات هي 512x7x7، لكن عشان نعرضه بصريًا كان لازم نعيد تشكيله إلى صورة ثنائية الأبعاد.
الحين خلونا نختبر هل تنفع هالسمات لتصنيف الصور. بناخذ يدويًا جزءًا من الصور — 800 صورة في حالتنا — ونحسب متجهات سماتها مسبقًا. بنخزّن النتيجة في موتر كبير اسمه `feature_tensor`، والتسميات في `label_tensor`:
bs = 8
dl = torch.utils.data.DataLoader(dataset,batch_size=bs,shuffle=True)
num = len(dataset)
feature_tensor = torch.zeros(num,512*7*7).to(device)
label_tensor = torch.zeros(num).to(device)
i = 0
for x,l in dl:
with torch.no_grad():
f = vgg.features(x.to(device))
batch_count = len(l)
feature_tensor[i:i+batch_count] = f.view(batch_count, -1)
label_tensor[i:i+batch_count] = l
i += batch_count
print('.',end='')
if i>=num:
breakالحين نقدر نعرّف `vgg_dataset` عشان ياخذ البيانات من هالموتر، ويقسمها إلى مجموعتي تدريب واختبار بدالة `random_split`، ثم ندرّب فوق السمات المستخرجة شبكة تصنيف صغيرة فيها طبقة كثيفة وحدة:
vgg_dataset = torch.utils.data.TensorDataset(feature_tensor,label_tensor.to(torch.long))
train_count = max(1, int(0.8 * len(vgg_dataset)))
test_count = len(vgg_dataset) - train_count
train_ds, test_ds = torch.utils.data.random_split(vgg_dataset, [train_count, test_count], generator=torch.Generator().manual_seed(2026))
train_loader = torch.utils.data.DataLoader(train_ds,batch_size=32)
test_loader = torch.utils.data.DataLoader(test_ds,batch_size=32)
net = torch.nn.Sequential(torch.nn.Linear(512*7*7,2),torch.nn.LogSoftmax(dim=1)).to(device)
history = train(net,train_loader,test_loader)النتيجة ممتازة: نقدر نميّز بين القط والكلب باحتمال يقارب 98%! لكننا اختبرنا هالأسلوب على جزء صغير من الصور بس، لأن استخراج السمات يدويًا ياخذ وقت طويل.
## التعلم بالنقل باستخدام شبكة VGG كاملة
نقدر نتجنب حساب السمات يدويًا قبل التدريب إذا استخدمنا شبكة VGG-16 الأصلية كاملة أثناء التدريب. خلونا نطالع بنية كائن VGG-16:
print(vgg)تقدرون تشوفون إن الشبكة تحتوي على:
* مستخرج سمات (`features`) مكوّن من عدة طبقات التفاف وتجميع
* طبقة تجميع بالمتوسط (`avgpool`)
* المصنّف الأخير `classifier`، وهو عدة طبقات كثيفة تحوّل 25088 سمة إدخال إلى 1000 فئة — عدد فئات ImageNet
عشان ندرّب نموذجًا متكاملًا يصنّف مجموعة البيانات الخاصة بنا، نحتاج إلى:
* **استبدال المصنّف الأخير** بمصنّف ينتج عدد الفئات المطلوب. في حالتنا تكفينا طبقة `Linear` وحدة فيها 25088 مدخلًا و 2 من عصبونات الإخراج.
* **تجميد أوزان مستخرج السمات الالتفافي** عشان ما تتدرّب. يُنصح نجمّدها بالبداية؛ لأن طبقة المصنّف غير المدرّبة قد تخرّب أوزان مستخرج السمات المدرّبة مسبقًا. نجمّد الأوزان بضبط الخاصية `requires_grad` لكل المعاملات إلى `False`.
vgg.classifier = torch.nn.Linear(25088,2).to(device)
for x in vgg.features.parameters():
x.requires_grad = False
summary(vgg,(1, 3,244,244))مثل ما يوضح الملخص، في النموذج قرابة 15 مليون معامل، لكن القابل للتدريب منها 50k بس، وهي أوزان طبقة التصنيف. هالشي زين؛ لأننا نقدر نضبط عددًا أصغر من المعاملات بعدد أقل من الأمثلة.
الحين بندرّب النموذج على مجموعة البيانات الأصلية. العملية بتطول، ولذلك بنستخدم الدالة `train_long` اللي تطبع نتائج وسيطة من غير ما تنتظر نهاية الحقبة. ننصح بقوة تشغّلون هالتدريب على جهاز فيه GPU!
trainset, testset = torch.utils.data.random_split(dataset, [max(1, int(0.8 * len(dataset))), len(dataset) - max(1, int(0.8 * len(dataset)))], generator=torch.Generator().manual_seed(42))
train_loader = torch.utils.data.DataLoader(trainset,batch_size=16)
test_loader = torch.utils.data.DataLoader(testset,batch_size=16)
train_long(vgg,train_loader,test_loader,loss_fn=torch.nn.CrossEntropyLoss(),epochs=1)واضح إننا حصلنا على مصنّف للقطط والكلاب دقته زينة! خلونا نحفظه للاستخدام بعدين.
torch.save(vgg.state_dict(), 'data/cats_dogs.pth')بعدها نقدر نحمّل النموذج من الملف بأي وقت. هالشي يفيد لو خربت التجربة الجاية النموذج؛ ما نضطر نبدأ من الصفر.
vgg.load_state_dict(torch.load('data/cats_dogs.pth', map_location=device, weights_only=True))
vgg = vgg.to(device)## الضبط الدقيق في التعلم بالنقل
بالقسم السابق درّبنا طبقة المصنّف الأخيرة عشان تصنّف صور مجموعة البيانات الخاصة بنا، لكننا ما أعدنا تدريب مستخرج السمات. اعتمد نموذجنا على السمات اللي تعلمها من بيانات ImageNet. إذا كانت أجسامكم تختلف بصريًا عن صور ImageNet المعتادة، يمكن ما تكون تركيبة السمات هذي الأفضل؛ لذلك من المنطقي نبدأ ندرّب الطبقات الالتفافية بعد.
عشان نسوي كذا، نفك تجميد معاملات المرشّحات الالتفافية اللي جمّدناها قبل.
> **ملاحظة:** مهم تجمّدون المعاملات أول، وتدرّبون عدة حقب عشان تستقر أوزان طبقة التصنيف. لو بدأتوا فورًا بتدريب الشبكة كاملة ومعاملاتها مفكوكة، فالأخطاء الكبيرة غالبًا بتخرّب الأوزان المدرّبة مسبقًا في الطبقات الالتفافية.
for x in vgg.features.parameters():
x.requires_grad = Trueبعد فك التجميد نقدر ندرّب كم حقبة زيادة. ونقدر بعد نختار معدل تعلم أقل عشان نقلّل الأثر على الأوزان المدرّبة مسبقًا. حتى مع معدل منخفض، توقّعوا إن الدقة تنزل ببداية التدريب، ثم توصل بالنهاية لمستوى أعلى شوي من حالة الأوزان الثابتة.
> **ملاحظة:** هالتدريب أبطأ بكثير؛ لأننا نحتاج نمرّر التدرجات للخلف عبر طبقات كثيرة من الشبكة. ممكن تراقبون أول كم دفعة مصغّرة عشان تشوفون الاتجاه، ثم توقفون الحساب.
train_long(vgg,train_loader,test_loader,loss_fn=torch.nn.CrossEntropyLoss(),epochs=1,lr=0.0001)## نماذج ثانية للرؤية الحاسوبية
VGG-16 من أبسط بنى الرؤية الحاسوبية، وحزمة `torchvision` توفّر شبكات كثيرة مدرّبة مسبقًا. من أشهرها بنى **ResNet** اللي طورتها Microsoft، و**Inception** من Google. خلونا مثلًا نستكشف بنية ResNet-18، وهو أبسط نموذج فيها. ResNet عائلة نماذج بأعماق مختلفة؛ جرّبوا ResNet-151 إذا بغيتوا تشوفون وش يعني نموذج عميق فعلًا:
resnet = torchvision.models.resnet18()
print(resnet)مثل ما تشوفون، النموذج فيه المكوّنان الأساسيان نفسهما: مستخرج السمات والمصنّف الأخير (`fc`). لذلك نقدر نستخدمه بالطريقة نفسها اللي استخدمنا فيها VGG-16 في التعلم بالنقل. جرّبوا الكود اللي فوق مع نماذج ResNet مختلفة بصفتها النموذج الأساسي، وراقبوا كيف تتغيّر الدقة.
## التطبيع على مستوى الدفعة
في هالشبكة نوع إضافي من الطبقات: **التطبيع على مستوى الدفعة (Batch Normalization)**. فكرته يرجّع القيم اللي تمشي داخل الشبكة العصبية إلى النطاق المناسب. عادةً تشتغل الشبكات أفضل إذا كانت القيم كلها ضمن [-1,1] أو [0,1]، وعشان كذا نعيد تحجيم بيانات الإدخال ونطبّعها. لكن أثناء تدريب شبكة عميقة يمكن تطلع القيم بعيدًا عن هالنطاق ويصير التدريب صعب. تحسب طبقة التطبيع متوسط كل قيم الدفعة المصغّرة الحالية وانحرافها المعياري، ثم تستخدمهما لتطبيع الإشارة قبل تمريرها في طبقة الشبكة العصبية. وهذا يرفع استقرار الشبكات العميقة بشكل واضح.
## الزبدة
بالتعلم بالنقل قدرنا نبني بسرعة مصنّفًا لمهمة **التصنيف** المخصصة ونوصل لدقة مرتفعة. لكن المثال مب عادل تمامًا؛ لأن شبكة VGG-16 الأصلية تدرّبت من قبل على التعرّف على القطط والكلاب، وإحنا أعدنا استخدام أغلب الأنماط الموجودة داخلها. توقّعوا دقة أقل مع أجسام أغرب ومتخصصة في مجال معيّن، مثل تفاصيل خط إنتاج في مصنع أو أوراق أنواع مختلفة من الأشجار.
تقدرون تلاحظون بعد إن المهام الأعقد اللي نحلها الحين تحتاج قدرة حوسبية أعلى، وما يسهل حلها على CPU. بالوحدة الجاية بنجرّب تنفيذًا أخف يدرّب النموذج نفسه بموارد حوسبة أقل، مقابل انخفاض بسيط جدًا في الدقة.
حذفنا المخرجات وعدّادات التشغيل والودجات والمحتوى النشط وقت الاستيراد. شغّل الدفاتر بس في بيئة خارجية تثق فيها.
سجّل تطبيقك
التسجيل اختياري، يفيدك تتذكر وش طبّقت، ولا يمنع إكمال الدورة.