فاز 4 · درس 7

بخش‌بندی معنایی تصویر با U-Net و PyTorch

نوع
ساختنی
زمان
حدود 75 دقیقه
زبان‌ها
Python
یک نقاشی دیجیتال از نمای تفکیک‌شده قطعات دقیق یک تصویر به لایه‌های پیکسلی رنگی با خطوط اتصالی خمیده شبیه حرف لاتین U
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفهوم
  4. تفاوت بخش‌بندی معنایی، نمونه‌ای و سراسری
  5. ساختار U-Net
  6. مقایسه‌ی نمونه‌برداری افزایشی ترانهاده با بیلینیر
  7. آنتروپی متقاطع روی شبکه‌ی پیکسلی
  8. تابع زیان دایس (Dice loss) و علت نیاز به آن
  9. معیارهای ارزیابی
  10. موازنه‌ی وضوح ورودی و حافظه
  11. پیاده‌سازی از صفر
  12. مرحله 1: بلوک رمزگذار
  13. مرحله 2: بلوک‌های کاهش و افزایش ابعاد (Down و Up)
  14. مرحله 3: ساخت U-Net کامل
  15. مرحله 4: توابع زیان
  16. مرحله 5: معیار IoU
  17. مرحله 6: مجموعه‌داده‌ی ساختگی برای اعتبارسنجی سراسری
  18. مرحله 7: چرخه‌ی آموزش
  19. استفاده از ابزارهای واقعی
  20. خروجی این درس
  21. تمرین‌ها
  22. اصطلاحات کلیدی
  23. برای مطالعه‌ی بیشتر

در بینایی ماشین کلاسیک، تشخیص شیء صرفاً با یک کادر مستطیلی دور هدف تعریف می‌شود؛ اما در کاربردهایی مثل پردازش تصاویر پزشکی، شناسایی مسیر خودروهای خودران یا نقشه‌برداری هوایی، دانستن مرز دقیق و پیکسل‌به‌پیکسل اشیاء ضرورت دارد. این همان نقطه‌ای است که بخش‌بندی معنایی (Semantic Segmentation) وارد میدان می‌شود تا به هر تک‌پیکسل تصویر برچسب دسته‌بندی اختصاص دهد.

معماری U-Net به دلیل طراحی متقارن رمزگذار-رمزگشا و اتصالات میان‌بر (Skip Connections)، استاندارد بی‌رقیب تفکیک پیکسلی تصاویر در حوزه‌های حساس است. با این معماری، شبکه بدون افت رزولوشن، اطلاعات مکانی ظریف و لبه‌ها را همراه با زمینه کلی تصویر بازسازی می‌کند.

در این درس، معماری U-Net را از صفر با PyTorch پیاده‌سازی می‌کنید، چالش رایج عدم توازن کلاس‌ها را با تابع زیان ترکیبی دایس (Dice Loss) حل می‌کنید و معیارهای ارزیابی استانداردی مانند IoU را برای سنجش کیفیت مرزها به کار می‌گیرید. این کدها بدون نیاز به تجهیزات گران‌قیمت یا سرویس‌های پردازش ابری، حتی روی سیستم‌های شخصی معمولی نیز قابل آموزش و آزمایش هستند.

بخش‌بندی (segmentation) یعنی دسته‌بندی در سطح تک‌تک پیکسل‌ها. معماری U-Net با جفت‌کردن یک رمزگذار (encoder) کاهنده‌ی ابعاد و یک رمزگشا (decoder) افزاینده‌ی ابعاد و برقراری اتصالات میان‌بر (skip connections) بین آن‌ها، این کار را ممکن می‌سازد.

اهداف یادگیری

  • تمایز بین بخش‌بندی معنایی (semantic)، نمونه‌ای (instance) و سراسری (panoptic) و انتخاب تسک مناسب برای مسئله‌ی مدنظر
  • ساخت U-Net از صفر در PyTorch با بلوک‌های رمزگذار، گلوگاه (bottleneck)، رمزگشا با کانولوشن‌های ترانهاده (transposed convolutions) و اتصالات میان‌بر
  • پیاده‌سازی آنتروپی متقاطع پیکسلی، تابع زیان دایس (Dice loss) و تابع زیان ترکیبی (که انتخاب پیش‌فرض فعلی در بخش‌بندی تصاویر پزشکی و صنعتی است)
  • خواندن و تحلیل معیارهای IoU و دایس به‌تفکیک هر کلاس و تشخیص اینکه امتیاز پایین ناشی از بازخوانی ضعیف اشیاء کوچک است، دقت در مرزها یا عدم‌توازن کلاس‌ها

صورت مسئله

