معاينة مختبر آمنة
Semantic Segmentation Pytorch
هذي معاينة منقّحة للقراءة فقط؛ ما فيه أي شيء يشتغل داخل الصفحة.
قراءة فقط
معاينة الدفتر
Semantic Segmentation Pytorch
> **ملاحظة بيئة التشغيل المدمجة:** هالمعاينة تستخدم عيّنة صغيرة وثابتة وآمنة من ناحية الحقوق عشان تكون النتايج قابلة للتكرار. النتايج بالحجم الكامل تحتاج مجموعة البيانات أو النموذج الموثّق بالدرس داخل بيئة خارجية معتمدة.
# التجزئة
تعلّمنا قبل عن **اكتشاف الأجسام (Object Detection)**، اللي يحدد مواقع الأجسام في الصورة بالتنبؤ بـ *الصناديق المحيطة*. لكن بعض المهام تحتاج تحديدًا أدق من الصندوق المحيط؛ وهذي المهمة اسمها **التجزئة (Segmentation)**.
نقدر ننظر للتجزئة على إنها **تصنيف لكل بكسل**: نتنبأ بفئة **كل** بكسل في الصورة، وتكون *الخلفية* إحدى الفئات. وعندنا نوعان رئيسيان:
* **التجزئة الدلالية (Semantic Segmentation)** تحدد فئة البكسل فقط، وما تفرّق بين أجسام مختلفة من الفئة نفسها.
* **تجزئة المثيلات (Instance Segmentation)** تفصل أفراد الفئة إلى مثيلات مستقلة.
مثلًا، في تجزئة المثيلات تكون 10 خرفان أجسامًا مستقلة، أما في التجزئة الدلالية فكل الخرفان تمثل فئة وحدة.
> **وصف الشكل:** حُذف الأصل لأن حقوق إعادة استخدامه غير موثّقة.
> الصورة من [هالتدوينة](https://www.tensorflow.org/tutorials/images/segmentation)
فيه بُنى عصبية مختلفة للتجزئة، لكنها تشترك ببنية عامة وحدة:
* **المشفّر (Encoder)** يستخرج السمات من صورة الإدخال.
* **فاكّ الترميز (Decoder)** يحوّل هالسمات إلى **صورة قناع** لها حجم الصورة نفسه، وعدد قنوات يوافق عدد الفئات.
> **وصف الشكل:** حُذف الأصل لأن حقوق إعادة استخدامه غير موثّقة.
> الصورة من [هالبحث](https://arxiv.org/pdf/2001.05566.pdf)
## المتطلبات
بالبداية بنستورد المكتبات المطلوبة، ونتأكد هل فيه وحدة معالجة رسومات (GPU) متاحة للتدريب.
import torch
import torchvision
import matplotlib.pyplot as plt
from torchvision import transforms
from torch import nn
from torch import optim
from tqdm import tqdm
import numpy as np
import torch.nn.functional as F
torch.manual_seed(42)
np.random.seed(42)device = 'cuda:0' if torch.cuda.is_available() else 'cpu'
train_size = 0.9
lr = 1e-3
weight_decay = 1e-6
batch_size = 8
epochs = 2## مجموعة تقسيم اصطناعية
نبني أشكالًا وأقنعة بكسلية مؤلَّفة لهالنسخة عشان يشتغل المثال بدون تنزيل بيانات طبية أو قبول ترخيص خارجي.
# No system package, archive utility, or external download is required.ننشئ صور RGB وأقنعة ثنائية بحجم 64×64، ثم نقسمها تقسيمًا ثابتًا للتدريب والاختبار.
def make_segmentation_dataset(samples=160, size=64, seed=2026):
rng = np.random.default_rng(seed)
y, x = np.mgrid[0:size, 0:size]
images, masks = [], []
for _ in range(samples):
cx, cy = rng.integers(size // 4, 3 * size // 4, size=2)
radius = int(rng.integers(size // 8, size // 4))
mask = ((x - cx) ** 2 + (y - cy) ** 2 <= radius ** 2).astype("float32")
image = rng.normal(0.12, 0.035, (size, size, 3)).astype("float32")
image[mask.astype(bool)] = rng.uniform(0.55, 0.95, 3)
images.append(np.clip(image, 0, 1))
masks.append(mask[None, ...])
image_tensor = torch.from_numpy(np.asarray(images)).permute(0, 3, 1, 2).float()
mask_tensor = torch.from_numpy(np.asarray(masks)).float()
split = int(len(image_tensor) * train_size)
return (
(image_tensor[:split], mask_tensor[:split]),
(image_tensor[split:], mask_tensor[split:]),
)
train_dataset, test_dataset = make_segmentation_dataset()خلونا نعرض بعض صور مجموعة البيانات ونشوف شكلها:
def plotn(n, data, only_mask=False):
images, masks = data[0], data[1]
fig, ax = plt.subplots(1, n)
fig1, ax1 = plt.subplots(1, n)
for i, (img, mask) in enumerate(zip(images, masks)):
if i == n:
break
if not only_mask:
ax[i].imshow(torch.permute(img, (1, 2, 0)))
else:
ax[i].imshow(img[0])
ax1[i].imshow(mask[0])
ax[i].axis('off')
ax1[i].axis('off')
plt.show()
plotn(5, train_dataset)بنحتاج محمّلات بيانات (Data Loaders) تمرر البيانات إلى **الشبكة العصبية (Neural Network)** اللي ندرّبها.
train_dataloader = torch.utils.data.DataLoader(list(zip(train_dataset[0], train_dataset[1])), batch_size=batch_size, shuffle=True)
test_dataloader = torch.utils.data.DataLoader(list(zip(test_dataset[0], test_dataset[1])), batch_size=1, shuffle=False)
dataloaders = (train_dataloader, test_dataloader)## SegNet
أبسط بنية من نوع مشفّر–فاكّ ترميز اسمها **SegNet**. يستخدم المشفّر شبكة عصبية التفافية (Convolutional Neural Network, CNN) قياسية فيها عمليات التفاف وتجميع، ويستخدم فاكّ الترميز CNN عكسية فيها عمليات التفاف ورفع للدقة (Upsampling). وتعتمد البنية على التطبيع الدفعي (Batch Normalization) عشان نقدر ندرّب شبكة متعددة الطبقات بنجاح.
> **وصف الشكل:** حُذف الأصل لأن حقوق إعادة استخدامه غير موثّقة.
> الصورة من هالبحث: Badrinarayanan, V., Kendall, A., & Cipolla, R. (2015). [SegNet: A deep convolutional
encoder-decoder architecture for image segmentation](https://arxiv.org/pdf/1511.00561.pdf)
class SegNet(nn.Module):
def __init__(self):
super().__init__()
self.enc_conv0 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=(3,3), padding=1)
self.act0 = nn.ReLU()
self.bn0 = nn.BatchNorm2d(16)
self.pool0 = nn.MaxPool2d(kernel_size=(2,2))
self.enc_conv1 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=(3,3), padding=1)
self.act1 = nn.ReLU()
self.bn1 = nn.BatchNorm2d(32)
self.pool1 = nn.MaxPool2d(kernel_size=(2,2))
self.enc_conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=(3,3), padding=1)
self.act2 = nn.ReLU()
self.bn2 = nn.BatchNorm2d(64)
self.pool2 = nn.MaxPool2d(kernel_size=(2,2))
self.enc_conv3 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=(3,3), padding=1)
self.act3 = nn.ReLU()
self.bn3 = nn.BatchNorm2d(128)
self.pool3 = nn.MaxPool2d(kernel_size=(2,2))
self.bottleneck_conv = nn.Conv2d(in_channels=128, out_channels=256, kernel_size=(3,3), padding=1)
self.upsample0 = nn.UpsamplingBilinear2d(scale_factor=2)
self.dec_conv0 = nn.Conv2d(in_channels=256, out_channels=128, kernel_size=(3,3), padding=1)
self.dec_act0 = nn.ReLU()
self.dec_bn0 = nn.BatchNorm2d(128)
self.upsample1 = nn.UpsamplingBilinear2d(scale_factor=2)
self.dec_conv1 = nn.Conv2d(in_channels=128, out_channels=64, kernel_size=(3,3), padding=1)
self.dec_act1 = nn.ReLU()
self.dec_bn1 = nn.BatchNorm2d(64)
self.upsample2 = nn.UpsamplingBilinear2d(scale_factor=2)
self.dec_conv2 = nn.Conv2d(in_channels=64, out_channels=32, kernel_size=(3,3), padding=1)
self.dec_act2 = nn.ReLU()
self.dec_bn2 = nn.BatchNorm2d(32)
self.upsample3 = nn.UpsamplingBilinear2d(scale_factor=2)
self.dec_conv3 = nn.Conv2d(in_channels=32, out_channels=1, kernel_size=(1,1))
self.sigmoid = nn.Sigmoid()
def forward(self, x):
e0 = self.pool0(self.bn0(self.act0(self.enc_conv0(x))))
e1 = self.pool1(self.bn1(self.act1(self.enc_conv1(e0))))
e2 = self.pool2(self.bn2(self.act2(self.enc_conv2(e1))))
e3 = self.pool3(self.bn3(self.act3(self.enc_conv3(e2))))
b = self.bottleneck_conv(e3)
d0 = self.dec_bn0(self.dec_act0(self.dec_conv0(self.upsample0(b))))
d1 = self.dec_bn1(self.dec_act1(self.dec_conv1(self.upsample1(d0))))
d2 = self.dec_bn2(self.dec_act2(self.dec_conv2(self.upsample2(d1))))
d3 = self.sigmoid(self.dec_conv3(self.upsample3(d2)))
return d3لازم نوقف عند دالة الخسارة المستخدمة في التجزئة. في المشفّرات التلقائية التقليدية نقيس التشابه بين صورتين، ونقدر نستخدم متوسط مربع الخطأ (MSE). أما في التجزئة، فكل بكسل في القناع الهدف يمثّل رقم فئة، ويُرمّز على البعد الثالث بترميز one-hot. لذلك نستخدم دالة خسارة خاصة بالتصنيف: خسارة الإنتروبيا المتقاطعة، بعد أخذ متوسطها على كل البكسلات. وبما إن القناع في مثالنا ثنائي، بنستخدم **خسارة الإنتروبيا المتقاطعة الثنائية (Binary Cross-Entropy, BCE)**.
model = SegNet().to(device)
optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay)
loss_fn = nn.BCELoss()نعرّف حلقة التدريب بالطريقة المعتادة:
def train(dataloaders, model, loss_fn, optimizer, epochs, device):
tqdm_iter = tqdm(range(epochs))
train_dataloader, test_dataloader = dataloaders[0], dataloaders[1]
for epoch in tqdm_iter:
model.train()
train_loss = 0.0
test_loss = 0.0
for batch in train_dataloader:
imgs, labels = batch
imgs = imgs.to(device)
labels = labels.to(device)
preds = model(imgs)
loss = loss_fn(preds, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
train_loss += loss.item()
model.eval()
with torch.no_grad():
for batch in test_dataloader:
imgs, labels = batch
imgs = imgs.to(device)
labels = labels.to(device)
preds = model(imgs)
loss = loss_fn(preds, labels)
test_loss += loss.item()
train_loss /= len(train_dataloader)
test_loss /= len(test_dataloader)
tqdm_dct = {'train loss:': train_loss, 'test loss:': test_loss}
tqdm_iter.set_postfix(tqdm_dct, refresh=True)
tqdm_iter.refresh()train(dataloaders, model, loss_fn, optimizer, epochs, device)عشان نقيّم النموذج، بنعرض الأقنعة الهدف والأقنعة اللي تنبأ بها لعدد من الصور:
model.eval()
predictions = []
image_mask = []
plots = 5
images, masks = test_dataset[0], test_dataset[1]
for i, (img, mask) in enumerate(zip(images, masks)):
if i == plots:
break
img = img.to(device).unsqueeze(0)
predictions.append((model(img).detach().cpu()[0] > 0.5).float())
image_mask.append(mask)
plotn(plots, (predictions, image_mask), only_mask=True)فيه مقاييس رسمية لتقييم الأداء، وتقدرون تقرؤون عنها [هنا](https://towardsdatascience.com/metrics-to-evaluate-your-semantic-segmentation-model-6bcb99639aa2). أسهلها **دقة البكسل (Pixel Accuracy)**، وهي نسبة البكسلات اللي تصنّفت بشكل صحيح.
## U-Net
بنية SegNet منطقية، لكنها مب الأدق. بالبداية نطبّق بنية CNN هرمية على الصورة الأصلية، وهذا يقلل الدقة المكانية لسمات الصورة. ولما نعيد بناء الصورة، ما نقدر نرجّع مواقع البكسلات بدقتها الأصلية.
وهذا يوصلنا لفكرة **وصلات التخطي (Skip Connections)** بين طبقات الالتفاف في المشفّر وفاكّ الترميز. هالبنية شائعة جدًا في التجزئة الدلالية، واسمها **U-Net**. تساعد وصلة التخطي عند كل مستوى التفاف الشبكة تحتفظ بمعلومات السمات الموجودة في إدخال هالمستوى.
بنستخدم هنا بنية CNN بسيطة، لكن U-Net تقدر تستخدم مشفّرًا أعقد لاستخراج السمات، مثل ResNet-50.
> **وصف الشكل:** حُذف الأصل لأن حقوق إعادة استخدامه غير موثّقة.
> الصورة من البحث: Ronneberger, Olaf, Philipp Fischer, and Thomas Brox. [U-Net: Convolutional networks for biomedical image segmentation.](https://arxiv.org/pdf/1505.04597.pdf)
class UNet(nn.Module):
def __init__(self):
super().__init__()
self.enc_conv0 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=(3,3), padding=1)
self.act0 = nn.ReLU()
self.bn0 = nn.BatchNorm2d(16)
self.pool0 = nn.MaxPool2d(kernel_size=(2,2))
self.enc_conv1 = nn.Conv2d(in_channels=16, out_channels=32, kernel_size=(3,3), padding=1)
self.act1 = nn.ReLU()
self.bn1 = nn.BatchNorm2d(32)
self.pool1 = nn.MaxPool2d(kernel_size=(2,2))
self.enc_conv2 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=(3,3), padding=1)
self.act2 = nn.ReLU()
self.bn2 = nn.BatchNorm2d(64)
self.pool2 = nn.MaxPool2d(kernel_size=(2,2))
self.enc_conv3 = nn.Conv2d(in_channels=64, out_channels=128, kernel_size=(3,3), padding=1)
self.act3 = nn.ReLU()
self.bn3 = nn.BatchNorm2d(128)
self.pool3 = nn.MaxPool2d(kernel_size=(2,2))
self.bottleneck_conv = nn.Conv2d(in_channels=128, out_channels=256, kernel_size=(3,3), padding=1)
self.upsample0 = nn.UpsamplingBilinear2d(scale_factor=2)
self.dec_conv0 = nn.Conv2d(in_channels=384, out_channels=128, kernel_size=(3,3), padding=1)
self.dec_act0 = nn.ReLU()
self.dec_bn0 = nn.BatchNorm2d(128)
self.upsample1 = nn.UpsamplingBilinear2d(scale_factor=2)
self.dec_conv1 = nn.Conv2d(in_channels=192, out_channels=64, kernel_size=(3,3), padding=1)
self.dec_act1 = nn.ReLU()
self.dec_bn1 = nn.BatchNorm2d(64)
self.upsample2 = nn.UpsamplingBilinear2d(scale_factor=2)
self.dec_conv2 = nn.Conv2d(in_channels=96, out_channels=32, kernel_size=(3,3), padding=1)
self.dec_act2 = nn.ReLU()
self.dec_bn2 = nn.BatchNorm2d(32)
self.upsample3 = nn.UpsamplingBilinear2d(scale_factor=2)
self.dec_conv3 = nn.Conv2d(in_channels=48, out_channels=1, kernel_size=(1,1))
self.sigmoid = nn.Sigmoid()
def forward(self, x):
e0 = self.pool0(self.bn0(self.act0(self.enc_conv0(x))))
e1 = self.pool1(self.bn1(self.act1(self.enc_conv1(e0))))
e2 = self.pool2(self.bn2(self.act2(self.enc_conv2(e1))))
e3 = self.pool3(self.bn3(self.act3(self.enc_conv3(e2))))
cat0 = self.bn0(self.act0(self.enc_conv0(x)))
cat1 = self.bn1(self.act1(self.enc_conv1(e0)))
cat2 = self.bn2(self.act2(self.enc_conv2(e1)))
cat3 = self.bn3(self.act3(self.enc_conv3(e2)))
b = self.bottleneck_conv(e3)
d0 = self.dec_bn0(self.dec_act0(self.dec_conv0(torch.cat((self.upsample0(b), cat3), dim=1))))
d1 = self.dec_bn1(self.dec_act1(self.dec_conv1(torch.cat((self.upsample1(d0), cat2), dim=1))))
d2 = self.dec_bn2(self.dec_act2(self.dec_conv2(torch.cat((self.upsample2(d1), cat1), dim=1))))
d3 = self.sigmoid(self.dec_conv3(torch.cat((self.upsample3(d2), cat0), dim=1)))
return d3model = UNet().to(device)
optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=weight_decay)
loss_fn = nn.BCELoss()train(dataloaders, model, loss_fn, optimizer, epochs, device)model.eval()
predictions = []
image_mask = []
plots = 5
images, masks = test_dataset[0], test_dataset[1]
for i, (img, mask) in enumerate(zip(images, masks)):
if i == plots:
break
img = img.to(device).unsqueeze(0)
predictions.append((model(img).detach().cpu()[0] > 0.5).float())
image_mask.append(mask)
plotn(plots, (predictions, image_mask), only_mask=True)حذفنا المخرجات وعدّادات التشغيل والودجات والمحتوى النشط وقت الاستيراد. شغّل الدفاتر بس في بيئة خارجية تثق فيها.
سجّل تطبيقك
التسجيل اختياري، يفيدك تتذكر وش طبّقت، ولا يمنع إكمال الدورة.