from __future__ import annotations import os import pickle from pathlib import Path from typing import Dict, List, Tuple import numpy as np EMBEDDING_DIM = 512 EMBEDDINGS_CACHE_DIR = Path(__file__).parent / "embeddings_cache" EMBEDDINGS_CACHE_DIR.mkdir(exist_ok=True) TASK_CONFIGS = { "task1": { "classes": ["normal", "pneumonia"], "pool_size": 200, "val_size": 50, "class_weights": [0.5, 0.5], }, "task2": { "classes": ["normal", "pneumonia", "covid", "tuberculosis"], "pool_size": 400, "val_size": 80, "class_weights": [0.4, 0.3, 0.2, 0.1], }, "task3": { "classes": ["normal", "nodule", "effusion", "pneumothorax"], "pool_size": 600, "val_size": 100, "class_weights": [0.85, 0.05, 0.05, 0.05], }, } def _generate_synthetic_embeddings( task_id: str, seed: int = 42 ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: rng = np.random.RandomState(seed) config = TASK_CONFIGS[task_id] classes = config["classes"] pool_size = config["pool_size"] val_size = config["val_size"] class_weights = config["class_weights"] n_classes = len(classes) total_size = pool_size + val_size class_centers = rng.randn(n_classes, EMBEDDING_DIM) * 0.2 all_embeddings = [] all_labels = [] for idx, (cls_name, weight) in enumerate(zip(classes, class_weights)): n_samples = int(total_size * weight) noise = rng.randn(n_samples, EMBEDDING_DIM) * 5.0 embeddings = class_centers[idx] + noise all_embeddings.append(embeddings) all_labels.extend([cls_name] * n_samples) all_embeddings_arr = np.vstack(all_embeddings).astype(np.float32) total_actual = all_embeddings_arr.shape[0] actual_pool_size = min(pool_size, total_actual - val_size) perm = rng.permutation(total_actual) pool_idx = perm[:actual_pool_size] val_idx = perm[actual_pool_size : actual_pool_size + val_size] pool_embeddings = all_embeddings_arr[pool_idx] pool_labels = np.array(all_labels)[pool_idx] val_embeddings = all_embeddings_arr[val_idx] val_labels = np.array(all_labels)[val_idx] return pool_embeddings, pool_labels, val_embeddings, val_labels def _try_load_from_huggingface( task_id: str, ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] | None: try: from datasets import load_dataset import torch import torchvision.models as models import torchvision.transforms as transforms from PIL import Image import io import base64 dataset = load_dataset( "keremberke/chest-xray-classification", name="full", trust_remote_code=True ) model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) model.fc = torch.nn.Identity() model.eval() transform = transforms.Compose( [ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize( mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ), ] ) config = TASK_CONFIGS[task_id] classes = config["classes"] pool_size = config["pool_size"] val_size = config["val_size"] split = dataset.get("train", dataset.get("train")) if split is None: return None hf_label_map = {0: "normal", 1: "pneumonia"} embeddings_list = [] labels_list = [] max_samples = min(pool_size + val_size, len(split)) for i in range(max_samples): try: sample = split[i] img = sample.get("image") or sample.get("img") if img is None: continue if not isinstance(img, Image.Image): img = Image.fromarray(img).convert("RGB") else: img = img.convert("RGB") tensor = transform(img).unsqueeze(0) with torch.no_grad(): emb = model(tensor).squeeze().numpy() raw_label = sample.get("label", 0) label_str = hf_label_map.get(int(raw_label), "normal") if label_str not in classes: label_str = "normal" embeddings_list.append(emb) labels_list.append(label_str) except Exception: continue if len(embeddings_list) < 50: return None embeddings_arr = np.array(embeddings_list, dtype=np.float32) labels_arr = np.array(labels_list) rng = np.random.RandomState(42) perm = rng.permutation(len(embeddings_arr)) actual_pool = min(pool_size, len(perm) - val_size) pool_idx = perm[:actual_pool] val_idx = perm[actual_pool : actual_pool + val_size] return ( embeddings_arr[pool_idx], labels_arr[pool_idx], embeddings_arr[val_idx], labels_arr[val_idx], ) except Exception: return None def load_or_extract_embeddings( task_id: str, ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: cache_path = EMBEDDINGS_CACHE_DIR / f"{task_id}_embeddings.pkl" if cache_path.exists(): with open(cache_path, "rb") as f: return pickle.load(f) result = _try_load_from_huggingface(task_id) if result is None: result = _generate_synthetic_embeddings(task_id) with open(cache_path, "wb") as f: pickle.dump(result, f) return result def get_task_classes(task_id: str) -> List[str]: return TASK_CONFIGS[task_id]["classes"] def get_task_class_weights(task_id: str) -> List[float]: return TASK_CONFIGS[task_id]["class_weights"]