دسته‌بندی (classification) برای هر تصویر یک برچسب خروجی می‌دهد. تشخیص شیء (detection) چند کادر محدود به‌ازای هر تصویر برمی‌گرداند. بخش‌بندی به ازای هر پیکسل یک برچسب تولید می‌کند. برای ورودی‌ای با ابعاد H x W، خروجی تنسوری با ابعاد H x W (در بخش‌بندی معنایی) یا H x W x N_instances (در بخش‌بندی نمونه‌ای) است؛ یعنی میلیون‌ها پیش‌بینی برای هر تصویر، نه فقط یک پیش‌بینی.

ساختار بخش‌بندی دلیل اصلی به‌کارگیری آن در تقریباً تمام محصولات بینایی ماشین با پیش‌بینی متراکم (dense-prediction) است: تصویربرداری پزشکی (ماسک تومورها)، رانندگی خودران (جاده، خطوط مسیر، موانع)، تصاویر ماهواره‌ای (ردپای ساختمان‌ها، مرز زمین‌های کشاورزی)، تحلیل اسناد (ناحیه‌بندی چیدمان متن) و رباتیک (نواحی قابل‌گرفتن با دست ربات). هیچ‌کدام از این تسک‌ها با کشیدن یک کادر دور شیء حل نمی‌شوند؛ آن‌ها دقیقاً به مرز و سایه‌نمای (silhouette) دقیق شیء نیاز دارند.

بیان مسئله‌ی معماری ساده است اما حل آن اصلاً ساده نیست: مدل باید بافتار کلی (global context) تصویر (اینکه صحنه از چه نوعی است) و جزئیات محلی پیکسل‌ها (دقیقاً کدام پیکسل جاده است و کدام پیاده‌رو) را هم‌زمان ببیند. یک CNN استاندارد ابعاد مکانی را فشرده می‌کند تا بافتار را به دست آورد، اما با این کار جزئیات ریز از دست می‌روند. U-Net همان معماری هوشمندانه‌ای بود که هر دو را هم‌زمان به دست آورد.

مفهوم

تفاوت بخش‌بندی معنایی، نمونه‌ای و سراسری

تصویر ورودیمعنایی(پیکسل ← کلاس)نمونه(پیکسل ← شناسه شیء،فقط کلاس‌های پیش‌زمینه)سراسرنما(تمام پیکسل‌ها ← کلاس + شناسه)
  • معنایی (Semantic): می‌گوید «این پیکسل جاده است، آن پیکسل خودرو است». اگر دو خودرو کنار هم باشند، در یک لکه‌ی یکپارچه merge می‌شوند.
  • نمونه‌ای (Instance): می‌گوید «این پیکسل خودروی شماره 3 است، آن پیکسل خودروی شماره 5 است». این رویکرد پس‌زمینه‌ی پیوسته («stuff» مانند آسمان، جاده، چمن) را نادیده می‌گیرد.
  • سراسری (Panoptic): هر دو را یکی می‌کند؛ به هر پیکسل یک برچسب کلاس اختصاص می‌یابد، هر نمونه از اشیاء یک شناسه‌ی منحصربه‌فرد می‌گیرد و هم پس‌زمینه‌ها و هم اشیاء مجزا بخش‌بندی می‌شوند.

این درس بخش‌بندی معنایی را پوشش می‌دهد. درس بعد (Mask R-CNN) به بخش‌بندی نمونه‌ای می‌پردازد.

ساختار U-Net

رمزگشا (انبساطی)تنگنارمزگذار (انقباضی)جهشجهشجهشجهش64H x W128H/2 x W/2256H/4 x W/4512H/8 x W/81024H/16 x W/16512H/8 x W/8256H/4 x W/4128H/2 x W/264H x Wکانولوشن 1x1کلاس‌ها

رمزگذار وضوح مکانی را چهار بار نصف می‌کند و تعداد کانال‌ها را دو برابر می‌سازد. رمزگشا این روند را برعکس می‌کند: وضوح مکانی را چهار بار دو برابر کرده و کانال‌ها را نصف می‌کند. اتصالات میان‌بر، ویژگی‌های متناظر از رمزگذار را با ویژگی‌های رمزگشا در همان وضوح الحاق (concatenate) می‌کنند. در نهایت، یک کانولوشن 1x1 در وضوح کامل، ابعاد 64 -> num_classes را نگاشت می‌دهد.

