arnavmishra4's picture
Upload 64 files
83913eb verified
Raw
History Blame Contribute Delete
6.85 kB
"""
model.py
--------
3D Residual U-Net with Attention Gates.
Mirrors the architecture used during training exactly so that checkpoints
load without key mismatches.
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
# ── Constants (must match training) ──────────────────────────────────────────
IN_CHANNELS = 4
NUM_CLASSES = 4
BASE_FILTERS = 24
# ── Helper ────────────────────────────────────────────────────────────────────
def group_norm(num_channels: int) -> nn.GroupNorm:
"""Returns the largest valid GroupNorm for the given channel count."""
for g in [32, 16, 8, 4, 2, 1]:
if num_channels % g == 0:
return nn.GroupNorm(g, num_channels)
return nn.GroupNorm(1, num_channels)
# ── Building blocks ───────────────────────────────────────────────────────────
class ConvBnRelu(nn.Sequential):
def __init__(self, in_c: int, out_c: int, stride: int = 1):
super().__init__(
nn.Conv3d(in_c, out_c, 3, stride=stride, padding=1, bias=False),
group_norm(out_c),
nn.ReLU(inplace=True),
)
class ResBlock(nn.Module):
def __init__(self, in_c: int, out_c: int, stride: int = 1):
super().__init__()
self.conv1 = ConvBnRelu(in_c, out_c, stride=stride)
self.conv2 = nn.Sequential(
nn.Conv3d(out_c, out_c, 3, padding=1, bias=False),
group_norm(out_c),
)
self.relu = nn.ReLU(inplace=True)
self.skip = (
nn.Sequential(
nn.Conv3d(in_c, out_c, 1, stride=stride, bias=False),
group_norm(out_c),
)
if (in_c != out_c or stride != 1)
else nn.Identity()
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.relu(self.conv2(self.conv1(x)) + self.skip(x))
class EncoderBlock(nn.Module):
def __init__(self, in_c: int, out_c: int):
super().__init__()
self.block1 = ResBlock(in_c, out_c)
self.block2 = ResBlock(out_c, out_c)
self.pool = nn.MaxPool3d(2)
def forward(self, x: torch.Tensor):
x = self.block1(x)
skip = self.block2(x)
return self.pool(skip), skip
class AttentionGate(nn.Module):
def __init__(self, f_g: int, f_x: int, f_int: int):
super().__init__()
self.W_g = nn.Sequential(
nn.Conv3d(f_g, f_int, 1, bias=False),
group_norm(f_int),
)
self.W_x = nn.Sequential(
nn.Conv3d(f_x, f_int, 1, bias=False),
group_norm(f_int),
)
self.psi = nn.Sequential(
nn.Conv3d(f_int, 1, 1, bias=False),
nn.GroupNorm(1, 1),
nn.Sigmoid(),
)
self.relu = nn.ReLU(inplace=True)
def forward(self, g: torch.Tensor, x: torch.Tensor) -> torch.Tensor:
g_up = F.interpolate(
self.W_g(g), size=x.shape[2:], mode="trilinear", align_corners=False
)
att = self.relu(g_up + self.W_x(x))
att = self.psi(att)
return x * att
class DecoderBlock(nn.Module):
def __init__(self, in_c: int, skip_c: int, out_c: int):
super().__init__()
self.up = nn.ConvTranspose3d(in_c, out_c, kernel_size=2, stride=2)
self.att = AttentionGate(f_g=out_c, f_x=skip_c, f_int=max(skip_c // 2, 8))
self.block1 = ResBlock(out_c + skip_c, out_c)
self.block2 = ResBlock(out_c, out_c)
def forward(self, x: torch.Tensor, skip: torch.Tensor) -> torch.Tensor:
x = self.up(x)
x = F.interpolate(
x, size=skip.shape[2:], mode="trilinear", align_corners=False
)
skip = self.att(g=x, x=skip)
x = self.block1(torch.cat([x, skip], dim=1))
return self.block2(x)
# ── Full model ────────────────────────────────────────────────────────────────
class ResUNet3D(nn.Module):
"""
3-D Residual U-Net with Attention Gates.
At inference time (model.eval()) the forward pass returns only the
full-resolution logit map (B, C, D, H, W).
At training time it additionally returns three deep-supervision heads
(aux4, aux3, aux2) upsampled to the same spatial size as the main output.
"""
def __init__(
self,
in_channels: int = IN_CHANNELS,
num_classes: int = NUM_CLASSES,
base_filters: int = BASE_FILTERS,
):
super().__init__()
f = base_filters
self.enc1 = EncoderBlock(in_channels, f)
self.enc2 = EncoderBlock(f, f * 2)
self.enc3 = EncoderBlock(f * 2, f * 4)
self.enc4 = EncoderBlock(f * 4, f * 8)
self.bridge = nn.Sequential(
ResBlock(f * 8, f * 16),
ResBlock(f * 16, f * 16),
ResBlock(f * 16, f * 16),
)
self.dec4 = DecoderBlock(f * 16, f * 8, f * 8)
self.dec3 = DecoderBlock(f * 8, f * 4, f * 4)
self.dec2 = DecoderBlock(f * 4, f * 2, f * 2)
self.dec1 = DecoderBlock(f * 2, f, f)
self.head = nn.Conv3d(f, num_classes, kernel_size=1)
# Deep-supervision heads (used only during training)
self.ds4 = nn.Conv3d(f * 8, num_classes, kernel_size=1)
self.ds3 = nn.Conv3d(f * 4, num_classes, kernel_size=1)
self.ds2 = nn.Conv3d(f * 2, num_classes, kernel_size=1)
def forward(self, x: torch.Tensor):
x, s1 = self.enc1(x)
x, s2 = self.enc2(x)
x, s3 = self.enc3(x)
x, s4 = self.enc4(x)
x = self.bridge(x)
d4 = self.dec4(x, s4)
d3 = self.dec3(d4, s3)
d2 = self.dec2(d3, s2)
d1 = self.dec1(d2, s1)
out = self.head(d1)
if self.training:
size = out.shape[2:]
aux4 = F.interpolate(self.ds4(d4), size=size,
mode="trilinear", align_corners=False)
aux3 = F.interpolate(self.ds3(d3), size=size,
mode="trilinear", align_corners=False)
aux2 = F.interpolate(self.ds2(d2), size=size,
mode="trilinear", align_corners=False)
return out, aux4, aux3, aux2
return out