Spaces:
Running
Running
style(dashboard): translate commentaries
Browse files- .gitignore +3 -1
- analysis/cache_utils.py +49 -47
- analysis/config.py +10 -13
- analysis/datasets.py +253 -77
- analysis/features/feature_filters.py +37 -37
- analysis/features/feature_matrix.py +107 -28
- analysis/features/feature_selectors.py +5 -5
- analysis/features/feature_stats.py +20 -20
- analysis/metrics/correlations.py +2 -3
- analysis/models.py +20 -20
- analysis/utils.py +17 -17
- analysis/viz/umap_utils.py +69 -12
- analysis/viz/vis_heatmaps.py +199 -116
- analysis/viz/vis_scatter.py +26 -26
- assets/style.css +4 -0
- dashboard/__init__.py +11 -1
- dashboard/image_utils.py +6 -8
- dashboard/model_catalog.py +213 -92
- dashboard/umap_service.py +158 -208
- dashboard/user_image_service.py +10 -28
- dashboard/viz_helpers.py +63 -11
- default_configs/06_config.json +1 -0
- default_configs/sae_vis_config.json +3 -7
- pages/feature_maps.py +45 -1
- pages/visualizations.py +18 -40
- train_code/benjdataset.py +7 -7
- train_code/timmdataset.py +8 -8
- train_code/train_script_arniqa.py +9 -9
- train_code/train_script_liqe.py +1 -1
- train_code/train_script_maniqa.py +8 -8
- train_code/train_utils.py +6 -6
.gitignore
CHANGED
|
@@ -1,2 +1,4 @@
|
|
| 1 |
data/
|
| 2 |
-
tests/
|
|
|
|
|
|
|
|
|
| 1 |
data/
|
| 2 |
+
tests/
|
| 3 |
+
__pycache__/
|
| 4 |
+
.venv/
|
analysis/cache_utils.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
"""
|
| 4 |
|
| 5 |
from pathlib import Path
|
|
@@ -17,20 +17,20 @@ from analysis.models import _iqa_activations
|
|
| 17 |
|
| 18 |
|
| 19 |
def _df_mb(df: pd.DataFrame) -> float:
|
| 20 |
-
"""
|
| 21 |
return df.memory_usage(deep=True).sum() / 1024 ** 2
|
| 22 |
|
| 23 |
|
| 24 |
def _sparse_mb(mat: sp.csr_matrix) -> float:
|
| 25 |
-
"""
|
| 26 |
return (mat.data.nbytes + mat.indices.nbytes + mat.indptr.nbytes) / 1024 ** 2
|
| 27 |
|
| 28 |
|
| 29 |
def _cache_paths(base_path: str) -> Tuple[str, str, str]:
|
| 30 |
"""
|
| 31 |
-
|
| 32 |
|
| 33 |
-
|
| 34 |
'cache/kadid_acts.feather'
|
| 35 |
→ ('cache/kadid_acts_meta.feather', 'cache/kadid_acts_codes.npz',
|
| 36 |
'cache/kadid_acts_steps.npz')
|
|
@@ -273,8 +273,8 @@ def collect_and_cache(
|
|
| 273 |
sparse_mb = _sparse_mb(codes_csr)
|
| 274 |
steps_mb = _sparse_mb(steps_csr)
|
| 275 |
|
| 276 |
-
print(f'Activations: shape={codes_csr.shape}, {sparse_mb:.1f}
|
| 277 |
-
print(f'Activation steps: shape={steps_csr.shape}, {steps_mb:.1f}
|
| 278 |
|
| 279 |
meta_path, codes_path, steps_path = _cache_paths(output_path)
|
| 280 |
|
|
@@ -285,9 +285,9 @@ def collect_and_cache(
|
|
| 285 |
print(f'Saved metadata ({len(meta_df)} rows) -> {meta_path}')
|
| 286 |
print(f'Saved activations -> {codes_path}')
|
| 287 |
print(f'Saved activation steps -> {steps_path}')
|
| 288 |
-
print(f' Metadata: {_df_mb(meta_df):.1f}
|
| 289 |
-
print(f' Activations: {sparse_mb:.1f}
|
| 290 |
-
print(f' Steps: {steps_mb:.1f}
|
| 291 |
|
| 292 |
if pristine_dataloader is not None:
|
| 293 |
print('\nProcessing pristine dataset...')
|
|
@@ -318,8 +318,8 @@ def collect_and_cache(
|
|
| 318 |
sp.save_npz(pristine_codes_path, pristine_codes)
|
| 319 |
sp.save_npz(pristine_steps_path, pristine_steps)
|
| 320 |
|
| 321 |
-
print(f'Pristine activations: shape={pristine_codes.shape}, {pristine_sparse_mb:.1f}
|
| 322 |
-
print(f'Pristine steps: shape={pristine_steps.shape}, {pristine_steps_mb:.1f}
|
| 323 |
|
| 324 |
print(f'Saved pristine metadata ({len(pristine_meta)} rows) -> {pristine_meta_path}')
|
| 325 |
print(f'Saved pristine activations -> {pristine_codes_path}')
|
|
@@ -333,7 +333,7 @@ def build_activation_cache(
|
|
| 333 |
dataset: str,
|
| 334 |
cache_path: str,
|
| 335 |
checkpoint_path: str,
|
| 336 |
-
|
| 337 |
layer_num: int,
|
| 338 |
iqa_metric: str,
|
| 339 |
swin_num: int,
|
|
@@ -348,6 +348,7 @@ def build_activation_cache(
|
|
| 348 |
add_patch_mask_stats: bool = True,
|
| 349 |
include_pristine: bool = True,
|
| 350 |
show_progress_bars: bool = True,
|
|
|
|
| 351 |
) -> Dict[str, Any]:
|
| 352 |
"""Build activation cache end-to-end for KADID/local-KADID datasets."""
|
| 353 |
from .datasets import (
|
|
@@ -377,7 +378,7 @@ def build_activation_cache(
|
|
| 377 |
label_to_dist_group = None
|
| 378 |
|
| 379 |
if dataset == 'local_kadid':
|
| 380 |
-
data = LocalKadidPresavedDataset(root=
|
| 381 |
collate_fn = local_kadid_collate_fn
|
| 382 |
meta_keys = [
|
| 383 |
'dist_type',
|
|
@@ -392,11 +393,11 @@ def build_activation_cache(
|
|
| 392 |
'distorted_img_path',
|
| 393 |
'original_img_path',
|
| 394 |
]
|
| 395 |
-
pristine_data = LocalKadidPristineDataset(root=
|
| 396 |
pristine_collate = local_kadid_pristine_collate_fn
|
| 397 |
elif dataset in {'kadid10k', 'kadid'}:
|
| 398 |
data = Kadid10kDataset(
|
| 399 |
-
root=
|
| 400 |
crop_size=crop_size,
|
| 401 |
min_distortion_level=min_distortion_level or 1,
|
| 402 |
)
|
|
@@ -409,11 +410,11 @@ def build_activation_cache(
|
|
| 409 |
'distorted_img_path',
|
| 410 |
'original_img_path',
|
| 411 |
]
|
| 412 |
-
pristine_data = KadidPristineDataset(root=
|
| 413 |
pristine_collate = kadid_pristine_collate_fn
|
| 414 |
elif dataset == 'QGround':
|
| 415 |
data = QGroundDataset(
|
| 416 |
-
root=
|
| 417 |
split='test',
|
| 418 |
crop_size=crop_size,
|
| 419 |
)
|
|
@@ -438,9 +439,10 @@ def build_activation_cache(
|
|
| 438 |
pristine_collate = None
|
| 439 |
elif dataset == 'SRGround':
|
| 440 |
data = SRGroundSmallDataset(
|
| 441 |
-
root=
|
| 442 |
json_path='srground_train.json',
|
| 443 |
crop_size=crop_size,
|
|
|
|
| 444 |
)
|
| 445 |
collate_fn = srground_collate_fn
|
| 446 |
meta_keys = [
|
|
@@ -538,11 +540,10 @@ def load_cache(
|
|
| 538 |
Tuple[pd.DataFrame, sp.csr_matrix],
|
| 539 |
Tuple[pd.DataFrame, sp.csr_matrix, sp.csr_matrix],
|
| 540 |
]:
|
| 541 |
-
"""
|
| 542 |
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
а ``k>0`` соответствует шагу ``k`` в pursuit.
|
| 546 |
"""
|
| 547 |
meta_path, codes_path, steps_path = _cache_paths(path)
|
| 548 |
meta = pd.read_feather(meta_path)
|
|
@@ -550,8 +551,8 @@ def load_cache(
|
|
| 550 |
|
| 551 |
print(f'Loaded from {meta_path}: {meta.shape[0]} rows × {meta.shape[1]} cols')
|
| 552 |
print(f'Loaded from {codes_path}: shape={codes.shape}, dtype={codes.dtype}')
|
| 553 |
-
print(f' Metadata: {_df_mb(meta):.1f}
|
| 554 |
-
print(f' Activations: {_sparse_mb(codes):.1f}
|
| 555 |
|
| 556 |
keep_idx: Optional[np.ndarray] = None
|
| 557 |
if min_distortion_level is not None or max_distortion_level is not None:
|
|
@@ -569,7 +570,7 @@ def load_cache(
|
|
| 569 |
keep_idx = np.flatnonzero(keep_mask.to_numpy())
|
| 570 |
else:
|
| 571 |
keep_mask = (meta['dist_level'] >= -1000)
|
| 572 |
-
keep_idx = np.flatnonzero(keep_mask.to_numpy())
|
| 573 |
|
| 574 |
if return_activation_steps:
|
| 575 |
if Path(steps_path).exists():
|
|
@@ -592,7 +593,7 @@ def load_cache(
|
|
| 592 |
f'{meta.shape[0]} rows kept'
|
| 593 |
)
|
| 594 |
|
| 595 |
-
print(f' Steps: {_sparse_mb(steps):.1f}
|
| 596 |
return meta, codes, steps
|
| 597 |
|
| 598 |
if keep_idx is not None:
|
|
@@ -620,8 +621,8 @@ def load_pristine_cache(
|
|
| 620 |
|
| 621 |
print(f'Loaded pristine from {meta_path}: {meta.shape[0]} rows × {meta.shape[1]} cols')
|
| 622 |
print(f'Loaded pristine from {codes_path}: shape={codes.shape}, dtype={codes.dtype}')
|
| 623 |
-
print(f' Metadata: {_df_mb(meta):.1f}
|
| 624 |
-
print(f' Activations: {_sparse_mb(codes):.1f}
|
| 625 |
|
| 626 |
if return_activation_steps:
|
| 627 |
if Path(steps_path).exists():
|
|
@@ -635,7 +636,7 @@ def load_pristine_cache(
|
|
| 635 |
print('No pristine steps cache found. Using all-zero activation steps.')
|
| 636 |
steps = sp.csr_matrix(codes.shape, dtype=np.int32)
|
| 637 |
|
| 638 |
-
print(f' Steps: {_sparse_mb(steps):.1f}
|
| 639 |
return meta, codes, steps
|
| 640 |
|
| 641 |
return meta, codes
|
|
@@ -649,12 +650,12 @@ def ensure_cache_ready(
|
|
| 649 |
load_cache_kwargs: Optional[Dict[str, Any]] = None,
|
| 650 |
build_cache_fn: Optional[Callable[[], None]] = None,
|
| 651 |
) -> None:
|
| 652 |
-
"""
|
| 653 |
|
| 654 |
-
|
| 655 |
-
-
|
| 656 |
-
-
|
| 657 |
-
-
|
| 658 |
"""
|
| 659 |
needs_rebuild = force_recache
|
| 660 |
if not needs_rebuild:
|
|
@@ -688,18 +689,18 @@ def zero_codes_outside_activation_steps(
|
|
| 688 |
activation_steps_csr: sp.csr_matrix,
|
| 689 |
activation_steps_to_keep: List[int],
|
| 690 |
) -> sp.csr_matrix:
|
| 691 |
-
"""
|
| 692 |
|
| 693 |
-
|
| 694 |
----------
|
| 695 |
-
codes_csr : CSR
|
| 696 |
-
activation_steps_csr : CSR
|
| 697 |
-
activation_steps_to_keep :
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
-------
|
| 701 |
-
CSR
|
| 702 |
-
|
| 703 |
"""
|
| 704 |
if not activation_steps_to_keep:
|
| 705 |
return codes_csr
|
|
@@ -739,7 +740,7 @@ def zero_codes_outside_activation_steps(
|
|
| 739 |
def ensure_activation_cache(
|
| 740 |
dataset: str,
|
| 741 |
acts_cache_path: str,
|
| 742 |
-
|
| 743 |
min_distortion_level: int,
|
| 744 |
params: dict,
|
| 745 |
include_pristine_cache: Optional[bool] = None,
|
|
@@ -769,7 +770,7 @@ def ensure_activation_cache(
|
|
| 769 |
dataset=dataset,
|
| 770 |
cache_path=acts_cache_path,
|
| 771 |
checkpoint_path=params.get('SAE_CHECKPOINT_PATH'),
|
| 772 |
-
|
| 773 |
layer_num=params.get('LAYER_NUM'),
|
| 774 |
iqa_metric=params.get('IQA_METRIC'),
|
| 775 |
swin_num=params.get('SWIN_NUM'),
|
|
@@ -783,5 +784,6 @@ def ensure_activation_cache(
|
|
| 783 |
max_memory_gb=30.0,
|
| 784 |
add_patch_mask_stats=True,
|
| 785 |
include_pristine=needs_pristine_cache,
|
|
|
|
| 786 |
)
|
| 787 |
print('[run] Activation cache build completed.')
|
|
|
|
| 1 |
"""
|
| 2 |
+
Utilities for caching and loading SAE activations.
|
| 3 |
"""
|
| 4 |
|
| 5 |
from pathlib import Path
|
|
|
|
| 17 |
|
| 18 |
|
| 19 |
def _df_mb(df: pd.DataFrame) -> float:
|
| 20 |
+
"""DataFrame memory usage in MB."""
|
| 21 |
return df.memory_usage(deep=True).sum() / 1024 ** 2
|
| 22 |
|
| 23 |
|
| 24 |
def _sparse_mb(mat: sp.csr_matrix) -> float:
|
| 25 |
+
"""CSR matrix memory usage (data + indices) in MB."""
|
| 26 |
return (mat.data.nbytes + mat.indices.nbytes + mat.indptr.nbytes) / 1024 ** 2
|
| 27 |
|
| 28 |
|
| 29 |
def _cache_paths(base_path: str) -> Tuple[str, str, str]:
|
| 30 |
"""
|
| 31 |
+
Return paths to the three cache files derived from a base path.
|
| 32 |
|
| 33 |
+
Example:
|
| 34 |
'cache/kadid_acts.feather'
|
| 35 |
→ ('cache/kadid_acts_meta.feather', 'cache/kadid_acts_codes.npz',
|
| 36 |
'cache/kadid_acts_steps.npz')
|
|
|
|
| 273 |
sparse_mb = _sparse_mb(codes_csr)
|
| 274 |
steps_mb = _sparse_mb(steps_csr)
|
| 275 |
|
| 276 |
+
print(f'Activations: shape={codes_csr.shape}, {sparse_mb:.1f} MB (sparse)')
|
| 277 |
+
print(f'Activation steps: shape={steps_csr.shape}, {steps_mb:.1f} MB (sparse)')
|
| 278 |
|
| 279 |
meta_path, codes_path, steps_path = _cache_paths(output_path)
|
| 280 |
|
|
|
|
| 285 |
print(f'Saved metadata ({len(meta_df)} rows) -> {meta_path}')
|
| 286 |
print(f'Saved activations -> {codes_path}')
|
| 287 |
print(f'Saved activation steps -> {steps_path}')
|
| 288 |
+
print(f' Metadata: {_df_mb(meta_df):.1f} MB')
|
| 289 |
+
print(f' Activations: {sparse_mb:.1f} MB')
|
| 290 |
+
print(f' Steps: {steps_mb:.1f} MB')
|
| 291 |
|
| 292 |
if pristine_dataloader is not None:
|
| 293 |
print('\nProcessing pristine dataset...')
|
|
|
|
| 318 |
sp.save_npz(pristine_codes_path, pristine_codes)
|
| 319 |
sp.save_npz(pristine_steps_path, pristine_steps)
|
| 320 |
|
| 321 |
+
print(f'Pristine activations: shape={pristine_codes.shape}, {pristine_sparse_mb:.1f} MB')
|
| 322 |
+
print(f'Pristine steps: shape={pristine_steps.shape}, {pristine_steps_mb:.1f} MB')
|
| 323 |
|
| 324 |
print(f'Saved pristine metadata ({len(pristine_meta)} rows) -> {pristine_meta_path}')
|
| 325 |
print(f'Saved pristine activations -> {pristine_codes_path}')
|
|
|
|
| 333 |
dataset: str,
|
| 334 |
cache_path: str,
|
| 335 |
checkpoint_path: str,
|
| 336 |
+
dataset_root: str,
|
| 337 |
layer_num: int,
|
| 338 |
iqa_metric: str,
|
| 339 |
swin_num: int,
|
|
|
|
| 348 |
add_patch_mask_stats: bool = True,
|
| 349 |
include_pristine: bool = True,
|
| 350 |
show_progress_bars: bool = True,
|
| 351 |
+
srground_include_sr_artifact: bool = False,
|
| 352 |
) -> Dict[str, Any]:
|
| 353 |
"""Build activation cache end-to-end for KADID/local-KADID datasets."""
|
| 354 |
from .datasets import (
|
|
|
|
| 378 |
label_to_dist_group = None
|
| 379 |
|
| 380 |
if dataset == 'local_kadid':
|
| 381 |
+
data = LocalKadidPresavedDataset(root=dataset_root, crop_size=crop_size)
|
| 382 |
collate_fn = local_kadid_collate_fn
|
| 383 |
meta_keys = [
|
| 384 |
'dist_type',
|
|
|
|
| 393 |
'distorted_img_path',
|
| 394 |
'original_img_path',
|
| 395 |
]
|
| 396 |
+
pristine_data = LocalKadidPristineDataset(root=dataset_root, crop_size=crop_size) if include_pristine else None
|
| 397 |
pristine_collate = local_kadid_pristine_collate_fn
|
| 398 |
elif dataset in {'kadid10k', 'kadid'}:
|
| 399 |
data = Kadid10kDataset(
|
| 400 |
+
root=dataset_root,
|
| 401 |
crop_size=crop_size,
|
| 402 |
min_distortion_level=min_distortion_level or 1,
|
| 403 |
)
|
|
|
|
| 410 |
'distorted_img_path',
|
| 411 |
'original_img_path',
|
| 412 |
]
|
| 413 |
+
pristine_data = KadidPristineDataset(root=dataset_root, crop_size=crop_size) if include_pristine else None
|
| 414 |
pristine_collate = kadid_pristine_collate_fn
|
| 415 |
elif dataset == 'QGround':
|
| 416 |
data = QGroundDataset(
|
| 417 |
+
root=dataset_root,
|
| 418 |
split='test',
|
| 419 |
crop_size=crop_size,
|
| 420 |
)
|
|
|
|
| 439 |
pristine_collate = None
|
| 440 |
elif dataset == 'SRGround':
|
| 441 |
data = SRGroundSmallDataset(
|
| 442 |
+
root=dataset_root,
|
| 443 |
json_path='srground_train.json',
|
| 444 |
crop_size=crop_size,
|
| 445 |
+
include_sr_artifact=srground_include_sr_artifact,
|
| 446 |
)
|
| 447 |
collate_fn = srground_collate_fn
|
| 448 |
meta_keys = [
|
|
|
|
| 540 |
Tuple[pd.DataFrame, sp.csr_matrix],
|
| 541 |
Tuple[pd.DataFrame, sp.csr_matrix, sp.csr_matrix],
|
| 542 |
]:
|
| 543 |
+
"""Load SAE activation cache from separate files.
|
| 544 |
|
| 545 |
+
If ``return_activation_steps=True``, also returns a CSR matrix of activation
|
| 546 |
+
order, where ``0`` means no activation and ``k>0`` corresponds to pursuit step ``k``.
|
|
|
|
| 547 |
"""
|
| 548 |
meta_path, codes_path, steps_path = _cache_paths(path)
|
| 549 |
meta = pd.read_feather(meta_path)
|
|
|
|
| 551 |
|
| 552 |
print(f'Loaded from {meta_path}: {meta.shape[0]} rows × {meta.shape[1]} cols')
|
| 553 |
print(f'Loaded from {codes_path}: shape={codes.shape}, dtype={codes.dtype}')
|
| 554 |
+
print(f' Metadata: {_df_mb(meta):.1f} MB')
|
| 555 |
+
print(f' Activations: {_sparse_mb(codes):.1f} MB (sparse)')
|
| 556 |
|
| 557 |
keep_idx: Optional[np.ndarray] = None
|
| 558 |
if min_distortion_level is not None or max_distortion_level is not None:
|
|
|
|
| 570 |
keep_idx = np.flatnonzero(keep_mask.to_numpy())
|
| 571 |
else:
|
| 572 |
keep_mask = (meta['dist_level'] >= -1000)
|
| 573 |
+
keep_idx = np.flatnonzero(keep_mask.to_numpy()) # Temporary workaround -- fix later
|
| 574 |
|
| 575 |
if return_activation_steps:
|
| 576 |
if Path(steps_path).exists():
|
|
|
|
| 593 |
f'{meta.shape[0]} rows kept'
|
| 594 |
)
|
| 595 |
|
| 596 |
+
print(f' Steps: {_sparse_mb(steps):.1f} MB (sparse)')
|
| 597 |
return meta, codes, steps
|
| 598 |
|
| 599 |
if keep_idx is not None:
|
|
|
|
| 621 |
|
| 622 |
print(f'Loaded pristine from {meta_path}: {meta.shape[0]} rows × {meta.shape[1]} cols')
|
| 623 |
print(f'Loaded pristine from {codes_path}: shape={codes.shape}, dtype={codes.dtype}')
|
| 624 |
+
print(f' Metadata: {_df_mb(meta):.1f} MB')
|
| 625 |
+
print(f' Activations: {_sparse_mb(codes):.1f} MB (sparse)')
|
| 626 |
|
| 627 |
if return_activation_steps:
|
| 628 |
if Path(steps_path).exists():
|
|
|
|
| 636 |
print('No pristine steps cache found. Using all-zero activation steps.')
|
| 637 |
steps = sp.csr_matrix(codes.shape, dtype=np.int32)
|
| 638 |
|
| 639 |
+
print(f' Steps: {_sparse_mb(steps):.1f} MB (sparse)')
|
| 640 |
return meta, codes, steps
|
| 641 |
|
| 642 |
return meta, codes
|
|
|
|
| 650 |
load_cache_kwargs: Optional[Dict[str, Any]] = None,
|
| 651 |
build_cache_fn: Optional[Callable[[], None]] = None,
|
| 652 |
) -> None:
|
| 653 |
+
"""Check cache availability and rebuild it if needed.
|
| 654 |
|
| 655 |
+
Behavior:
|
| 656 |
+
- try loading the cache via ``load_cache``;
|
| 657 |
+
- if the cache is missing or ``force_recache=True``, start a rebuild;
|
| 658 |
+
- if building is disabled, raise ``FileNotFoundError``.
|
| 659 |
"""
|
| 660 |
needs_rebuild = force_recache
|
| 661 |
if not needs_rebuild:
|
|
|
|
| 689 |
activation_steps_csr: sp.csr_matrix,
|
| 690 |
activation_steps_to_keep: List[int],
|
| 691 |
) -> sp.csr_matrix:
|
| 692 |
+
"""Zero out activations whose appearance step is not in the allow-list.
|
| 693 |
|
| 694 |
+
Parameters
|
| 695 |
----------
|
| 696 |
+
codes_csr : CSR matrix of SAE activations.
|
| 697 |
+
activation_steps_csr : CSR matrix of activation steps (0 = not activated).
|
| 698 |
+
activation_steps_to_keep : list of steps to keep.
|
| 699 |
+
|
| 700 |
+
Returns
|
| 701 |
+
-------
|
| 702 |
+
CSR matrix of the same shape with values outside the specified steps zeroed.
|
| 703 |
+
If the step list is empty, returns the original matrix unchanged.
|
| 704 |
"""
|
| 705 |
if not activation_steps_to_keep:
|
| 706 |
return codes_csr
|
|
|
|
| 740 |
def ensure_activation_cache(
|
| 741 |
dataset: str,
|
| 742 |
acts_cache_path: str,
|
| 743 |
+
dataset_root: str,
|
| 744 |
min_distortion_level: int,
|
| 745 |
params: dict,
|
| 746 |
include_pristine_cache: Optional[bool] = None,
|
|
|
|
| 770 |
dataset=dataset,
|
| 771 |
cache_path=acts_cache_path,
|
| 772 |
checkpoint_path=params.get('SAE_CHECKPOINT_PATH'),
|
| 773 |
+
dataset_root=dataset_root,
|
| 774 |
layer_num=params.get('LAYER_NUM'),
|
| 775 |
iqa_metric=params.get('IQA_METRIC'),
|
| 776 |
swin_num=params.get('SWIN_NUM'),
|
|
|
|
| 784 |
max_memory_gb=30.0,
|
| 785 |
add_patch_mask_stats=True,
|
| 786 |
include_pristine=needs_pristine_cache,
|
| 787 |
+
srground_include_sr_artifact=bool(params.get('SRGROUND_INCLUDE_SR_ARTIFACT', False)),
|
| 788 |
)
|
| 789 |
print('[run] Activation cache build completed.')
|
analysis/config.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
"""
|
| 4 |
|
| 5 |
from __future__ import annotations
|
|
@@ -12,14 +12,10 @@ from typing import Dict, List, Mapping, Sequence, Tuple
|
|
| 12 |
DEFAULT_SAE_VIS_CONFIG_PATH = os.path.join(
|
| 13 |
os.path.dirname(__file__), '..', 'default_configs', 'sae_vis_config.json'
|
| 14 |
)
|
|
|
|
| 15 |
SUPPORTED_DATASETS: Tuple[str, ...] = ('kadid10k', 'local_kadid', 'QGround', 'SRGround')
|
| 16 |
|
| 17 |
-
_DATASET_IMAGE_SUBDIRS = {
|
| 18 |
-
'kadid10k': 'kadid10k',
|
| 19 |
-
'local_kadid': 'local_kadid',
|
| 20 |
-
'QGround': 'QGround',
|
| 21 |
-
'SRGround': 'SRGround',
|
| 22 |
-
}
|
| 23 |
|
| 24 |
|
| 25 |
def dataset_images_root(datasets_root: str, dataset: str) -> str:
|
|
@@ -74,6 +70,7 @@ class SaeVisConfig:
|
|
| 74 |
DOWNSCALE_FACTOR: int
|
| 75 |
KADID_MIN_DISTORTION_LEVEL: int
|
| 76 |
KADID_MAX_DISTORTION_LEVEL: int
|
|
|
|
| 77 |
SCALING_FACTOR: float
|
| 78 |
BATCH_SIZE: int
|
| 79 |
NUM_WORKERS: int
|
|
@@ -116,6 +113,7 @@ class SaeVisConfig:
|
|
| 116 |
'CROP_SIZE': self.CROP_SIZE,
|
| 117 |
'SCALING_FACTOR': self.SCALING_FACTOR,
|
| 118 |
'KADID_MAX_DISTORTION_LEVEL': self.KADID_MAX_DISTORTION_LEVEL,
|
|
|
|
| 119 |
}
|
| 120 |
|
| 121 |
|
|
@@ -256,10 +254,8 @@ def load_sae_vis_config(path: str | None = None) -> SaeVisConfig:
|
|
| 256 |
raise ValueError('ACTIVATION_STEPS_TO_KEEP must contain only positive integers')
|
| 257 |
|
| 258 |
dataset = str(_cfg_get(vis_cfg, 'DATASET', 'kadid10k')).strip()
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
if dataset not in supported_datasets:
|
| 262 |
-
raise ValueError(f'Unsupported DATASET={dataset!r}; expected one of {supported_datasets}')
|
| 263 |
|
| 264 |
cache_dir = str(
|
| 265 |
_cfg_get(
|
|
@@ -268,7 +264,7 @@ def load_sae_vis_config(path: str | None = None) -> SaeVisConfig:
|
|
| 268 |
os.path.join(os.path.dirname(os.path.dirname(sae_checkpoint_path)), 'cache/'),
|
| 269 |
)
|
| 270 |
)
|
| 271 |
-
dataset_cache_configs = build_dataset_cache_paths(cache_dir,
|
| 272 |
|
| 273 |
raw_selector_configs = _cfg_get(vis_cfg, 'SELECTOR_CONFIGS', [])
|
| 274 |
if raw_selector_configs is None:
|
|
@@ -301,6 +297,7 @@ def load_sae_vis_config(path: str | None = None) -> SaeVisConfig:
|
|
| 301 |
DOWNSCALE_FACTOR=int(_cfg_get(vis_cfg, 'DOWNSCALE_FACTOR', 2)),
|
| 302 |
KADID_MIN_DISTORTION_LEVEL=kadid_min_distortion_level,
|
| 303 |
KADID_MAX_DISTORTION_LEVEL=kadid_max_distortion_level,
|
|
|
|
| 304 |
SCALING_FACTOR=float(_cfg_get(sae_cfg, 'scaling_factor', 1.0)),
|
| 305 |
BATCH_SIZE=int(_cfg_get(vis_cfg, 'BATCH_SIZE', 32)),
|
| 306 |
NUM_WORKERS=int(_cfg_get(vis_cfg, 'NUM_WORKERS', 4)),
|
|
@@ -324,7 +321,7 @@ def load_sae_vis_config(path: str | None = None) -> SaeVisConfig:
|
|
| 324 |
SHOW_PROGRESS_BARS=bool(_cfg_get(vis_cfg, 'SHOW_PROGRESS_BARS', True)),
|
| 325 |
CACHE_DIR=cache_dir,
|
| 326 |
DATASET=dataset,
|
| 327 |
-
SUPPORTED_DATASETS=
|
| 328 |
DATASET_CACHE_CONFIGS=dataset_cache_configs,
|
| 329 |
CACHE_PATHS=dataset_cache_configs[dataset],
|
| 330 |
KADID_IMAGES_PATH=os.path.join(datasets_root, dataset_images_subdir),
|
|
|
|
| 1 |
"""
|
| 2 |
+
SAE visualization configuration.
|
| 3 |
"""
|
| 4 |
|
| 5 |
from __future__ import annotations
|
|
|
|
| 12 |
DEFAULT_SAE_VIS_CONFIG_PATH = os.path.join(
|
| 13 |
os.path.dirname(__file__), '..', 'default_configs', 'sae_vis_config.json'
|
| 14 |
)
|
| 15 |
+
|
| 16 |
SUPPORTED_DATASETS: Tuple[str, ...] = ('kadid10k', 'local_kadid', 'QGround', 'SRGround')
|
| 17 |
|
| 18 |
+
_DATASET_IMAGE_SUBDIRS = {dataset: dataset for dataset in SUPPORTED_DATASETS}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
def dataset_images_root(datasets_root: str, dataset: str) -> str:
|
|
|
|
| 70 |
DOWNSCALE_FACTOR: int
|
| 71 |
KADID_MIN_DISTORTION_LEVEL: int
|
| 72 |
KADID_MAX_DISTORTION_LEVEL: int
|
| 73 |
+
SRGROUND_INCLUDE_SR_ARTIFACT: bool
|
| 74 |
SCALING_FACTOR: float
|
| 75 |
BATCH_SIZE: int
|
| 76 |
NUM_WORKERS: int
|
|
|
|
| 113 |
'CROP_SIZE': self.CROP_SIZE,
|
| 114 |
'SCALING_FACTOR': self.SCALING_FACTOR,
|
| 115 |
'KADID_MAX_DISTORTION_LEVEL': self.KADID_MAX_DISTORTION_LEVEL,
|
| 116 |
+
'SRGROUND_INCLUDE_SR_ARTIFACT': self.SRGROUND_INCLUDE_SR_ARTIFACT,
|
| 117 |
}
|
| 118 |
|
| 119 |
|
|
|
|
| 254 |
raise ValueError('ACTIVATION_STEPS_TO_KEEP must contain only positive integers')
|
| 255 |
|
| 256 |
dataset = str(_cfg_get(vis_cfg, 'DATASET', 'kadid10k')).strip()
|
| 257 |
+
if dataset not in SUPPORTED_DATASETS:
|
| 258 |
+
raise ValueError(f'Unsupported DATASET={dataset!r}; expected one of {SUPPORTED_DATASETS}')
|
|
|
|
|
|
|
| 259 |
|
| 260 |
cache_dir = str(
|
| 261 |
_cfg_get(
|
|
|
|
| 264 |
os.path.join(os.path.dirname(os.path.dirname(sae_checkpoint_path)), 'cache/'),
|
| 265 |
)
|
| 266 |
)
|
| 267 |
+
dataset_cache_configs = build_dataset_cache_paths(cache_dir, SUPPORTED_DATASETS)
|
| 268 |
|
| 269 |
raw_selector_configs = _cfg_get(vis_cfg, 'SELECTOR_CONFIGS', [])
|
| 270 |
if raw_selector_configs is None:
|
|
|
|
| 297 |
DOWNSCALE_FACTOR=int(_cfg_get(vis_cfg, 'DOWNSCALE_FACTOR', 2)),
|
| 298 |
KADID_MIN_DISTORTION_LEVEL=kadid_min_distortion_level,
|
| 299 |
KADID_MAX_DISTORTION_LEVEL=kadid_max_distortion_level,
|
| 300 |
+
SRGROUND_INCLUDE_SR_ARTIFACT=bool(_cfg_get(vis_cfg, 'SRGROUND_INCLUDE_SR_ARTIFACT', False)),
|
| 301 |
SCALING_FACTOR=float(_cfg_get(sae_cfg, 'scaling_factor', 1.0)),
|
| 302 |
BATCH_SIZE=int(_cfg_get(vis_cfg, 'BATCH_SIZE', 32)),
|
| 303 |
NUM_WORKERS=int(_cfg_get(vis_cfg, 'NUM_WORKERS', 4)),
|
|
|
|
| 321 |
SHOW_PROGRESS_BARS=bool(_cfg_get(vis_cfg, 'SHOW_PROGRESS_BARS', True)),
|
| 322 |
CACHE_DIR=cache_dir,
|
| 323 |
DATASET=dataset,
|
| 324 |
+
SUPPORTED_DATASETS=SUPPORTED_DATASETS,
|
| 325 |
DATASET_CACHE_CONFIGS=dataset_cache_configs,
|
| 326 |
CACHE_PATHS=dataset_cache_configs[dataset],
|
| 327 |
KADID_IMAGES_PATH=os.path.join(datasets_root, dataset_images_subdir),
|
analysis/datasets.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
"""
|
| 4 |
|
| 5 |
import gzip
|
|
@@ -186,7 +186,20 @@ def _sr_artifact_labels(
|
|
| 186 |
return np.where(sr_maps * sr_prom >= threshold, 6, 0).astype(np.uint8)
|
| 187 |
|
| 188 |
|
| 189 |
-
def merge_srground_masks(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
return np.where(annot_sr == 6, 6, annot_rd).astype(np.uint8)
|
| 191 |
|
| 192 |
|
|
@@ -319,55 +332,114 @@ def srground_rows_for_image_paths(
|
|
| 319 |
def srground_prominences_by_image_paths(
|
| 320 |
image_paths: Sequence[str],
|
| 321 |
*,
|
|
|
|
| 322 |
datasets_root: str | None = None,
|
| 323 |
) -> dict[str, np.ndarray]:
|
| 324 |
"""Look up prominences for paths (absolute or SRGround-relative) via cached index."""
|
| 325 |
if not image_paths:
|
| 326 |
return {}
|
| 327 |
|
|
|
|
|
|
|
| 328 |
root = _resolve_datasets_root(datasets_root)
|
| 329 |
index = srground_prominences_index(root)
|
| 330 |
keys = {srground_image_key(path, datasets_root=root) for path in image_paths if path}
|
| 331 |
return {key: index[key] for key in keys if key in index}
|
| 332 |
|
| 333 |
|
| 334 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
meta_row: pd.Series | None,
|
| 336 |
*,
|
| 337 |
-
|
| 338 |
-
include_sr_artifact: bool = True,
|
| 339 |
-
crop_size: int = 224,
|
| 340 |
-
sr_artifact_threshold: float = SRGROUND_SR_ARTIFACT_THRESHOLD,
|
| 341 |
-
prominences: np.ndarray | None = None,
|
| 342 |
) -> np.ndarray | None:
|
| 343 |
-
"""
|
| 344 |
if meta_row is None:
|
| 345 |
return None
|
| 346 |
|
| 347 |
-
|
|
|
|
|
|
|
| 348 |
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
|
| 358 |
image_rel = _image_rel_from_meta_row(meta_row)
|
| 359 |
-
real_rel =
|
| 360 |
-
sr_rel =
|
| 361 |
-
if not real_rel and not
|
| 362 |
return None
|
| 363 |
|
| 364 |
if prominences is None and image_rel:
|
| 365 |
-
prominences = srground_prominences_by_image_paths(
|
|
|
|
|
|
|
|
|
|
| 366 |
|
| 367 |
annot_rd = None
|
| 368 |
annot_sr = None
|
| 369 |
if real_rel:
|
| 370 |
-
real_path =
|
| 371 |
if real_path.is_file():
|
| 372 |
real_maps = _load_npy_gz(real_path)
|
| 373 |
prom = prominences
|
|
@@ -375,8 +447,8 @@ def srground_mask_rgb_for_meta_row(
|
|
| 375 |
prom = np.ones(int(real_maps.shape[0]) + 1, dtype=np.float64)
|
| 376 |
annot_rd = _real_distortion_labels(real_maps, prom)
|
| 377 |
|
| 378 |
-
if
|
| 379 |
-
sr_path =
|
| 380 |
if sr_path.is_file():
|
| 381 |
sr_maps = _load_npy_gz(sr_path)
|
| 382 |
prom = prominences
|
|
@@ -391,25 +463,97 @@ def srground_mask_rgb_for_meta_row(
|
|
| 391 |
if annot_rd is None and annot_sr is None:
|
| 392 |
return None
|
| 393 |
|
| 394 |
-
if annot_rd is None
|
| 395 |
-
annot_rd
|
| 396 |
-
|
| 397 |
-
merged = merge_srground_masks(annot_rd, annot_sr)
|
| 398 |
-
else:
|
| 399 |
-
merged = annot_rd.astype(np.uint8, copy=False)
|
| 400 |
-
|
| 401 |
if image_rel:
|
| 402 |
-
image_path =
|
| 403 |
if image_path.is_file():
|
| 404 |
with Image.open(image_path) as image:
|
| 405 |
reference_size = image.size
|
| 406 |
-
merged = _center_crop_label_map(merged, crop_size, reference_size)
|
| 407 |
-
else:
|
| 408 |
-
merged = _center_crop_label_map(merged, crop_size, (merged.shape[1], merged.shape[0]))
|
| 409 |
-
else:
|
| 410 |
-
merged = _center_crop_label_map(merged, crop_size, (merged.shape[1], merged.shape[0]))
|
| 411 |
|
| 412 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
|
| 414 |
|
| 415 |
def _rgb2label_qground(mask_rgb: np.ndarray) -> np.ndarray:
|
|
@@ -443,7 +587,7 @@ def _to_relative_dataset_path(path: Path, root: Path) -> str:
|
|
| 443 |
|
| 444 |
class Kadid10kDataset(Dataset):
|
| 445 |
"""
|
| 446 |
-
KADID-10k dataset.
|
| 447 |
"""
|
| 448 |
|
| 449 |
def __init__(
|
|
@@ -595,11 +739,11 @@ class LocalKadidPresavedDataset(Dataset):
|
|
| 595 |
|
| 596 |
def kadid_collate_fn(batch: List[dict]) -> dict:
|
| 597 |
"""
|
| 598 |
-
Collate
|
| 599 |
|
| 600 |
-
|
| 601 |
images: Tensor (B, C, H, W)
|
| 602 |
-
+
|
| 603 |
"""
|
| 604 |
images = torch.stack([item["img"] for item in batch], dim=0)
|
| 605 |
collated: dict = {"images": images}
|
|
@@ -799,10 +943,12 @@ class SRGroundSmallDataset(Dataset):
|
|
| 799 |
allowed_methods: Optional[List[str]] = ['DiT4SR_x2'],
|
| 800 |
crop_size: Optional[int] = None,
|
| 801 |
sr_artifact_threshold: float = SRGROUND_SR_ARTIFACT_THRESHOLD,
|
|
|
|
| 802 |
transform=None,
|
| 803 |
):
|
| 804 |
self.root = Path(root)
|
| 805 |
self.sr_artifact_threshold = float(sr_artifact_threshold)
|
|
|
|
| 806 |
self.json_path = self.root / 'srground_train.json'
|
| 807 |
df = pd.read_json(self.json_path)
|
| 808 |
|
|
@@ -845,10 +991,15 @@ class SRGroundSmallDataset(Dataset):
|
|
| 845 |
if real_ann_path.exists():
|
| 846 |
annot_rd = _real_distortion_labels(_load_npy_gz(real_ann_path), prominences)
|
| 847 |
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 852 |
|
| 853 |
if annot_rd is None and annot_sr is None:
|
| 854 |
return None, None
|
|
@@ -883,15 +1034,31 @@ class SRGroundSmallDataset(Dataset):
|
|
| 883 |
annot_sr = np.asarray(self.crop(mask_image), dtype=np.uint8)
|
| 884 |
|
| 885 |
img_tensor = self.image_to_tensor(image)
|
|
|
|
| 886 |
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 891 |
mask_coverage = float((mask > 0).float().mean().item())
|
| 892 |
|
| 893 |
real_ann_path = self._resolve_path(sample.get('real_distortions_ann_path'))
|
| 894 |
-
sr_ann_path =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 895 |
|
| 896 |
return {
|
| 897 |
'img': img_tensor,
|
|
@@ -903,7 +1070,9 @@ class SRGroundSmallDataset(Dataset):
|
|
| 903 |
'mask_coverage': mask_coverage,
|
| 904 |
'prominences': sample.get('prominences'),
|
| 905 |
'has_markup': sample.get('has_markup', False),
|
| 906 |
-
'sr_artifacts_ann_path':
|
|
|
|
|
|
|
| 907 |
'real_distortions_ann_path': _to_relative_dataset_path(real_ann_path, self.root),
|
| 908 |
'sample_id': sample.get('sample_id'),
|
| 909 |
'distorted_img_path': _to_relative_dataset_path(img_path, self.root),
|
|
@@ -936,8 +1105,8 @@ def srground_collate_fn(batch: List[dict]) -> dict:
|
|
| 936 |
|
| 937 |
class KadidPristineDataset(Dataset):
|
| 938 |
"""
|
| 939 |
-
KADID-10k pristine (reference) images dataset.
|
| 940 |
-
|
| 941 |
"""
|
| 942 |
|
| 943 |
def __init__(
|
|
@@ -957,7 +1126,7 @@ class KadidPristineDataset(Dataset):
|
|
| 957 |
else:
|
| 958 |
self.transform = transform
|
| 959 |
|
| 960 |
-
#
|
| 961 |
images_dir = self.root / "images"
|
| 962 |
pristine_pattern = re.compile(r'^I\d+\.png$')
|
| 963 |
|
|
@@ -983,13 +1152,13 @@ class KadidPristineDataset(Dataset):
|
|
| 983 |
|
| 984 |
return {
|
| 985 |
"img": img,
|
| 986 |
-
"mos": 5.0, #
|
| 987 |
"dist_type": "pristine",
|
| 988 |
"dist_group": "pristine",
|
| 989 |
-
"dist_level": 0, #
|
| 990 |
"distorted_img_path": img_rel_path,
|
| 991 |
-
"original_img_path": img_rel_path, #
|
| 992 |
-
"sample_id": img_path.stem, #
|
| 993 |
}
|
| 994 |
|
| 995 |
|
|
@@ -997,9 +1166,9 @@ class LocalKadidPristineDataset(Dataset):
|
|
| 997 |
"""
|
| 998 |
Pristine KADID dataset.
|
| 999 |
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
"""
|
| 1004 |
|
| 1005 |
def __init__(
|
|
@@ -1058,11 +1227,11 @@ class LocalKadidPristineDataset(Dataset):
|
|
| 1058 |
|
| 1059 |
def kadid_pristine_collate_fn(batch: List[dict]) -> dict:
|
| 1060 |
"""
|
| 1061 |
-
Collate
|
| 1062 |
|
| 1063 |
-
|
| 1064 |
images: Tensor (B, C, H, W)
|
| 1065 |
-
+
|
| 1066 |
"""
|
| 1067 |
images = torch.stack([item["img"] for item in batch], dim=0)
|
| 1068 |
collated: dict = {"images": images}
|
|
@@ -1077,18 +1246,18 @@ def kadid_pristine_collate_fn(batch: List[dict]) -> dict:
|
|
| 1077 |
|
| 1078 |
def local_kadid_pristine_collate_fn(batch: List[dict]) -> dict:
|
| 1079 |
"""
|
| 1080 |
-
Collate
|
| 1081 |
|
| 1082 |
-
|
| 1083 |
images: Tensor (B, C, H, W)
|
| 1084 |
-
masks: Tensor (B, 1, H, W)
|
| 1085 |
-
+
|
| 1086 |
"""
|
| 1087 |
images = torch.stack([item["img"] for item in batch], dim=0)
|
| 1088 |
|
| 1089 |
collated: dict = {"images": images}
|
| 1090 |
|
| 1091 |
-
#
|
| 1092 |
if "mask" in batch[0]:
|
| 1093 |
masks = torch.stack([item["mask"] for item in batch], dim=0)
|
| 1094 |
collated["masks"] = masks
|
|
@@ -1116,33 +1285,40 @@ def resolve_dataset_image_path(
|
|
| 1116 |
)
|
| 1117 |
dataset_root = Path(dataset_images_root(str(root), dataset))
|
| 1118 |
path = Path(path_from_meta)
|
| 1119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1120 |
|
| 1121 |
|
| 1122 |
def dataset_image_paths(
|
| 1123 |
dataset: str,
|
| 1124 |
-
|
| 1125 |
crop_size: int,
|
| 1126 |
min_distortion_level: int,
|
| 1127 |
) -> List[str]:
|
| 1128 |
if dataset == 'kadid10k':
|
| 1129 |
ds = Kadid10kDataset(
|
| 1130 |
-
|
| 1131 |
crop_size=crop_size,
|
| 1132 |
min_distortion_level=min_distortion_level,
|
| 1133 |
)
|
| 1134 |
return [_to_relative_dataset_path(Path(p), ds.root) for p in ds.images]
|
| 1135 |
|
| 1136 |
if dataset == 'local_kadid':
|
| 1137 |
-
ds = LocalKadidPresavedDataset(
|
| 1138 |
return [_to_relative_dataset_path(Path(str(p)), ds.root) for p in ds.df['distorted_img_path'].tolist()]
|
| 1139 |
|
| 1140 |
if dataset == 'QGround':
|
| 1141 |
-
ds = QGroundDataset(
|
| 1142 |
return [str(sample['image_rel']) for sample in ds.samples]
|
| 1143 |
|
| 1144 |
if dataset == 'SRGround':
|
| 1145 |
-
ds = SRGroundSmallDataset(
|
| 1146 |
return [_to_relative_dataset_path(Path(str(p)), ds.root) for p in ds.df['image_path'].tolist()]
|
| 1147 |
|
| 1148 |
raise ValueError(f'Unsupported dataset: {dataset}')
|
|
|
|
| 1 |
"""
|
| 2 |
+
Datasets and helper data structures for KADID-10k. Taken from the PatchSAE repository.
|
| 3 |
"""
|
| 4 |
|
| 5 |
import gzip
|
|
|
|
| 186 |
return np.where(sr_maps * sr_prom >= threshold, 6, 0).astype(np.uint8)
|
| 187 |
|
| 188 |
|
| 189 |
+
def merge_srground_masks(
|
| 190 |
+
annot_rd: np.ndarray | None,
|
| 191 |
+
annot_sr: np.ndarray | None,
|
| 192 |
+
) -> np.ndarray:
|
| 193 |
+
"""Merge real-distortion and SR-artifact label maps (0=background, 1..6=classes).
|
| 194 |
+
|
| 195 |
+
If ``annot_sr`` is None, returns ``annot_rd`` unchanged (no SR merge).
|
| 196 |
+
"""
|
| 197 |
+
if annot_sr is None:
|
| 198 |
+
if annot_rd is None:
|
| 199 |
+
raise ValueError('merge_srground_masks requires annot_rd when annot_sr is None')
|
| 200 |
+
return annot_rd.astype(np.uint8, copy=False)
|
| 201 |
+
if annot_rd is None:
|
| 202 |
+
return annot_sr.astype(np.uint8, copy=False)
|
| 203 |
return np.where(annot_sr == 6, 6, annot_rd).astype(np.uint8)
|
| 204 |
|
| 205 |
|
|
|
|
| 332 |
def srground_prominences_by_image_paths(
|
| 333 |
image_paths: Sequence[str],
|
| 334 |
*,
|
| 335 |
+
dataset_root: str | Path | None = None,
|
| 336 |
datasets_root: str | None = None,
|
| 337 |
) -> dict[str, np.ndarray]:
|
| 338 |
"""Look up prominences for paths (absolute or SRGround-relative) via cached index."""
|
| 339 |
if not image_paths:
|
| 340 |
return {}
|
| 341 |
|
| 342 |
+
if datasets_root is None and dataset_root is not None:
|
| 343 |
+
datasets_root = parent_datasets_root(dataset_root)
|
| 344 |
root = _resolve_datasets_root(datasets_root)
|
| 345 |
index = srground_prominences_index(root)
|
| 346 |
keys = {srground_image_key(path, datasets_root=root) for path in image_paths if path}
|
| 347 |
return {key: index[key] for key in keys if key in index}
|
| 348 |
|
| 349 |
|
| 350 |
+
def _meta_row_path(meta_row: pd.Series, column: str) -> str | None:
|
| 351 |
+
if column not in meta_row:
|
| 352 |
+
return None
|
| 353 |
+
value = meta_row.get(column)
|
| 354 |
+
if value is None or (isinstance(value, float) and np.isnan(value)):
|
| 355 |
+
return None
|
| 356 |
+
text = str(value).strip()
|
| 357 |
+
return text or None
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def parent_datasets_root(dataset_root: str | Path) -> str:
|
| 361 |
+
"""Parent directory that contains dataset folders (e.g. ``Kadid`` for ``Kadid/SRGround``)."""
|
| 362 |
+
return str(Path(dataset_root).resolve().parent)
|
| 363 |
+
|
| 364 |
+
|
| 365 |
+
def _resolve_dataset_file_path(rel_path: str, *, dataset_root: str | Path) -> Path:
|
| 366 |
+
path = Path(str(rel_path).strip())
|
| 367 |
+
if path.is_absolute():
|
| 368 |
+
return path
|
| 369 |
+
return Path(dataset_root) / path
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def infer_spatial_mask_dataset(meta_row: pd.Series | None) -> str | None:
|
| 373 |
+
"""Return ``QGround`` or ``SRGround`` when meta row carries spatial mask fields."""
|
| 374 |
+
if meta_row is None:
|
| 375 |
+
return None
|
| 376 |
+
if _meta_row_path(meta_row, 'real_distortions_ann_path') or _meta_row_path(meta_row, 'sr_artifacts_ann_path'):
|
| 377 |
+
return 'SRGround'
|
| 378 |
+
if _meta_row_path(meta_row, 'mask_path'):
|
| 379 |
+
return 'QGround'
|
| 380 |
+
return None
|
| 381 |
+
|
| 382 |
+
|
| 383 |
+
def qground_mask_rgb_for_meta_row(
|
| 384 |
meta_row: pd.Series | None,
|
| 385 |
*,
|
| 386 |
+
dataset_root: str | Path,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
) -> np.ndarray | None:
|
| 388 |
+
"""Load QGround RGB segmentation mask PNG referenced from cache metadata."""
|
| 389 |
if meta_row is None:
|
| 390 |
return None
|
| 391 |
|
| 392 |
+
mask_rel = _meta_row_path(meta_row, 'mask_path')
|
| 393 |
+
if not mask_rel:
|
| 394 |
+
return None
|
| 395 |
|
| 396 |
+
mask_path = _resolve_dataset_file_path(mask_rel, dataset_root=dataset_root)
|
| 397 |
+
if not mask_path.is_file():
|
| 398 |
+
return None
|
| 399 |
+
return np.asarray(Image.open(mask_path).convert('RGB'), dtype=np.uint8)
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def _resize_label_map_to_image(
|
| 403 |
+
mask_label: np.ndarray | None,
|
| 404 |
+
reference_size: tuple[int, int],
|
| 405 |
+
) -> np.ndarray | None:
|
| 406 |
+
if mask_label is None:
|
| 407 |
+
return None
|
| 408 |
+
width, height = reference_size
|
| 409 |
+
if mask_label.shape[:2] == (height, width):
|
| 410 |
+
return mask_label.astype(np.uint8, copy=False)
|
| 411 |
+
mask_image = Image.fromarray(mask_label.astype(np.uint8), mode='L')
|
| 412 |
+
mask_image = mask_image.resize((width, height), resample=Image.NEAREST)
|
| 413 |
+
return np.asarray(mask_image, dtype=np.uint8)
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def srground_label_map_for_meta_row(
|
| 417 |
+
meta_row: pd.Series | None,
|
| 418 |
+
*,
|
| 419 |
+
dataset_root: str | Path,
|
| 420 |
+
sr_artifact_threshold: float = SRGROUND_SR_ARTIFACT_THRESHOLD,
|
| 421 |
+
prominences: np.ndarray | None = None,
|
| 422 |
+
) -> np.ndarray | None:
|
| 423 |
+
"""Build a full-frame SRGround label map from paths stored in cache metadata."""
|
| 424 |
+
if meta_row is None:
|
| 425 |
+
return None
|
| 426 |
|
| 427 |
image_rel = _image_rel_from_meta_row(meta_row)
|
| 428 |
+
real_rel = _meta_row_path(meta_row, 'real_distortions_ann_path')
|
| 429 |
+
sr_rel = _meta_row_path(meta_row, 'sr_artifacts_ann_path')
|
| 430 |
+
if not real_rel and not sr_rel:
|
| 431 |
return None
|
| 432 |
|
| 433 |
if prominences is None and image_rel:
|
| 434 |
+
prominences = srground_prominences_by_image_paths(
|
| 435 |
+
[image_rel],
|
| 436 |
+
dataset_root=dataset_root,
|
| 437 |
+
).get(image_rel)
|
| 438 |
|
| 439 |
annot_rd = None
|
| 440 |
annot_sr = None
|
| 441 |
if real_rel:
|
| 442 |
+
real_path = _resolve_dataset_file_path(real_rel, dataset_root=dataset_root)
|
| 443 |
if real_path.is_file():
|
| 444 |
real_maps = _load_npy_gz(real_path)
|
| 445 |
prom = prominences
|
|
|
|
| 447 |
prom = np.ones(int(real_maps.shape[0]) + 1, dtype=np.float64)
|
| 448 |
annot_rd = _real_distortion_labels(real_maps, prom)
|
| 449 |
|
| 450 |
+
if sr_rel:
|
| 451 |
+
sr_path = _resolve_dataset_file_path(sr_rel, dataset_root=dataset_root)
|
| 452 |
if sr_path.is_file():
|
| 453 |
sr_maps = _load_npy_gz(sr_path)
|
| 454 |
prom = prominences
|
|
|
|
| 463 |
if annot_rd is None and annot_sr is None:
|
| 464 |
return None
|
| 465 |
|
| 466 |
+
reference_size = (annot_rd if annot_rd is not None else annot_sr).shape[1], (
|
| 467 |
+
annot_rd if annot_rd is not None else annot_sr
|
| 468 |
+
).shape[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
if image_rel:
|
| 470 |
+
image_path = _resolve_dataset_file_path(image_rel, dataset_root=dataset_root)
|
| 471 |
if image_path.is_file():
|
| 472 |
with Image.open(image_path) as image:
|
| 473 |
reference_size = image.size
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
|
| 475 |
+
annot_rd = _resize_label_map_to_image(annot_rd, reference_size)
|
| 476 |
+
annot_sr = _resize_label_map_to_image(annot_sr, reference_size)
|
| 477 |
+
return merge_srground_masks(annot_rd, annot_sr)
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
def annotation_mask_rgb_for_meta_row(
|
| 481 |
+
meta_row: pd.Series | None,
|
| 482 |
+
*,
|
| 483 |
+
dataset_root: str | Path,
|
| 484 |
+
dataset: str | None = None,
|
| 485 |
+
crop_size: int = 512,
|
| 486 |
+
full_frame: bool = True,
|
| 487 |
+
sr_artifact_threshold: float = SRGROUND_SR_ARTIFACT_THRESHOLD,
|
| 488 |
+
prominences: np.ndarray | None = None,
|
| 489 |
+
) -> np.ndarray | None:
|
| 490 |
+
"""RGB annotation mask for QGround / SRGround visualization (QGround-style full frame by default)."""
|
| 491 |
+
|
| 492 |
+
if dataset_root is None:
|
| 493 |
+
from analysis.config import load_sae_vis_config
|
| 494 |
+
cfg = load_sae_vis_config()
|
| 495 |
+
datasets_root = str(cfg.DATASETS_ROOT)
|
| 496 |
+
dataset_root = Path(datasets_root) / dataset
|
| 497 |
+
|
| 498 |
+
dataset_name = dataset or infer_spatial_mask_dataset(meta_row)
|
| 499 |
+
if dataset_name == 'QGround':
|
| 500 |
+
return qground_mask_rgb_for_meta_row(meta_row, dataset_root=dataset_root)
|
| 501 |
+
if dataset_name == 'SRGround':
|
| 502 |
+
label_map = srground_label_map_for_meta_row(
|
| 503 |
+
meta_row,
|
| 504 |
+
dataset_root=dataset_root,
|
| 505 |
+
sr_artifact_threshold=sr_artifact_threshold,
|
| 506 |
+
prominences=prominences,
|
| 507 |
+
)
|
| 508 |
+
if label_map is None:
|
| 509 |
+
return None
|
| 510 |
+
if not full_frame:
|
| 511 |
+
image_rel = _image_rel_from_meta_row(meta_row)
|
| 512 |
+
reference_size = (label_map.shape[1], label_map.shape[0])
|
| 513 |
+
if image_rel:
|
| 514 |
+
image_path = _resolve_dataset_file_path(image_rel, dataset_root=dataset_root)
|
| 515 |
+
if image_path.is_file():
|
| 516 |
+
with Image.open(image_path) as image:
|
| 517 |
+
reference_size = image.size
|
| 518 |
+
label_map = _center_crop_label_map(label_map, crop_size, reference_size)
|
| 519 |
+
return label2rgb_srground(label_map)
|
| 520 |
+
return None
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
def get_srground_rgb_mask(
|
| 524 |
+
meta_row: pd.Series | None,
|
| 525 |
+
*,
|
| 526 |
+
dataset_root: str | Path,
|
| 527 |
+
crop_size: int = 224,
|
| 528 |
+
full_frame: bool = True,
|
| 529 |
+
sr_artifact_threshold: float = SRGROUND_SR_ARTIFACT_THRESHOLD,
|
| 530 |
+
) -> np.ndarray | None:
|
| 531 |
+
"""RGB SRGround mask for one cache meta row (prominences from ``srground_train.json``)."""
|
| 532 |
+
return annotation_mask_rgb_for_meta_row(
|
| 533 |
+
meta_row,
|
| 534 |
+
dataset_root=dataset_root,
|
| 535 |
+
dataset='SRGround',
|
| 536 |
+
crop_size=crop_size,
|
| 537 |
+
full_frame=full_frame,
|
| 538 |
+
sr_artifact_threshold=sr_artifact_threshold,
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
|
| 542 |
+
def srground_mask_rgb_for_meta_row(
|
| 543 |
+
meta_row: pd.Series | None,
|
| 544 |
+
*,
|
| 545 |
+
dataset_root: str | Path,
|
| 546 |
+
crop_size: int = 224,
|
| 547 |
+
sr_artifact_threshold: float = SRGROUND_SR_ARTIFACT_THRESHOLD,
|
| 548 |
+
) -> np.ndarray | None:
|
| 549 |
+
"""Center-cropped SRGround mask preview (alias of :func:`get_srground_rgb_mask`)."""
|
| 550 |
+
return get_srground_rgb_mask(
|
| 551 |
+
meta_row,
|
| 552 |
+
dataset_root=dataset_root,
|
| 553 |
+
crop_size=crop_size,
|
| 554 |
+
full_frame=False,
|
| 555 |
+
sr_artifact_threshold=sr_artifact_threshold,
|
| 556 |
+
)
|
| 557 |
|
| 558 |
|
| 559 |
def _rgb2label_qground(mask_rgb: np.ndarray) -> np.ndarray:
|
|
|
|
| 587 |
|
| 588 |
class Kadid10kDataset(Dataset):
|
| 589 |
"""
|
| 590 |
+
KADID-10k dataset. RandomCrop is applied during sampling.
|
| 591 |
"""
|
| 592 |
|
| 593 |
def __init__(
|
|
|
|
| 739 |
|
| 740 |
def kadid_collate_fn(batch: List[dict]) -> dict:
|
| 741 |
"""
|
| 742 |
+
Collate for Kadid10kDataset.
|
| 743 |
|
| 744 |
+
Returns:
|
| 745 |
images: Tensor (B, C, H, W)
|
| 746 |
+
+ all other keys as lists of length B
|
| 747 |
"""
|
| 748 |
images = torch.stack([item["img"] for item in batch], dim=0)
|
| 749 |
collated: dict = {"images": images}
|
|
|
|
| 943 |
allowed_methods: Optional[List[str]] = ['DiT4SR_x2'],
|
| 944 |
crop_size: Optional[int] = None,
|
| 945 |
sr_artifact_threshold: float = SRGROUND_SR_ARTIFACT_THRESHOLD,
|
| 946 |
+
include_sr_artifact: bool = False,
|
| 947 |
transform=None,
|
| 948 |
):
|
| 949 |
self.root = Path(root)
|
| 950 |
self.sr_artifact_threshold = float(sr_artifact_threshold)
|
| 951 |
+
self.include_sr_artifact = bool(include_sr_artifact)
|
| 952 |
self.json_path = self.root / 'srground_train.json'
|
| 953 |
df = pd.read_json(self.json_path)
|
| 954 |
|
|
|
|
| 991 |
if real_ann_path.exists():
|
| 992 |
annot_rd = _real_distortion_labels(_load_npy_gz(real_ann_path), prominences)
|
| 993 |
|
| 994 |
+
if self.include_sr_artifact:
|
| 995 |
+
sr_path = sample.get('sr_artifacts_ann_path')
|
| 996 |
+
sr_ann_path = self._resolve_path(sr_path)
|
| 997 |
+
if sr_ann_path.exists():
|
| 998 |
+
annot_sr = _sr_artifact_labels(
|
| 999 |
+
_load_npy_gz(sr_ann_path),
|
| 1000 |
+
prominences,
|
| 1001 |
+
threshold=self.sr_artifact_threshold,
|
| 1002 |
+
)
|
| 1003 |
|
| 1004 |
if annot_rd is None and annot_sr is None:
|
| 1005 |
return None, None
|
|
|
|
| 1034 |
annot_sr = np.asarray(self.crop(mask_image), dtype=np.uint8)
|
| 1035 |
|
| 1036 |
img_tensor = self.image_to_tensor(image)
|
| 1037 |
+
mask_hw = (image.height, image.width)
|
| 1038 |
|
| 1039 |
+
if annot_rd is None and annot_sr is None:
|
| 1040 |
+
mask_label = np.zeros(mask_hw, dtype=np.uint8)
|
| 1041 |
+
else:
|
| 1042 |
+
mask_label = merge_srground_masks(annot_rd, annot_sr)
|
| 1043 |
+
mask = torch.from_numpy(mask_label.astype(np.float32)).unsqueeze(0)
|
| 1044 |
+
mask_rd = (
|
| 1045 |
+
torch.from_numpy(annot_rd.astype(np.float32)).unsqueeze(0)
|
| 1046 |
+
if annot_rd is not None
|
| 1047 |
+
else torch.zeros((1, *mask_hw), dtype=torch.float32)
|
| 1048 |
+
)
|
| 1049 |
+
mask_sr = (
|
| 1050 |
+
torch.from_numpy(annot_sr.astype(np.float32)).unsqueeze(0)
|
| 1051 |
+
if annot_sr is not None
|
| 1052 |
+
else torch.zeros((1, *mask_hw), dtype=torch.float32)
|
| 1053 |
+
)
|
| 1054 |
mask_coverage = float((mask > 0).float().mean().item())
|
| 1055 |
|
| 1056 |
real_ann_path = self._resolve_path(sample.get('real_distortions_ann_path'))
|
| 1057 |
+
sr_ann_path = None
|
| 1058 |
+
if self.include_sr_artifact:
|
| 1059 |
+
candidate = self._resolve_path(sample.get('sr_artifacts_ann_path'))
|
| 1060 |
+
if candidate.exists():
|
| 1061 |
+
sr_ann_path = candidate
|
| 1062 |
|
| 1063 |
return {
|
| 1064 |
'img': img_tensor,
|
|
|
|
| 1070 |
'mask_coverage': mask_coverage,
|
| 1071 |
'prominences': sample.get('prominences'),
|
| 1072 |
'has_markup': sample.get('has_markup', False),
|
| 1073 |
+
'sr_artifacts_ann_path': (
|
| 1074 |
+
_to_relative_dataset_path(sr_ann_path, self.root) if sr_ann_path is not None else None
|
| 1075 |
+
),
|
| 1076 |
'real_distortions_ann_path': _to_relative_dataset_path(real_ann_path, self.root),
|
| 1077 |
'sample_id': sample.get('sample_id'),
|
| 1078 |
'distorted_img_path': _to_relative_dataset_path(img_path, self.root),
|
|
|
|
| 1105 |
|
| 1106 |
class KadidPristineDataset(Dataset):
|
| 1107 |
"""
|
| 1108 |
+
KADID-10k pristine (reference) images dataset.
|
| 1109 |
+
Returns only original undistorted images with RandomCrop.
|
| 1110 |
"""
|
| 1111 |
|
| 1112 |
def __init__(
|
|
|
|
| 1126 |
else:
|
| 1127 |
self.transform = transform
|
| 1128 |
|
| 1129 |
+
# Find all original images (I{number}.png format without suffixes)
|
| 1130 |
images_dir = self.root / "images"
|
| 1131 |
pristine_pattern = re.compile(r'^I\d+\.png$')
|
| 1132 |
|
|
|
|
| 1152 |
|
| 1153 |
return {
|
| 1154 |
"img": img,
|
| 1155 |
+
"mos": 5.0, # maximum quality for pristine images
|
| 1156 |
"dist_type": "pristine",
|
| 1157 |
"dist_group": "pristine",
|
| 1158 |
+
"dist_level": 0, # distortion level = 0
|
| 1159 |
"distorted_img_path": img_rel_path,
|
| 1160 |
+
"original_img_path": img_rel_path, # self-reference
|
| 1161 |
+
"sample_id": img_path.stem, # e.g. "I01"
|
| 1162 |
}
|
| 1163 |
|
| 1164 |
|
|
|
|
| 1166 |
"""
|
| 1167 |
Pristine KADID dataset.
|
| 1168 |
|
| 1169 |
+
Expects a root directory with index.csv.
|
| 1170 |
+
Required columns: original_img_path
|
| 1171 |
+
Optional: mask_path (if a distortion-free region mask is available)
|
| 1172 |
"""
|
| 1173 |
|
| 1174 |
def __init__(
|
|
|
|
| 1227 |
|
| 1228 |
def kadid_pristine_collate_fn(batch: List[dict]) -> dict:
|
| 1229 |
"""
|
| 1230 |
+
Collate for KadidPristineDataset.
|
| 1231 |
|
| 1232 |
+
Returns:
|
| 1233 |
images: Tensor (B, C, H, W)
|
| 1234 |
+
+ all other keys as lists of length B
|
| 1235 |
"""
|
| 1236 |
images = torch.stack([item["img"] for item in batch], dim=0)
|
| 1237 |
collated: dict = {"images": images}
|
|
|
|
| 1246 |
|
| 1247 |
def local_kadid_pristine_collate_fn(batch: List[dict]) -> dict:
|
| 1248 |
"""
|
| 1249 |
+
Collate for LocalKadidPristinePresavedDataset.
|
| 1250 |
|
| 1251 |
+
Returns:
|
| 1252 |
images: Tensor (B, C, H, W)
|
| 1253 |
+
masks: Tensor (B, 1, H, W) if has_masks=True
|
| 1254 |
+
+ remaining keys as lists of length B
|
| 1255 |
"""
|
| 1256 |
images = torch.stack([item["img"] for item in batch], dim=0)
|
| 1257 |
|
| 1258 |
collated: dict = {"images": images}
|
| 1259 |
|
| 1260 |
+
# If masks are present in the batch
|
| 1261 |
if "mask" in batch[0]:
|
| 1262 |
masks = torch.stack([item["mask"] for item in batch], dim=0)
|
| 1263 |
collated["masks"] = masks
|
|
|
|
| 1285 |
)
|
| 1286 |
dataset_root = Path(dataset_images_root(str(root), dataset))
|
| 1287 |
path = Path(path_from_meta)
|
| 1288 |
+
if path.is_absolute():
|
| 1289 |
+
parts = Path(path).parts
|
| 1290 |
+
i = parts.index(dataset)
|
| 1291 |
+
suffix = Path(*parts[i:])
|
| 1292 |
+
path = dataset_root / suffix
|
| 1293 |
+
else:
|
| 1294 |
+
path = dataset_root / path
|
| 1295 |
+
return path
|
| 1296 |
|
| 1297 |
|
| 1298 |
def dataset_image_paths(
|
| 1299 |
dataset: str,
|
| 1300 |
+
dataset_root: str,
|
| 1301 |
crop_size: int,
|
| 1302 |
min_distortion_level: int,
|
| 1303 |
) -> List[str]:
|
| 1304 |
if dataset == 'kadid10k':
|
| 1305 |
ds = Kadid10kDataset(
|
| 1306 |
+
dataset_root,
|
| 1307 |
crop_size=crop_size,
|
| 1308 |
min_distortion_level=min_distortion_level,
|
| 1309 |
)
|
| 1310 |
return [_to_relative_dataset_path(Path(p), ds.root) for p in ds.images]
|
| 1311 |
|
| 1312 |
if dataset == 'local_kadid':
|
| 1313 |
+
ds = LocalKadidPresavedDataset(dataset_root, crop_size=crop_size)
|
| 1314 |
return [_to_relative_dataset_path(Path(str(p)), ds.root) for p in ds.df['distorted_img_path'].tolist()]
|
| 1315 |
|
| 1316 |
if dataset == 'QGround':
|
| 1317 |
+
ds = QGroundDataset(dataset_root, split='test', crop_size=crop_size)
|
| 1318 |
return [str(sample['image_rel']) for sample in ds.samples]
|
| 1319 |
|
| 1320 |
if dataset == 'SRGround':
|
| 1321 |
+
ds = SRGroundSmallDataset(dataset_root, json_path='srground_train.json', allowed_methods=['DiT4SR_x2'])
|
| 1322 |
return [_to_relative_dataset_path(Path(str(p)), ds.root) for p in ds.df['image_path'].tolist()]
|
| 1323 |
|
| 1324 |
raise ValueError(f'Unsupported dataset: {dataset}')
|
analysis/features/feature_filters.py
CHANGED
|
@@ -85,57 +85,57 @@ def my_kruskal(
|
|
| 85 |
col_data: np.ndarray,
|
| 86 |
col_indices: np.ndarray,
|
| 87 |
n_samples: int,
|
| 88 |
-
group_indices: list[np.ndarray], #
|
| 89 |
) -> float:
|
| 90 |
"""
|
| 91 |
-
Kruskal-Wallis H-test
|
| 92 |
|
| 93 |
-
|
| 94 |
----------
|
| 95 |
-
col_data :
|
| 96 |
-
col_indices :
|
| 97 |
-
n_samples :
|
| 98 |
-
group_indices: group_indices[g] —
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
-------
|
| 102 |
-
p-value (float),
|
| 103 |
"""
|
| 104 |
n = n_samples
|
| 105 |
n_zeros = n - len(col_data)
|
| 106 |
|
| 107 |
-
# --- 1.
|
| 108 |
-
#
|
| 109 |
-
#
|
| 110 |
|
| 111 |
-
#
|
| 112 |
nonzero_vals = col_data.astype(np.float64)
|
| 113 |
order = np.argsort(nonzero_vals, kind='stable')
|
| 114 |
-
#
|
| 115 |
ranks_local = np.empty(len(nonzero_vals), dtype=np.float64)
|
| 116 |
-
#
|
| 117 |
ranks_local[order] = np.arange(1, len(nonzero_vals) + 1, dtype=np.float64)
|
| 118 |
-
#
|
| 119 |
sorted_vals = nonzero_vals[order]
|
| 120 |
unique_vals, inverse, counts = np.unique(sorted_vals, return_inverse=True, return_counts=True)
|
| 121 |
-
#
|
| 122 |
cum = np.concatenate([[0], np.cumsum(counts)])
|
| 123 |
mean_local_ranks = (cum[:-1] + cum[1:] + 1) / 2.0 # (first + last + 1) / 2
|
| 124 |
ranks_local = mean_local_ranks[inverse]
|
| 125 |
|
| 126 |
-
#
|
| 127 |
-
#
|
| 128 |
-
global_ranks_nonzero = ranks_local + n_zeros #
|
| 129 |
|
| 130 |
-
#
|
| 131 |
zero_mean_rank = (n_zeros + 1) / 2.0 if n_zeros > 0 else 0.0
|
| 132 |
|
| 133 |
-
# --- 2.
|
| 134 |
n_groups = len(group_indices)
|
| 135 |
if n_groups < 2:
|
| 136 |
return np.nan
|
| 137 |
|
| 138 |
-
#
|
| 139 |
rank_map = dict(zip(col_indices, global_ranks_nonzero))
|
| 140 |
|
| 141 |
H_num = 0.0
|
|
@@ -149,7 +149,7 @@ def my_kruskal(
|
|
| 149 |
if ng == 0:
|
| 150 |
continue
|
| 151 |
|
| 152 |
-
#
|
| 153 |
r_sum = 0.0
|
| 154 |
for i in g_idx:
|
| 155 |
r_sum += rank_map.get(int(i), zero_mean_rank)
|
|
@@ -161,9 +161,9 @@ def my_kruskal(
|
|
| 161 |
if len(group_sizes) < 2:
|
| 162 |
return np.nan
|
| 163 |
|
| 164 |
-
n_t = n_total_valid #
|
| 165 |
|
| 166 |
-
# --- 3. H-
|
| 167 |
# H = 12 / (n*(n+1)) * sum(R_i^2 / n_i) - 3*(n+1)
|
| 168 |
H = 0.0
|
| 169 |
for ng, rs in zip(group_sizes, group_rank_sums):
|
|
@@ -171,13 +171,13 @@ def my_kruskal(
|
|
| 171 |
|
| 172 |
H = (12.0 / (n_t * (n_t + 1))) * H - 3.0 * (n_t + 1)
|
| 173 |
|
| 174 |
-
# --- 4.
|
| 175 |
-
# T = sum(t^3 - t)
|
| 176 |
-
# Ties
|
| 177 |
tie_correction = 1.0
|
| 178 |
if len(unique_vals) < len(nonzero_vals):
|
| 179 |
T = np.sum(counts ** 3 - counts, dtype=np.float64)
|
| 180 |
-
#
|
| 181 |
if n_zeros > 1:
|
| 182 |
T += n_zeros ** 3 - n_zeros
|
| 183 |
denom = float(n_t) ** 3 - n_t
|
|
@@ -190,7 +190,7 @@ def my_kruskal(
|
|
| 190 |
if H < 0:
|
| 191 |
H = 0.0
|
| 192 |
|
| 193 |
-
# --- 5. p-value
|
| 194 |
df = len(group_sizes) - 1
|
| 195 |
p_value = float(stats.chi2.sf(H, df))
|
| 196 |
return p_value
|
|
@@ -314,7 +314,7 @@ class KruskalWallisFeatureFilter(BaseFeatureFilter):
|
|
| 314 |
except Exception as exc:
|
| 315 |
print(f'[cache][feature_filter] invalid cache {cache_path}: {exc}. Recomputing...')
|
| 316 |
|
| 317 |
-
#
|
| 318 |
group_indices = []
|
| 319 |
for group in unique_groups:
|
| 320 |
idx = np.where(group_values == group)[0]
|
|
@@ -331,7 +331,7 @@ class KruskalWallisFeatureFilter(BaseFeatureFilter):
|
|
| 331 |
col_data = codes_csc.data[start:stop]
|
| 332 |
col_indices = codes_csc.indices[start:stop]
|
| 333 |
|
| 334 |
-
#
|
| 335 |
n_nonzero = stop - start
|
| 336 |
all_same = (
|
| 337 |
n_nonzero == 0
|
|
@@ -339,9 +339,9 @@ class KruskalWallisFeatureFilter(BaseFeatureFilter):
|
|
| 339 |
or (n_nonzero < n_samples and n_nonzero == 0)
|
| 340 |
)
|
| 341 |
if all_same and n_nonzero == 0:
|
| 342 |
-
continue #
|
| 343 |
if n_nonzero > 0 and n_nonzero == n_samples and np.all(col_data == col_data[0]):
|
| 344 |
-
continue #
|
| 345 |
|
| 346 |
p_value = my_kruskal(col_data, col_indices, n_samples, group_indices)
|
| 347 |
|
|
|
|
| 85 |
col_data: np.ndarray,
|
| 86 |
col_indices: np.ndarray,
|
| 87 |
n_samples: int,
|
| 88 |
+
group_indices: list[np.ndarray], # list of index arrays, one per group
|
| 89 |
) -> float:
|
| 90 |
"""
|
| 91 |
+
Kruskal-Wallis H-test for a single CSC matrix column.
|
| 92 |
|
| 93 |
+
Parameters
|
| 94 |
----------
|
| 95 |
+
col_data : nonzero column values (csc.data[csc.indptr[j]:csc.indptr[j+1]])
|
| 96 |
+
col_indices : row indices of nonzero elements (csc.indices[...])
|
| 97 |
+
n_samples : total number of rows (n)
|
| 98 |
+
group_indices: group_indices[g] — array of row indices for group g
|
| 99 |
+
|
| 100 |
+
Returns
|
| 101 |
+
-------
|
| 102 |
+
p-value (float), or np.nan if the test is not applicable
|
| 103 |
"""
|
| 104 |
n = n_samples
|
| 105 |
n_zeros = n - len(col_data)
|
| 106 |
|
| 107 |
+
# --- 1. Ranks of nonzero elements among ALL n values -------------------
|
| 108 |
+
# Zeros occupy positions 1..n_zeros in the overall order.
|
| 109 |
+
# Nonzero elements start at position n_zeros + 1.
|
| 110 |
|
| 111 |
+
# Sort nonzero values and compute ranks within them
|
| 112 |
nonzero_vals = col_data.astype(np.float64)
|
| 113 |
order = np.argsort(nonzero_vals, kind='stable')
|
| 114 |
+
# Handle ties: assign each unique value its mean rank
|
| 115 |
ranks_local = np.empty(len(nonzero_vals), dtype=np.float64)
|
| 116 |
+
# Temporarily assign 1-based ranks among nonzero values
|
| 117 |
ranks_local[order] = np.arange(1, len(nonzero_vals) + 1, dtype=np.float64)
|
| 118 |
+
# Resolve ties by averaging
|
| 119 |
sorted_vals = nonzero_vals[order]
|
| 120 |
unique_vals, inverse, counts = np.unique(sorted_vals, return_inverse=True, return_counts=True)
|
| 121 |
+
# mean rank for each unique value (1-based among nonzero values)
|
| 122 |
cum = np.concatenate([[0], np.cumsum(counts)])
|
| 123 |
mean_local_ranks = (cum[:-1] + cum[1:] + 1) / 2.0 # (first + last + 1) / 2
|
| 124 |
ranks_local = mean_local_ranks[inverse]
|
| 125 |
|
| 126 |
+
# Shift nonzero ranks by n_zeros (they all lie to the right of zeros)
|
| 127 |
+
# Ties between zeros and nonzero values must be accounted for.
|
| 128 |
+
global_ranks_nonzero = ranks_local + n_zeros # shift by the number of zeros
|
| 129 |
|
| 130 |
+
# Mean rank of zeros: zeros occupy ranks 1..n_zeros
|
| 131 |
zero_mean_rank = (n_zeros + 1) / 2.0 if n_zeros > 0 else 0.0
|
| 132 |
|
| 133 |
+
# --- 2. Sum ranks for each group ----------------------------------------
|
| 134 |
n_groups = len(group_indices)
|
| 135 |
if n_groups < 2:
|
| 136 |
return np.nan
|
| 137 |
|
| 138 |
+
# Nonzero mask: build index->global_rank dict for fast lookup
|
| 139 |
rank_map = dict(zip(col_indices, global_ranks_nonzero))
|
| 140 |
|
| 141 |
H_num = 0.0
|
|
|
|
| 149 |
if ng == 0:
|
| 150 |
continue
|
| 151 |
|
| 152 |
+
# Ranks of group elements
|
| 153 |
r_sum = 0.0
|
| 154 |
for i in g_idx:
|
| 155 |
r_sum += rank_map.get(int(i), zero_mean_rank)
|
|
|
|
| 161 |
if len(group_sizes) < 2:
|
| 162 |
return np.nan
|
| 163 |
|
| 164 |
+
n_t = n_total_valid # should equal n
|
| 165 |
|
| 166 |
+
# --- 3. H-statistic (standard formula) ----------------------------------
|
| 167 |
# H = 12 / (n*(n+1)) * sum(R_i^2 / n_i) - 3*(n+1)
|
| 168 |
H = 0.0
|
| 169 |
for ng, rs in zip(group_sizes, group_rank_sums):
|
|
|
|
| 171 |
|
| 172 |
H = (12.0 / (n_t * (n_t + 1))) * H - 3.0 * (n_t + 1)
|
| 173 |
|
| 174 |
+
# --- 4. Tie correction --------------------------------------------------
|
| 175 |
+
# T = sum(t^3 - t) for each tie group, C = 1 - T / (n^3 - n)
|
| 176 |
+
# Ties among nonzero values:
|
| 177 |
tie_correction = 1.0
|
| 178 |
if len(unique_vals) < len(nonzero_vals):
|
| 179 |
T = np.sum(counts ** 3 - counts, dtype=np.float64)
|
| 180 |
+
# Add ties among zeros:
|
| 181 |
if n_zeros > 1:
|
| 182 |
T += n_zeros ** 3 - n_zeros
|
| 183 |
denom = float(n_t) ** 3 - n_t
|
|
|
|
| 190 |
if H < 0:
|
| 191 |
H = 0.0
|
| 192 |
|
| 193 |
+
# --- 5. p-value from chi2 with df = n_groups - 1 ------------------------
|
| 194 |
df = len(group_sizes) - 1
|
| 195 |
p_value = float(stats.chi2.sf(H, df))
|
| 196 |
return p_value
|
|
|
|
| 314 |
except Exception as exc:
|
| 315 |
print(f'[cache][feature_filter] invalid cache {cache_path}: {exc}. Recomputing...')
|
| 316 |
|
| 317 |
+
# Precompute row indices for each group once
|
| 318 |
group_indices = []
|
| 319 |
for group in unique_groups:
|
| 320 |
idx = np.where(group_values == group)[0]
|
|
|
|
| 331 |
col_data = codes_csc.data[start:stop]
|
| 332 |
col_indices = codes_csc.indices[start:stop]
|
| 333 |
|
| 334 |
+
# Skip constant columns
|
| 335 |
n_nonzero = stop - start
|
| 336 |
all_same = (
|
| 337 |
n_nonzero == 0
|
|
|
|
| 339 |
or (n_nonzero < n_samples and n_nonzero == 0)
|
| 340 |
)
|
| 341 |
if all_same and n_nonzero == 0:
|
| 342 |
+
continue # all zeros — constant
|
| 343 |
if n_nonzero > 0 and n_nonzero == n_samples and np.all(col_data == col_data[0]):
|
| 344 |
+
continue # all identical nonzero values
|
| 345 |
|
| 346 |
p_value = my_kruskal(col_data, col_indices, n_samples, group_indices)
|
| 347 |
|
analysis/features/feature_matrix.py
CHANGED
|
@@ -3,10 +3,17 @@ from __future__ import annotations
|
|
| 3 |
from typing import List, Sequence
|
| 4 |
|
| 5 |
import numpy as np
|
|
|
|
| 6 |
|
| 7 |
from analysis.features.feature_indexing import FeatureMatrix
|
| 8 |
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
def _zscore_columns(x: np.ndarray) -> np.ndarray:
|
| 11 |
mu = x.mean(axis=0, keepdims=True)
|
| 12 |
sigma = x.std(axis=0, keepdims=True)
|
|
@@ -40,7 +47,7 @@ def build_image_feature_matrix(
|
|
| 40 |
|
| 41 |
`image_row_indices` is a sequence where each element is an array of row indices
|
| 42 |
(patch indices) belonging to that image.
|
| 43 |
-
Aggregation modes: 'max', 'sum', 'mean_acts' (mean over positive activations
|
| 44 |
"""
|
| 45 |
return _zscore_columns(
|
| 46 |
build_image_feature_matrix_raw(
|
|
@@ -52,6 +59,96 @@ def build_image_feature_matrix(
|
|
| 52 |
)
|
| 53 |
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
def build_image_feature_matrix_raw(
|
| 56 |
features: FeatureMatrix,
|
| 57 |
image_row_indices: Sequence[Sequence[int]],
|
|
@@ -64,37 +161,19 @@ def build_image_feature_matrix_raw(
|
|
| 64 |
skips the final z-score normalization so it can be used for paired deltas.
|
| 65 |
"""
|
| 66 |
mode = str(aggregation_mode)
|
| 67 |
-
if mode not in {'max', 'sum', 'mean_acts'
|
| 68 |
raise ValueError(f'Unknown aggregation mode: {mode!r}')
|
| 69 |
|
| 70 |
-
n_features_total = features.n_features
|
| 71 |
codes_subset = features.codes
|
| 72 |
-
|
| 73 |
-
|
| 74 |
for img_i, row_ids in enumerate(image_row_indices):
|
| 75 |
if len(row_ids) == 0:
|
| 76 |
continue
|
| 77 |
-
|
| 78 |
-
if mode == 'max':
|
| 79 |
-
x[img_i] = np.asarray(chunk.max(axis=0)).ravel().astype(np.float32)
|
| 80 |
-
elif mode == 'sum':
|
| 81 |
-
x[img_i] = np.asarray(chunk.sum(axis=0)).ravel().astype(np.float32)
|
| 82 |
-
elif mode == 'mean':
|
| 83 |
-
x[img_i] = np.asarray(chunk.mean(axis=0)).ravel().astype(np.float32)
|
| 84 |
-
else: # mean_acts: mean over positive activations
|
| 85 |
-
if hasattr(chunk, 'multiply'):
|
| 86 |
-
positive_mask = chunk > 0
|
| 87 |
-
positive_chunk = chunk.multiply(positive_mask)
|
| 88 |
-
pos_sum = np.asarray(positive_chunk.sum(axis=0)).ravel().astype(np.float32)
|
| 89 |
-
pos_count = np.asarray(positive_mask.sum(axis=0)).ravel().astype(np.float32)
|
| 90 |
-
else:
|
| 91 |
-
chunk_arr = np.asarray(chunk, dtype=np.float32)
|
| 92 |
-
positive_mask = chunk_arr > 0
|
| 93 |
-
pos_sum = np.asarray((chunk_arr * positive_mask).sum(axis=0)).ravel().astype(np.float32)
|
| 94 |
-
pos_count = np.asarray(positive_mask.sum(axis=0)).ravel().astype(np.float32)
|
| 95 |
-
mean_pos = np.zeros(int(n_features_total), dtype=np.float32)
|
| 96 |
-
nonzero = pos_count > 0
|
| 97 |
-
mean_pos[nonzero] = pos_sum[nonzero] / pos_count[nonzero]
|
| 98 |
-
x[img_i] = mean_pos
|
| 99 |
|
| 100 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from typing import List, Sequence
|
| 4 |
|
| 5 |
import numpy as np
|
| 6 |
+
import scipy.sparse as sp
|
| 7 |
|
| 8 |
from analysis.features.feature_indexing import FeatureMatrix
|
| 9 |
|
| 10 |
|
| 11 |
+
def _axis0_to_dense(values) -> np.ndarray:
|
| 12 |
+
if hasattr(values, 'toarray'):
|
| 13 |
+
return np.asarray(values.toarray()).ravel().astype(np.float32)
|
| 14 |
+
return np.asarray(values).ravel().astype(np.float32)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
def _zscore_columns(x: np.ndarray) -> np.ndarray:
|
| 18 |
mu = x.mean(axis=0, keepdims=True)
|
| 19 |
sigma = x.std(axis=0, keepdims=True)
|
|
|
|
| 47 |
|
| 48 |
`image_row_indices` is a sequence where each element is an array of row indices
|
| 49 |
(patch indices) belonging to that image.
|
| 50 |
+
Aggregation modes: 'max', 'sum', 'mean_acts' (mean over positive activations only).
|
| 51 |
"""
|
| 52 |
return _zscore_columns(
|
| 53 |
build_image_feature_matrix_raw(
|
|
|
|
| 59 |
)
|
| 60 |
|
| 61 |
|
| 62 |
+
def _to_dense_image_features(values) -> np.ndarray:
|
| 63 |
+
if sp.issparse(values):
|
| 64 |
+
return np.asarray(values.toarray(), dtype=np.float32)
|
| 65 |
+
return np.asarray(values, dtype=np.float32)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _image_group_matrix(image_idx_arr: np.ndarray, n_images_used: int) -> sp.csr_matrix:
|
| 69 |
+
"""Map each patch row to one image column (n_patches, n_images)."""
|
| 70 |
+
n_patches = int(len(image_idx_arr))
|
| 71 |
+
rows = np.arange(n_patches, dtype=np.int32)
|
| 72 |
+
cols = np.asarray(image_idx_arr, dtype=np.int32)
|
| 73 |
+
return sp.csr_matrix(
|
| 74 |
+
(np.ones(n_patches, dtype=np.float32), (rows, cols)),
|
| 75 |
+
shape=(n_patches, int(n_images_used)),
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _grouped_sum(codes, group: sp.csr_matrix) -> np.ndarray:
|
| 80 |
+
return _to_dense_image_features(group.T @ codes)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _grouped_mean_acts(codes, group: sp.csr_matrix) -> np.ndarray:
|
| 84 |
+
if sp.issparse(codes):
|
| 85 |
+
positive = codes.multiply(codes > 0)
|
| 86 |
+
pos_count = group.T @ (codes > 0).astype(np.float32)
|
| 87 |
+
else:
|
| 88 |
+
codes_arr = np.asarray(codes, dtype=np.float32)
|
| 89 |
+
positive = codes_arr * (codes_arr > 0)
|
| 90 |
+
pos_count = group.T @ (codes_arr > 0).astype(np.float32)
|
| 91 |
+
|
| 92 |
+
pos_sum = _to_dense_image_features(group.T @ positive)
|
| 93 |
+
pos_count = _to_dense_image_features(pos_count)
|
| 94 |
+
out = np.zeros_like(pos_sum, dtype=np.float32)
|
| 95 |
+
nonzero = pos_count > 0
|
| 96 |
+
out[nonzero] = pos_sum[nonzero] / pos_count[nonzero]
|
| 97 |
+
return out
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def _grouped_max(codes, image_idx_arr: np.ndarray, n_images_used: int, n_features_total: int) -> np.ndarray:
|
| 101 |
+
x = np.zeros((int(n_images_used), int(n_features_total)), dtype=np.float32)
|
| 102 |
+
for img_i in range(int(n_images_used)):
|
| 103 |
+
patch_mask = image_idx_arr == img_i
|
| 104 |
+
if not np.any(patch_mask):
|
| 105 |
+
continue
|
| 106 |
+
chunk = codes[patch_mask, :]
|
| 107 |
+
x[img_i] = _axis0_to_dense(chunk.max(axis=0))
|
| 108 |
+
return x
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _aggregate_codes_by_image_idx(
|
| 112 |
+
codes,
|
| 113 |
+
image_idx_arr: np.ndarray,
|
| 114 |
+
n_images_used: int,
|
| 115 |
+
aggregation_mode: str,
|
| 116 |
+
) -> np.ndarray:
|
| 117 |
+
"""Aggregate patch-level codes per image using dense image_idx labels."""
|
| 118 |
+
mode = str(aggregation_mode)
|
| 119 |
+
if mode not in {'max', 'sum', 'mean_acts'}:
|
| 120 |
+
raise ValueError(f'Unknown aggregation mode: {mode!r}')
|
| 121 |
+
|
| 122 |
+
image_idx_arr = np.asarray(image_idx_arr, dtype=np.int32)
|
| 123 |
+
n_images_used = int(n_images_used)
|
| 124 |
+
n_features_total = int(codes.shape[1])
|
| 125 |
+
|
| 126 |
+
if mode == 'max':
|
| 127 |
+
return _grouped_max(codes, image_idx_arr, n_images_used, n_features_total)
|
| 128 |
+
|
| 129 |
+
group = _image_group_matrix(image_idx_arr, n_images_used)
|
| 130 |
+
if mode == 'sum':
|
| 131 |
+
return _grouped_sum(codes, group)
|
| 132 |
+
return _grouped_mean_acts(codes, group)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def build_image_feature_matrix_from_image_idx(
|
| 136 |
+
codes,
|
| 137 |
+
image_idx_arr: np.ndarray,
|
| 138 |
+
n_images_used: int,
|
| 139 |
+
aggregation_mode: str = 'max',
|
| 140 |
+
) -> np.ndarray:
|
| 141 |
+
"""Build z-scored image-level matrix using per-patch ``image_idx`` labels."""
|
| 142 |
+
return _zscore_columns(
|
| 143 |
+
_aggregate_codes_by_image_idx(
|
| 144 |
+
codes,
|
| 145 |
+
image_idx_arr,
|
| 146 |
+
n_images_used,
|
| 147 |
+
aggregation_mode,
|
| 148 |
+
)
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
def build_image_feature_matrix_raw(
|
| 153 |
features: FeatureMatrix,
|
| 154 |
image_row_indices: Sequence[Sequence[int]],
|
|
|
|
| 161 |
skips the final z-score normalization so it can be used for paired deltas.
|
| 162 |
"""
|
| 163 |
mode = str(aggregation_mode)
|
| 164 |
+
if mode not in {'max', 'sum', 'mean_acts'}:
|
| 165 |
raise ValueError(f'Unknown aggregation mode: {mode!r}')
|
| 166 |
|
|
|
|
| 167 |
codes_subset = features.codes
|
| 168 |
+
image_idx_arr = np.empty(int(codes_subset.shape[0]), dtype=np.int32)
|
|
|
|
| 169 |
for img_i, row_ids in enumerate(image_row_indices):
|
| 170 |
if len(row_ids) == 0:
|
| 171 |
continue
|
| 172 |
+
image_idx_arr[list(row_ids)] = int(img_i)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
+
return _aggregate_codes_by_image_idx(
|
| 175 |
+
codes_subset,
|
| 176 |
+
image_idx_arr,
|
| 177 |
+
n_images_used,
|
| 178 |
+
mode,
|
| 179 |
+
)
|
analysis/features/feature_selectors.py
CHANGED
|
@@ -252,7 +252,7 @@ class TopKAbsCorrSelector(BaseFeatureSelector):
|
|
| 252 |
name: str = 'topk_abs_corr'
|
| 253 |
description: str = 'Top-k features by max absolute dist_type correlation.'
|
| 254 |
corr_key: str = 'corr_type_df'
|
| 255 |
-
relation_label: str = '
|
| 256 |
|
| 257 |
def compute_metric_tables(
|
| 258 |
self,
|
|
@@ -316,7 +316,7 @@ class TopKMutualInfoSelector(BaseFeatureSelector):
|
|
| 316 |
name: str = 'topk_mi'
|
| 317 |
description: str = 'Top-k features by max dist_type mutual information.'
|
| 318 |
mi_key: str = 'mi_type_df'
|
| 319 |
-
relation_label: str = '
|
| 320 |
|
| 321 |
def compute_metric_tables(
|
| 322 |
self,
|
|
@@ -386,7 +386,7 @@ class TopKRocAucSelector(BaseFeatureSelector):
|
|
| 386 |
name: str = 'topk_auc'
|
| 387 |
description: str = 'Top-k features by one-vs-rest ROC-AUC at dist_type level.'
|
| 388 |
auc_key: str = 'auc_type_df'
|
| 389 |
-
relation_label: str = '
|
| 390 |
|
| 391 |
def compute_metric_tables(
|
| 392 |
self,
|
|
@@ -456,7 +456,7 @@ class TopKPairedDeltaSelector(BaseFeatureSelector):
|
|
| 456 |
name: str = 'paired_delta'
|
| 457 |
description: str = 'Top-k features by max relative paired activation delta.'
|
| 458 |
delta_key: str = 'paired_relative_dist_type_df'
|
| 459 |
-
relation_label: str = '
|
| 460 |
|
| 461 |
@staticmethod
|
| 462 |
def _delta_mode_from_key(delta_key: str) -> str:
|
|
@@ -636,7 +636,7 @@ class TopKIoUSelector(BaseFeatureSelector):
|
|
| 636 |
|
| 637 |
name: str = 'topk_iou_type'
|
| 638 |
description: str = 'Top-k features by median IoU with distortion regions at dist_type level.'
|
| 639 |
-
relation_label: str = '
|
| 640 |
|
| 641 |
def compute_metric_tables(
|
| 642 |
self,
|
|
|
|
| 252 |
name: str = 'topk_abs_corr'
|
| 253 |
description: str = 'Top-k features by max absolute dist_type correlation.'
|
| 254 |
corr_key: str = 'corr_type_df'
|
| 255 |
+
relation_label: str = 'high correlation'
|
| 256 |
|
| 257 |
def compute_metric_tables(
|
| 258 |
self,
|
|
|
|
| 316 |
name: str = 'topk_mi'
|
| 317 |
description: str = 'Top-k features by max dist_type mutual information.'
|
| 318 |
mi_key: str = 'mi_type_df'
|
| 319 |
+
relation_label: str = 'high mutual information'
|
| 320 |
|
| 321 |
def compute_metric_tables(
|
| 322 |
self,
|
|
|
|
| 386 |
name: str = 'topk_auc'
|
| 387 |
description: str = 'Top-k features by one-vs-rest ROC-AUC at dist_type level.'
|
| 388 |
auc_key: str = 'auc_type_df'
|
| 389 |
+
relation_label: str = 'high ROC-AUC separability'
|
| 390 |
|
| 391 |
def compute_metric_tables(
|
| 392 |
self,
|
|
|
|
| 456 |
name: str = 'paired_delta'
|
| 457 |
description: str = 'Top-k features by max relative paired activation delta.'
|
| 458 |
delta_key: str = 'paired_relative_dist_type_df'
|
| 459 |
+
relation_label: str = 'high paired activation delta'
|
| 460 |
|
| 461 |
@staticmethod
|
| 462 |
def _delta_mode_from_key(delta_key: str) -> str:
|
|
|
|
| 636 |
|
| 637 |
name: str = 'topk_iou_type'
|
| 638 |
description: str = 'Top-k features by median IoU with distortion regions at dist_type level.'
|
| 639 |
+
relation_label: str = 'high spatial localization'
|
| 640 |
|
| 641 |
def compute_metric_tables(
|
| 642 |
self,
|
analysis/features/feature_stats.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
"""
|
| 4 |
|
| 5 |
from typing import List, Optional, Tuple
|
|
@@ -13,19 +13,19 @@ from analysis.features.feature_indexing import FeatureMatrix
|
|
| 13 |
|
| 14 |
def compute_feature_stats(features: FeatureMatrix) -> pd.DataFrame:
|
| 15 |
"""
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
| 19 |
----------
|
| 20 |
features : CSR activations; ``column_feature_ids[j]`` = global SAE id
|
| 21 |
|
| 22 |
-
|
| 23 |
-
-------
|
| 24 |
-
DataFrame
|
| 25 |
-
mean —
|
| 26 |
-
frequency —
|
| 27 |
-
max —
|
| 28 |
-
mean_acts —
|
| 29 |
"""
|
| 30 |
codes = features.codes
|
| 31 |
mat = codes if codes.dtype == np.float32 else codes.astype(np.float32)
|
|
@@ -55,18 +55,18 @@ def get_top_features(
|
|
| 55 |
min_mean_acts: Optional[float] = None,
|
| 56 |
) -> List[int]:
|
| 57 |
"""
|
| 58 |
-
|
| 59 |
|
| 60 |
-
|
| 61 |
----------
|
| 62 |
-
stats : DataFrame
|
| 63 |
-
top_k :
|
| 64 |
criterion : 'mean' | 'frequency' | 'max' | 'mean_acts'
|
| 65 |
-
min_mean_acts :
|
| 66 |
|
| 67 |
-
|
| 68 |
-
-------
|
| 69 |
-
List[int] — feature_id
|
| 70 |
"""
|
| 71 |
assert criterion in ('mean', 'frequency', 'max', 'mean_acts'), \
|
| 72 |
f"criterion must be one of 'mean', 'frequency', 'max', 'mean_acts', got {criterion!r}"
|
|
@@ -84,8 +84,8 @@ def plot_top_features(
|
|
| 84 |
min_mean_acts: Optional[float] = None,
|
| 85 |
) -> None:
|
| 86 |
"""
|
| 87 |
-
Bar
|
| 88 |
-
|
| 89 |
"""
|
| 90 |
top_ids = get_top_features(stats, top_k=top_k, criterion=criterion,
|
| 91 |
min_mean_acts=min_mean_acts)
|
|
|
|
| 1 |
"""
|
| 2 |
+
SAE feature statistics and visualization (bar charts).
|
| 3 |
"""
|
| 4 |
|
| 5 |
from typing import List, Optional, Tuple
|
|
|
|
| 13 |
|
| 14 |
def compute_feature_stats(features: FeatureMatrix) -> pd.DataFrame:
|
| 15 |
"""
|
| 16 |
+
Compute statistics for each SAE feature.
|
| 17 |
|
| 18 |
+
Parameters
|
| 19 |
----------
|
| 20 |
features : CSR activations; ``column_feature_ids[j]`` = global SAE id
|
| 21 |
|
| 22 |
+
Returns
|
| 23 |
+
-------
|
| 24 |
+
DataFrame with columns: feature_id (global SAE id), mean, frequency, max, mean_acts
|
| 25 |
+
mean — mean activation across all patches
|
| 26 |
+
frequency — fraction of patches with nonzero activation
|
| 27 |
+
max — maximum activation among all patches
|
| 28 |
+
mean_acts — mean activation over nonzero patches
|
| 29 |
"""
|
| 30 |
codes = features.codes
|
| 31 |
mat = codes if codes.dtype == np.float32 else codes.astype(np.float32)
|
|
|
|
| 55 |
min_mean_acts: Optional[float] = None,
|
| 56 |
) -> List[int]:
|
| 57 |
"""
|
| 58 |
+
Return top-K feature indices by the selected criterion.
|
| 59 |
|
| 60 |
+
Parameters
|
| 61 |
----------
|
| 62 |
+
stats : DataFrame from compute_feature_stats
|
| 63 |
+
top_k : number of top features
|
| 64 |
criterion : 'mean' | 'frequency' | 'max' | 'mean_acts'
|
| 65 |
+
min_mean_acts : preliminary filter on mean_acts
|
| 66 |
|
| 67 |
+
Returns
|
| 68 |
+
-------
|
| 69 |
+
List[int] — feature_id in descending order of the criterion
|
| 70 |
"""
|
| 71 |
assert criterion in ('mean', 'frequency', 'max', 'mean_acts'), \
|
| 72 |
f"criterion must be one of 'mean', 'frequency', 'max', 'mean_acts', got {criterion!r}"
|
|
|
|
| 84 |
min_mean_acts: Optional[float] = None,
|
| 85 |
) -> None:
|
| 86 |
"""
|
| 87 |
+
Bar chart of top-K features by the selected criterion.
|
| 88 |
+
Each bar is labeled with frequency (fraction of nonzero patches).
|
| 89 |
"""
|
| 90 |
top_ids = get_top_features(stats, top_k=top_k, criterion=criterion,
|
| 91 |
min_mean_acts=min_mean_acts)
|
analysis/metrics/correlations.py
CHANGED
|
@@ -21,8 +21,8 @@ def _prepare_codes_and_categories(
|
|
| 21 |
feature_names = [int(fid) for fid in features.column_feature_ids]
|
| 22 |
|
| 23 |
if binarize:
|
| 24 |
-
threshold = 0.2 #
|
| 25 |
-
print(f'[binarize]
|
| 26 |
if sp.issparse(codes):
|
| 27 |
codes = codes.copy()
|
| 28 |
codes.data = (codes.data > threshold).astype(np.float32)
|
|
@@ -74,7 +74,6 @@ def compute_distortion_correlations(
|
|
| 74 |
cached = load_parquet_cache(cache_path, label='correlations')
|
| 75 |
if cached is not None:
|
| 76 |
return cached
|
| 77 |
-
|
| 78 |
work_features = features.subset(global_feature_ids)
|
| 79 |
|
| 80 |
work_codes, _, feature_names, unique_categories, cat_idx = _prepare_codes_and_categories(
|
|
|
|
| 21 |
feature_names = [int(fid) for fid in features.column_feature_ids]
|
| 22 |
|
| 23 |
if binarize:
|
| 24 |
+
threshold = 0.2 # Same value as in PatchSAE
|
| 25 |
+
print(f'[binarize] Binarizing with threshold {threshold}')
|
| 26 |
if sp.issparse(codes):
|
| 27 |
codes = codes.copy()
|
| 28 |
codes.data = (codes.data > threshold).astype(np.float32)
|
|
|
|
| 74 |
cached = load_parquet_cache(cache_path, label='correlations')
|
| 75 |
if cached is not None:
|
| 76 |
return cached
|
|
|
|
| 77 |
work_features = features.subset(global_feature_ids)
|
| 78 |
|
| 79 |
work_codes, _, feature_names, unique_categories, cat_idx = _prepare_codes_and_categories(
|
analysis/models.py
CHANGED
|
@@ -15,16 +15,16 @@ _hook_handle = None
|
|
| 15 |
|
| 16 |
|
| 17 |
def _make_hook(name: str, sequence_layout: str = 'blc'):
|
| 18 |
-
"""Forward hook
|
| 19 |
|
| 20 |
-
|
| 21 |
- (B, C, H, W) -> (B*H*W, C)
|
| 22 |
- (B, L, C) -> (B*L, C) (MANIQA)
|
| 23 |
- (L, B, C) -> (B*L, C) (LIQE/LIQE-MIX)
|
| 24 |
"""
|
| 25 |
def hook(module, inp, out):
|
| 26 |
if hasattr(out, 'last_hidden_state'):
|
| 27 |
-
out_detached = out.last_hidden_state.detach()
|
| 28 |
else:
|
| 29 |
out_detached = out.detach()
|
| 30 |
if out_detached.ndim == 4:
|
|
@@ -101,23 +101,23 @@ def load_sae(
|
|
| 101 |
sae_config: Optional[Dict[str, Any]] = None,
|
| 102 |
**overrides: Any,
|
| 103 |
) -> torch.nn.Module:
|
| 104 |
-
"""
|
| 105 |
|
| 106 |
Parameters
|
| 107 |
----------
|
| 108 |
checkpoint_path : str
|
| 109 |
-
|
| 110 |
config_path : str, optional
|
| 111 |
-
|
| 112 |
-
|
| 113 |
device : str
|
| 114 |
-
|
| 115 |
dtype : torch.dtype
|
| 116 |
-
|
| 117 |
sae_config : dict, optional
|
| 118 |
-
|
| 119 |
**overrides
|
| 120 |
-
|
| 121 |
"""
|
| 122 |
import accelerate
|
| 123 |
from train_code.sae import SAE, MatchingPursuitSAE
|
|
@@ -159,17 +159,17 @@ def load_iqa_model(
|
|
| 159 |
swin_num: int = 2,
|
| 160 |
):
|
| 161 |
"""
|
| 162 |
-
|
| 163 |
|
| 164 |
-
|
| 165 |
-
- arniqa-kadid: hook
|
| 166 |
-
- maniqa: hook
|
| 167 |
-
- liqe / liqe_mix: hook
|
| 168 |
|
| 169 |
-
|
| 170 |
-
-------
|
| 171 |
-
iqa :
|
| 172 |
-
layer_name :
|
| 173 |
"""
|
| 174 |
import pyiqa
|
| 175 |
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
def _make_hook(name: str, sequence_layout: str = 'blc'):
|
| 18 |
+
"""Forward hook for ARNIQA/MANIQA/LIQE.
|
| 19 |
|
| 20 |
+
Supported layer output formats:
|
| 21 |
- (B, C, H, W) -> (B*H*W, C)
|
| 22 |
- (B, L, C) -> (B*L, C) (MANIQA)
|
| 23 |
- (L, B, C) -> (B*L, C) (LIQE/LIQE-MIX)
|
| 24 |
"""
|
| 25 |
def hook(module, inp, out):
|
| 26 |
if hasattr(out, 'last_hidden_state'):
|
| 27 |
+
out_detached = out.last_hidden_state.detach() # Temporary workaround
|
| 28 |
else:
|
| 29 |
out_detached = out.detach()
|
| 30 |
if out_detached.ndim == 4:
|
|
|
|
| 101 |
sae_config: Optional[Dict[str, Any]] = None,
|
| 102 |
**overrides: Any,
|
| 103 |
) -> torch.nn.Module:
|
| 104 |
+
"""Load SAE from a checkpoint, inferring architecture from the JSON config.
|
| 105 |
|
| 106 |
Parameters
|
| 107 |
----------
|
| 108 |
checkpoint_path : str
|
| 109 |
+
Path to the checkpoint directory (output_dir/checkpoint-<step>/) or weights file.
|
| 110 |
config_path : str, optional
|
| 111 |
+
Path to sae_config.json. By default, looked up in the parent directory
|
| 112 |
+
of the checkpoint (i.e. output_dir/sae_config.json).
|
| 113 |
device : str
|
| 114 |
+
Device to load onto.
|
| 115 |
dtype : torch.dtype
|
| 116 |
+
Data type for loading.
|
| 117 |
sae_config : dict, optional
|
| 118 |
+
Pre-loaded config.
|
| 119 |
**overrides
|
| 120 |
+
Override individual config fields (e.g. mp_threshold=0.05).
|
| 121 |
"""
|
| 122 |
import accelerate
|
| 123 |
from train_code.sae import SAE, MatchingPursuitSAE
|
|
|
|
| 159 |
swin_num: int = 2,
|
| 160 |
):
|
| 161 |
"""
|
| 162 |
+
Load an IQA model and register a hook on the selected layer.
|
| 163 |
|
| 164 |
+
Supported metrics:
|
| 165 |
+
- arniqa-kadid: hook on iqa.net.encoder[layer_num]
|
| 166 |
+
- maniqa: hook on iqa.net.swintransformer{swin_num}.layers[layer_num]
|
| 167 |
+
- liqe / liqe_mix: hook on iqa.net.clip_model.visual.transformer.resblocks[layer_num]
|
| 168 |
|
| 169 |
+
Returns
|
| 170 |
+
-------
|
| 171 |
+
iqa : loaded IQA model
|
| 172 |
+
layer_name : string key under which activations are stored in _iqa_activations
|
| 173 |
"""
|
| 174 |
import pyiqa
|
| 175 |
|
analysis/utils.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
"""
|
| 4 |
|
| 5 |
from typing import List
|
|
@@ -18,25 +18,25 @@ def get_top_images_for_feature(
|
|
| 18 |
aggregation: str = 'mean_acts',
|
| 19 |
) -> List[int]:
|
| 20 |
"""
|
| 21 |
-
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
|
| 26 |
-
|
| 27 |
----------
|
| 28 |
features : CSR activations with global id per column
|
| 29 |
-
meta : DataFrame
|
| 30 |
feature_id : global SAE feature id
|
| 31 |
-
top_n :
|
| 32 |
aggregation : 'mean_acts' | 'max' | 'sum'
|
| 33 |
-
mean_acts —
|
| 34 |
-
max —
|
| 35 |
-
sum —
|
| 36 |
|
| 37 |
-
|
| 38 |
-
-------
|
| 39 |
-
List[int] — image_idx
|
| 40 |
"""
|
| 41 |
assert aggregation in ('mean_acts', 'max', 'sum'), (
|
| 42 |
f"aggregation must be 'mean_acts', 'max' or 'sum', got {aggregation!r}"
|
|
@@ -72,11 +72,11 @@ def get_top_images_for_feature_by_iou(
|
|
| 72 |
dataset: str | None = None,
|
| 73 |
) -> List[int]:
|
| 74 |
"""
|
| 75 |
-
|
| 76 |
-
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
"""
|
| 81 |
from analysis.metrics import iou_utils
|
| 82 |
|
|
|
|
| 1 |
"""
|
| 2 |
+
Shared helper utilities for the analysis package.
|
| 3 |
"""
|
| 4 |
|
| 5 |
from typing import List
|
|
|
|
| 18 |
aggregation: str = 'mean_acts',
|
| 19 |
) -> List[int]:
|
| 20 |
"""
|
| 21 |
+
Return image indices where feature_id activates most strongly.
|
| 22 |
|
| 23 |
+
Patch activations within each image are aggregated into a single scalar,
|
| 24 |
+
then images are sorted in descending order.
|
| 25 |
|
| 26 |
+
Parameters
|
| 27 |
----------
|
| 28 |
features : CSR activations with global id per column
|
| 29 |
+
meta : DataFrame with an 'image_idx' column (one patch per row)
|
| 30 |
feature_id : global SAE feature id
|
| 31 |
+
top_n : number of images to return
|
| 32 |
aggregation : 'mean_acts' | 'max' | 'sum'
|
| 33 |
+
mean_acts — mean over patches with activation > 0
|
| 34 |
+
max — maximum activation among patches
|
| 35 |
+
sum — sum of all activations
|
| 36 |
|
| 37 |
+
Returns
|
| 38 |
+
-------
|
| 39 |
+
List[int] — image_idx in descending order of aggregated activation
|
| 40 |
"""
|
| 41 |
assert aggregation in ('mean_acts', 'max', 'sum'), (
|
| 42 |
f"aggregation must be 'mean_acts', 'max' or 'sum', got {aggregation!r}"
|
|
|
|
| 72 |
dataset: str | None = None,
|
| 73 |
) -> List[int]:
|
| 74 |
"""
|
| 75 |
+
Return image indices with the highest IoU between the feature's binary
|
| 76 |
+
activation map and the distortion mask for each image.
|
| 77 |
|
| 78 |
+
Requires `image_idx` and either `patch_mask_label` or `patch_is_distorted`
|
| 79 |
+
in `meta` (see iou_utils._load_patch_mask_for_group).
|
| 80 |
"""
|
| 81 |
from analysis.metrics import iou_utils
|
| 82 |
|
analysis/viz/umap_utils.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import hashlib
|
| 4 |
import json
|
|
|
|
| 5 |
import pickle
|
| 6 |
import re
|
| 7 |
import time
|
|
@@ -9,10 +10,62 @@ from pathlib import Path
|
|
| 9 |
from typing import Any, Callable, Dict, Hashable, Mapping, Optional
|
| 10 |
|
| 11 |
import numpy as np
|
|
|
|
| 12 |
import umap
|
| 13 |
|
| 14 |
_UMAP_CACHE: Dict[Hashable, Dict[str, Any]] = {}
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
def _json_default(value: object) -> object:
|
| 18 |
if isinstance(value, (np.integer, np.floating)):
|
|
@@ -116,36 +169,40 @@ def compute_umap_from_features(features: np.ndarray, umap_params: Dict) -> Dict[
|
|
| 116 |
|
| 117 |
|
| 118 |
def get_or_compute_umap_with_builder(
|
| 119 |
-
key: Hashable,
|
| 120 |
build_features_fn: Callable[[], np.ndarray],
|
| 121 |
umap_params: Dict,
|
| 122 |
-
cache: Optional[Dict[Hashable, Dict[str, Any]]] = None,
|
| 123 |
*,
|
|
|
|
|
|
|
|
|
|
| 124 |
cache_dir: str | Path | None = None,
|
| 125 |
-
cache_id: str | None = None,
|
| 126 |
-
cache_signature: Mapping[str, object] | None = None,
|
| 127 |
) -> Dict[str, Any]:
|
| 128 |
"""Get or compute UMAP embedding using a builder function to produce features.
|
| 129 |
|
| 130 |
-
- ``key``: a hashable cache key (e.g. (feature_id, params_tuple)).
|
| 131 |
- ``build_features_fn``: zero-arg callable that returns a np.ndarray of shape (n_samples, n_features).
|
| 132 |
- ``umap_params``: parameters passed to ``umap.UMAP``.
|
|
|
|
|
|
|
|
|
|
| 133 |
- ``cache``: optional dict to use for caching; if None, module-level cache is used.
|
| 134 |
-
- ``cache_dir``: optional directory for on-disk NPZ cache
|
| 135 |
-
- ``cache_id``: stable logical id used in cache filenames (defaults to ``str(key)``).
|
| 136 |
-
- ``cache_signature``: optional data fingerprint for disk cache lookup before feature build.
|
| 137 |
|
| 138 |
Returns a dict with embedding and timing and other meta fields. Adds 'cache_hit' flag.
|
| 139 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
cache = _UMAP_CACHE if cache is None else cache
|
| 141 |
if key in cache:
|
| 142 |
cached = dict(cache[key])
|
| 143 |
cached["cache_hit"] = True
|
| 144 |
return cached
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
if cache_dir is not None and cache_signature is not None:
|
| 149 |
disk_cache_path = _resolve_disk_cache_path(
|
| 150 |
cache_dir=cache_dir,
|
| 151 |
cache_id=resolved_cache_id,
|
|
@@ -169,7 +226,7 @@ def get_or_compute_umap_with_builder(
|
|
| 169 |
cache_id=resolved_cache_id,
|
| 170 |
umap_params=umap_params,
|
| 171 |
signature=cache_signature,
|
| 172 |
-
x_shape=None
|
| 173 |
)
|
| 174 |
loaded = _load_umap_from_disk(disk_cache_path)
|
| 175 |
if loaded is not None:
|
|
|
|
| 2 |
|
| 3 |
import hashlib
|
| 4 |
import json
|
| 5 |
+
import os
|
| 6 |
import pickle
|
| 7 |
import re
|
| 8 |
import time
|
|
|
|
| 10 |
from typing import Any, Callable, Dict, Hashable, Mapping, Optional
|
| 11 |
|
| 12 |
import numpy as np
|
| 13 |
+
import pandas as pd
|
| 14 |
import umap
|
| 15 |
|
| 16 |
_UMAP_CACHE: Dict[Hashable, Dict[str, Any]] = {}
|
| 17 |
|
| 18 |
+
UMAP_IMAGE_LIMIT = 1000
|
| 19 |
+
UMAP_IMAGE_RANDOM_STATE = 42
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def select_random_image_indices(
|
| 23 |
+
max_images: int,
|
| 24 |
+
*,
|
| 25 |
+
limit: int = UMAP_IMAGE_LIMIT,
|
| 26 |
+
random_state: int = UMAP_IMAGE_RANDOM_STATE,
|
| 27 |
+
) -> np.ndarray:
|
| 28 |
+
"""Pick up to ``limit`` random unique image_idx values from ``[0, max_images)``."""
|
| 29 |
+
max_images = int(max_images)
|
| 30 |
+
n_pick = min(int(limit), max_images)
|
| 31 |
+
if n_pick <= 0:
|
| 32 |
+
raise ValueError(f'max_images must be positive, got {max_images}')
|
| 33 |
+
rng = np.random.default_rng(int(random_state))
|
| 34 |
+
return np.sort(rng.choice(np.arange(max_images), size=n_pick, replace=False)).astype(np.int32)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def build_kadid_index_lookup(meta_df: pd.DataFrame, kadid_ds: Any, n_images_used: int) -> np.ndarray:
|
| 38 |
+
"""Map dense UMAP image index (0..n-1) to ``Kadid10kDataset`` positional index."""
|
| 39 |
+
lookup = np.arange(n_images_used, dtype=np.int32)
|
| 40 |
+
if kadid_ds is None:
|
| 41 |
+
return lookup
|
| 42 |
+
|
| 43 |
+
first_rows = meta_df.groupby('image_idx', sort=True).first()
|
| 44 |
+
kadid_paths = [str(p) for p in kadid_ds.images]
|
| 45 |
+
path_to_idx = {p: i for i, p in enumerate(kadid_paths)}
|
| 46 |
+
base_to_idx = {os.path.basename(p): i for i, p in enumerate(kadid_paths)}
|
| 47 |
+
|
| 48 |
+
for dense_i in range(n_images_used):
|
| 49 |
+
row = first_rows.iloc[dense_i]
|
| 50 |
+
kadid_idx = None
|
| 51 |
+
for col in ('distorted_img_path', 'image_path', 'original_img_path'):
|
| 52 |
+
if col not in row.index:
|
| 53 |
+
continue
|
| 54 |
+
val = row[col]
|
| 55 |
+
if val is None or (isinstance(val, float) and np.isnan(val)):
|
| 56 |
+
continue
|
| 57 |
+
path = str(val)
|
| 58 |
+
kadid_idx = path_to_idx.get(path)
|
| 59 |
+
if kadid_idx is None and not Path(path).is_absolute():
|
| 60 |
+
kadid_idx = path_to_idx.get(str(kadid_ds.root / Path(path)))
|
| 61 |
+
if kadid_idx is None:
|
| 62 |
+
kadid_idx = base_to_idx.get(os.path.basename(path))
|
| 63 |
+
if kadid_idx is not None:
|
| 64 |
+
break
|
| 65 |
+
if kadid_idx is not None:
|
| 66 |
+
lookup[dense_i] = int(kadid_idx)
|
| 67 |
+
return lookup
|
| 68 |
+
|
| 69 |
|
| 70 |
def _json_default(value: object) -> object:
|
| 71 |
if isinstance(value, (np.integer, np.floating)):
|
|
|
|
| 169 |
|
| 170 |
|
| 171 |
def get_or_compute_umap_with_builder(
|
|
|
|
| 172 |
build_features_fn: Callable[[], np.ndarray],
|
| 173 |
umap_params: Dict,
|
|
|
|
| 174 |
*,
|
| 175 |
+
cache_id: str,
|
| 176 |
+
cache_signature: Mapping[str, object],
|
| 177 |
+
cache: Optional[Dict[Hashable, Dict[str, Any]]] = None,
|
| 178 |
cache_dir: str | Path | None = None,
|
|
|
|
|
|
|
| 179 |
) -> Dict[str, Any]:
|
| 180 |
"""Get or compute UMAP embedding using a builder function to produce features.
|
| 181 |
|
|
|
|
| 182 |
- ``build_features_fn``: zero-arg callable that returns a np.ndarray of shape (n_samples, n_features).
|
| 183 |
- ``umap_params``: parameters passed to ``umap.UMAP``.
|
| 184 |
+
- ``cache_id``: stable logical id used in cache filenames.
|
| 185 |
+
- ``cache_signature``: data fingerprint for cache lookup (paired with ``umap_params`` via
|
| 186 |
+
:func:`build_umap_cache_key` for both in-memory and on-disk cache).
|
| 187 |
- ``cache``: optional dict to use for caching; if None, module-level cache is used.
|
| 188 |
+
- ``cache_dir``: optional directory for on-disk NPZ cache.
|
|
|
|
|
|
|
| 189 |
|
| 190 |
Returns a dict with embedding and timing and other meta fields. Adds 'cache_hit' flag.
|
| 191 |
"""
|
| 192 |
+
key = build_umap_cache_key(
|
| 193 |
+
cache_id=str(cache_id),
|
| 194 |
+
umap_params=umap_params,
|
| 195 |
+
signature=cache_signature,
|
| 196 |
+
)
|
| 197 |
+
resolved_cache_id = _sanitize_cache_id(cache_id)
|
| 198 |
+
|
| 199 |
cache = _UMAP_CACHE if cache is None else cache
|
| 200 |
if key in cache:
|
| 201 |
cached = dict(cache[key])
|
| 202 |
cached["cache_hit"] = True
|
| 203 |
return cached
|
| 204 |
|
| 205 |
+
if cache_dir is not None:
|
|
|
|
|
|
|
| 206 |
disk_cache_path = _resolve_disk_cache_path(
|
| 207 |
cache_dir=cache_dir,
|
| 208 |
cache_id=resolved_cache_id,
|
|
|
|
| 226 |
cache_id=resolved_cache_id,
|
| 227 |
umap_params=umap_params,
|
| 228 |
signature=cache_signature,
|
| 229 |
+
x_shape=None,
|
| 230 |
)
|
| 231 |
loaded = _load_umap_from_disk(disk_cache_path)
|
| 232 |
if loaded is not None:
|
analysis/viz/vis_heatmaps.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
"""
|
| 4 |
|
| 5 |
import io
|
|
@@ -27,25 +27,25 @@ from analysis.datasets import (
|
|
| 27 |
QGROUND_DISTORTION_TYPES,
|
| 28 |
SRGROUND_DISTORTION_TYPES,
|
| 29 |
SRGROUND_LEGEND_LABELS,
|
|
|
|
| 30 |
available_distortions,
|
| 31 |
distortion_types_mapping,
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
srground_prominences_by_image_paths,
|
| 35 |
)
|
| 36 |
|
| 37 |
# Show per-image filenames (and distortion subtitles where enabled) under heatmap panels.
|
| 38 |
SHOW_HEATMAP_IMAGE_TITLES = False
|
| 39 |
|
| 40 |
-
MASK_LEGEND_FONTSIZE =
|
| 41 |
MASK_LEGEND_HANDLE_HEIGHT = 1.15
|
| 42 |
MASK_LEGEND_HANDLE_LENGTH = 1.55
|
| 43 |
|
| 44 |
|
| 45 |
def _get_distortion_info(img_path: str):
|
| 46 |
"""
|
| 47 |
-
|
| 48 |
-
(distortion_name, distortion_group)
|
| 49 |
"""
|
| 50 |
name = Path(img_path).name
|
| 51 |
m = _re.match(r'I\d+_(\d+)_(\d+)\.png$', name, _re.IGNORECASE)
|
|
@@ -59,8 +59,8 @@ def _get_distortion_info(img_path: str):
|
|
| 59 |
|
| 60 |
def _get_original_path(distorted_path: str) -> Optional[str]:
|
| 61 |
"""
|
| 62 |
-
|
| 63 |
-
|
| 64 |
"""
|
| 65 |
p = Path(distorted_path)
|
| 66 |
match = _re.match(r'(I\d+)_\d+_\d+\.png$', p.name, _re.IGNORECASE)
|
|
@@ -207,15 +207,15 @@ def _plot_heatmap_grid(
|
|
| 207 |
title_feature_id: int | None = None,
|
| 208 |
) -> FigureSaveResult:
|
| 209 |
"""
|
| 210 |
-
|
| 211 |
|
| 212 |
-
|
| 213 |
----------
|
| 214 |
-
imgs_list :
|
| 215 |
-
codes_tensor :
|
| 216 |
-
feature_id :
|
| 217 |
-
image_paths :
|
| 218 |
-
img_size_inches:
|
| 219 |
"""
|
| 220 |
B = len(imgs_list)
|
| 221 |
inner_concepts = codes_tensor.shape[-1]
|
|
@@ -268,15 +268,15 @@ def _plot_diff_grid(
|
|
| 268 |
show_img: bool = True,
|
| 269 |
) -> FigureSaveResult:
|
| 270 |
"""
|
| 271 |
-
|
| 272 |
|
| 273 |
-
|
| 274 |
----------
|
| 275 |
-
imgs_list :
|
| 276 |
-
orig_tensors :
|
| 277 |
-
feature_id :
|
| 278 |
-
image_paths :
|
| 279 |
-
img_size_inches:
|
| 280 |
"""
|
| 281 |
B = len(imgs_list)
|
| 282 |
fig_diff, axes = plt.subplots(1, B, figsize=(img_size_inches * B, img_size_inches * 1.25))
|
|
@@ -302,37 +302,54 @@ def _plot_diff_grid(
|
|
| 302 |
return _finalize_figure(fig_diff, save_path=save_path, show_img=show_img)
|
| 303 |
|
| 304 |
|
| 305 |
-
def
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
feature_id: int,
|
| 308 |
image_paths: List[str],
|
|
|
|
|
|
|
| 309 |
crop_size: int,
|
| 310 |
img_size_inches: float = 4.0,
|
| 311 |
meta_rows: Optional[List[Optional[pd.Series]]] = None,
|
| 312 |
save_path: Optional[str] = None,
|
| 313 |
show_img: bool = True,
|
| 314 |
) -> FigureSaveResult:
|
| 315 |
-
|
|
|
|
| 316 |
fig, axes = plt.subplots(1, B, figsize=(img_size_inches * B, img_size_inches * 1.35))
|
| 317 |
if B == 1:
|
| 318 |
axes = [axes]
|
| 319 |
if meta_rows is None:
|
| 320 |
meta_rows = [None] * B
|
| 321 |
|
| 322 |
-
for ax,
|
| 323 |
-
if
|
| 324 |
-
mask_rgb = np.asarray(Image.open(mask_path).convert('RGB'), dtype=np.uint8)
|
| 325 |
ax.imshow(mask_rgb)
|
| 326 |
height, width = mask_rgb.shape[:2]
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
rect_y = (height - rect_height) / 2.0
|
| 331 |
ax.add_patch(
|
| 332 |
Rectangle(
|
| 333 |
(rect_x, rect_y),
|
| 334 |
-
|
| 335 |
-
|
| 336 |
fill=False,
|
| 337 |
edgecolor='black',
|
| 338 |
linewidth=2.0,
|
|
@@ -341,21 +358,64 @@ def _plot_qground_mask_grid(
|
|
| 341 |
)
|
| 342 |
_set_image_subplot_title(ax, img_path, meta_row)
|
| 343 |
else:
|
| 344 |
-
ax.text(
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
ax.axis('off')
|
| 347 |
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
_add_mask_legend(fig, handles)
|
| 354 |
-
fig.suptitle(f'QGround annotation mask (feature {feature_id})', fontsize=12, x=0.5, ha='center')
|
| 355 |
fig.tight_layout(rect=(0, 0.14, 1, 1))
|
| 356 |
return _finalize_figure(fig, save_path=save_path, show_img=show_img)
|
| 357 |
|
| 358 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
def _add_mask_legend(fig, handles: list[Patch], *, ncol: int = 3) -> None:
|
| 360 |
fig.legend(
|
| 361 |
handles=handles,
|
|
@@ -371,10 +431,23 @@ def _add_mask_legend(fig, handles: list[Patch], *, ncol: int = 3) -> None:
|
|
| 371 |
)
|
| 372 |
|
| 373 |
|
| 374 |
-
def srground_legend_handles(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
handles = [Patch(facecolor='black', edgecolor='none', label='background')]
|
| 376 |
for dist_name, rgb in SRGROUND_DISTORTION_TYPES.items():
|
| 377 |
-
if
|
| 378 |
continue
|
| 379 |
color = tuple((np.asarray(rgb, dtype=np.float32) / 255.0).tolist())
|
| 380 |
label = SRGROUND_LEGEND_LABELS.get(dist_name, dist_name)
|
|
@@ -392,7 +465,6 @@ def _plot_overlay_and_srground_mask_rows(
|
|
| 392 |
meta_rows: Optional[List[Optional[pd.Series]]],
|
| 393 |
*,
|
| 394 |
title_feature_id: int,
|
| 395 |
-
include_sr_artifact: bool,
|
| 396 |
crop_size: int,
|
| 397 |
save_path: Optional[str] = None,
|
| 398 |
show_img: bool = False,
|
|
@@ -468,7 +540,7 @@ def _plot_overlay_and_srground_mask_rows(
|
|
| 468 |
|
| 469 |
_add_mask_legend(
|
| 470 |
fig,
|
| 471 |
-
srground_legend_handles(
|
| 472 |
)
|
| 473 |
fig.suptitle(f'Feature {display_id}', fontsize=13, x=0.5, ha='center')
|
| 474 |
fig.tight_layout(rect=(0, 0.16, 1, 0.96))
|
|
@@ -483,12 +555,12 @@ def render_top_feature_panel_srground(
|
|
| 483 |
feature_id: int,
|
| 484 |
*,
|
| 485 |
patches_per_image: Optional[int] = None,
|
| 486 |
-
crop_size: int =
|
| 487 |
img_size_inches: float = 3.5,
|
| 488 |
-
|
| 489 |
-
datasets_root: str | None = None,
|
| 490 |
) -> bytes | None:
|
| 491 |
"""Two-row panel: SAE heatmap overlays and SRGround annotation masks with legend."""
|
|
|
|
| 492 |
pil_images = [Image.open(p).convert('RGB') for p in image_paths]
|
| 493 |
preprocess = transforms.Compose(
|
| 494 |
[
|
|
@@ -511,20 +583,13 @@ def render_top_feature_panel_srground(
|
|
| 511 |
|
| 512 |
meta_lookup = _build_meta_lookup(meta)
|
| 513 |
meta_rows = [meta_lookup.get(int(img_idx)) for img_idx in image_indices]
|
| 514 |
-
image_rels = [_image_rel_from_meta_row(row) for row in meta_rows]
|
| 515 |
-
prom_map = srground_prominences_by_image_paths(
|
| 516 |
-
[rel for rel in image_rels if rel],
|
| 517 |
-
datasets_root=datasets_root,
|
| 518 |
-
)
|
| 519 |
mask_rgb_list = [
|
| 520 |
-
|
| 521 |
row,
|
| 522 |
-
|
| 523 |
-
include_sr_artifact=include_sr_artifact,
|
| 524 |
crop_size=crop_size,
|
| 525 |
-
prominences=prom_map.get(image_rel) if image_rel else None,
|
| 526 |
)
|
| 527 |
-
for row
|
| 528 |
]
|
| 529 |
|
| 530 |
matrix_col = features.column_for(int(feature_id))
|
|
@@ -537,7 +602,6 @@ def render_top_feature_panel_srground(
|
|
| 537 |
img_size_inches,
|
| 538 |
meta_rows,
|
| 539 |
title_feature_id=int(feature_id),
|
| 540 |
-
include_sr_artifact=include_sr_artifact,
|
| 541 |
crop_size=crop_size,
|
| 542 |
save_path=None,
|
| 543 |
show_img=False,
|
|
@@ -554,39 +618,41 @@ def visualize_feature_heatmaps(
|
|
| 554 |
patches_per_image: Optional[int] = None,
|
| 555 |
crop_size: int = 224,
|
| 556 |
img_size_inches: float = 4.0,
|
| 557 |
-
|
| 558 |
save_dir: Optional[str] = None,
|
| 559 |
file_prefix: str = '',
|
| 560 |
show_img: bool = True,
|
|
|
|
|
|
|
| 561 |
) -> List[Dict[str, object]]:
|
| 562 |
"""
|
| 563 |
-
|
| 564 |
|
| 565 |
-
|
| 566 |
-
- overlay
|
| 567 |
-
- (
|
| 568 |
|
| 569 |
-
|
| 570 |
----------
|
| 571 |
-
meta : DataFrame
|
| 572 |
features : CSR activations with global id per column
|
| 573 |
-
image_indices :
|
| 574 |
-
image_paths :
|
| 575 |
feature_ids : global SAE feature ids to visualize
|
| 576 |
-
patches_per_image:
|
| 577 |
-
crop_size :
|
| 578 |
-
img_size_inches :
|
| 579 |
-
|
| 580 |
-
save_dir :
|
| 581 |
-
file_prefix :
|
| 582 |
-
show_img :
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
-------
|
| 586 |
-
List[Dict[str, object]] —
|
| 587 |
"""
|
| 588 |
assert len(image_indices) == len(image_paths), \
|
| 589 |
-
"image_indices
|
| 590 |
|
| 591 |
codes = features.codes
|
| 592 |
inner_dim = codes.shape[1]
|
|
@@ -596,8 +662,8 @@ def visualize_feature_heatmaps(
|
|
| 596 |
|
| 597 |
spatial = int(math.isqrt(patches_per_image))
|
| 598 |
assert spatial * spatial == patches_per_image, (
|
| 599 |
-
f"
|
| 600 |
-
f"
|
| 601 |
)
|
| 602 |
|
| 603 |
preprocess = transforms.Compose(
|
|
@@ -618,11 +684,13 @@ def visualize_feature_heatmaps(
|
|
| 618 |
patches_per_image=patches_per_image,
|
| 619 |
crop_size=crop_size,
|
| 620 |
img_size_inches=img_size_inches,
|
| 621 |
-
|
| 622 |
save_dir=save_dir,
|
| 623 |
file_prefix=file_prefix,
|
| 624 |
show_img=show_img,
|
| 625 |
preprocess=preprocess,
|
|
|
|
|
|
|
| 626 |
)
|
| 627 |
|
| 628 |
|
|
@@ -636,12 +704,14 @@ def visualize_feature_heatmaps_from_images(
|
|
| 636 |
patches_per_image: Optional[int] = None,
|
| 637 |
crop_size: int = 224,
|
| 638 |
img_size_inches: float = 4.0,
|
| 639 |
-
|
| 640 |
save_dir: Optional[str] = None,
|
| 641 |
file_prefix: str = '',
|
| 642 |
show_img: bool = True,
|
| 643 |
*,
|
| 644 |
preprocess: Optional[object] = None,
|
|
|
|
|
|
|
| 645 |
) -> List[Dict[str, str]]:
|
| 646 |
"""Same as `visualize_feature_heatmaps`, but accepts already opened PIL images.
|
| 647 |
|
|
@@ -659,8 +729,8 @@ def visualize_feature_heatmaps_from_images(
|
|
| 659 |
|
| 660 |
spatial = int(math.isqrt(patches_per_image))
|
| 661 |
assert spatial * spatial == patches_per_image, (
|
| 662 |
-
f"
|
| 663 |
-
f"
|
| 664 |
)
|
| 665 |
|
| 666 |
if preprocess is None:
|
|
@@ -677,9 +747,10 @@ def visualize_feature_heatmaps_from_images(
|
|
| 677 |
meta_rows = [meta_lookup.get(int(img_idx)) for img_idx in image_indices]
|
| 678 |
|
| 679 |
orig_tensors: List[Optional[torch.Tensor]] = []
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
|
|
|
| 683 |
for img_name, meta_row in zip(image_names, meta_rows):
|
| 684 |
orig_path = _get_original_path(img_name) or _meta_path_value(meta_row, 'original_img_path')
|
| 685 |
if orig_path and Path(orig_path).exists():
|
|
@@ -687,10 +758,21 @@ def visualize_feature_heatmaps_from_images(
|
|
| 687 |
else:
|
| 688 |
orig_tensors.append(None)
|
| 689 |
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
|
| 695 |
image_idx_arr = meta['image_idx'].values
|
| 696 |
codes_list = []
|
|
@@ -728,43 +810,44 @@ def visualize_feature_heatmaps_from_images(
|
|
| 728 |
feature_id=int(global_feature_id),
|
| 729 |
saved=saved_overlay,
|
| 730 |
)
|
| 731 |
-
if
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
orig_tensors,
|
| 737 |
global_feature_id,
|
| 738 |
image_names,
|
| 739 |
-
|
|
|
|
| 740 |
meta_rows=meta_rows,
|
| 741 |
-
|
|
|
|
| 742 |
show_img=show_img,
|
| 743 |
)
|
| 744 |
_append_heatmap_artifact(
|
| 745 |
artifacts,
|
| 746 |
-
kind='
|
| 747 |
feature_id=int(global_feature_id),
|
| 748 |
-
saved=
|
| 749 |
)
|
| 750 |
-
elif
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
|
|
|
| 755 |
global_feature_id,
|
| 756 |
image_names,
|
|
|
|
| 757 |
meta_rows=meta_rows,
|
| 758 |
-
|
| 759 |
-
crop_size=crop_size,
|
| 760 |
-
save_path=mask_path,
|
| 761 |
show_img=show_img,
|
| 762 |
)
|
| 763 |
_append_heatmap_artifact(
|
| 764 |
artifacts,
|
| 765 |
-
kind='
|
| 766 |
feature_id=int(global_feature_id),
|
| 767 |
-
saved=
|
| 768 |
)
|
| 769 |
|
| 770 |
return artifacts
|
|
|
|
| 1 |
"""
|
| 2 |
+
Visualization of SAE feature heatmaps overlaid on KADID-10k images.
|
| 3 |
"""
|
| 4 |
|
| 5 |
import io
|
|
|
|
| 27 |
QGROUND_DISTORTION_TYPES,
|
| 28 |
SRGROUND_DISTORTION_TYPES,
|
| 29 |
SRGROUND_LEGEND_LABELS,
|
| 30 |
+
annotation_mask_rgb_for_meta_row,
|
| 31 |
available_distortions,
|
| 32 |
distortion_types_mapping,
|
| 33 |
+
get_srground_rgb_mask,
|
| 34 |
+
infer_spatial_mask_dataset,
|
|
|
|
| 35 |
)
|
| 36 |
|
| 37 |
# Show per-image filenames (and distortion subtitles where enabled) under heatmap panels.
|
| 38 |
SHOW_HEATMAP_IMAGE_TITLES = False
|
| 39 |
|
| 40 |
+
MASK_LEGEND_FONTSIZE = 14
|
| 41 |
MASK_LEGEND_HANDLE_HEIGHT = 1.15
|
| 42 |
MASK_LEGEND_HANDLE_LENGTH = 1.55
|
| 43 |
|
| 44 |
|
| 45 |
def _get_distortion_info(img_path: str):
|
| 46 |
"""
|
| 47 |
+
From a KADID filename (I04_07_05.png), return
|
| 48 |
+
(distortion_name, distortion_group) or (None, None) for originals.
|
| 49 |
"""
|
| 50 |
name = Path(img_path).name
|
| 51 |
m = _re.match(r'I\d+_(\d+)_(\d+)\.png$', name, _re.IGNORECASE)
|
|
|
|
| 59 |
|
| 60 |
def _get_original_path(distorted_path: str) -> Optional[str]:
|
| 61 |
"""
|
| 62 |
+
From a KADID distorted image path (I04_07_05.png),
|
| 63 |
+
return the path to the original (I04.png).
|
| 64 |
"""
|
| 65 |
p = Path(distorted_path)
|
| 66 |
match = _re.match(r'(I\d+)_\d+_\d+\.png$', p.name, _re.IGNORECASE)
|
|
|
|
| 207 |
title_feature_id: int | None = None,
|
| 208 |
) -> FigureSaveResult:
|
| 209 |
"""
|
| 210 |
+
Draw a grid of overlay heatmaps for a single feature_id.
|
| 211 |
|
| 212 |
+
Parameters
|
| 213 |
----------
|
| 214 |
+
imgs_list : list of image tensors (C, H, W)
|
| 215 |
+
codes_tensor : activation tensor (B, spatial, spatial, inner_dim)
|
| 216 |
+
feature_id : SAE feature index
|
| 217 |
+
image_paths : paths to PNG files (for titles)
|
| 218 |
+
img_size_inches: size of one cell in inches
|
| 219 |
"""
|
| 220 |
B = len(imgs_list)
|
| 221 |
inner_concepts = codes_tensor.shape[-1]
|
|
|
|
| 268 |
show_img: bool = True,
|
| 269 |
) -> FigureSaveResult:
|
| 270 |
"""
|
| 271 |
+
Draw a grid of |distorted − original| for a single feature_id.
|
| 272 |
|
| 273 |
+
Parameters
|
| 274 |
----------
|
| 275 |
+
imgs_list : list of distorted image tensors (C, H, W)
|
| 276 |
+
orig_tensors : list of original tensors (or None if the file is missing)
|
| 277 |
+
feature_id : SAE feature index (for the title)
|
| 278 |
+
image_paths : paths to PNG files (for titles)
|
| 279 |
+
img_size_inches: size of one cell in inches
|
| 280 |
"""
|
| 281 |
B = len(imgs_list)
|
| 282 |
fig_diff, axes = plt.subplots(1, B, figsize=(img_size_inches * B, img_size_inches * 1.25))
|
|
|
|
| 302 |
return _finalize_figure(fig_diff, save_path=save_path, show_img=show_img)
|
| 303 |
|
| 304 |
|
| 305 |
+
def annotation_mask_legend_handles(
|
| 306 |
+
dataset: str,
|
| 307 |
+
*,
|
| 308 |
+
mask_rgb_list: Optional[List[Optional[np.ndarray]]] = None,
|
| 309 |
+
) -> list[Patch]:
|
| 310 |
+
if dataset == 'SRGround':
|
| 311 |
+
return srground_legend_handles(mask_rgb_list)
|
| 312 |
+
if dataset == 'QGround':
|
| 313 |
+
handles = [Patch(facecolor='black', edgecolor='none', label='background')]
|
| 314 |
+
for label_name, rgb in QGROUND_DISTORTION_TYPES.items():
|
| 315 |
+
color = tuple((np.asarray(rgb, dtype=np.float32) / 255.0).tolist())
|
| 316 |
+
handles.append(Patch(facecolor=color, edgecolor='none', label=label_name))
|
| 317 |
+
return handles
|
| 318 |
+
return [Patch(facecolor='black', edgecolor='none', label='background')]
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def _plot_annotation_mask_grid(
|
| 322 |
+
mask_rgb_list: List[Optional[np.ndarray]],
|
| 323 |
feature_id: int,
|
| 324 |
image_paths: List[str],
|
| 325 |
+
*,
|
| 326 |
+
dataset: str,
|
| 327 |
crop_size: int,
|
| 328 |
img_size_inches: float = 4.0,
|
| 329 |
meta_rows: Optional[List[Optional[pd.Series]]] = None,
|
| 330 |
save_path: Optional[str] = None,
|
| 331 |
show_img: bool = True,
|
| 332 |
) -> FigureSaveResult:
|
| 333 |
+
"""Plot GT annotation masks (QGround PNG or SRGround synthesized RGB) in one row."""
|
| 334 |
+
B = len(mask_rgb_list)
|
| 335 |
fig, axes = plt.subplots(1, B, figsize=(img_size_inches * B, img_size_inches * 1.35))
|
| 336 |
if B == 1:
|
| 337 |
axes = [axes]
|
| 338 |
if meta_rows is None:
|
| 339 |
meta_rows = [None] * B
|
| 340 |
|
| 341 |
+
for ax, mask_rgb, img_path, meta_row in zip(axes, mask_rgb_list, image_paths, meta_rows):
|
| 342 |
+
if mask_rgb is not None:
|
|
|
|
| 343 |
ax.imshow(mask_rgb)
|
| 344 |
height, width = mask_rgb.shape[:2]
|
| 345 |
+
rect_size = float(crop_size)
|
| 346 |
+
rect_x = (width - rect_size) / 2.0
|
| 347 |
+
rect_y = (height - rect_size) / 2.0
|
|
|
|
| 348 |
ax.add_patch(
|
| 349 |
Rectangle(
|
| 350 |
(rect_x, rect_y),
|
| 351 |
+
rect_size,
|
| 352 |
+
rect_size,
|
| 353 |
fill=False,
|
| 354 |
edgecolor='black',
|
| 355 |
linewidth=2.0,
|
|
|
|
| 358 |
)
|
| 359 |
_set_image_subplot_title(ax, img_path, meta_row)
|
| 360 |
else:
|
| 361 |
+
ax.text(
|
| 362 |
+
0.5,
|
| 363 |
+
0.5,
|
| 364 |
+
f'{dataset} annotation mask not found',
|
| 365 |
+
ha='center',
|
| 366 |
+
va='center',
|
| 367 |
+
transform=ax.transAxes,
|
| 368 |
+
fontsize=9,
|
| 369 |
+
)
|
| 370 |
ax.axis('off')
|
| 371 |
|
| 372 |
+
_add_mask_legend(
|
| 373 |
+
fig,
|
| 374 |
+
annotation_mask_legend_handles(dataset, mask_rgb_list=mask_rgb_list),
|
| 375 |
+
)
|
| 376 |
+
fig.suptitle(f'{dataset} annotation mask (feature {feature_id})', fontsize=12, x=0.5, ha='center')
|
|
|
|
|
|
|
| 377 |
fig.tight_layout(rect=(0, 0.14, 1, 1))
|
| 378 |
return _finalize_figure(fig, save_path=save_path, show_img=show_img)
|
| 379 |
|
| 380 |
|
| 381 |
+
def _plot_qground_mask_grid(
|
| 382 |
+
mask_paths: List[Optional[str]],
|
| 383 |
+
feature_id: int,
|
| 384 |
+
image_paths: List[str],
|
| 385 |
+
crop_size: int,
|
| 386 |
+
img_size_inches: float = 4.0,
|
| 387 |
+
meta_rows: Optional[List[Optional[pd.Series]]] = None,
|
| 388 |
+
save_path: Optional[str] = None,
|
| 389 |
+
show_img: bool = True,
|
| 390 |
+
*,
|
| 391 |
+
dataset_root: str | None = None,
|
| 392 |
+
) -> FigureSaveResult:
|
| 393 |
+
"""Backward-compatible wrapper around :func:`_plot_annotation_mask_grid`."""
|
| 394 |
+
if meta_rows is None:
|
| 395 |
+
meta_rows = [None] * len(mask_paths)
|
| 396 |
+
if dataset_root is None:
|
| 397 |
+
raise ValueError('dataset_root is required for QGround mask visualization')
|
| 398 |
+
mask_rgb_list = [
|
| 399 |
+
annotation_mask_rgb_for_meta_row(
|
| 400 |
+
meta_row,
|
| 401 |
+
dataset_root=dataset_root,
|
| 402 |
+
dataset='QGround',
|
| 403 |
+
)
|
| 404 |
+
for meta_row in meta_rows
|
| 405 |
+
]
|
| 406 |
+
return _plot_annotation_mask_grid(
|
| 407 |
+
mask_rgb_list,
|
| 408 |
+
feature_id,
|
| 409 |
+
image_paths,
|
| 410 |
+
dataset='QGround',
|
| 411 |
+
crop_size=crop_size,
|
| 412 |
+
img_size_inches=img_size_inches,
|
| 413 |
+
meta_rows=meta_rows,
|
| 414 |
+
save_path=save_path,
|
| 415 |
+
show_img=show_img,
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
def _add_mask_legend(fig, handles: list[Patch], *, ncol: int = 3) -> None:
|
| 420 |
fig.legend(
|
| 421 |
handles=handles,
|
|
|
|
| 431 |
)
|
| 432 |
|
| 433 |
|
| 434 |
+
def srground_legend_handles(
|
| 435 |
+
mask_rgb_list: Optional[List[Optional[np.ndarray]]] = None,
|
| 436 |
+
) -> list[Patch]:
|
| 437 |
+
"""Legend entries for distortion classes present in plotted SRGround RGB masks."""
|
| 438 |
+
present: set[str] | None = None
|
| 439 |
+
if mask_rgb_list is not None:
|
| 440 |
+
present = set()
|
| 441 |
+
for mask_rgb in mask_rgb_list:
|
| 442 |
+
if mask_rgb is None:
|
| 443 |
+
continue
|
| 444 |
+
for dist_name, rgb in SRGROUND_DISTORTION_TYPES.items():
|
| 445 |
+
if np.isclose(mask_rgb, rgb, rtol=0.2, atol=20).any():
|
| 446 |
+
present.add(dist_name)
|
| 447 |
+
|
| 448 |
handles = [Patch(facecolor='black', edgecolor='none', label='background')]
|
| 449 |
for dist_name, rgb in SRGROUND_DISTORTION_TYPES.items():
|
| 450 |
+
if present is not None and dist_name not in present:
|
| 451 |
continue
|
| 452 |
color = tuple((np.asarray(rgb, dtype=np.float32) / 255.0).tolist())
|
| 453 |
label = SRGROUND_LEGEND_LABELS.get(dist_name, dist_name)
|
|
|
|
| 465 |
meta_rows: Optional[List[Optional[pd.Series]]],
|
| 466 |
*,
|
| 467 |
title_feature_id: int,
|
|
|
|
| 468 |
crop_size: int,
|
| 469 |
save_path: Optional[str] = None,
|
| 470 |
show_img: bool = False,
|
|
|
|
| 540 |
|
| 541 |
_add_mask_legend(
|
| 542 |
fig,
|
| 543 |
+
srground_legend_handles(mask_rgb_list),
|
| 544 |
)
|
| 545 |
fig.suptitle(f'Feature {display_id}', fontsize=13, x=0.5, ha='center')
|
| 546 |
fig.tight_layout(rect=(0, 0.16, 1, 0.96))
|
|
|
|
| 555 |
feature_id: int,
|
| 556 |
*,
|
| 557 |
patches_per_image: Optional[int] = None,
|
| 558 |
+
crop_size: int = 512,
|
| 559 |
img_size_inches: float = 3.5,
|
| 560 |
+
dataset_root: str,
|
|
|
|
| 561 |
) -> bytes | None:
|
| 562 |
"""Two-row panel: SAE heatmap overlays and SRGround annotation masks with legend."""
|
| 563 |
+
crop_size = 512 # TODO: fix later
|
| 564 |
pil_images = [Image.open(p).convert('RGB') for p in image_paths]
|
| 565 |
preprocess = transforms.Compose(
|
| 566 |
[
|
|
|
|
| 583 |
|
| 584 |
meta_lookup = _build_meta_lookup(meta)
|
| 585 |
meta_rows = [meta_lookup.get(int(img_idx)) for img_idx in image_indices]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
mask_rgb_list = [
|
| 587 |
+
get_srground_rgb_mask(
|
| 588 |
row,
|
| 589 |
+
dataset_root=dataset_root,
|
|
|
|
| 590 |
crop_size=crop_size,
|
|
|
|
| 591 |
)
|
| 592 |
+
for row in meta_rows
|
| 593 |
]
|
| 594 |
|
| 595 |
matrix_col = features.column_for(int(feature_id))
|
|
|
|
| 602 |
img_size_inches,
|
| 603 |
meta_rows,
|
| 604 |
title_feature_id=int(feature_id),
|
|
|
|
| 605 |
crop_size=crop_size,
|
| 606 |
save_path=None,
|
| 607 |
show_img=False,
|
|
|
|
| 618 |
patches_per_image: Optional[int] = None,
|
| 619 |
crop_size: int = 224,
|
| 620 |
img_size_inches: float = 4.0,
|
| 621 |
+
show_mask: bool = True,
|
| 622 |
save_dir: Optional[str] = None,
|
| 623 |
file_prefix: str = '',
|
| 624 |
show_img: bool = True,
|
| 625 |
+
dataset_root: Optional[str] = None,
|
| 626 |
+
dataset: Optional[str] = None,
|
| 627 |
) -> List[Dict[str, object]]:
|
| 628 |
"""
|
| 629 |
+
Visualize SAE feature heatmaps from precomputed sparse activations.
|
| 630 |
|
| 631 |
+
For each feature_id:
|
| 632 |
+
- overlay heatmap on the distorted image
|
| 633 |
+
- (optional) second row: GT mask (QGround/SRGround) or |distorted − original|
|
| 634 |
|
| 635 |
+
Parameters
|
| 636 |
----------
|
| 637 |
+
meta : metadata DataFrame (image_idx, patch_idx, ...)
|
| 638 |
features : CSR activations with global id per column
|
| 639 |
+
image_indices : image indices (image_idx) to visualize
|
| 640 |
+
image_paths : paths to the corresponding PNG files
|
| 641 |
feature_ids : global SAE feature ids to visualize
|
| 642 |
+
patches_per_image: number of patches per image; if None — inferred from patch_idx
|
| 643 |
+
crop_size : crop size used during caching
|
| 644 |
+
img_size_inches : size of one image on the figure (in inches)
|
| 645 |
+
show_mask : second row — GT mask or |distorted − original| if no mask is available
|
| 646 |
+
save_dir : directory for saving PNGs; if None — PNGs in memory (``bytes`` key)
|
| 647 |
+
file_prefix : filename prefix (e.g. stage/agg)
|
| 648 |
+
show_img : whether to display figures inline
|
| 649 |
+
|
| 650 |
+
Returns
|
| 651 |
+
-------
|
| 652 |
+
List[Dict[str, object]] — artifacts with ``path`` (disk) or ``bytes`` (memory) key
|
| 653 |
"""
|
| 654 |
assert len(image_indices) == len(image_paths), \
|
| 655 |
+
"image_indices and image_paths must have the same length"
|
| 656 |
|
| 657 |
codes = features.codes
|
| 658 |
inner_dim = codes.shape[1]
|
|
|
|
| 662 |
|
| 663 |
spatial = int(math.isqrt(patches_per_image))
|
| 664 |
assert spatial * spatial == patches_per_image, (
|
| 665 |
+
f"Number of patches {patches_per_image} is not a perfect square — "
|
| 666 |
+
f"check crop_size or model layer"
|
| 667 |
)
|
| 668 |
|
| 669 |
preprocess = transforms.Compose(
|
|
|
|
| 684 |
patches_per_image=patches_per_image,
|
| 685 |
crop_size=crop_size,
|
| 686 |
img_size_inches=img_size_inches,
|
| 687 |
+
show_mask=show_mask,
|
| 688 |
save_dir=save_dir,
|
| 689 |
file_prefix=file_prefix,
|
| 690 |
show_img=show_img,
|
| 691 |
preprocess=preprocess,
|
| 692 |
+
dataset_root=dataset_root,
|
| 693 |
+
dataset=dataset,
|
| 694 |
)
|
| 695 |
|
| 696 |
|
|
|
|
| 704 |
patches_per_image: Optional[int] = None,
|
| 705 |
crop_size: int = 224,
|
| 706 |
img_size_inches: float = 4.0,
|
| 707 |
+
show_mask: bool = True,
|
| 708 |
save_dir: Optional[str] = None,
|
| 709 |
file_prefix: str = '',
|
| 710 |
show_img: bool = True,
|
| 711 |
*,
|
| 712 |
preprocess: Optional[object] = None,
|
| 713 |
+
dataset_root: Optional[str] = None,
|
| 714 |
+
dataset: Optional[str] = None,
|
| 715 |
) -> List[Dict[str, str]]:
|
| 716 |
"""Same as `visualize_feature_heatmaps`, but accepts already opened PIL images.
|
| 717 |
|
|
|
|
| 729 |
|
| 730 |
spatial = int(math.isqrt(patches_per_image))
|
| 731 |
assert spatial * spatial == patches_per_image, (
|
| 732 |
+
f"Number of patches {patches_per_image} is not a perfect square — "
|
| 733 |
+
f"check crop_size or model layer"
|
| 734 |
)
|
| 735 |
|
| 736 |
if preprocess is None:
|
|
|
|
| 747 |
meta_rows = [meta_lookup.get(int(img_idx)) for img_idx in image_indices]
|
| 748 |
|
| 749 |
orig_tensors: List[Optional[torch.Tensor]] = []
|
| 750 |
+
mask_rgb_list: List[Optional[np.ndarray]] = []
|
| 751 |
+
has_annotation_masks = False
|
| 752 |
+
mask_dataset = dataset
|
| 753 |
+
if show_mask:
|
| 754 |
for img_name, meta_row in zip(image_names, meta_rows):
|
| 755 |
orig_path = _get_original_path(img_name) or _meta_path_value(meta_row, 'original_img_path')
|
| 756 |
if orig_path and Path(orig_path).exists():
|
|
|
|
| 758 |
else:
|
| 759 |
orig_tensors.append(None)
|
| 760 |
|
| 761 |
+
row_dataset = mask_dataset or infer_spatial_mask_dataset(meta_row)
|
| 762 |
+
mask_rgb = None
|
| 763 |
+
if dataset_root is not None and row_dataset in {'QGround', 'SRGround'}:
|
| 764 |
+
mask_rgb = annotation_mask_rgb_for_meta_row(
|
| 765 |
+
meta_row,
|
| 766 |
+
dataset_root=dataset_root,
|
| 767 |
+
dataset=row_dataset,
|
| 768 |
+
crop_size=crop_size,
|
| 769 |
+
full_frame=True,
|
| 770 |
+
)
|
| 771 |
+
if mask_rgb is not None:
|
| 772 |
+
has_annotation_masks = True
|
| 773 |
+
mask_rgb_list.append(mask_rgb)
|
| 774 |
+
if mask_dataset is None and meta_rows:
|
| 775 |
+
mask_dataset = infer_spatial_mask_dataset(meta_rows[0]) or 'QGround'
|
| 776 |
|
| 777 |
image_idx_arr = meta['image_idx'].values
|
| 778 |
codes_list = []
|
|
|
|
| 810 |
feature_id=int(global_feature_id),
|
| 811 |
saved=saved_overlay,
|
| 812 |
)
|
| 813 |
+
if show_mask and has_annotation_masks:
|
| 814 |
+
mask_name = f'{safe_prefix}feature_{global_feature_id}_mask.png'
|
| 815 |
+
mask_save_path = str(save_root / mask_name) if save_root is not None else None
|
| 816 |
+
saved_mask = _plot_annotation_mask_grid(
|
| 817 |
+
mask_rgb_list,
|
|
|
|
| 818 |
global_feature_id,
|
| 819 |
image_names,
|
| 820 |
+
dataset=str(mask_dataset or 'QGround'),
|
| 821 |
+
crop_size=crop_size,
|
| 822 |
meta_rows=meta_rows,
|
| 823 |
+
img_size_inches=img_size_inches,
|
| 824 |
+
save_path=mask_save_path,
|
| 825 |
show_img=show_img,
|
| 826 |
)
|
| 827 |
_append_heatmap_artifact(
|
| 828 |
artifacts,
|
| 829 |
+
kind='mask',
|
| 830 |
feature_id=int(global_feature_id),
|
| 831 |
+
saved=saved_mask,
|
| 832 |
)
|
| 833 |
+
elif show_mask and any(t is not None for t in orig_tensors):
|
| 834 |
+
diff_name = f'{safe_prefix}feature_{global_feature_id}_diff.png'
|
| 835 |
+
diff_path = str(save_root / diff_name) if save_root is not None else None
|
| 836 |
+
saved_diff = _plot_diff_grid(
|
| 837 |
+
imgs_list,
|
| 838 |
+
orig_tensors,
|
| 839 |
global_feature_id,
|
| 840 |
image_names,
|
| 841 |
+
img_size_inches,
|
| 842 |
meta_rows=meta_rows,
|
| 843 |
+
save_path=diff_path,
|
|
|
|
|
|
|
| 844 |
show_img=show_img,
|
| 845 |
)
|
| 846 |
_append_heatmap_artifact(
|
| 847 |
artifacts,
|
| 848 |
+
kind='diff',
|
| 849 |
feature_id=int(global_feature_id),
|
| 850 |
+
saved=saved_diff,
|
| 851 |
)
|
| 852 |
|
| 853 |
return artifacts
|
analysis/viz/vis_scatter.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
-
Scatter
|
| 3 |
-
log(sparsity) × log(mean_acts),
|
| 4 |
"""
|
| 5 |
|
| 6 |
from pathlib import Path
|
|
@@ -56,20 +56,20 @@ def get_top_k_patches(
|
|
| 56 |
label_col: str = 'dist_group',
|
| 57 |
) -> Tuple[torch.Tensor, torch.Tensor, List[str]]:
|
| 58 |
"""
|
| 59 |
-
|
| 60 |
|
| 61 |
-
|
| 62 |
----------
|
| 63 |
-
codes : CSR
|
| 64 |
-
meta : DataFrame
|
| 65 |
-
top_k :
|
| 66 |
-
label_col : 'dist_group'
|
| 67 |
|
| 68 |
-
|
| 69 |
-
-------
|
| 70 |
top_val : FloatTensor (n_features, effective_k)
|
| 71 |
top_label : LongTensor (n_features, effective_k)
|
| 72 |
-
label_names : List[str] —
|
| 73 |
"""
|
| 74 |
n_patches, n_features = codes.shape
|
| 75 |
|
|
@@ -118,19 +118,19 @@ def prepare_scatter_stats(
|
|
| 118 |
group_filter: Optional[str] = None,
|
| 119 |
) -> Tuple[Dict[str, torch.Tensor], List[str]]:
|
| 120 |
"""
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
Параметры
|
| 124 |
-
----------
|
| 125 |
-
codes : CSR матрица (n_patches, n_features)
|
| 126 |
-
meta : DataFrame с метаданными
|
| 127 |
-
top_k : число top-патчей для расчёта энтропии
|
| 128 |
-
label_col : 'dist_group' или 'dist_type'
|
| 129 |
-
group_filter : если задана — фильтрует строки по dist_group == group_filter
|
| 130 |
|
| 131 |
-
|
| 132 |
----------
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
label_names : List[str]
|
| 135 |
"""
|
| 136 |
if group_filter is not None:
|
|
@@ -198,10 +198,10 @@ def _compute_color_metric_values(color_metric: str,
|
|
| 198 |
*,
|
| 199 |
dataset: str) -> torch.Tensor:
|
| 200 |
'''
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
'''
|
| 206 |
color_metric = str(color_metric).strip().lower()
|
| 207 |
if color_metric == 'entropy':
|
|
|
|
| 1 |
"""
|
| 2 |
+
Scatter plot of SAE feature activations; some functions are also from PatchSAE:
|
| 3 |
+
log(sparsity) × log(mean_acts), colored by distortion-label entropy.
|
| 4 |
"""
|
| 5 |
|
| 6 |
from pathlib import Path
|
|
|
|
| 56 |
label_col: str = 'dist_group',
|
| 57 |
) -> Tuple[torch.Tensor, torch.Tensor, List[str]]:
|
| 58 |
"""
|
| 59 |
+
For each SAE feature, select the top-K patches with the largest activations.
|
| 60 |
|
| 61 |
+
Parameters
|
| 62 |
----------
|
| 63 |
+
codes : CSR matrix (n_patches, n_features)
|
| 64 |
+
meta : DataFrame with a label_col column
|
| 65 |
+
top_k : number of patches; None — all nonzero (padded to max nnz)
|
| 66 |
+
label_col : 'dist_group' or 'dist_type'
|
| 67 |
|
| 68 |
+
Returns
|
| 69 |
+
-------
|
| 70 |
top_val : FloatTensor (n_features, effective_k)
|
| 71 |
top_label : LongTensor (n_features, effective_k)
|
| 72 |
+
label_names : List[str] — mapping for numeric labels
|
| 73 |
"""
|
| 74 |
n_patches, n_features = codes.shape
|
| 75 |
|
|
|
|
| 118 |
group_filter: Optional[str] = None,
|
| 119 |
) -> Tuple[Dict[str, torch.Tensor], List[str]]:
|
| 120 |
"""
|
| 121 |
+
Build a stats dict compatible with get_stats_scatter_plot.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
+
Parameters
|
| 124 |
----------
|
| 125 |
+
codes : CSR matrix (n_patches, n_features)
|
| 126 |
+
meta : metadata DataFrame
|
| 127 |
+
top_k : number of top patches for entropy computation
|
| 128 |
+
label_col : 'dist_group' or 'dist_type'
|
| 129 |
+
group_filter : if set, filter rows by dist_group == group_filter
|
| 130 |
+
|
| 131 |
+
Returns
|
| 132 |
+
-------
|
| 133 |
+
stats : dict with keys 'sparsity', 'mean_acts', 'top_entropy' — FloatTensors
|
| 134 |
label_names : List[str]
|
| 135 |
"""
|
| 136 |
if group_filter is not None:
|
|
|
|
| 198 |
*,
|
| 199 |
dataset: str) -> torch.Tensor:
|
| 200 |
'''
|
| 201 |
+
Compute a metric vector for coloring scatter-plot points.
|
| 202 |
+
Supports 'entropy', 'roc_auc', 'iou', 'precision', and 'recall'.
|
| 203 |
+
For tabular metrics, returns a vector of length n_features,
|
| 204 |
+
aggregating values across categories from label_col.
|
| 205 |
'''
|
| 206 |
color_metric = str(color_metric).strip().lower()
|
| 207 |
if color_metric == 'entropy':
|
assets/style.css
CHANGED
|
@@ -712,16 +712,20 @@ body {
|
|
| 712 |
}
|
| 713 |
|
| 714 |
.feature-jump-input .Select-control,
|
|
|
|
| 715 |
.feature-jump-input input {
|
| 716 |
min-height: 42px;
|
| 717 |
}
|
| 718 |
|
| 719 |
/* Hide +/- steppers in feature id number input (browser-native spinners). */
|
|
|
|
| 720 |
.feature-jump-input input[type="number"] {
|
| 721 |
-moz-appearance: textfield;
|
| 722 |
appearance: textfield;
|
| 723 |
}
|
| 724 |
|
|
|
|
|
|
|
| 725 |
.feature-jump-input input[type="number"]::-webkit-outer-spin-button,
|
| 726 |
.feature-jump-input input[type="number"]::-webkit-inner-spin-button {
|
| 727 |
-webkit-appearance: none;
|
|
|
|
| 712 |
}
|
| 713 |
|
| 714 |
.feature-jump-input .Select-control,
|
| 715 |
+
input.feature-jump-input,
|
| 716 |
.feature-jump-input input {
|
| 717 |
min-height: 42px;
|
| 718 |
}
|
| 719 |
|
| 720 |
/* Hide +/- steppers in feature id number input (browser-native spinners). */
|
| 721 |
+
input.feature-jump-input[type="number"],
|
| 722 |
.feature-jump-input input[type="number"] {
|
| 723 |
-moz-appearance: textfield;
|
| 724 |
appearance: textfield;
|
| 725 |
}
|
| 726 |
|
| 727 |
+
input.feature-jump-input[type="number"]::-webkit-outer-spin-button,
|
| 728 |
+
input.feature-jump-input[type="number"]::-webkit-inner-spin-button,
|
| 729 |
.feature-jump-input input[type="number"]::-webkit-outer-spin-button,
|
| 730 |
.feature-jump-input input[type="number"]::-webkit-inner-spin-button {
|
| 731 |
-webkit-appearance: none;
|
dashboard/__init__.py
CHANGED
|
@@ -1,3 +1,13 @@
|
|
| 1 |
"""Dash application helpers for xIQA."""
|
| 2 |
|
| 3 |
-
from .model_catalog import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Dash application helpers for xIQA."""
|
| 2 |
|
| 3 |
+
from .model_catalog import (
|
| 4 |
+
MODEL_FAMILIES,
|
| 5 |
+
ModelRecord,
|
| 6 |
+
checkpoint_cache_dir,
|
| 7 |
+
checkpoint_dataset_cache_paths,
|
| 8 |
+
discover_models_for_metric,
|
| 9 |
+
discover_supported_datasets,
|
| 10 |
+
get_model_record,
|
| 11 |
+
require_model_record,
|
| 12 |
+
selector_cache_files_for_model,
|
| 13 |
+
)
|
dashboard/image_utils.py
CHANGED
|
@@ -7,7 +7,7 @@ from typing import List
|
|
| 7 |
from analysis.datasets import resolve_dataset_image_path
|
| 8 |
from analysis.utils import get_top_images_for_feature, get_top_images_for_feature_by_iou
|
| 9 |
from analysis.viz.vis_heatmaps import render_top_feature_panel_srground, visualize_feature_heatmaps
|
| 10 |
-
from dashboard.model_catalog import
|
| 11 |
|
| 12 |
ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
|
|
@@ -52,12 +52,12 @@ def get_top_feature_overlays(
|
|
| 52 |
"""
|
| 53 |
ranking_mode = str(ranking_mode or "iou")
|
| 54 |
|
| 55 |
-
record =
|
| 56 |
if record is None:
|
| 57 |
return []
|
| 58 |
|
| 59 |
try:
|
| 60 |
-
filtered = load_and_filter_model_activations(
|
| 61 |
except Exception as exc:
|
| 62 |
print(f"Error occurred while loading activations for {selection_dataset!r}: {exc}")
|
| 63 |
return []
|
|
@@ -92,7 +92,6 @@ def get_top_feature_overlays(
|
|
| 92 |
return []
|
| 93 |
|
| 94 |
first_rows = meta.groupby("image_idx", sort=False).first()
|
| 95 |
-
datasets_root = str(dataset_root) if dataset_root is not None else None
|
| 96 |
image_paths = []
|
| 97 |
for idx in top_image_idxs:
|
| 98 |
try:
|
|
@@ -115,7 +114,7 @@ def get_top_feature_overlays(
|
|
| 115 |
resolved = resolve_dataset_image_path(
|
| 116 |
selection_dataset,
|
| 117 |
img_path,
|
| 118 |
-
datasets_root=
|
| 119 |
)
|
| 120 |
image_paths.append(str(resolved))
|
| 121 |
|
|
@@ -135,8 +134,7 @@ def get_top_feature_overlays(
|
|
| 135 |
patches_per_image=None,
|
| 136 |
crop_size=224,
|
| 137 |
img_size_inches=3.5,
|
| 138 |
-
|
| 139 |
-
datasets_root=str(dataset_root) if dataset_root is not None else None,
|
| 140 |
)
|
| 141 |
if panel_bytes is None:
|
| 142 |
return []
|
|
@@ -158,7 +156,7 @@ def get_top_feature_overlays(
|
|
| 158 |
patches_per_image=None,
|
| 159 |
crop_size=224,
|
| 160 |
img_size_inches=3.5,
|
| 161 |
-
|
| 162 |
save_dir=None,
|
| 163 |
file_prefix="topmax",
|
| 164 |
show_img=False,
|
|
|
|
| 7 |
from analysis.datasets import resolve_dataset_image_path
|
| 8 |
from analysis.utils import get_top_images_for_feature, get_top_images_for_feature_by_iou
|
| 9 |
from analysis.viz.vis_heatmaps import render_top_feature_panel_srground, visualize_feature_heatmaps
|
| 10 |
+
from dashboard.model_catalog import get_model_record, load_and_filter_model_activations
|
| 11 |
|
| 12 |
ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
|
|
|
|
| 52 |
"""
|
| 53 |
ranking_mode = str(ranking_mode or "iou")
|
| 54 |
|
| 55 |
+
record = get_model_record(metric, model_key)
|
| 56 |
if record is None:
|
| 57 |
return []
|
| 58 |
|
| 59 |
try:
|
| 60 |
+
filtered = load_and_filter_model_activations(record.checkpoint_path, selection_dataset)
|
| 61 |
except Exception as exc:
|
| 62 |
print(f"Error occurred while loading activations for {selection_dataset!r}: {exc}")
|
| 63 |
return []
|
|
|
|
| 92 |
return []
|
| 93 |
|
| 94 |
first_rows = meta.groupby("image_idx", sort=False).first()
|
|
|
|
| 95 |
image_paths = []
|
| 96 |
for idx in top_image_idxs:
|
| 97 |
try:
|
|
|
|
| 114 |
resolved = resolve_dataset_image_path(
|
| 115 |
selection_dataset,
|
| 116 |
img_path,
|
| 117 |
+
datasets_root=str(dataset_root) if dataset_root is not None else None,
|
| 118 |
)
|
| 119 |
image_paths.append(str(resolved))
|
| 120 |
|
|
|
|
| 134 |
patches_per_image=None,
|
| 135 |
crop_size=224,
|
| 136 |
img_size_inches=3.5,
|
| 137 |
+
dataset_root=dataset_root,
|
|
|
|
| 138 |
)
|
| 139 |
if panel_bytes is None:
|
| 140 |
return []
|
|
|
|
| 156 |
patches_per_image=None,
|
| 157 |
crop_size=224,
|
| 158 |
img_size_inches=3.5,
|
| 159 |
+
show_mask=False,
|
| 160 |
save_dir=None,
|
| 161 |
file_prefix="topmax",
|
| 162 |
show_img=False,
|
dashboard/model_catalog.py
CHANGED
|
@@ -1,18 +1,25 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import ast
|
|
|
|
| 4 |
import os
|
| 5 |
import re
|
| 6 |
from dataclasses import dataclass
|
| 7 |
from pathlib import Path
|
| 8 |
-
from typing import Any, Iterable, Mapping
|
| 9 |
|
| 10 |
import numpy as np
|
| 11 |
from functools import lru_cache
|
| 12 |
import pandas as pd
|
| 13 |
import scipy.sparse as sp
|
| 14 |
|
| 15 |
-
from analysis.cache_utils import load_cache
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
from analysis.features.feature_filters import (
|
| 17 |
build_filter,
|
| 18 |
)
|
|
@@ -21,7 +28,7 @@ from analysis.features.feature_indexing import FeatureMatrix
|
|
| 21 |
|
| 22 |
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 23 |
DEFAULT_MODEL_STORAGE = PROJECT_ROOT / "logs"
|
| 24 |
-
MODEL_FAMILIES = ("
|
| 25 |
METRIC_DESCRIPTIONS: dict[str, tuple[str, ...]] = {
|
| 26 |
"MANIQA": (
|
| 27 |
"No-reference IQA with two Swin Transformer stages: stage 1 (768-d) and stage 2 (384-d). "
|
|
@@ -41,8 +48,6 @@ METRIC_DESCRIPTIONS: dict[str, tuple[str, ...]] = {
|
|
| 41 |
"l0 is the validation mean count of active SAE features per image (higher = less sparse).",
|
| 42 |
),
|
| 43 |
}
|
| 44 |
-
SUPPORTED_DATASETS = ("SRGround", "local_kadid", "kadid10k")
|
| 45 |
-
|
| 46 |
_FAMILY_ROOT_CANDIDATES = {
|
| 47 |
"MANIQA": ("maniqa_logs", "MANIQA_logs", "maniqa"),
|
| 48 |
"ARNIQA": ("arniqa_logs", "ARNIQA_logs", "arniqa"),
|
|
@@ -63,7 +68,8 @@ _MODEL_DIR_PATTERN = re.compile(
|
|
| 63 |
class ModelRecord:
|
| 64 |
family: str
|
| 65 |
model_name: str
|
| 66 |
-
model_path: str
|
|
|
|
| 67 |
experiment_num: int
|
| 68 |
layer_num: int | None
|
| 69 |
lambda_token: str | None
|
|
@@ -234,10 +240,27 @@ def discover_models_for_metric(metric: str, model_storage: Path | None = None) -
|
|
| 234 |
)
|
| 235 |
|
| 236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
@lru_cache(maxsize=32)
|
| 238 |
def _discover_models_for_metric_cached(metric_name: str, storage_root_str: str) -> tuple[ModelRecord, ...]:
|
| 239 |
storage_root = Path(storage_root_str)
|
| 240 |
family_root = resolve_family_root(metric_name, model_storage=storage_root)
|
|
|
|
| 241 |
if not family_root.exists():
|
| 242 |
return ()
|
| 243 |
|
|
@@ -259,11 +282,16 @@ def _discover_models_for_metric_cached(metric_name: str, storage_root_str: str)
|
|
| 259 |
except Exception:
|
| 260 |
l0_loss_value = None
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
records.append(
|
| 263 |
ModelRecord(
|
| 264 |
family=metric_name,
|
| 265 |
model_name=str(model_dir.name),
|
| 266 |
-
model_path=str(model_dir),
|
|
|
|
| 267 |
experiment_num=int(parsed_name["experiment_num"] or -1),
|
| 268 |
layer_num=parsed_name["layer_num"],
|
| 269 |
lambda_token=parsed_name["lambda_token"],
|
|
@@ -276,7 +304,33 @@ def _discover_models_for_metric_cached(metric_name: str, storage_root_str: str)
|
|
| 276 |
|
| 277 |
records.sort(key=lambda r: (r.experiment_num, r.model_name), reverse=True)
|
| 278 |
|
| 279 |
-
return tuple(records)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
|
| 281 |
|
| 282 |
def build_model_option_label(record: ModelRecord) -> str:
|
|
@@ -297,25 +351,38 @@ def summarize_model_record(record: ModelRecord) -> str:
|
|
| 297 |
return "\n • ".join(params_split)
|
| 298 |
|
| 299 |
|
| 300 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
dataset_name = str(dataset).strip()
|
| 302 |
if not dataset_name:
|
| 303 |
return {}
|
| 304 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
cache_files: dict[str, Path] = {}
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
continue
|
| 312 |
-
for parquet_path in sorted(cache_dir.glob("*.parquet")):
|
| 313 |
-
stem = parquet_path.stem
|
| 314 |
-
if patch_only and "_patch" not in stem:
|
| 315 |
-
# skip non-patch tables for these datasets
|
| 316 |
-
continue
|
| 317 |
-
cache_files.setdefault(stem, parquet_path)
|
| 318 |
-
|
| 319 |
return cache_files
|
| 320 |
|
| 321 |
|
|
@@ -326,7 +393,7 @@ def summarize_selector_cache_file(cache_path: Path) -> tuple[float | None, float
|
|
| 326 |
aggregate those per-feature scores across all features. Returns
|
| 327 |
(max, mean, variance, min) computed over per-feature scores.
|
| 328 |
"""
|
| 329 |
-
feature_table = load_selector_cache_feature_table(cache_path)
|
| 330 |
if feature_table.empty:
|
| 331 |
return None, None, None, None
|
| 332 |
|
|
@@ -338,7 +405,8 @@ def summarize_selector_cache_file(cache_path: Path) -> tuple[float | None, float
|
|
| 338 |
return float(arr.max()), float(arr.mean()), float(arr.var()), float(arr.min())
|
| 339 |
|
| 340 |
|
| 341 |
-
|
|
|
|
| 342 |
"""Return ordered per-feature selector scores with their feature ids.
|
| 343 |
|
| 344 |
The returned frame contains:
|
|
@@ -346,18 +414,12 @@ def load_selector_cache_feature_table(cache_path: Path) -> pd.DataFrame:
|
|
| 346 |
- feature_score: max absolute score for that feature column
|
| 347 |
- feature_rank: 1-based rank after sorting by score descending, then feature id ascending
|
| 348 |
"""
|
| 349 |
-
# Delegate to cached loader keyed by the cache path string to avoid
|
| 350 |
-
# repeated expensive parquet reads when users rapidly switch features.
|
| 351 |
-
return _load_selector_cache_feature_table_cached(str(cache_path))
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
@lru_cache(maxsize=256)
|
| 355 |
-
def _load_selector_cache_feature_table_cached(cache_path_str: str) -> pd.DataFrame:
|
| 356 |
columns = ["feature_id", "feature_score", "feature_rank"]
|
| 357 |
-
|
| 358 |
-
if not
|
| 359 |
return pd.DataFrame(columns=columns)
|
| 360 |
-
|
|
|
|
| 361 |
numeric = table.select_dtypes(include=["number"])
|
| 362 |
if numeric.empty:
|
| 363 |
return pd.DataFrame(columns=columns)
|
|
@@ -389,7 +451,7 @@ def load_selector_cache_values(cache_path: Path) -> np.ndarray:
|
|
| 389 |
|
| 390 |
Each value is the max absolute value for one numeric feature column.
|
| 391 |
"""
|
| 392 |
-
feature_table = load_selector_cache_feature_table(cache_path)
|
| 393 |
if feature_table.empty:
|
| 394 |
return np.array([], dtype=float)
|
| 395 |
|
|
@@ -397,41 +459,47 @@ def load_selector_cache_values(cache_path: Path) -> np.ndarray:
|
|
| 397 |
return values[np.isfinite(values)]
|
| 398 |
|
| 399 |
|
| 400 |
-
def summarize_selector_cache(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
if exclude_prefixes is None:
|
| 402 |
exclude_prefixes = list(DEFAULT_SELECTOR_HIDE_PREFIXES)
|
| 403 |
-
return _summarize_selector_cache_cached(str(
|
| 404 |
|
| 405 |
|
| 406 |
@lru_cache(maxsize=256)
|
| 407 |
def _summarize_selector_cache_cached(
|
| 408 |
-
|
| 409 |
dataset: str,
|
| 410 |
exclude_prefixes: tuple[str, ...],
|
| 411 |
) -> tuple[SelectorCacheSummary, ...]:
|
| 412 |
|
| 413 |
-
cache_files =
|
| 414 |
|
| 415 |
summaries: list[SelectorCacheSummary] = []
|
| 416 |
-
|
| 417 |
for selector_name in sorted(cache_files):
|
| 418 |
if any(selector_name.startswith(p) for p in exclude_prefixes):
|
| 419 |
continue
|
| 420 |
|
| 421 |
cache_path = cache_files[selector_name]
|
| 422 |
-
|
| 423 |
max_v, mean_v, var_v, min_v = summarize_selector_cache_file(cache_path)
|
| 424 |
|
| 425 |
summaries.append(
|
|
|
|
| 426 |
selector_name=selector_name,
|
| 427 |
-
cache_path=str(cache_path),
|
| 428 |
max_value=max_v,
|
| 429 |
mean_value=mean_v,
|
| 430 |
variance_value=var_v,
|
| 431 |
min_value=min_v,
|
| 432 |
)
|
|
|
|
| 433 |
return tuple(summaries)
|
| 434 |
|
|
|
|
| 435 |
def format_selector_cache_summary(summaries: list[SelectorCacheSummary]) -> list[str]:
|
| 436 |
if not summaries:
|
| 437 |
return ["No selector cache files were found for this model."]
|
|
@@ -451,54 +519,102 @@ def format_selector_cache_summary(summaries: list[SelectorCacheSummary]) -> list
|
|
| 451 |
return lines
|
| 452 |
|
| 453 |
|
| 454 |
-
def
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
|
| 503 |
meta_df, codes_csr, activation_steps_csr = load_cache(
|
| 504 |
str(cache_base_path),
|
|
@@ -506,8 +622,13 @@ def load_and_filter_model_activations(model_path: Path,
|
|
| 506 |
min_distortion_level=min_distortion_level,
|
| 507 |
max_distortion_level=max_distortion_level
|
| 508 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
|
| 510 |
-
feature_filter_cache_dir = cache_dir / '
|
| 511 |
feature_filter_summary: list[dict[str, Any]] = []
|
| 512 |
|
| 513 |
if not requested_filters:
|
|
@@ -578,7 +699,7 @@ def load_and_filter_model_activations(model_path: Path,
|
|
| 578 |
|
| 579 |
|
| 580 |
@lru_cache(maxsize=256)
|
| 581 |
-
def summarize_feature_filter_cache(
|
| 582 |
"""Return human-readable summary lines for feature filtering.
|
| 583 |
|
| 584 |
The model cache is loaded first, then configured feature filters are applied
|
|
@@ -587,7 +708,7 @@ def summarize_feature_filter_cache(model_path: Path, dataset: str) -> list[str]:
|
|
| 587 |
``load_and_filter_model_activations()`` for downstream use.
|
| 588 |
"""
|
| 589 |
try:
|
| 590 |
-
filtered_cache = load_and_filter_model_activations(
|
| 591 |
except Exception as exc:
|
| 592 |
return [f'Feature filtering unavailable: {exc}']
|
| 593 |
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import ast
|
| 4 |
+
import json
|
| 5 |
import os
|
| 6 |
import re
|
| 7 |
from dataclasses import dataclass
|
| 8 |
from pathlib import Path
|
| 9 |
+
from typing import Any, Iterable, Mapping, Sequence
|
| 10 |
|
| 11 |
import numpy as np
|
| 12 |
from functools import lru_cache
|
| 13 |
import pandas as pd
|
| 14 |
import scipy.sparse as sp
|
| 15 |
|
| 16 |
+
from analysis.cache_utils import load_cache, zero_codes_outside_activation_steps
|
| 17 |
+
from analysis.config import (
|
| 18 |
+
SUPPORTED_DATASETS,
|
| 19 |
+
DatasetCachePaths,
|
| 20 |
+
build_dataset_cache_paths,
|
| 21 |
+
load_sae_vis_config,
|
| 22 |
+
)
|
| 23 |
from analysis.features.feature_filters import (
|
| 24 |
build_filter,
|
| 25 |
)
|
|
|
|
| 28 |
|
| 29 |
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 30 |
DEFAULT_MODEL_STORAGE = PROJECT_ROOT / "logs"
|
| 31 |
+
MODEL_FAMILIES = ("ARNIQA", "MANIQA", "LIQE")
|
| 32 |
METRIC_DESCRIPTIONS: dict[str, tuple[str, ...]] = {
|
| 33 |
"MANIQA": (
|
| 34 |
"No-reference IQA with two Swin Transformer stages: stage 1 (768-d) and stage 2 (384-d). "
|
|
|
|
| 48 |
"l0 is the validation mean count of active SAE features per image (higher = less sparse).",
|
| 49 |
),
|
| 50 |
}
|
|
|
|
|
|
|
| 51 |
_FAMILY_ROOT_CANDIDATES = {
|
| 52 |
"MANIQA": ("maniqa_logs", "MANIQA_logs", "maniqa"),
|
| 53 |
"ARNIQA": ("arniqa_logs", "ARNIQA_logs", "arniqa"),
|
|
|
|
| 68 |
class ModelRecord:
|
| 69 |
family: str
|
| 70 |
model_name: str
|
| 71 |
+
model_path: str
|
| 72 |
+
checkpoint_path: str
|
| 73 |
experiment_num: int
|
| 74 |
layer_num: int | None
|
| 75 |
lambda_token: str | None
|
|
|
|
| 240 |
)
|
| 241 |
|
| 242 |
|
| 243 |
+
def _latest_checkpoint_dir(model_root: Path) -> Path | None:
|
| 244 |
+
candidates: list[tuple[int, Path]] = []
|
| 245 |
+
for entry in model_root.iterdir():
|
| 246 |
+
if not entry.is_dir() or not entry.name.startswith('checkpoint-'):
|
| 247 |
+
continue
|
| 248 |
+
try:
|
| 249 |
+
step = int(entry.name.removeprefix('checkpoint-'))
|
| 250 |
+
except ValueError:
|
| 251 |
+
continue
|
| 252 |
+
candidates.append((step, entry))
|
| 253 |
+
if not candidates:
|
| 254 |
+
return None
|
| 255 |
+
candidates.sort(key=lambda item: item[0])
|
| 256 |
+
return candidates[-1][1]
|
| 257 |
+
|
| 258 |
+
|
| 259 |
@lru_cache(maxsize=32)
|
| 260 |
def _discover_models_for_metric_cached(metric_name: str, storage_root_str: str) -> tuple[ModelRecord, ...]:
|
| 261 |
storage_root = Path(storage_root_str)
|
| 262 |
family_root = resolve_family_root(metric_name, model_storage=storage_root)
|
| 263 |
+
|
| 264 |
if not family_root.exists():
|
| 265 |
return ()
|
| 266 |
|
|
|
|
| 282 |
except Exception:
|
| 283 |
l0_loss_value = None
|
| 284 |
|
| 285 |
+
checkpoint_dir = _latest_checkpoint_dir(model_dir)
|
| 286 |
+
if checkpoint_dir is None:
|
| 287 |
+
continue
|
| 288 |
+
|
| 289 |
records.append(
|
| 290 |
ModelRecord(
|
| 291 |
family=metric_name,
|
| 292 |
model_name=str(model_dir.name),
|
| 293 |
+
model_path=str(model_dir),
|
| 294 |
+
checkpoint_path=str(checkpoint_dir.resolve()),
|
| 295 |
experiment_num=int(parsed_name["experiment_num"] or -1),
|
| 296 |
layer_num=parsed_name["layer_num"],
|
| 297 |
lambda_token=parsed_name["lambda_token"],
|
|
|
|
| 304 |
|
| 305 |
records.sort(key=lambda r: (r.experiment_num, r.model_name), reverse=True)
|
| 306 |
|
| 307 |
+
return tuple(records)
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def get_model_record(
|
| 311 |
+
metric: str,
|
| 312 |
+
model_key: str,
|
| 313 |
+
model_storage: Path | None = None,
|
| 314 |
+
) -> ModelRecord | None:
|
| 315 |
+
return next(
|
| 316 |
+
(
|
| 317 |
+
record
|
| 318 |
+
for record in discover_models_for_metric(metric, model_storage)
|
| 319 |
+
if record.model_key == model_key
|
| 320 |
+
),
|
| 321 |
+
None,
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def require_model_record(
|
| 326 |
+
metric: str,
|
| 327 |
+
model_key: str,
|
| 328 |
+
model_storage: Path | None = None,
|
| 329 |
+
) -> ModelRecord:
|
| 330 |
+
record = get_model_record(metric, model_key, model_storage)
|
| 331 |
+
if record is None:
|
| 332 |
+
raise FileNotFoundError(f'Model {model_key!r} not found for metric {metric!r}')
|
| 333 |
+
return record
|
| 334 |
|
| 335 |
|
| 336 |
def build_model_option_label(record: ModelRecord) -> str:
|
|
|
|
| 351 |
return "\n • ".join(params_split)
|
| 352 |
|
| 353 |
|
| 354 |
+
def checkpoint_cache_dir(checkpoint_path: str | Path) -> Path:
|
| 355 |
+
return Path(checkpoint_path) / 'cache'
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def checkpoint_dataset_cache_paths(checkpoint_path: str | Path, dataset: str) -> DatasetCachePaths:
|
| 359 |
+
dataset_name = str(dataset).strip()
|
| 360 |
+
if dataset_name not in SUPPORTED_DATASETS:
|
| 361 |
+
raise ValueError(
|
| 362 |
+
f'Unsupported dataset={dataset_name!r}; expected one of {SUPPORTED_DATASETS}'
|
| 363 |
+
)
|
| 364 |
+
cache_dir = checkpoint_cache_dir(checkpoint_path)
|
| 365 |
+
return build_dataset_cache_paths(str(cache_dir), SUPPORTED_DATASETS)[dataset_name]
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def selector_cache_files_for_model(checkpoint_path: Path | str, dataset: str) -> dict[str, Path]:
|
| 369 |
dataset_name = str(dataset).strip()
|
| 370 |
if not dataset_name:
|
| 371 |
return {}
|
| 372 |
|
| 373 |
+
dataset_dir = checkpoint_cache_dir(checkpoint_path) / dataset_name
|
| 374 |
+
if not dataset_dir.is_dir():
|
| 375 |
+
return {}
|
| 376 |
+
|
| 377 |
+
patch_only = dataset_name.lower() in ('local_kadid', 'srground')
|
| 378 |
+
parquet_paths = sorted(dataset_dir.glob('*.parquet'))
|
| 379 |
+
patch_stems = {path.stem for path in parquet_paths if '_patch' in path.stem}
|
| 380 |
cache_files: dict[str, Path] = {}
|
| 381 |
+
for parquet_path in parquet_paths:
|
| 382 |
+
stem = parquet_path.stem
|
| 383 |
+
if patch_only and '_patch' not in stem and f'{stem}_patch' in patch_stems:
|
| 384 |
+
continue
|
| 385 |
+
cache_files[stem] = parquet_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
return cache_files
|
| 387 |
|
| 388 |
|
|
|
|
| 393 |
aggregate those per-feature scores across all features. Returns
|
| 394 |
(max, mean, variance, min) computed over per-feature scores.
|
| 395 |
"""
|
| 396 |
+
feature_table = load_selector_cache_feature_table(str(cache_path))
|
| 397 |
if feature_table.empty:
|
| 398 |
return None, None, None, None
|
| 399 |
|
|
|
|
| 405 |
return float(arr.max()), float(arr.mean()), float(arr.var()), float(arr.min())
|
| 406 |
|
| 407 |
|
| 408 |
+
@lru_cache(maxsize=256)
|
| 409 |
+
def load_selector_cache_feature_table(cache_path_str: str) -> pd.DataFrame:
|
| 410 |
"""Return ordered per-feature selector scores with their feature ids.
|
| 411 |
|
| 412 |
The returned frame contains:
|
|
|
|
| 414 |
- feature_score: max absolute score for that feature column
|
| 415 |
- feature_rank: 1-based rank after sorting by score descending, then feature id ascending
|
| 416 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
columns = ["feature_id", "feature_score", "feature_rank"]
|
| 418 |
+
path = Path(cache_path_str)
|
| 419 |
+
if not path.exists():
|
| 420 |
return pd.DataFrame(columns=columns)
|
| 421 |
+
|
| 422 |
+
table = pd.read_parquet(path)
|
| 423 |
numeric = table.select_dtypes(include=["number"])
|
| 424 |
if numeric.empty:
|
| 425 |
return pd.DataFrame(columns=columns)
|
|
|
|
| 451 |
|
| 452 |
Each value is the max absolute value for one numeric feature column.
|
| 453 |
"""
|
| 454 |
+
feature_table = load_selector_cache_feature_table(str(cache_path))
|
| 455 |
if feature_table.empty:
|
| 456 |
return np.array([], dtype=float)
|
| 457 |
|
|
|
|
| 459 |
return values[np.isfinite(values)]
|
| 460 |
|
| 461 |
|
| 462 |
+
def summarize_selector_cache(
|
| 463 |
+
checkpoint_path: Path | str,
|
| 464 |
+
dataset: str,
|
| 465 |
+
exclude_prefixes: list[str] | None = None,
|
| 466 |
+
) -> list[SelectorCacheSummary]:
|
| 467 |
if exclude_prefixes is None:
|
| 468 |
exclude_prefixes = list(DEFAULT_SELECTOR_HIDE_PREFIXES)
|
| 469 |
+
return _summarize_selector_cache_cached(str(checkpoint_path), dataset, tuple(exclude_prefixes))
|
| 470 |
|
| 471 |
|
| 472 |
@lru_cache(maxsize=256)
|
| 473 |
def _summarize_selector_cache_cached(
|
| 474 |
+
checkpoint_path_str: str,
|
| 475 |
dataset: str,
|
| 476 |
exclude_prefixes: tuple[str, ...],
|
| 477 |
) -> tuple[SelectorCacheSummary, ...]:
|
| 478 |
|
| 479 |
+
cache_files = selector_cache_files_for_model(checkpoint_path_str, dataset)
|
| 480 |
|
| 481 |
summaries: list[SelectorCacheSummary] = []
|
|
|
|
| 482 |
for selector_name in sorted(cache_files):
|
| 483 |
if any(selector_name.startswith(p) for p in exclude_prefixes):
|
| 484 |
continue
|
| 485 |
|
| 486 |
cache_path = cache_files[selector_name]
|
| 487 |
+
|
| 488 |
max_v, mean_v, var_v, min_v = summarize_selector_cache_file(cache_path)
|
| 489 |
|
| 490 |
summaries.append(
|
| 491 |
+
SelectorCacheSummary(
|
| 492 |
selector_name=selector_name,
|
| 493 |
+
cache_path=str(cache_path), # <-- FIX
|
| 494 |
max_value=max_v,
|
| 495 |
mean_value=mean_v,
|
| 496 |
variance_value=var_v,
|
| 497 |
min_value=min_v,
|
| 498 |
)
|
| 499 |
+
)
|
| 500 |
return tuple(summaries)
|
| 501 |
|
| 502 |
+
|
| 503 |
def format_selector_cache_summary(summaries: list[SelectorCacheSummary]) -> list[str]:
|
| 504 |
if not summaries:
|
| 505 |
return ["No selector cache files were found for this model."]
|
|
|
|
| 519 |
return lines
|
| 520 |
|
| 521 |
|
| 522 |
+
def _acts_meta_path(acts_cache_path: Path) -> Path:
|
| 523 |
+
stem = acts_cache_path.stem.removesuffix('.feather')
|
| 524 |
+
return acts_cache_path.parent / f'{stem}_meta.feather'
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
@lru_cache(maxsize=1)
|
| 528 |
+
def _dashboard_default_params():
|
| 529 |
+
return load_sae_vis_config()
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
def _filters_to_cache_key(requested_filters: list[dict]) -> str:
|
| 533 |
+
return json.dumps(requested_filters, sort_keys=True, default=str)
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
def _activation_steps_cache_key(activation_steps_to_keep: Sequence[int]) -> str:
|
| 537 |
+
return json.dumps([int(step) for step in activation_steps_to_keep])
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
def clear_activation_load_cache() -> None:
|
| 541 |
+
"""Drop in-process activation caches (e.g. after rebuilding cache files on disk)."""
|
| 542 |
+
_load_and_filter_model_activations_cached.cache_clear()
|
| 543 |
+
|
| 544 |
|
| 545 |
+
@lru_cache(maxsize=32)
|
| 546 |
+
def _load_and_filter_model_activations_cached(
|
| 547 |
+
checkpoint_path: str,
|
| 548 |
+
dataset: str,
|
| 549 |
+
filters_key: str,
|
| 550 |
+
min_distortion_level: int,
|
| 551 |
+
max_distortion_level: int,
|
| 552 |
+
activation_steps_key: str,
|
| 553 |
+
) -> FilteredActivationCache:
|
| 554 |
+
requested_filters = json.loads(filters_key)
|
| 555 |
+
activation_steps_to_keep = [int(step) for step in json.loads(activation_steps_key)]
|
| 556 |
+
return _load_and_filter_model_activations_impl(
|
| 557 |
+
checkpoint_path,
|
| 558 |
+
dataset,
|
| 559 |
+
requested_filters=requested_filters,
|
| 560 |
+
min_distortion_level=min_distortion_level,
|
| 561 |
+
max_distortion_level=max_distortion_level,
|
| 562 |
+
activation_steps_to_keep=activation_steps_to_keep,
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
def load_and_filter_model_activations(
|
| 567 |
+
checkpoint_path: Path | str,
|
| 568 |
+
dataset: str,
|
| 569 |
+
*,
|
| 570 |
+
requested_filters: list[dict] | None = None,
|
| 571 |
+
min_distortion_level: int | None = None,
|
| 572 |
+
max_distortion_level: int | None = None,
|
| 573 |
+
activation_steps_to_keep: Sequence[int] | None = None,
|
| 574 |
+
) -> FilteredActivationCache:
|
| 575 |
+
default_cfg = _dashboard_default_params()
|
| 576 |
+
filters = list(default_cfg.FEATURE_FILTERS if requested_filters is None else requested_filters)
|
| 577 |
+
min_level = int(
|
| 578 |
+
min_distortion_level
|
| 579 |
+
if min_distortion_level is not None
|
| 580 |
+
else default_cfg.KADID_MIN_DISTORTION_LEVEL
|
| 581 |
+
)
|
| 582 |
+
max_level = int(
|
| 583 |
+
max_distortion_level
|
| 584 |
+
if max_distortion_level is not None
|
| 585 |
+
else default_cfg.KADID_MAX_DISTORTION_LEVEL
|
| 586 |
+
)
|
| 587 |
+
keep_steps = list(
|
| 588 |
+
activation_steps_to_keep
|
| 589 |
+
if activation_steps_to_keep is not None
|
| 590 |
+
else default_cfg.ACTIVATION_STEPS_TO_KEEP
|
| 591 |
+
)
|
| 592 |
+
return _load_and_filter_model_activations_cached(
|
| 593 |
+
str(Path(checkpoint_path).resolve()),
|
| 594 |
+
str(dataset),
|
| 595 |
+
_filters_to_cache_key(filters),
|
| 596 |
+
min_level,
|
| 597 |
+
max_level,
|
| 598 |
+
_activation_steps_cache_key(keep_steps),
|
| 599 |
+
)
|
| 600 |
+
|
| 601 |
+
|
| 602 |
+
def _load_and_filter_model_activations_impl(
|
| 603 |
+
checkpoint_path: str,
|
| 604 |
+
dataset: str,
|
| 605 |
+
*,
|
| 606 |
+
requested_filters: list[dict],
|
| 607 |
+
min_distortion_level: int,
|
| 608 |
+
max_distortion_level: int,
|
| 609 |
+
activation_steps_to_keep: Sequence[int],
|
| 610 |
+
) -> FilteredActivationCache:
|
| 611 |
+
cache_paths = checkpoint_dataset_cache_paths(checkpoint_path, dataset)
|
| 612 |
+
acts_cache_path = Path(cache_paths.acts_cache_path)
|
| 613 |
+
meta_path = _acts_meta_path(acts_cache_path)
|
| 614 |
+
if not meta_path.exists():
|
| 615 |
+
raise FileNotFoundError(f'Activation cache not found: {meta_path}')
|
| 616 |
+
|
| 617 |
+
cache_base_path = acts_cache_path
|
| 618 |
|
| 619 |
meta_df, codes_csr, activation_steps_csr = load_cache(
|
| 620 |
str(cache_base_path),
|
|
|
|
| 622 |
min_distortion_level=min_distortion_level,
|
| 623 |
max_distortion_level=max_distortion_level
|
| 624 |
)
|
| 625 |
+
codes_csr = zero_codes_outside_activation_steps(
|
| 626 |
+
codes_csr,
|
| 627 |
+
activation_steps_csr,
|
| 628 |
+
activation_steps_to_keep,
|
| 629 |
+
)
|
| 630 |
|
| 631 |
+
feature_filter_cache_dir = Path(cache_paths.cache_dir) / 'feature_filter_cache'
|
| 632 |
feature_filter_summary: list[dict[str, Any]] = []
|
| 633 |
|
| 634 |
if not requested_filters:
|
|
|
|
| 699 |
|
| 700 |
|
| 701 |
@lru_cache(maxsize=256)
|
| 702 |
+
def summarize_feature_filter_cache(checkpoint_path: Path | str, dataset: str) -> list[str]:
|
| 703 |
"""Return human-readable summary lines for feature filtering.
|
| 704 |
|
| 705 |
The model cache is loaded first, then configured feature filters are applied
|
|
|
|
| 708 |
``load_and_filter_model_activations()`` for downstream use.
|
| 709 |
"""
|
| 710 |
try:
|
| 711 |
+
filtered_cache = load_and_filter_model_activations(checkpoint_path, dataset)
|
| 712 |
except Exception as exc:
|
| 713 |
return [f'Feature filtering unavailable: {exc}']
|
| 714 |
|
dashboard/umap_service.py
CHANGED
|
@@ -11,14 +11,29 @@ import pandas as pd
|
|
| 11 |
import plotly.graph_objects as go
|
| 12 |
|
| 13 |
from analysis.cache_utils import load_pristine_cache, zero_codes_outside_activation_steps
|
| 14 |
-
from analysis.config import dataset_images_root
|
| 15 |
from analysis.datasets import Kadid10kDataset
|
| 16 |
-
from analysis.features.feature_matrix import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
from analysis.features.feature_indexing import FeatureMatrix, global_id_set, global_to_column
|
| 18 |
from analysis.metrics.correlations import compute_distortion_correlations
|
| 19 |
from analysis.viz.umap_plot import build_umap_figure, make_thumb_b64 as _make_thumb_b64
|
| 20 |
-
from analysis.viz.umap_utils import
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
import scipy.sparse as sp
|
| 23 |
from analysis.models import load_sae
|
| 24 |
|
|
@@ -42,7 +57,6 @@ CORR_GARBAGE_THRESHOLD = 0.20
|
|
| 42 |
|
| 43 |
# Activation cache and image previews for dashboard UMAP (images + features modes).
|
| 44 |
UMAP_DATASET = 'kadid10k'
|
| 45 |
-
|
| 46 |
UMAP_PARAMS = dict(
|
| 47 |
n_neighbors=15,
|
| 48 |
min_dist=0.1,
|
|
@@ -62,6 +76,8 @@ class UmapContext:
|
|
| 62 |
model_key: str
|
| 63 |
features: FeatureMatrix
|
| 64 |
image_row_indices: tuple[tuple[int, ...], ...]
|
|
|
|
|
|
|
| 65 |
n_images_used: int
|
| 66 |
n_features_total: int
|
| 67 |
dist_type_arr: np.ndarray
|
|
@@ -69,32 +85,36 @@ class UmapContext:
|
|
| 69 |
dist_level_arr: np.ndarray
|
| 70 |
mos_arr: np.ndarray
|
| 71 |
umap_cache_dir: Path
|
|
|
|
| 72 |
kadid_ds: Any | None
|
| 73 |
meta_df: pd.DataFrame
|
| 74 |
use_reference_delta: bool
|
|
|
|
| 75 |
pristine_meta_df: pd.DataFrame | None = None
|
| 76 |
pristine_features: FeatureMatrix | None = None
|
| 77 |
kadid_image_idx: np.ndarray | None = None
|
| 78 |
|
| 79 |
|
| 80 |
-
def
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
| 98 |
|
| 99 |
|
| 100 |
def empty_umap_figure(message: str) -> go.Figure:
|
|
@@ -137,39 +157,6 @@ def _compact_meta_image_indices(meta_df: pd.DataFrame) -> tuple[pd.DataFrame, in
|
|
| 137 |
return meta_out, int(unique_ids.size)
|
| 138 |
|
| 139 |
|
| 140 |
-
def _kadid_index_lookup(meta_df: pd.DataFrame, kadid_ds: Any, n_images_used: int) -> np.ndarray:
|
| 141 |
-
"""Map dense UMAP image index (0..n-1) to ``Kadid10kDataset`` positional index."""
|
| 142 |
-
lookup = np.arange(n_images_used, dtype=np.int32)
|
| 143 |
-
if kadid_ds is None:
|
| 144 |
-
return lookup
|
| 145 |
-
|
| 146 |
-
first_rows = meta_df.groupby('image_idx', sort=True).first()
|
| 147 |
-
kadid_paths = [str(p) for p in kadid_ds.images]
|
| 148 |
-
path_to_idx = {p: i for i, p in enumerate(kadid_paths)}
|
| 149 |
-
base_to_idx = {os.path.basename(p): i for i, p in enumerate(kadid_paths)}
|
| 150 |
-
|
| 151 |
-
for dense_i in range(n_images_used):
|
| 152 |
-
row = first_rows.iloc[dense_i]
|
| 153 |
-
kadid_idx = None
|
| 154 |
-
for col in ('distorted_img_path', 'image_path', 'original_img_path'):
|
| 155 |
-
if col not in row.index:
|
| 156 |
-
continue
|
| 157 |
-
val = row[col]
|
| 158 |
-
if val is None or (isinstance(val, float) and np.isnan(val)):
|
| 159 |
-
continue
|
| 160 |
-
path = str(val)
|
| 161 |
-
kadid_idx = path_to_idx.get(path)
|
| 162 |
-
if kadid_idx is None and not Path(path).is_absolute():
|
| 163 |
-
kadid_idx = path_to_idx.get(str(kadid_ds.root / Path(path)))
|
| 164 |
-
if kadid_idx is None:
|
| 165 |
-
kadid_idx = base_to_idx.get(os.path.basename(path))
|
| 166 |
-
if kadid_idx is not None:
|
| 167 |
-
break
|
| 168 |
-
if kadid_idx is not None:
|
| 169 |
-
lookup[dense_i] = int(kadid_idx)
|
| 170 |
-
return lookup
|
| 171 |
-
|
| 172 |
-
|
| 173 |
def _image_labels_from_meta(meta_df, n_images_used: int) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
| 174 |
first_rows = meta_df.groupby('image_idx', sort=True).first()
|
| 175 |
expected_index = np.arange(n_images_used, dtype=np.int32)
|
|
@@ -271,34 +258,6 @@ def _build_reference_delta_matrix(
|
|
| 271 |
)
|
| 272 |
|
| 273 |
|
| 274 |
-
def _resolve_kadid_dataset(
|
| 275 |
-
dataset: str,
|
| 276 |
-
n_images_used: int,
|
| 277 |
-
kadid_images_path: Optional[str],
|
| 278 |
-
crop_size: Optional[int],
|
| 279 |
-
min_distortion_level: Optional[int],
|
| 280 |
-
):
|
| 281 |
-
if 'kadid' not in str(dataset).lower():
|
| 282 |
-
return None
|
| 283 |
-
try:
|
| 284 |
-
if kadid_images_path is None or crop_size is None or min_distortion_level is None:
|
| 285 |
-
cfg = load_sae_vis_config()
|
| 286 |
-
kadid_images_path = kadid_images_path or cfg.KADID_IMAGES_PATH
|
| 287 |
-
crop_size = crop_size or cfg.CROP_SIZE
|
| 288 |
-
min_distortion_level = min_distortion_level or cfg.KADID_MIN_DISTORTION_LEVEL
|
| 289 |
-
|
| 290 |
-
kadid_ds = Kadid10kDataset(
|
| 291 |
-
kadid_images_path,
|
| 292 |
-
crop_size=crop_size,
|
| 293 |
-
min_distortion_level=min_distortion_level,
|
| 294 |
-
)
|
| 295 |
-
if n_images_used > len(kadid_ds):
|
| 296 |
-
return None
|
| 297 |
-
return kadid_ds
|
| 298 |
-
except Exception:
|
| 299 |
-
return None
|
| 300 |
-
|
| 301 |
-
|
| 302 |
@lru_cache(maxsize=32)
|
| 303 |
def load_umap_context(
|
| 304 |
metric: str,
|
|
@@ -312,54 +271,48 @@ def load_umap_context(
|
|
| 312 |
crop_size: Optional[int] = None,
|
| 313 |
use_reference_delta: bool = True,
|
| 314 |
) -> UmapContext:
|
| 315 |
-
record =
|
| 316 |
-
if record is None:
|
| 317 |
-
raise FileNotFoundError(f'Model {model_key!r} not found for metric {metric!r}')
|
| 318 |
-
|
| 319 |
-
# Load default config only for missing parameters
|
| 320 |
-
cfg: Optional[object] = None
|
| 321 |
-
need_cfg = any(
|
| 322 |
-
v is None
|
| 323 |
-
for v in (
|
| 324 |
-
feature_filters,
|
| 325 |
-
kadid_min_dist_level,
|
| 326 |
-
kadid_max_dist_level,
|
| 327 |
-
activation_steps_to_keep,
|
| 328 |
-
kadid_images_path,
|
| 329 |
-
crop_size,
|
| 330 |
-
)
|
| 331 |
-
)
|
| 332 |
-
if need_cfg:
|
| 333 |
-
cfg = load_sae_vis_config()
|
| 334 |
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
| 338 |
|
| 339 |
filtered = load_and_filter_model_activations(
|
| 340 |
-
|
| 341 |
dataset,
|
| 342 |
-
requested_filters=
|
| 343 |
-
min_distortion_level=
|
| 344 |
-
max_distortion_level=
|
|
|
|
| 345 |
)
|
| 346 |
|
| 347 |
-
|
| 348 |
-
codes_csr =
|
| 349 |
-
filtered.features.codes,
|
| 350 |
-
filtered.activation_steps_csr,
|
| 351 |
-
keep_steps,
|
| 352 |
-
)
|
| 353 |
-
features = FeatureMatrix(
|
| 354 |
-
codes=codes_csr,
|
| 355 |
-
column_feature_ids=filtered.features.column_feature_ids,
|
| 356 |
-
)
|
| 357 |
|
| 358 |
meta_df = filtered.meta_df
|
| 359 |
if 'image_idx' not in meta_df.columns:
|
| 360 |
raise ValueError('Activation cache metadata is missing required column "image_idx"')
|
| 361 |
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
image_idx_arr = meta_df['image_idx'].to_numpy(dtype=np.int32)
|
| 364 |
n_features_total = features.n_features
|
| 365 |
|
|
@@ -370,12 +323,12 @@ def load_umap_context(
|
|
| 370 |
image_row_indices = _build_image_row_indices(image_idx_arr, n_images_used)
|
| 371 |
|
| 372 |
dist_type_arr, dist_group_arr, dist_level_arr, mos_arr = _image_labels_from_meta(meta_df, n_images_used)
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
umap_cache_dir = (discovered[0] / 'umap_cache') if discovered else (cache_dir / 'cache' / dataset / 'umap_cache')
|
| 376 |
|
| 377 |
pristine_meta_df: pd.DataFrame | None = None
|
| 378 |
pristine_features: FeatureMatrix | None = None
|
|
|
|
| 379 |
if use_reference_delta:
|
| 380 |
try:
|
| 381 |
pristine_meta_df, pristine_codes, pristine_steps = load_pristine_cache(str(filtered.cache_base_path), return_activation_steps=True)
|
|
@@ -391,9 +344,14 @@ def load_umap_context(
|
|
| 391 |
raise ValueError(
|
| 392 |
f'Pristine and distorted feature dimensions differ: {pristine_features.n_features} != {n_features_total}'
|
| 393 |
)
|
|
|
|
|
|
|
| 394 |
except Exception as exc:
|
| 395 |
print(f'[umap] reference delta unavailable, falling back to raw features: {exc}')
|
| 396 |
use_reference_delta = False
|
|
|
|
|
|
|
|
|
|
| 397 |
|
| 398 |
kadid_ds = _kadid_dataset_for_context_cached(
|
| 399 |
metric,
|
|
@@ -401,7 +359,7 @@ def load_umap_context(
|
|
| 401 |
dataset,
|
| 402 |
n_images_used,
|
| 403 |
)
|
| 404 |
-
kadid_image_idx =
|
| 405 |
|
| 406 |
return UmapContext(
|
| 407 |
metric=metric,
|
|
@@ -409,6 +367,8 @@ def load_umap_context(
|
|
| 409 |
model_key=model_key,
|
| 410 |
features=features,
|
| 411 |
image_row_indices=image_row_indices,
|
|
|
|
|
|
|
| 412 |
n_images_used=n_images_used,
|
| 413 |
n_features_total=n_features_total,
|
| 414 |
dist_type_arr=dist_type_arr,
|
|
@@ -416,9 +376,11 @@ def load_umap_context(
|
|
| 416 |
dist_level_arr=dist_level_arr,
|
| 417 |
mos_arr=mos_arr,
|
| 418 |
umap_cache_dir=umap_cache_dir,
|
|
|
|
| 419 |
kadid_ds=kadid_ds,
|
| 420 |
meta_df=meta_df,
|
| 421 |
use_reference_delta=use_reference_delta,
|
|
|
|
| 422 |
pristine_meta_df=pristine_meta_df,
|
| 423 |
pristine_features=pristine_features,
|
| 424 |
kadid_image_idx=kadid_image_idx,
|
|
@@ -434,15 +396,18 @@ def _kadid_dataset_for_context_cached(
|
|
| 434 |
) -> Any:
|
| 435 |
if 'kadid' not in str(dataset).lower():
|
| 436 |
return None
|
|
|
|
|
|
|
|
|
|
| 437 |
try:
|
| 438 |
-
|
| 439 |
-
images_root = dataset_images_root(
|
| 440 |
return _resolve_kadid_dataset(
|
| 441 |
dataset,
|
| 442 |
n_images_used,
|
| 443 |
images_root,
|
| 444 |
-
|
| 445 |
-
|
| 446 |
)
|
| 447 |
except Exception:
|
| 448 |
return None
|
|
@@ -464,11 +429,13 @@ def _labels_for_color_mode(context: UmapContext, color_mode: str) -> np.ndarray:
|
|
| 464 |
|
| 465 |
|
| 466 |
@lru_cache(maxsize=16)
|
| 467 |
-
def _resolve_feature_atoms_cached(
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
|
|
|
|
|
|
| 472 |
sae = load_sae(checkpoint_path=str(checkpoint_path), device='cpu')
|
| 473 |
try:
|
| 474 |
decoder_weight = sae.decoder.weight
|
|
@@ -513,13 +480,17 @@ def _feature_stats_for_ids(
|
|
| 513 |
return means, stds, nonzeros
|
| 514 |
|
| 515 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
def _feature_stats(context: UmapContext) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 517 |
-
return _feature_stats_cached(
|
| 518 |
-
context.metric,
|
| 519 |
-
context.dataset,
|
| 520 |
-
context.model_key,
|
| 521 |
-
bool(context.use_reference_delta),
|
| 522 |
-
)
|
| 523 |
|
| 524 |
|
| 525 |
@lru_cache(maxsize=32)
|
|
@@ -527,9 +498,8 @@ def _feature_stats_cached(
|
|
| 527 |
metric: str,
|
| 528 |
dataset: str,
|
| 529 |
model_key: str,
|
| 530 |
-
use_reference_delta: bool,
|
| 531 |
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 532 |
-
context = load_umap_context(metric, dataset, model_key, use_reference_delta=
|
| 533 |
return _feature_stats_all_columns(context)
|
| 534 |
|
| 535 |
|
|
@@ -544,12 +514,10 @@ def _feature_marker_sizes(mean_feats: np.ndarray) -> np.ndarray:
|
|
| 544 |
|
| 545 |
|
| 546 |
def _feature_similarity_cache_path(context: UmapContext, source: str = 'type') -> str:
|
| 547 |
-
cfg = load_sae_vis_config()
|
| 548 |
-
dataset_cfg = cfg.DATASET_CACHE_CONFIGS[context.dataset]
|
| 549 |
if source == 'type':
|
| 550 |
-
return
|
| 551 |
if source == 'group':
|
| 552 |
-
return
|
| 553 |
raise ValueError(f"source must be 'type' or 'group', got {source!r}")
|
| 554 |
|
| 555 |
|
|
@@ -646,24 +614,28 @@ def _feature_top_corr_labels(
|
|
| 646 |
return np.asarray(top_labels, dtype=object)
|
| 647 |
|
| 648 |
|
| 649 |
-
def
|
| 650 |
-
|
| 651 |
-
context.
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
|
| 660 |
|
| 661 |
def _umap_cache_signature(context: UmapContext) -> dict[str, object]:
|
| 662 |
-
|
| 663 |
-
|
|
|
|
| 664 |
'n_images_used': int(context.n_images_used),
|
| 665 |
'n_features_total': int(context.n_features_total),
|
| 666 |
'use_reference_delta': bool(context.use_reference_delta),
|
|
|
|
|
|
|
|
|
|
| 667 |
'feature_ids': [int(fid) for fid in context.features.column_feature_ids],
|
| 668 |
}
|
| 669 |
if context.pristine_features is not None:
|
|
@@ -671,32 +643,21 @@ def _umap_cache_signature(context: UmapContext) -> dict[str, object]:
|
|
| 671 |
return signature
|
| 672 |
|
| 673 |
|
| 674 |
-
def
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
context.image_row_indices,
|
| 683 |
-
context.meta_df,
|
| 684 |
-
context.pristine_features,
|
| 685 |
-
context.pristine_meta_df,
|
| 686 |
-
context.n_images_used,
|
| 687 |
-
aggregation_mode,
|
| 688 |
-
)
|
| 689 |
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
context.n_images_used,
|
| 694 |
-
aggregation_mode,
|
| 695 |
-
)
|
| 696 |
|
| 697 |
return get_or_compute_umap_with_builder(
|
| 698 |
-
|
| 699 |
-
_build_features,
|
| 700 |
UMAP_PARAMS,
|
| 701 |
cache_dir=context.umap_cache_dir,
|
| 702 |
cache_id=cache_id,
|
|
@@ -704,35 +665,26 @@ def get_or_compute_umap(context: UmapContext, aggregation_mode: str) -> dict[str
|
|
| 704 |
)
|
| 705 |
|
| 706 |
|
| 707 |
-
def get_or_compute_feature_umap(
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
)
|
| 715 |
-
cache_id = f"{context.model_key}_{context.dataset}_features_v3"
|
| 716 |
|
| 717 |
def _build_atoms() -> np.ndarray:
|
| 718 |
-
atoms, _aligned = _resolve_feature_atoms_cached(
|
| 719 |
return atoms
|
| 720 |
|
| 721 |
-
def _build_signature() -> dict[str, object]:
|
| 722 |
-
sig = _umap_cache_signature(context)
|
| 723 |
-
_atoms, aligned = _resolve_feature_atoms_cached(context.metric, context.model_key)
|
| 724 |
-
sig['n_feature_atoms'] = len(aligned)
|
| 725 |
-
return sig
|
| 726 |
-
|
| 727 |
info = get_or_compute_umap_with_builder(
|
| 728 |
-
key,
|
| 729 |
_build_atoms,
|
| 730 |
UMAP_PARAMS,
|
| 731 |
-
cache_dir=
|
| 732 |
cache_id=cache_id,
|
| 733 |
-
cache_signature=
|
| 734 |
)
|
| 735 |
-
_atoms, aligned = _resolve_feature_atoms_cached(
|
| 736 |
expected_n = len(aligned)
|
| 737 |
emb = np.asarray(info['embedding_2d'])
|
| 738 |
if emb.shape[0] != expected_n:
|
|
@@ -760,10 +712,10 @@ def build_feature_umap_figure(
|
|
| 760 |
of the code (including `context`, `feature_ids`, `unique_categories`, and
|
| 761 |
cache-related fields returned by the UMAP computation).
|
| 762 |
"""
|
| 763 |
-
context = load_umap_context(metric, dataset, model_key)
|
| 764 |
-
_atoms, aligned_feature_ids = _resolve_feature_atoms_cached(context.metric, context.model_key)
|
| 765 |
aligned_feature_ids = list(aligned_feature_ids)
|
| 766 |
-
info = get_or_compute_feature_umap(context)
|
| 767 |
emb = np.asarray(info['embedding_2d'])
|
| 768 |
n_points = min(int(emb.shape[0]), len(aligned_feature_ids))
|
| 769 |
emb = emb[:n_points]
|
|
@@ -872,7 +824,7 @@ def build_feature_umap_figure(
|
|
| 872 |
|
| 873 |
@lru_cache(maxsize=32)
|
| 874 |
def activation_feature_id_set(metric: str, dataset: str, model_key: str) -> frozenset[int]:
|
| 875 |
-
context = load_umap_context(metric, dataset, model_key)
|
| 876 |
return global_id_set(context.features.column_feature_ids)
|
| 877 |
|
| 878 |
|
|
@@ -973,10 +925,8 @@ def make_hover_thumb_b64(context: UmapContext, img_idx: int) -> str | None:
|
|
| 973 |
|
| 974 |
|
| 975 |
def format_feature_umap_status(info: dict[str, object], color_mode: str) -> str:
|
| 976 |
-
context = info.get('context')
|
| 977 |
-
feature_mode = 'delta(ref)' if getattr(context, 'use_reference_delta', False) else 'raw'
|
| 978 |
corr_source = str(info.get('feature_corr_source', DEFAULT_FEATURE_CORR_SOURCE))
|
| 979 |
-
return f'
|
| 980 |
|
| 981 |
|
| 982 |
def format_image_color_mode(color_mode: str) -> str:
|
|
|
|
| 11 |
import plotly.graph_objects as go
|
| 12 |
|
| 13 |
from analysis.cache_utils import load_pristine_cache, zero_codes_outside_activation_steps
|
| 14 |
+
from analysis.config import dataset_images_root
|
| 15 |
from analysis.datasets import Kadid10kDataset
|
| 16 |
+
from analysis.features.feature_matrix import (
|
| 17 |
+
build_image_feature_matrix,
|
| 18 |
+
build_image_feature_matrix_from_image_idx,
|
| 19 |
+
build_image_feature_matrix_raw,
|
| 20 |
+
)
|
| 21 |
from analysis.features.feature_indexing import FeatureMatrix, global_id_set, global_to_column
|
| 22 |
from analysis.metrics.correlations import compute_distortion_correlations
|
| 23 |
from analysis.viz.umap_plot import build_umap_figure, make_thumb_b64 as _make_thumb_b64
|
| 24 |
+
from analysis.viz.umap_utils import (
|
| 25 |
+
UMAP_IMAGE_LIMIT,
|
| 26 |
+
UMAP_IMAGE_RANDOM_STATE,
|
| 27 |
+
build_kadid_index_lookup,
|
| 28 |
+
get_or_compute_umap_with_builder,
|
| 29 |
+
select_random_image_indices,
|
| 30 |
+
)
|
| 31 |
+
from dashboard.model_catalog import (
|
| 32 |
+
_dashboard_default_params,
|
| 33 |
+
get_model_record,
|
| 34 |
+
load_and_filter_model_activations,
|
| 35 |
+
require_model_record,
|
| 36 |
+
)
|
| 37 |
import scipy.sparse as sp
|
| 38 |
from analysis.models import load_sae
|
| 39 |
|
|
|
|
| 57 |
|
| 58 |
# Activation cache and image previews for dashboard UMAP (images + features modes).
|
| 59 |
UMAP_DATASET = 'kadid10k'
|
|
|
|
| 60 |
UMAP_PARAMS = dict(
|
| 61 |
n_neighbors=15,
|
| 62 |
min_dist=0.1,
|
|
|
|
| 76 |
model_key: str
|
| 77 |
features: FeatureMatrix
|
| 78 |
image_row_indices: tuple[tuple[int, ...], ...]
|
| 79 |
+
image_idx_arr: np.ndarray
|
| 80 |
+
selected_image_idx: tuple[int, ...]
|
| 81 |
n_images_used: int
|
| 82 |
n_features_total: int
|
| 83 |
dist_type_arr: np.ndarray
|
|
|
|
| 85 |
dist_level_arr: np.ndarray
|
| 86 |
mos_arr: np.ndarray
|
| 87 |
umap_cache_dir: Path
|
| 88 |
+
dataset_cache_dir: Path
|
| 89 |
kadid_ds: Any | None
|
| 90 |
meta_df: pd.DataFrame
|
| 91 |
use_reference_delta: bool
|
| 92 |
+
delta_codes: Any | None = None
|
| 93 |
pristine_meta_df: pd.DataFrame | None = None
|
| 94 |
pristine_features: FeatureMatrix | None = None
|
| 95 |
kadid_image_idx: np.ndarray | None = None
|
| 96 |
|
| 97 |
|
| 98 |
+
def _resolve_kadid_dataset(
|
| 99 |
+
dataset: str,
|
| 100 |
+
n_images_used: int,
|
| 101 |
+
kadid_images_path: str,
|
| 102 |
+
crop_size: int,
|
| 103 |
+
min_distortion_level: int,
|
| 104 |
+
):
|
| 105 |
+
if 'kadid' not in str(dataset).lower():
|
| 106 |
+
return None
|
| 107 |
+
try:
|
| 108 |
+
kadid_ds = Kadid10kDataset(
|
| 109 |
+
kadid_images_path,
|
| 110 |
+
crop_size=crop_size,
|
| 111 |
+
min_distortion_level=min_distortion_level,
|
| 112 |
+
)
|
| 113 |
+
if n_images_used > len(kadid_ds):
|
| 114 |
+
return None
|
| 115 |
+
return kadid_ds
|
| 116 |
+
except Exception:
|
| 117 |
+
return None
|
| 118 |
|
| 119 |
|
| 120 |
def empty_umap_figure(message: str) -> go.Figure:
|
|
|
|
| 157 |
return meta_out, int(unique_ids.size)
|
| 158 |
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
def _image_labels_from_meta(meta_df, n_images_used: int) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
| 161 |
first_rows = meta_df.groupby('image_idx', sort=True).first()
|
| 162 |
expected_index = np.arange(n_images_used, dtype=np.int32)
|
|
|
|
| 258 |
)
|
| 259 |
|
| 260 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
@lru_cache(maxsize=32)
|
| 262 |
def load_umap_context(
|
| 263 |
metric: str,
|
|
|
|
| 271 |
crop_size: Optional[int] = None,
|
| 272 |
use_reference_delta: bool = True,
|
| 273 |
) -> UmapContext:
|
| 274 |
+
record = require_model_record(metric, model_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
+
default_cfg = _dashboard_default_params()
|
| 277 |
+
keep_steps = list(
|
| 278 |
+
activation_steps_to_keep
|
| 279 |
+
if activation_steps_to_keep is not None
|
| 280 |
+
else default_cfg.ACTIVATION_STEPS_TO_KEEP
|
| 281 |
+
)
|
| 282 |
|
| 283 |
filtered = load_and_filter_model_activations(
|
| 284 |
+
record.checkpoint_path,
|
| 285 |
dataset,
|
| 286 |
+
requested_filters=feature_filters,
|
| 287 |
+
min_distortion_level=kadid_min_dist_level,
|
| 288 |
+
max_distortion_level=kadid_max_dist_level,
|
| 289 |
+
activation_steps_to_keep=keep_steps,
|
| 290 |
)
|
| 291 |
|
| 292 |
+
features = filtered.features
|
| 293 |
+
codes_csr = features.codes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
|
| 295 |
meta_df = filtered.meta_df
|
| 296 |
if 'image_idx' not in meta_df.columns:
|
| 297 |
raise ValueError('Activation cache metadata is missing required column "image_idx"')
|
| 298 |
|
| 299 |
+
image_idx_arr = meta_df['image_idx'].to_numpy(dtype=np.int64)
|
| 300 |
+
max_images = int(image_idx_arr.max()) + 1
|
| 301 |
+
selected_image_idx = select_random_image_indices(
|
| 302 |
+
max_images,
|
| 303 |
+
limit=UMAP_IMAGE_LIMIT,
|
| 304 |
+
random_state=UMAP_IMAGE_RANDOM_STATE,
|
| 305 |
+
)
|
| 306 |
+
row_mask = np.isin(image_idx_arr, selected_image_idx)
|
| 307 |
+
dense_map = {int(orig): i for i, orig in enumerate(selected_image_idx.tolist())}
|
| 308 |
+
meta_df = meta_df.loc[row_mask].copy()
|
| 309 |
+
meta_df['image_idx'] = meta_df['image_idx'].map(lambda v: dense_map[int(v)]).astype(np.int32)
|
| 310 |
+
codes_csr = codes_csr[row_mask]
|
| 311 |
+
features = FeatureMatrix(
|
| 312 |
+
codes=codes_csr,
|
| 313 |
+
column_feature_ids=features.column_feature_ids,
|
| 314 |
+
)
|
| 315 |
+
n_images_used = int(selected_image_idx.size)
|
| 316 |
image_idx_arr = meta_df['image_idx'].to_numpy(dtype=np.int32)
|
| 317 |
n_features_total = features.n_features
|
| 318 |
|
|
|
|
| 323 |
image_row_indices = _build_image_row_indices(image_idx_arr, n_images_used)
|
| 324 |
|
| 325 |
dist_type_arr, dist_group_arr, dist_level_arr, mos_arr = _image_labels_from_meta(meta_df, n_images_used)
|
| 326 |
+
dataset_cache_dir = Path(record.checkpoint_path) / 'cache' / dataset
|
| 327 |
+
umap_cache_dir = Path(record.checkpoint_path) / 'cache' / 'umap_cache'
|
|
|
|
| 328 |
|
| 329 |
pristine_meta_df: pd.DataFrame | None = None
|
| 330 |
pristine_features: FeatureMatrix | None = None
|
| 331 |
+
delta_codes: Any | None = None
|
| 332 |
if use_reference_delta:
|
| 333 |
try:
|
| 334 |
pristine_meta_df, pristine_codes, pristine_steps = load_pristine_cache(str(filtered.cache_base_path), return_activation_steps=True)
|
|
|
|
| 344 |
raise ValueError(
|
| 345 |
f'Pristine and distorted feature dimensions differ: {pristine_features.n_features} != {n_features_total}'
|
| 346 |
)
|
| 347 |
+
pristine_row_indices = _build_reference_row_indices(meta_df, pristine_meta_df)
|
| 348 |
+
delta_codes = features.codes - pristine_features.codes[pristine_row_indices]
|
| 349 |
except Exception as exc:
|
| 350 |
print(f'[umap] reference delta unavailable, falling back to raw features: {exc}')
|
| 351 |
use_reference_delta = False
|
| 352 |
+
pristine_meta_df = None
|
| 353 |
+
pristine_features = None
|
| 354 |
+
delta_codes = None
|
| 355 |
|
| 356 |
kadid_ds = _kadid_dataset_for_context_cached(
|
| 357 |
metric,
|
|
|
|
| 359 |
dataset,
|
| 360 |
n_images_used,
|
| 361 |
)
|
| 362 |
+
kadid_image_idx = build_kadid_index_lookup(meta_df, kadid_ds, n_images_used)
|
| 363 |
|
| 364 |
return UmapContext(
|
| 365 |
metric=metric,
|
|
|
|
| 367 |
model_key=model_key,
|
| 368 |
features=features,
|
| 369 |
image_row_indices=image_row_indices,
|
| 370 |
+
image_idx_arr=image_idx_arr,
|
| 371 |
+
selected_image_idx=tuple(int(v) for v in selected_image_idx.tolist()),
|
| 372 |
n_images_used=n_images_used,
|
| 373 |
n_features_total=n_features_total,
|
| 374 |
dist_type_arr=dist_type_arr,
|
|
|
|
| 376 |
dist_level_arr=dist_level_arr,
|
| 377 |
mos_arr=mos_arr,
|
| 378 |
umap_cache_dir=umap_cache_dir,
|
| 379 |
+
dataset_cache_dir=dataset_cache_dir,
|
| 380 |
kadid_ds=kadid_ds,
|
| 381 |
meta_df=meta_df,
|
| 382 |
use_reference_delta=use_reference_delta,
|
| 383 |
+
delta_codes=delta_codes,
|
| 384 |
pristine_meta_df=pristine_meta_df,
|
| 385 |
pristine_features=pristine_features,
|
| 386 |
kadid_image_idx=kadid_image_idx,
|
|
|
|
| 396 |
) -> Any:
|
| 397 |
if 'kadid' not in str(dataset).lower():
|
| 398 |
return None
|
| 399 |
+
record = get_model_record(metric, model_key)
|
| 400 |
+
if record is None:
|
| 401 |
+
return None
|
| 402 |
try:
|
| 403 |
+
default_cfg = _dashboard_default_params()
|
| 404 |
+
images_root = dataset_images_root(default_cfg.DATASETS_ROOT, dataset)
|
| 405 |
return _resolve_kadid_dataset(
|
| 406 |
dataset,
|
| 407 |
n_images_used,
|
| 408 |
images_root,
|
| 409 |
+
default_cfg.CROP_SIZE,
|
| 410 |
+
default_cfg.KADID_MIN_DISTORTION_LEVEL,
|
| 411 |
)
|
| 412 |
except Exception:
|
| 413 |
return None
|
|
|
|
| 429 |
|
| 430 |
|
| 431 |
@lru_cache(maxsize=16)
|
| 432 |
+
def _resolve_feature_atoms_cached(
|
| 433 |
+
metric: str,
|
| 434 |
+
model_key: str,
|
| 435 |
+
dataset: str,
|
| 436 |
+
) -> tuple[np.ndarray, tuple[int, ...]]:
|
| 437 |
+
record = require_model_record(metric, model_key)
|
| 438 |
+
checkpoint_path = Path(record.checkpoint_path)
|
| 439 |
sae = load_sae(checkpoint_path=str(checkpoint_path), device='cpu')
|
| 440 |
try:
|
| 441 |
decoder_weight = sae.decoder.weight
|
|
|
|
| 480 |
return means, stds, nonzeros
|
| 481 |
|
| 482 |
|
| 483 |
+
def _feature_umap_cache_signature(metric: str, model_key: str, dataset: str) -> dict[str, object]:
|
| 484 |
+
"""Cache fingerprint for decoder-atom UMAP (matches notebook 07)."""
|
| 485 |
+
atoms, _aligned = _resolve_feature_atoms_cached(metric, model_key, dataset)
|
| 486 |
+
return {
|
| 487 |
+
'n_feature_atoms': int(atoms.shape[0]),
|
| 488 |
+
'atom_dim': int(atoms.shape[1]),
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
|
| 492 |
def _feature_stats(context: UmapContext) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 493 |
+
return _feature_stats_cached(context.metric, context.dataset, context.model_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
|
| 495 |
|
| 496 |
@lru_cache(maxsize=32)
|
|
|
|
| 498 |
metric: str,
|
| 499 |
dataset: str,
|
| 500 |
model_key: str,
|
|
|
|
| 501 |
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 502 |
+
context = load_umap_context(metric, dataset, model_key, use_reference_delta=False)
|
| 503 |
return _feature_stats_all_columns(context)
|
| 504 |
|
| 505 |
|
|
|
|
| 514 |
|
| 515 |
|
| 516 |
def _feature_similarity_cache_path(context: UmapContext, source: str = 'type') -> str:
|
|
|
|
|
|
|
| 517 |
if source == 'type':
|
| 518 |
+
return str(context.dataset_cache_dir / 'corr_type.parquet')
|
| 519 |
if source == 'group':
|
| 520 |
+
return str(context.dataset_cache_dir / 'corr_group.parquet')
|
| 521 |
raise ValueError(f"source must be 'type' or 'group', got {source!r}")
|
| 522 |
|
| 523 |
|
|
|
|
| 614 |
return np.asarray(top_labels, dtype=object)
|
| 615 |
|
| 616 |
|
| 617 |
+
def _umap_codes_for_signature(context: UmapContext):
|
| 618 |
+
if context.use_reference_delta and context.delta_codes is not None:
|
| 619 |
+
return context.delta_codes
|
| 620 |
+
return context.features.codes
|
| 621 |
+
|
| 622 |
+
|
| 623 |
+
def _images_umap_cache_id(context: UmapContext, aggregation_mode: str) -> str:
|
| 624 |
+
if context.use_reference_delta and context.delta_codes is not None:
|
| 625 |
+
return f'grid_delta_agg_{aggregation_mode}'
|
| 626 |
+
return f'grid_raw_agg_{aggregation_mode}'
|
| 627 |
|
| 628 |
|
| 629 |
def _umap_cache_signature(context: UmapContext) -> dict[str, object]:
|
| 630 |
+
codes_for_sig = _umap_codes_for_signature(context)
|
| 631 |
+
signature: dict[str, object] = {
|
| 632 |
+
'codes_shape': tuple(int(v) for v in codes_for_sig.shape),
|
| 633 |
'n_images_used': int(context.n_images_used),
|
| 634 |
'n_features_total': int(context.n_features_total),
|
| 635 |
'use_reference_delta': bool(context.use_reference_delta),
|
| 636 |
+
'image_selection': 'random',
|
| 637 |
+
'random_state': int(UMAP_IMAGE_RANDOM_STATE),
|
| 638 |
+
'selected_image_idx': [int(v) for v in context.selected_image_idx],
|
| 639 |
'feature_ids': [int(fid) for fid in context.features.column_feature_ids],
|
| 640 |
}
|
| 641 |
if context.pristine_features is not None:
|
|
|
|
| 643 |
return signature
|
| 644 |
|
| 645 |
|
| 646 |
+
def _build_images_umap_matrix(context: UmapContext, aggregation_mode: str) -> np.ndarray:
|
| 647 |
+
codes = _umap_codes_for_signature(context)
|
| 648 |
+
return build_image_feature_matrix_from_image_idx(
|
| 649 |
+
codes,
|
| 650 |
+
context.image_idx_arr,
|
| 651 |
+
context.n_images_used,
|
| 652 |
+
aggregation_mode,
|
| 653 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
|
| 655 |
+
|
| 656 |
+
def get_or_compute_umap(context: UmapContext, aggregation_mode: str) -> dict[str, object]:
|
| 657 |
+
cache_id = _images_umap_cache_id(context, aggregation_mode)
|
|
|
|
|
|
|
|
|
|
| 658 |
|
| 659 |
return get_or_compute_umap_with_builder(
|
| 660 |
+
lambda: _build_images_umap_matrix(context, aggregation_mode),
|
|
|
|
| 661 |
UMAP_PARAMS,
|
| 662 |
cache_dir=context.umap_cache_dir,
|
| 663 |
cache_id=cache_id,
|
|
|
|
| 665 |
)
|
| 666 |
|
| 667 |
|
| 668 |
+
def get_or_compute_feature_umap(
|
| 669 |
+
metric: str,
|
| 670 |
+
model_key: str,
|
| 671 |
+
dataset: str,
|
| 672 |
+
umap_cache_dir: Path,
|
| 673 |
+
) -> dict[str, object]:
|
| 674 |
+
cache_id = 'features_grid'
|
|
|
|
|
|
|
| 675 |
|
| 676 |
def _build_atoms() -> np.ndarray:
|
| 677 |
+
atoms, _aligned = _resolve_feature_atoms_cached(metric, model_key, dataset)
|
| 678 |
return atoms
|
| 679 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 680 |
info = get_or_compute_umap_with_builder(
|
|
|
|
| 681 |
_build_atoms,
|
| 682 |
UMAP_PARAMS,
|
| 683 |
+
cache_dir=umap_cache_dir,
|
| 684 |
cache_id=cache_id,
|
| 685 |
+
cache_signature=_feature_umap_cache_signature(metric, model_key, dataset),
|
| 686 |
)
|
| 687 |
+
_atoms, aligned = _resolve_feature_atoms_cached(metric, model_key, dataset)
|
| 688 |
expected_n = len(aligned)
|
| 689 |
emb = np.asarray(info['embedding_2d'])
|
| 690 |
if emb.shape[0] != expected_n:
|
|
|
|
| 712 |
of the code (including `context`, `feature_ids`, `unique_categories`, and
|
| 713 |
cache-related fields returned by the UMAP computation).
|
| 714 |
"""
|
| 715 |
+
context = load_umap_context(metric, dataset, model_key, use_reference_delta=False)
|
| 716 |
+
_atoms, aligned_feature_ids = _resolve_feature_atoms_cached(context.metric, context.model_key, dataset)
|
| 717 |
aligned_feature_ids = list(aligned_feature_ids)
|
| 718 |
+
info = get_or_compute_feature_umap(context.metric, context.model_key, dataset, context.umap_cache_dir)
|
| 719 |
emb = np.asarray(info['embedding_2d'])
|
| 720 |
n_points = min(int(emb.shape[0]), len(aligned_feature_ids))
|
| 721 |
emb = emb[:n_points]
|
|
|
|
| 824 |
|
| 825 |
@lru_cache(maxsize=32)
|
| 826 |
def activation_feature_id_set(metric: str, dataset: str, model_key: str) -> frozenset[int]:
|
| 827 |
+
context = load_umap_context(metric, dataset, model_key, use_reference_delta=False)
|
| 828 |
return global_id_set(context.features.column_feature_ids)
|
| 829 |
|
| 830 |
|
|
|
|
| 925 |
|
| 926 |
|
| 927 |
def format_feature_umap_status(info: dict[str, object], color_mode: str) -> str:
|
|
|
|
|
|
|
| 928 |
corr_source = str(info.get('feature_corr_source', DEFAULT_FEATURE_CORR_SOURCE))
|
| 929 |
+
return f'features=decoder_atoms | color=corr({corr_source}) | X={info.get("x_shape")}'
|
| 930 |
|
| 931 |
|
| 932 |
def format_image_color_mode(color_mode: str) -> str:
|
dashboard/user_image_service.py
CHANGED
|
@@ -19,7 +19,7 @@ from analysis.models import load_iqa_model, load_sae, read_sae_config
|
|
| 19 |
import analysis.models as _analysis_models
|
| 20 |
from analysis.viz.vis_heatmaps import visualize_feature_heatmaps, visualize_feature_heatmaps_from_images
|
| 21 |
from dashboard.image_utils import png_bytes_to_data_url
|
| 22 |
-
from dashboard.model_catalog import
|
| 23 |
|
| 24 |
|
| 25 |
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
@@ -38,24 +38,6 @@ class FeatureMapRequest:
|
|
| 38 |
upload_filename: str | None = None
|
| 39 |
|
| 40 |
|
| 41 |
-
def _resolve_checkpoint_path(model_path: str | Path) -> Path:
|
| 42 |
-
model_root = Path(model_path)
|
| 43 |
-
if model_root.is_file():
|
| 44 |
-
return model_root
|
| 45 |
-
|
| 46 |
-
candidate_dirs: list[Path] = []
|
| 47 |
-
for pattern in ("checkpoint-*", "checkpoints/checkpoint-*", "checkpoint"):
|
| 48 |
-
candidate_dirs.extend(p for p in model_root.glob(pattern) if p.is_dir())
|
| 49 |
-
if not candidate_dirs:
|
| 50 |
-
return model_root
|
| 51 |
-
|
| 52 |
-
def _checkpoint_sort_key(path: Path) -> tuple[int, str]:
|
| 53 |
-
digits = "".join(ch for ch in path.name if ch.isdigit())
|
| 54 |
-
numeric = int(digits) if digits else -1
|
| 55 |
-
return numeric, str(path)
|
| 56 |
-
|
| 57 |
-
return max(candidate_dirs, key=_checkpoint_sort_key)
|
| 58 |
-
|
| 59 |
|
| 60 |
def _assets_url(path: Path) -> str:
|
| 61 |
rel = path.relative_to(ASSETS_DIR)
|
|
@@ -213,12 +195,9 @@ def list_baseline_examples(model_key: str) -> list[str]:
|
|
| 213 |
|
| 214 |
|
| 215 |
@lru_cache(maxsize=16)
|
| 216 |
-
def _load_models(metric: str, model_key: str, device: str):
|
| 217 |
-
record =
|
| 218 |
-
|
| 219 |
-
raise FileNotFoundError(f"Model {model_key!r} not found for metric {metric!r}")
|
| 220 |
-
|
| 221 |
-
checkpoint_path = _resolve_checkpoint_path(record.model_path)
|
| 222 |
sae_cfg = read_sae_config(str(checkpoint_path))
|
| 223 |
|
| 224 |
layer_num = int(sae_cfg.get("layer_num", 3))
|
|
@@ -282,10 +261,11 @@ def _codes_for_single_image(
|
|
| 282 |
*,
|
| 283 |
metric: str,
|
| 284 |
model_key: str,
|
|
|
|
| 285 |
image_path: Path,
|
| 286 |
) -> tuple[pd.DataFrame, FeatureMatrix, int]:
|
| 287 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 288 |
-
models = _load_models(metric, model_key, device)
|
| 289 |
|
| 290 |
iqa = models["iqa"]
|
| 291 |
layer_name = str(models["layer_name"])
|
|
@@ -333,6 +313,7 @@ def _resolve_overlay_activations(
|
|
| 333 |
) -> tuple[pd.DataFrame, FeatureMatrix, int]:
|
| 334 |
metric = str(request.metric).strip().upper()
|
| 335 |
model_key = str(request.model_key).strip()
|
|
|
|
| 336 |
|
| 337 |
meta_df: pd.DataFrame | None = None
|
| 338 |
features: FeatureMatrix | None = None
|
|
@@ -346,7 +327,7 @@ def _resolve_overlay_activations(
|
|
| 346 |
if meta_df is None or features is None or crop_size is None:
|
| 347 |
if upload_image is not None:
|
| 348 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 349 |
-
models = _load_models(metric, model_key, device)
|
| 350 |
iqa = models["iqa"]
|
| 351 |
layer_name = str(models["layer_name"])
|
| 352 |
sae = models["sae"]
|
|
@@ -383,6 +364,7 @@ def _resolve_overlay_activations(
|
|
| 383 |
meta_df, features, crop_size = _codes_for_single_image(
|
| 384 |
metric=metric,
|
| 385 |
model_key=model_key,
|
|
|
|
| 386 |
image_path=image_path,
|
| 387 |
)
|
| 388 |
if request.baseline_image_path and image_path is not None and str(image_path).startswith(str(ASSETS_DIR)):
|
|
@@ -436,7 +418,7 @@ def render_feature_overlay_png(request: FeatureMapRequest) -> bytes | None:
|
|
| 436 |
patches_per_image=None,
|
| 437 |
crop_size=crop_size,
|
| 438 |
img_size_inches=4.5,
|
| 439 |
-
|
| 440 |
save_dir=None,
|
| 441 |
show_img=False,
|
| 442 |
)
|
|
|
|
| 19 |
import analysis.models as _analysis_models
|
| 20 |
from analysis.viz.vis_heatmaps import visualize_feature_heatmaps, visualize_feature_heatmaps_from_images
|
| 21 |
from dashboard.image_utils import png_bytes_to_data_url
|
| 22 |
+
from dashboard.model_catalog import get_model_record, require_model_record
|
| 23 |
|
| 24 |
|
| 25 |
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
| 38 |
upload_filename: str | None = None
|
| 39 |
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
def _assets_url(path: Path) -> str:
|
| 43 |
rel = path.relative_to(ASSETS_DIR)
|
|
|
|
| 195 |
|
| 196 |
|
| 197 |
@lru_cache(maxsize=16)
|
| 198 |
+
def _load_models(metric: str, model_key: str, dataset: str, device: str):
|
| 199 |
+
record = require_model_record(metric, model_key)
|
| 200 |
+
checkpoint_path = Path(record.checkpoint_path)
|
|
|
|
|
|
|
|
|
|
| 201 |
sae_cfg = read_sae_config(str(checkpoint_path))
|
| 202 |
|
| 203 |
layer_num = int(sae_cfg.get("layer_num", 3))
|
|
|
|
| 261 |
*,
|
| 262 |
metric: str,
|
| 263 |
model_key: str,
|
| 264 |
+
dataset: str,
|
| 265 |
image_path: Path,
|
| 266 |
) -> tuple[pd.DataFrame, FeatureMatrix, int]:
|
| 267 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 268 |
+
models = _load_models(metric, model_key, dataset, device)
|
| 269 |
|
| 270 |
iqa = models["iqa"]
|
| 271 |
layer_name = str(models["layer_name"])
|
|
|
|
| 313 |
) -> tuple[pd.DataFrame, FeatureMatrix, int]:
|
| 314 |
metric = str(request.metric).strip().upper()
|
| 315 |
model_key = str(request.model_key).strip()
|
| 316 |
+
dataset = str(request.dataset).strip()
|
| 317 |
|
| 318 |
meta_df: pd.DataFrame | None = None
|
| 319 |
features: FeatureMatrix | None = None
|
|
|
|
| 327 |
if meta_df is None or features is None or crop_size is None:
|
| 328 |
if upload_image is not None:
|
| 329 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 330 |
+
models = _load_models(metric, model_key, dataset, device)
|
| 331 |
iqa = models["iqa"]
|
| 332 |
layer_name = str(models["layer_name"])
|
| 333 |
sae = models["sae"]
|
|
|
|
| 364 |
meta_df, features, crop_size = _codes_for_single_image(
|
| 365 |
metric=metric,
|
| 366 |
model_key=model_key,
|
| 367 |
+
dataset=dataset,
|
| 368 |
image_path=image_path,
|
| 369 |
)
|
| 370 |
if request.baseline_image_path and image_path is not None and str(image_path).startswith(str(ASSETS_DIR)):
|
|
|
|
| 418 |
patches_per_image=None,
|
| 419 |
crop_size=crop_size,
|
| 420 |
img_size_inches=4.5,
|
| 421 |
+
show_mask=False,
|
| 422 |
save_dir=None,
|
| 423 |
show_img=False,
|
| 424 |
)
|
dashboard/viz_helpers.py
CHANGED
|
@@ -8,8 +8,8 @@ from dash import dcc, html
|
|
| 8 |
|
| 9 |
from dashboard.model_catalog import (
|
| 10 |
MODEL_FAMILIES,
|
| 11 |
-
discover_models_for_metric,
|
| 12 |
discover_supported_datasets,
|
|
|
|
| 13 |
load_selector_cache_feature_table,
|
| 14 |
summarize_selector_cache,
|
| 15 |
)
|
|
@@ -51,17 +51,17 @@ def feature_rows_for_selector(
|
|
| 51 |
if not selector_name:
|
| 52 |
return []
|
| 53 |
|
| 54 |
-
record =
|
| 55 |
if record is None:
|
| 56 |
return []
|
| 57 |
|
| 58 |
-
summaries = summarize_selector_cache(record.
|
| 59 |
summary_by_name = {summary.selector_name: summary for summary in summaries}
|
| 60 |
selected_summary = summary_by_name.get(selector_name, summaries[0] if summaries else None)
|
| 61 |
if selected_summary is None:
|
| 62 |
return []
|
| 63 |
|
| 64 |
-
feature_table = load_selector_cache_feature_table(selected_summary.cache_path)
|
| 65 |
if feature_table.empty:
|
| 66 |
return []
|
| 67 |
|
|
@@ -86,14 +86,14 @@ def _feature_comparison_rows_cached(
|
|
| 86 |
feature_id: int,
|
| 87 |
active_selector_name: str | None,
|
| 88 |
) -> list[dict[str, object]]:
|
| 89 |
-
record =
|
| 90 |
if record is None:
|
| 91 |
return []
|
| 92 |
|
| 93 |
-
summaries = summarize_selector_cache(record.
|
| 94 |
comparison_rows: list[dict[str, object]] = []
|
| 95 |
for summary in summaries:
|
| 96 |
-
feature_table = load_selector_cache_feature_table(summary.cache_path)
|
| 97 |
if feature_table.empty:
|
| 98 |
feature_score = None
|
| 99 |
feature_rank = None
|
|
@@ -242,14 +242,12 @@ def select_feature_id(
|
|
| 242 |
|
| 243 |
def feature_unavailable_message(
|
| 244 |
metric: str,
|
| 245 |
-
dataset: str,
|
| 246 |
model_key: str,
|
| 247 |
feature_id: int,
|
| 248 |
*,
|
| 249 |
-
activation_dataset: str
|
| 250 |
) -> str | None:
|
| 251 |
-
|
| 252 |
-
if feature_in_activation_cache(metric, cache_dataset, model_key, int(feature_id)):
|
| 253 |
return None
|
| 254 |
return FILTERED_FEATURE_MESSAGE
|
| 255 |
|
|
@@ -258,6 +256,60 @@ def feature_empty_state(message: str) -> html.Div:
|
|
| 258 |
return html.Div(message, className="feature-empty")
|
| 259 |
|
| 260 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
def feature_detail_children(
|
| 262 |
selector_name: str,
|
| 263 |
feature_rows: list[dict[str, object]],
|
|
|
|
| 8 |
|
| 9 |
from dashboard.model_catalog import (
|
| 10 |
MODEL_FAMILIES,
|
|
|
|
| 11 |
discover_supported_datasets,
|
| 12 |
+
get_model_record,
|
| 13 |
load_selector_cache_feature_table,
|
| 14 |
summarize_selector_cache,
|
| 15 |
)
|
|
|
|
| 51 |
if not selector_name:
|
| 52 |
return []
|
| 53 |
|
| 54 |
+
record = get_model_record(metric, model_key)
|
| 55 |
if record is None:
|
| 56 |
return []
|
| 57 |
|
| 58 |
+
summaries = summarize_selector_cache(record.checkpoint_path, dataset)
|
| 59 |
summary_by_name = {summary.selector_name: summary for summary in summaries}
|
| 60 |
selected_summary = summary_by_name.get(selector_name, summaries[0] if summaries else None)
|
| 61 |
if selected_summary is None:
|
| 62 |
return []
|
| 63 |
|
| 64 |
+
feature_table = load_selector_cache_feature_table(str(selected_summary.cache_path))
|
| 65 |
if feature_table.empty:
|
| 66 |
return []
|
| 67 |
|
|
|
|
| 86 |
feature_id: int,
|
| 87 |
active_selector_name: str | None,
|
| 88 |
) -> list[dict[str, object]]:
|
| 89 |
+
record = get_model_record(metric, model_key)
|
| 90 |
if record is None:
|
| 91 |
return []
|
| 92 |
|
| 93 |
+
summaries = summarize_selector_cache(record.checkpoint_path, dataset)
|
| 94 |
comparison_rows: list[dict[str, object]] = []
|
| 95 |
for summary in summaries:
|
| 96 |
+
feature_table = load_selector_cache_feature_table(str(summary.cache_path))
|
| 97 |
if feature_table.empty:
|
| 98 |
feature_score = None
|
| 99 |
feature_rank = None
|
|
|
|
| 242 |
|
| 243 |
def feature_unavailable_message(
|
| 244 |
metric: str,
|
|
|
|
| 245 |
model_key: str,
|
| 246 |
feature_id: int,
|
| 247 |
*,
|
| 248 |
+
activation_dataset: str,
|
| 249 |
) -> str | None:
|
| 250 |
+
if feature_in_activation_cache(metric, activation_dataset, model_key, int(feature_id)):
|
|
|
|
| 251 |
return None
|
| 252 |
return FILTERED_FEATURE_MESSAGE
|
| 253 |
|
|
|
|
| 256 |
return html.Div(message, className="feature-empty")
|
| 257 |
|
| 258 |
|
| 259 |
+
def build_feature_detail_content(
|
| 260 |
+
metric: str,
|
| 261 |
+
dataset: str,
|
| 262 |
+
model_key: str,
|
| 263 |
+
feature_id: int | None,
|
| 264 |
+
selector_name: str | None = None,
|
| 265 |
+
) -> list[object] | html.Div:
|
| 266 |
+
record = get_model_record(metric, model_key)
|
| 267 |
+
if record is None:
|
| 268 |
+
return html.Div("No selector cache files were found for this model.", className="feature-empty")
|
| 269 |
+
|
| 270 |
+
if not selector_name:
|
| 271 |
+
summaries = summarize_selector_cache(record.checkpoint_path, dataset)
|
| 272 |
+
if not summaries:
|
| 273 |
+
return html.Div("No selector cache files were found for this model.", className="feature-empty")
|
| 274 |
+
selector_name = summaries[0].selector_name
|
| 275 |
+
|
| 276 |
+
rows = feature_rows_for_selector(metric, dataset, model_key, selector_name)
|
| 277 |
+
if not rows:
|
| 278 |
+
return html.Div("No selector cache files were found for this model.", className="feature-empty")
|
| 279 |
+
|
| 280 |
+
selected_ids = feature_ids(rows)
|
| 281 |
+
if not selected_ids:
|
| 282 |
+
return html.Div("No selector cache files were found for this model.", className="feature-empty")
|
| 283 |
+
|
| 284 |
+
try:
|
| 285 |
+
selected_feature_id = int(feature_id)
|
| 286 |
+
except (TypeError, ValueError):
|
| 287 |
+
selected_feature_id = selected_ids[0]
|
| 288 |
+
|
| 289 |
+
unavailable = feature_unavailable_message(
|
| 290 |
+
metric,
|
| 291 |
+
model_key,
|
| 292 |
+
selected_feature_id,
|
| 293 |
+
activation_dataset=UMAP_DATASET,
|
| 294 |
+
)
|
| 295 |
+
if unavailable is not None:
|
| 296 |
+
return feature_empty_state(unavailable)
|
| 297 |
+
|
| 298 |
+
comparison_rows = feature_comparison_rows(
|
| 299 |
+
metric,
|
| 300 |
+
dataset,
|
| 301 |
+
model_key,
|
| 302 |
+
selected_feature_id,
|
| 303 |
+
selector_name,
|
| 304 |
+
)
|
| 305 |
+
return feature_detail_children(
|
| 306 |
+
selector_name or "selector",
|
| 307 |
+
rows,
|
| 308 |
+
selected_feature_id,
|
| 309 |
+
comparison_rows,
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
|
| 313 |
def feature_detail_children(
|
| 314 |
selector_name: str,
|
| 315 |
feature_rows: list[dict[str, object]],
|
default_configs/06_config.json
CHANGED
|
@@ -4,6 +4,7 @@
|
|
| 4 |
"DATASET_ROOT": "/home_2/28m_gov@lab.graphicon.ru/SAE/Kadid",
|
| 5 |
"CACHE_DIR": "/home_2/28m_gov@lab.graphicon.ru/SAE/xIQA/logs/arniqa_logs/ARNIQA_layer5_lambda5e-4_scale1_exp37/cache",
|
| 6 |
"IMAGE_LIMIT": 1000,
|
|
|
|
| 7 |
"AGGREGATION_MODES": ["mean", "max", "sum"],
|
| 8 |
"DEFAULT_AGGREGATION_MODE": "mean",
|
| 9 |
"UMAP_PARAMS": {
|
|
|
|
| 4 |
"DATASET_ROOT": "/home_2/28m_gov@lab.graphicon.ru/SAE/Kadid",
|
| 5 |
"CACHE_DIR": "/home_2/28m_gov@lab.graphicon.ru/SAE/xIQA/logs/arniqa_logs/ARNIQA_layer5_lambda5e-4_scale1_exp37/cache",
|
| 6 |
"IMAGE_LIMIT": 1000,
|
| 7 |
+
"IMAGE_RANDOM_STATE": 42,
|
| 8 |
"AGGREGATION_MODES": ["mean", "max", "sum"],
|
| 9 |
"DEFAULT_AGGREGATION_MODE": "mean",
|
| 10 |
"UMAP_PARAMS": {
|
default_configs/sae_vis_config.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
-
"SAE_CHECKPOINT_PATH": "/
|
| 3 |
-
"DATASETS_ROOT": "/
|
| 4 |
-
"CACHE_DIR": "/
|
| 5 |
"DATASET": "kadid10k",
|
| 6 |
"CROP_SIZE": 224,
|
| 7 |
"DOWNSCALE_FACTOR": 2,
|
|
@@ -9,10 +9,6 @@
|
|
| 9 |
"NUM_WORKERS": 4,
|
| 10 |
"DEVICE": "cuda",
|
| 11 |
"ACTIVATION_STEPS_TO_KEEP": [],
|
| 12 |
-
"SUPPORTED_DATASETS": [
|
| 13 |
-
"kadid10k",
|
| 14 |
-
"local_kadid"
|
| 15 |
-
],
|
| 16 |
"CORR_TOP_K": 30,
|
| 17 |
"HEATMAP_CRITERION": "max",
|
| 18 |
"N_TOP_FEATURES_HEATMAPS": 3,
|
|
|
|
| 1 |
{
|
| 2 |
+
"SAE_CHECKPOINT_PATH": "/home/28m_gov@lab.graphicon.ru/SAE/xIQA/logs/ARNIQA_layer3_lambda1e-5_scale1_exp2/checkpoint-200200",
|
| 3 |
+
"DATASETS_ROOT": "/home/28m_gov@lab.graphicon.ru/SAE/Kadid",
|
| 4 |
+
"CACHE_DIR": "/home/28m_gov@lab.graphicon.ru/SAE/xIQA/logs/ARNIQA_layer3_lambda1e-5_scale1_exp2/checkpoint-200200/cache",
|
| 5 |
"DATASET": "kadid10k",
|
| 6 |
"CROP_SIZE": 224,
|
| 7 |
"DOWNSCALE_FACTOR": 2,
|
|
|
|
| 9 |
"NUM_WORKERS": 4,
|
| 10 |
"DEVICE": "cuda",
|
| 11 |
"ACTIVATION_STEPS_TO_KEEP": [],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
"CORR_TOP_K": 30,
|
| 13 |
"HEATMAP_CRITERION": "max",
|
| 14 |
"N_TOP_FEATURES_HEATMAPS": 3,
|
pages/feature_maps.py
CHANGED
|
@@ -12,7 +12,7 @@ from dashboard.user_image_service import (
|
|
| 12 |
list_baseline_example_records,
|
| 13 |
render_feature_overlay,
|
| 14 |
)
|
| 15 |
-
from dashboard.viz_helpers import visualization_params
|
| 16 |
|
| 17 |
|
| 18 |
ROOT = Path(__file__).resolve().parents[1]
|
|
@@ -103,6 +103,25 @@ layout = html.Div(
|
|
| 103 |
debounce=True,
|
| 104 |
className="feature-jump-input",
|
| 105 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
html.Div(
|
| 107 |
id="fm-status",
|
| 108 |
className="home-status",
|
|
@@ -290,3 +309,28 @@ def update_overlay(
|
|
| 290 |
return "", "Overlay unavailable."
|
| 291 |
return overlay_url, ""
|
| 292 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
list_baseline_example_records,
|
| 13 |
render_feature_overlay,
|
| 14 |
)
|
| 15 |
+
from dashboard.viz_helpers import build_feature_detail_content, visualization_params
|
| 16 |
|
| 17 |
|
| 18 |
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
| 103 |
debounce=True,
|
| 104 |
className="feature-jump-input",
|
| 105 |
),
|
| 106 |
+
html.Div(
|
| 107 |
+
dcc.Loading(
|
| 108 |
+
id="fm-feature-detail-loading",
|
| 109 |
+
type="default",
|
| 110 |
+
color="#2563eb",
|
| 111 |
+
className="loading-block feature-detail-body-loading",
|
| 112 |
+
children=html.Div(
|
| 113 |
+
id="fm-feature-detail-body",
|
| 114 |
+
className="feature-detail-card",
|
| 115 |
+
children=[
|
| 116 |
+
html.Div(
|
| 117 |
+
"Enter a feature id to see statistics.",
|
| 118 |
+
className="feature-empty",
|
| 119 |
+
),
|
| 120 |
+
],
|
| 121 |
+
),
|
| 122 |
+
),
|
| 123 |
+
style={"marginTop": "16px"},
|
| 124 |
+
),
|
| 125 |
html.Div(
|
| 126 |
id="fm-status",
|
| 127 |
className="home-status",
|
|
|
|
| 309 |
return "", "Overlay unavailable."
|
| 310 |
return overlay_url, ""
|
| 311 |
|
| 312 |
+
|
| 313 |
+
@callback(
|
| 314 |
+
Output("fm-feature-detail-body", "children"),
|
| 315 |
+
Input("_pages_location", "search"),
|
| 316 |
+
Input("fm-feature-id", "value"),
|
| 317 |
+
)
|
| 318 |
+
def render_fm_feature_detail(search: str | None, feature_id_value):
|
| 319 |
+
params = visualization_params(search)
|
| 320 |
+
if params is None:
|
| 321 |
+
return html.Div("Missing page parameters.", className="feature-empty")
|
| 322 |
+
|
| 323 |
+
metric, dataset, model_key = params
|
| 324 |
+
if not model_key:
|
| 325 |
+
return html.Div("Select a model on the Home page first.", className="feature-empty")
|
| 326 |
+
|
| 327 |
+
try:
|
| 328 |
+
feature_id = int(feature_id_value) if feature_id_value is not None else None
|
| 329 |
+
except (TypeError, ValueError):
|
| 330 |
+
return html.Div("Enter a valid feature id.", className="feature-empty")
|
| 331 |
+
|
| 332 |
+
if feature_id is None:
|
| 333 |
+
return html.Div("Enter a feature id to see statistics.", className="feature-empty")
|
| 334 |
+
|
| 335 |
+
return build_feature_detail_content(metric, dataset, model_key, feature_id)
|
| 336 |
+
|
pages/visualizations.py
CHANGED
|
@@ -9,7 +9,7 @@ from dash import Input, Output, State, callback, clientside_callback, ctx, dcc,
|
|
| 9 |
from dashboard.image_utils import get_top_feature_overlays, overlay_artifact_data_url
|
| 10 |
from dashboard.layout import page_back_nav
|
| 11 |
from dashboard.model_catalog import (
|
| 12 |
-
|
| 13 |
summarize_model_record,
|
| 14 |
summarize_selector_cache,
|
| 15 |
)
|
|
@@ -31,10 +31,9 @@ from dashboard.umap_service import (
|
|
| 31 |
format_umap_status,
|
| 32 |
)
|
| 33 |
from dashboard.viz_helpers import (
|
|
|
|
| 34 |
cached_hover_meta_line,
|
| 35 |
cached_hover_thumb,
|
| 36 |
-
feature_comparison_rows,
|
| 37 |
-
feature_detail_children,
|
| 38 |
feature_empty_state,
|
| 39 |
feature_ids,
|
| 40 |
feature_rows_for_selector,
|
|
@@ -506,9 +505,9 @@ layout = html.Div(
|
|
| 506 |
),
|
| 507 |
dcc.Input(
|
| 508 |
id="feature-jump-input",
|
| 509 |
-
type="
|
| 510 |
-
|
| 511 |
-
|
| 512 |
debounce=True,
|
| 513 |
placeholder="Jump to feature id",
|
| 514 |
className="feature-jump-input",
|
|
@@ -607,7 +606,7 @@ def render_model_info(search: str | None):
|
|
| 607 |
return html.Div("Missing visualization parameters.", className="feature-empty")
|
| 608 |
|
| 609 |
metric, selection_dataset, model_key = params
|
| 610 |
-
record =
|
| 611 |
|
| 612 |
children = [
|
| 613 |
html.Div([html.Span("Metric: "), html.Strong(metric)]),
|
|
@@ -624,7 +623,7 @@ def render_model_info(search: str | None):
|
|
| 624 |
try:
|
| 625 |
from dashboard.model_catalog import summarize_feature_filter_cache
|
| 626 |
|
| 627 |
-
filter_lines = summarize_feature_filter_cache(record.
|
| 628 |
except Exception:
|
| 629 |
filter_lines = ["Feature filter summary unavailable"]
|
| 630 |
|
|
@@ -653,11 +652,11 @@ def init_feature_selector(search: str | None):
|
|
| 653 |
return [], None, True
|
| 654 |
|
| 655 |
metric, selection_dataset, model_key = params
|
| 656 |
-
record =
|
| 657 |
if record is None:
|
| 658 |
return [], None, True
|
| 659 |
|
| 660 |
-
summaries = summarize_selector_cache(record.
|
| 661 |
if not summaries:
|
| 662 |
return [], None, True
|
| 663 |
|
|
@@ -918,10 +917,9 @@ def display_umap_hover_content(hover_data, search: str | None, umap_mode: str |
|
|
| 918 |
|
| 919 |
params = visualization_params(search)
|
| 920 |
if params is not None:
|
| 921 |
-
metric,
|
| 922 |
unavailable = feature_unavailable_message(
|
| 923 |
metric,
|
| 924 |
-
selection_dataset,
|
| 925 |
model_key,
|
| 926 |
fid,
|
| 927 |
activation_dataset=UMAP_DATASET,
|
|
@@ -1053,13 +1051,11 @@ def update_selected_feature(
|
|
| 1053 |
requested_feature_id=requested_feature_id,
|
| 1054 |
)
|
| 1055 |
if selected_feature_id is not None:
|
| 1056 |
-
activation_dataset = UMAP_DATASET if trigger_id == "umap-graph" else None
|
| 1057 |
unavailable = feature_unavailable_message(
|
| 1058 |
metric,
|
| 1059 |
-
selection_dataset,
|
| 1060 |
model_key,
|
| 1061 |
int(selected_feature_id),
|
| 1062 |
-
activation_dataset=
|
| 1063 |
)
|
| 1064 |
if unavailable is not None:
|
| 1065 |
status = unavailable
|
|
@@ -1082,36 +1078,13 @@ def render_feature_detail(
|
|
| 1082 |
return html.Div("No selector cache files were found for this model.", className="feature-empty")
|
| 1083 |
|
| 1084 |
metric, selection_dataset, model_key = params
|
| 1085 |
-
|
| 1086 |
-
if not rows:
|
| 1087 |
-
return html.Div("No selector cache files were found for this model.", className="feature-empty")
|
| 1088 |
-
|
| 1089 |
-
selected_ids = feature_ids(rows)
|
| 1090 |
-
if not selected_ids:
|
| 1091 |
-
return html.Div("No selector cache files were found for this model.", className="feature-empty")
|
| 1092 |
-
|
| 1093 |
-
try:
|
| 1094 |
-
selected_feature_id = int(selected_feature_id)
|
| 1095 |
-
except (TypeError, ValueError):
|
| 1096 |
-
selected_feature_id = selected_ids[0]
|
| 1097 |
-
|
| 1098 |
-
unavailable = feature_unavailable_message(metric, selection_dataset, model_key, selected_feature_id)
|
| 1099 |
-
if unavailable is not None:
|
| 1100 |
-
return feature_empty_state(unavailable)
|
| 1101 |
-
|
| 1102 |
-
comparison_rows = feature_comparison_rows(
|
| 1103 |
metric,
|
| 1104 |
selection_dataset,
|
| 1105 |
model_key,
|
| 1106 |
selected_feature_id,
|
| 1107 |
selector_name,
|
| 1108 |
)
|
| 1109 |
-
return feature_detail_children(
|
| 1110 |
-
selector_name or "selector",
|
| 1111 |
-
rows,
|
| 1112 |
-
selected_feature_id,
|
| 1113 |
-
comparison_rows,
|
| 1114 |
-
)
|
| 1115 |
|
| 1116 |
|
| 1117 |
@callback(
|
|
@@ -1137,7 +1110,12 @@ def render_feature_top_images(
|
|
| 1137 |
except Exception:
|
| 1138 |
return _empty_feature_image_outputs("No feature selected.")
|
| 1139 |
|
| 1140 |
-
unavailable = feature_unavailable_message(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1141 |
if unavailable is not None:
|
| 1142 |
return _empty_feature_image_outputs(unavailable)
|
| 1143 |
|
|
|
|
| 9 |
from dashboard.image_utils import get_top_feature_overlays, overlay_artifact_data_url
|
| 10 |
from dashboard.layout import page_back_nav
|
| 11 |
from dashboard.model_catalog import (
|
| 12 |
+
get_model_record,
|
| 13 |
summarize_model_record,
|
| 14 |
summarize_selector_cache,
|
| 15 |
)
|
|
|
|
| 31 |
format_umap_status,
|
| 32 |
)
|
| 33 |
from dashboard.viz_helpers import (
|
| 34 |
+
build_feature_detail_content,
|
| 35 |
cached_hover_meta_line,
|
| 36 |
cached_hover_thumb,
|
|
|
|
|
|
|
| 37 |
feature_empty_state,
|
| 38 |
feature_ids,
|
| 39 |
feature_rows_for_selector,
|
|
|
|
| 505 |
),
|
| 506 |
dcc.Input(
|
| 507 |
id="feature-jump-input",
|
| 508 |
+
type="text",
|
| 509 |
+
inputMode="numeric",
|
| 510 |
+
pattern="[0-9]*",
|
| 511 |
debounce=True,
|
| 512 |
placeholder="Jump to feature id",
|
| 513 |
className="feature-jump-input",
|
|
|
|
| 606 |
return html.Div("Missing visualization parameters.", className="feature-empty")
|
| 607 |
|
| 608 |
metric, selection_dataset, model_key = params
|
| 609 |
+
record = get_model_record(metric, model_key)
|
| 610 |
|
| 611 |
children = [
|
| 612 |
html.Div([html.Span("Metric: "), html.Strong(metric)]),
|
|
|
|
| 623 |
try:
|
| 624 |
from dashboard.model_catalog import summarize_feature_filter_cache
|
| 625 |
|
| 626 |
+
filter_lines = summarize_feature_filter_cache(record.checkpoint_path, selection_dataset)
|
| 627 |
except Exception:
|
| 628 |
filter_lines = ["Feature filter summary unavailable"]
|
| 629 |
|
|
|
|
| 652 |
return [], None, True
|
| 653 |
|
| 654 |
metric, selection_dataset, model_key = params
|
| 655 |
+
record = get_model_record(metric, model_key)
|
| 656 |
if record is None:
|
| 657 |
return [], None, True
|
| 658 |
|
| 659 |
+
summaries = summarize_selector_cache(record.checkpoint_path, selection_dataset)
|
| 660 |
if not summaries:
|
| 661 |
return [], None, True
|
| 662 |
|
|
|
|
| 917 |
|
| 918 |
params = visualization_params(search)
|
| 919 |
if params is not None:
|
| 920 |
+
metric, _selection_dataset, model_key = params
|
| 921 |
unavailable = feature_unavailable_message(
|
| 922 |
metric,
|
|
|
|
| 923 |
model_key,
|
| 924 |
fid,
|
| 925 |
activation_dataset=UMAP_DATASET,
|
|
|
|
| 1051 |
requested_feature_id=requested_feature_id,
|
| 1052 |
)
|
| 1053 |
if selected_feature_id is not None:
|
|
|
|
| 1054 |
unavailable = feature_unavailable_message(
|
| 1055 |
metric,
|
|
|
|
| 1056 |
model_key,
|
| 1057 |
int(selected_feature_id),
|
| 1058 |
+
activation_dataset=UMAP_DATASET,
|
| 1059 |
)
|
| 1060 |
if unavailable is not None:
|
| 1061 |
status = unavailable
|
|
|
|
| 1078 |
return html.Div("No selector cache files were found for this model.", className="feature-empty")
|
| 1079 |
|
| 1080 |
metric, selection_dataset, model_key = params
|
| 1081 |
+
return build_feature_detail_content(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1082 |
metric,
|
| 1083 |
selection_dataset,
|
| 1084 |
model_key,
|
| 1085 |
selected_feature_id,
|
| 1086 |
selector_name,
|
| 1087 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1088 |
|
| 1089 |
|
| 1090 |
@callback(
|
|
|
|
| 1110 |
except Exception:
|
| 1111 |
return _empty_feature_image_outputs("No feature selected.")
|
| 1112 |
|
| 1113 |
+
unavailable = feature_unavailable_message(
|
| 1114 |
+
metric,
|
| 1115 |
+
model_key,
|
| 1116 |
+
feature_id,
|
| 1117 |
+
activation_dataset=UMAP_DATASET,
|
| 1118 |
+
)
|
| 1119 |
if unavailable is not None:
|
| 1120 |
return _empty_feature_image_outputs(unavailable)
|
| 1121 |
|
train_code/benjdataset.py
CHANGED
|
@@ -8,14 +8,14 @@ import torch
|
|
| 8 |
from torch.utils.data import Dataset, DataLoader
|
| 9 |
from torchvision import transforms # type:ignore
|
| 10 |
|
| 11 |
-
#
|
| 12 |
from train_code.aug_utils.utils_data import distort_images
|
| 13 |
from utils.utils_data import resize_crop
|
| 14 |
|
| 15 |
|
| 16 |
def collate_fn(data):
|
| 17 |
-
#
|
| 18 |
-
#
|
| 19 |
# images = torch.stack([resize_crop(example['image'], crop_size=CROP_SIZE,
|
| 20 |
# downscale_factor=DOWNSCALE_FACTOR) for example in data])
|
| 21 |
# print(data.keys())
|
|
@@ -61,8 +61,8 @@ class DeepFeaturesDataset(Dataset):
|
|
| 61 |
raw_image = resize_crop(raw_image, crop_size=self.crop_size, downscale_factor=self.downscale_factor)
|
| 62 |
if random.random() > self.prestine_prob:
|
| 63 |
try:
|
| 64 |
-
#
|
| 65 |
-
#
|
| 66 |
raw_image = distort_images(raw_image.clone())[0]
|
| 67 |
except Exception as e:
|
| 68 |
print(f'[Warning] Degradation operation failed due to {e}!')
|
|
@@ -82,8 +82,8 @@ def create_dataloaders(dataset_name: str,
|
|
| 82 |
token: str,
|
| 83 |
transform: Optional[Callable] = None) -> Tuple[DataLoader, DataLoader]:
|
| 84 |
# ds = load_dataset("timm/imagenet-1k-wds", split='train', cache_dir='D:/adapter_experiment/hf_cache', data_files='**/*-validation-*.tar')
|
| 85 |
-
#
|
| 86 |
-
#
|
| 87 |
|
| 88 |
ds = load_dataset(dataset_name, split=split, streaming=True, cache_dir='./datasets/')
|
| 89 |
# ds_data = datasets.Dataset.from_list(ds_head)
|
|
|
|
| 8 |
from torch.utils.data import Dataset, DataLoader
|
| 9 |
from torchvision import transforms # type:ignore
|
| 10 |
|
| 11 |
+
# ARNIQA distortions for augmentation
|
| 12 |
from train_code.aug_utils.utils_data import distort_images
|
| 13 |
from utils.utils_data import resize_crop
|
| 14 |
|
| 15 |
|
| 16 |
def collate_fn(data):
|
| 17 |
+
# TODO: reconsider whether resize/crop should live in __getitem__ instead
|
| 18 |
+
# Their code appears to do it the same way
|
| 19 |
# images = torch.stack([resize_crop(example['image'], crop_size=CROP_SIZE,
|
| 20 |
# downscale_factor=DOWNSCALE_FACTOR) for example in data])
|
| 21 |
# print(data.keys())
|
|
|
|
| 61 |
raw_image = resize_crop(raw_image, crop_size=self.crop_size, downscale_factor=self.downscale_factor)
|
| 62 |
if random.random() > self.prestine_prob:
|
| 63 |
try:
|
| 64 |
+
# During SAE training, the metric should see distorted images; use the ARNIQA paper distortion pipeline.
|
| 65 |
+
# aug_utils code is taken from the ARNIQA repo
|
| 66 |
raw_image = distort_images(raw_image.clone())[0]
|
| 67 |
except Exception as e:
|
| 68 |
print(f'[Warning] Degradation operation failed due to {e}!')
|
|
|
|
| 82 |
token: str,
|
| 83 |
transform: Optional[Callable] = None) -> Tuple[DataLoader, DataLoader]:
|
| 84 |
# ds = load_dataset("timm/imagenet-1k-wds", split='train', cache_dir='D:/adapter_experiment/hf_cache', data_files='**/*-validation-*.tar')
|
| 85 |
+
# The full dataset is huge (several TB); swap in any other data source. Commented out to avoid loading it by accident.
|
| 86 |
+
# Use a small dataset for testing
|
| 87 |
|
| 88 |
ds = load_dataset(dataset_name, split=split, streaming=True, cache_dir='./datasets/')
|
| 89 |
# ds_data = datasets.Dataset.from_list(ds_head)
|
train_code/timmdataset.py
CHANGED
|
@@ -6,13 +6,13 @@ import torch
|
|
| 6 |
from torch.utils.data import Dataset, DataLoader
|
| 7 |
from torchvision import transforms # type:ignore
|
| 8 |
|
| 9 |
-
#
|
| 10 |
from train_code.aug_utils.utils_data import distort_images
|
| 11 |
from utils.utils_data import resize_crop
|
| 12 |
|
| 13 |
def collate_fn(data):
|
| 14 |
-
#
|
| 15 |
-
#
|
| 16 |
# images = torch.stack([resize_crop(example['image'], crop_size=CROP_SIZE,
|
| 17 |
# downscale_factor=DOWNSCALE_FACTOR) for example in data])
|
| 18 |
# print(data.keys())
|
|
@@ -58,8 +58,8 @@ class DeepFeaturesDataset(Dataset):
|
|
| 58 |
raw_image = resize_crop(raw_image, crop_size=self.crop_size, downscale_factor=self.downscale_factor)
|
| 59 |
if random.random() > self.prestine_prob:
|
| 60 |
try:
|
| 61 |
-
#
|
| 62 |
-
#
|
| 63 |
raw_image = distort_images(raw_image.clone())[0]
|
| 64 |
except Exception as e:
|
| 65 |
print(f'[Warning] Degradation operation failed due to {e}!')
|
|
@@ -79,15 +79,15 @@ def create_dataloaders(dataset_name: str,
|
|
| 79 |
token: str,
|
| 80 |
transform: Optional[Callable] = None) -> Tuple[DataLoader, DataLoader]:
|
| 81 |
# ds = load_dataset("timm/imagenet-1k-wds", split='train', cache_dir='D:/adapter_experiment/hf_cache', data_files='**/*-validation-*.tar')
|
| 82 |
-
#
|
| 83 |
-
#
|
| 84 |
|
| 85 |
# ds = load_dataset(dataset_name, split=split, streaming=True, cache_dir='./datasets/')
|
| 86 |
# ds_head = list(ds.take(10))
|
| 87 |
# ds_data = datasets.Dataset.from_list(ds_head)
|
| 88 |
# dset = DeepFeaturesDataset(ds_data, prestine_prob = prestine_prob)
|
| 89 |
|
| 90 |
-
#
|
| 91 |
ds = load_dataset(dataset_name, split=split, cache_dir='/mgov/datasets/', token=token)
|
| 92 |
# ds = ds.select(range(10000)) # !!!!!!!!!!!
|
| 93 |
dset = DeepFeaturesDataset(
|
|
|
|
| 6 |
from torch.utils.data import Dataset, DataLoader
|
| 7 |
from torchvision import transforms # type:ignore
|
| 8 |
|
| 9 |
+
# ARNIQA distortions for augmentation
|
| 10 |
from train_code.aug_utils.utils_data import distort_images
|
| 11 |
from utils.utils_data import resize_crop
|
| 12 |
|
| 13 |
def collate_fn(data):
|
| 14 |
+
# TODO: reconsider whether resize/crop should live in __getitem__ instead
|
| 15 |
+
# Their code appears to do it the same way
|
| 16 |
# images = torch.stack([resize_crop(example['image'], crop_size=CROP_SIZE,
|
| 17 |
# downscale_factor=DOWNSCALE_FACTOR) for example in data])
|
| 18 |
# print(data.keys())
|
|
|
|
| 58 |
raw_image = resize_crop(raw_image, crop_size=self.crop_size, downscale_factor=self.downscale_factor)
|
| 59 |
if random.random() > self.prestine_prob:
|
| 60 |
try:
|
| 61 |
+
# During SAE training, the metric should see distorted images; use the ARNIQA paper distortion pipeline.
|
| 62 |
+
# aug_utils code is taken from the ARNIQA repo
|
| 63 |
raw_image = distort_images(raw_image.clone())[0]
|
| 64 |
except Exception as e:
|
| 65 |
print(f'[Warning] Degradation operation failed due to {e}!')
|
|
|
|
| 79 |
token: str,
|
| 80 |
transform: Optional[Callable] = None) -> Tuple[DataLoader, DataLoader]:
|
| 81 |
# ds = load_dataset("timm/imagenet-1k-wds", split='train', cache_dir='D:/adapter_experiment/hf_cache', data_files='**/*-validation-*.tar')
|
| 82 |
+
# The full dataset is huge (several TB); swap in any other data source. Commented out to avoid loading it by accident.
|
| 83 |
+
# Use a small dataset for testing
|
| 84 |
|
| 85 |
# ds = load_dataset(dataset_name, split=split, streaming=True, cache_dir='./datasets/')
|
| 86 |
# ds_head = list(ds.take(10))
|
| 87 |
# ds_data = datasets.Dataset.from_list(ds_head)
|
| 88 |
# dset = DeepFeaturesDataset(ds_data, prestine_prob = prestine_prob)
|
| 89 |
|
| 90 |
+
# Load only the first 1000 images for testing
|
| 91 |
ds = load_dataset(dataset_name, split=split, cache_dir='/mgov/datasets/', token=token)
|
| 92 |
# ds = ds.select(range(10000)) # !!!!!!!!!!!
|
| 93 |
dset = DeepFeaturesDataset(
|
train_code/train_script_arniqa.py
CHANGED
|
@@ -78,7 +78,7 @@ def parse_args():
|
|
| 78 |
help="Initialization of SAE weights with fixed norm.",
|
| 79 |
)
|
| 80 |
# parser.add_argument(
|
| 81 |
-
# "--sae_input_dim", #
|
| 82 |
# type=int,
|
| 83 |
# default=256,
|
| 84 |
# help="Dimentionality of activations.",
|
|
@@ -311,8 +311,8 @@ def get_layer_output_size(model: InferenceModel, device: Union[str, torch.device
|
|
| 311 |
|
| 312 |
def set_hook(model: InferenceModel, layer_num: int, layer_name: str) -> Tuple[torch.utils.hooks.RemovableHandle, dict]:
|
| 313 |
|
| 314 |
-
#
|
| 315 |
-
#
|
| 316 |
activations = {}
|
| 317 |
|
| 318 |
def getActivation(name):
|
|
@@ -331,7 +331,7 @@ def create_model(device: Union[str, torch.device], iqa_weight_dtype: torch.dtype
|
|
| 331 |
return model
|
| 332 |
|
| 333 |
# create_sae, setup_lambda_scaling, setup_optimizer_and_scheduler, extract_activations,
|
| 334 |
-
# compute_lambda, train_step, log_train_metrics, save_checkpoint
|
| 335 |
|
| 336 |
|
| 337 |
def save_sae_config(
|
|
@@ -340,19 +340,19 @@ def save_sae_config(
|
|
| 340 |
inner_dim: int,
|
| 341 |
output_dir: Optional[str] = None,
|
| 342 |
) -> None:
|
| 343 |
-
"""
|
| 344 |
-
|
| 345 |
"""
|
| 346 |
cfg = {
|
| 347 |
"iqa_metric": "arniqa-kadid",
|
| 348 |
"layer_num": args.layer_num,
|
| 349 |
"scaling_factor": args.scaling_factor,
|
| 350 |
-
#
|
| 351 |
"sae_type": args.sae_type,
|
| 352 |
"sae_input_dim": sae_input_dim,
|
| 353 |
"inner_dim": inner_dim,
|
| 354 |
"weight_norm_init": args.weight_norm_init,
|
| 355 |
-
# MatchingPursuitSAE-
|
| 356 |
"mp_threshold": args.mp_threshold,
|
| 357 |
"mp_normalize": bool(args.mp_normalize),
|
| 358 |
}
|
|
@@ -364,7 +364,7 @@ def save_sae_config(
|
|
| 364 |
print(f"SAE config saved to {path}")
|
| 365 |
|
| 366 |
|
| 367 |
-
# validate_epoch, train_epoch, fix_seed
|
| 368 |
|
| 369 |
|
| 370 |
def print_hyperparams(
|
|
|
|
| 78 |
help="Initialization of SAE weights with fixed norm.",
|
| 79 |
)
|
| 80 |
# parser.add_argument(
|
| 81 |
+
# "--sae_input_dim", # Determined automatically
|
| 82 |
# type=int,
|
| 83 |
# default=256,
|
| 84 |
# help="Dimentionality of activations.",
|
|
|
|
| 311 |
|
| 312 |
def set_hook(model: InferenceModel, layer_num: int, layer_name: str) -> Tuple[torch.utils.hooks.RemovableHandle, dict]:
|
| 313 |
|
| 314 |
+
# Register a hook on the target IQA layer to capture activations
|
| 315 |
+
# More on hooks: https://web.stanford.edu/~nanbhas/blog/forward-hooks-pytorch/
|
| 316 |
activations = {}
|
| 317 |
|
| 318 |
def getActivation(name):
|
|
|
|
| 331 |
return model
|
| 332 |
|
| 333 |
# create_sae, setup_lambda_scaling, setup_optimizer_and_scheduler, extract_activations,
|
| 334 |
+
# compute_lambda, train_step, log_train_metrics, save_checkpoint are imported from train_utils.py
|
| 335 |
|
| 336 |
|
| 337 |
def save_sae_config(
|
|
|
|
| 340 |
inner_dim: int,
|
| 341 |
output_dir: Optional[str] = None,
|
| 342 |
) -> None:
|
| 343 |
+
"""Save SAE hyperparameters to a JSON file next to checkpoints.
|
| 344 |
+
The format is sufficient to reproduce the model architecture without extra arguments.
|
| 345 |
"""
|
| 346 |
cfg = {
|
| 347 |
"iqa_metric": "arniqa-kadid",
|
| 348 |
"layer_num": args.layer_num,
|
| 349 |
"scaling_factor": args.scaling_factor,
|
| 350 |
+
# architecture
|
| 351 |
"sae_type": args.sae_type,
|
| 352 |
"sae_input_dim": sae_input_dim,
|
| 353 |
"inner_dim": inner_dim,
|
| 354 |
"weight_norm_init": args.weight_norm_init,
|
| 355 |
+
# MatchingPursuitSAE-specific parameters
|
| 356 |
"mp_threshold": args.mp_threshold,
|
| 357 |
"mp_normalize": bool(args.mp_normalize),
|
| 358 |
}
|
|
|
|
| 364 |
print(f"SAE config saved to {path}")
|
| 365 |
|
| 366 |
|
| 367 |
+
# validate_epoch, train_epoch, fix_seed are imported from train_utils.py
|
| 368 |
|
| 369 |
|
| 370 |
def print_hyperparams(
|
train_code/train_script_liqe.py
CHANGED
|
@@ -361,7 +361,7 @@ def create_model(
|
|
| 361 |
|
| 362 |
|
| 363 |
# create_sae, setup_lambda_scaling, setup_optimizer_and_scheduler, extract_activations,
|
| 364 |
-
# compute_lambda, train_step, log_train_metrics, save_checkpoint
|
| 365 |
|
| 366 |
|
| 367 |
def save_sae_config(
|
|
|
|
| 361 |
|
| 362 |
|
| 363 |
# create_sae, setup_lambda_scaling, setup_optimizer_and_scheduler, extract_activations,
|
| 364 |
+
# compute_lambda, train_step, log_train_metrics, save_checkpoint are imported from train_utils.py
|
| 365 |
|
| 366 |
|
| 367 |
def save_sae_config(
|
train_code/train_script_maniqa.py
CHANGED
|
@@ -75,12 +75,12 @@ def get_layer_output_size(
|
|
| 75 |
|
| 76 |
|
| 77 |
def set_hook(model, layer_name: str, swin_num: int = 2, layer_num: int = 1) -> Tuple[Any, dict]:
|
| 78 |
-
"""
|
| 79 |
|
| 80 |
-
swin_num: 1 (swintransformer1, dim=768)
|
| 81 |
-
layer_num:
|
| 82 |
-
|
| 83 |
-
|
| 84 |
"""
|
| 85 |
activations: dict = {}
|
| 86 |
|
|
@@ -99,7 +99,7 @@ def save_sae_config(
|
|
| 99 |
inner_dim: int,
|
| 100 |
output_dir: Optional[str] = None,
|
| 101 |
) -> None:
|
| 102 |
-
"""
|
| 103 |
cfg = {
|
| 104 |
"iqa_metric": "maniqa",
|
| 105 |
"swin_num": args.swin_num,
|
|
@@ -494,7 +494,7 @@ def main():
|
|
| 494 |
save_sae_config(args, sae_input_dim, inner_dim)
|
| 495 |
|
| 496 |
dataloader_train, dataloader_test = create_dataloaders(
|
| 497 |
-
DATASET_NAME, 'train', 0, args.crop_size, 1, args.train_frac, bs, num_workers, collate_fn, token=args.hf_token) # Downscale=1
|
| 498 |
|
| 499 |
autoenc_model = create_sae(
|
| 500 |
sae_input_dim, inner_dim, weight_norm_init, device, weight_dtype,
|
|
@@ -503,7 +503,7 @@ def main():
|
|
| 503 |
mp_normalize=bool(args.mp_normalize),
|
| 504 |
)
|
| 505 |
|
| 506 |
-
#
|
| 507 |
_loss_base = get_loss_fn(args.sae_type)
|
| 508 |
_aux_alpha = args.aux_alpha
|
| 509 |
|
|
|
|
| 75 |
|
| 76 |
|
| 77 |
def set_hook(model, layer_name: str, swin_num: int = 2, layer_num: int = 1) -> Tuple[Any, dict]:
|
| 78 |
+
"""Register a hook on the selected MANIQA SwinTransformer BasicLayer.
|
| 79 |
|
| 80 |
+
swin_num: 1 (swintransformer1, dim=768) or 2 (swintransformer2, dim=384).
|
| 81 |
+
layer_num: BasicLayer index inside the selected SwinTransformer (0 or 1).
|
| 82 |
+
Layer output: (B, L, C) token sequence.
|
| 83 |
+
The hook flattens to (B*L, C) so the SAE can process individual tokens.
|
| 84 |
"""
|
| 85 |
activations: dict = {}
|
| 86 |
|
|
|
|
| 99 |
inner_dim: int,
|
| 100 |
output_dir: Optional[str] = None,
|
| 101 |
) -> None:
|
| 102 |
+
"""Save SAE hyperparameters to a JSON file next to checkpoints."""
|
| 103 |
cfg = {
|
| 104 |
"iqa_metric": "maniqa",
|
| 105 |
"swin_num": args.swin_num,
|
|
|
|
| 494 |
save_sae_config(args, sae_input_dim, inner_dim)
|
| 495 |
|
| 496 |
dataloader_train, dataloader_test = create_dataloaders(
|
| 497 |
+
DATASET_NAME, 'train', 0, args.crop_size, 1, args.train_frac, bs, num_workers, collate_fn, token=args.hf_token) # Downscale=1 as in the original; may be worth making configurable.
|
| 498 |
|
| 499 |
autoenc_model = create_sae(
|
| 500 |
sae_input_dim, inner_dim, weight_norm_init, device, weight_dtype,
|
|
|
|
| 503 |
mp_normalize=bool(args.mp_normalize),
|
| 504 |
)
|
| 505 |
|
| 506 |
+
# Choose the loss function based on SAE type
|
| 507 |
_loss_base = get_loss_fn(args.sae_type)
|
| 508 |
_aux_alpha = args.aux_alpha
|
| 509 |
|
train_code/train_utils.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
"""
|
| 3 |
|
| 4 |
import os
|
|
@@ -52,7 +52,7 @@ def fix_seed(seed: int) -> None:
|
|
| 52 |
torch.backends.cudnn.deterministic = True
|
| 53 |
torch.backends.cudnn.benchmark = False
|
| 54 |
|
| 55 |
-
#
|
| 56 |
|
| 57 |
def _extract_checkpoint_step(name: str) -> Optional[int]:
|
| 58 |
prefix = "checkpoint-"
|
|
@@ -117,7 +117,7 @@ def create_sae(
|
|
| 117 |
autoenc_model.train()
|
| 118 |
return autoenc_model
|
| 119 |
|
| 120 |
-
#
|
| 121 |
|
| 122 |
def create_optimizer(
|
| 123 |
model: torch.nn.Module,
|
|
@@ -203,7 +203,7 @@ def compute_lambda(
|
|
| 203 |
def calc_loss_reconstr(x: torch.Tensor, x_rec: torch.Tensor) -> torch.Tensor:
|
| 204 |
return torch.nn.functional.mse_loss(x_rec, x)
|
| 205 |
|
| 206 |
-
#
|
| 207 |
|
| 208 |
def extract_activations(
|
| 209 |
model: torch.nn.Module,
|
|
@@ -222,7 +222,7 @@ def extract_activations(
|
|
| 222 |
data *= scaling_factor
|
| 223 |
return data
|
| 224 |
|
| 225 |
-
#
|
| 226 |
|
| 227 |
def train_step(
|
| 228 |
autoenc_model: torch.nn.Module,
|
|
@@ -240,7 +240,7 @@ def train_step(
|
|
| 240 |
real_batch_size = data.shape[0]
|
| 241 |
optimizer.zero_grad()
|
| 242 |
|
| 243 |
-
#
|
| 244 |
perm = torch.randperm(real_batch_size, device=data.device)
|
| 245 |
data = data[perm[: int(len(perm) * sample_frac)]]
|
| 246 |
chunk_size = max(1, real_batch_size // grad_accum_steps)
|
|
|
|
| 1 |
+
"""Shared utilities for SAE training.
|
| 2 |
"""
|
| 3 |
|
| 4 |
import os
|
|
|
|
| 52 |
torch.backends.cudnn.deterministic = True
|
| 53 |
torch.backends.cudnn.benchmark = False
|
| 54 |
|
| 55 |
+
# Checkpoints
|
| 56 |
|
| 57 |
def _extract_checkpoint_step(name: str) -> Optional[int]:
|
| 58 |
prefix = "checkpoint-"
|
|
|
|
| 117 |
autoenc_model.train()
|
| 118 |
return autoenc_model
|
| 119 |
|
| 120 |
+
# Optimizers and schedulers
|
| 121 |
|
| 122 |
def create_optimizer(
|
| 123 |
model: torch.nn.Module,
|
|
|
|
| 203 |
def calc_loss_reconstr(x: torch.Tensor, x_rec: torch.Tensor) -> torch.Tensor:
|
| 204 |
return torch.nn.functional.mse_loss(x_rec, x)
|
| 205 |
|
| 206 |
+
# Activation extraction
|
| 207 |
|
| 208 |
def extract_activations(
|
| 209 |
model: torch.nn.Module,
|
|
|
|
| 222 |
data *= scaling_factor
|
| 223 |
return data
|
| 224 |
|
| 225 |
+
# Training
|
| 226 |
|
| 227 |
def train_step(
|
| 228 |
autoenc_model: torch.nn.Module,
|
|
|
|
| 240 |
real_batch_size = data.shape[0]
|
| 241 |
optimizer.zero_grad()
|
| 242 |
|
| 243 |
+
# Shuffle the batch and sample sample_frac of spatial positions
|
| 244 |
perm = torch.randperm(real_batch_size, device=data.device)
|
| 245 |
data = data[perm[: int(len(perm) * sample_frac)]]
|
| 246 |
chunk_size = max(1, real_batch_size // grad_accum_steps)
|