چرا اتصالات میان‌بر ضروری هستند؟ رمزگشا زمانی که می‌خواهد پیش‌بینی‌های سطح پیکسل را تولید کند، فقط نقشه‌های ویژگی کوچک را دیده است. بدون اتصالات میان‌بر، مدل نمی‌تواند لبه‌ها را با دقت مکان‌یابی کند، زیرا آن اطلاعات در مرحله‌ی رمزگذار فشرده شده و از دست رفته‌اند. اتصالات میان‌بر، نقشه‌های ویژگی با وضوح بالایی را که رمزگذار در مسیر نزولی محاسبه کرده است، مستقیماً به رمزگشا تحویل می‌دهند.

مقایسه‌ی نمونه‌برداری افزایشی ترانهاده با بیلینیر

رمزگشا باید ابعاد مکانی را افزایش دهد. برای این کار دو راه وجود دارد:

  • کانولوشن ترانهاده (Transposed convolution) (nn.ConvTranspose2d) — روشی یادگیرنده برای نمونه‌برداری افزایشی (upsample). این روش گزینه‌ی پیش‌فرض و تاریخی U-Net بود، اما اگر گام حرکت (stride) و اندازه‌ی کرنل بر هم بخش‌پذیر نباشند، ممکن است آرتیفکت‌های شطرنجی (checkerboard artifacts) تولید کند.
  • نمونه‌برداری افزایشی بیلینیر (Bilinear) + کانولوشن 3x3 — بزرگ‌نمایی نرم به همراه یک کانولوشن. آرتیفکت‌های کمتر و پارامترهای کمتری دارد و امروزه به گزینه‌ی پیش‌فرض مدرن تبدیل شده است.

در عمل از هر دو روش استفاده می‌شود، اما برای اولین پیاده‌سازی U-Net، استفاده از بیلینیر مطمئن‌تر است.

آنتروپی متقاطع روی شبکه‌ی پیکسلی

در بخش‌بندی معنایی با CC کلاس، خروجی مدل ابعاد (N, C, H, W) دارد. هدف (target) ابعادی به‌صورت (N, H, W) با شناسه‌های صحیح کلاس‌ها دارد. آنتروپی متقاطع دقیقاً مشابه حالت دسته‌بندی معمولی عمل می‌کند، فقط در تمام موقعیت‌های مکانی اعمال می‌شود:

Loss=mean(n,h,w)[−log⁡(softmax(logits[n,:,h,w])[target[n,h,w]])]\text{Loss} = \mathop{\mathrm{mean}}\limits_{(n, h, w)} \left[ -\log\left( \mathop{\mathrm{softmax}}(\text{logits}[n, :, h, w])[\text{target}[n, h, w]] \right) \right]

تابع F.cross_entropy در PyTorch به‌طور پیش‌فرض این شکل تنسور را مدیریت می‌کند و نیازی به تغییر ابعاد (reshape) نیست.

تابع زیان دایس (Dice loss) و علت نیاز به آن

آنتروپی متقاطع با تمام پیکسل‌ها یکسان رفتار می‌کند. این مسئله زمانی که یک کلاس بخش عمده‌ی تصویر را پر کرده باشد به مشکل می‌خورد (مثلاً در تصویربرداری پزشکی: 99% پس‌زمینه و 1% تومور). مدل می‌تواند با پیش‌بینی همه‌ی پیکسل‌ها به‌عنوان پس‌زمینه به دقت 99% برسد، درحالی‌که کاملاً بی‌فایده است.

تابع زیان دایس با بهینه‌سازی مستقیم میزان هم‌پوشانی ماسک پیش‌بینی‌شده و ماسک واقعی، این مشکل را حل می‌کند:

Dice(p,y)=2∑(p⋅y)∑p+∑y+ϵDiceloss=1−Dice\begin{aligned} \mathop{\mathrm{Dice}}(p, y) &= \frac{2 \sum (p \cdot y)}{\sum p + \sum y + \epsilon} \\ \text{Dice}_{\text{loss}} &= 1 - \text{Dice} \end{aligned}

که در آن p نقشه‌ی احتمالاتی (سافت‌مکس یا زیگموئید) برای یک کلاس و y ماسک باینری مقدار واقعی (ground-truth) است. تابع زیان (loss) تنها زمانی صفر می‌شود که هم‌پوشانی کامل باشد. از آنجا که ساختار این تابع مبتنی بر نسبت است، عدم‌توازن کلاس‌ها اهمیتی ندارد.

در کاربردهای عملی، از تابع زیان ترکیبی استفاده کنید:

L=Lcross_entropy+λ⋅Ldice(λ≈1)L = L_{\text{cross\_entropy}} + \lambda \cdot L_{\text{dice}} \qquad (\lambda \approx 1)

