تجزئة الصور: UNet وDeepLab
“تصنيف كل بكسل — أقنعة دلالية وحدود المثيلات والفهم الشامل”
التصنيف على مستوى البكسل — التجزئة الدلالية ومقابل الحالات في UNet وDeepLab وخسارة Dice لعدم توازن الفئات وتطبيقات التصوير الطبي والقيادة الذاتية.
المتطلبات الأساسية
المفاهيم المغطاة
∑الصيغ الرئيسية
خسارة Dice
تقيس التداخل بين الأقنعة المتنبأة والحقيقية — مقاومة لعدم توازن الفئات
mIoU
متوسط التقاطع على الاتحاد لكل فئة — مقياس التجزئة القياسي
اتصال التخطي
الاتصال المتبقي في UNet يدمج ميزات المشفّر مع ميزات المفكّك — يسترد التفاصيل المكانية المفقودة خلال التقليص
▶محاكاة تفاعلية
التجزئة: الفهم على مستوى البكسل
يُعطي الكشف عن الأجسام صناديق حدودية — تقريبات خشنة. تُعطي التجزئة أقنعة دقيقة على مستوى البكسل. يهم هذا لـ: التصوير الطبي (تحديد حدود ورم بدقة لتخطيط العلاج الإشعاعي)، القيادة الذاتية (التمييز بين السطح القابل للقيادة والرصيف عند كل بكسل)، صور الأقمار الاصطناعية (حساب مساحة المحاصيل بدقة م²)، وضع البورتريه (فصل الشخص عن الخلفية). ثلاثة مستويات: **التجزئة الدلالية** — وضع علامة على كل بكسل بفئة. **تجزئة المثيل** — كشف وإخفاء مثيلات أجسام فردية (Mask R-CNN). **التجزئة الشاملة** — مدمجة: أشياء (مثيلات) + مواد (مناطق غير محددة).
يستغرق الطبيب الشعاعي تحديد حدود الورم يدوياً 30-60 دقيقة لكل مسح. نموذج الذكاء الاصطناعي يفعل ذلك في < 1 ثانية — يتحول عنق الزجاجة إلى التحقق من المخرجات لا إنتاجها.
هندسة المشفر-المفكك (UNet)
UNet هو بنية التجزئة النموذجية. المشفّر (المسار المنقبض) هو شبكة CNN تُقلّل خريطة الميزات تدريجياً — تتعلم 'ماذا' في الصورة وتفقد 'أين'. المفكّك (المسار الموسّع) يُوسّع تدريجياً إلى الدقة الأصلية باستخدام التلافيف المنقولة — يسترد 'أين'. الابتكار الرئيسي: اتصالات التخطي التي تدمج مباشرةً خرائط ميزات المشفّر مع نظيراتها في المفكّك. تُتيح هذه الاتصالات للنموذج الجمع بين الميزات الدلالية عالية المستوى مع التفاصيل المكانية منخفضة المستوى — أساسية للحدود الدقيقة والواضحة.
صُمِّمت UNet للتجزئة الطبية الحيوية عام 2015 مع صور تدريب قليلة جداً (~30). كفاءة البيانات تأتي من اتصالات التخطي وزيادة البيانات المكثفة.
التجزئة مع torchvision وAlbumentations
import torch import torch.nn as nn import torchvision.models as models from torchvision.models.segmentation import DeepLabV3_ResNet101_Weights class="tok-comment"># ── class="tok-num">1. Pretrained semantic segmentation (DeepLabV3) ─────────────────────────── model = models.segmentation.deeplabv3_resnet101( weights=DeepLabV3_ResNet101_Weights.DEFAULT ) model.eval() class="tok-comment"># Inference from PIL import Image import torchvision.transforms.functional as F import numpy as np img = Image.open(class="tok-str">"street.jpg").convert(class="tok-str">"RGB") img_t = F.to_tensor(img).unsqueeze(class="tok-num">0) class="tok-comment"># (class="tok-num">1, class="tok-num">3, H, W) img_t = F.normalize(img_t, mean=[class="tok-num">0.485,class="tok-num">0.456,class="tok-num">0.406], std=[class="tok-num">0.229,class="tok-num">0.224,class="tok-num">0.225]) with torch.no_grad(): output = model(img_t)[class="tok-str">"out"] class="tok-comment"># (class="tok-num">1, class="tok-num">21, H, W) — class="tok-num">21 PASCAL VOC classes pred_mask = output.argmax(dim=class="tok-num">1)[class="tok-num">0] class="tok-comment"># (H, W) class labels class="tok-comment"># ── class="tok-num">2. Minimal UNet ──────────────────────────────────────────────────────────── class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv = nn.Sequential( nn.Conv2d(in_ch, out_ch, class="tok-num">3, padding=class="tok-num">1, bias=False), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, class="tok-num">3, padding=class="tok-num">1, bias=False), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_channels=class="tok-num">3, n_classes=class="tok-num">2, base_channels=class="tok-num">64): super().__init__() bc = base_channels class="tok-comment"># Encoder self.enc1 = DoubleConv(in_channels, bc) self.enc2 = DoubleConv(bc, bc*class="tok-num">2) self.enc3 = DoubleConv(bc*class="tok-num">2, bc*class="tok-num">4) self.pool = nn.MaxPool2d(class="tok-num">2) class="tok-comment"># Bottleneck self.bottleneck = DoubleConv(bc*class="tok-num">4, bc*class="tok-num">8) class="tok-comment"># Decoder self.up3 = nn.ConvTranspose2d(bc*class="tok-num">8, bc*class="tok-num">4, class="tok-num">2, stride=class="tok-num">2) self.dec3 = DoubleConv(bc*class="tok-num">8, bc*class="tok-num">4) class="tok-comment"># bc*class="tok-num">8 because of skip connection self.up2 = nn.ConvTranspose2d(bc*class="tok-num">4, bc*class="tok-num">2, class="tok-num">2, stride=class="tok-num">2) self.dec2 = DoubleConv(bc*class="tok-num">4, bc*class="tok-num">2) self.up1 = nn.ConvTranspose2d(bc*class="tok-num">2, bc, class="tok-num">2, stride=class="tok-num">2) self.dec1 = DoubleConv(bc*class="tok-num">2, bc) class="tok-comment"># Output self.out = nn.Conv2d(bc, n_classes, class="tok-num">1) def forward(self, x): e1 = self.enc1(x) e2 = self.enc2(self.pool(e1)) e3 = self.enc3(self.pool(e2)) b = self.bottleneck(self.pool(e3)) d3 = self.dec3(torch.cat([self.up3(b), e3], dim=class="tok-num">1)) d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=class="tok-num">1)) d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=class="tok-num">1)) return self.out(d1) unet = UNet(n_classes=class="tok-num">2) x = torch.randn(class="tok-num">2, class="tok-num">3, class="tok-num">256, class="tok-num">256) out = unet(x) print(fclass="tok-str">"UNet output: {out.shape}") class="tok-comment"># (class="tok-num">2, class="tok-num">2, class="tok-num">256, class="tok-num">256) class="tok-comment"># ── class="tok-num">3. Dice loss ────────────────────────────────────────────────────────────── def dice_loss(pred, target, eps=class="tok-num">1e-6): class="tok-str">"""pred: (B, C, H, W) softmax, target: (B, H, W) long""" pred_soft = torch.softmax(pred, dim=class="tok-num">1) target_oh = torch.zeros_like(pred_soft) target_oh.scatter_(class="tok-num">1, target.unsqueeze(class="tok-num">1), class="tok-num">1) inter = (pred_soft * target_oh).sum(dim=(class="tok-num">2,class="tok-num">3)) union = pred_soft.sum(dim=(class="tok-num">2,class="tok-num">3)) + target_oh.sum(dim=(class="tok-num">2,class="tok-num">3)) return class="tok-num">1 - (class="tok-num">2*inter + eps) / (union + eps) pred = torch.randn(class="tok-num">2, class="tok-num">2, class="tok-num">64, class="tok-num">64) target = torch.randint(class="tok-num">0, class="tok-num">2, (class="tok-num">2, class="tok-num">64, class="tok-num">64)) loss = dice_loss(pred, target).mean() print(fclass="tok-str">"Dice loss: {loss.item():.4f}")
عدم توازن الفئات يدمر نماذج التجزئة
في معظم مهام التجزئة تهيمن فئة الخلفية — قد تكون مشهد القيادة 95% سماء+طريق و5% مشاة. تعامل الإنتروبيا المتقاطعة جميع البكسلات بالتساوي فيتعلم النموذج التنبؤ بـ'خلفية' في كل مكان ويحصل على دقة 95% مع تفويت كل المشاة. الحلول: (1) الإنتروبيا المتقاطعة الموزونة — وزّن الخسارة عكسياً بتكرار الفئة. (2) خسارة Dice — غير حساسة طبيعياً للاختلال لأنها تقيس نسبة التقاطع. (3) Focal loss (من RetinaNet) — تُقلّل وزن البكسلات المصنّفة جيداً حتى يركز التدريب على الأمثلة الصعبة. من الناحية العملية الجمع بين الإنتروبيا المتقاطعة وخسارة Dice يعمل بشكل أفضل في التصوير الطبي.
تحقق دائماً من IoU لكل فئة في مقاييس التحقق — الدقة الإجمالية تُخفي الأداء السيئ على الفئات الصغيرة/النادرة التي غالباً ما تكون الأكثر أهمية.
?اختبار المعرفة
يتم حفظ التقدم في متصفحك — لا حاجة لحساب.