Spaces:
Running
Running
| """ | |
| 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 |