آنتروپی متقاطع در اوایل آموزش گرادیان‌های پایداری فراهم می‌کند؛ تابع زیان دایس هم مراحل پایانی آموزش را روی تطبیق دقیق شکل ماسک متمرکز می‌سازد. این ترکیب در پردازش تصاویر پزشکی به یک استاندارد پیش‌فرض تبدیل شده و روی هر مجموعه‌داده با عدم‌توازن کلاس‌ها شکست‌دادن آن بسیار دشوار است.

معیارهای ارزیابی

  • دقت پیکسلی (Pixel accuracy): درصد پیکسل‌هایی که درست پیش‌بینی شده‌اند. محاسبه‌ی ارزان و سریعی دارد، اما روی داده‌های نامتوازن، درست مثل معیار دقت در دسته‌بندی، گمراه‌کننده است.
  • شاخص IoU به‌ازای هر کلاس: نسبت اشتراک به اجتماع (Intersection over Union) برای ماسک هر کلاس؛ میانگین آن در تمام کلاس‌ها همان mIoU نامیده می‌شود.
  • دایس (F1 روی پیکسل‌ها): ساختاری شبیه به IoU دارد؛ Dice=2⋅IoU/(1+IoU)\text{Dice} = 2 \cdot \text{IoU} / (1 + \text{IoU}). جامعه‌ی مهندسی پزشکی معیار دایس را ترجیح می‌دهد و جامعه‌ی خودروهای خودران IoU را؛ این دو ارتباط یکنوایی با هم دارند.
  • امتیاز F1 در مرزها (Boundary F1): میزان نزدیکی مرزهای پیش‌بینی‌شده به مرزهای واقعی را می‌سنجد و حتی جابه‌جایی‌های کوچک را جریمه می‌کند. این معیار برای تسک‌های با دقت بالا مثل بازرسی نیمه‌رساناها بسیار حیاتی است.

همیشه IoU را به‌ازای تک‌تک کلاس‌ها گزارش کنید، نه صرفاً mIoU کلی؛ میانگین IoU ممکن است ضعف یک کلاس با امتیاز 15% را پشت امتیاز 85 درصدی نه کلاس دیگر پنهان کند.

موازنه‌ی وضوح ورودی و حافظه

رمزگذار U-Net وضوح تصویر را چهار بار نصف می‌کند، بنابراین ابعاد تصاویر ورودی فقط زمانی دقیقاً در اتصالات میان‌بر تراز می‌شوند که بر 16 بخش‌پذیر باشند. برای ابعاد دیگر به پدینگ (padding) یا تغییر اندازه در بلوک رمزگشا (مرحله 2) نیاز خواهید داشت. تصاویر پزشکی اغلب 512x512 یا 1024x1024 هستند و برش‌های رانندگی خودران معمولاً ابعاد 2048x1024 دارند. مصرف حافظه‌ی U-Net با رابطه‌ی H * W * C_max افزایش می‌یابد و در ابعاد 1024x1024 با 1024 کانال در گلوگاه، گذر رو به جلو همان ابتدا چند گیگابایت از حافظه‌ی گرافیکی را پر می‌کند.

دو راهکار استاندارد برای حل این مشکل:

  1. قطعه‌بندی ورودی (Tiling) — تصویر را به قطعات 256x256 با هم‌پوشانی تقسیم کنید، آن‌ها را پردازش کرده و دوباره به هم متصل کنید.
  2. جایگزینی گلوگاه با کانولوشن‌های اتساعی (dilated convolutions) که بدون کاهش وضوح مکانی، میدان دید (receptive field) مدل را بزرگ‌تر می‌کنند (مانند خانواده‌ی DeepLab).

برای ساخت اولین مدل، ورودی با ابعاد 256x256 همراه با U-Net با پایه‌ی 64 کانال به‌راحتی روی یک کارت گرافیک با 8 گیگابایت حافظه‌ی گرافیکی آموزش می‌بیند.

بخش‌بندی معناییبرچسب‌گذاری هر پیکسل، پخش شدن نواحی
SEMANTIC SEGMENTATIONSegmentation is classification at every pixel. The encoder compresses the image for context, the decoder upsamples back to full resolution, and a skip connection carries the fine spatial detail across so boundaries stay crisp. Watch each pixel claim its class as the mask floods outward.تصویر ورودیماسک به ازای پیکسلskip connection
بخش‌بندی در واقع دسته‌بندی در سطح تک‌تک پیکسل‌هاست. رمزگذار (encoder) تصویر را برای دریافت بافت و زمینه فشرده می‌کند، رمزگشا (decoder) دوباره ابعاد را به رزولوشن کامل بازمی‌گرداند، و اتصالات میان‌بر (skip connection) جزئیات مکانی دقیق را انتقال می‌دهند تا مرزها واضح بمانند. مشاهده کنید که با گسترش ماسک، هر پیکسل کلاس خود را مشخص می‌کند.

