LTF-v3.3 / mil_module /mil_core.py
MuskAI's picture
Translate legacy source documentation to English without changing executable code
0d8aa44 verified
Raw History Blame Contribute Delete
7.01 kB
"""Compose independent token augmentation, transformation and MIL pooling stages."""
from __future__ import annotations
import inspect
from typing import Any, Dict, Optional, Set, Tuple, Union
import torch
import torch.nn as nn
from .cross_shuffle import CrossTokenShuffle
from .mil_head_beta import MILHead
def _forward_param_names(module: nn.Module) -> Set[str]:
try:
return set(inspect.signature(module.forward).parameters.keys())
except (TypeError, ValueError):
return set()
class CrossShuffleStage(nn.Module):
"""Optional cross-batch token replacement; disabled stages act as the identity."""
def __init__(self, use: bool = True) -> None:
super().__init__()
self._use = use
self.op: Optional[CrossTokenShuffle]
if use:
self.op = CrossTokenShuffle()
else:
self.op = None
def forward(
self,
x: torch.Tensor,
y: Optional[torch.Tensor],
*,
apply: bool,
replace_ratio: float = 0.5,
with_replacement: bool = False,
detach_inserted: bool = True,
min_keep_original: int = 0,
) -> torch.Tensor:
if not self._use or not apply or self.op is None or y is None:
return x
if y.dim() != 1 or y.shape[0] != x.shape[0]:
raise ValueError(
f"y must be 1D [B], got {tuple(y.shape)} for B={x.shape[0]}"
)
return self.op(
x, y,
replace_ratio=replace_ratio,
with_replacement=with_replacement,
detach_inserted=detach_inserted,
min_keep_original=min_keep_original,
)
class TransStage(nn.Module):
"""Apply an optional token transformation with supported keyword arguments.
A missing core acts as the identity. Fixed-length masking can be passed
through key_padding_mask without dropping tokens.
"""
def __init__(self, core: Optional[nn.Module] = None) -> None:
super().__init__()
self.core = core
self._params = _forward_param_names(core) if core is not None else set()
def forward(
self,
x: torch.Tensor,
*,
p_shuffle: float = 0.0,
p_drop: float = 0.0,
key_padding_mask: Optional[torch.Tensor] = None,
**kwargs: Any,
) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
if self.core is None:
return x
if "p_shuffle" in self._params:
call_kw: Dict[str, Any] = {}
for name, val in (
("p_shuffle", p_shuffle),
("p_drop", p_drop),
("key_padding_mask", key_padding_mask),
):
if name in self._params:
call_kw[name] = val
for k, v in kwargs.items():
if k in self._params:
call_kw[k] = v
return self.core(x, **call_kw)
kw2: Dict[str, Any] = {}
if key_padding_mask is not None and "src_key_padding_mask" in self._params:
kw2["src_key_padding_mask"] = key_padding_mask
for k, v in kwargs.items():
if k in self._params:
kw2[k] = v
return self.core(x, **kw2) if kw2 else self.core(x)
class MILHeadStage(nn.Module):
"""Classify instances and pool tokens [B, N, D] using the wrapped MIL head."""
def __init__(self, **mil_kw: Any) -> None:
super().__init__()
self.head = MILHead(**mil_kw)
def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> Dict[str, Any]:
return self.head(x, mask=mask)
class MILCore(nn.Module):
"""Compose Cross-Shuffle, token transformation and MIL head stages.
Fixed-length bags can use attention masks instead of token dropping.
Optional masks and transformation arguments are forwarded to supported stages.
"""
def __init__(
self,
d_in: int,
num_classes: int = 2,
mil_head_pooling: str = "attn",
mil_head_kwargs: Optional[Dict[str, Any]] = None,
*,
use_cross_shuffle: bool = True,
trans: Optional[nn.Module] = None,
allow_trans_token_drop: bool = False,
) -> None:
super().__init__()
self.allow_trans_token_drop = bool(allow_trans_token_drop)
self.shuffle = CrossShuffleStage(use=use_cross_shuffle)
self.trans = TransStage(core=trans)
mil_kw: Dict[str, Any] = {"d_in": d_in, "num_classes": num_classes, "pooling": mil_head_pooling}
if mil_head_kwargs:
mil_kw.update(mil_head_kwargs)
self.head = MILHeadStage(**mil_kw)
def forward(
self,
tokens: torch.Tensor,
y: Optional[torch.Tensor] = None,
mask: Optional[torch.Tensor] = None,
*,
do_cross_shuffle: bool = False,
replace_ratio: float = 0.5,
with_replacement: bool = False,
detach_inserted: bool = True,
cross_shuffle_min_keep: int = 0,
p_shuffle: float = 0.0,
p_drop: float = 0.0,
trans_key_padding_mask: Optional[torch.Tensor] = None,
trans_extra: Optional[Dict[str, Any]] = None,
return_dict: bool = False,
):
x = self.shuffle(
tokens, y,
apply=do_cross_shuffle,
replace_ratio=replace_ratio,
with_replacement=with_replacement,
detach_inserted=detach_inserted,
min_keep_original=cross_shuffle_min_keep,
)
trans_p_drop = float(p_drop) if self.allow_trans_token_drop else 0.0
tex = dict(trans_extra or {})
trans_out = self.trans(
x,
p_shuffle=p_shuffle,
p_drop=trans_p_drop,
key_padding_mask=trans_key_padding_mask,
**tex,
)
if isinstance(trans_out, tuple) and len(trans_out) == 2:
x, trans_mil_mask = trans_out
if mask is not None:
raise ValueError(
"MILCore: `trans` returned (x, mil_valid); omit `mask` and use the trans-stage mask."
)
mask = trans_mil_mask
else:
x = trans_out # type: ignore[assignment]
if mask is not None and mask.shape != x.shape[:2]:
raise ValueError(
f"mask must be [B,N], got {tuple(mask.shape)} vs x {tuple(x.shape[:2])}"
)
mil_out = self.head(x, mask=mask)
if not return_dict:
return mil_out["bag_logits"]
return {"bag_logits": mil_out["bag_logits"], "mil_head": mil_out, "features": x}
def forward_logits(
self,
tokens: torch.Tensor,
y: Optional[torch.Tensor] = None,
mask: Optional[torch.Tensor] = None,
**kwargs: Any,
) -> torch.Tensor:
extra = dict(kwargs)
extra.pop("return_dict", None)
return self.forward(tokens, y, mask, return_dict=False, **extra)
CrossShuffleTransMIL = MILCore