Image Feature Extraction
Transformers
Safetensors
skinmap
feature-extraction
dermatology
medical-imaging
embeddings
clip
custom_code
Instructions to use Digital-Dermatology/SkinMap with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Digital-Dermatology/SkinMap with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-feature-extraction", model="Digital-Dermatology/SkinMap", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("Digital-Dermatology/SkinMap", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Commit ·
4fc0ad6
0
Parent(s):
SkinMap: 12-teacher ensemble + predict_meta (validated release)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +8 -0
- README.md +97 -0
- config.json +13 -0
- configuration_skinmap.py +30 -0
- meta_predictor.py +102 -0
- modeling_skinmap.py +301 -0
- probes/label_encoder_body_region.pkl +0 -0
- probes/label_encoder_fitzpatrick.pkl +0 -0
- probes/label_encoder_fitzpatrick_modspecific.pkl +0 -0
- probes/label_encoder_gender.pkl +0 -0
- probes/label_encoder_laterality.pkl +0 -0
- probes/label_encoder_modality.pkl +0 -0
- probes/label_encoder_origin.pkl +0 -0
- probes/manifest.json +100 -0
- probes/model_age.pkl +0 -0
- probes/model_body_region.pkl +0 -0
- probes/model_fitzpatrick.pkl +0 -0
- probes/model_fitzpatrick__clinical.pkl +0 -0
- probes/model_fitzpatrick__derm.pkl +0 -0
- probes/model_gender.pkl +0 -0
- probes/model_laterality.pkl +0 -0
- probes/model_modality.pkl +0 -0
- probes/model_origin.pkl +3 -0
- requirements.txt +13 -0
- skinmap_runtime/__init__.py +0 -0
- skinmap_runtime/_compat.py +153 -0
- skinmap_runtime/combined_embedder.py +926 -0
- skinmap_runtime/core/__init__.py +0 -0
- skinmap_runtime/core/models/__init__.py +0 -0
- skinmap_runtime/core/models/byol/__init__.py +0 -0
- skinmap_runtime/core/models/byol/model.py +42 -0
- skinmap_runtime/core/models/byol/predictor.py +15 -0
- skinmap_runtime/core/models/colorme/__init__.py +0 -0
- skinmap_runtime/core/models/colorme/model.py +49 -0
- skinmap_runtime/core/models/dino/__init__.py +0 -0
- skinmap_runtime/core/models/dino/head.py +52 -0
- skinmap_runtime/core/models/dino/multi_crop_wrapper.py +70 -0
- skinmap_runtime/core/models/encoders/__init__.py +1 -0
- skinmap_runtime/core/models/encoders/swin_transformer.py +1050 -0
- skinmap_runtime/core/models/encoders/utils.py +34 -0
- skinmap_runtime/core/models/encoders/vision_transformer.py +374 -0
- skinmap_runtime/core/models/fine_tuning/__init__.py +1 -0
- skinmap_runtime/core/models/fine_tuning/classifiers.py +69 -0
- skinmap_runtime/core/models/ibot/__init__.py +0 -0
- skinmap_runtime/core/models/ibot/head.py +80 -0
- skinmap_runtime/core/models/mae/__init__.py +0 -0
- skinmap_runtime/core/models/mae/model.py +297 -0
- skinmap_runtime/core/models/mae/utils.py +58 -0
- skinmap_runtime/core/models/simclr/__init__.py +1 -0
- skinmap_runtime/core/models/simclr/model.py +33 -0
.gitattributes
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.faiss filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.bin.index filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
analysis/**/*.bin filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc-by-nc-4.0
|
| 3 |
+
library_name: transformers
|
| 4 |
+
tags:
|
| 5 |
+
- dermatology
|
| 6 |
+
- medical-imaging
|
| 7 |
+
- embeddings
|
| 8 |
+
- image-feature-extraction
|
| 9 |
+
- clip
|
| 10 |
+
pipeline_tag: image-feature-extraction
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# A Global Atlas of Digital Dermatology to Map Innovation and Disparities
|
| 14 |
+
|
| 15 |
+