پیاده‌سازی از صفر

مرحله 1: بلوک رمزگذار

دو لایه کانولوشن 3x3 همراه با نرمال‌سازی دسته‌ای (Batch Normalization) و تابع فعال‌سازی ReLU. کانولوشن اول تعداد کانال‌ها را تغییر می‌دهد و کانولوشن دوم همان تعداد را حفظ می‌کند.

import torch
import torch.nn as nn
import torch.nn.functional as F

class DoubleConv(nn.Module):
    def __init__(self, in_c, out_c):
        super().__init__()
        self.net = nn.Sequential(
            nn.Conv2d(in_c, out_c, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(out_c),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_c, out_c, kernel_size=3, padding=1, bias=False),
            nn.BatchNorm2d(out_c),
            nn.ReLU(inplace=True),
        )

    def forward(self, x):
        return self.net(x)

این بلوک در کل معماری بازاستفاده می‌شود. مقدار bias=False در نظر گرفته شده است، زیرا پارامتر بتای لایه‌ی نرمال‌سازی دسته‌ای کار بایاس را انجام می‌دهد.

مرحله 2: بلوک‌های کاهش و افزایش ابعاد (Down و Up)

class Down(nn.Module):
    def __init__(self, in_c, out_c):
        super().__init__()
        self.net = nn.Sequential(
            nn.MaxPool2d(2),
            DoubleConv(in_c, out_c),
        )

    def forward(self, x):
        return self.net(x)


class Up(nn.Module):
    def __init__(self, in_c, out_c):
        super().__init__()
        self.up = nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False)
        self.conv = DoubleConv(in_c, out_c)

    def forward(self, x, skip):
        x = self.up(x)
        if x.shape[-2:] != skip.shape[-2:]:
            x = F.interpolate(x, size=skip.shape[-2:], mode="bilinear", align_corners=False)
        x = torch.cat([skip, x], dim=1)
        return self.conv(x)

بررسی ابعاد مکانی با shape[-2:] برای ورودی‌هایی به کار می‌رود که ابعاد آن‌ها بر 16 بخش‌پذیر نیست؛ تابع ایمن F.interpolate تنسور را قبل از الحاق هم‌تراز می‌کند. اگر ابعاد کل تنسور مقایسه می‌شد، تفاوت در تعداد کانال‌ها نیز این بلوک را فعال می‌کرد؛ درحالی‌که تفاوت کانال باید خطای صریح تولید کند، نه اینکه بی‌صدا درون‌یابی شود.

مرحله 3: ساخت U-Net کامل

class UNet(nn.Module):
    def __init__(self, in_channels=3, num_classes=2, base=64):
        super().__init__()
        self.inc = DoubleConv(in_channels, base)
        self.d1 = Down(base, base * 2)
        self.d2 = Down(base * 2, base * 4)
        self.d3 = Down(base * 4, base * 8)
        self.d4 = Down(base * 8, base * 16)
        self.u1 = Up(base * 16 + base * 8, base * 8)
        self.u2 = Up(base * 8 + base * 4, base * 4)
        self.u3 = Up(base * 4 + base * 2, base * 2)
        self.u4 = Up(base * 2 + base, base)
        self.outc = nn.Conv2d(base, num_classes, kernel_size=1)

    def forward(self, x):
        x1 = self.inc(x)
        x2 = self.d1(x1)
        x3 = self.d2(x2)
        x4 = self.d3(x3)
        x5 = self.d4(x4)
        x = self.u1(x5, x4)
        x = self.u2(x, x3)
        x = self.u3(x, x2)
        x = self.u4(x, x1)
        return self.outc(x)

net = UNet(in_channels=3, num_classes=2, base=32)
x = torch.randn(1, 3, 256, 256)
print(f"output: {net(x).shape}")
print(f"params: {sum(p.numel() for p in net.parameters()):,}")

شکل خروجی به‌صورت (1, 2, 256, 256) است؛ یعنی اندازه‌ی مکانی دقیقاً با ورودی برابر بوده و تعداد کانال‌ها معادل num_classes است. این مدل با پارامتر base=32 حدود 7.7 میلیون پارامتر دارد.

مرحله 4: توابع زیان

def dice_loss(logits, targets, num_classes, eps=1e-6):
    probs = F.softmax(logits, dim=1)
    targets_one_hot = F.one_hot(targets, num_classes).permute(0, 3, 1, 2).float()
    dims = (0, 2, 3)
    intersection = (probs * targets_one_hot).sum(dim=dims)
    denom = probs.sum(dim=dims) + targets_one_hot.sum(dim=dims)
    dice = (2 * intersection + eps) / (denom + eps)
    return 1 - dice.mean()


