Tri-Netra-AI / src /segmentation_torch.py
anannyavyas1's picture
Upload folder using huggingface_hub (part 2)
3366f95 verified
Raw
History Blame Contribute Delete
6.6 kB
"""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',
]