er-resunet-setup-67 / model_def.py
zouinkhim's picture
Add PyTorch original: model_state_dict.pt + model_def.py + torch_source.json
c0aeb30 verified
Raw History Blame Contribute Delete
4.39 kB
"""Custom 3D residual U-Net (~10.7M params), valid-padding throughout.
Valid (unpadded) convolutions are used instead of same-padding because this setup is intended to run
under blockwise inference (daisy/gunpowder tiling adjacent crops across a large volume). Same-padding
convs synthesize zero-context at every block's border, which produces seam artifacts at block
boundaries; valid convs give every output voxel genuine surrounding context, matching the convention
already used by fly_organelles.model.StandardUnet and fish_trainer.model.Unet3d.
Verified (base_channels=40, factor=2, blocks_per_level=1): 10,702,493 params,
input (148,148,148) -> output (60,60,60).
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
def center_crop_to(x, target):
diffs = [x.shape[i] - target.shape[i] for i in (2, 3, 4)]
starts = [d // 2 for d in diffs]
return x[
:,
:,
starts[0] : starts[0] + target.shape[2],
starts[1] : starts[1] + target.shape[3],
starts[2] : starts[2] + target.shape[4],
]
class ResBlock(nn.Module):
"""Two valid 3x3x3 convs + InstanceNorm + ReLU, with a crop-and-add identity skip
(1x1x1 conv on the skip path when channel count changes; identity is center-cropped
to match the main path's spatially-shrunk output before the residual add)."""
def __init__(self, c_in, c_out):
super().__init__()
self.conv1 = nn.Conv3d(c_in, c_out, 3, padding=0)
self.norm1 = nn.InstanceNorm3d(c_out, affine=True)
self.conv2 = nn.Conv3d(c_out, c_out, 3, padding=0)
self.norm2 = nn.InstanceNorm3d(c_out, affine=True)
self.act = nn.ReLU(inplace=True)
self.skip = nn.Conv3d(c_in, c_out, 1) if c_in != c_out else nn.Identity()
def forward(self, x):
idn = self.skip(x)
h = self.act(self.norm1(self.conv1(x)))
h = self.norm2(self.conv2(h))
idn = center_crop_to(idn, h)
return self.act(h + idn)
class Down(nn.Module):
"""Exact 2x downsample (kernel==stride, divides evenly, no cropping needed)."""
def __init__(self, c_in, c_out):
super().__init__()
self.conv = nn.Conv3d(c_in, c_out, 2, stride=2)
def forward(self, x):
return self.conv(x)
class Up(nn.Module):
"""Non-learnable nearest-neighbor upsample (exact 2x) + 1x1x1 channel-reduce conv.
Deliberately non-learnable to match this codebase's own constant_upsample=True choice in
StandardUnet (avoids checkerboard/seam artifacts from learned transposed convs)."""
def __init__(self, c_in, c_out):
super().__init__()
self.reduce = nn.Conv3d(c_in, c_out, 1)
def forward(self, x):
x = F.interpolate(x, scale_factor=2, mode="nearest")
return self.reduce(x)
class ResUNet3DValid(nn.Module):
def __init__(self, out_channels, base_channels=40, factor=2, blocks_per_level=1, in_channels=1):
super().__init__()
C = [base_channels * (factor**i) for i in range(4)] # 3 down/up levels + bottleneck
self.C = C
self.stem = ResBlock(in_channels, C[0])
self.downs = nn.ModuleList()
self.enc_blocks = nn.ModuleList()
for i in range(3):
self.downs.append(Down(C[i], C[i + 1]))
self.enc_blocks.append(nn.ModuleList([ResBlock(C[i + 1], C[i + 1]) for _ in range(blocks_per_level)]))
self.ups = nn.ModuleList()
self.dec_blocks = nn.ModuleList()
for i in reversed(range(3)):
self.ups.append(Up(C[i + 1], C[i]))
blocks = [ResBlock(C[i] * 2, C[i])] + [ResBlock(C[i], C[i]) for _ in range(blocks_per_level - 1)]
self.dec_blocks.append(nn.ModuleList(blocks))
self.final_conv = nn.Conv3d(C[0], out_channels, 1)
def forward(self, input):
skips = []
x = self.stem(input)
skips.append(x)
for down, enc in zip(self.downs, self.enc_blocks):
x = down(x)
for blk in enc:
x = blk(x)
skips.append(x)
skips.pop() # bottleneck itself is not a skip target
for up, dec, skip in zip(self.ups, self.dec_blocks, reversed(skips)):
x = up(x)
skip_c = center_crop_to(skip, x)
x = torch.cat([x, skip_c], dim=1)
for blk in dec:
x = blk(x)
return self.final_conv(x)