def combined_loss(logits, targets, num_classes, lam=1.0):
    ce = F.cross_entropy(logits, targets)
    dc = dice_loss(logits, targets, num_classes)
    return ce + lam * dc, {"ce": ce.item(), "dice": dc.item()}

تابع زیان دایس ابتدا به‌ازای هر کلاس محاسبه شده و سپس میانگین‌گیری می‌شود (macro Dice). مقدار eps از تقسیم بر صفر در کلاس‌هایی که در بچ حضور ندارند جلوگیری می‌کند.

مرحله 5: معیار IoU

@torch.no_grad()
def iou_per_class(logits, targets, num_classes):
    preds = logits.argmax(dim=1)
    ious = torch.zeros(num_classes)
    for c in range(num_classes):
        pred_c = (preds == c)
        true_c = (targets == c)
        inter = (pred_c & true_c).sum().float()
        union = (pred_c | true_c).sum().float()
        ious[c] = (inter / union) if union > 0 else torch.tensor(float("nan"))
    return ious

یک بردار با طول C برمی‌گرداند. مقدار nan نشان‌دهنده‌ی کلاس‌هایی است که در بچ ورودی غایب بوده‌اند؛ در زمان محاسبه‌ی mIoU نباید روی این موارد میانگین بگیرید.

مرحله 6: مجموعه‌داده‌ی ساختگی برای اعتبارسنجی سراسری

اشکالی هندسی روی پس‌زمینه‌های رنگی تولید می‌کنیم تا شبکه مجبور شود فرم و هندسه‌ی اشیاء را یاد بگیرد، نه رنگ پیکسل‌ها را.

import numpy as np
from torch.utils.data import Dataset, DataLoader

def synthetic_segmentation(num_samples=200, size=64, seed=0):
    rng = np.random.default_rng(seed)
    images = np.zeros((num_samples, size, size, 3), dtype=np.float32)
    masks = np.zeros((num_samples, size, size), dtype=np.int64)
    for i in range(num_samples):
        bg = rng.uniform(0, 1, (3,))
        images[i] = bg
        masks[i] = 0
        num_shapes = rng.integers(1, 4)
        for _ in range(num_shapes):
            cls = int(rng.integers(1, 3))
            color = rng.uniform(0, 1, (3,))
            cx, cy = rng.integers(10, size - 10, size=2)
            r = int(rng.integers(4, 12))
            yy, xx = np.meshgrid(np.arange(size), np.arange(size), indexing="ij")
            if cls == 1:
                mask = (xx - cx) ** 2 + (yy - cy) ** 2 < r ** 2
            else:
                mask = (np.abs(xx - cx) < r) & (np.abs(yy - cy) < r)
            images[i][mask] = color
            masks[i][mask] = cls
        images[i] += rng.normal(0, 0.02, images[i].shape)
        images[i] = np.clip(images[i], 0, 1)
    return images, masks


class SegDataset(Dataset):
    def __init__(self, images, masks):
        self.images = images
        self.masks = masks

    def __len__(self):
        return len(self.images)

    def __getitem__(self, i):
        img = torch.from_numpy(self.images[i]).permute(2, 0, 1).float()
        mask = torch.from_numpy(self.masks[i]).long()
        return img, mask

سه کلاس داریم: پس‌زمینه (0)، دایره‌ها (1) و مربع‌ها (2). شبکه باید یاد بگیرد که این اشکال را از هم تشخیص دهد.

مرحله 7: چرخه‌ی آموزش

def train_one_epoch(model, loader, optimizer, device, num_classes):
    model.train()
    loss_sum, total = 0.0, 0
    iou_sum = torch.zeros(num_classes)
    for x, y in loader:
        x, y = x.to(device), y.to(device)
        logits = model(x)
        loss, _ = combined_loss(logits, y, num_classes)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        loss_sum += loss.item() * x.size(0)
        total += x.size(0)
        iou_sum += iou_per_class(logits, y, num_classes).nan_to_num(0)
    return loss_sum / total, iou_sum / len(loader)

این مدل را برای 10 تا 30 دور (epoch) روی مجموعه‌داده‌ی ساختگی اجرا کنید تا ببینید چگونه معیار mIoU برای کلاس‌های شکل هندسی به بالای 0.9 می‌رسد. توجه داشته باشید که تابع nan_to_num(0) کلاس‌های غایب در بچ را صفر فرض می‌کند؛ برای محاسبه‌ی دقیق IoU هر کلاس در زمان ارزیابی، بهتر است حضور کلاس‌ها را با ماسک مشخص کرده و به‌جای میانگین‌گیری در اینجا، از torch.nanmean در طول تمام بچ‌ها استفاده کنید.

