Spaces:
Runtime error
Runtime error
File size: 6,600 Bytes
3366f95 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | """PyTorch Attention U-Net for brain-tumor segmentation.
Why PyTorch and not the existing TF segmentation_models.py? TensorFlow 2.21 on
native Windows has no GPU support — only CPU — and there is no path back to
TF + CUDA on Windows without dropping to Python 3.10 / TF 2.10 / CUDA 11.2 /
cuDNN 8.1, or moving to WSL2. The user has an RTX 4060 with PyTorch 2.11 +
CUDA 12.6 already working, so we train the U-Net in PyTorch on GPU and keep
the existing TF classifier code unchanged.
The architecture mirrors the Attention U-Net described in Oktay et al. (MIDL
2018) and matches the TF reference in src/segmentation_models.py: four-level
encoder/decoder, attention gates on the skip connections, BatchNorm + ReLU +
Dropout, binary sigmoid output. Default base_filters=64 gives ~31M params; for
a small dataset on a single GPU base_filters=32 (~7M) is usually enough.
"""
from __future__ import annotations
import torch
import torch.nn as nn
import torch.nn.functional as F
class ConvBlock(nn.Module):
"""Two 3x3 conv -> BN -> ReLU with Dropout in the middle."""
def __init__(self, in_ch: int, out_ch: int, dropout: float = 0.2):
super().__init__()
self.block = nn.Sequential(
nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(out_ch),
nn.ReLU(inplace=True),
nn.Dropout2d(dropout),
nn.Conv2d(out_ch, out_ch, kernel_size=3, padding=1, bias=False),
nn.BatchNorm2d(out_ch),
nn.ReLU(inplace=True),
)
def forward(self, x):
return self.block(x)
class AttentionGate(nn.Module):
"""Additive attention gate (Oktay et al.) applied to a skip connection.
g (gating from decoder, upsampled to skip resolution) and x (skip from
encoder) are 1x1-projected to a shared dimension, summed, ReLU'd, then
1x1-projected to a single channel + sigmoid -> attention map alpha.
Output = alpha * x (element-wise gating on the skip connection).
"""
def __init__(self, x_ch: int, g_ch: int, inter_ch: int):
super().__init__()
self.theta_x = nn.Conv2d(x_ch, inter_ch, kernel_size=1, bias=False)
self.phi_g = nn.Conv2d(g_ch, inter_ch, kernel_size=1, bias=False)
self.psi = nn.Conv2d(inter_ch, 1, kernel_size=1, bias=True)
def forward(self, x, g):
if g.shape[-2:] != x.shape[-2:]:
g = F.interpolate(g, size=x.shape[-2:], mode='bilinear', align_corners=False)
attn = F.relu(self.theta_x(x) + self.phi_g(g), inplace=True)
attn = torch.sigmoid(self.psi(attn))
return x * attn
class AttentionUNet(nn.Module):
"""4-level Attention U-Net for binary segmentation. Input: (B, 3, H, W)."""
def __init__(self, in_channels: int = 3, base_filters: int = 32, dropout: float = 0.2):
super().__init__()
f = base_filters
# Encoder
self.enc1 = ConvBlock(in_channels, f, dropout)
self.enc2 = ConvBlock(f, f * 2, dropout)
self.enc3 = ConvBlock(f * 2, f * 4, dropout)
self.enc4 = ConvBlock(f * 4, f * 8, dropout)
self.pool = nn.MaxPool2d(2)
# Bottleneck
self.bottleneck = ConvBlock(f * 8, f * 16, dropout)
# Decoder
self.up4 = nn.ConvTranspose2d(f * 16, f * 8, kernel_size=2, stride=2)
self.att4 = AttentionGate(x_ch=f * 8, g_ch=f * 8, inter_ch=f * 4)
self.dec4 = ConvBlock(f * 16, f * 8, dropout)
self.up3 = nn.ConvTranspose2d(f * 8, f * 4, kernel_size=2, stride=2)
self.att3 = AttentionGate(x_ch=f * 4, g_ch=f * 4, inter_ch=f * 2)
self.dec3 = ConvBlock(f * 8, f * 4, dropout)
self.up2 = nn.ConvTranspose2d(f * 4, f * 2, kernel_size=2, stride=2)
self.att2 = AttentionGate(x_ch=f * 2, g_ch=f * 2, inter_ch=f)
self.dec2 = ConvBlock(f * 4, f * 2, dropout)
self.up1 = nn.ConvTranspose2d(f * 2, f, kernel_size=2, stride=2)
self.att1 = AttentionGate(x_ch=f, g_ch=f, inter_ch=max(f // 2, 1))
self.dec1 = ConvBlock(f * 2, f, dropout)
# Output: 1-channel logits; apply sigmoid outside (or use BCEWithLogits).
self.out_conv = nn.Conv2d(f, 1, kernel_size=1)
def forward(self, x):
e1 = self.enc1(x)
e2 = self.enc2(self.pool(e1))
e3 = self.enc3(self.pool(e2))
e4 = self.enc4(self.pool(e3))
b = self.bottleneck(self.pool(e4))
u4 = self.up4(b)
a4 = self.att4(e4, u4)
d4 = self.dec4(torch.cat([u4, a4], dim=1))
u3 = self.up3(d4)
a3 = self.att3(e3, u3)
d3 = self.dec3(torch.cat([u3, a3], dim=1))
u2 = self.up2(d3)
a2 = self.att2(e2, u2)
d2 = self.dec2(torch.cat([u2, a2], dim=1))
u1 = self.up1(d2)
a1 = self.att1(e1, u1)
d1 = self.dec1(torch.cat([u1, a1], dim=1))
return self.out_conv(d1) # logits
def dice_coefficient(probs: torch.Tensor, targets: torch.Tensor, smooth: float = 1e-6) -> torch.Tensor:
"""Per-batch Dice on binary probability maps. Returns scalar in [0,1]."""
probs = probs.contiguous().view(probs.size(0), -1)
targets = targets.contiguous().view(targets.size(0), -1)
inter = (probs * targets).sum(dim=1)
denom = probs.sum(dim=1) + targets.sum(dim=1)
return ((2.0 * inter + smooth) / (denom + smooth)).mean()
def iou_metric(probs: torch.Tensor, targets: torch.Tensor, smooth: float = 1e-6) -> torch.Tensor:
probs = probs.contiguous().view(probs.size(0), -1)
targets = targets.contiguous().view(targets.size(0), -1)
inter = (probs * targets).sum(dim=1)
union = probs.sum(dim=1) + targets.sum(dim=1) - inter
return ((inter + smooth) / (union + smooth)).mean()
def dice_loss(logits: torch.Tensor, targets: torch.Tensor, smooth: float = 1e-6) -> torch.Tensor:
probs = torch.sigmoid(logits)
return 1.0 - dice_coefficient(probs, targets, smooth)
def combined_dice_bce_loss(
logits: torch.Tensor,
targets: torch.Tensor,
dice_weight: float = 0.6,
) -> torch.Tensor:
"""Weighted Dice + BCE-with-logits, matching the TF combined_loss(0.6, 0.4)."""
bce = F.binary_cross_entropy_with_logits(logits, targets)
dl = dice_loss(logits, targets)
return dice_weight * dl + (1.0 - dice_weight) * bce
__all__ = [
'AttentionUNet',
'ConvBlock',
'AttentionGate',
'dice_coefficient',
'iou_metric',
'dice_loss',
'combined_dice_bce_loss',
]
|