512ebd13b2
- Introduced `protocol.py` for shared data models used in server/client communication, including request and response schemas for registration, job submission, and status updates. - Implemented `server.py` to manage a SQLite job queue and client registry, handling job polling, status updates, and job completion. - Created a cheat sheet for server usage, detailing commands for starting the server, submitting jobs, and monitoring clients. - Added several experiment configuration files for various training setups, including geometry vector injections and baseline ensembles.
224 lines
7.0 KiB
Python
224 lines
7.0 KiB
Python
"""backbones — backbone registry and builder for v4 image towers."""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Callable, Dict, List
|
|
|
|
import torch
|
|
from torch import nn
|
|
from torchvision import models, transforms
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
|
|
REFUGELIKE_BACKBONE_PATH = _REPO_ROOT / "models/v2/refuge/refugelike_backbone.pt"
|
|
REFUGE_DENSENET_PATH = _REPO_ROOT / "models/refuge/classifier/refuge_densenet_backbone.pt"
|
|
REFUGE_EFFICIENT_B0_PATH = _REPO_ROOT / "models/refuge/classifier/refuge_efficient_b0_backbone.pt"
|
|
REFUGE_EFFICIENT_B7_PATH = _REPO_ROOT / "models/refuge/classifier/refuge_efficient_b7_backbone.pt"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BackboneSpec:
|
|
ctor: Callable
|
|
weights_default: object
|
|
strip: Callable[[nn.Module], tuple]
|
|
blocks: Callable[[nn.Module], List[nn.Module]]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Strip helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _strip_efficientnet(m: models.EfficientNet):
|
|
out_dim = m.classifier[1].in_features
|
|
m.classifier = nn.Identity()
|
|
return out_dim, m
|
|
|
|
def _strip_resnet(m: models.ResNet):
|
|
out_dim = m.fc.in_features
|
|
m.fc = nn.Identity()
|
|
return out_dim, m
|
|
|
|
def _strip_densenet(m: models.DenseNet):
|
|
out_dim = m.classifier.in_features
|
|
m.classifier = nn.Identity()
|
|
return out_dim, m
|
|
|
|
def _strip_vgg(m: models.VGG):
|
|
out_dim = m.classifier[0].in_features
|
|
m.classifier = nn.Identity()
|
|
return out_dim, m
|
|
|
|
def _strip_mobilenet_v2(m: models.MobileNetV2):
|
|
out_dim = m.classifier[1].in_features
|
|
m.classifier = nn.Identity()
|
|
return out_dim, m
|
|
|
|
def _strip_inception_v3(m: models.Inception3):
|
|
out_dim = m.fc.in_features
|
|
m.fc = nn.Identity()
|
|
m.aux_logits = False
|
|
m.AuxLogits = None
|
|
return out_dim, m
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Block splitters for ratio-based freezing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _blocks_efficientnet(m: models.EfficientNet):
|
|
return list(m.features)
|
|
|
|
def _blocks_resnet(m: models.ResNet):
|
|
stem = nn.Sequential(m.conv1, m.bn1, m.relu, m.maxpool)
|
|
return [stem, m.layer1, m.layer2, m.layer3, m.layer4]
|
|
|
|
def _blocks_densenet(m: models.DenseNet):
|
|
f = m.features
|
|
stem = nn.Sequential(f.conv0, f.norm0, f.relu0, f.pool0)
|
|
return [stem, f.denseblock1, f.transition1, f.denseblock2, f.transition2,
|
|
f.denseblock3, f.transition3, f.denseblock4, f.norm5]
|
|
|
|
def _blocks_vgg(m: models.VGG):
|
|
stages, cur = [], []
|
|
for mod in m.features:
|
|
cur.append(mod)
|
|
if isinstance(mod, nn.MaxPool2d):
|
|
stages.append(nn.Sequential(*cur)); cur = []
|
|
if cur:
|
|
stages.append(nn.Sequential(*cur))
|
|
return stages
|
|
|
|
def _blocks_mobilenet_v2(m: models.MobileNetV2):
|
|
return list(m.features)
|
|
|
|
def _blocks_inception_v3(m: models.Inception3):
|
|
return [child for name, child in m.named_children()
|
|
if name not in ("fc", "AuxLogits")]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Registry
|
|
# ---------------------------------------------------------------------------
|
|
|
|
BACKBONES: Dict[str, BackboneSpec] = {
|
|
"efficientnet_b0": BackboneSpec(
|
|
ctor=models.efficientnet_b0,
|
|
weights_default=models.EfficientNet_B0_Weights.DEFAULT,
|
|
strip=_strip_efficientnet,
|
|
blocks=_blocks_efficientnet,
|
|
),
|
|
"resnet18": BackboneSpec(
|
|
ctor=models.resnet18,
|
|
weights_default=models.ResNet18_Weights.DEFAULT,
|
|
strip=_strip_resnet,
|
|
blocks=_blocks_resnet,
|
|
),
|
|
"resnet50": BackboneSpec(
|
|
ctor=models.resnet50,
|
|
weights_default=models.ResNet50_Weights.DEFAULT,
|
|
strip=_strip_resnet,
|
|
blocks=_blocks_resnet,
|
|
),
|
|
"densenet121": BackboneSpec(
|
|
ctor=models.densenet121,
|
|
weights_default=models.DenseNet121_Weights.DEFAULT,
|
|
strip=_strip_densenet,
|
|
blocks=_blocks_densenet,
|
|
),
|
|
"vgg16": BackboneSpec(
|
|
ctor=models.vgg16,
|
|
weights_default=models.VGG16_Weights.DEFAULT,
|
|
strip=_strip_vgg,
|
|
blocks=_blocks_vgg,
|
|
),
|
|
"mobilenet_v2": BackboneSpec(
|
|
ctor=models.mobilenet_v2,
|
|
weights_default=models.MobileNet_V2_Weights.DEFAULT,
|
|
strip=_strip_mobilenet_v2,
|
|
blocks=_blocks_mobilenet_v2,
|
|
),
|
|
"inception_v3": BackboneSpec(
|
|
ctor=models.inception_v3,
|
|
weights_default=models.Inception_V3_Weights.DEFAULT,
|
|
strip=_strip_inception_v3,
|
|
blocks=_blocks_inception_v3,
|
|
),
|
|
"refugelike": BackboneSpec(
|
|
ctor=models.resnet50,
|
|
weights_default=None,
|
|
strip=_strip_resnet,
|
|
blocks=_blocks_resnet,
|
|
),
|
|
"refuge_densenet": BackboneSpec(
|
|
ctor=models.densenet121,
|
|
weights_default=None,
|
|
strip=_strip_densenet,
|
|
blocks=_blocks_densenet,
|
|
),
|
|
"refuge_efficient_b0": BackboneSpec(
|
|
ctor=models.efficientnet_b0,
|
|
weights_default=None,
|
|
strip=_strip_efficientnet,
|
|
blocks=_blocks_efficientnet,
|
|
),
|
|
"refuge_efficient_b7": BackboneSpec(
|
|
ctor=models.efficientnet_b7,
|
|
weights_default=None,
|
|
strip=_strip_efficientnet,
|
|
blocks=_blocks_efficientnet,
|
|
),
|
|
}
|
|
|
|
|
|
def list_names() -> List[str]:
|
|
return list(BACKBONES.keys())
|
|
|
|
|
|
def load_backbone_weights(key: str, model: nn.Module) -> None:
|
|
paths = {
|
|
"refugelike": REFUGELIKE_BACKBONE_PATH,
|
|
"refuge_densenet": REFUGE_DENSENET_PATH,
|
|
"refuge_efficient_b0": REFUGE_EFFICIENT_B0_PATH,
|
|
"refuge_efficient_b7": REFUGE_EFFICIENT_B7_PATH,
|
|
}
|
|
path = paths.get(key)
|
|
if path is None:
|
|
return
|
|
if not path.exists():
|
|
raise FileNotFoundError(
|
|
f"Custom backbone weights not found at {path}. "
|
|
"Export them via refuge_build.py --export-backbone first."
|
|
)
|
|
state = torch.load(path, map_location="cpu")
|
|
model.load_state_dict(state, strict=False)
|
|
|
|
|
|
def build_backbone(name: str, freeze_ratio: float = 0.0) -> tuple[nn.Module, int, list]:
|
|
"""Instantiate a backbone, strip its classifier head, apply freeze ratio.
|
|
|
|
Returns (model, out_dim, blocks) where blocks is the ordered list of
|
|
freezable units — callers use it to dynamically adjust freeze_ratio later.
|
|
"""
|
|
key = (name or "").lower()
|
|
if key not in BACKBONES:
|
|
raise ValueError(f"Unknown backbone '{name}'. Available: {list_names()}")
|
|
|
|
spec = BACKBONES[key]
|
|
if spec.weights_default is not None:
|
|
m = spec.ctor(weights=spec.weights_default)
|
|
else:
|
|
m = spec.ctor(weights=None)
|
|
out_dim, m = spec.strip(m)
|
|
load_backbone_weights(key, m)
|
|
|
|
blocks = spec.blocks(m)
|
|
fr = max(0.0, min(1.0, float(freeze_ratio)))
|
|
n_freeze = int(math.floor(len(blocks) * fr))
|
|
for b in blocks[:n_freeze]:
|
|
for p in b.parameters():
|
|
p.requires_grad = False
|
|
|
|
return m, out_dim, blocks
|