|
| 16 |
+
|
| 17 |
+
SkinMap is a dermatology image/text embedding model. It fuses **12 frozen teacher
|
| 18 |
+
encoders** (multi-modal and self-supervised models) through a trained projector
|
| 19 |
+
into a single **1024-d shared image/text space**, that can be used to infer
|
| 20 |
+
missing metadata, solve downstream tasks, and get relational information about the
|
| 21 |
+
samples.
|
| 22 |
+
|
| 23 |
+
**Compute.** This is the faithful full ensemble: 12 backbones load into memory at
|
| 24 |
+
once and need roughly **16 GB of GPU VRAM**. It runs on CPU but slowly.
|
| 25 |
+
|
| 26 |
+
## Usage
|
| 27 |
+
|
| 28 |
+
```python
|
| 29 |
+
from transformers import AutoModel
|
| 30 |
+
|
| 31 |
+
model = AutoModel.from_pretrained(
|
| 32 |
+
"Digital-Dermatology/SkinMap",
|
| 33 |
+
trust_remote_code=True,
|
| 34 |
+
device="cuda", # "cpu" also works
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
# 1) Encode an image into the 1024-d SkinMap space (L2-normalized)
|
| 38 |
+
img_emb = model.encode_image("lesion.jpg") # np.ndarray, shape (1024,)
|
| 39 |
+
|
| 40 |
+
# 2) Encode a text query into the same space
|
| 41 |
+
txt_emb = model.encode_text("melanoma on the back") # np.ndarray, shape (1024,)
|
| 42 |
+
|
| 43 |
+
# 3) Predict clinical metadata from the image
|
| 44 |
+
meta = model.predict_meta("lesion.jpg")
|
| 45 |
+
# {'fitzpatrick': 3, 'fitzpatrick_grouped': '3-4', 'age': 42.1,
|
| 46 |
+
# 'gender': 'female', 'origin': ..., 'body_region': ..., ...}
|
| 47 |
+
|
| 48 |
+
import numpy as np
|
| 49 |
+
sim = float(np.dot(img_emb, txt_emb)) # cosine in the shared space
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
`encode_image` accepts a file path, raw bytes, or a `PIL.Image`. `encode_text`
|
| 53 |
+
expands the query into clinical prompt templates and averages the results; pass
|
| 54 |
+
`templates=["{}"]` to encode the raw text as-is.
|
| 55 |
+
|
| 56 |
+
### Metadata prediction
|
| 57 |
+
|
| 58 |
+
`predict_meta` applies bundled linear probes to the SkinMap embedding to estimate
|
| 59 |
+
Fitzpatrick skin type, age, sex, geographic origin, body region and more. By
|
| 60 |
+
default it uses **global** probes, which reproduce the demographic estimates
|
| 61 |
+
reported in the paper. If you know the imaging modality, pass it to switch the
|
| 62 |
+
Fitzpatrick prediction to a **modality-specific** probe (more accurate when the
|
| 63 |
+
modality is known); the other attributes stay global:
|
| 64 |
+
|
| 65 |
+
```python
|
| 66 |
+
meta = model.predict_meta("lesion.jpg", modality="dermoscopy") # clinical | dermoscopy | TBP
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
`predict_meta` also accepts a precomputed 1024-d vector instead of an image, and a
|
| 70 |
+
subset via `attributes=[...]`.
|
| 71 |
+
|
| 72 |
+
### Nearest-neighbor search (optional)
|
| 73 |
+
|
| 74 |
+
If this repo was built with an atlas index bundled, you can retrieve neighbors
|
| 75 |
+
directly:
|
| 76 |
+
|
| 77 |
+
```python
|
| 78 |
+
indices, distances = model.search("lesion.jpg", k=10)
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
Without a bundled index, `search()` raises a clear error. Encoding still works.
|
| 82 |
+
|
| 83 |
+
## What's inside
|
| 84 |
+
|
| 85 |
+
| Component | Role |
|
| 86 |
+
|---|---|
|
| 87 |
+
| 9 multi-modal teachers (CLIP & SigLIP; ViT-L/14 & ViT-B/32) | image + text features |
|
| 88 |
+
| 3 self-supervised teachers (DINO, iBOT, MAE; ViT-B/16) | dermatology image features |
|
| 89 |
+
| Per-teacher whitening (`whitening_stats.npz`) | decorrelate before fusion |
|
| 90 |
+
| Trained projector (`projector_model.pth`) | fuse to 1024-d image/text heads |
|
| 91 |
+
| Metadata probes (`probes/`) | linear probes for `predict_meta` |
|
| 92 |
+
|
| 93 |
+
## Licensing
|
| 94 |
+
|
| 95 |
+
SkinMap is released under the Creative Commons Attribution-NonCommercial 4.0
|
| 96 |
+
International license, for research use only. **Not a medical device. Not for
|
| 97 |
+
clinical decision-making.**
|
config.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "skinmap",
|
| 3 |
+
"architectures": [
|
| 4 |
+
"SkinMapModel"
|
| 5 |
+
],
|
| 6 |
+
"auto_map": {
|
| 7 |
+
"AutoConfig": "configuration_skinmap.SkinMapConfig",
|
| 8 |
+
"AutoModel": "modeling_skinmap.SkinMapModel"
|
| 9 |
+
},
|
| 10 |
+
"embedding_dim": 1024,
|
| 11 |
+
"pipeline_config": "weights/embedding_pipeline_config.json",
|
| 12 |
+
"probes_dir": "probes"
|
| 13 |
+
}
|
configuration_skinmap.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HuggingFace config for the packaged SkinMap multi-teacher embedding model."""
|
| 2 |
+
|
| 3 |
+
from transformers import PretrainedConfig
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class SkinMapConfig(PretrainedConfig):
|
| 7 |
+
"""Minimal config for SkinMap.
|
| 8 |
+
|
| 9 |
+
SkinMap is not a single set of HF weights but an ensemble of teacher encoders
|
| 10 |
+
plus a trained projector, whose loading is driven by a bundled pipeline-config
|
| 11 |
+
JSON. This config only points at that JSON and records the output
|
| 12 |
+
dimensionality. The heavy lifting happens in `SkinMapModel`.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
model_type = "skinmap"
|
| 16 |
+
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
embedding_dim: int = 1024,
|
| 20 |
+
pipeline_config: str = "weights/embedding_pipeline_config.json",
|
| 21 |
+
probes_dir: str = "probes",
|
| 22 |
+
**kwargs,
|
| 23 |
+
):
|
| 24 |
+
self.embedding_dim = embedding_dim
|
| 25 |
+
# Paths are relative to the repo snapshot root.
|
| 26 |
+
self.pipeline_config = pipeline_config
|
| 27 |
+
# Directory of bundled metadata probes (enables predict_meta); may be
|
| 28 |
+
# absent in an embedding-only package.
|
| 29 |
+
self.probes_dir = probes_dir
|
| 30 |
+
super().__init__(**kwargs)
|
meta_predictor.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Metadata prediction from SkinMap embeddings via bundled linear probes.
|
| 2 |
+
|
| 3 |
+
Default uses the global probes, which reproduce the demographic estimates reported
|
| 4 |
+
in the paper (e.g. Fitzpatrick V--VI ~ 11%). Passing ``modality`` switches the
|
| 5 |
+
Fitzpatrick prediction to a modality-specific probe (clinical vs. dermoscopy/TBP),
|
| 6 |
+
which is more accurate when the imaging modality is known.
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import json
|
| 11 |
+
import pickle
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Dict, List, Optional, Union
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
# imaging modality -> probe group used for the modality-specific Fitzpatrick model
|
| 18 |
+
_MODALITY_GROUP = {"clinical": "clinical", "dermoscopy": "derm", "tbp": "derm"}
|
| 19 |
+
_FST_GROUP = {1.0: "1-2", 2.0: "1-2", 3.0: "3-4", 4.0: "3-4", 5.0: "5-6", 6.0: "5-6"}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class MetaPredictor:
|
| 23 |
+
"""Loads a bundled probe directory and predicts metadata from embeddings."""
|
| 24 |
+
|
| 25 |
+
def __init__(self, probes_dir: Union[str, Path]):
|
| 26 |
+
self.dir = Path(probes_dir)
|
| 27 |
+
manifest = self.dir / "manifest.json"
|
| 28 |
+
if not manifest.exists():
|
| 29 |
+
raise FileNotFoundError(f"probe manifest not found at {manifest}")
|
| 30 |
+
self.manifest = json.loads(manifest.read_text())
|
| 31 |
+
self._cache: Dict[str, object] = {}
|
| 32 |
+
|
| 33 |
+
def _load(self, name: str):
|
| 34 |
+
if name not in self._cache:
|
| 35 |
+
with open(self.dir / f"{name}.pkl", "rb") as f:
|
| 36 |
+
self._cache[name] = pickle.load(f)
|
| 37 |
+
return self._cache[name]
|
| 38 |
+
|
| 39 |
+
@property
|
| 40 |
+
def attributes(self) -> List[str]:
|
| 41 |
+
return list(self.manifest["attributes"].keys())
|
| 42 |
+
|
| 43 |
+
def predict(
|
| 44 |
+
self,
|
| 45 |
+
embedding: np.ndarray,
|
| 46 |
+
modality: Optional[str] = None,
|
| 47 |
+
attributes: Optional[List[str]] = None,
|
| 48 |
+
) -> Dict[str, object]:
|
| 49 |
+
"""Predict metadata for one embedding (1024,) or a batch (N, 1024).
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
embedding: L2-normalized SkinMap embedding(s).
|
| 53 |
+
modality: optional {'clinical','dermoscopy','TBP'}; switches Fitzpatrick
|
| 54 |
+
to the modality-specific probe. Other attributes stay global.
|
| 55 |
+
attributes: subset to predict (default: all available).
|
| 56 |
+
Returns:
|
| 57 |
+
dict attribute -> scalar (single input) or np.ndarray (batch).
|
| 58 |
+
"""
|
| 59 |
+
emb = np.asarray(embedding, dtype=np.float32)
|
| 60 |
+
single = emb.ndim == 1
|
| 61 |
+
if single:
|
| 62 |
+
emb = emb[None, :]
|
| 63 |
+
grp = _MODALITY_GROUP.get(str(modality).lower()) if modality else None
|
| 64 |
+
attrs = attributes or self.attributes
|
| 65 |
+
out: Dict[str, object] = {}
|
| 66 |
+
for a in attrs:
|
| 67 |
+
spec = self.manifest["attributes"].get(a)
|
| 68 |
+
if spec is None:
|
| 69 |
+
continue
|
| 70 |
+
if spec["task"] == "regression":
|
| 71 |
+
pred = self._load(f"model_{a}").predict(emb).astype(float)
|
| 72 |
+
clip = spec.get("clip")
|
| 73 |
+
if clip:
|
| 74 |
+
pred = np.clip(pred, clip[0], clip[1])
|
| 75 |
+
vals = pred
|
| 76 |
+
else:
|
| 77 |
+
if a == "fitzpatrick" and grp and spec.get("modality_specific"):
|
| 78 |
+
clf = self._load(f"model_fitzpatrick__{grp}")
|
| 79 |
+
le = self._load("label_encoder_fitzpatrick_modspecific")
|
| 80 |
+
else:
|
| 81 |
+
clf = self._load(f"model_{a}")
|
| 82 |
+
le = self._load(f"label_encoder_{a}")
|
| 83 |
+
vals = le.inverse_transform(clf.predict(emb))
|
| 84 |
+
try:
|
| 85 |
+
vals = vals.astype(float)
|
| 86 |
+
except (ValueError, TypeError):
|
| 87 |
+
pass
|
| 88 |
+
out[a] = vals
|
| 89 |
+
|
| 90 |
+
# derived: grouped Fitzpatrick (I-II / III-IV / V-VI)
|
| 91 |
+
if "fitzpatrick" in out:
|
| 92 |
+
fst = np.asarray(out["fitzpatrick"], dtype=float)
|
| 93 |
+
out["fitzpatrick_grouped"] = np.array(
|
| 94 |
+
[_FST_GROUP.get(round(v), "unknown") for v in np.atleast_1d(fst)]
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
if single:
|
| 98 |
+
out = {
|
| 99 |
+
k: (v[0].item() if hasattr(v[0], "item") else v[0])
|
| 100 |
+
for k, v in out.items()
|
| 101 |
+
}
|
| 102 |
+
return out
|
modeling_skinmap.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HuggingFace wrapper for the packaged SkinMap multi-teacher embedding model.
|
| 2 |
+
|
| 3 |
+
SkinMap fuses ~12 frozen teacher encoders (CLIP fine-tunes plus dermatology SSL
|
| 4 |
+
backbones) through a trained projector into a single 1024-d image/text space.
|
| 5 |
+
The public API is ``encode_image``, ``encode_text`` and ``search``.
|
| 6 |
+
|
| 7 |
+
SkinMap is not a single state-dict, so the standard PreTrainedModel weight
|
| 8 |
+
loading is not used. ``from_pretrained`` resolves the repo snapshot, puts the
|
| 9 |
+
bundled ``skinmap_runtime`` package on ``sys.path``, and builds the underlying
|
| 10 |
+
``CombinedEmbeddingPipeline`` from the bundled pipeline-config JSON. Top-level
|
| 11 |
+
imports stay light (stdlib, torch, transformers, hub) because the snapshot
|
| 12 |
+
location, and therefore ``skinmap_runtime``, is only known at call time.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import io
|
| 18 |
+
import sys
|
| 19 |
+
import warnings
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import List, Optional, Tuple, Union
|
| 22 |
+
|
| 23 |
+
import numpy as np
|
| 24 |
+
import torch
|
| 25 |
+
from transformers import PreTrainedModel
|
| 26 |
+
|
| 27 |
+
from .configuration_skinmap import SkinMapConfig
|
| 28 |
+
from .meta_predictor import MetaPredictor
|
| 29 |
+
|
| 30 |
+
ImageInput = Union[str, "Path", bytes, "object"] # path | bytes | PIL.Image.Image
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class SkinMapModel(PreTrainedModel):
|
| 34 |
+
config_class = SkinMapConfig
|
| 35 |
+
# SkinMap manages its own weights from the bundled files. There is no HF
|
| 36 |
+
# state-dict to load or split for device_map / low-cpu-mem paths.
|
| 37 |
+
_supports_device_map = False
|
| 38 |
+
|
| 39 |
+
def __init__(self, config: SkinMapConfig):
|
| 40 |
+
super().__init__(config)
|
| 41 |
+
# Keep a trivial buffer so .device / .to() behave on a Module with no
|
| 42 |
+
# registered parameters of its own.
|
| 43 |
+
self.register_buffer("_anchor", torch.zeros(1), persistent=False)
|
| 44 |
+
self._pipeline = None
|
| 45 |
+
self._predictor = None
|
| 46 |
+
self._skinmap_device = torch.device("cpu")
|
| 47 |
+
|
| 48 |
+
# ------------------------------------------------------------------ load
|
| 49 |
+
@classmethod
|
| 50 |
+
def from_pretrained(
|
| 51 |
+
cls,
|
| 52 |
+
pretrained_model_name_or_path,
|
| 53 |
+
*model_args,
|
| 54 |
+
device: Optional[str] = None,
|
| 55 |
+
**kwargs,
|
| 56 |
+
):
|
| 57 |
+
"""Resolve the repo snapshot and build the SkinMap pipeline.
|
| 58 |
+
|
| 59 |
+
Accepts a local directory or a (private) hub repo id. Hub/auth kwargs
|
| 60 |
+
(`token`, `revision`, `cache_dir`) are forwarded to `snapshot_download`.
|
| 61 |
+
Standard weight-loading kwargs that SkinMap cannot honor (`torch_dtype`,
|
| 62 |
+
`device_map`, `low_cpu_mem_usage`) are warned about rather than silently
|
| 63 |
+
ignored. Use `device=...` to choose the device.
|
| 64 |
+
"""
|
| 65 |
+
token = kwargs.pop("token", kwargs.pop("use_auth_token", None))
|
| 66 |
+
revision = kwargs.pop("revision", None)
|
| 67 |
+
cache_dir = kwargs.pop("cache_dir", None)
|
| 68 |
+
config = kwargs.pop("config", None)
|
| 69 |
+
|
| 70 |
+
# SkinMap loads its own weights from the bundled files, so the standard
|
| 71 |
+
# dtype/placement/low-mem machinery does not apply. Warn instead of
|
| 72 |
+
# silently dropping these. A user passing torch_dtype=fp16 to fit the
|
| 73 |
+
# ensemble in limited VRAM would otherwise get fp32 and OOM with no signal.
|
| 74 |
+
for unsupported in ("torch_dtype", "device_map", "low_cpu_mem_usage"):
|
| 75 |
+
if kwargs.pop(unsupported, None) is not None:
|
| 76 |
+
warnings.warn(
|
| 77 |
+
f"SkinMapModel.from_pretrained ignores `{unsupported}`: the "
|
| 78 |
+
"ensemble runs in fp32 on a single device. Pass `device=...` "
|
| 79 |
+
"to choose the device. fp16/sharded loading is unsupported.",
|
| 80 |
+
stacklevel=2,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
path = Path(pretrained_model_name_or_path)
|
| 84 |
+
if path.is_dir():
|
| 85 |
+
snapshot = path
|
| 86 |
+
else:
|
| 87 |
+
from huggingface_hub import snapshot_download
|
| 88 |
+
|
| 89 |
+
snapshot = Path(
|
| 90 |
+
snapshot_download(
|
| 91 |
+
repo_id=str(pretrained_model_name_or_path),
|
| 92 |
+
revision=revision,
|
| 93 |
+
cache_dir=cache_dir,
|
| 94 |
+
token=token,
|
| 95 |
+
)
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
if config is None:
|
| 99 |
+
config = SkinMapConfig.from_pretrained(snapshot)
|
| 100 |
+
|
| 101 |
+
model = cls(config)
|
| 102 |
+
model._load_pipeline(snapshot, device=device)
|
| 103 |
+
return model
|
| 104 |
+
|
| 105 |
+
def _load_pipeline(self, snapshot, device: Optional[str] = None):
|
| 106 |
+
snapshot = Path(snapshot).resolve()
|
| 107 |
+
if str(snapshot) not in sys.path:
|
| 108 |
+
sys.path.insert(0, str(snapshot))
|
| 109 |
+
|
| 110 |
+
# `skinmap_runtime` is a fixed top-level package name resolved off sys.path,
|
| 111 |
+
# so only one snapshot's runtime can be live per process. If a different
|
| 112 |
+
# snapshot already imported it, that one is reused, so warn rather than
|
| 113 |
+
# silently run model B on model A's code.
|
| 114 |
+
already = sys.modules.get("skinmap_runtime")
|
| 115 |
+
if already is not None and getattr(already, "__path__", None):
|
| 116 |
+
loaded_from = Path(list(already.__path__)[0]).resolve().parent
|
| 117 |
+
if loaded_from != snapshot:
|
| 118 |
+
warnings.warn(
|
| 119 |
+
"A different SkinMap snapshot's runtime is already loaded in "
|
| 120 |
+
f"this process (from {loaded_from}). Reusing it. Loading two "
|
| 121 |
+
"different SkinMap revisions in one process is unsupported.",
|
| 122 |
+
stacklevel=2,
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Import the bundled runtime via importlib (NOT `from skinmap_runtime ...`):
|
| 126 |
+
# transformers' check_imports statically scans this file for import
|
| 127 |
+
# statements and would otherwise treat `skinmap_runtime` as a missing
|
| 128 |
+
# PyPI dependency, breaking from_pretrained for every user.
|
| 129 |
+
import importlib
|
| 130 |
+
|
| 131 |
+
CombinedEmbeddingPipeline = importlib.import_module(
|
| 132 |
+
"skinmap_runtime.combined_embedder"
|
| 133 |
+
).CombinedEmbeddingPipeline
|
| 134 |
+
|
| 135 |
+
cfg_path = snapshot / self.config.pipeline_config
|
| 136 |
+
if not cfg_path.exists():
|
| 137 |
+
raise FileNotFoundError(
|
| 138 |
+
f"SkinMap pipeline config not found at {cfg_path}. "
|
| 139 |
+
"The repo snapshot may be incomplete."
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
resolved = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 143 |
+
self._pipeline = CombinedEmbeddingPipeline.from_config(
|
| 144 |
+
cfg_path, device=resolved
|
| 145 |
+
)
|
| 146 |
+
self._skinmap_device = torch.device(resolved)
|
| 147 |
+
# Keep the module's own buffer on the resolved device so standard
|
| 148 |
+
# introspection (next(model.buffers()).device) agrees with .device.
|
| 149 |
+
self._anchor = self._anchor.to(self._skinmap_device)
|
| 150 |
+
# Bundled metadata probes (optional): enables predict_meta(). Absent in
|
| 151 |
+
# embedding-only snapshots, so load lazily and stay quiet if not present.
|
| 152 |
+
probes_path = snapshot / getattr(self.config, "probes_dir", "probes")
|
| 153 |
+
if (probes_path / "manifest.json").exists():
|
| 154 |
+
self._predictor = MetaPredictor(probes_path)
|
| 155 |
+
return self
|
| 156 |
+
|
| 157 |
+
def _require_pipeline(self):
|
| 158 |
+
if self._pipeline is None:
|
| 159 |
+
raise RuntimeError(
|
| 160 |
+
"SkinMap pipeline is not loaded. Use "
|
| 161 |
+
"SkinMapModel.from_pretrained(...) rather than constructing the "
|
| 162 |
+
"model directly."
|
| 163 |
+
)
|
| 164 |
+
return self._pipeline
|
| 165 |
+
|
| 166 |
+
# ------------------------------------------------------------- device
|
| 167 |
+
@property
|
| 168 |
+
def device(self) -> torch.device: # type: ignore[override]
|
| 169 |
+
return self._skinmap_device
|
| 170 |
+
|
| 171 |
+
def to(self, *args, **kwargs): # type: ignore[override]
|
| 172 |
+
"""Move every teacher + the projector onto the requested device."""
|
| 173 |
+
device = None
|
| 174 |
+
if args and (isinstance(args[0], (str, torch.device)) or args[0] is None):
|
| 175 |
+
device = args[0]
|
| 176 |
+
device = kwargs.get("device", device)
|
| 177 |
+
if device is None:
|
| 178 |
+
return self
|
| 179 |
+
device = torch.device(device)
|
| 180 |
+
super().to(device)
|
| 181 |
+
self._skinmap_device = device
|
| 182 |
+
|
| 183 |
+
pipe = self._pipeline
|
| 184 |
+
if pipe is not None:
|
| 185 |
+
pipe.device = device
|
| 186 |
+
for wrapper in getattr(pipe, "clip_models", []):
|
| 187 |
+
wrapper.model.to(device)
|
| 188 |
+
for wrapper in getattr(pipe, "ssl_models", []):
|
| 189 |
+
wrapper.model.to(device)
|
| 190 |
+
if getattr(pipe, "projector_model", None) is not None:
|
| 191 |
+
pipe.projector_model.to(device)
|
| 192 |
+
return self
|
| 193 |
+
|
| 194 |
+
def cuda(self, device=None): # type: ignore[override]
|
| 195 |
+
return self.to(f"cuda:{device}" if isinstance(device, int) else "cuda")
|
| 196 |
+
|
| 197 |
+
def cpu(self): # type: ignore[override]
|
| 198 |
+
return self.to("cpu")
|
| 199 |
+
|
| 200 |
+
# ------------------------------------------------------------- encode
|
| 201 |
+
@staticmethod
|
| 202 |
+
def _as_pil(image: ImageInput):
|
| 203 |
+
from PIL import Image
|
| 204 |
+
|
| 205 |
+
if isinstance(image, Image.Image):
|
| 206 |
+
return image.convert("RGB")
|
| 207 |
+
if isinstance(image, (bytes, bytearray)):
|
| 208 |
+
return Image.open(io.BytesIO(image)).convert("RGB")
|
| 209 |
+
if isinstance(image, (str, Path)):
|
| 210 |
+
return Image.open(image).convert("RGB")
|
| 211 |
+
raise TypeError(
|
| 212 |
+
f"Unsupported image input type: {type(image).__name__}. "
|
| 213 |
+
"Pass a file path, raw bytes, or a PIL.Image."
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
@torch.inference_mode()
|
| 217 |
+
def encode_image(self, image: ImageInput) -> np.ndarray:
|
| 218 |
+
"""Encode one image into the 1024-d SkinMap space (L2-normalized)."""
|
| 219 |
+
pipe = self._require_pipeline()
|
| 220 |
+
return pipe.embed_image(self._as_pil(image))
|
| 221 |
+
|
| 222 |
+
@torch.inference_mode()
|
| 223 |
+
def encode_text(
|
| 224 |
+
self, text: str, templates: Optional[List[str]] = None
|
| 225 |
+
) -> np.ndarray:
|
| 226 |
+
"""Encode a text query into the shared 1024-d space.
|
| 227 |
+
|
| 228 |
+
By default the query is expanded into clinical prompt templates and the
|
| 229 |
+
results averaged. Pass ``templates=["{}"]`` to encode the raw text.
|
| 230 |
+
"""
|
| 231 |
+
pipe = self._require_pipeline()
|
| 232 |
+
return pipe.encode_text(text, templates=templates)
|
| 233 |
+
|
| 234 |
+
# ------------------------------------------------------------- predict_meta
|
| 235 |
+
def _require_predictor(self):
|
| 236 |
+
if self._predictor is None:
|
| 237 |
+
raise RuntimeError(
|
| 238 |
+
"No metadata probes are bundled with this SkinMap repo, so "
|
| 239 |
+
"predict_meta() is unavailable. Rebuild the package with the "
|
| 240 |
+
"probes/ directory to enable metadata prediction."
|
| 241 |
+
)
|
| 242 |
+
return self._predictor
|
| 243 |
+
|
| 244 |
+
@torch.inference_mode()
|
| 245 |
+
def predict_meta(
|
| 246 |
+
self,
|
| 247 |
+
image: Union[ImageInput, np.ndarray],
|
| 248 |
+
modality: Optional[str] = None,
|
| 249 |
+
attributes: Optional[List[str]] = None,
|
| 250 |
+
) -> dict:
|
| 251 |
+
"""Predict metadata from an image or a precomputed 1024-d SkinMap vector.
|
| 252 |
+
|
| 253 |
+
Returns a dict of attributes (Fitzpatrick skin type, age, sex, geographic
|
| 254 |
+
origin, body region, ...). By default the global probes are used, which
|
| 255 |
+
reproduce the demographic estimates reported in the paper. Pass
|
| 256 |
+
``modality`` (``"clinical"``, ``"dermoscopy"`` or ``"TBP"``) to use the
|
| 257 |
+
modality-specific Fitzpatrick probe, which is more accurate when the
|
| 258 |
+
imaging modality is known; the other attributes stay global.
|
| 259 |
+
"""
|
| 260 |
+
predictor = self._require_predictor()
|
| 261 |
+
emb = image if isinstance(image, np.ndarray) else self.encode_image(image)
|
| 262 |
+
return predictor.predict(emb, modality=modality, attributes=attributes)
|
| 263 |
+
|
| 264 |
+
def forward(self, image: Optional[ImageInput] = None, **kwargs) -> torch.Tensor:
|
| 265 |
+
"""Convenience: ``forward(image)`` returns ``encode_image(image)`` as a tensor.
|
| 266 |
+
|
| 267 |
+
SkinMap is an encoder, not a trainable head, so prefer the explicit
|
| 268 |
+
``encode_image`` / ``encode_text``. HF-style tensor calls such as
|
| 269 |
+
``model(pixel_values=...)`` raise a clear error instead of crashing
|
| 270 |
+
cryptically.
|
| 271 |
+
"""
|
| 272 |
+
if image is None:
|
| 273 |
+
raise TypeError(
|
| 274 |
+
"SkinMapModel is an encoder: call model.encode_image(img) or "
|
| 275 |
+
"model.encode_text(txt). forward() accepts a single image "
|
| 276 |
+
f"(path/bytes/PIL), got keyword args {sorted(kwargs)} instead."
|
| 277 |
+
)
|
| 278 |
+
return torch.from_numpy(np.ascontiguousarray(self.encode_image(image)))
|
| 279 |
+
|
| 280 |
+
# ------------------------------------------------------------- search
|
| 281 |
+
def search(
|
| 282 |
+
self, query: ImageInput, k: int = 10
|
| 283 |
+
) -> Tuple[List[int], List[float]]:
|
| 284 |
+
"""Nearest-neighbor search against the bundled atlas index.
|
| 285 |
+
|
| 286 |
+
`query` may be an image (path/bytes/PIL) or a precomputed 1024-d vector.
|
| 287 |
+
Raises a clear error if no atlas/index was bundled with this repo.
|
| 288 |
+
"""
|
| 289 |
+
pipe = self._require_pipeline()
|
| 290 |
+
if isinstance(query, np.ndarray):
|
| 291 |
+
vector = query
|
| 292 |
+
else:
|
| 293 |
+
vector = self.encode_image(query)
|
| 294 |
+
try:
|
| 295 |
+
return pipe.find_nearest_neighbors(np.asarray(vector), k=k)
|
| 296 |
+
except RuntimeError as exc:
|
| 297 |
+
raise RuntimeError(
|
| 298 |
+
"Nearest-neighbor search needs a bundled atlas index, which is "
|
| 299 |
+
"not present in this repo. Rebuild the package with --with-atlas "
|
| 300 |
+
"to enable search()."
|
| 301 |
+
) from exc
|
probes/label_encoder_body_region.pkl
ADDED
|
Binary file (359 Bytes). View file
|
|
|
probes/label_encoder_fitzpatrick.pkl
ADDED
|
Binary file (289 Bytes). View file
|
|
|
probes/label_encoder_fitzpatrick_modspecific.pkl
ADDED
|
Binary file (278 Bytes). View file
|
|
|
probes/label_encoder_gender.pkl
ADDED
|
Binary file (258 Bytes). View file
|
|
|
probes/label_encoder_laterality.pkl
ADDED
|
Binary file (257 Bytes). View file
|
|
|
probes/label_encoder_modality.pkl
ADDED
|
Binary file (272 Bytes). View file
|
|
|
probes/label_encoder_origin.pkl
ADDED
|
Binary file (439 Bytes). View file
|
|
|
probes/manifest.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"attributes": {
|
| 3 |
+
"fitzpatrick": {
|
| 4 |
+
"task": "classification",
|
| 5 |
+
"n_train": 33082,
|
| 6 |
+
"classes": [
|
| 7 |
+
"1.0",
|
| 8 |
+
"2.0",
|
| 9 |
+
"3.0",
|
| 10 |
+
"4.0",
|
| 11 |
+
"5.0",
|
| 12 |
+
"6.0"
|
| 13 |
+
],
|
| 14 |
+
"modality_specific": true,
|
| 15 |
+
"modality_groups": {
|
| 16 |
+
"clinical": "clinical",
|
| 17 |
+
"dermoscopy": "derm",
|
| 18 |
+
"TBP": "derm"
|
| 19 |
+
}
|
| 20 |
+
},
|
| 21 |
+
"gender": {
|
| 22 |
+
"task": "classification",
|
| 23 |
+
"n_train": 523878,
|
| 24 |
+
"classes": [
|
| 25 |
+
"female",
|
| 26 |
+
"male"
|
| 27 |
+
],
|
| 28 |
+
"modality_specific": false
|
| 29 |
+
},
|
| 30 |
+
"origin": {
|
| 31 |
+
"task": "classification",
|
| 32 |
+
"n_train": 518420,
|
| 33 |
+
"classes": [
|
| 34 |
+
"Argentina",
|
| 35 |
+
"Australia",
|
| 36 |
+
"Austria",
|
| 37 |
+
"Brazil",
|
| 38 |
+
"Germany",
|
| 39 |
+
"Greece",
|
| 40 |
+
"Guinea",
|
| 41 |
+
"India",
|
| 42 |
+
"Italy",
|
| 43 |
+
"Madagascar",
|
| 44 |
+
"Malawi",
|
| 45 |
+
"Netherlands",
|
| 46 |
+
"New Zealand",
|
| 47 |
+
"Portugal",
|
| 48 |
+
"Spain",
|
| 49 |
+
"Switzerland",
|
| 50 |
+
"Tanzania",
|
| 51 |
+
"United States"
|
| 52 |
+
],
|
| 53 |
+
"modality_specific": false
|
| 54 |
+
},
|
| 55 |
+
"body_region": {
|
| 56 |
+
"task": "classification",
|
| 57 |
+
"n_train": 613385,
|
| 58 |
+
"classes": [
|
| 59 |
+
"acral",
|
| 60 |
+
"gluteal_buttocks",
|
| 61 |
+
"groin_genital_perineum",
|
| 62 |
+
"head_neck",
|
| 63 |
+
"lower_limb",
|
| 64 |
+
"trunk_back",
|
| 65 |
+
"trunk_front",
|
| 66 |
+
"upper_limb"
|
| 67 |
+
],
|
| 68 |
+
"modality_specific": false
|
| 69 |
+
},
|
| 70 |
+
"laterality": {
|
| 71 |
+
"task": "classification",
|
| 72 |
+
"n_train": 173585,
|
| 73 |
+
"classes": [
|
| 74 |
+
"left",
|
| 75 |
+
"right"
|
| 76 |
+
],
|
| 77 |
+
"modality_specific": false
|
| 78 |
+
},
|
| 79 |
+
"modality": {
|
| 80 |
+
"task": "classification",
|
| 81 |
+
"n_train": 596759,
|
| 82 |
+
"classes": [
|
| 83 |
+
"TBP",
|
| 84 |
+
"clinical",
|
| 85 |
+
"dermoscopy"
|
| 86 |
+
],
|
| 87 |
+
"modality_specific": false
|
| 88 |
+
},
|
| 89 |
+
"age": {
|
| 90 |
+
"task": "regression",
|
| 91 |
+
"n_train": 534789,
|
| 92 |
+
"clip": [
|
| 93 |
+
0,
|
| 94 |
+
100
|
| 95 |
+
],
|
| 96 |
+
"modality_specific": false
|
| 97 |
+
}
|
| 98 |
+
},
|
| 99 |
+
"note": "Global probes are the original frozen Jun-17 (paper) prediction models that produced the atlas *_pred imputations; modality-specific Fitzpatrick probes are fit on the frozen atlas. Input = 1024-d L2-normalized SkinMap embedding."
|
| 100 |
+
}
|
probes/model_age.pkl
ADDED
|
Binary file (4.52 kB). View file
|
|
|
probes/model_body_region.pkl
ADDED
|
Binary file (66.4 kB). View file
|
|
|
probes/model_fitzpatrick.pkl
ADDED
|
Binary file (49.9 kB). View file
|
|
|
probes/model_fitzpatrick__clinical.pkl
ADDED
|
Binary file (50 kB). View file
|
|
|
probes/model_fitzpatrick__derm.pkl
ADDED
|
Binary file (33.5 kB). View file
|
|
|
probes/model_gender.pkl
ADDED
|
Binary file (8.91 kB). View file
|
|
|
probes/model_laterality.pkl
ADDED
|
Binary file (8.91 kB). View file
|
|
|
probes/model_modality.pkl
ADDED
|
Binary file (25.3 kB). View file
|
|
|
probes/model_origin.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6402dd67061dc178ee158a64842e426a57f62f95f201e3d1142488dabe112e7d
|
| 3 |
+
size 148444
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Inference dependencies for the packaged SkinMap model.
|
| 2 |
+
# (Training-only deps — wandb, umap, matplotlib, plotly, glasbey — are intentionally absent.)
|
| 3 |
+
torch>=2.0
|
| 4 |
+
torchvision>=0.15
|
| 5 |
+
transformers>=4.40
|
| 6 |
+
huggingface_hub>=0.23
|
| 7 |
+
numpy>=1.24
|
| 8 |
+
scipy>=1.10
|
| 9 |
+
tqdm>=4.65
|
| 10 |
+
loguru>=0.7
|
| 11 |
+
faiss-cpu>=1.7
|
| 12 |
+
joblib>=1.3
|
| 13 |
+
Pillow>=9.0
|
skinmap_runtime/__init__.py
ADDED
|
File without changes
|
skinmap_runtime/_compat.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Self-contained inference-only shims for the packaged SkinMap model.
|
| 2 |
+
|
| 3 |
+
The runtime `combined_embedder.py` normally pulls three helpers out of the
|
| 4 |
+
training code base (`src.create_skinmap`, `src.train_clip`). Those modules carry
|
| 5 |
+
heavy, training-only top-level imports (wandb, umap, matplotlib, plotly, DDP,
|
| 6 |
+
torch.compile) that have no place in a portable inference package. This module
|
| 7 |
+
re-implements exactly the three symbols the runtime needs, with nothing else.
|
| 8 |
+
|
| 9 |
+
Keep this file dependency-light: stdlib + torch + transformers only.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
from loguru import logger
|
| 17 |
+
from torchvision import transforms
|
| 18 |
+
from torchvision.transforms import InterpolationMode
|
| 19 |
+
|
| 20 |
+
# Mirrors src/skinmap/models/loaders.py: names the SSL teacher checkpoints are
|
| 21 |
+
# known by. Only the first three are used by the shipped SkinMap ensemble.
|
| 22 |
+
SSL_MODEL_NAMES = [
|
| 23 |
+
"dino_qderma",
|
| 24 |
+
"ibot_qderma",
|
| 25 |
+
"mae_qderma",
|
| 26 |
+
"simclr_qderma",
|
| 27 |
+
"byol_qderma",
|
| 28 |
+
"colorme_qderma",
|
| 29 |
+
"panderm_base",
|
| 30 |
+
"panderm_large",
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def get_imagenet_transform():
|
| 35 |
+
"""Standard ImageNet preprocessing for the SSL teachers.
|
| 36 |
+
|
| 37 |
+
Verbatim copy of src/skinmap/data/transforms.py:get_imagenet_transform so the
|
| 38 |
+
packaged pipeline preprocesses SSL inputs identically to training.
|
| 39 |
+
"""
|
| 40 |
+
return transforms.Compose(
|
| 41 |
+
[
|
| 42 |
+
transforms.Resize(256, interpolation=InterpolationMode.BICUBIC),
|
| 43 |
+
transforms.CenterCrop(224),
|
| 44 |
+
transforms.ToTensor(),
|
| 45 |
+
transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225)),
|
| 46 |
+
]
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _processor_name_fallback(model_name: str) -> str:
|
| 51 |
+
"""Infer the upstream processor repo for a local CLIP checkpoint name.
|
| 52 |
+
|
| 53 |
+
Only a fallback: the shipped CLIP checkpoints are self-contained HF dirs, so
|
| 54 |
+
the processor loads locally first. Match on the architecture substring so it
|
| 55 |
+
stays robust to clean teacher directory names (e.g. ``siglip_vit-base-patch32``).
|
| 56 |
+
"""
|
| 57 |
+
if "monet" in model_name:
|
| 58 |
+
return "suinleelab/monet"
|
| 59 |
+
if "vit-base-patch32" in model_name:
|
| 60 |
+
return "openai/clip-vit-base-patch32"
|
| 61 |
+
if "vit-large-patch14" in model_name:
|
| 62 |
+
return "openai/clip-vit-large-patch14"
|
| 63 |
+
return model_name
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def load_model_and_processor(model_name, device, *args, **kwargs):
|
| 67 |
+
"""Load a CLIP teacher (model + processor) for inference.
|
| 68 |
+
|
| 69 |
+
A trimmed equivalent of src/train_clip.py:load_model_and_processor: no
|
| 70 |
+
DDP wrapping, no torch.compile, no random-init / siglip-bias training paths.
|
| 71 |
+
The shipped CLIP checkpoints are self-contained HF directories (config +
|
| 72 |
+
safetensors + tokenizer + preprocessor), so we load them locally first and
|
| 73 |
+
only reach out to the hub if a processor artifact is somehow missing.
|
| 74 |
+
"""
|
| 75 |
+
from transformers import (
|
| 76 |
+
CLIPImageProcessor,
|
| 77 |
+
CLIPModel,
|
| 78 |
+
CLIPProcessor,
|
| 79 |
+
CLIPTokenizer,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
is_local = Path(model_name).exists()
|
| 83 |
+
|
| 84 |
+
# Candidate processor sources, local checkpoint dir first.
|
| 85 |
+
candidates = [model_name]
|
| 86 |
+
fallback = _processor_name_fallback(model_name)
|
| 87 |
+
if fallback != model_name:
|
| 88 |
+
candidates.append(fallback)
|
| 89 |
+
|
| 90 |
+
def _load_processor(candidate: str, local_only: bool):
|
| 91 |
+
try:
|
| 92 |
+
return CLIPProcessor.from_pretrained(
|
| 93 |
+
candidate, local_files_only=local_only
|
| 94 |
+
)
|
| 95 |
+
except Exception as base_exc:
|
| 96 |
+
# Some checkpoints ship tokenizer + image processor separately.
|
| 97 |
+
try:
|
| 98 |
+
tok = CLIPTokenizer.from_pretrained(
|
| 99 |
+
candidate, local_files_only=local_only
|
| 100 |
+
)
|
| 101 |
+
img = CLIPImageProcessor.from_pretrained(
|
| 102 |
+
candidate, local_files_only=local_only
|
| 103 |
+
)
|
| 104 |
+
return CLIPProcessor(tokenizer=tok, image_processor=img)
|
| 105 |
+
except Exception:
|
| 106 |
+
raise base_exc
|
| 107 |
+
|
| 108 |
+
processor = None
|
| 109 |
+
errors = []
|
| 110 |
+
for candidate in candidates:
|
| 111 |
+
local_only = Path(candidate).exists()
|
| 112 |
+
try:
|
| 113 |
+
processor = _load_processor(candidate, local_only=local_only)
|
| 114 |
+
break
|
| 115 |
+
except Exception as exc: # pragma: no cover - runtime safeguard
|
| 116 |
+
errors.append(exc)
|
| 117 |
+
logger.warning(f"Could not load CLIP processor from {candidate}: {exc}")
|
| 118 |
+
if processor is None:
|
| 119 |
+
raise RuntimeError(
|
| 120 |
+
f"Failed to load a CLIP processor for {model_name}: {errors[-1]}"
|
| 121 |
+
) from errors[-1]
|
| 122 |
+
|
| 123 |
+
model = CLIPModel.from_pretrained(model_name, local_files_only=is_local)
|
| 124 |
+
model.to(device)
|
| 125 |
+
model.eval()
|
| 126 |
+
return model, processor
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def load_ssl_local(ckpt_path, n_head_layers: int = 0):
|
| 130 |
+
"""Load an SSL teacher from a bundled local checkpoint.
|
| 131 |
+
|
| 132 |
+
`Embedder.load_pretrained` always downloads from the upstream vm02 URL, so we
|
| 133 |
+
bypass it: the per-family loader functions (`load_dino`/`load_ibot`/
|
| 134 |
+
`load_mae`) accept a local `ckp_path` and read their architecture config out
|
| 135 |
+
of the checkpoint itself. The teacher family (dino/ibot/mae) is inferred from
|
| 136 |
+
the file stem, robustly to clean names (``mae_vit-base-patch16.pth`` as well as
|
| 137 |
+
``mae_qderma.pth``); the family key maps to the same loader either way.
|
| 138 |
+
"""
|
| 139 |
+
from skinmap_runtime.core.pkg.embedder import Embedder
|
| 140 |
+
|
| 141 |
+
stem = Path(ckpt_path).stem
|
| 142 |
+
parts = set(stem.split("-")) | set(stem.split("_")) | {stem}
|
| 143 |
+
family = next((f for f in ("dino", "ibot", "mae") if f in parts), None)
|
| 144 |
+
# Map the family to its dermatology-pretrained registry key
|
| 145 |
+
# (dino_qderma/ibot_qderma/mae_qderma); all three exist and select
|
| 146 |
+
# load_dino/load_ibot/load_mae, which read the architecture from the checkpoint.
|
| 147 |
+
key = f"{family}_qderma" if family else stem
|
| 148 |
+
loader_func = Embedder.get_model_func(key)
|
| 149 |
+
return loader_func(
|
| 150 |
+
ckp_path=str(ckpt_path),
|
| 151 |
+
return_info=True,
|
| 152 |
+
n_head_layers=n_head_layers,
|
| 153 |
+
)
|
skinmap_runtime/combined_embedder.py
ADDED
|
@@ -0,0 +1,926 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
import threading
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any, Iterable, List, Optional, Sequence, Tuple
|
| 10 |
+
|
| 11 |
+
import faiss # type: ignore
|
| 12 |
+
import numpy as np
|
| 13 |
+
import torch
|
| 14 |
+
from PIL import Image
|
| 15 |
+
from torch import nn
|
| 16 |
+
|
| 17 |
+
from skinmap_runtime.core.pkg.embedder import Embedder
|
| 18 |
+
from skinmap_runtime._compat import SSL_MODEL_NAMES, get_imagenet_transform
|
| 19 |
+
from skinmap_runtime._compat import load_model_and_processor
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
import joblib
|
| 23 |
+
except ImportError: # pragma: no cover - fallback for older sklearn versions
|
| 24 |
+
from sklearn.externals import joblib # type: ignore
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _coerce_clip_features(feats, embeds_attr: str):
|
| 31 |
+
"""Return the projected feature tensor from a CLIP ``get_*_features`` call.
|
| 32 |
+
|
| 33 |
+
Some transformers versions return a ``BaseModelOutputWithPooling`` rather
|
| 34 |
+
than a tensor. In that case the projected embedding lives in the ``*_embeds``
|
| 35 |
+
field, or in ``pooler_output`` (which holds the projected vector for the
|
| 36 |
+
shipped CLIP checkpoints). ``last_hidden_state`` is not used as a fallback:
|
| 37 |
+
its CLS token has the encoder hidden dim, not the projection dim, so it would
|
| 38 |
+
silently corrupt the embedding. Shared by the image and text paths.
|
| 39 |
+
"""
|
| 40 |
+
if isinstance(feats, torch.Tensor):
|
| 41 |
+
return feats
|
| 42 |
+
for attr in (embeds_attr, "pooler_output"):
|
| 43 |
+
val = getattr(feats, attr, None)
|
| 44 |
+
if val is not None:
|
| 45 |
+
return val
|
| 46 |
+
kind = embeds_attr.split("_")[0]
|
| 47 |
+
raise TypeError(
|
| 48 |
+
f"get_{kind}_features returned {type(feats).__name__} with neither "
|
| 49 |
+
f"'{embeds_attr}' nor 'pooler_output'; cannot extract the projected "
|
| 50 |
+
f"embedding (unsupported transformers version?)."
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@dataclass
|
| 55 |
+
class ClipModelWrapper:
|
| 56 |
+
name: str
|
| 57 |
+
model: nn.Module
|
| 58 |
+
processor: Any # CLIPProcessor
|
| 59 |
+
|
| 60 |
+
def embed(self, image: Image.Image, device: torch.device) -> np.ndarray:
|
| 61 |
+
prepared = image.copy().convert("RGB")
|
| 62 |
+
prepared.thumbnail((512, 512), Image.Resampling.LANCZOS)
|
| 63 |
+
inputs = self.processor(images=[prepared], return_tensors="pt", padding=True)
|
| 64 |
+
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 65 |
+
self.model.eval()
|
| 66 |
+
with torch.inference_mode():
|
| 67 |
+
feats = _coerce_clip_features(
|
| 68 |
+
self.model.get_image_features(**inputs), "image_embeds"
|
| 69 |
+
)
|
| 70 |
+
# Normalize to match training pipeline
|
| 71 |
+
feats = feats / feats.norm(p=2, dim=-1, keepdim=True)
|
| 72 |
+
return feats.detach().cpu().numpy()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
@dataclass
|
| 76 |
+
class SSLModelWrapper:
|
| 77 |
+
name: str
|
| 78 |
+
model: nn.Module
|
| 79 |
+
transform: Any # torchvision.transforms.Compose
|
| 80 |
+
|
| 81 |
+
def embed(self, image: Image.Image, device: torch.device) -> np.ndarray:
|
| 82 |
+
prepared = image.copy().convert("RGB")
|
| 83 |
+
tensor = self.transform(prepared).unsqueeze(0).to(device)
|
| 84 |
+
self.model.eval()
|
| 85 |
+
with torch.inference_mode():
|
| 86 |
+
feats = self.model(tensor)
|
| 87 |
+
feats = torch.nn.functional.normalize(feats, dim=-1, p=2)
|
| 88 |
+
return feats.detach().cpu().numpy()
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class CombinedEmbeddingPipeline:
|
| 92 |
+
"""
|
| 93 |
+
Runtime helper that mirrors the embedding combination used to build SkinMap.
|
| 94 |
+
|
| 95 |
+
It loads the individual models, the saved SVD projection (if present),
|
| 96 |
+
and the FAISS index so that uploaded images can be embedded and searched
|
| 97 |
+
in the same latent space.
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
def __init__(self, config: dict, config_path: Path, device: Optional[str] = None):
|
| 101 |
+
self.config = config
|
| 102 |
+
self.config_path = config_path.resolve()
|
| 103 |
+
self.config_dir = self.config_path.parent.resolve()
|
| 104 |
+
self.device = torch.device(device or "cpu")
|
| 105 |
+
self._faiss_lock = threading.Lock()
|
| 106 |
+
|
| 107 |
+
self.skinmap_root_path = self._resolve_skinmap_root(config.get("skinmap_root"))
|
| 108 |
+
self.output_dir = self._resolve_output_dir(config.get("output_dir"))
|
| 109 |
+
self._model_search_roots = self._build_model_search_roots()
|
| 110 |
+
|
| 111 |
+
self.clip_models: List[ClipModelWrapper] = []
|
| 112 |
+
self.ssl_models: List[SSLModelWrapper] = []
|
| 113 |
+
self.models_in_order: List[Tuple[str, Any]] = (
|
| 114 |
+
[]
|
| 115 |
+
) # (type, wrapper) preserving config order
|
| 116 |
+
|
| 117 |
+
self._load_models()
|
| 118 |
+
|
| 119 |
+
# Load SVD (legacy pipeline) or projector (new pipeline)
|
| 120 |
+
self.svd_image = self._load_joblib(config.get("svd", {}).get("image"))
|
| 121 |
+
self.projector_model = None
|
| 122 |
+
self.whitening_stats = None
|
| 123 |
+
self._load_projector_if_available()
|
| 124 |
+
|
| 125 |
+
artifacts = config.get("artifacts", {})
|
| 126 |
+
self._embeddings_npz_path = self._resolve_artifact(
|
| 127 |
+
artifacts.get("embeddings_npz")
|
| 128 |
+
)
|
| 129 |
+
self._faiss_index_path = self._resolve_artifact(artifacts.get("faiss_index"))
|
| 130 |
+
self.faiss_index = self._load_faiss(self._faiss_index_path)
|
| 131 |
+
self.vector_dim = config.get("vector", {}).get("dimension")
|
| 132 |
+
self.index_metric = "cosine" # Matches build_faiss_index default
|
| 133 |
+
self.umap_model = self._load_umap(config.get("umap", {}).get("model"))
|
| 134 |
+
|
| 135 |
+
@classmethod
|
| 136 |
+
def from_config(cls, path: str | os.PathLike, device: Optional[str] = None):
|
| 137 |
+
path = Path(path)
|
| 138 |
+
with open(path, "r") as f:
|
| 139 |
+
config = json.load(f)
|
| 140 |
+
return cls(config=config, config_path=path, device=device)
|
| 141 |
+
|
| 142 |
+
def _resolve_artifact(self, rel_path: Optional[str]) -> Optional[Path]:
|
| 143 |
+
if rel_path is None:
|
| 144 |
+
return None
|
| 145 |
+
return (self.output_dir / rel_path).resolve()
|
| 146 |
+
|
| 147 |
+
def _resolve_skinmap_root(self, raw_root: Optional[str]) -> Optional[Path]:
|
| 148 |
+
candidate_roots: List[Path] = []
|
| 149 |
+
|
| 150 |
+
def _add_candidate(path_like: Optional[str | os.PathLike]):
|
| 151 |
+
if not path_like:
|
| 152 |
+
return
|
| 153 |
+
path_obj = Path(path_like)
|
| 154 |
+
if not path_obj.is_absolute():
|
| 155 |
+
path_obj = (self.config_dir / path_obj).resolve()
|
| 156 |
+
else:
|
| 157 |
+
path_obj = path_obj.resolve()
|
| 158 |
+
candidate_roots.append(path_obj)
|
| 159 |
+
|
| 160 |
+
if raw_root:
|
| 161 |
+
_add_candidate(raw_root)
|
| 162 |
+
raw_path = Path(raw_root)
|
| 163 |
+
if not raw_path.is_absolute():
|
| 164 |
+
_add_candidate(self.config_dir.parent / raw_path)
|
| 165 |
+
|
| 166 |
+
# Walk up the config directory hierarchy to find the project root.
|
| 167 |
+
for ancestor in self.config_dir.parents:
|
| 168 |
+
candidate_roots.append(ancestor.resolve())
|
| 169 |
+
|
| 170 |
+
seen: set[str] = set()
|
| 171 |
+
for candidate in candidate_roots:
|
| 172 |
+
key = str(candidate)
|
| 173 |
+
if key in seen:
|
| 174 |
+
continue
|
| 175 |
+
seen.add(key)
|
| 176 |
+
if not candidate.exists():
|
| 177 |
+
continue
|
| 178 |
+
# Heuristic: the SkinMap repo root contains src/combined_embedder.py
|
| 179 |
+
if (candidate / "src" / "combined_embedder.py").exists():
|
| 180 |
+
if key not in sys.path:
|
| 181 |
+
sys.path.append(key)
|
| 182 |
+
return candidate
|
| 183 |
+
|
| 184 |
+
logger.warning(
|
| 185 |
+
"Unable to resolve SkinMap project root from config %s; "
|
| 186 |
+
"nearest-neighbor upload search may fail.",
|
| 187 |
+
self.config_path,
|
| 188 |
+
)
|
| 189 |
+
return None
|
| 190 |
+
|
| 191 |
+
def _resolve_with_bases(
|
| 192 |
+
self,
|
| 193 |
+
path_value: str | os.PathLike,
|
| 194 |
+
bases: Iterable[Optional[Path]],
|
| 195 |
+
*,
|
| 196 |
+
must_exist: bool = False,
|
| 197 |
+
) -> Optional[Path]:
|
| 198 |
+
path_obj = Path(path_value)
|
| 199 |
+
candidates: List[Path] = []
|
| 200 |
+
|
| 201 |
+
if path_obj.is_absolute():
|
| 202 |
+
candidates.append(path_obj)
|
| 203 |
+
else:
|
| 204 |
+
candidates.append((Path.cwd() / path_obj).resolve())
|
| 205 |
+
for base in bases:
|
| 206 |
+
if base is None:
|
| 207 |
+
continue
|
| 208 |
+
candidates.append((base / path_obj).resolve())
|
| 209 |
+
|
| 210 |
+
for candidate in candidates:
|
| 211 |
+
if candidate.exists():
|
| 212 |
+
return candidate
|
| 213 |
+
if must_exist:
|
| 214 |
+
return None
|
| 215 |
+
return candidates[0] if candidates else None
|
| 216 |
+
|
| 217 |
+
def _resolve_output_dir(self, raw_output: Optional[str]) -> Path:
|
| 218 |
+
if not raw_output:
|
| 219 |
+
return self.config_dir
|
| 220 |
+
|
| 221 |
+
bases: List[Optional[Path]] = [
|
| 222 |
+
self.skinmap_root_path,
|
| 223 |
+
self.config_dir,
|
| 224 |
+
(
|
| 225 |
+
self.config_dir.parent
|
| 226 |
+
if self.config_dir.parent != self.config_dir
|
| 227 |
+
else None
|
| 228 |
+
),
|
| 229 |
+
]
|
| 230 |
+
resolved = self._resolve_with_bases(raw_output, bases, must_exist=True)
|
| 231 |
+
if resolved is None or not resolved.exists():
|
| 232 |
+
logger.warning(
|
| 233 |
+
"Configured output_dir '%s' could not be resolved relative to %s; "
|
| 234 |
+
"falling back to %s",
|
| 235 |
+
raw_output,
|
| 236 |
+
self.config_path,
|
| 237 |
+
self.config_dir,
|
| 238 |
+
)
|
| 239 |
+
return self.config_dir
|
| 240 |
+
return resolved
|
| 241 |
+
|
| 242 |
+
def _build_model_search_roots(self) -> List[Path]:
|
| 243 |
+
roots: List[Path] = []
|
| 244 |
+
for candidate in (
|
| 245 |
+
self.output_dir,
|
| 246 |
+
self.output_dir.parent,
|
| 247 |
+
self.skinmap_root_path,
|
| 248 |
+
self.skinmap_root_path / "assets" if self.skinmap_root_path else None,
|
| 249 |
+
self.config_dir,
|
| 250 |
+
):
|
| 251 |
+
if candidate is None:
|
| 252 |
+
continue
|
| 253 |
+
resolved = candidate.resolve()
|
| 254 |
+
if resolved not in roots:
|
| 255 |
+
roots.append(resolved)
|
| 256 |
+
return roots
|
| 257 |
+
|
| 258 |
+
def _resolve_model_path(self, source: str) -> Optional[Path]:
|
| 259 |
+
resolved = self._resolve_with_bases(
|
| 260 |
+
source, self._model_search_roots, must_exist=False
|
| 261 |
+
)
|
| 262 |
+
if resolved is not None and resolved.exists():
|
| 263 |
+
return resolved
|
| 264 |
+
return None
|
| 265 |
+
|
| 266 |
+
def _load_models(self):
|
| 267 |
+
models_config: Sequence[dict] = self.config.get("models", [])
|
| 268 |
+
if not models_config:
|
| 269 |
+
raise ValueError("No model definitions found in configuration.")
|
| 270 |
+
|
| 271 |
+
ssl_transform = get_imagenet_transform()
|
| 272 |
+
for entry in models_config:
|
| 273 |
+
source = entry.get("source")
|
| 274 |
+
if source is None:
|
| 275 |
+
continue
|
| 276 |
+
resolved_path = self._resolve_model_path(source)
|
| 277 |
+
resolved_source = str(resolved_path) if resolved_path else source
|
| 278 |
+
model_type = entry.get("type") or (
|
| 279 |
+
"ssl" if source in SSL_MODEL_NAMES else "clip"
|
| 280 |
+
)
|
| 281 |
+
if model_type == "ssl":
|
| 282 |
+
if resolved_path is not None and entry.get("is_local"):
|
| 283 |
+
from skinmap_runtime._compat import load_ssl_local
|
| 284 |
+
|
| 285 |
+
model, _info, _cfg = load_ssl_local(
|
| 286 |
+
str(resolved_path), n_head_layers=0
|
| 287 |
+
)
|
| 288 |
+
else:
|
| 289 |
+
model, _info, _cfg = Embedder.load_pretrained(
|
| 290 |
+
source, return_info=True, n_head_layers=0
|
| 291 |
+
)
|
| 292 |
+
model.to(self.device)
|
| 293 |
+
wrapper = SSLModelWrapper(
|
| 294 |
+
name=source, model=model, transform=ssl_transform
|
| 295 |
+
)
|
| 296 |
+
self.ssl_models.append(wrapper)
|
| 297 |
+
self.models_in_order.append(("ssl", wrapper))
|
| 298 |
+
else:
|
| 299 |
+
model, processor = load_model_and_processor(
|
| 300 |
+
resolved_source, self.device
|
| 301 |
+
)
|
| 302 |
+
wrapper = ClipModelWrapper(
|
| 303 |
+
name=source, model=model, processor=processor
|
| 304 |
+
)
|
| 305 |
+
self.clip_models.append(wrapper)
|
| 306 |
+
self.models_in_order.append(("clip", wrapper))
|
| 307 |
+
|
| 308 |
+
def _load_joblib(self, rel_path: Optional[str]):
|
| 309 |
+
resolved = self._resolve_artifact(rel_path)
|
| 310 |
+
if resolved is None or not resolved.exists():
|
| 311 |
+
return None
|
| 312 |
+
try:
|
| 313 |
+
logger.info("Loading model from %s", resolved)
|
| 314 |
+
return joblib.load(resolved)
|
| 315 |
+
except Exception as exc:
|
| 316 |
+
logger.warning("Failed to load model from %s: %s", resolved, exc)
|
| 317 |
+
return None
|
| 318 |
+
|
| 319 |
+
def _load_umap(self, rel_path: Optional[str]):
|
| 320 |
+
model = self._load_joblib(rel_path)
|
| 321 |
+
if model is None:
|
| 322 |
+
# Retry with NNDescent monkey-patch for pynndescent version mismatch.
|
| 323 |
+
# Newer pynndescent adds a `quantization` attribute that older versions
|
| 324 |
+
# lack, causing joblib.load to fail.
|
| 325 |
+
resolved = self._resolve_artifact(rel_path)
|
| 326 |
+
if resolved is not None and resolved.exists():
|
| 327 |
+
try:
|
| 328 |
+
os.environ.setdefault("NUMBA_CACHE_DIR", "/tmp/numba_cache")
|
| 329 |
+
import pynndescent
|
| 330 |
+
|
| 331 |
+
NNDescent = pynndescent.NNDescent
|
| 332 |
+
if not hasattr(NNDescent, "quantization"):
|
| 333 |
+
NNDescent.quantization = None # type: ignore[attr-defined]
|
| 334 |
+
logger.info(
|
| 335 |
+
"Patched NNDescent with missing 'quantization' attribute"
|
| 336 |
+
)
|
| 337 |
+
if not hasattr(NNDescent, "_min_distance"):
|
| 338 |
+
NNDescent._min_distance = None # type: ignore[attr-defined]
|
| 339 |
+
logger.info(
|
| 340 |
+
"Patched NNDescent with missing '_min_distance' attribute"
|
| 341 |
+
)
|
| 342 |
+
model = joblib.load(resolved)
|
| 343 |
+
except Exception as exc:
|
| 344 |
+
logger.warning(
|
| 345 |
+
"Failed to load UMAP model even after NNDescent patch: %s", exc
|
| 346 |
+
)
|
| 347 |
+
model = None
|
| 348 |
+
if model is not None:
|
| 349 |
+
logger.info("UMAP model loaded for upload projections")
|
| 350 |
+
return model
|
| 351 |
+
|
| 352 |
+
def _load_projector_if_available(self):
|
| 353 |
+
"""Load trained projector model if available in config."""
|
| 354 |
+
projector_cfg = self.config.get("projector", {})
|
| 355 |
+
if not projector_cfg.get("used", False):
|
| 356 |
+
logger.info("No trained projector in config; using legacy SVD pipeline")
|
| 357 |
+
return
|
| 358 |
+
|
| 359 |
+
# Load projector config
|
| 360 |
+
projector_config_path = self._resolve_artifact(projector_cfg.get("config"))
|
| 361 |
+
if projector_config_path is None or not projector_config_path.exists():
|
| 362 |
+
logger.warning("Projector config not found; falling back to SVD pipeline")
|
| 363 |
+
return
|
| 364 |
+
|
| 365 |
+
try:
|
| 366 |
+
with open(projector_config_path, "r") as f:
|
| 367 |
+
proj_config = json.load(f)
|
| 368 |
+
except Exception as exc:
|
| 369 |
+
logger.warning(f"Failed to load projector config: {exc}")
|
| 370 |
+
return
|
| 371 |
+
|
| 372 |
+
# Load whitening stats
|
| 373 |
+
whitening_path = self._resolve_artifact(projector_cfg.get("whitening_stats"))
|
| 374 |
+
if whitening_path is None or not whitening_path.exists():
|
| 375 |
+
logger.warning("Whitening stats not found; falling back to SVD pipeline")
|
| 376 |
+
return
|
| 377 |
+
|
| 378 |
+
try:
|
| 379 |
+
with np.load(whitening_path, allow_pickle=True) as data:
|
| 380 |
+
files = set(data.files)
|
| 381 |
+
dims = list(data["dims"])
|
| 382 |
+
n_models = int(data["n_models"]) if "n_models" in files else len(dims)
|
| 383 |
+
|
| 384 |
+
# Load mu/W arrays if they exist (skip_whitening=False case)
|
| 385 |
+
mu_list = []
|
| 386 |
+
W_list = []
|
| 387 |
+
if n_models > 0:
|
| 388 |
+
if "mu" in files:
|
| 389 |
+
mu_list = list(data["mu"])
|
| 390 |
+
else:
|
| 391 |
+
# Try to load individual mu_i files
|
| 392 |
+
try:
|
| 393 |
+
mu_list = [data[f"mu_{i}"] for i in range(n_models)]
|
| 394 |
+
except KeyError:
|
| 395 |
+
logger.warning(
|
| 396 |
+
"No mu whitening parameters found (skip_whitening=True?)"
|
| 397 |
+
)
|
| 398 |
+
|
| 399 |
+
if "W" in files:
|
| 400 |
+
W_list = list(data["W"])
|
| 401 |
+
else:
|
| 402 |
+
# Try to load individual W_i files
|
| 403 |
+
try:
|
| 404 |
+
W_list = [data[f"W_{i}"] for i in range(n_models)]
|
| 405 |
+
except KeyError:
|
| 406 |
+
logger.warning(
|
| 407 |
+
"No W whitening parameters found (skip_whitening=True?)"
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
clip_indices = (
|
| 411 |
+
list(data["clip_indices"]) if "clip_indices" in files else []
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
# Load text whitening if available
|
| 415 |
+
text_mu_list = []
|
| 416 |
+
text_W_list = []
|
| 417 |
+
text_dims = []
|
| 418 |
+
if "text_mu" in files and "text_W" in files:
|
| 419 |
+
text_mu_list = list(data["text_mu"])
|
| 420 |
+
text_W_list = list(data["text_W"])
|
| 421 |
+
text_dims = (
|
| 422 |
+
list(data["text_dims"])
|
| 423 |
+
if "text_dims" in files
|
| 424 |
+
else [w.shape[0] for w in text_W_list]
|
| 425 |
+
)
|
| 426 |
+
elif "n_text_models" in files:
|
| 427 |
+
n_text = int(data["n_text_models"])
|
| 428 |
+
if n_text > 0:
|
| 429 |
+
try:
|
| 430 |
+
text_mu_list = [data[f"text_mu_{i}"] for i in range(n_text)]
|
| 431 |
+
text_W_list = [data[f"text_W_{i}"] for i in range(n_text)]
|
| 432 |
+
text_dims = (
|
| 433 |
+
list(data["text_dims"])
|
| 434 |
+
if "text_dims" in files
|
| 435 |
+
else [w.shape[0] for w in text_W_list]
|
| 436 |
+
)
|
| 437 |
+
except KeyError:
|
| 438 |
+
logger.warning("No text whitening parameters found")
|
| 439 |
+
|
| 440 |
+
self.whitening_stats = {
|
| 441 |
+
"mu": mu_list,
|
| 442 |
+
"W": W_list,
|
| 443 |
+
"dims": dims,
|
| 444 |
+
"clip_indices": clip_indices,
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
if text_mu_list and text_W_list:
|
| 448 |
+
self.whitening_stats["text_whitening"] = {
|
| 449 |
+
"mu": text_mu_list,
|
| 450 |
+
"W": text_W_list,
|
| 451 |
+
"dims": text_dims,
|
| 452 |
+
}
|
| 453 |
+
logger.info(
|
| 454 |
+
f"Loaded text whitening stats for {len(text_mu_list)} CLIP models"
|
| 455 |
+
)
|
| 456 |
+
else:
|
| 457 |
+
self.whitening_stats["text_whitening"] = None
|
| 458 |
+
|
| 459 |
+
if len(mu_list) > 0:
|
| 460 |
+
logger.info(
|
| 461 |
+
f"Loaded whitening stats for {len(self.whitening_stats['mu'])} teachers"
|
| 462 |
+
)
|
| 463 |
+
else:
|
| 464 |
+
logger.info("Whitening was skipped during training (--skip_whitening)")
|
| 465 |
+
logger.info(
|
| 466 |
+
f"CLIP model indices for text: {self.whitening_stats['clip_indices']}"
|
| 467 |
+
)
|
| 468 |
+
except Exception as exc:
|
| 469 |
+
logger.warning(f"Failed to load whitening stats: {exc}")
|
| 470 |
+
return
|
| 471 |
+
|
| 472 |
+
# Load projector model
|
| 473 |
+
projector_model_path = self._resolve_artifact(projector_cfg.get("model"))
|
| 474 |
+
if projector_model_path is None or not projector_model_path.exists():
|
| 475 |
+
logger.warning("Projector model not found; falling back to SVD pipeline")
|
| 476 |
+
return
|
| 477 |
+
|
| 478 |
+
try:
|
| 479 |
+
from skinmap_runtime.embedding_fusion import BuildSpec, build_model
|
| 480 |
+
|
| 481 |
+
# Calculate actual text input dimension from CLIP models
|
| 482 |
+
# text_dim in config is the OUTPUT dimension, but we need INPUT dimension
|
| 483 |
+
clip_indices = self.whitening_stats.get("clip_indices", [])
|
| 484 |
+
if clip_indices and "text_dims" in self.whitening_stats:
|
| 485 |
+
# Use text_dims from whitening stats (these are the actual input dims)
|
| 486 |
+
text_input_dim = sum(self.whitening_stats["text_dims"])
|
| 487 |
+
logger.info(f"Calculated text input dimension: {text_input_dim}")
|
| 488 |
+
else:
|
| 489 |
+
# Fallback: calculate from teacher_dims at clip_indices
|
| 490 |
+
teacher_dims = proj_config["teacher_dims"]
|
| 491 |
+
text_input_dim = (
|
| 492 |
+
sum(teacher_dims[i] for i in clip_indices)
|
| 493 |
+
if clip_indices
|
| 494 |
+
else teacher_dims[0]
|
| 495 |
+
)
|
| 496 |
+
logger.info(
|
| 497 |
+
f"Calculated text input dimension from teacher_dims: {text_input_dim}"
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
spec = BuildSpec(
|
| 501 |
+
teacher_names=proj_config.get("model_paths", []),
|
| 502 |
+
teacher_dims=proj_config["teacher_dims"],
|
| 503 |
+
text_dim=text_input_dim, # Use calculated INPUT dimension, not output
|
| 504 |
+
out_dim=proj_config["projector_dim"],
|
| 505 |
+
pca_image=None,
|
| 506 |
+
pca_text=None,
|
| 507 |
+
kind=proj_config["projector_type"],
|
| 508 |
+
)
|
| 509 |
+
|
| 510 |
+
self.projector_model = build_model(spec)
|
| 511 |
+
self.projector_model.load_state_dict(
|
| 512 |
+
torch.load(
|
| 513 |
+
projector_model_path, map_location=self.device, weights_only=False
|
| 514 |
+
)
|
| 515 |
+
)
|
| 516 |
+
self.projector_model.to(self.device)
|
| 517 |
+
self.projector_model.eval()
|
| 518 |
+
logger.info(
|
| 519 |
+
f"Loaded trained projector ({proj_config['projector_type']}, dim={proj_config['projector_dim']})"
|
| 520 |
+
)
|
| 521 |
+
except Exception as exc:
|
| 522 |
+
logger.warning(f"Failed to load projector model: {exc}")
|
| 523 |
+
self.projector_model = None
|
| 524 |
+
self.whitening_stats = None
|
| 525 |
+
|
| 526 |
+
def _load_faiss(self, path: Optional[Path]):
|
| 527 |
+
if path is None or not path.exists():
|
| 528 |
+
return None
|
| 529 |
+
try:
|
| 530 |
+
logger.info("Loading FAISS index from %s", path)
|
| 531 |
+
return faiss.read_index(str(path))
|
| 532 |
+
except Exception as exc:
|
| 533 |
+
logger.warning("Failed to load FAISS index from %s: %s", path, exc)
|
| 534 |
+
return None
|
| 535 |
+
|
| 536 |
+
def _ensure_faiss_index(self) -> faiss.Index:
|
| 537 |
+
index = self.faiss_index
|
| 538 |
+
if index is not None:
|
| 539 |
+
return index
|
| 540 |
+
if self._embeddings_npz_path is None or not self._embeddings_npz_path.exists():
|
| 541 |
+
raise RuntimeError(
|
| 542 |
+
"FAISS index not available and embeddings_npz artifact is missing."
|
| 543 |
+
)
|
| 544 |
+
with self._faiss_lock:
|
| 545 |
+
if self.faiss_index is not None:
|
| 546 |
+
return self.faiss_index
|
| 547 |
+
self.faiss_index = self._build_faiss_from_embeddings(
|
| 548 |
+
self._embeddings_npz_path
|
| 549 |
+
)
|
| 550 |
+
return self.faiss_index
|
| 551 |
+
|
| 552 |
+
def _build_faiss_from_embeddings(self, npz_path: Path) -> faiss.Index:
|
| 553 |
+
logger.info(
|
| 554 |
+
"Building FAISS index on the fly from %s; this may take several minutes.",
|
| 555 |
+
npz_path,
|
| 556 |
+
)
|
| 557 |
+
try:
|
| 558 |
+
data = np.load(npz_path, mmap_mode="r")
|
| 559 |
+
except Exception as exc:
|
| 560 |
+
raise RuntimeError(
|
| 561 |
+
f"Failed to load embeddings NPZ from {npz_path}: {exc}"
|
| 562 |
+
) from exc
|
| 563 |
+
|
| 564 |
+
if "image_embeddings" not in data:
|
| 565 |
+
data.close()
|
| 566 |
+
raise RuntimeError(
|
| 567 |
+
f"Embeddings NPZ at {npz_path} does not contain 'image_embeddings'."
|
| 568 |
+
)
|
| 569 |
+
|
| 570 |
+
embeddings = data["image_embeddings"]
|
| 571 |
+
if embeddings.ndim != 2:
|
| 572 |
+
data.close()
|
| 573 |
+
raise RuntimeError("Embeddings array must be 2-dimensional.")
|
| 574 |
+
|
| 575 |
+
dim = embeddings.shape[1]
|
| 576 |
+
if self.vector_dim is not None and self.vector_dim != dim:
|
| 577 |
+
logger.warning(
|
| 578 |
+
"Configured vector dimension (%s) does not match embeddings (%s); using %s.",
|
| 579 |
+
self.vector_dim,
|
| 580 |
+
dim,
|
| 581 |
+
dim,
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
index = faiss.IndexFlatIP(dim)
|
| 585 |
+
batch_size = 8192
|
| 586 |
+
total = embeddings.shape[0]
|
| 587 |
+
try:
|
| 588 |
+
for start in range(0, total, batch_size):
|
| 589 |
+
end = min(start + batch_size, total)
|
| 590 |
+
batch = np.array(embeddings[start:end], dtype=np.float32, copy=False)
|
| 591 |
+
norms = np.linalg.norm(batch, axis=1, keepdims=True)
|
| 592 |
+
norms[norms == 0] = 1.0
|
| 593 |
+
batch = batch / norms
|
| 594 |
+
index.add(batch)
|
| 595 |
+
if (start // batch_size) % 50 == 0:
|
| 596 |
+
logger.info(
|
| 597 |
+
"FAISS build progress: %d/%d vectors (%.1f%%)",
|
| 598 |
+
end,
|
| 599 |
+
total,
|
| 600 |
+
(end / total) * 100.0,
|
| 601 |
+
)
|
| 602 |
+
finally:
|
| 603 |
+
data.close()
|
| 604 |
+
logger.info("FAISS index build complete (%d vectors).", total)
|
| 605 |
+
|
| 606 |
+
target_path = self._faiss_index_path
|
| 607 |
+
if target_path is not None:
|
| 608 |
+
try:
|
| 609 |
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
| 610 |
+
faiss.write_index(index, str(target_path))
|
| 611 |
+
logger.info("Cached FAISS index to %s", target_path)
|
| 612 |
+
self._faiss_index_path = target_path
|
| 613 |
+
except Exception as exc:
|
| 614 |
+
logger.warning(
|
| 615 |
+
"Failed to write FAISS index to %s: %s. Continuing without persistence.",
|
| 616 |
+
target_path,
|
| 617 |
+
exc,
|
| 618 |
+
)
|
| 619 |
+
return index
|
| 620 |
+
|
| 621 |
+
def embed_image(self, image: Image.Image) -> np.ndarray:
|
| 622 |
+
"""
|
| 623 |
+
Embed a PIL image into the combined latent space.
|
| 624 |
+
|
| 625 |
+
Uses trained projector if available, otherwise falls back to SVD.
|
| 626 |
+
"""
|
| 627 |
+
if not self.models_in_order:
|
| 628 |
+
raise RuntimeError("No models loaded for embedding.")
|
| 629 |
+
|
| 630 |
+
features: List[np.ndarray] = []
|
| 631 |
+
|
| 632 |
+
# Iterate models in the same order as in config (critical for projector)
|
| 633 |
+
for model_type, model_wrapper in self.models_in_order:
|
| 634 |
+
if model_type == "clip":
|
| 635 |
+
features.append(model_wrapper.embed(image, self.device))
|
| 636 |
+
else: # ssl
|
| 637 |
+
# SSL transforms expect tensor input; ensure consistent preprocessing
|
| 638 |
+
prepared = image.copy().convert("RGB")
|
| 639 |
+
features.append(model_wrapper.embed(prepared, self.device))
|
| 640 |
+
|
| 641 |
+
# Now apply either projector or SVD based on what's available
|
| 642 |
+
if self.projector_model is not None and self.whitening_stats is not None:
|
| 643 |
+
# Use trained projector pipeline
|
| 644 |
+
combined = self._apply_projector_pipeline(features)
|
| 645 |
+
else:
|
| 646 |
+
# Legacy SVD pipeline
|
| 647 |
+
combined = np.concatenate(
|
| 648 |
+
[feat.astype(np.float32) for feat in features], axis=1
|
| 649 |
+
)
|
| 650 |
+
|
| 651 |
+
if self.svd_image is not None:
|
| 652 |
+
combined = self.svd_image.transform(combined)
|
| 653 |
+
|
| 654 |
+
if combined.ndim == 2:
|
| 655 |
+
return combined[0]
|
| 656 |
+
return combined
|
| 657 |
+
|
| 658 |
+
def _apply_projector_pipeline(self, features: List[np.ndarray]) -> np.ndarray:
|
| 659 |
+
"""Apply whitening + projector to per-model features."""
|
| 660 |
+
from skinmap_runtime.embedding_fusion import apply_whiten, l2_normalize
|
| 661 |
+
|
| 662 |
+
# Check if whitening was used (mu/W arrays present)
|
| 663 |
+
has_whitening = (
|
| 664 |
+
self.whitening_stats is not None
|
| 665 |
+
and "mu" in self.whitening_stats
|
| 666 |
+
and "W" in self.whitening_stats
|
| 667 |
+
and len(self.whitening_stats["mu"]) > 0
|
| 668 |
+
and len(self.whitening_stats["W"]) > 0
|
| 669 |
+
)
|
| 670 |
+
|
| 671 |
+
# Step 1: L2 normalize and optionally whiten each model's embeddings
|
| 672 |
+
processed = []
|
| 673 |
+
for i, feat in enumerate(features):
|
| 674 |
+
# L2 normalize
|
| 675 |
+
feat_norm = l2_normalize(feat, axis=1)
|
| 676 |
+
|
| 677 |
+
# Whiten if whitening stats are available
|
| 678 |
+
if has_whitening:
|
| 679 |
+
feat_processed = apply_whiten(
|
| 680 |
+
feat_norm,
|
| 681 |
+
self.whitening_stats["mu"][i],
|
| 682 |
+
self.whitening_stats["W"][i],
|
| 683 |
+
)
|
| 684 |
+
else:
|
| 685 |
+
# No whitening - projector was trained on raw L2-normalized embeddings
|
| 686 |
+
feat_processed = feat_norm
|
| 687 |
+
|
| 688 |
+
processed.append(feat_processed)
|
| 689 |
+
|
| 690 |
+
# Step 2: Concatenate
|
| 691 |
+
z_cat = np.concatenate(processed, axis=1).astype(np.float32)
|
| 692 |
+
|
| 693 |
+
# Step 3: L2-normalize each teacher block (matches training pipeline)
|
| 694 |
+
dims = self.whitening_stats["dims"]
|
| 695 |
+
start = 0
|
| 696 |
+
for dim in dims:
|
| 697 |
+
block = z_cat[:, start : start + dim]
|
| 698 |
+
norms = np.linalg.norm(block, axis=1, keepdims=True)
|
| 699 |
+
norms[norms == 0] = 1.0
|
| 700 |
+
z_cat[:, start : start + dim] = block / norms
|
| 701 |
+
start += dim
|
| 702 |
+
|
| 703 |
+
# Step 4: Project through trained model
|
| 704 |
+
z_cat_tensor = torch.from_numpy(z_cat).to(self.device)
|
| 705 |
+
with torch.inference_mode():
|
| 706 |
+
projected = self.projector_model.img_head(z_cat_tensor)
|
| 707 |
+
|
| 708 |
+
return projected.detach().cpu().numpy()
|
| 709 |
+
|
| 710 |
+
def embed_bytes(self, data: bytes) -> np.ndarray:
|
| 711 |
+
image = Image.open(io.BytesIO(data)).convert("RGB")
|
| 712 |
+
return self.embed_image(image)
|
| 713 |
+
|
| 714 |
+
# Default prompt templates for text search. CLIP models are trained on
|
| 715 |
+
# image-caption pairs so bare keywords ("melanoma") live in a very
|
| 716 |
+
# different part of the embedding space than descriptive captions.
|
| 717 |
+
# Wrapping the query in templates and averaging the resulting embeddings
|
| 718 |
+
# brings the query much closer to the image distribution.
|
| 719 |
+
DEFAULT_TEXT_TEMPLATES: List[str] = [
|
| 720 |
+
"This image shows {}",
|
| 721 |
+
"a clinical photograph of {}",
|
| 722 |
+
"a dermatoscopic image of {}",
|
| 723 |
+
"a medical image showing {}",
|
| 724 |
+
"a close-up photograph of {} on skin",
|
| 725 |
+
]
|
| 726 |
+
|
| 727 |
+
def encode_text(
|
| 728 |
+
self,
|
| 729 |
+
text: str,
|
| 730 |
+
templates: Optional[List[str]] = None,
|
| 731 |
+
) -> np.ndarray:
|
| 732 |
+
"""Encode a text query into the shared embedding space.
|
| 733 |
+
|
| 734 |
+
When *templates* is provided (or defaults are used), the query is
|
| 735 |
+
formatted into each template, encoded separately, and the resulting
|
| 736 |
+
embeddings are averaged and re-normalized. Pass ``templates=["{}"]``
|
| 737 |
+
to encode the raw text without any prompt engineering.
|
| 738 |
+
|
| 739 |
+
For projector pipeline: extracts text embeddings from CLIP models only,
|
| 740 |
+
concatenates them, and projects through the text head.
|
| 741 |
+
|
| 742 |
+
For SVD pipeline: uses first CLIP model only.
|
| 743 |
+
"""
|
| 744 |
+
if not self.clip_models:
|
| 745 |
+
raise RuntimeError("No CLIP models loaded. Cannot encode text.")
|
| 746 |
+
|
| 747 |
+
if templates is None:
|
| 748 |
+
templates = self.DEFAULT_TEXT_TEMPLATES
|
| 749 |
+
|
| 750 |
+
# Expand the query into all templates
|
| 751 |
+
prompts = [t.format(text) for t in templates]
|
| 752 |
+
|
| 753 |
+
# Encode all prompts and average. With a trained projector, all prompts
|
| 754 |
+
# go through each CLIP teacher in one batched forward (C forwards, not
|
| 755 |
+
# N*C). The legacy pipeline falls back to per-prompt encoding.
|
| 756 |
+
if self.projector_model is not None and self.whitening_stats is not None:
|
| 757 |
+
per_prompt = self._encode_texts_projector(prompts)
|
| 758 |
+
else:
|
| 759 |
+
per_prompt = np.stack(
|
| 760 |
+
[self._encode_single_text(p) for p in prompts], axis=0
|
| 761 |
+
)
|
| 762 |
+
avg = per_prompt.mean(axis=0)
|
| 763 |
+
avg = avg / (np.linalg.norm(avg) + 1e-8)
|
| 764 |
+
return avg.astype(np.float32)
|
| 765 |
+
|
| 766 |
+
def _encode_texts_projector(self, texts: List[str]) -> np.ndarray:
|
| 767 |
+
"""Batched projector-path text encoding.
|
| 768 |
+
|
| 769 |
+
Returns an ``(N, d_out)`` array of projected, L2-normalized text
|
| 770 |
+
embeddings, one per input string. All prompts pass through each CLIP
|
| 771 |
+
teacher in a single padded forward. CLIP's EOS-token pooling is
|
| 772 |
+
right-padding invariant, so this matches per-prompt encoding.
|
| 773 |
+
"""
|
| 774 |
+
from skinmap_runtime.embedding_fusion import apply_whiten, l2_normalize
|
| 775 |
+
|
| 776 |
+
clip_indices = self.whitening_stats.get("clip_indices", [])
|
| 777 |
+
if not clip_indices:
|
| 778 |
+
clip_indices = list(range(len(self.clip_models)))
|
| 779 |
+
clip_index_set = set(clip_indices)
|
| 780 |
+
text_whitening = self.whitening_stats.get("text_whitening")
|
| 781 |
+
|
| 782 |
+
per_model = [] # each (N, d_i)
|
| 783 |
+
clip_idx_counter = 0
|
| 784 |
+
for i, (_model_type, model_wrapper) in enumerate(self.models_in_order):
|
| 785 |
+
if i not in clip_index_set:
|
| 786 |
+
continue
|
| 787 |
+
inputs = model_wrapper.processor(
|
| 788 |
+
text=list(texts), return_tensors="pt", padding=True
|
| 789 |
+
)
|
| 790 |
+
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
| 791 |
+
with torch.inference_mode():
|
| 792 |
+
text_emb = _coerce_clip_features(
|
| 793 |
+
model_wrapper.model.get_text_features(**inputs), "text_embeds"
|
| 794 |
+
)
|
| 795 |
+
text_emb = text_emb / text_emb.norm(p=2, dim=-1, keepdim=True)
|
| 796 |
+
feat = text_emb.cpu().numpy() # (N, d_i)
|
| 797 |
+
if text_whitening is not None:
|
| 798 |
+
feat = apply_whiten(
|
| 799 |
+
l2_normalize(feat, axis=1),
|
| 800 |
+
text_whitening["mu"][clip_idx_counter],
|
| 801 |
+
text_whitening["W"][clip_idx_counter],
|
| 802 |
+
)
|
| 803 |
+
per_model.append(feat)
|
| 804 |
+
clip_idx_counter += 1
|
| 805 |
+
|
| 806 |
+
# Concatenate per-teacher blocks, L2-normalize each row, project.
|
| 807 |
+
text_concat = np.concatenate(per_model, axis=1).astype(np.float32)
|
| 808 |
+
norms = np.linalg.norm(text_concat, axis=1, keepdims=True)
|
| 809 |
+
norms[norms == 0] = 1.0
|
| 810 |
+
text_concat = text_concat / norms
|
| 811 |
+
text_tensor = torch.from_numpy(text_concat).to(self.device)
|
| 812 |
+
with torch.inference_mode():
|
| 813 |
+
projected = self.projector_model.txt_head(text_tensor)
|
| 814 |
+
return projected.cpu().numpy() # (N, d_out)
|
| 815 |
+
|
| 816 |
+
def _encode_single_text(self, text: str) -> np.ndarray:
|
| 817 |
+
"""Encode a single text string (no template expansion)."""
|
| 818 |
+
if self.projector_model is not None and self.whitening_stats is not None:
|
| 819 |
+
# Projector pipeline: concatenate text from CLIP models only
|
| 820 |
+
from skinmap_runtime.embedding_fusion import apply_whiten, l2_normalize
|
| 821 |
+
|
| 822 |
+
clip_indices = self.whitening_stats.get("clip_indices", [])
|
| 823 |
+
if not clip_indices:
|
| 824 |
+
# Fallback: use all CLIP models if indices not available
|
| 825 |
+
clip_indices = list(range(len(self.clip_models)))
|
| 826 |
+
|
| 827 |
+
text_features = []
|
| 828 |
+
clip_idx_counter = 0
|
| 829 |
+
for i, (model_type, model_wrapper) in enumerate(self.models_in_order):
|
| 830 |
+
if i in clip_indices:
|
| 831 |
+
# This is a CLIP model, extract text embedding
|
| 832 |
+
inputs = model_wrapper.processor(text=[text], return_tensors="pt")
|
| 833 |
+
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
| 834 |
+
with torch.inference_mode():
|
| 835 |
+
text_emb = _coerce_clip_features(
|
| 836 |
+
model_wrapper.model.get_text_features(**inputs),
|
| 837 |
+
"text_embeds",
|
| 838 |
+
)
|
| 839 |
+
# Normalize
|
| 840 |
+
text_emb = text_emb / text_emb.norm(p=2, dim=-1, keepdim=True)
|
| 841 |
+
text_feat = text_emb.cpu().numpy()
|
| 842 |
+
|
| 843 |
+
# Apply text whitening if available
|
| 844 |
+
text_whitening = self.whitening_stats.get("text_whitening")
|
| 845 |
+
if text_whitening is not None:
|
| 846 |
+
text_feat_normed = l2_normalize(text_feat, axis=1)
|
| 847 |
+
text_feat_whitened = apply_whiten(
|
| 848 |
+
text_feat_normed,
|
| 849 |
+
text_whitening["mu"][clip_idx_counter],
|
| 850 |
+
text_whitening["W"][clip_idx_counter],
|
| 851 |
+
)
|
| 852 |
+
text_features.append(text_feat_whitened[0])
|
| 853 |
+
else:
|
| 854 |
+
text_features.append(text_feat[0])
|
| 855 |
+
|
| 856 |
+
clip_idx_counter += 1
|
| 857 |
+
|
| 858 |
+
# Concatenate text features from all CLIP models
|
| 859 |
+
text_concat = np.concatenate(text_features).astype(np.float32)
|
| 860 |
+
|
| 861 |
+
# L2-normalize the concatenated text vector (matches training pipeline)
|
| 862 |
+
norm = np.linalg.norm(text_concat)
|
| 863 |
+
if norm > 0:
|
| 864 |
+
text_concat = text_concat / norm
|
| 865 |
+
|
| 866 |
+
# Project through text head
|
| 867 |
+
text_tensor = torch.from_numpy(text_concat).unsqueeze(0).to(self.device)
|
| 868 |
+
with torch.inference_mode():
|
| 869 |
+
projected = self.projector_model.txt_head(text_tensor)
|
| 870 |
+
|
| 871 |
+
return projected.cpu().numpy()[0]
|
| 872 |
+
else:
|
| 873 |
+
# Legacy SVD pipeline or no projector: use first CLIP model
|
| 874 |
+
clip_model = self.clip_models[0]
|
| 875 |
+
inputs = clip_model.processor(text=[text], return_tensors="pt")
|
| 876 |
+
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
| 877 |
+
with torch.inference_mode():
|
| 878 |
+
text_emb = _coerce_clip_features(
|
| 879 |
+
clip_model.model.get_text_features(**inputs), "text_embeds"
|
| 880 |
+
)
|
| 881 |
+
text_emb = text_emb / text_emb.norm(p=2, dim=-1, keepdim=True)
|
| 882 |
+
|
| 883 |
+
# If SVD available, apply it
|
| 884 |
+
text_feat = text_emb.cpu().numpy()
|
| 885 |
+
if self.svd_image is not None:
|
| 886 |
+
# Note: this assumes SVD was fitted on concatenated features
|
| 887 |
+
# For single model, this might not work well
|
| 888 |
+
pass
|
| 889 |
+
|
| 890 |
+
return text_feat[0]
|
| 891 |
+
|
| 892 |
+
def find_nearest_neighbors(
|
| 893 |
+
self, vector: np.ndarray, k: int = 16
|
| 894 |
+
) -> Tuple[List[int], List[float]]:
|
| 895 |
+
index = self._ensure_faiss_index()
|
| 896 |
+
vec = vector.astype(np.float32)
|
| 897 |
+
norm = np.linalg.norm(vec)
|
| 898 |
+
if norm > 0:
|
| 899 |
+
vec = vec / norm
|
| 900 |
+
similarities, indices = index.search(vec.reshape(1, -1), k)
|
| 901 |
+
flat_indices = indices.reshape(-1).tolist()
|
| 902 |
+
flat_distances = (1.0 - similarities.reshape(-1)).tolist()
|
| 903 |
+
return flat_indices, flat_distances
|
| 904 |
+
|
| 905 |
+
def project_vector(self, vector: np.ndarray) -> Optional[np.ndarray]:
|
| 906 |
+
if self.umap_model is None:
|
| 907 |
+
logger.warning(
|
| 908 |
+
"Upload embedding pipeline has no UMAP reducer; skipping projection. "
|
| 909 |
+
"Ensure the embedding pipeline config includes a saved UMAP model."
|
| 910 |
+
)
|
| 911 |
+
return None
|
| 912 |
+
try:
|
| 913 |
+
projected = self.umap_model.transform(vector.reshape(1, -1))
|
| 914 |
+
if projected.ndim == 2 and projected.shape[1] >= 2:
|
| 915 |
+
return projected[0, :2].astype(float)
|
| 916 |
+
except Exception as exc:
|
| 917 |
+
logger.warning("Failed to project vector with UMAP: %s", exc)
|
| 918 |
+
return None
|
| 919 |
+
|
| 920 |
+
def search_image(
|
| 921 |
+
self, image_bytes: bytes, k: int = 16
|
| 922 |
+
) -> Tuple[List[int], List[float], Optional[np.ndarray]]:
|
| 923 |
+
vector = self.embed_bytes(image_bytes)
|
| 924 |
+
indices, distances = self.find_nearest_neighbors(vector, k=k)
|
| 925 |
+
coords = self.project_vector(vector)
|
| 926 |
+
return indices, distances, coords
|
skinmap_runtime/core/__init__.py
ADDED
|
File without changes
|
skinmap_runtime/core/models/__init__.py
ADDED
|
File without changes
|
skinmap_runtime/core/models/byol/__init__.py
ADDED
|
File without changes
|
skinmap_runtime/core/models/byol/model.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from torch import nn
|
| 2 |
+
|
| 3 |
+
from ..encoders.utils import get_encoder_class
|
| 4 |
+
from ..utils import ModelType
|
| 5 |
+
from .predictor import MLP
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class BYOLModel(nn.Module):
|
| 9 |
+
def __init__(
|
| 10 |
+
self,
|
| 11 |
+
base_model: str,
|
| 12 |
+
projection_size=256,
|
| 13 |
+
projection_hidden_size=4096,
|
| 14 |
+
**kwargs,
|
| 15 |
+
):
|
| 16 |
+
super(BYOLModel, self).__init__()
|
| 17 |
+
encoder_cls, model_type = get_encoder_class(base_model)
|
| 18 |
+
if model_type is ModelType.VIT:
|
| 19 |
+
self.backbone = encoder_cls(**kwargs)
|
| 20 |
+
n_feat = self.backbone.embed_dim
|
| 21 |
+
elif model_type is ModelType.CNN:
|
| 22 |
+
encoder = encoder_cls(**kwargs)
|
| 23 |
+
n_feat = encoder.fc.in_features
|
| 24 |
+
self.backbone = nn.Sequential(*list(encoder.children())[:-1])
|
| 25 |
+
else:
|
| 26 |
+
raise ValueError(f"Unknown model type: {model_type}")
|
| 27 |
+
# projection head
|
| 28 |
+
self.projection = MLP(
|
| 29 |
+
in_channels=n_feat,
|
| 30 |
+
projection_size=projection_size,
|
| 31 |
+
hidden_size=projection_hidden_size,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
def forward(self, x, return_embedding=False):
|
| 35 |
+
# embedding
|
| 36 |
+
e = self.backbone(x)
|
| 37 |
+
e = e.squeeze()
|
| 38 |
+
if return_embedding:
|
| 39 |
+
return e
|
| 40 |
+
# project
|
| 41 |
+
z = self.projection(e)
|
| 42 |
+
return z
|
skinmap_runtime/core/models/byol/predictor.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from torch import nn
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class MLP(nn.Module):
|
| 5 |
+
def __init__(self, in_channels, hidden_size=256, projection_size=4096):
|
| 6 |
+
super(MLP, self).__init__()
|
| 7 |
+
self.net = nn.Sequential(
|
| 8 |
+
nn.Linear(in_channels, hidden_size),
|
| 9 |
+
nn.BatchNorm1d(hidden_size),
|
| 10 |
+
nn.ReLU(inplace=True),
|
| 11 |
+
nn.Linear(hidden_size, projection_size),
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
def forward(self, x):
|
| 15 |
+
return self.net(x)
|
skinmap_runtime/core/models/colorme/__init__.py
ADDED
|
File without changes
|
skinmap_runtime/core/models/colorme/model.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import segmentation_models_pytorch as smp
|
| 2 |
+
from torch import nn
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class MLPHead(nn.Module):
|
| 6 |
+
def __init__(self, out_features: int = 512):
|
| 7 |
+
super(MLPHead, self).__init__()
|
| 8 |
+
self.head = nn.Sequential(
|
| 9 |
+
nn.Linear(out_features, 10),
|
| 10 |
+
nn.Softmax(dim=-1),
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
def forward(self, x):
|
| 14 |
+
return self.head(x)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class ColorMeModel(nn.Module):
|
| 18 |
+
def __init__(self, num_classes=2, encoder_name="resnet18"):
|
| 19 |
+
super(ColorMeModel, self).__init__()
|
| 20 |
+
# create our encoder decoder model
|
| 21 |
+
self.enc_dec_model = smp.Unet(
|
| 22 |
+
encoder_name=encoder_name,
|
| 23 |
+
encoder_weights="imagenet",
|
| 24 |
+
in_channels=1,
|
| 25 |
+
classes=num_classes,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# color distribution MLP head
|
| 29 |
+
if encoder_name == "resnet18":
|
| 30 |
+
self.color_dist_mlp = MLPHead(out_features=512)
|
| 31 |
+
elif encoder_name == "resnet50":
|
| 32 |
+
self.color_dist_mlp = MLPHead(out_features=2048)
|
| 33 |
+
else:
|
| 34 |
+
raise ValueError("Unrecognized encoder")
|
| 35 |
+
|
| 36 |
+
def forward(self, x, return_embedding=False):
|
| 37 |
+
# color reconstruction
|
| 38 |
+
rec = self.enc_dec_model(x)
|
| 39 |
+
# retreive embedding
|
| 40 |
+
emb = self.enc_dec_model.encoder(x)[-1]
|
| 41 |
+
emb = nn.AdaptiveAvgPool2d((1, 1))(emb)
|
| 42 |
+
emb = emb.squeeze()
|
| 43 |
+
# return the embedding if needed
|
| 44 |
+
if return_embedding:
|
| 45 |
+
return emb
|
| 46 |
+
# color distribution
|
| 47 |
+
dist = self.color_dist_mlp(emb)
|
| 48 |
+
|
| 49 |
+
return rec, dist
|
skinmap_runtime/core/models/dino/__init__.py
ADDED
|
File without changes
|
skinmap_runtime/core/models/dino/head.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn as nn
|
| 2 |
+
|
| 3 |
+
from ...models.utils import trunc_normal_
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class DINOHead(nn.Module):
|
| 7 |
+
def __init__(
|
| 8 |
+
self,
|
| 9 |
+
in_dim,
|
| 10 |
+
out_dim,
|
| 11 |
+
use_bn=False,
|
| 12 |
+
norm_last_layer=True,
|
| 13 |
+
n_layers=3,
|
| 14 |
+
hidden_dim=2048,
|
| 15 |
+
bottleneck_dim=256,
|
| 16 |
+
):
|
| 17 |
+
super().__init__()
|
| 18 |
+
|
| 19 |
+
n_layers = max(n_layers, 1)
|
| 20 |
+
if n_layers == 1:
|
| 21 |
+
self.mlp = nn.Linear(in_dim, bottleneck_dim)
|
| 22 |
+
else:
|
| 23 |
+
layers = [nn.Linear(in_dim, hidden_dim)]
|
| 24 |
+
if use_bn:
|
| 25 |
+
layers.append(nn.BatchNorm1d(hidden_dim))
|
| 26 |
+
layers.append(nn.GELU())
|
| 27 |
+
for _ in range(n_layers - 2):
|
| 28 |
+
layers.append(nn.Linear(hidden_dim, hidden_dim))
|
| 29 |
+
if use_bn:
|
| 30 |
+
layers.append(nn.BatchNorm1d(hidden_dim))
|
| 31 |
+
layers.append(nn.GELU())
|
| 32 |
+
layers.append(nn.Linear(hidden_dim, bottleneck_dim))
|
| 33 |
+
self.mlp = nn.Sequential(*layers)
|
| 34 |
+
self.apply(self._init_weights)
|
| 35 |
+
self.last_layer = nn.utils.weight_norm(
|
| 36 |
+
nn.Linear(bottleneck_dim, out_dim, bias=False)
|
| 37 |
+
)
|
| 38 |
+
self.last_layer.weight_g.data.fill_(1)
|
| 39 |
+
if norm_last_layer:
|
| 40 |
+
self.last_layer.weight_g.requires_grad = False
|
| 41 |
+
|
| 42 |
+
def _init_weights(self, m):
|
| 43 |
+
if isinstance(m, nn.Linear):
|
| 44 |
+
trunc_normal_(m.weight, std=0.02)
|
| 45 |
+
if isinstance(m, nn.Linear) and m.bias is not None:
|
| 46 |
+
nn.init.constant_(m.bias, 0)
|
| 47 |
+
|
| 48 |
+
def forward(self, x):
|
| 49 |
+
x = self.mlp(x)
|
| 50 |
+
x = nn.functional.normalize(x, dim=-1, p=2)
|
| 51 |
+
x = self.last_layer(x)
|
| 52 |
+
return x
|
skinmap_runtime/core/models/dino/multi_crop_wrapper.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
from torch import nn
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class MultiCropWrapper(nn.Module):
|
| 6 |
+
"""
|
| 7 |
+
Perform forward pass separately on each resolution input.
|
| 8 |
+
The inputs corresponding to a single resolution are clubbed and single
|
| 9 |
+
forward is run on the same resolution inputs. Hence we do several
|
| 10 |
+
forward passes = number of different resolutions used. We then
|
| 11 |
+
concatenate all the output features and run the head forward on these
|
| 12 |
+
concatenated features.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
def __init__(
|
| 16 |
+
self,
|
| 17 |
+
backbone: torch.nn.Module,
|
| 18 |
+
head: torch.nn.Module,
|
| 19 |
+
apply_l2_norm: float = True,
|
| 20 |
+
):
|
| 21 |
+
super(MultiCropWrapper, self).__init__()
|
| 22 |
+
self.apply_l2_norm = apply_l2_norm
|
| 23 |
+
# disable layers dedicated to ImageNet labels classification
|
| 24 |
+
backbone.fc, backbone.head = nn.Identity(), nn.Identity()
|
| 25 |
+
self.backbone = backbone
|
| 26 |
+
if head is None:
|
| 27 |
+
self.head = nn.Identity()
|
| 28 |
+
else:
|
| 29 |
+
self.head = head
|
| 30 |
+
|
| 31 |
+
def forward(self, x, mask=None, return_backbone_feat=False, **kwargs):
|
| 32 |
+
# convert to list
|
| 33 |
+
if not isinstance(x, list):
|
| 34 |
+
x = [x]
|
| 35 |
+
mask = [mask] if mask is not None else None
|
| 36 |
+
idx_crops = torch.cumsum(
|
| 37 |
+
torch.unique_consecutive(
|
| 38 |
+
torch.tensor([inp.shape[-1] for inp in x]),
|
| 39 |
+
return_counts=True,
|
| 40 |
+
)[1],
|
| 41 |
+
0,
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# start concatenating the features
|
| 45 |
+
start_idx = 0
|
| 46 |
+
output = torch.empty(0).to(x[0].device)
|
| 47 |
+
for end_idx in idx_crops:
|
| 48 |
+
inp_x = torch.cat(x[start_idx:end_idx])
|
| 49 |
+
|
| 50 |
+
if mask is not None:
|
| 51 |
+
inp_m = torch.cat(mask[start_idx:end_idx])
|
| 52 |
+
kwargs.update(dict(mask=inp_m))
|
| 53 |
+
|
| 54 |
+
_out = self.backbone(inp_x, **kwargs)
|
| 55 |
+
if start_idx == 0:
|
| 56 |
+
output = _out
|
| 57 |
+
else:
|
| 58 |
+
output = torch.cat((output, _out))
|
| 59 |
+
start_idx = end_idx
|
| 60 |
+
|
| 61 |
+
# SelfClean: apply L2 norm if requested
|
| 62 |
+
if self.apply_l2_norm:
|
| 63 |
+
output = torch.nn.functional.normalize(output, dim=-1, p=2)
|
| 64 |
+
|
| 65 |
+
# run the head forward on the concatenated features
|
| 66 |
+
output_ = self.head(output)
|
| 67 |
+
if return_backbone_feat:
|
| 68 |
+
return output, output_
|
| 69 |
+
|
| 70 |
+
return output_
|
skinmap_runtime/core/models/encoders/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
skinmap_runtime/core/models/encoders/swin_transformer.py
ADDED
|
@@ -0,0 +1,1050 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Mostly copy-paste from Swin-Transformer library:
|
| 3 |
+
https://github.com/facebookresearch/dino
|
| 4 |
+
https://github.com/microsoft/Swin-Transformer/blob/main/models/swin_transformer.py
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
from functools import partial
|
| 9 |
+
from math import sqrt
|
| 10 |
+
|
| 11 |
+
import numpy as np
|
| 12 |
+
import torch
|
| 13 |
+
import torch.distributed as dist
|
| 14 |
+
import torch.nn as nn
|
| 15 |
+
import torch.nn.functional as F
|
| 16 |
+
from loguru import logger
|
| 17 |
+
|
| 18 |
+
from ..utils import DropPath, to_2tuple, trunc_normal_
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class Mlp(nn.Module):
|
| 22 |
+
def __init__(
|
| 23 |
+
self,
|
| 24 |
+
in_features,
|
| 25 |
+
hidden_features=None,
|
| 26 |
+
out_features=None,
|
| 27 |
+
act_layer=nn.GELU,
|
| 28 |
+
drop=0.0,
|
| 29 |
+
):
|
| 30 |
+
super(Mlp, self).__init__()
|
| 31 |
+
out_features = out_features or in_features
|
| 32 |
+
hidden_features = hidden_features or in_features
|
| 33 |
+
self.fc1 = nn.Linear(in_features, hidden_features)
|
| 34 |
+
self.act = act_layer()
|
| 35 |
+
self.fc2 = nn.Linear(hidden_features, out_features)
|
| 36 |
+
self.drop = nn.Dropout(drop)
|
| 37 |
+
|
| 38 |
+
def forward(self, x):
|
| 39 |
+
x = self.fc1(x)
|
| 40 |
+
x = self.act(x)
|
| 41 |
+
x = self.drop(x)
|
| 42 |
+
x = self.fc2(x)
|
| 43 |
+
x = self.drop(x)
|
| 44 |
+
return x
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def window_partition(x, window_size):
|
| 48 |
+
"""
|
| 49 |
+
Args:
|
| 50 |
+
x: (B, H, W, C)
|
| 51 |
+
window_size (int): window size
|
| 52 |
+
Returns:
|
| 53 |
+
windows: (num_windows*B, window_size, window_size, C)
|
| 54 |
+
"""
|
| 55 |
+
B, H, W, C = x.shape
|
| 56 |
+
x = x.view(B, H // window_size, window_size, W // window_size, window_size, C)
|
| 57 |
+
windows = (
|
| 58 |
+
x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, window_size, window_size, C)
|
| 59 |
+
)
|
| 60 |
+
return windows
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def window_reverse(windows, window_size, H, W):
|
| 64 |
+
"""
|
| 65 |
+
Args:
|
| 66 |
+
windows: (num_windows*B, window_size, window_size, C)
|
| 67 |
+
window_size (int): Window size
|
| 68 |
+
H (int): Height of image
|
| 69 |
+
W (int): Width of image
|
| 70 |
+
Returns:
|
| 71 |
+
x: (B, H, W, C)
|
| 72 |
+
"""
|
| 73 |
+
B = int(windows.shape[0] / (H * W / window_size / window_size))
|
| 74 |
+
x = windows.view(
|
| 75 |
+
B, H // window_size, W // window_size, window_size, window_size, -1
|
| 76 |
+
)
|
| 77 |
+
x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, H, W, -1)
|
| 78 |
+
return x
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class WindowAttention(nn.Module):
|
| 82 |
+
r"""Window based multi-head self attention (W-MSA) module with relative position bias.
|
| 83 |
+
It supports both of shifted and non-shifted window.
|
| 84 |
+
Args:
|
| 85 |
+
dim (int): Number of input channels.
|
| 86 |
+
window_size (tuple[int]): The height and width of the window.
|
| 87 |
+
num_heads (int): Number of attention heads.
|
| 88 |
+
qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True
|
| 89 |
+
qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set
|
| 90 |
+
attn_drop (float, optional): Dropout ratio of attention weight. Default: 0.0
|
| 91 |
+
proj_drop (float, optional): Dropout ratio of output. Default: 0.0
|
| 92 |
+
"""
|
| 93 |
+
|
| 94 |
+
def __init__(
|
| 95 |
+
self,
|
| 96 |
+
dim,
|
| 97 |
+
window_size,
|
| 98 |
+
num_heads,
|
| 99 |
+
qkv_bias=True,
|
| 100 |
+
qk_scale=None,
|
| 101 |
+
attn_drop=0.0,
|
| 102 |
+
proj_drop=0.0,
|
| 103 |
+
):
|
| 104 |
+
super(WindowAttention, self).__init__()
|
| 105 |
+
self.dim = dim
|
| 106 |
+
self.window_size = window_size # Wh, Ww
|
| 107 |
+
self.num_heads = num_heads
|
| 108 |
+
head_dim = dim // num_heads
|
| 109 |
+
self.scale = qk_scale or head_dim**-0.5
|
| 110 |
+
|
| 111 |
+
# define a parameter table of relative position bias
|
| 112 |
+
self.relative_position_bias_table = nn.Parameter(
|
| 113 |
+
torch.zeros((2 * window_size[0] - 1) * (2 * window_size[1] - 1), num_heads)
|
| 114 |
+
) # 2*Wh-1 * 2*Ww-1, nH
|
| 115 |
+
|
| 116 |
+
# get pair-wise relative position index for each token inside the window
|
| 117 |
+
coords_h = torch.arange(self.window_size[0])
|
| 118 |
+
coords_w = torch.arange(self.window_size[1])
|
| 119 |
+
coords = torch.stack(torch.meshgrid([coords_h, coords_w])) # 2, Wh, Ww
|
| 120 |
+
coords_flatten = torch.flatten(coords, 1) # 2 Wh*Ww
|
| 121 |
+
relative_coords = (
|
| 122 |
+
coords_flatten[:, :, None] - coords_flatten[:, None, :]
|
| 123 |
+
) # 2, Wh*Ww, Wh*Ww
|
| 124 |
+
relative_coords = relative_coords.permute(
|
| 125 |
+
1, 2, 0
|
| 126 |
+
).contiguous() # Wh*Ww, Wh*Ww, 2
|
| 127 |
+
relative_coords[:, :, 0] += self.window_size[0] - 1 # shift to start from 0
|
| 128 |
+
relative_coords[:, :, 1] += self.window_size[1] - 1
|
| 129 |
+
relative_coords[:, :, 0] *= 2 * self.window_size[1] - 1
|
| 130 |
+
relative_position_index = relative_coords.sum(-1) # Wh*Ww, Wh*Ww
|
| 131 |
+
self.register_buffer("relative_position_index", relative_position_index)
|
| 132 |
+
|
| 133 |
+
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
|
| 134 |
+
self.attn_drop = nn.Dropout(attn_drop)
|
| 135 |
+
self.proj = nn.Linear(dim, dim)
|
| 136 |
+
self.proj_drop = nn.Dropout(proj_drop)
|
| 137 |
+
|
| 138 |
+
trunc_normal_(self.relative_position_bias_table, std=0.02)
|
| 139 |
+
self.softmax = nn.Softmax(dim=-1)
|
| 140 |
+
|
| 141 |
+
def forward(self, x, mask=None):
|
| 142 |
+
"""
|
| 143 |
+
Args:
|
| 144 |
+
x: input features with shape of (num_windows*B, N, C)
|
| 145 |
+
mask: (0/-inf) mask with shape of (num_windows, Wh*Ww, Wh*Ww) or None
|
| 146 |
+
"""
|
| 147 |
+
B_, N, C = x.shape
|
| 148 |
+
qkv = (
|
| 149 |
+
self.qkv(x)
|
| 150 |
+
.reshape(B_, N, 3, self.num_heads, C // self.num_heads)
|
| 151 |
+
.permute(2, 0, 3, 1, 4)
|
| 152 |
+
)
|
| 153 |
+
q, k, v = (
|
| 154 |
+
qkv[0],
|
| 155 |
+
qkv[1],
|
| 156 |
+
qkv[2],
|
| 157 |
+
) # make torchscript happy (cannot use tensor as tuple)
|
| 158 |
+
|
| 159 |
+
q = q * self.scale
|
| 160 |
+
attn = q @ k.transpose(-2, -1)
|
| 161 |
+
|
| 162 |
+
relative_position_bias = self.relative_position_bias_table[
|
| 163 |
+
self.relative_position_index.view(-1)
|
| 164 |
+
].view(
|
| 165 |
+
self.window_size[0] * self.window_size[1],
|
| 166 |
+
self.window_size[0] * self.window_size[1],
|
| 167 |
+
-1,
|
| 168 |
+
) # Wh*Ww,Wh*Ww,nH
|
| 169 |
+
relative_position_bias = relative_position_bias.permute(
|
| 170 |
+
2, 0, 1
|
| 171 |
+
).contiguous() # nH, Wh*Ww, Wh*Ww
|
| 172 |
+
attn = attn + relative_position_bias.unsqueeze(0)
|
| 173 |
+
|
| 174 |
+
if mask is not None:
|
| 175 |
+
nW = mask.shape[0]
|
| 176 |
+
attn = attn.view(B_ // nW, nW, self.num_heads, N, N) + mask.unsqueeze(
|
| 177 |
+
1
|
| 178 |
+
).unsqueeze(0)
|
| 179 |
+
attn = attn.view(-1, self.num_heads, N, N)
|
| 180 |
+
attn = self.softmax(attn)
|
| 181 |
+
else:
|
| 182 |
+
attn = self.softmax(attn)
|
| 183 |
+
|
| 184 |
+
attn_out = attn
|
| 185 |
+
attn = self.attn_drop(attn)
|
| 186 |
+
|
| 187 |
+
x = (attn @ v).transpose(1, 2).reshape(B_, N, C)
|
| 188 |
+
x = self.proj(x)
|
| 189 |
+
x = self.proj_drop(x)
|
| 190 |
+
return x, attn_out
|
| 191 |
+
|
| 192 |
+
def extra_repr(self) -> str:
|
| 193 |
+
return f"dim={self.dim}, window_size={self.window_size}, num_heads={self.num_heads}"
|
| 194 |
+
|
| 195 |
+
def flops(self, N):
|
| 196 |
+
# calculate flops for 1 window with token length of N
|
| 197 |
+
flops = 0
|
| 198 |
+
# qkv = self.qkv(x)
|
| 199 |
+
flops += N * self.dim * 3 * self.dim
|
| 200 |
+
# attn = (q @ k.transpose(-2, -1))
|
| 201 |
+
flops += self.num_heads * N * (self.dim // self.num_heads) * N
|
| 202 |
+
# x = (attn @ v)
|
| 203 |
+
flops += self.num_heads * N * N * (self.dim // self.num_heads)
|
| 204 |
+
# x = self.proj(x)
|
| 205 |
+
flops += N * self.dim * self.dim
|
| 206 |
+
return flops
|
| 207 |
+
|
| 208 |
+
@staticmethod
|
| 209 |
+
def compute_macs(module, input, output):
|
| 210 |
+
B, N, C = input[0].shape
|
| 211 |
+
|
| 212 |
+
module.__flops__ += module.flops(N) * B
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
class SwinTransformerBlock(nn.Module):
|
| 216 |
+
r"""Swin Transformer Block.
|
| 217 |
+
Args:
|
| 218 |
+
dim (int): Number of input channels.
|
| 219 |
+
input_resolution (tuple[int]): Input resolution.
|
| 220 |
+
num_heads (int): Number of attention heads.
|
| 221 |
+
window_size (int): Window size.
|
| 222 |
+
shift_size (int): Shift size for SW-MSA.
|
| 223 |
+
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
|
| 224 |
+
qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True
|
| 225 |
+
qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set.
|
| 226 |
+
drop (float, optional): Dropout rate. Default: 0.0
|
| 227 |
+
attn_drop (float, optional): Attention dropout rate. Default: 0.0
|
| 228 |
+
drop_path (float, optional): Stochastic depth rate. Default: 0.0
|
| 229 |
+
act_layer (nn.Module, optional): Activation layer. Default: nn.GELU
|
| 230 |
+
norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm
|
| 231 |
+
"""
|
| 232 |
+
|
| 233 |
+
def __init__(
|
| 234 |
+
self,
|
| 235 |
+
dim,
|
| 236 |
+
input_resolution,
|
| 237 |
+
num_heads,
|
| 238 |
+
window_size=7,
|
| 239 |
+
shift_size=0,
|
| 240 |
+
mlp_ratio=4.0,
|
| 241 |
+
qkv_bias=True,
|
| 242 |
+
qk_scale=None,
|
| 243 |
+
drop=0.0,
|
| 244 |
+
attn_drop=0.0,
|
| 245 |
+
drop_path=0.0,
|
| 246 |
+
act_layer=nn.GELU,
|
| 247 |
+
norm_layer=nn.LayerNorm,
|
| 248 |
+
):
|
| 249 |
+
super().__init__()
|
| 250 |
+
self.dim = dim
|
| 251 |
+
self.input_resolution = input_resolution
|
| 252 |
+
self.num_heads = num_heads
|
| 253 |
+
self.window_size = window_size
|
| 254 |
+
self.shift_size = shift_size
|
| 255 |
+
self.mlp_ratio = mlp_ratio
|
| 256 |
+
if min(self.input_resolution) <= self.window_size:
|
| 257 |
+
# if window size is larger than input resolution, we don't partition windows
|
| 258 |
+
self.shift_size = 0
|
| 259 |
+
self.window_size = min(self.input_resolution)
|
| 260 |
+
assert (
|
| 261 |
+
0 <= self.shift_size < self.window_size
|
| 262 |
+
), "shift_size must in 0-window_size"
|
| 263 |
+
|
| 264 |
+
self.norm1 = norm_layer(dim)
|
| 265 |
+
self.attn = WindowAttention(
|
| 266 |
+
dim,
|
| 267 |
+
window_size=to_2tuple(self.window_size),
|
| 268 |
+
num_heads=num_heads,
|
| 269 |
+
qkv_bias=qkv_bias,
|
| 270 |
+
qk_scale=qk_scale,
|
| 271 |
+
attn_drop=attn_drop,
|
| 272 |
+
proj_drop=drop,
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
|
| 276 |
+
self.norm2 = norm_layer(dim)
|
| 277 |
+
mlp_hidden_dim = int(dim * mlp_ratio)
|
| 278 |
+
self.mlp = Mlp(
|
| 279 |
+
in_features=dim,
|
| 280 |
+
hidden_features=mlp_hidden_dim,
|
| 281 |
+
act_layer=act_layer,
|
| 282 |
+
drop=drop,
|
| 283 |
+
)
|
| 284 |
+
|
| 285 |
+
self.H = input_resolution[0]
|
| 286 |
+
self.W = input_resolution[1]
|
| 287 |
+
|
| 288 |
+
self.attn_mask_dict = {} # {self.H: self.create_attn_mask(self.H, self.W)}
|
| 289 |
+
|
| 290 |
+
def create_attn_mask(self, H, W):
|
| 291 |
+
# calculate attention mask for SW-MSA
|
| 292 |
+
|
| 293 |
+
Hp = int(np.ceil(H / self.window_size)) * self.window_size
|
| 294 |
+
Wp = int(np.ceil(W / self.window_size)) * self.window_size
|
| 295 |
+
img_mask = torch.zeros((1, Hp, Wp, 1)) # 1 Hp Wp 1
|
| 296 |
+
h_slices = (
|
| 297 |
+
slice(0, -self.window_size),
|
| 298 |
+
slice(-self.window_size, -self.shift_size),
|
| 299 |
+
slice(-self.shift_size, None),
|
| 300 |
+
)
|
| 301 |
+
w_slices = (
|
| 302 |
+
slice(0, -self.window_size),
|
| 303 |
+
slice(-self.window_size, -self.shift_size),
|
| 304 |
+
slice(-self.shift_size, None),
|
| 305 |
+
)
|
| 306 |
+
cnt = 0
|
| 307 |
+
for h in h_slices:
|
| 308 |
+
for w in w_slices:
|
| 309 |
+
img_mask[:, h, w, :] = cnt
|
| 310 |
+
cnt += 1
|
| 311 |
+
|
| 312 |
+
mask_windows = window_partition(
|
| 313 |
+
img_mask, self.window_size
|
| 314 |
+
) # nW, window_size, window_size, 1
|
| 315 |
+
mask_windows = mask_windows.view(-1, self.window_size * self.window_size)
|
| 316 |
+
attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2)
|
| 317 |
+
attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(
|
| 318 |
+
attn_mask == 0, float(0.0)
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
return attn_mask
|
| 322 |
+
|
| 323 |
+
def forward(self, x):
|
| 324 |
+
B, L, C = x.shape
|
| 325 |
+
H = int(sqrt(L))
|
| 326 |
+
W = H
|
| 327 |
+
|
| 328 |
+
shortcut = x
|
| 329 |
+
x = self.norm1(x)
|
| 330 |
+
x = x.view(B, H, W, C)
|
| 331 |
+
|
| 332 |
+
# pad feature maps to multiples of window size
|
| 333 |
+
pad_l = pad_t = 0
|
| 334 |
+
pad_r = (self.window_size - W % self.window_size) % self.window_size
|
| 335 |
+
pad_b = (self.window_size - H % self.window_size) % self.window_size
|
| 336 |
+
x = F.pad(x, (0, 0, pad_l, pad_r, pad_t, pad_b))
|
| 337 |
+
_, Hp, Wp, _ = x.shape
|
| 338 |
+
|
| 339 |
+
# cyclic shift
|
| 340 |
+
if self.shift_size > 0:
|
| 341 |
+
shifted_x = torch.roll(
|
| 342 |
+
x,
|
| 343 |
+
shifts=(-self.shift_size, -self.shift_size),
|
| 344 |
+
dims=(1, 2),
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
if H is self.attn_mask_dict.keys():
|
| 348 |
+
attn_mask = self.attn_mask_dict[H]
|
| 349 |
+
else:
|
| 350 |
+
self.attn_mask_dict[H] = self.create_attn_mask(H, W).to(x.device)
|
| 351 |
+
attn_mask = self.attn_mask_dict[H]
|
| 352 |
+
|
| 353 |
+
else:
|
| 354 |
+
shifted_x = x
|
| 355 |
+
attn_mask = None
|
| 356 |
+
|
| 357 |
+
# partition windows
|
| 358 |
+
x_windows = window_partition(
|
| 359 |
+
shifted_x, self.window_size
|
| 360 |
+
) # nW*B, window_size, window_size, C
|
| 361 |
+
x_windows = x_windows.view(
|
| 362 |
+
-1, self.window_size * self.window_size, C
|
| 363 |
+
) # nW*B, window_size*window_size, C
|
| 364 |
+
|
| 365 |
+
# W-MSA/SW-MSA
|
| 366 |
+
attn_windows, attn = self.attn(
|
| 367 |
+
x_windows, attn_mask
|
| 368 |
+
) # nW*B, window_size*window_size, C
|
| 369 |
+
|
| 370 |
+
# merge windows
|
| 371 |
+
attn_windows = attn_windows.view(-1, self.window_size, self.window_size, C)
|
| 372 |
+
shifted_x = window_reverse(attn_windows, self.window_size, Hp, Wp) # B H' W' C
|
| 373 |
+
|
| 374 |
+
# reverse cyclic shift
|
| 375 |
+
if self.shift_size > 0:
|
| 376 |
+
x = torch.roll(
|
| 377 |
+
shifted_x,
|
| 378 |
+
shifts=(self.shift_size, self.shift_size),
|
| 379 |
+
dims=(1, 2),
|
| 380 |
+
)
|
| 381 |
+
else:
|
| 382 |
+
x = shifted_x
|
| 383 |
+
|
| 384 |
+
if pad_r > 0 or pad_b > 0:
|
| 385 |
+
x = x[:, :H, :W, :].contiguous()
|
| 386 |
+
|
| 387 |
+
x = x.view(B, H * W, C)
|
| 388 |
+
|
| 389 |
+
# FFN
|
| 390 |
+
x = shortcut + self.drop_path(x)
|
| 391 |
+
x = x + self.drop_path(self.mlp(self.norm2(x)))
|
| 392 |
+
|
| 393 |
+
return x, attn
|
| 394 |
+
|
| 395 |
+
def extra_repr(self) -> str:
|
| 396 |
+
return (
|
| 397 |
+
f"dim={self.dim}, input_resolution={self.input_resolution}, num_heads={self.num_heads}, "
|
| 398 |
+
f"window_size={self.window_size}, shift_size={self.shift_size} mlp_ratio={self.mlp_ratio}"
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
def flops(self):
|
| 402 |
+
flops = 0
|
| 403 |
+
H, W = self.input_resolution
|
| 404 |
+
# norm1
|
| 405 |
+
flops += self.dim * H * W
|
| 406 |
+
# W-MSA/SW-MSA
|
| 407 |
+
nW = H * W / self.window_size / self.window_size
|
| 408 |
+
flops += nW * self.attn.flops(self.window_size * self.window_size)
|
| 409 |
+
# mlp
|
| 410 |
+
flops += 2 * H * W * self.dim * self.dim * self.mlp_ratio
|
| 411 |
+
# norm2
|
| 412 |
+
flops += self.dim * H * W
|
| 413 |
+
return flops
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
class PatchMerging(nn.Module):
|
| 417 |
+
r"""Patch Merging Layer.
|
| 418 |
+
Args:
|
| 419 |
+
input_resolution (tuple[int]): Resolution of input feature.
|
| 420 |
+
dim (int): Number of input channels.
|
| 421 |
+
norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm
|
| 422 |
+
"""
|
| 423 |
+
|
| 424 |
+
def __init__(self, input_resolution, dim, norm_layer=nn.LayerNorm):
|
| 425 |
+
super().__init__()
|
| 426 |
+
self.input_resolution = input_resolution
|
| 427 |
+
self.dim = dim
|
| 428 |
+
self.reduction = nn.Linear(4 * dim, 2 * dim, bias=False)
|
| 429 |
+
self.norm = norm_layer(4 * dim)
|
| 430 |
+
|
| 431 |
+
def forward(self, x):
|
| 432 |
+
"""Forward function.
|
| 433 |
+
Args:
|
| 434 |
+
x: Input feature, tensor size (B, H*W, C).
|
| 435 |
+
H, W: Spatial resolution of the input feature.
|
| 436 |
+
"""
|
| 437 |
+
B, L, C = x.shape
|
| 438 |
+
H = int(sqrt(L))
|
| 439 |
+
W = H
|
| 440 |
+
|
| 441 |
+
x = x.view(B, H, W, C)
|
| 442 |
+
|
| 443 |
+
# padding
|
| 444 |
+
pad_input = (H % 2 == 1) or (W % 2 == 1)
|
| 445 |
+
if pad_input:
|
| 446 |
+
x = F.pad(x, (0, 0, 0, W % 2, 0, H % 2))
|
| 447 |
+
|
| 448 |
+
x0 = x[:, 0::2, 0::2, :] # B H/2 W/2 C
|
| 449 |
+
x1 = x[:, 1::2, 0::2, :] # B H/2 W/2 C
|
| 450 |
+
x2 = x[:, 0::2, 1::2, :] # B H/2 W/2 C
|
| 451 |
+
x3 = x[:, 1::2, 1::2, :] # B H/2 W/2 C
|
| 452 |
+
x = torch.cat([x0, x1, x2, x3], -1) # B H/2 W/2 4*C
|
| 453 |
+
x = x.view(B, -1, 4 * C) # B H/2*W/2 4*C
|
| 454 |
+
|
| 455 |
+
x = self.norm(x)
|
| 456 |
+
x = self.reduction(x)
|
| 457 |
+
|
| 458 |
+
return x
|
| 459 |
+
|
| 460 |
+
def extra_repr(self) -> str:
|
| 461 |
+
return f"input_resolution={self.input_resolution}, dim={self.dim}"
|
| 462 |
+
|
| 463 |
+
def flops(self):
|
| 464 |
+
H, W = self.input_resolution
|
| 465 |
+
flops = H * W * self.dim
|
| 466 |
+
flops += (H // 2) * (W // 2) * 4 * self.dim * 2 * self.dim
|
| 467 |
+
return flops
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
class BasicLayer(nn.Module):
|
| 471 |
+
"""A basic Swin Transformer layer for one stage.
|
| 472 |
+
Args:
|
| 473 |
+
dim (int): Number of input channels.
|
| 474 |
+
input_resolution (tuple[int]): Input resolution.
|
| 475 |
+
depth (int): Number of blocks.
|
| 476 |
+
num_heads (int): Number of attention heads.
|
| 477 |
+
window_size (int): Window size.
|
| 478 |
+
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
|
| 479 |
+
qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True
|
| 480 |
+
qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set.
|
| 481 |
+
drop (float, optional): Dropout rate. Default: 0.0
|
| 482 |
+
attn_drop (float, optional): Attention dropout rate. Default: 0.0
|
| 483 |
+
drop_path (float | tuple[float], optional): Stochastic depth rate. Default: 0.0
|
| 484 |
+
norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm
|
| 485 |
+
downsample (nn.Module | None, optional): Downsample layer at the end of the layer. Default: None
|
| 486 |
+
"""
|
| 487 |
+
|
| 488 |
+
def __init__(
|
| 489 |
+
self,
|
| 490 |
+
dim,
|
| 491 |
+
input_resolution,
|
| 492 |
+
depth,
|
| 493 |
+
num_heads,
|
| 494 |
+
window_size,
|
| 495 |
+
mlp_ratio=4.0,
|
| 496 |
+
qkv_bias=True,
|
| 497 |
+
qk_scale=None,
|
| 498 |
+
drop=0.0,
|
| 499 |
+
attn_drop=0.0,
|
| 500 |
+
drop_path=0.0,
|
| 501 |
+
norm_layer=nn.LayerNorm,
|
| 502 |
+
downsample=None,
|
| 503 |
+
):
|
| 504 |
+
super().__init__()
|
| 505 |
+
self.dim = dim
|
| 506 |
+
self.input_resolution = input_resolution
|
| 507 |
+
self.depth = depth
|
| 508 |
+
|
| 509 |
+
self.blocks = nn.ModuleList(
|
| 510 |
+
[
|
| 511 |
+
SwinTransformerBlock(
|
| 512 |
+
dim=dim,
|
| 513 |
+
input_resolution=input_resolution,
|
| 514 |
+
num_heads=num_heads,
|
| 515 |
+
window_size=window_size,
|
| 516 |
+
shift_size=0 if (i % 2 == 0) else window_size // 2,
|
| 517 |
+
mlp_ratio=mlp_ratio,
|
| 518 |
+
qkv_bias=qkv_bias,
|
| 519 |
+
qk_scale=qk_scale,
|
| 520 |
+
drop=drop,
|
| 521 |
+
attn_drop=attn_drop,
|
| 522 |
+
drop_path=(
|
| 523 |
+
drop_path[i] if isinstance(drop_path, list) else drop_path
|
| 524 |
+
),
|
| 525 |
+
norm_layer=norm_layer,
|
| 526 |
+
)
|
| 527 |
+
for i in range(depth)
|
| 528 |
+
]
|
| 529 |
+
)
|
| 530 |
+
if downsample is not None:
|
| 531 |
+
self.downsample = downsample(
|
| 532 |
+
input_resolution,
|
| 533 |
+
dim=dim,
|
| 534 |
+
norm_layer=norm_layer,
|
| 535 |
+
)
|
| 536 |
+
else:
|
| 537 |
+
self.downsample = None
|
| 538 |
+
|
| 539 |
+
def forward(self, x):
|
| 540 |
+
for blk in self.blocks:
|
| 541 |
+
x, _ = blk(x)
|
| 542 |
+
if self.downsample is not None:
|
| 543 |
+
x = self.downsample(x)
|
| 544 |
+
return x
|
| 545 |
+
|
| 546 |
+
def forward_with_features(self, x):
|
| 547 |
+
fea = []
|
| 548 |
+
for blk in self.blocks:
|
| 549 |
+
x, _ = blk(x)
|
| 550 |
+
fea.append(x)
|
| 551 |
+
if self.downsample is not None:
|
| 552 |
+
x = self.downsample(x)
|
| 553 |
+
return x, fea
|
| 554 |
+
|
| 555 |
+
def forward_with_attention(self, x):
|
| 556 |
+
attns = []
|
| 557 |
+
for blk in self.blocks:
|
| 558 |
+
x, attn = blk(x)
|
| 559 |
+
attns.append(attn)
|
| 560 |
+
if self.downsample is not None:
|
| 561 |
+
x = self.downsample(x)
|
| 562 |
+
return x, attns
|
| 563 |
+
|
| 564 |
+
def extra_repr(self) -> str:
|
| 565 |
+
return f"dim={self.dim}, input_resolution={self.input_resolution}, depth={self.depth}"
|
| 566 |
+
|
| 567 |
+
def flops(self):
|
| 568 |
+
flops = 0
|
| 569 |
+
for blk in self.blocks:
|
| 570 |
+
flops += blk.flops()
|
| 571 |
+
if self.downsample is not None:
|
| 572 |
+
flops += self.downsample.flops()
|
| 573 |
+
return flops
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
class PatchEmbed(nn.Module):
|
| 577 |
+
"""Image to Patch Embedding"""
|
| 578 |
+
|
| 579 |
+
def __init__(
|
| 580 |
+
self,
|
| 581 |
+
img_size=224,
|
| 582 |
+
patch_size=16,
|
| 583 |
+
in_chans=3,
|
| 584 |
+
embed_dim=768,
|
| 585 |
+
norm_layer=None,
|
| 586 |
+
):
|
| 587 |
+
super().__init__()
|
| 588 |
+
img_size = to_2tuple(img_size)
|
| 589 |
+
patch_size = to_2tuple(patch_size)
|
| 590 |
+
patches_resolution = [
|
| 591 |
+
img_size[0] // patch_size[0],
|
| 592 |
+
img_size[1] // patch_size[1],
|
| 593 |
+
]
|
| 594 |
+
self.img_size = img_size
|
| 595 |
+
self.patch_size = patch_size
|
| 596 |
+
self.patches_resolution = patches_resolution
|
| 597 |
+
self.num_patches = patches_resolution[0] * patches_resolution[1]
|
| 598 |
+
|
| 599 |
+
self.in_chans = in_chans
|
| 600 |
+
self.embed_dim = embed_dim
|
| 601 |
+
|
| 602 |
+
self.proj = nn.Conv2d(
|
| 603 |
+
in_chans,
|
| 604 |
+
embed_dim,
|
| 605 |
+
kernel_size=patch_size,
|
| 606 |
+
stride=patch_size,
|
| 607 |
+
)
|
| 608 |
+
if norm_layer is not None:
|
| 609 |
+
self.norm = norm_layer(embed_dim)
|
| 610 |
+
else:
|
| 611 |
+
self.norm = None
|
| 612 |
+
|
| 613 |
+
def forward(self, x):
|
| 614 |
+
# # FIXME look at relaxing size constraints
|
| 615 |
+
# assert H == self.img_size[0] and W == self.img_size[1], \
|
| 616 |
+
# f"Input image size ({H}*{W}) doesn't match model ({self.img_size[0]}*{self.img_size[1]})."
|
| 617 |
+
|
| 618 |
+
x = self.proj(x)
|
| 619 |
+
B, C, H, W = x.shape
|
| 620 |
+
x = x.flatten(2).transpose(1, 2) # B Ph*Pw C
|
| 621 |
+
if self.norm is not None:
|
| 622 |
+
x = self.norm(x)
|
| 623 |
+
return x.transpose(1, 2).reshape(B, C, H, W)
|
| 624 |
+
|
| 625 |
+
def flops(self):
|
| 626 |
+
Ho, Wo = self.patches_resolution
|
| 627 |
+
flops = (
|
| 628 |
+
Ho
|
| 629 |
+
* Wo
|
| 630 |
+
* self.embed_dim
|
| 631 |
+
* self.in_chans
|
| 632 |
+
* (self.patch_size[0] * self.patch_size[1])
|
| 633 |
+
)
|
| 634 |
+
if self.norm is not None:
|
| 635 |
+
flops += Ho * Wo * self.embed_dim
|
| 636 |
+
return flops
|
| 637 |
+
|
| 638 |
+
|
| 639 |
+
class SwinTransformer(nn.Module):
|
| 640 |
+
r"""Swin Transformer
|
| 641 |
+
A PyTorch impl of : `Swin Transformer: Hierarchical Vision Transformer using Shifted Windows` -
|
| 642 |
+
https://arxiv.org/pdf/2103.14030
|
| 643 |
+
Args:
|
| 644 |
+
img_size (int | tuple(int)): Input image size.
|
| 645 |
+
patch_size (int | tuple(int)): Patch size.
|
| 646 |
+
in_chans (int): Number of input channels.
|
| 647 |
+
num_classes (int): Number of classes for classification head.
|
| 648 |
+
embed_dim (int): Embedding dimension.
|
| 649 |
+
depths (tuple(int)): Depth of Swin Transformer layers.
|
| 650 |
+
num_heads (tuple(int)): Number of attention heads in different layers.
|
| 651 |
+
window_size (int): Window size.
|
| 652 |
+
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
|
| 653 |
+
qkv_bias (bool): If True, add a learnable bias to query, key, value. Default: True
|
| 654 |
+
qk_scale (float): Override default qk scale of head_dim ** -0.5 if set.
|
| 655 |
+
drop_rate (float): Dropout rate.
|
| 656 |
+
attn_drop_rate (float): Attention dropout rate.
|
| 657 |
+
drop_path_rate (float): Stochastic depth rate.
|
| 658 |
+
norm_layer (nn.Module): normalization layer.
|
| 659 |
+
ape (bool): If True, add absolute position embedding to the patch embedding.
|
| 660 |
+
patch_norm (bool): If True, add normalization after patch embedding.
|
| 661 |
+
"""
|
| 662 |
+
|
| 663 |
+
def __init__(
|
| 664 |
+
self,
|
| 665 |
+
img_size=224,
|
| 666 |
+
patch_size=4,
|
| 667 |
+
in_chans=3,
|
| 668 |
+
num_classes=1000,
|
| 669 |
+
embed_dim=96,
|
| 670 |
+
depths=[2, 2, 6, 2],
|
| 671 |
+
num_heads=[3, 6, 12, 24],
|
| 672 |
+
window_size=7,
|
| 673 |
+
mlp_ratio=4.0,
|
| 674 |
+
qkv_bias=True,
|
| 675 |
+
qk_scale=None,
|
| 676 |
+
drop_rate=0.0,
|
| 677 |
+
attn_drop_rate=0.0,
|
| 678 |
+
drop_path_rate=0.0,
|
| 679 |
+
norm_layer=partial(nn.LayerNorm, eps=1e-6),
|
| 680 |
+
ape=False,
|
| 681 |
+
patch_norm=True,
|
| 682 |
+
return_all_tokens=False,
|
| 683 |
+
use_mean_pooling=True,
|
| 684 |
+
masked_im_modeling=False,
|
| 685 |
+
):
|
| 686 |
+
super().__init__()
|
| 687 |
+
|
| 688 |
+
self.num_classes = num_classes
|
| 689 |
+
self.depths = depths
|
| 690 |
+
self.num_layers = len(depths)
|
| 691 |
+
self.embed_dim = embed_dim
|
| 692 |
+
self.ape = ape
|
| 693 |
+
self.patch_norm = patch_norm
|
| 694 |
+
self.num_features = int(embed_dim * 2 ** (self.num_layers - 1))
|
| 695 |
+
self.mlp_ratio = mlp_ratio
|
| 696 |
+
self.return_all_tokens = return_all_tokens
|
| 697 |
+
|
| 698 |
+
self.patch_embed = PatchEmbed(
|
| 699 |
+
img_size=img_size,
|
| 700 |
+
patch_size=patch_size,
|
| 701 |
+
in_chans=in_chans,
|
| 702 |
+
embed_dim=embed_dim,
|
| 703 |
+
norm_layer=norm_layer if self.patch_norm else None,
|
| 704 |
+
)
|
| 705 |
+
num_patches = self.patch_embed.num_patches
|
| 706 |
+
patches_resolution = self.patch_embed.patches_resolution
|
| 707 |
+
self.patches_resolution = patches_resolution
|
| 708 |
+
|
| 709 |
+
if self.ape:
|
| 710 |
+
self.absolute_pos_embed = nn.Parameter(
|
| 711 |
+
torch.zeros(1, num_patches, embed_dim)
|
| 712 |
+
)
|
| 713 |
+
trunc_normal_(self.absolute_pos_embed, std=0.02)
|
| 714 |
+
|
| 715 |
+
self.pos_drop = nn.Dropout(p=drop_rate)
|
| 716 |
+
|
| 717 |
+
dpr = [
|
| 718 |
+
x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))
|
| 719 |
+
] # stochastic depth decay rule
|
| 720 |
+
self.layers = nn.ModuleList()
|
| 721 |
+
for i_layer in range(self.num_layers):
|
| 722 |
+
layer = BasicLayer(
|
| 723 |
+
dim=int(embed_dim * 2**i_layer),
|
| 724 |
+
input_resolution=(
|
| 725 |
+
patches_resolution[0] // (2**i_layer),
|
| 726 |
+
patches_resolution[1] // (2**i_layer),
|
| 727 |
+
),
|
| 728 |
+
depth=depths[i_layer],
|
| 729 |
+
num_heads=num_heads[i_layer],
|
| 730 |
+
window_size=window_size,
|
| 731 |
+
mlp_ratio=self.mlp_ratio,
|
| 732 |
+
qkv_bias=qkv_bias,
|
| 733 |
+
qk_scale=qk_scale,
|
| 734 |
+
drop=drop_rate,
|
| 735 |
+
attn_drop=attn_drop_rate,
|
| 736 |
+
drop_path=dpr[sum(depths[:i_layer]) : sum(depths[: i_layer + 1])],
|
| 737 |
+
norm_layer=norm_layer,
|
| 738 |
+
downsample=PatchMerging if (i_layer < self.num_layers - 1) else None,
|
| 739 |
+
)
|
| 740 |
+
self.layers.append(layer)
|
| 741 |
+
|
| 742 |
+
self.norm = norm_layer(self.num_features)
|
| 743 |
+
self.avgpool = nn.AdaptiveAvgPool1d(1)
|
| 744 |
+
self.head = (
|
| 745 |
+
nn.Linear(self.num_features, num_classes)
|
| 746 |
+
if num_classes > 0
|
| 747 |
+
else nn.Identity()
|
| 748 |
+
)
|
| 749 |
+
|
| 750 |
+
self.apply(self._init_weights)
|
| 751 |
+
|
| 752 |
+
# masked image modeling
|
| 753 |
+
self.masked_im_modeling = masked_im_modeling
|
| 754 |
+
if masked_im_modeling:
|
| 755 |
+
self.masked_embed = nn.Parameter(torch.zeros(1, embed_dim))
|
| 756 |
+
|
| 757 |
+
def _init_weights(self, m):
|
| 758 |
+
if isinstance(m, nn.Linear):
|
| 759 |
+
trunc_normal_(m.weight, std=0.02)
|
| 760 |
+
if isinstance(m, nn.Linear) and m.bias is not None:
|
| 761 |
+
nn.init.constant_(m.bias, 0)
|
| 762 |
+
elif isinstance(m, nn.LayerNorm):
|
| 763 |
+
nn.init.constant_(m.bias, 0)
|
| 764 |
+
nn.init.constant_(m.weight, 1.0)
|
| 765 |
+
|
| 766 |
+
@torch.jit.ignore
|
| 767 |
+
def no_weight_decay(self):
|
| 768 |
+
return {"absolute_pos_embed"}
|
| 769 |
+
|
| 770 |
+
@torch.jit.ignore
|
| 771 |
+
def no_weight_decay_keywords(self):
|
| 772 |
+
# todo: to be implemented
|
| 773 |
+
return {"relative_position_bias_table"}
|
| 774 |
+
|
| 775 |
+
def forward(self, x, return_all_tokens=None, mask=None):
|
| 776 |
+
# patch linear embedding
|
| 777 |
+
x = self.patch_embed(x)
|
| 778 |
+
# mask image modeling
|
| 779 |
+
if mask is not None:
|
| 780 |
+
x = self.mask_model(x, mask)
|
| 781 |
+
x = x.flatten(2).transpose(1, 2)
|
| 782 |
+
|
| 783 |
+
if self.ape:
|
| 784 |
+
x = x + self.absolute_pos_embed
|
| 785 |
+
x = self.pos_drop(x)
|
| 786 |
+
|
| 787 |
+
for layer in self.layers:
|
| 788 |
+
x = layer(x)
|
| 789 |
+
|
| 790 |
+
x_region = self.norm(x) # B L C
|
| 791 |
+
x = self.avgpool(x_region.transpose(1, 2)) # B C 1
|
| 792 |
+
x = torch.flatten(x, 1)
|
| 793 |
+
|
| 794 |
+
return_all_tokens = (
|
| 795 |
+
self.return_all_tokens if return_all_tokens is None else return_all_tokens
|
| 796 |
+
)
|
| 797 |
+
if return_all_tokens:
|
| 798 |
+
return torch.cat([x.unsqueeze(1), x_region], dim=1)
|
| 799 |
+
return x
|
| 800 |
+
|
| 801 |
+
def prepare_tokens(self, x):
|
| 802 |
+
x = self.patch_embed(x)
|
| 803 |
+
x = x.flatten(2).transpose(1, 2)
|
| 804 |
+
if self.ape:
|
| 805 |
+
x = x + self.absolute_pos_embed
|
| 806 |
+
return self.pos_drop(x)
|
| 807 |
+
|
| 808 |
+
def get_last_selfattention(self, x):
|
| 809 |
+
x = self.prepare_tokens(x)
|
| 810 |
+
for i, layer in enumerate(self.layers):
|
| 811 |
+
if i < len(self.layers) - 1:
|
| 812 |
+
x = layer(x)
|
| 813 |
+
else:
|
| 814 |
+
x, attns = layer.forward_with_attention(x)
|
| 815 |
+
return attns[-1]
|
| 816 |
+
|
| 817 |
+
def get_all_selfattention(self, x):
|
| 818 |
+
x = self.prepare_tokens(x)
|
| 819 |
+
attn_out = []
|
| 820 |
+
for layer in self.layers:
|
| 821 |
+
x, attns = layer.forward_with_attention(x)
|
| 822 |
+
attn_out += attns
|
| 823 |
+
return attn_out
|
| 824 |
+
|
| 825 |
+
def get_intermediate_layers(self, x, n=1, return_patch_avgpool=False):
|
| 826 |
+
num_blks = sum(self.depths)
|
| 827 |
+
start_idx = num_blks - n
|
| 828 |
+
|
| 829 |
+
sum_cur = 0
|
| 830 |
+
for i, d in enumerate(self.depths):
|
| 831 |
+
sum_cur_new = sum_cur + d
|
| 832 |
+
if start_idx >= sum_cur and start_idx < sum_cur_new:
|
| 833 |
+
start_stage = i
|
| 834 |
+
start_blk = start_idx - sum_cur
|
| 835 |
+
sum_cur = sum_cur_new
|
| 836 |
+
|
| 837 |
+
x = self.patch_embed(x)
|
| 838 |
+
x = x.flatten(2).transpose(1, 2)
|
| 839 |
+
if self.ape:
|
| 840 |
+
x = x + self.absolute_pos_embed
|
| 841 |
+
x = self.pos_drop(x)
|
| 842 |
+
|
| 843 |
+
# we will return the averaged token features from the `n` last blocks
|
| 844 |
+
# note: there is no [CLS] token in Swin Transformer
|
| 845 |
+
output = []
|
| 846 |
+
s = 0
|
| 847 |
+
for i, layer in enumerate(self.layers):
|
| 848 |
+
x, fea = layer.forward_with_features(x)
|
| 849 |
+
|
| 850 |
+
if i >= start_stage:
|
| 851 |
+
for x_ in fea[start_blk:]:
|
| 852 |
+
if i == len(self.layers) - 1: # use the norm in the last stage
|
| 853 |
+
x_ = self.norm(x_)
|
| 854 |
+
|
| 855 |
+
x_avg = torch.flatten(self.avgpool(x_.transpose(1, 2)), 1) # B C
|
| 856 |
+
if return_patch_avgpool:
|
| 857 |
+
x_o = x_avg
|
| 858 |
+
else:
|
| 859 |
+
x_o = torch.cat((x_avg.unsqueeze(1), x_), dim=1)
|
| 860 |
+
logger.debug(f"Stage {i}, x_o {x_o.shape}")
|
| 861 |
+
output.append(x_o)
|
| 862 |
+
|
| 863 |
+
start_blk = 0
|
| 864 |
+
|
| 865 |
+
# return torch.cat(output, dim=-1)
|
| 866 |
+
return output
|
| 867 |
+
|
| 868 |
+
def flops(self):
|
| 869 |
+
flops = 0
|
| 870 |
+
flops += self.patch_embed.flops()
|
| 871 |
+
for i, layer in enumerate(self.layers):
|
| 872 |
+
flops += layer.flops()
|
| 873 |
+
if dist.get_rank() == 0:
|
| 874 |
+
logger.debug(f"GFLOPs layer_{i}: {layer.flops() / 1e9}")
|
| 875 |
+
flops += (
|
| 876 |
+
self.num_features
|
| 877 |
+
* self.patches_resolution[0]
|
| 878 |
+
* self.patches_resolution[1]
|
| 879 |
+
// (2**self.num_layers)
|
| 880 |
+
)
|
| 881 |
+
flops += self.num_features * self.num_classes
|
| 882 |
+
return flops
|
| 883 |
+
|
| 884 |
+
def init_weights(self, pretrained="", pretrained_layers=[], verbose=True):
|
| 885 |
+
if os.path.isfile(pretrained):
|
| 886 |
+
pretrained_dict = torch.load(pretrained, map_location="cpu")
|
| 887 |
+
logger.info(f"=> loading pretrained model {pretrained}")
|
| 888 |
+
model_dict = self.state_dict()
|
| 889 |
+
pretrained_dict = {
|
| 890 |
+
k: v for k, v in pretrained_dict.items() if k in model_dict.keys()
|
| 891 |
+
}
|
| 892 |
+
need_init_state_dict = {}
|
| 893 |
+
for k, v in pretrained_dict.items():
|
| 894 |
+
need_init = (
|
| 895 |
+
k.split(".")[0] in pretrained_layers
|
| 896 |
+
or pretrained_layers[0] == "*"
|
| 897 |
+
or "relative_position_index" not in k
|
| 898 |
+
or "attn_mask" not in k
|
| 899 |
+
)
|
| 900 |
+
|
| 901 |
+
if need_init:
|
| 902 |
+
logger.info(f"=> init {k} from {pretrained}")
|
| 903 |
+
if (
|
| 904 |
+
"relative_position_bias_table" in k
|
| 905 |
+
and v.size() != model_dict[k].size()
|
| 906 |
+
):
|
| 907 |
+
relative_position_bias_table_pretrained = v
|
| 908 |
+
relative_position_bias_table_current = model_dict[k]
|
| 909 |
+
L1, nH1 = relative_position_bias_table_pretrained.size()
|
| 910 |
+
L2, nH2 = relative_position_bias_table_current.size()
|
| 911 |
+
if nH1 != nH2:
|
| 912 |
+
logger.info(f"Error in loading {k}, passing")
|
| 913 |
+
else:
|
| 914 |
+
if L1 != L2:
|
| 915 |
+
logger.info(
|
| 916 |
+
"=> load_pretrained: resized variant: {} to {}".format(
|
| 917 |
+
(L1, nH1), (L2, nH2)
|
| 918 |
+
)
|
| 919 |
+
)
|
| 920 |
+
S1 = int(L1**0.5)
|
| 921 |
+
S2 = int(L2**0.5)
|
| 922 |
+
relative_position_bias_table_pretrained_resized = (
|
| 923 |
+
torch.nn.functional.interpolate(
|
| 924 |
+
relative_position_bias_table_pretrained.permute(
|
| 925 |
+
1, 0
|
| 926 |
+
).view(1, nH1, S1, S1),
|
| 927 |
+
size=(S2, S2),
|
| 928 |
+
mode="bicubic",
|
| 929 |
+
)
|
| 930 |
+
)
|
| 931 |
+
v = relative_position_bias_table_pretrained_resized.view(
|
| 932 |
+
nH2, L2
|
| 933 |
+
).permute(
|
| 934 |
+
1, 0
|
| 935 |
+
)
|
| 936 |
+
|
| 937 |
+
if "absolute_pos_embed" in k and v.size() != model_dict[k].size():
|
| 938 |
+
absolute_pos_embed_pretrained = v
|
| 939 |
+
absolute_pos_embed_current = model_dict[k]
|
| 940 |
+
_, L1, C1 = absolute_pos_embed_pretrained.size()
|
| 941 |
+
_, L2, C2 = absolute_pos_embed_current.size()
|
| 942 |
+
if C1 != C1:
|
| 943 |
+
logger.info(f"Error in loading {k}, passing")
|
| 944 |
+
else:
|
| 945 |
+
if L1 != L2:
|
| 946 |
+
logger.info(
|
| 947 |
+
"=> load_pretrained: resized variant: {} to {}".format(
|
| 948 |
+
(1, L1, C1), (1, L2, C2)
|
| 949 |
+
)
|
| 950 |
+
)
|
| 951 |
+
S1 = int(L1**0.5)
|
| 952 |
+
S2 = int(L2**0.5)
|
| 953 |
+
absolute_pos_embed_pretrained = (
|
| 954 |
+
absolute_pos_embed_pretrained.reshape(
|
| 955 |
+
-1, S1, S1, C1
|
| 956 |
+
)
|
| 957 |
+
)
|
| 958 |
+
absolute_pos_embed_pretrained = (
|
| 959 |
+
absolute_pos_embed_pretrained.permute(0, 3, 1, 2)
|
| 960 |
+
)
|
| 961 |
+
absolute_pos_embed_pretrained_resized = (
|
| 962 |
+
torch.nn.functional.interpolate(
|
| 963 |
+
absolute_pos_embed_pretrained,
|
| 964 |
+
size=(S2, S2),
|
| 965 |
+
mode="bicubic",
|
| 966 |
+
)
|
| 967 |
+
)
|
| 968 |
+
v = absolute_pos_embed_pretrained_resized.permute(
|
| 969 |
+
0, 2, 3, 1
|
| 970 |
+
).flatten(1, 2)
|
| 971 |
+
|
| 972 |
+
need_init_state_dict[k] = v
|
| 973 |
+
self.load_state_dict(need_init_state_dict, strict=False)
|
| 974 |
+
|
| 975 |
+
def freeze_pretrained_layers(self, frozen_layers=[]):
|
| 976 |
+
for name, module in self.named_modules():
|
| 977 |
+
if (
|
| 978 |
+
name.split(".")[0] in frozen_layers
|
| 979 |
+
or ".".join(name.split(".")[0:2]) in frozen_layers
|
| 980 |
+
or (len(frozen_layers) > 0 and frozen_layers[0] == "*")
|
| 981 |
+
):
|
| 982 |
+
for _name, param in module.named_parameters():
|
| 983 |
+
param.requires_grad = False
|
| 984 |
+
logger.info("=> set param {} requires grad to False".format(name))
|
| 985 |
+
for name, param in self.named_parameters():
|
| 986 |
+
if (
|
| 987 |
+
name.split(".")[0] in frozen_layers
|
| 988 |
+
or (len(frozen_layers) > 0 and frozen_layers[0] == "*")
|
| 989 |
+
and param.requires_grad is True
|
| 990 |
+
):
|
| 991 |
+
param.requires_grad = False
|
| 992 |
+
logger.info("=> set param {} requires grad to False".format(name))
|
| 993 |
+
return self
|
| 994 |
+
|
| 995 |
+
def get_num_layers(self):
|
| 996 |
+
# return len(self.layers)
|
| 997 |
+
return sum(self.depths)
|
| 998 |
+
|
| 999 |
+
def mask_model(self, x, mask):
|
| 1000 |
+
# extend mask for hierarchical features
|
| 1001 |
+
if x.shape[-2:] != mask.shape[-2:]:
|
| 1002 |
+
htimes, wtimes = np.array(x.shape[-2:]) // np.array(mask.shape[-2:])
|
| 1003 |
+
mask = mask.repeat_interleave(htimes, -2).repeat_interleave(wtimes, -1)
|
| 1004 |
+
|
| 1005 |
+
# mask embed
|
| 1006 |
+
x.permute(0, 2, 3, 1)[mask, :] = self.masked_embed.to(x.dtype)
|
| 1007 |
+
|
| 1008 |
+
return x
|
| 1009 |
+
|
| 1010 |
+
|
| 1011 |
+
def swin_tiny(window_size=7, **kwargs):
|
| 1012 |
+
model = SwinTransformer(
|
| 1013 |
+
window_size=window_size,
|
| 1014 |
+
embed_dim=96,
|
| 1015 |
+
depths=[2, 2, 6, 2],
|
| 1016 |
+
num_heads=[3, 6, 12, 24],
|
| 1017 |
+
mlp_ratio=4,
|
| 1018 |
+
qkv_bias=True,
|
| 1019 |
+
drop_path_rate=kwargs.pop("drop_path_rate", 0.1),
|
| 1020 |
+
**kwargs,
|
| 1021 |
+
)
|
| 1022 |
+
return model
|
| 1023 |
+
|
| 1024 |
+
|
| 1025 |
+
def swin_small(window_size=7, **kwargs):
|
| 1026 |
+
model = SwinTransformer(
|
| 1027 |
+
window_size=window_size,
|
| 1028 |
+
embed_dim=96,
|
| 1029 |
+
depths=[2, 2, 18, 2],
|
| 1030 |
+
num_heads=[3, 6, 12, 24],
|
| 1031 |
+
mlp_ratio=4,
|
| 1032 |
+
qkv_bias=True,
|
| 1033 |
+
drop_path_rate=kwargs.pop("drop_path_rate", 0.2),
|
| 1034 |
+
**kwargs,
|
| 1035 |
+
)
|
| 1036 |
+
return model
|
| 1037 |
+
|
| 1038 |
+
|
| 1039 |
+
def swin_base(window_size=7, **kwargs):
|
| 1040 |
+
model = SwinTransformer(
|
| 1041 |
+
window_size=window_size,
|
| 1042 |
+
embed_dim=128,
|
| 1043 |
+
depths=[2, 2, 18, 2],
|
| 1044 |
+
num_heads=[4, 8, 16, 32],
|
| 1045 |
+
mlp_ratio=4,
|
| 1046 |
+
qkv_bias=True,
|
| 1047 |
+
drop_path_rate=kwargs.pop("drop_path_rate", 0.2),
|
| 1048 |
+
**kwargs,
|
| 1049 |
+
)
|
| 1050 |
+
return model
|
skinmap_runtime/core/models/encoders/utils.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Callable, Tuple
|
| 2 |
+
|
| 3 |
+
from torchvision import models as torchvision_models
|
| 4 |
+
|
| 5 |
+
from ....src.models.encoders.swin_transformer import swin_base, swin_small, swin_tiny
|
| 6 |
+
from ....src.models.encoders.vision_transformer import (
|
| 7 |
+
vit_base,
|
| 8 |
+
vit_large,
|
| 9 |
+
vit_small,
|
| 10 |
+
vit_tiny,
|
| 11 |
+
)
|
| 12 |
+
from ....src.models.utils import ModelType
|
| 13 |
+
|
| 14 |
+
VIT_DICT = {
|
| 15 |
+
"vit_tiny": vit_tiny,
|
| 16 |
+
"vit_small": vit_small,
|
| 17 |
+
"vit_base": vit_base,
|
| 18 |
+
"vit_large": vit_large,
|
| 19 |
+
"swin_tiny": swin_tiny,
|
| 20 |
+
"swin_small": swin_small,
|
| 21 |
+
"swin_base": swin_base,
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def get_encoder_class(base_model_name: str) -> Tuple[Callable, ModelType]:
|
| 26 |
+
encoder_cls = VIT_DICT.get(base_model_name, None)
|
| 27 |
+
model_type = ModelType.VIT
|
| 28 |
+
if encoder_cls is None:
|
| 29 |
+
if base_model_name in torchvision_models.__dict__.keys():
|
| 30 |
+
encoder_cls = torchvision_models.__dict__[base_model_name]
|
| 31 |
+
model_type = ModelType.CNN
|
| 32 |
+
else:
|
| 33 |
+
raise ValueError(f"Invalid base model name: {base_model_name}")
|
| 34 |
+
return encoder_cls, model_type
|
skinmap_runtime/core/models/encoders/vision_transformer.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Mostly copy-paste from timm library.
|
| 3 |
+
https://github.com/rwightman/pytorch-image-models/blob/master/timm/models/vision_transformer.py
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import math
|
| 7 |
+
from functools import partial
|
| 8 |
+
from typing import List
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn as nn
|
| 12 |
+
|
| 13 |
+
from ..utils import DropPath, trunc_normal_
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class Mlp(nn.Module):
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
in_features,
|
| 20 |
+
hidden_features=None,
|
| 21 |
+
out_features=None,
|
| 22 |
+
act_layer=nn.GELU,
|
| 23 |
+
drop=0.0,
|
| 24 |
+
):
|
| 25 |
+
super().__init__()
|
| 26 |
+
out_features = out_features or in_features
|
| 27 |
+
hidden_features = hidden_features or in_features
|
| 28 |
+
self.fc1 = nn.Linear(in_features, hidden_features)
|
| 29 |
+
self.act = act_layer()
|
| 30 |
+
self.fc2 = nn.Linear(hidden_features, out_features)
|
| 31 |
+
self.drop = nn.Dropout(drop)
|
| 32 |
+
|
| 33 |
+
def forward(self, x):
|
| 34 |
+
x = self.fc1(x)
|
| 35 |
+
x = self.act(x)
|
| 36 |
+
x = self.drop(x)
|
| 37 |
+
x = self.fc2(x)
|
| 38 |
+
x = self.drop(x)
|
| 39 |
+
return x
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class Attention(nn.Module):
|
| 43 |
+
def __init__(
|
| 44 |
+
self,
|
| 45 |
+
dim,
|
| 46 |
+
num_heads=8,
|
| 47 |
+
qkv_bias=False,
|
| 48 |
+
qk_scale=None,
|
| 49 |
+
attn_drop=0.0,
|
| 50 |
+
proj_drop=0.0,
|
| 51 |
+
):
|
| 52 |
+
super().__init__()
|
| 53 |
+
self.num_heads = num_heads
|
| 54 |
+
head_dim = dim // num_heads
|
| 55 |
+
self.scale = qk_scale or head_dim**-0.5
|
| 56 |
+
|
| 57 |
+
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
|
| 58 |
+
self.attn_drop = nn.Dropout(attn_drop)
|
| 59 |
+
self.proj = nn.Linear(dim, dim)
|
| 60 |
+
self.proj_drop = nn.Dropout(proj_drop)
|
| 61 |
+
|
| 62 |
+
def forward(self, x):
|
| 63 |
+
B, N, C = x.shape
|
| 64 |
+
qkv = (
|
| 65 |
+
self.qkv(x)
|
| 66 |
+
.reshape(B, N, 3, self.num_heads, C // self.num_heads)
|
| 67 |
+
.permute(2, 0, 3, 1, 4)
|
| 68 |
+
)
|
| 69 |
+
q, k, v = qkv[0], qkv[1], qkv[2]
|
| 70 |
+
|
| 71 |
+
attn = (q @ k.transpose(-2, -1)) * self.scale
|
| 72 |
+
attn = attn.softmax(dim=-1)
|
| 73 |
+
attn = self.attn_drop(attn)
|
| 74 |
+
|
| 75 |
+
x = (attn @ v).transpose(1, 2).reshape(B, N, C)
|
| 76 |
+
x = self.proj(x)
|
| 77 |
+
x = self.proj_drop(x)
|
| 78 |
+
return x, attn
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class Block(nn.Module):
|
| 82 |
+
def __init__(
|
| 83 |
+
self,
|
| 84 |
+
dim,
|
| 85 |
+
num_heads,
|
| 86 |
+
mlp_ratio=4.0,
|
| 87 |
+
qkv_bias=False,
|
| 88 |
+
qk_scale=None,
|
| 89 |
+
drop=0.0,
|
| 90 |
+
attn_drop=0.0,
|
| 91 |
+
drop_path=0.0,
|
| 92 |
+
act_layer=nn.GELU,
|
| 93 |
+
norm_layer=nn.LayerNorm,
|
| 94 |
+
):
|
| 95 |
+
super().__init__()
|
| 96 |
+
self.norm1 = norm_layer(dim)
|
| 97 |
+
self.attn = Attention(
|
| 98 |
+
dim,
|
| 99 |
+
num_heads=num_heads,
|
| 100 |
+
qkv_bias=qkv_bias,
|
| 101 |
+
qk_scale=qk_scale,
|
| 102 |
+
attn_drop=attn_drop,
|
| 103 |
+
proj_drop=drop,
|
| 104 |
+
)
|
| 105 |
+
self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
|
| 106 |
+
self.norm2 = norm_layer(dim)
|
| 107 |
+
mlp_hidden_dim = int(dim * mlp_ratio)
|
| 108 |
+
self.mlp = Mlp(
|
| 109 |
+
in_features=dim,
|
| 110 |
+
hidden_features=mlp_hidden_dim,
|
| 111 |
+
act_layer=act_layer,
|
| 112 |
+
drop=drop,
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
def forward(self, x, return_attention=False):
|
| 116 |
+
y, attn = self.attn(self.norm1(x))
|
| 117 |
+
if return_attention:
|
| 118 |
+
return attn
|
| 119 |
+
x = x + self.drop_path(y)
|
| 120 |
+
x = x + self.drop_path(self.mlp(self.norm2(x)))
|
| 121 |
+
return x
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class PatchEmbed(nn.Module):
|
| 125 |
+
"""Image to Patch Embedding"""
|
| 126 |
+
|
| 127 |
+
def __init__(self, img_size=224, patch_size=16, in_channels=3, embed_dim=768):
|
| 128 |
+
super().__init__()
|
| 129 |
+
num_patches = (img_size // patch_size) * (img_size // patch_size)
|
| 130 |
+
self.img_size = img_size
|
| 131 |
+
self.patch_size = patch_size
|
| 132 |
+
self.num_patches = num_patches
|
| 133 |
+
|
| 134 |
+
self.proj = nn.Conv2d(
|
| 135 |
+
in_channels, embed_dim, kernel_size=patch_size, stride=patch_size
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
def forward(self, x):
|
| 139 |
+
B, C, H, W = x.shape
|
| 140 |
+
return self.proj(x)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class VisionTransformer(nn.Module):
|
| 144 |
+
"""Vision Transformer"""
|
| 145 |
+
|
| 146 |
+
def __init__(
|
| 147 |
+
self,
|
| 148 |
+
img_size: List[int] = [224],
|
| 149 |
+
patch_size: int = 16,
|
| 150 |
+
in_channels: int = 3,
|
| 151 |
+
num_classes: int = 0,
|
| 152 |
+
embed_dim: int = 768,
|
| 153 |
+
depth: int = 12,
|
| 154 |
+
num_heads: int = 12,
|
| 155 |
+
mlp_ratio: float = 4.0,
|
| 156 |
+
qkv_bias: bool = False,
|
| 157 |
+
qk_scale=None,
|
| 158 |
+
drop_rate: float = 0.0,
|
| 159 |
+
attn_drop_rate: float = 0.0,
|
| 160 |
+
drop_path_rate: float = 0.0,
|
| 161 |
+
norm_layer: nn.Module = nn.LayerNorm,
|
| 162 |
+
return_all_tokens: bool = False,
|
| 163 |
+
masked_im_modeling: bool = False,
|
| 164 |
+
**kwargs
|
| 165 |
+
):
|
| 166 |
+
super().__init__()
|
| 167 |
+
self.num_features = self.embed_dim = embed_dim
|
| 168 |
+
self.return_all_tokens = return_all_tokens
|
| 169 |
+
|
| 170 |
+
self.patch_embed = PatchEmbed(
|
| 171 |
+
img_size=img_size[0],
|
| 172 |
+
patch_size=patch_size,
|
| 173 |
+
in_channels=in_channels,
|
| 174 |
+
embed_dim=embed_dim,
|
| 175 |
+
)
|
| 176 |
+
num_patches = self.patch_embed.num_patches
|
| 177 |
+
|
| 178 |
+
self.cls_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
|
| 179 |
+
self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim))
|
| 180 |
+
self.pos_drop = nn.Dropout(p=drop_rate)
|
| 181 |
+
# stochastic depth decay rule
|
| 182 |
+
dpr = [x.item() for x in torch.linspace(0, drop_path_rate, depth)]
|
| 183 |
+
self.blocks = nn.ModuleList(
|
| 184 |
+
[
|
| 185 |
+
Block(
|
| 186 |
+
dim=embed_dim,
|
| 187 |
+
num_heads=num_heads,
|
| 188 |
+
mlp_ratio=mlp_ratio,
|
| 189 |
+
qkv_bias=qkv_bias,
|
| 190 |
+
qk_scale=qk_scale,
|
| 191 |
+
drop=drop_rate,
|
| 192 |
+
attn_drop=attn_drop_rate,
|
| 193 |
+
drop_path=dpr[i],
|
| 194 |
+
norm_layer=norm_layer,
|
| 195 |
+
)
|
| 196 |
+
for i in range(depth)
|
| 197 |
+
]
|
| 198 |
+
)
|
| 199 |
+
self.norm = norm_layer(embed_dim)
|
| 200 |
+
|
| 201 |
+
# Classifier head
|
| 202 |
+
if num_classes > 0:
|
| 203 |
+
self.head = nn.Linear(embed_dim, num_classes)
|
| 204 |
+
else:
|
| 205 |
+
self.head = nn.Identity()
|
| 206 |
+
|
| 207 |
+
trunc_normal_(self.pos_embed, std=0.02)
|
| 208 |
+
trunc_normal_(self.cls_token, std=0.02)
|
| 209 |
+
self.apply(self._init_weights)
|
| 210 |
+
|
| 211 |
+
# Masked image modeling (MIM)
|
| 212 |
+
self.masked_im_modeling = masked_im_modeling
|
| 213 |
+
if masked_im_modeling:
|
| 214 |
+
self.masked_embed = nn.Parameter(torch.zeros(1, embed_dim))
|
| 215 |
+
|
| 216 |
+
def _init_weights(self, m):
|
| 217 |
+
if isinstance(m, nn.Linear):
|
| 218 |
+
trunc_normal_(m.weight, std=0.02)
|
| 219 |
+
if isinstance(m, nn.Linear) and m.bias is not None:
|
| 220 |
+
nn.init.constant_(m.bias, 0)
|
| 221 |
+
elif isinstance(m, nn.LayerNorm):
|
| 222 |
+
nn.init.constant_(m.bias, 0)
|
| 223 |
+
nn.init.constant_(m.weight, 1.0)
|
| 224 |
+
|
| 225 |
+
def interpolate_pos_encoding(self, x, w, h):
|
| 226 |
+
n_patch = x.shape[1] - 1
|
| 227 |
+
N = self.pos_embed.shape[1] - 1
|
| 228 |
+
if n_patch == N and w == h:
|
| 229 |
+
return self.pos_embed
|
| 230 |
+
class_pos_embed = self.pos_embed[:, 0]
|
| 231 |
+
patch_pos_embed = self.pos_embed[:, 1:]
|
| 232 |
+
dim = x.shape[-1]
|
| 233 |
+
w0 = w // self.patch_embed.patch_size
|
| 234 |
+
h0 = h // self.patch_embed.patch_size
|
| 235 |
+
# we add a small number to avoid floating point error in the interpolation
|
| 236 |
+
# see discussion at https://github.com/facebookresearch/dino/issues/8
|
| 237 |
+
w0, h0 = w0 + 0.1, h0 + 0.1
|
| 238 |
+
patch_pos_embed = nn.functional.interpolate(
|
| 239 |
+
patch_pos_embed.reshape(
|
| 240 |
+
1, int(math.sqrt(N)), int(math.sqrt(N)), dim
|
| 241 |
+
).permute(0, 3, 1, 2),
|
| 242 |
+
scale_factor=(w0 / math.sqrt(N), h0 / math.sqrt(N)),
|
| 243 |
+
mode="bicubic",
|
| 244 |
+
)
|
| 245 |
+
assert (
|
| 246 |
+
int(w0) == patch_pos_embed.shape[-2]
|
| 247 |
+
and int(h0) == patch_pos_embed.shape[-1]
|
| 248 |
+
)
|
| 249 |
+
patch_pos_embed = patch_pos_embed.permute(0, 2, 3, 1).view(1, -1, dim)
|
| 250 |
+
return torch.cat((class_pos_embed.unsqueeze(0), patch_pos_embed), dim=1)
|
| 251 |
+
|
| 252 |
+
def prepare_tokens(self, x, mask=None):
|
| 253 |
+
B, nc, w, h = x.shape
|
| 254 |
+
# patch linear embedding
|
| 255 |
+
x = self.patch_embed(x)
|
| 256 |
+
|
| 257 |
+
# MIM
|
| 258 |
+
if mask is not None:
|
| 259 |
+
x = self.mask_model(x, mask)
|
| 260 |
+
x = x.flatten(2).transpose(1, 2)
|
| 261 |
+
|
| 262 |
+
# add the [CLS] token to the embed patch tokens
|
| 263 |
+
cls_tokens = self.cls_token.expand(B, -1, -1)
|
| 264 |
+
x = torch.cat((cls_tokens, x), dim=1)
|
| 265 |
+
|
| 266 |
+
# add positional encoding to each token
|
| 267 |
+
x = x + self.interpolate_pos_encoding(x, w, h)
|
| 268 |
+
|
| 269 |
+
return self.pos_drop(x)
|
| 270 |
+
|
| 271 |
+
def forward(self, x: torch.Tensor, return_all_tokens=None, mask=None, **kwargs):
|
| 272 |
+
# MIM
|
| 273 |
+
if self.masked_im_modeling:
|
| 274 |
+
assert mask is not None
|
| 275 |
+
x = self.prepare_tokens(x, mask=mask)
|
| 276 |
+
else:
|
| 277 |
+
x = self.prepare_tokens(x)
|
| 278 |
+
for blk in self.blocks:
|
| 279 |
+
x = blk(x)
|
| 280 |
+
x = self.norm(x)
|
| 281 |
+
return_all_tokens = (
|
| 282 |
+
self.return_all_tokens if return_all_tokens is None else return_all_tokens
|
| 283 |
+
)
|
| 284 |
+
if return_all_tokens:
|
| 285 |
+
return x
|
| 286 |
+
|
| 287 |
+
return x[:, 0]
|
| 288 |
+
|
| 289 |
+
def get_last_selfattention(self, x: torch.Tensor):
|
| 290 |
+
x = self.prepare_tokens(x)
|
| 291 |
+
for i, blk in enumerate(self.blocks):
|
| 292 |
+
if i < len(self.blocks) - 1:
|
| 293 |
+
x = blk(x)
|
| 294 |
+
else:
|
| 295 |
+
# return attention of the last block
|
| 296 |
+
return blk(x, return_attention=True)
|
| 297 |
+
|
| 298 |
+
def get_all_selfattention(self, x: torch.Tensor):
|
| 299 |
+
x = self.prepare_tokens(x)
|
| 300 |
+
attn_out = []
|
| 301 |
+
for blk in self.blocks:
|
| 302 |
+
attn_out.append(blk(x, return_attention=True))
|
| 303 |
+
x = blk(x)
|
| 304 |
+
return attn_out
|
| 305 |
+
|
| 306 |
+
def get_intermediate_layers(self, x: torch.Tensor, n: int = 1):
|
| 307 |
+
x = self.prepare_tokens(x)
|
| 308 |
+
# we return the output tokens from the `n` last blocks
|
| 309 |
+
output = []
|
| 310 |
+
for i, blk in enumerate(self.blocks):
|
| 311 |
+
x = blk(x)
|
| 312 |
+
if len(self.blocks) - i <= n:
|
| 313 |
+
output.append(self.norm(x))
|
| 314 |
+
return output
|
| 315 |
+
|
| 316 |
+
def mask_model(self, x, mask):
|
| 317 |
+
x.permute(0, 2, 3, 1)[mask, :] = self.masked_embed.to(x.dtype)
|
| 318 |
+
return x
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def vit_tiny(patch_size: int = 16, **kwargs):
|
| 322 |
+
model = VisionTransformer(
|
| 323 |
+
patch_size=patch_size,
|
| 324 |
+
embed_dim=192,
|
| 325 |
+
depth=12,
|
| 326 |
+
num_heads=3,
|
| 327 |
+
mlp_ratio=4,
|
| 328 |
+
qkv_bias=True,
|
| 329 |
+
norm_layer=partial(nn.LayerNorm, eps=1e-6),
|
| 330 |
+
**kwargs
|
| 331 |
+
)
|
| 332 |
+
return model
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def vit_small(patch_size: int = 16, **kwargs):
|
| 336 |
+
model = VisionTransformer(
|
| 337 |
+
patch_size=patch_size,
|
| 338 |
+
embed_dim=384,
|
| 339 |
+
depth=12,
|
| 340 |
+
num_heads=6,
|
| 341 |
+
mlp_ratio=4,
|
| 342 |
+
qkv_bias=True,
|
| 343 |
+
norm_layer=partial(nn.LayerNorm, eps=1e-6),
|
| 344 |
+
**kwargs
|
| 345 |
+
)
|
| 346 |
+
return model
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def vit_base(patch_size: int = 16, **kwargs):
|
| 350 |
+
model = VisionTransformer(
|
| 351 |
+
patch_size=patch_size,
|
| 352 |
+
embed_dim=768,
|
| 353 |
+
depth=12,
|
| 354 |
+
num_heads=12,
|
| 355 |
+
mlp_ratio=4,
|
| 356 |
+
qkv_bias=True,
|
| 357 |
+
norm_layer=partial(nn.LayerNorm, eps=1e-6),
|
| 358 |
+
**kwargs
|
| 359 |
+
)
|
| 360 |
+
return model
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def vit_large(patch_size: int = 16, **kwargs):
|
| 364 |
+
model = VisionTransformer(
|
| 365 |
+
patch_size=patch_size,
|
| 366 |
+
embed_dim=1024,
|
| 367 |
+
depth=24,
|
| 368 |
+
num_heads=16,
|
| 369 |
+
mlp_ratio=4,
|
| 370 |
+
qkv_bias=True,
|
| 371 |
+
norm_layer=partial(nn.LayerNorm, eps=1e-6),
|
| 372 |
+
**kwargs
|
| 373 |
+
)
|
| 374 |
+
return model
|
skinmap_runtime/core/models/fine_tuning/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
skinmap_runtime/core/models/fine_tuning/classifiers.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from torch import nn
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class LinearClassifier(nn.Module):
|
| 5 |
+
"""Linear layer to train on top of frozen features"""
|
| 6 |
+
|
| 7 |
+
def __init__(
|
| 8 |
+
self,
|
| 9 |
+
dim: int,
|
| 10 |
+
num_labels: int = 1000,
|
| 11 |
+
use_dropout_in_head: bool = False,
|
| 12 |
+
dropout_rate: float = 0.3,
|
| 13 |
+
large_head: bool = True,
|
| 14 |
+
use_bn: bool = False,
|
| 15 |
+
log_softmax: bool = False,
|
| 16 |
+
):
|
| 17 |
+
super(LinearClassifier, self).__init__()
|
| 18 |
+
self.num_labels = num_labels
|
| 19 |
+
self.large_head = large_head
|
| 20 |
+
self.use_bn = use_bn
|
| 21 |
+
self.log_softmax = log_softmax
|
| 22 |
+
self.use_dropout_in_head = use_dropout_in_head
|
| 23 |
+
|
| 24 |
+
if self.use_dropout_in_head:
|
| 25 |
+
self.dropout = nn.Dropout(dropout_rate)
|
| 26 |
+
if self.use_bn:
|
| 27 |
+
self.bn = nn.BatchNorm1d(dim)
|
| 28 |
+
|
| 29 |
+
if self.large_head:
|
| 30 |
+
self.linear = nn.Linear(dim, 128)
|
| 31 |
+
self.linear.weight.data.normal_(mean=0.0, std=0.01)
|
| 32 |
+
self.linear.bias.data.zero_()
|
| 33 |
+
self.relu = nn.ReLU()
|
| 34 |
+
|
| 35 |
+
self.dropout2 = nn.Dropout(dropout_rate)
|
| 36 |
+
if self.use_bn:
|
| 37 |
+
self.bn2 = nn.BatchNorm1d(128)
|
| 38 |
+
|
| 39 |
+
self.linear2 = nn.Linear(128, num_labels)
|
| 40 |
+
self.linear2.weight.data.normal_(mean=0.0, std=0.01)
|
| 41 |
+
self.linear2.bias.data.zero_()
|
| 42 |
+
else:
|
| 43 |
+
self.linear = nn.Linear(dim, num_labels)
|
| 44 |
+
self.linear.weight.data.normal_(mean=0.0, std=0.01)
|
| 45 |
+
self.linear.bias.data.zero_()
|
| 46 |
+
|
| 47 |
+
def forward(self, x):
|
| 48 |
+
# flatten
|
| 49 |
+
x = x.view(x.size(0), -1)
|
| 50 |
+
# dropout
|
| 51 |
+
if self.use_dropout_in_head:
|
| 52 |
+
x = self.dropout(x)
|
| 53 |
+
if self.use_bn:
|
| 54 |
+
x = self.bn(x)
|
| 55 |
+
# 1. linear layer
|
| 56 |
+
x = self.linear(x)
|
| 57 |
+
# smaller version of head
|
| 58 |
+
if self.large_head:
|
| 59 |
+
x = self.relu(x)
|
| 60 |
+
x = self.dropout2(x)
|
| 61 |
+
if self.use_bn:
|
| 62 |
+
x = self.bn2(x)
|
| 63 |
+
# 2. linear layer
|
| 64 |
+
x = self.linear2(x)
|
| 65 |
+
# output
|
| 66 |
+
if self.log_softmax:
|
| 67 |
+
return nn.LogSoftmax(dim=1)(x)
|
| 68 |
+
else:
|
| 69 |
+
return x
|
skinmap_runtime/core/models/ibot/__init__.py
ADDED
|
File without changes
|
skinmap_runtime/core/models/ibot/head.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn as nn
|
| 2 |
+
|
| 3 |
+
from ...models.dino.head import DINOHead
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class iBOTHead(DINOHead):
|
| 7 |
+
def __init__(
|
| 8 |
+
self,
|
| 9 |
+
*args,
|
| 10 |
+
patch_out_dim=8192,
|
| 11 |
+
n_layers=3,
|
| 12 |
+
hidden_dim=2048,
|
| 13 |
+
bottleneck_dim=256,
|
| 14 |
+
norm_last_layer=True,
|
| 15 |
+
shared_head=False,
|
| 16 |
+
**kwargs
|
| 17 |
+
):
|
| 18 |
+
super(iBOTHead, self).__init__(
|
| 19 |
+
*args,
|
| 20 |
+
n_layers=n_layers,
|
| 21 |
+
hidden_dim=hidden_dim,
|
| 22 |
+
bottleneck_dim=bottleneck_dim,
|
| 23 |
+
norm_last_layer=norm_last_layer,
|
| 24 |
+
**kwargs
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
if not shared_head:
|
| 28 |
+
if bottleneck_dim > 0:
|
| 29 |
+
self.last_layer2 = nn.utils.weight_norm(
|
| 30 |
+
nn.Linear(bottleneck_dim, patch_out_dim, bias=False)
|
| 31 |
+
)
|
| 32 |
+
self.last_layer2.weight_g.data.fill_(1)
|
| 33 |
+
if norm_last_layer:
|
| 34 |
+
self.last_layer2.weight_g.requires_grad = False
|
| 35 |
+
else:
|
| 36 |
+
self.mlp2 = nn.Linear(hidden_dim, patch_out_dim)
|
| 37 |
+
self.last_layer2 = None
|
| 38 |
+
|
| 39 |
+
else:
|
| 40 |
+
if bottleneck_dim > 0:
|
| 41 |
+
self.last_layer2 = self.last_layer
|
| 42 |
+
else:
|
| 43 |
+
self.mlp2 = self.mlp[-1]
|
| 44 |
+
self.last_layer2 = None
|
| 45 |
+
|
| 46 |
+
def forward(self, x):
|
| 47 |
+
if len(x.shape) == 2:
|
| 48 |
+
return super(iBOTHead, self).forward(x)
|
| 49 |
+
|
| 50 |
+
if self.last_layer is not None:
|
| 51 |
+
x = self.mlp(x)
|
| 52 |
+
x = nn.functional.normalize(x, dim=-1, p=2)
|
| 53 |
+
x1 = self.last_layer(x[:, 0])
|
| 54 |
+
x2 = self.last_layer2(x[:, 1:])
|
| 55 |
+
else:
|
| 56 |
+
x = self.mlp[:-1](x)
|
| 57 |
+
x1 = self.mlp[-1](x[:, 0])
|
| 58 |
+
x2 = self.mlp2(x[:, 1:])
|
| 59 |
+
|
| 60 |
+
return x1, x2
|
| 61 |
+
|
| 62 |
+
def _build_norm(self, norm, hidden_dim, **kwargs):
|
| 63 |
+
if norm == "bn":
|
| 64 |
+
norm = nn.BatchNorm1d(hidden_dim, **kwargs)
|
| 65 |
+
elif norm == "syncbn":
|
| 66 |
+
norm = nn.SyncBatchNorm(hidden_dim, **kwargs)
|
| 67 |
+
elif norm == "ln":
|
| 68 |
+
norm = nn.LayerNorm(hidden_dim, **kwargs)
|
| 69 |
+
else:
|
| 70 |
+
assert norm is None, "unknown norm type {}".format(norm)
|
| 71 |
+
return norm
|
| 72 |
+
|
| 73 |
+
def _build_act(self, act):
|
| 74 |
+
if act == "relu":
|
| 75 |
+
act = nn.ReLU()
|
| 76 |
+
elif act == "gelu":
|
| 77 |
+
act = nn.GELU()
|
| 78 |
+
else:
|
| 79 |
+
assert False, "unknown act type {}".format(act)
|
| 80 |
+
return act
|
skinmap_runtime/core/models/mae/__init__.py
ADDED
|
File without changes
|
skinmap_runtime/core/models/mae/model.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from functools import partial
|
| 2 |
+
from typing import List
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
import torch.nn as nn
|
| 6 |
+
|
| 7 |
+
from ..encoders.vision_transformer import Block, VisionTransformer
|
| 8 |
+
from ..utils import get_2d_sincos_pos_embed
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class MaskedAutoencoderViT(VisionTransformer):
|
| 12 |
+
"""Masked Autoencoder with VisionTransformer backbone."""
|
| 13 |
+
|
| 14 |
+
def __init__(
|
| 15 |
+
self,
|
| 16 |
+
img_size: List[int] = [224],
|
| 17 |
+
patch_size: int = 16,
|
| 18 |
+
in_channels: int = 3,
|
| 19 |
+
num_classes: int = 0,
|
| 20 |
+
embed_dim: int = 768,
|
| 21 |
+
depth: int = 12,
|
| 22 |
+
num_heads: int = 12,
|
| 23 |
+
mlp_ratio: float = 4.0,
|
| 24 |
+
qkv_bias: bool = False,
|
| 25 |
+
qk_scale=None,
|
| 26 |
+
drop_rate: float = 0.0,
|
| 27 |
+
attn_drop_rate: float = 0.0,
|
| 28 |
+
drop_path_rate: float = 0.0,
|
| 29 |
+
norm_layer: nn.Module = nn.LayerNorm,
|
| 30 |
+
return_all_tokens: bool = False,
|
| 31 |
+
masked_im_modeling: bool = False,
|
| 32 |
+
decoder_embed_dim: int = 512,
|
| 33 |
+
decoder_depth: int = 8,
|
| 34 |
+
decoder_num_heads: int = 16,
|
| 35 |
+
):
|
| 36 |
+
super().__init__(
|
| 37 |
+
img_size=img_size,
|
| 38 |
+
patch_size=patch_size,
|
| 39 |
+
in_channels=in_channels,
|
| 40 |
+
num_classes=num_classes,
|
| 41 |
+
embed_dim=embed_dim,
|
| 42 |
+
depth=depth,
|
| 43 |
+
num_heads=num_heads,
|
| 44 |
+
mlp_ratio=mlp_ratio,
|
| 45 |
+
qkv_bias=qkv_bias,
|
| 46 |
+
qk_scale=qk_scale,
|
| 47 |
+
drop_rate=drop_rate,
|
| 48 |
+
attn_drop_rate=attn_drop_rate,
|
| 49 |
+
drop_path_rate=drop_path_rate,
|
| 50 |
+
norm_layer=norm_layer,
|
| 51 |
+
return_all_tokens=return_all_tokens,
|
| 52 |
+
masked_im_modeling=masked_im_modeling,
|
| 53 |
+
)
|
| 54 |
+
num_patches = self.patch_embed.num_patches
|
| 55 |
+
# fixed sin-cos embedding
|
| 56 |
+
self.pos_embed.requires_grad = False
|
| 57 |
+
|
| 58 |
+
# MAE decoder specifics
|
| 59 |
+
self.decoder_embed = nn.Linear(embed_dim, decoder_embed_dim, bias=True)
|
| 60 |
+
self.mask_token = nn.Parameter(torch.zeros(1, 1, decoder_embed_dim))
|
| 61 |
+
# fixed sin-cos embedding
|
| 62 |
+
self.decoder_pos_embed = nn.Parameter(
|
| 63 |
+
data=torch.zeros(1, num_patches + 1, decoder_embed_dim),
|
| 64 |
+
requires_grad=False,
|
| 65 |
+
)
|
| 66 |
+
self.decoder_blocks = nn.ModuleList(
|
| 67 |
+
[
|
| 68 |
+
Block(
|
| 69 |
+
dim=decoder_embed_dim,
|
| 70 |
+
num_heads=decoder_num_heads,
|
| 71 |
+
mlp_ratio=mlp_ratio,
|
| 72 |
+
qkv_bias=True,
|
| 73 |
+
qk_scale=None,
|
| 74 |
+
norm_layer=norm_layer,
|
| 75 |
+
)
|
| 76 |
+
for _ in range(decoder_depth)
|
| 77 |
+
]
|
| 78 |
+
)
|
| 79 |
+
# decoder to patch
|
| 80 |
+
self.decoder_norm = norm_layer(decoder_embed_dim)
|
| 81 |
+
self.decoder_pred = nn.Linear(
|
| 82 |
+
decoder_embed_dim,
|
| 83 |
+
patch_size**2 * in_channels,
|
| 84 |
+
bias=True,
|
| 85 |
+
)
|
| 86 |
+
self.initialize_weights()
|
| 87 |
+
|
| 88 |
+
def initialize_weights(self):
|
| 89 |
+
# initialize (and freeze) pos_embed by sin-cos embedding
|
| 90 |
+
pos_embed = get_2d_sincos_pos_embed(
|
| 91 |
+
self.pos_embed.shape[-1],
|
| 92 |
+
int(self.patch_embed.num_patches**0.5),
|
| 93 |
+
cls_token=True,
|
| 94 |
+
)
|
| 95 |
+
self.pos_embed.data.copy_(torch.from_numpy(pos_embed).float().unsqueeze(0))
|
| 96 |
+
|
| 97 |
+
decoder_pos_embed = get_2d_sincos_pos_embed(
|
| 98 |
+
self.decoder_pos_embed.shape[-1],
|
| 99 |
+
int(self.patch_embed.num_patches**0.5),
|
| 100 |
+
cls_token=True,
|
| 101 |
+
)
|
| 102 |
+
self.decoder_pos_embed.data.copy_(
|
| 103 |
+
torch.from_numpy(decoder_pos_embed).float().unsqueeze(0)
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
# initialize patch_embed like nn.Linear (instead of nn.Conv2d)
|
| 107 |
+
w = self.patch_embed.proj.weight.data
|
| 108 |
+
torch.nn.init.xavier_uniform_(w.view([w.shape[0], -1]))
|
| 109 |
+
|
| 110 |
+
# timm's trunc_normal_(std=.02) is effectively normal_(std=0.02) as cutoff is too big (2.)
|
| 111 |
+
torch.nn.init.normal_(self.cls_token, std=0.02)
|
| 112 |
+
torch.nn.init.normal_(self.mask_token, std=0.02)
|
| 113 |
+
|
| 114 |
+
# initialize nn.Linear and nn.LayerNorm
|
| 115 |
+
self.apply(self._init_weights)
|
| 116 |
+
|
| 117 |
+
def _init_weights(self, m):
|
| 118 |
+
if isinstance(m, nn.Linear):
|
| 119 |
+
# we use xavier_uniform following official JAX ViT:
|
| 120 |
+
torch.nn.init.xavier_uniform_(m.weight)
|
| 121 |
+
if isinstance(m, nn.Linear) and m.bias is not None:
|
| 122 |
+
nn.init.constant_(m.bias, 0)
|
| 123 |
+
elif isinstance(m, nn.LayerNorm):
|
| 124 |
+
nn.init.constant_(m.bias, 0)
|
| 125 |
+
nn.init.constant_(m.weight, 1.0)
|
| 126 |
+
|
| 127 |
+
def prepare_tokens(
|
| 128 |
+
self,
|
| 129 |
+
x: torch.Tensor,
|
| 130 |
+
mask_ratio: float = 0.0,
|
| 131 |
+
return_helpers: bool = False,
|
| 132 |
+
**kwargs,
|
| 133 |
+
):
|
| 134 |
+
# embed patches
|
| 135 |
+
x = self.patch_embed(x)
|
| 136 |
+
x = x.flatten(2).transpose(1, 2)
|
| 137 |
+
|
| 138 |
+
# add pos embed w/o cls token
|
| 139 |
+
x = x + self.pos_embed[:, 1:, :]
|
| 140 |
+
|
| 141 |
+
# Perform per-sample random masking by per-sample shuffling.
|
| 142 |
+
# Per-sample shuffling is done by argsort random noise.
|
| 143 |
+
N, N_PATCHES, D = x.shape
|
| 144 |
+
len_keep = int(N_PATCHES * (1 - mask_ratio))
|
| 145 |
+
|
| 146 |
+
# generate random noise for the selection (in [0, 1])
|
| 147 |
+
noise = torch.rand(N, N_PATCHES, device=x.device)
|
| 148 |
+
# sort noise for each sample (ascend: small is keep, large is remove)
|
| 149 |
+
ids_shuffle = torch.argsort(noise, dim=1)
|
| 150 |
+
# IDs to restore the mask (used for the decoder)
|
| 151 |
+
ids_restore = torch.argsort(ids_shuffle, dim=1)
|
| 152 |
+
|
| 153 |
+
# keep the first subset (random ids to keep from every sample)
|
| 154 |
+
ids_keep = ids_shuffle[:, :len_keep]
|
| 155 |
+
# reshape for alignment with "x"
|
| 156 |
+
ids_keep = ids_keep.unsqueeze(-1).repeat(1, 1, D)
|
| 157 |
+
# masked input
|
| 158 |
+
x_masked = torch.gather(x, dim=1, index=ids_keep)
|
| 159 |
+
|
| 160 |
+
# generate the binary mask: 0 is keep, 1 is remove
|
| 161 |
+
mask = torch.ones([N, N_PATCHES], device=x.device)
|
| 162 |
+
mask[:, :len_keep] = 0
|
| 163 |
+
# unshuffle to get the binary mask
|
| 164 |
+
# binary mask used to create "x_masked"
|
| 165 |
+
mask = torch.gather(mask, dim=1, index=ids_restore)
|
| 166 |
+
|
| 167 |
+
# append cls token
|
| 168 |
+
cls_token = self.cls_token + self.pos_embed[:, :1, :]
|
| 169 |
+
cls_tokens = cls_token.expand(x_masked.shape[0], -1, -1)
|
| 170 |
+
x_masked = torch.cat((cls_tokens, x_masked), dim=1)
|
| 171 |
+
|
| 172 |
+
if return_helpers:
|
| 173 |
+
return x_masked, mask, ids_restore
|
| 174 |
+
else:
|
| 175 |
+
return x_masked
|
| 176 |
+
|
| 177 |
+
def forward_decoder(self, x: torch.Tensor, ids_restore: torch.Tensor):
|
| 178 |
+
# embed tokens
|
| 179 |
+
x = self.decoder_embed(x)
|
| 180 |
+
|
| 181 |
+
# append mask tokens to sequence
|
| 182 |
+
mask_tokens = self.mask_token.repeat(
|
| 183 |
+
x.shape[0], ids_restore.shape[1] + 1 - x.shape[1], 1
|
| 184 |
+
)
|
| 185 |
+
x_ = torch.cat([x[:, 1:, :], mask_tokens], dim=1) # no cls token
|
| 186 |
+
x_ = torch.gather(
|
| 187 |
+
x_, dim=1, index=ids_restore.unsqueeze(-1).repeat(1, 1, x.shape[2])
|
| 188 |
+
) # unshuffle
|
| 189 |
+
x = torch.cat([x[:, :1, :], x_], dim=1) # append cls token
|
| 190 |
+
|
| 191 |
+
# add pos embed
|
| 192 |
+
x = x + self.decoder_pos_embed
|
| 193 |
+
|
| 194 |
+
# apply Transformer blocks
|
| 195 |
+
for blk in self.decoder_blocks:
|
| 196 |
+
x = blk(x)
|
| 197 |
+
x = self.decoder_norm(x)
|
| 198 |
+
|
| 199 |
+
# predictor projection
|
| 200 |
+
x = self.decoder_pred(x)
|
| 201 |
+
|
| 202 |
+
# remove cls token
|
| 203 |
+
x = x[:, 1:, :]
|
| 204 |
+
|
| 205 |
+
return x
|
| 206 |
+
|
| 207 |
+
def forward(
|
| 208 |
+
self,
|
| 209 |
+
imgs: torch.Tensor,
|
| 210 |
+
mask_ratio: float = 0.75,
|
| 211 |
+
return_all_tokens=None,
|
| 212 |
+
**kwargs,
|
| 213 |
+
):
|
| 214 |
+
x, mask, ids_restore = self.prepare_tokens(
|
| 215 |
+
x=imgs,
|
| 216 |
+
mask_ratio=mask_ratio,
|
| 217 |
+
return_helpers=True,
|
| 218 |
+
)
|
| 219 |
+
for blk in self.blocks:
|
| 220 |
+
x = blk(x)
|
| 221 |
+
latent = self.norm(x)
|
| 222 |
+
# [N, N_PATCHES, p*p*3]
|
| 223 |
+
pred = self.forward_decoder(x=latent, ids_restore=ids_restore)
|
| 224 |
+
return_all_tokens = (
|
| 225 |
+
self.return_all_tokens if return_all_tokens is None else return_all_tokens
|
| 226 |
+
)
|
| 227 |
+
if return_all_tokens:
|
| 228 |
+
return imgs, pred, mask, latent
|
| 229 |
+
return imgs, pred, mask, latent[:, 0]
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def masked_vit_tiny(patch_size: int = 16, **kwargs):
|
| 233 |
+
model = MaskedAutoencoderViT(
|
| 234 |
+
patch_size=patch_size,
|
| 235 |
+
embed_dim=192,
|
| 236 |
+
depth=12,
|
| 237 |
+
num_heads=3,
|
| 238 |
+
mlp_ratio=4,
|
| 239 |
+
qkv_bias=True,
|
| 240 |
+
norm_layer=partial(nn.LayerNorm, eps=1e-6),
|
| 241 |
+
decoder_embed_dim=512,
|
| 242 |
+
decoder_depth=8,
|
| 243 |
+
decoder_num_heads=16,
|
| 244 |
+
**kwargs,
|
| 245 |
+
)
|
| 246 |
+
return model
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def masked_vit_small(patch_size: int = 16, **kwargs):
|
| 250 |
+
model = MaskedAutoencoderViT(
|
| 251 |
+
patch_size=patch_size,
|
| 252 |
+
embed_dim=384,
|
| 253 |
+
depth=12,
|
| 254 |
+
num_heads=6,
|
| 255 |
+
mlp_ratio=4,
|
| 256 |
+
qkv_bias=True,
|
| 257 |
+
norm_layer=partial(nn.LayerNorm, eps=1e-6),
|
| 258 |
+
decoder_embed_dim=512,
|
| 259 |
+
decoder_depth=8,
|
| 260 |
+
decoder_num_heads=16,
|
| 261 |
+
**kwargs,
|
| 262 |
+
)
|
| 263 |
+
return model
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def masked_vit_base(patch_size: int = 16, **kwargs):
|
| 267 |
+
model = MaskedAutoencoderViT(
|
| 268 |
+
patch_size=patch_size,
|
| 269 |
+
embed_dim=768,
|
| 270 |
+
depth=12,
|
| 271 |
+
num_heads=12,
|
| 272 |
+
mlp_ratio=4,
|
| 273 |
+
qkv_bias=True,
|
| 274 |
+
norm_layer=partial(nn.LayerNorm, eps=1e-6),
|
| 275 |
+
decoder_embed_dim=512,
|
| 276 |
+
decoder_depth=8,
|
| 277 |
+
decoder_num_heads=16,
|
| 278 |
+
**kwargs,
|
| 279 |
+
)
|
| 280 |
+
return model
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def masked_vit_large(patch_size: int = 16, **kwargs):
|
| 284 |
+
model = MaskedAutoencoderViT(
|
| 285 |
+
patch_size=patch_size,
|
| 286 |
+
embed_dim=1024,
|
| 287 |
+
depth=24,
|
| 288 |
+
num_heads=16,
|
| 289 |
+
mlp_ratio=4,
|
| 290 |
+
qkv_bias=True,
|
| 291 |
+
norm_layer=partial(nn.LayerNorm, eps=1e-6),
|
| 292 |
+
decoder_embed_dim=512,
|
| 293 |
+
decoder_depth=8,
|
| 294 |
+
decoder_num_heads=16,
|
| 295 |
+
**kwargs,
|
| 296 |
+
)
|
| 297 |
+
return model
|
skinmap_runtime/core/models/mae/utils.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Callable, Tuple
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
from ....src.models.utils import ModelType
|
| 6 |
+
from .model import masked_vit_base, masked_vit_large, masked_vit_small, masked_vit_tiny
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def patch_images(images: torch.Tensor, patch_size: int):
|
| 10 |
+
"""
|
| 11 |
+
Transforms images into patched images.
|
| 12 |
+
|
| 13 |
+
imgs: (N, C, H, W)
|
| 14 |
+
x: (N, #patches, patch_size**2 * C)
|
| 15 |
+
"""
|
| 16 |
+
# make sure the image properties are correct
|
| 17 |
+
img_is_square = images.shape[2] == images.shape[3]
|
| 18 |
+
img_is_patchable = images.shape[2] % patch_size == 0
|
| 19 |
+
assert img_is_square and img_is_patchable
|
| 20 |
+
|
| 21 |
+
channels = images.shape[1]
|
| 22 |
+
h = w = images.shape[2] // patch_size
|
| 23 |
+
x = images.reshape(shape=(images.shape[0], channels, h, patch_size, w, patch_size))
|
| 24 |
+
x = torch.einsum("nchpwq->nhwpqc", x)
|
| 25 |
+
x = x.reshape(shape=(images.shape[0], h * w, patch_size**2 * channels))
|
| 26 |
+
return x
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def unpatch_images(x: torch.Tensor, patch_size: int):
|
| 30 |
+
"""
|
| 31 |
+
Transforms patched images into images.
|
| 32 |
+
|
| 33 |
+
x: (N, #patches, patch_size**2 * C)
|
| 34 |
+
imgs: (N, C, H, W)
|
| 35 |
+
"""
|
| 36 |
+
h = w = int(x.shape[1] ** 0.5)
|
| 37 |
+
assert h * w == x.shape[1]
|
| 38 |
+
|
| 39 |
+
x = x.reshape(shape=(x.shape[0], h, w, patch_size, patch_size, 3))
|
| 40 |
+
x = torch.einsum("nhwpqc->nchpwq", x)
|
| 41 |
+
imgs = x.reshape(shape=(x.shape[0], 3, h * patch_size, h * patch_size))
|
| 42 |
+
return imgs
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
MASKED_VIT_DICT = {
|
| 46 |
+
"masked_vit_tiny": masked_vit_tiny,
|
| 47 |
+
"masked_vit_small": masked_vit_small,
|
| 48 |
+
"masked_vit_base": masked_vit_base,
|
| 49 |
+
"masked_vit_large": masked_vit_large,
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def get_model_class(base_model_name: str) -> Tuple[Callable, ModelType]:
|
| 54 |
+
encoder_cls = MASKED_VIT_DICT.get(base_model_name, None)
|
| 55 |
+
model_type = ModelType.VIT
|
| 56 |
+
if encoder_cls is None:
|
| 57 |
+
raise ValueError(f"Invalid base model name: {base_model_name}")
|
| 58 |
+
return encoder_cls, model_type
|
skinmap_runtime/core/models/simclr/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
skinmap_runtime/core/models/simclr/model.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn as nn
|
| 2 |
+
import torch.nn.functional as F
|
| 3 |
+
|
| 4 |
+
from ..encoders.utils import get_encoder_class
|
| 5 |
+
from ..utils import ModelType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class ResNetSimCLR(nn.Module):
|
| 9 |
+
def __init__(self, base_model: str, out_dim: int, **kwargs):
|
| 10 |
+
super(ResNetSimCLR, self).__init__()
|
| 11 |
+
encoder_cls, model_type = get_encoder_class(base_model)
|
| 12 |
+
if model_type is ModelType.VIT:
|
| 13 |
+
self.backbone = encoder_cls(**kwargs)
|
| 14 |
+
n_feat = self.backbone.embed_dim
|
| 15 |
+
elif model_type is ModelType.CNN:
|
| 16 |
+
encoder = encoder_cls(**kwargs)
|
| 17 |
+
n_feat = encoder.fc.in_features
|
| 18 |
+
self.backbone = nn.Sequential(*list(encoder.children())[:-1])
|
| 19 |
+
else:
|
| 20 |
+
raise ValueError(f"Unknown model type: {model_type}")
|
| 21 |
+
# projection MLP
|
| 22 |
+
self.dense1 = nn.Linear(n_feat, n_feat)
|
| 23 |
+
self.dense2 = nn.Linear(n_feat, out_dim)
|
| 24 |
+
|
| 25 |
+
def forward(self, z):
|
| 26 |
+
# embed
|
| 27 |
+
e = self.backbone(z)
|
| 28 |
+
e = e.squeeze()
|
| 29 |
+
# project
|
| 30 |
+
z = self.dense1(e)
|
| 31 |
+
z = F.relu(z)
|
| 32 |
+
z = self.dense2(z)
|
| 33 |
+
return e, z
|