استفاده از ابزارهای واقعی

در محیط‌های پروداکشن، کتابخانه‌ی segmentation_models_pytorch (یا همان smp) تمام معماری‌های استاندارد بخش‌بندی را در کنار ستون فقرات‌های (backbone) گوناگون torchvision و timm در اختیارتان می‌گذارد. پیاده‌سازی آن در سه خط انجام می‌شود:

import segmentation_models_pytorch as smp

model = smp.Unet(
    encoder_name="resnet34",
    encoder_weights="imagenet",
    in_channels=3,
    classes=3,
)

مدل‌های مدرن دیگری که در کارهای صنعتی ارزش دانستن دارند:

  • DeepLabV3+ کاهش ابعاد مبتنی بر max-pool را با کانولوشن‌های اتساعی جایگزین می‌کند تا وضوح گلوگاه حفظ شود؛ این کار استخراج سریع‌تر مرزها را در تصاویر ماهواره‌ای و رانندگی خودران ممکن می‌سازد.
  • SegFormer رمزگذار کانولوشنی را با یک ترنسفورمر سلسله‌مراتبی عوض می‌کند که در حال حاضر روی بسیاری از بنچمارک‌ها بهترین نتایج (SOTA) را ثبت کرده است.
  • Mask2Former / OneFormer بخش‌بندی‌های معنایی، نمونه‌ای و سراسری را در قالب یک معماری واحد یکپارچه می‌کنند.

هر سه مدل فوق جایگزین‌های مستقیمی در کتابخانه‌های smp یا transformers هستند و بدون تغییر در ساختار لودر داده اجرا می‌شوند.

خروجی این درس

این درس موارد زیر را تولید می‌کند:

  • فایل outputs/prompt-segmentation-task-picker.md — یک پرامپت مهندسی‌شده برای انتخاب مناسب‌ترین تسک از بین بخش‌بندی معنایی، نمونه‌ای یا سراسری و مشخص کردن معماری ایده‌آل برای یک مسئله‌ی خاص.
  • فایل outputs/skill-segmentation-mask-inspector.md — مهارتی برنامه‌نویسی برای گزارش توزیع کلاس‌ها، استخراج آمارهای ماسک پیش‌بینی‌شده و پیدا کردن کلاس‌هایی با پیش‌بینی ضعیف یا دارای مرزهای مبهم و تار.

تمرین‌ها

  1. (ساده) تابع زیان bce_dice_loss را برای یک تسک بخش‌بندی باینری (پیش‌زمینه در برابر پس‌زمینه) بنویسید. روی یک مجموعه‌داده‌ی ساختگی با دو کلاس بررسی و اثبات کنید در شرایطی که پیش‌زمینه تنها 5% از کل پیکسل‌ها را شامل می‌شود، تابع زیان ترکیبی بسیار سریع‌تر از BCE خالص همگرا می‌شود.
  2. (متوسط) بلوک نمونه‌برداری افزایشی مبتنی بر nn.Upsample + conv را با یک بلوک مبتنی بر nn.ConvTranspose2d عوض کنید. هر دو نسخه را روی مجموعه‌داده‌ی ساختگی آموزش دهید و مقدار mIoU آن‌ها را مقایسه کنید. ببینید در چه بخش‌هایی آرتیفکت‌های شطرنجی در مدل کانولوشن ترانهاده ظاهر می‌شوند.
  3. (سخت) یک مجموعه‌داده‌ی بخش‌بندی واقعی (مانند Oxford-IIIT Pets، بخشی از Cityscapes یا یک زیرمجموعه‌ی پزشکی) بردارید و مدل U-Net خود را طوری آموزش دهید که اختلاف آن با مدل مرجع smp.Unet کمتر از 2 امتیاز IoU شود. در گزارش خروجی، IoU هر کلاس را جداگانه ثبت کنید و مشخص کنید اضافه کردن دایس به تابع زیان، عملکرد کدام کلاس‌ها را بیشتر بهبود بخشیده است.

اصطلاحات کلیدی

