Feature Extraction
Transformers
Safetensors
jolia
medical
radiology
ct
3d
vision
foundation-model
self-supervised
custom_code
Instructions to use raidium/Jolia with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use raidium/Jolia with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("feature-extraction", model="raidium/Jolia", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("raidium/Jolia", trust_remote_code=True, dtype="auto") - Notebooks
- Google Colab
- Kaggle
File size: 18,419 Bytes
0129eb2 6858e35 0129eb2 6858e35 0129eb2 6858e35 0129eb2 6858e35 0129eb2 6858e35 0129eb2 6858e35 0129eb2 6858e35 0129eb2 6858e35 0129eb2 6858e35 0129eb2 6858e35 0129eb2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 | """Jolia: a self-contained Atlas vision backbone with named organ queries.
``JoliaModel`` is the public Hugging Face entry point. It wraps a vendored
``MultiModalAtlas`` 3D backbone, a per-scale bank of organ-query
cross-attention poolers, and a CLIP-style text-projection head used for
zero-shot classification.
Feature views:
* :meth:`forward` / ``__call__`` β the pooled global (CLS-equivalent) embedding.
* :meth:`encode_image` β L2-normalized image embedding in the shared CLIP space.
* :meth:`encode_text` β L2-normalized text embedding in the shared CLIP space
(input: pooled Qwen3 text features, output: 576-d).
* :meth:`zero_shot_logits` / :meth:`zero_shot` β image-vs-text similarity with
the trained temperature + bias.
* :meth:`encode_organs` β per-organ embeddings keyed by organ **name**.
* :meth:`extract_flat_feature` β the normalized ``[cls β organs]`` vector used
for linear probing.
Load it with::
from transformers import AutoModel
model = AutoModel.from_pretrained("raidium/Jolia", trust_remote_code=True).eval()
For zero-shot, pair it with the paired text encoder
(see :class:`text_encoder_jolia.JoliaTextEncoder`).
"""
from __future__ import annotations
import math
from dataclasses import dataclass
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import PreTrainedModel
from transformers.utils import ModelOutput
from .configuration_jolia import JoliaConfig
from .jolia_multimodal_atlas import MultiModalAtlas, MultiModalAtlasConfig
from .jolia_organ_query_attention import OrganQueryAttention
# HF's trust_remote_code loader copies only the *direct* relative imports of this
# entry module. The lines below force every transitively-vendored backbone file
# (and the text-encoder helper) into that copy set so the repo loads from the
# hub without missing modules.
from .jolia_atlas_encoders import ChestCTEmbed3D as _ensure_atlas_encoders # noqa: E402,F401
from .jolia_multimodal_msa import AtlasStage as _ensure_multimodal_msa # noqa: E402,F401
from .jolia_shim import BaseModel as _ensure_shim # noqa: E402,F401
# The text encoder is opt-in (loading Qwen3 is heavy); we still touch the
# module so it's bundled in the dynamic-module copy set for snapshot_download
# + sys.path.append users.
from .text_encoder_jolia import JoliaTextEncoder as _ensure_text_encoder # noqa: E402,F401
@dataclass
class JoliaOutput(ModelOutput):
"""Output of :meth:`JoliaModel.forward`.
Attributes:
pooler_output: ``(B, embed_dim)`` global embedding.
organ_queries: ``(B, num_organs, query_dim)`` per-organ embeddings, or
``None`` when ``output_organ_queries=False`` / no organ-query head.
"""
pooler_output: torch.FloatTensor | None = None
organ_queries: torch.FloatTensor | None = None
class JoliaModel(PreTrainedModel):
config_class = JoliaConfig
base_model_prefix = "jolia"
main_input_name = "image"
def __init__(self, config: JoliaConfig) -> None:
super().__init__(config)
atlas_config = MultiModalAtlasConfig(
embed_dim=config.embed_dim,
num_classes=0,
multiscale_feats=config.multiscale_feats,
atlas_config=config.atlas_config,
)
self.vision_model = MultiModalAtlas(atlas_config)
if config.has_queries:
self.organ_query_attn_scales: torch.nn.ModuleList | None = torch.nn.ModuleList(
OrganQueryAttention(
num_organs=config.num_organs,
query_dim=config.patch_embed_dim,
num_heads=config.num_heads,
)
for _ in range(config.num_scales)
)
else:
self.organ_query_attn_scales = None
# CLIP-style text head β small (~10 MB) projection from the paired text
# encoder's hidden size to the shared embedding space, plus the trained
# temperature (`logit_scale`) and additive `bias` used by `zero_shot_logits`.
if config.has_text_head:
self.text_projection: nn.Linear | None = nn.Linear(
config.text_embed_dim, config.embed_dim, bias=True
)
self.logit_scale = nn.Parameter(torch.zeros([]))
self.text_bias = nn.Parameter(torch.zeros([]))
else:
self.text_projection = None
self.register_parameter("logit_scale", None)
self.register_parameter("text_bias", None)
# ParallelOrganCLIP text head β used for organ-routed zero-shot (a text
# prompt compared against a *specific* organ-query embedding). Same
# shape as the global text head but trained against per-organ findings.
if config.has_text_head and config.has_queries:
self.organ_text_projection: nn.Linear | None = nn.Linear(
config.text_embed_dim, config.embed_dim, bias=True
)
self.organ_logit_scale = nn.Parameter(torch.zeros(config.num_organs))
self.organ_text_bias = nn.Parameter(torch.zeros(config.num_organs))
else:
self.organ_text_projection = None
self.register_parameter("organ_logit_scale", None)
self.register_parameter("organ_text_bias", None)
self.post_init()
# ------------------------------------------------------------------
# Capability / naming helpers
# ------------------------------------------------------------------
@property
def embed_dim(self) -> int:
return int(self.config.embed_dim)
@property
def query_dim(self) -> int:
return int(self.config.query_dim)
@property
def num_organs(self) -> int:
return int(self.config.num_organs)
@property
def has_queries(self) -> bool:
return self.organ_query_attn_scales is not None
@property
def organ_slot_names(self) -> list[str]:
"""Names for organ-query slots ``0 .. len-1`` (trailing slots unused)."""
return list(self.config.organ_slot_names)
@property
def has_text_head(self) -> bool:
return self.text_projection is not None
@property
def has_organ_text_head(self) -> bool:
return self.organ_text_projection is not None
@property
def text_encoder_id(self) -> str:
"""HuggingFace id of the paired text encoder (e.g. ``Qwen/Qwen3-Embedding-8B``)."""
return self.config.text_encoder_id
# ------------------------------------------------------------------
# Forward views
# ------------------------------------------------------------------
def forward(
self,
image: torch.Tensor,
output_organ_queries: bool = False,
return_dict: bool = True,
) -> JoliaOutput | tuple:
"""Return the pooled global embedding (and optionally organ queries)."""
if output_organ_queries:
cls, organ = self.forward_with_queries(image)
else:
cls, organ = self.vision_model(image), None
if not return_dict:
return (cls,) if organ is None else (cls, organ)
return JoliaOutput(pooler_output=cls, organ_queries=organ)
def forward_with_patch_tokens(self, image: torch.Tensor) -> tuple[torch.Tensor, list[torch.Tensor]]:
"""Return ``(cls, multi_scale_patch_tokens)`` from the Atlas backbone."""
cls, patch_scales = self.vision_model(image, with_patch_tokens=True)
return cls, patch_scales
def forward_with_queries(self, image: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""Return ``(cls, organ_queries)`` β ``(B, embed_dim)``, ``(B, num_organs, query_dim)``."""
self._require_queries()
cls, patch_scales = self.forward_with_patch_tokens(image)
scale_outputs = [
attn(tokens)[:, : self.num_organs, :]
for attn, tokens in zip(self.organ_query_attn_scales, patch_scales) # type: ignore[arg-type]
]
organ = torch.cat(scale_outputs, dim=-1)
if self.config.use_cls_residual:
organ = organ + cls.unsqueeze(1).expand(-1, self.num_organs, -1)
return cls, organ
def extract_flat_feature(self, image: torch.Tensor) -> torch.Tensor:
"""Normalized ``[F.normalize(cls) β F.normalize(organ).flatten(1)]`` feature."""
cls, organ = self.forward_with_queries(image)
cls_n = F.normalize(cls.float(), dim=-1, eps=1e-6)
organ_n = F.normalize(organ.float(), dim=-1, eps=1e-6)
return torch.cat([cls_n, organ_n.reshape(organ_n.size(0), -1)], dim=-1)
# ------------------------------------------------------------------
# Zero-shot CLIP β image / text embeddings in a shared space
# ------------------------------------------------------------------
def encode_image(self, image: torch.Tensor, normalize: bool = True) -> torch.Tensor:
"""Image embedding ready for zero-shot β ``(B, embed_dim)``.
The released checkpoint uses CLS-only (no vision projection) before
L2-normalization, matching ``MultimodalCLSZeroShotCLIP`` in the
training repo.
"""
cls = self.vision_model(image)
return F.normalize(cls.float(), dim=-1, eps=1e-6) if normalize else cls
def encode_text(self, text_features: torch.Tensor, normalize: bool = True) -> torch.Tensor:
"""Project pooled text features into the shared embedding space.
Args:
text_features: ``(N, text_embed_dim)`` last-token-pooled features
produced by the paired text encoder (use
:class:`text_encoder_jolia.JoliaTextEncoder` to obtain them).
normalize: L2-normalize the projected vectors (default).
"""
self._require_text_head()
projected = self.text_projection(text_features.float()) # type: ignore[misc]
return F.normalize(projected, dim=-1, eps=1e-6) if normalize else projected
def zero_shot_logits(self, image_emb: torch.Tensor, text_emb: torch.Tensor) -> torch.Tensor:
"""Calibrated image-vs-text logits with the trained temperature + bias.
``image_emb`` and ``text_emb`` must come from :meth:`encode_image` and
:meth:`encode_text` (both L2-normalized). The clamp matches the
training-time ``max_logit_scale=ln(100)``.
"""
self._require_text_head()
scale = torch.clamp(self.logit_scale.float(), max=math.log(100.0)).exp()
return image_emb @ text_emb.t() * scale + self.text_bias
@torch.no_grad()
def zero_shot(
self,
image: torch.Tensor,
text_features: torch.Tensor,
calibrated: bool = True,
) -> torch.Tensor:
"""One-call zero-shot scoring: image volume + pooled text -> ``(B, N)``.
Args:
image: Preprocessed volume ``(B, 11, 192, 192, 192)``.
text_features: Pooled text features ``(N, text_embed_dim)`` from
the paired text encoder.
calibrated: When ``True`` (default), returns the trained CLIP
logits ``cosine * exp(logit_scale) + bias`` β same output as
``MultimodalCLSZeroShotCLIP.get_logits_per_image`` in rarm,
and what you want for ``torch.sigmoid(...)`` /
``torch.softmax(...)``. Set ``calibrated=False`` for raw
cosine similarity in ``[-1, 1]`` (handy when you only need
ranking and don't want the bias offset).
"""
img = self.encode_image(image)
txt = self.encode_text(text_features)
if calibrated:
return self.zero_shot_logits(img, txt)
return img @ txt.t()
def _require_text_head(self) -> None:
if self.text_projection is None:
raise RuntimeError(
"This Jolia checkpoint has no text head β zero-shot is unavailable. "
"Re-export the model with text_embed_dim>0 from a CLIPObjective checkpoint."
)
# ------------------------------------------------------------------
# Per-organ (query-routed) zero-shot
# ------------------------------------------------------------------
def encode_organ_text(self, text_features: torch.Tensor, normalize: bool = True) -> torch.Tensor:
"""Project pooled text features through the ParallelOrganCLIP text head.
Use this for organ-routed zero-shot: the resulting embedding lives in
the same space as the per-organ image queries (different head than the
global :meth:`encode_text`).
Args:
text_features: ``(N, text_embed_dim)`` last-token-pooled Qwen3 features.
normalize: L2-normalize the projection (default).
"""
self._require_organ_text_head()
projected = self.organ_text_projection(text_features.float()) # type: ignore[misc]
return F.normalize(projected, dim=-1, eps=1e-6) if normalize else projected
@torch.no_grad()
def zero_shot_organ(
self,
image: torch.Tensor,
text_features: torch.Tensor,
organ: str,
calibrated: bool = True,
) -> torch.Tensor:
"""Score text prompts against a single organ's query embedding.
Routes the image through the per-organ cross-attention pooler for
``organ`` and contrasts that query embedding with text features
projected by the ParallelOrganCLIP text head. Returns ``(B, N)``.
Args:
image: ``(B, 11, 192, 192, 192)`` preprocessed CT volume.
text_features: ``(N, text_embed_dim)`` pooled Qwen3 features.
organ: Organ name (must be in :attr:`organ_slot_names`).
calibrated: When ``True`` (default), applies this organ's
trained temperature + bias. Set ``False`` for raw cosine.
"""
self._require_organ_text_head()
organ_emb = self.encode_organs(image, organs=[organ], normalize=True)[organ] # (B, 576)
txt_emb = self.encode_organ_text(text_features) # (N, 576)
cosine = organ_emb @ txt_emb.t() # (B, N)
if not calibrated:
return cosine
idx = self.organ_slot_names.index(organ)
scale = self.organ_logit_scale[idx].float().exp()
bias = self.organ_text_bias[idx].float()
return cosine * scale + bias
@torch.no_grad()
def zero_shot_organs(
self,
image: torch.Tensor,
text_features: torch.Tensor,
organs: list[str] | None = None,
calibrated: bool = True,
) -> dict[str, torch.Tensor]:
"""Per-organ zero-shot for many organs at once.
Returns ``{organ_name: (B, N)}``. ``organs=None`` runs every named slot.
``calibrated`` defaults to ``True`` (per-organ temperature + bias applied).
"""
self._require_organ_text_head()
organ_embeds = self.encode_organs(image, organs=organs, normalize=True)
txt_emb = self.encode_organ_text(text_features)
names = self.organ_slot_names
out: dict[str, torch.Tensor] = {}
for name, emb in organ_embeds.items():
cosine = emb @ txt_emb.t()
if calibrated:
idx = names.index(name)
scale = self.organ_logit_scale[idx].float().exp()
bias = self.organ_text_bias[idx].float()
cosine = cosine * scale + bias
out[name] = cosine
return out
def _require_organ_text_head(self) -> None:
if self.organ_text_projection is None:
raise RuntimeError(
"This Jolia checkpoint has no per-organ text head β organ-routed zero-shot "
"is unavailable. The base text head (encode_text / zero_shot) may still work."
)
# ------------------------------------------------------------------
# The easy, named organ-query API
# ------------------------------------------------------------------
def encode_organs(
self,
image: torch.Tensor,
organs: list[str] | None = None,
normalize: bool = False,
) -> dict[str, torch.Tensor]:
"""Per-organ embeddings keyed by organ name.
Args:
image: Preprocessed volume ``(B, 11, 192, 192, 192)``.
organs: Subset of organ names to return. ``None`` returns every
named slot. Unknown names raise ``KeyError`` (with the valid
names listed).
normalize: L2-normalize each organ embedding (cosine-ready).
Returns:
``{organ_name: (B, query_dim)}``. If the model has no organ-slot
names, keys fall back to ``"slot_<i>"``.
"""
self._require_queries()
_, organ = self.forward_with_queries(image) # (B, num_organs, query_dim)
names = self.organ_slot_names or [f"slot_{i}" for i in range(self.num_organs)]
name_to_idx = {name: i for i, name in enumerate(names)}
if organs is None:
wanted = list(name_to_idx.items())
else:
missing = [o for o in organs if o not in name_to_idx]
if missing:
raise KeyError(
f"Unknown organ(s) {missing}. Available organs: {sorted(name_to_idx)}"
)
wanted = [(o, name_to_idx[o]) for o in organs]
out = {name: organ[:, idx, :] for name, idx in wanted}
if normalize:
out = {name: F.normalize(vec.float(), dim=-1, eps=1e-6) for name, vec in out.items()}
return out
def organ_similarity(self, image: torch.Tensor, organs: list[str] | None = None) -> torch.Tensor:
"""Cosine-similarity matrix between organ embeddings (batch-averaged).
Returns ``(N, N)`` for the ``N`` requested organs β handy for probing
which organs the model represents similarly.
"""
embeds = self.encode_organs(image, organs=organs, normalize=True)
names = list(embeds)
mat = torch.stack([embeds[n].mean(0) for n in names], dim=0) # (N, query_dim)
return mat @ mat.t()
def _require_queries(self) -> None:
if self.organ_query_attn_scales is None:
raise RuntimeError(
"This Jolia checkpoint has no organ-query attention "
"(num_organs=0); organ-level methods are unavailable."
)
|