اصطلاح در گفت‌وگوها معنای دقیق فنی
بخش‌بندی معنایی (Semantic segmentation) «برچسب‌زدن به تمام پیکسل‌ها» دسته‌بندی تک‌تک پیکسل‌ها در C کلاس؛ نمونه‌های مختلف از یک کلاس با یکدیگر merge می‌شوند
بخش‌بندی نمونه‌ای (Instance segmentation) «برچسب‌زدن به تمام اشیاء» جداسازی نمونه‌های مستقل و تفکیک‌پذیر از یک کلاس یکسان؛ فقط روی پیش‌زمینه تمرکز دارد
بخش‌بندی سراسری (Panoptic segmentation) «ترکیب معنایی و نمونه‌ای» همه‌ی پیکسل‌ها کلاس می‌گیرند؛ علاوه‌بر آن به هر نمونه از اشیاء یک شناسه‌ی منحصربه‌فرد تعلق می‌گیرد
اتصال میان‌بر (Skip connection) «پل ارتباطی U-Net» الحاق ویژگی‌های رمزگذار به ویژگی‌های هم‌وضوح در رمزگشا؛ جزئیات فرکانس‌بالا و دقیق مکانی را حفظ می‌کند
کانولوشن ترانهاده (Transposed conv) «کانولوشن معکوس یا Deconvolution» نمونه‌برداری افزایشی با پارامترهای یادگیرنده؛ ممکن است آرتیفکت‌های شطرنجی ایجاد کند
تابع زیان دایس (Dice loss) «تابع زیان هم‌پوشانی» رابطه‌ی 1 - 2
mIoU «میانگین شاخص اشتراک بر اجتماع» میانگین مقادیر IoU در تمام کلاس‌ها؛ معیار استاندارد بین‌المللی برای سنجش مدل‌های بخش‌بندی
امتیاز F1 در مرزها (Boundary F1) «دقت مرزی» امتیاز F1 که منحصراً روی پیکسل‌های مرزی ارزیابی می‌شود؛ برای تسک‌های فوق‌العاده حساس و دقیق حیاتی است

برای مطالعه‌ی بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسچرا معماری U-Net به اتصالات میان‌بر (skip connections) بین رمزگذار و رمزگشا نیاز دارد؟
  2. پیش از درسیک مسئله‌ی بخش‌بندی دارای 99٪ پیکسل پس‌زمینه و 1٪ پیکسل تومور است. شما مدل را با تابع آنتروپی متقاطع ساده آموزش می‌دهید و به دقت پیکسلی 99٪ می‌رسید. چه اتفاقی افتاده است؟
  3. پس از درسکدام نوع وظیفه، خودروهای مجزا از یک کلاس را از یکدیگر تفکیک می‌کند؟
  4. پس از درسشما در U-Net نمونه‌برداری صعودی دوخطی (bilinear upsample) + کانولوشن 3x3 را با یک `ConvTranspose2d` با `kernel_size=2` و `stride=2` جایگزین می‌کنید و ناهنجاری‌های شطرنجی در خروجی می‌بینید. دلیل آن چیست؟
  5. پس از درسشما در یک مسئله‌ی بخش‌بندی با 10 کلاس، مقدار `mIoU = 0.78` را گزارش می‌دهید. چرا باید IoU تفکیک‌شده برای هر کلاس را نیز منتشر کنید؟

سؤالات متداول

چرا در بخش‌بندی پیکسلی نباید فقط از Cross-Entropy استفاده کرد؟

در بخش‌بندی تصاویری مانند تومورهای پزشکی، کلاس هدف تنها بخش ناچیزی از کل پیکسل‌ها را شامل می‌شود. تابع آنتروپی متقاطع با تمام پیکسل‌ها یکسان برخورد می‌کند و مدل می‌تواند با پیش‌بینی تمام تصویر به‌عنوان پس‌زمینه دقت کاذب بالایی کسب کند؛ استفاده از Dice Loss این مشکل عدم توازن را جبران می‌کند.

نقش اتصالات میان‌بر (Skip Connections) در ساختار U-Net چیست؟

در طول مسیر رمزگذار، وضوح مکانی تصویر برای استخراج ویژگی‌های معنایی عمیق کاهش می‌یابد. اتصالات میان‌بر نقشه‌های ویژگی اولیه و با وضوح بالا را مستقیماً به رمزگشا منتقل می‌کنند تا مدل مکان لبه‌ها و جزئیات مکانی دقیق را گم نکند.

تفاوت نمونه‌برداری افزایشی ترانهاده با بیلینیر چیست؟

کانولوشن ترانهاده لایه‌ای یادگیرنده است اما ممکن است آرتیفکت‌های شطرنجی ایجاد کند؛ در مقابل، نمونه‌برداری بیلینیر همراه با کانولوشن 3x3 پارامترهای کمتری مصرف می‌کند، خروجی نرم‌تری دارد و امروزه انتخاب امن‌تر و استانداردتری است.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.