Upload 126 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- MTIL_datasets/__init__.py +4 -0
- MTIL_datasets/caltech101.py +92 -0
- MTIL_datasets/cifar10.py +94 -0
- MTIL_datasets/cifar100.py +89 -0
- MTIL_datasets/clevr_count.py +118 -0
- MTIL_datasets/country211.py +386 -0
- MTIL_datasets/dtd.py +95 -0
- MTIL_datasets/eurosat.py +75 -0
- MTIL_datasets/fer2013.py +170 -0
- MTIL_datasets/fgvc_aircraft.py +73 -0
- MTIL_datasets/food101.py +52 -0
- MTIL_datasets/gtsrb.py +202 -0
- MTIL_datasets/hatefulmemes.py +117 -0
- MTIL_datasets/imagenet_r.py +231 -0
- MTIL_datasets/kitti_distance.py +228 -0
- MTIL_datasets/mnist.py +132 -0
- MTIL_datasets/oxford_flowers.py +89 -0
- MTIL_datasets/oxford_pets.py +176 -0
- MTIL_datasets/pcam.py +198 -0
- MTIL_datasets/resisc.py +199 -0
- MTIL_datasets/sst2.py +122 -0
- MTIL_datasets/stanford_cars.py +83 -0
- MTIL_datasets/stl10.py +143 -0
- MTIL_datasets/sun397.py +81 -0
- MTIL_datasets/ucf101.py +360 -0
- MTIL_datasets/utils.py +299 -0
- MTIL_datasets/voc2007.py +173 -0
- README.md +227 -0
- calculate_SCR.py +266 -0
- calculate_sim.py +456 -0
- class_orders/cifar100.yaml +1 -0
- class_orders/imagenet100.yaml +11 -0
- class_orders/imagenet1000.yaml +48 -0
- class_orders/tinyimagenet.yaml +17 -0
- clip/README.md +1 -0
- clip/__init__.py +1 -0
- clip/adapter.py +69 -0
- clip/bpe_simple_vocab_16e6.txt.gz +3 -0
- clip/clip.py +310 -0
- clip/model.py +713 -0
- clip/tokenizer.py +140 -0
- configs/class/aircraft.yaml +76 -0
- configs/class/caltech.yaml +79 -0
- configs/class/car.yaml +79 -0
- configs/class/cifar10.yaml +79 -0
- configs/class/cifar100.yaml +79 -0
- configs/class/clevr.yaml +79 -0
- configs/class/conuntry.yaml +79 -0
- configs/class/dtd.yaml +79 -0
.gitattributes
CHANGED
|
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
DFA-MoE/docs/intro.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
DFA-MoE/docs/intro.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
docs/intro.png filter=lfs diff=lfs merge=lfs -text
|
MTIL_datasets/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This dataset is originally proposed by Zangwei Zheng et al. (https://arxiv.org/abs/2303.06628)
|
| 2 |
+
# Code here is based on CoOp and its following works. (https://github.com/KaiyangZhou/CoOp)
|
| 3 |
+
# Modified by Longxiang Tang (lloong.x@gmail.com) to release the dependence of Dassl lib.
|
| 4 |
+
# To prepare data, please refer to https://github.com/muzairkhattak/PromptSRC/blob/main/docs/DATASETS.md
|
MTIL_datasets/caltech101.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
|
| 4 |
+
from .utils import *
|
| 5 |
+
|
| 6 |
+
from .oxford_pets import OxfordPets
|
| 7 |
+
from .dtd import DescribableTextures as DTD
|
| 8 |
+
|
| 9 |
+
IGNORED = ["BACKGROUND_Google", "Faces_easy"]
|
| 10 |
+
NEW_CNAMES = {
|
| 11 |
+
"airplanes": "airplane",
|
| 12 |
+
"Faces": "face",
|
| 13 |
+
"Leopards": "leopard",
|
| 14 |
+
"Motorbikes": "motorbike",
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class Caltech101(DatasetBase):
|
| 19 |
+
|
| 20 |
+
dataset_dir = "caltech-101"
|
| 21 |
+
|
| 22 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 23 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 24 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 25 |
+
self.image_dir = os.path.join(self.dataset_dir, "101_ObjectCategories")
|
| 26 |
+
self.split_path = os.path.join(self.dataset_dir, "split_zhou_Caltech101.json")
|
| 27 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 28 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 29 |
+
|
| 30 |
+
if os.path.exists(self.split_path):
|
| 31 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.image_dir)
|
| 32 |
+
else:
|
| 33 |
+
train, val, test = DTD.read_and_split_data(self.image_dir, ignored=IGNORED, new_cnames=NEW_CNAMES)
|
| 34 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.image_dir)
|
| 35 |
+
|
| 36 |
+
if num_shots >= 1:
|
| 37 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 38 |
+
|
| 39 |
+
if os.path.exists(preprocessed):
|
| 40 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 41 |
+
with open(preprocessed, "rb") as file:
|
| 42 |
+
data = pickle.load(file)
|
| 43 |
+
train, val = data["train"], data["val"]
|
| 44 |
+
else:
|
| 45 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 46 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 47 |
+
data = {"train": train, "val": val}
|
| 48 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 49 |
+
with open(preprocessed, "wb") as file:
|
| 50 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 51 |
+
|
| 52 |
+
subsample = subsample_classes
|
| 53 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample)
|
| 54 |
+
|
| 55 |
+
self.templates = [
|
| 56 |
+
lambda c: f"a photo of a {c}.",
|
| 57 |
+
lambda c: f"a painting of a {c}.",
|
| 58 |
+
lambda c: f"a plastic {c}.",
|
| 59 |
+
lambda c: f"a sculpture of a {c}.",
|
| 60 |
+
lambda c: f"a sketch of a {c}.",
|
| 61 |
+
lambda c: f"a tattoo of a {c}.",
|
| 62 |
+
lambda c: f"a toy {c}.",
|
| 63 |
+
lambda c: f"a rendition of a {c}.",
|
| 64 |
+
lambda c: f"a embroidered {c}.",
|
| 65 |
+
lambda c: f"a cartoon {c}.",
|
| 66 |
+
lambda c: f"a {c} in a video game.",
|
| 67 |
+
lambda c: f"a plushie {c}.",
|
| 68 |
+
lambda c: f"a origami {c}.",
|
| 69 |
+
lambda c: f"art of a {c}.",
|
| 70 |
+
lambda c: f"graffiti of a {c}.",
|
| 71 |
+
lambda c: f"a drawing of a {c}.",
|
| 72 |
+
lambda c: f"a doodle of a {c}.",
|
| 73 |
+
lambda c: f"a photo of the {c}.",
|
| 74 |
+
lambda c: f"a painting of the {c}.",
|
| 75 |
+
lambda c: f"the plastic {c}.",
|
| 76 |
+
lambda c: f"a sculpture of the {c}.",
|
| 77 |
+
lambda c: f"a sketch of the {c}.",
|
| 78 |
+
lambda c: f"a tattoo of the {c}.",
|
| 79 |
+
lambda c: f"the toy {c}.",
|
| 80 |
+
lambda c: f"a rendition of the {c}.",
|
| 81 |
+
lambda c: f"the embroidered {c}.",
|
| 82 |
+
lambda c: f"the cartoon {c}.",
|
| 83 |
+
lambda c: f"the {c} in a video game.",
|
| 84 |
+
lambda c: f"the plushie {c}.",
|
| 85 |
+
lambda c: f"the origami {c}.",
|
| 86 |
+
lambda c: f"art of the {c}.",
|
| 87 |
+
lambda c: f"graffiti of the {c}.",
|
| 88 |
+
lambda c: f"a drawing of the {c}.",
|
| 89 |
+
lambda c: f"a doodle of the {c}.",
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
super().__init__(train_x=train, val=val, test=test)
|
MTIL_datasets/cifar10.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import List
|
| 3 |
+
|
| 4 |
+
from .utils import * # Datum, DatasetBase
|
| 5 |
+
from .oxford_pets import OxfordPets
|
| 6 |
+
|
| 7 |
+
try:
|
| 8 |
+
from torchvision.datasets import CIFAR10 as TorchCIFAR10
|
| 9 |
+
except Exception as e:
|
| 10 |
+
TorchCIFAR10 = None
|
| 11 |
+
print(f"Warning: torchvision not available for CIFAR10: {e}")
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
CIFAR10_CLASSES: List[str] = [
|
| 15 |
+
'airplane',
|
| 16 |
+
'automobile',
|
| 17 |
+
'bird',
|
| 18 |
+
'cat',
|
| 19 |
+
'deer',
|
| 20 |
+
'dog',
|
| 21 |
+
'frog',
|
| 22 |
+
'horse',
|
| 23 |
+
'ship',
|
| 24 |
+
'truck',
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
CIFAR10_TEMPLATES: List[str] = [
|
| 28 |
+
'a photo of a {}.',
|
| 29 |
+
'a blurry photo of a {}.',
|
| 30 |
+
'a black and white photo of a {}.',
|
| 31 |
+
'a low contrast photo of a {}.',
|
| 32 |
+
'a high contrast photo of a {}.',
|
| 33 |
+
'a bad photo of a {}.',
|
| 34 |
+
'a good photo of a {}.',
|
| 35 |
+
'a photo of a small {}.',
|
| 36 |
+
'a photo of a big {}.',
|
| 37 |
+
'a photo of the {}.',
|
| 38 |
+
'a blurry photo of the {}.',
|
| 39 |
+
'a black and white photo of the {}.',
|
| 40 |
+
'a low contrast photo of the {}.',
|
| 41 |
+
'a high contrast photo of the {}.',
|
| 42 |
+
'a bad photo of the {}.',
|
| 43 |
+
'a good photo of the {}.',
|
| 44 |
+
'a photo of the small {}.',
|
| 45 |
+
'a photo of the big {}.',
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class CIFAR10(DatasetBase):
|
| 50 |
+
|
| 51 |
+
dataset_dir = "cifar10"
|
| 52 |
+
|
| 53 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 54 |
+
if TorchCIFAR10 is None:
|
| 55 |
+
raise ImportError("torchvision is required for CIFAR10 dataset. Please install torchvision.")
|
| 56 |
+
|
| 57 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 58 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 59 |
+
|
| 60 |
+
# Use torchvision to download/load data to dataset_dir
|
| 61 |
+
train_ds = TorchCIFAR10(root=self.dataset_dir, train=True, download=True)
|
| 62 |
+
test_ds = TorchCIFAR10(root=self.dataset_dir, train=False, download=True)
|
| 63 |
+
|
| 64 |
+
# Build Datum lists
|
| 65 |
+
trainval = []
|
| 66 |
+
# train_ds.data: numpy array HWC, train_ds.targets: list[int]
|
| 67 |
+
for idx in range(len(train_ds.data)):
|
| 68 |
+
img = Image.fromarray(train_ds.data[idx])
|
| 69 |
+
label = int(train_ds.targets[idx])
|
| 70 |
+
classname = CIFAR10_CLASSES[label]
|
| 71 |
+
trainval.append(Datum(impath=img, label=label, classname=classname))
|
| 72 |
+
|
| 73 |
+
test = []
|
| 74 |
+
for idx in range(len(test_ds.data)):
|
| 75 |
+
img = Image.fromarray(test_ds.data[idx])
|
| 76 |
+
label = int(test_ds.targets[idx])
|
| 77 |
+
classname = CIFAR10_CLASSES[label]
|
| 78 |
+
test.append(Datum(impath=img, label=label, classname=classname))
|
| 79 |
+
|
| 80 |
+
# Split train/val
|
| 81 |
+
train, val = OxfordPets.split_trainval(trainval)
|
| 82 |
+
|
| 83 |
+
# Few-shot sampling if requested
|
| 84 |
+
if num_shots >= 1:
|
| 85 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 86 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 87 |
+
|
| 88 |
+
# Optional class subsampling (base/new)
|
| 89 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample_classes)
|
| 90 |
+
|
| 91 |
+
# Templates
|
| 92 |
+
self.templates = CIFAR10_TEMPLATES
|
| 93 |
+
|
| 94 |
+
super().__init__(train_x=train, val=val, test=test)
|
MTIL_datasets/cifar100.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import numpy as np
|
| 4 |
+
|
| 5 |
+
from .utils import *
|
| 6 |
+
|
| 7 |
+
from .oxford_pets import OxfordPets
|
| 8 |
+
from .dtd import DescribableTextures as DTD
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class CIFAR100(DatasetBase):
|
| 12 |
+
|
| 13 |
+
dataset_dir = "cifar100"
|
| 14 |
+
|
| 15 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 16 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 17 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 18 |
+
|
| 19 |
+
file_path = os.path.join(root, self.dataset_dir, 'train')
|
| 20 |
+
with open(file_path, "rb") as f:
|
| 21 |
+
entry = pickle.load(f, encoding="latin1")
|
| 22 |
+
trainval_data = entry["data"]
|
| 23 |
+
if "labels" in entry:
|
| 24 |
+
trainval_targets = entry["labels"]
|
| 25 |
+
else:
|
| 26 |
+
trainval_targets = entry["fine_labels"]
|
| 27 |
+
trainval_data = trainval_data.reshape(-1, 3, 32, 32)
|
| 28 |
+
trainval_data = trainval_data.transpose((0, 2, 3, 1))
|
| 29 |
+
|
| 30 |
+
file_path = os.path.join(root, self.dataset_dir, 'test')
|
| 31 |
+
with open(file_path, "rb") as f:
|
| 32 |
+
entry = pickle.load(f, encoding="latin1")
|
| 33 |
+
test_data = entry["data"]
|
| 34 |
+
if "labels" in entry:
|
| 35 |
+
test_targets = entry["labels"]
|
| 36 |
+
else:
|
| 37 |
+
test_targets = entry["fine_labels"]
|
| 38 |
+
test_data = test_data.reshape(-1, 3, 32, 32)
|
| 39 |
+
test_data = test_data.transpose((0, 2, 3, 1))
|
| 40 |
+
|
| 41 |
+
path = os.path.join(self.dataset_dir, "meta")
|
| 42 |
+
with open(path, "rb") as infile:
|
| 43 |
+
data = pickle.load(infile, encoding="latin1")
|
| 44 |
+
classes = data["fine_label_names"]
|
| 45 |
+
classes = [s.replace("_", " ") for s in classes]
|
| 46 |
+
|
| 47 |
+
trainval = []
|
| 48 |
+
for idx in range(trainval_data.shape[0]):
|
| 49 |
+
item = Datum(impath=Image.fromarray(trainval_data[idx]),
|
| 50 |
+
label=int(trainval_targets[idx]), classname=classes[trainval_targets[idx]])
|
| 51 |
+
trainval.append(item)
|
| 52 |
+
|
| 53 |
+
test = []
|
| 54 |
+
for idx in range(test_data.shape[0]):
|
| 55 |
+
item = Datum(impath=Image.fromarray(test_data[idx]),
|
| 56 |
+
label=int(test_targets[idx]), classname=classes[test_targets[idx]])
|
| 57 |
+
test.append(item)
|
| 58 |
+
|
| 59 |
+
train, val = OxfordPets.split_trainval(trainval)
|
| 60 |
+
|
| 61 |
+
if num_shots >= 1:
|
| 62 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 63 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 64 |
+
|
| 65 |
+
subsample = subsample_classes
|
| 66 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample)
|
| 67 |
+
|
| 68 |
+
self.templates = [
|
| 69 |
+
lambda c : f'a bad photo of a {c}.',
|
| 70 |
+
lambda c : f'a blurry photo of a {c}.',
|
| 71 |
+
lambda c : f'a black and white photo of a {c}.',
|
| 72 |
+
lambda c : f'a low contrast photo of a {c}.',
|
| 73 |
+
lambda c : f'a high contrast photo of a {c}.',
|
| 74 |
+
lambda c : f'a photo of a {c}.',
|
| 75 |
+
lambda c : f'a good photo of a {c}.',
|
| 76 |
+
lambda c : f'a photo of a small {c}.',
|
| 77 |
+
lambda c : f'a photo of a big {c}.',
|
| 78 |
+
lambda c : f'a photo of the {c}.',
|
| 79 |
+
lambda c : f'a blurry photo of the {c}.',
|
| 80 |
+
lambda c : f'a black and white photo of the {c}.',
|
| 81 |
+
lambda c : f'a low contrast photo of the {c}.',
|
| 82 |
+
lambda c : f'a high contrast photo of the {c}.',
|
| 83 |
+
lambda c : f'a bad photo of the {c}.',
|
| 84 |
+
lambda c : f'a good photo of the {c}.',
|
| 85 |
+
lambda c : f'a photo of the small {c}.',
|
| 86 |
+
lambda c : f'a photo of the big {c}.',
|
| 87 |
+
]
|
| 88 |
+
|
| 89 |
+
super().__init__(train_x=train, val=val, test=test)
|
MTIL_datasets/clevr_count.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
from typing import List
|
| 4 |
+
|
| 5 |
+
from .utils import Datum, DatasetBase, mkdir_if_missing
|
| 6 |
+
from .oxford_pets import OxfordPets
|
| 7 |
+
from .utils import read_json, write_json
|
| 8 |
+
|
| 9 |
+
# Class list and templates as specified
|
| 10 |
+
CLEVR_COUNT_CLASSES: List[str] = [
|
| 11 |
+
'10', '3', '4', '5', '6', '7', '8', '9'
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
CLEVR_COUNT_TEMPLATES: List[str] = [
|
| 15 |
+
'a photo of {} objects.',
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class CLEVRCount(DatasetBase):
|
| 20 |
+
|
| 21 |
+
dataset_dir = 'clevr'
|
| 22 |
+
|
| 23 |
+
def __init__(self, root, num_shots: int = 0, seed: int = 1, subsample_classes: str = 'all'):
|
| 24 |
+
# Root and directories
|
| 25 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 26 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 27 |
+
self.images_dir = os.path.join(self.dataset_dir, 'images')
|
| 28 |
+
self.scenes_dir = os.path.join(self.dataset_dir, 'scenes')
|
| 29 |
+
self.split_path = os.path.join(self.dataset_dir, 'split_custom_CLEVRCount.json')
|
| 30 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, 'split_fewshot')
|
| 31 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 32 |
+
|
| 33 |
+
# Required files
|
| 34 |
+
train_scenes = os.path.join(self.scenes_dir, 'CLEVR_train_scenes.json')
|
| 35 |
+
val_scenes = os.path.join(self.scenes_dir, 'CLEVR_val_scenes.json')
|
| 36 |
+
if not os.path.isfile(train_scenes) or not os.path.isfile(val_scenes):
|
| 37 |
+
raise FileNotFoundError(
|
| 38 |
+
f"CLEVRCount expects scenes JSON at {train_scenes} and {val_scenes}"
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Load or build split
|
| 42 |
+
if os.path.exists(self.split_path):
|
| 43 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.dataset_dir)
|
| 44 |
+
else:
|
| 45 |
+
trainval = self._read_scenes(train_scenes, split='train')
|
| 46 |
+
test = self._read_scenes(val_scenes, split='val')
|
| 47 |
+
train, val = OxfordPets.split_trainval(trainval)
|
| 48 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.dataset_dir)
|
| 49 |
+
|
| 50 |
+
# Few-shot
|
| 51 |
+
if num_shots >= 1:
|
| 52 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 53 |
+
if os.path.exists(preprocessed):
|
| 54 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 55 |
+
with open(preprocessed, 'rb') as f:
|
| 56 |
+
data = pickle.load(f)
|
| 57 |
+
train, val = data['train'], data['val']
|
| 58 |
+
else:
|
| 59 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 60 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 61 |
+
data = {'train': train, 'val': val}
|
| 62 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 63 |
+
with open(preprocessed, 'wb') as f:
|
| 64 |
+
pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL)
|
| 65 |
+
|
| 66 |
+
# Optional class subsampling (base/new)
|
| 67 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample_classes)
|
| 68 |
+
|
| 69 |
+
# Templates
|
| 70 |
+
self.templates = CLEVR_COUNT_TEMPLATES
|
| 71 |
+
|
| 72 |
+
# Debug stats
|
| 73 |
+
try:
|
| 74 |
+
def _hist(items: List[Datum]):
|
| 75 |
+
from collections import Counter
|
| 76 |
+
cnt = Counter([it.label for it in items])
|
| 77 |
+
out = {i: int(cnt.get(i, 0)) for i in range(len(CLEVR_COUNT_CLASSES))}
|
| 78 |
+
return out
|
| 79 |
+
except Exception as e:
|
| 80 |
+
print(f"CLEVRCount stats printing failed: {e}")
|
| 81 |
+
|
| 82 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 83 |
+
# Ensure class metadata is stable and matches the provided list
|
| 84 |
+
self._classnames = CLEVR_COUNT_CLASSES
|
| 85 |
+
self._lab2cname = {i: c for i, c in enumerate(CLEVR_COUNT_CLASSES)}
|
| 86 |
+
self._num_classes = len(CLEVR_COUNT_CLASSES)
|
| 87 |
+
|
| 88 |
+
def _read_scenes(self, json_path: str, split: str) -> List[Datum]:
|
| 89 |
+
"""Read CLEVR scenes JSON and construct a list of Datum entries.
|
| 90 |
+
Only images whose object count appears in CLEVR_COUNT_CLASSES are kept.
|
| 91 |
+
"""
|
| 92 |
+
obj = read_json(json_path)
|
| 93 |
+
scenes = obj.get('scenes', [])
|
| 94 |
+
# Map count -> class index
|
| 95 |
+
lab2idx = {int(c): i for i, c in enumerate(CLEVR_COUNT_CLASSES)}
|
| 96 |
+
items: List[Datum] = []
|
| 97 |
+
for sc in scenes:
|
| 98 |
+
# Some files use key 'split', some typos list 'spit'; be robust
|
| 99 |
+
image_filename = sc.get('image_filename', None)
|
| 100 |
+
if not image_filename:
|
| 101 |
+
continue
|
| 102 |
+
n_objects = sc.get('objects', [])
|
| 103 |
+
try:
|
| 104 |
+
num = int(len(n_objects))
|
| 105 |
+
except Exception:
|
| 106 |
+
continue
|
| 107 |
+
if num not in lab2idx:
|
| 108 |
+
# skip counts not in the configured class list
|
| 109 |
+
continue
|
| 110 |
+
label_i = lab2idx[num]
|
| 111 |
+
# Build absolute path to the image based on split
|
| 112 |
+
if split not in ['train', 'val', 'test']:
|
| 113 |
+
split_dir = 'train'
|
| 114 |
+
else:
|
| 115 |
+
split_dir = split
|
| 116 |
+
impath = os.path.join(self.dataset_dir, 'images', split_dir, image_filename)
|
| 117 |
+
items.append(Datum(impath=impath, label=label_i, classname=str(num)))
|
| 118 |
+
return items
|
MTIL_datasets/country211.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import pickle
|
| 4 |
+
import re
|
| 5 |
+
|
| 6 |
+
from .utils import *
|
| 7 |
+
from .oxford_pets import OxfordPets
|
| 8 |
+
from .dtd import DescribableTextures as DTD
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Country211(DatasetBase):
|
| 12 |
+
"""
|
| 13 |
+
Country211 dataset loader for MTIL.
|
| 14 |
+
Expected structure:
|
| 15 |
+
<root>/country211/
|
| 16 |
+
├─ train/<ISO2>/*.jpg
|
| 17 |
+
├─ valid/<ISO2>/*.jpg
|
| 18 |
+
└─ test/<ISO2>/*.jpg
|
| 19 |
+
|
| 20 |
+
Where <ISO2> are ISO-3166 alpha-2 country codes (e.g., AD, US, CN).
|
| 21 |
+
Class names are mapped from ISO2 codes to provided human-readable names.
|
| 22 |
+
Optionally, a JSON mapping file can override defaults:
|
| 23 |
+
<root>/country211/iso2_to_name.json => {"AD": "Andorra", ...}
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
dataset_dir = "country211"
|
| 27 |
+
|
| 28 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 29 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 30 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 31 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 32 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 33 |
+
|
| 34 |
+
# Prefer explicit split directories if they exist
|
| 35 |
+
train_dir = os.path.join(self.dataset_dir, "train")
|
| 36 |
+
valid_dir = os.path.join(self.dataset_dir, "valid")
|
| 37 |
+
test_dir = os.path.join(self.dataset_dir, "test")
|
| 38 |
+
|
| 39 |
+
use_folder_splits = os.path.isdir(train_dir) and os.path.isdir(valid_dir) and os.path.isdir(test_dir)
|
| 40 |
+
|
| 41 |
+
if use_folder_splits:
|
| 42 |
+
# Build ISO2 -> name mapping (allow override via json)
|
| 43 |
+
iso_map = self._load_iso2_to_name()
|
| 44 |
+
# Create unified code set across splits to keep label mapping stable
|
| 45 |
+
all_codes = set()
|
| 46 |
+
for d in [train_dir, valid_dir, test_dir]:
|
| 47 |
+
if os.path.isdir(d):
|
| 48 |
+
for code in listdir_nohidden(d):
|
| 49 |
+
code_path = os.path.join(d, code)
|
| 50 |
+
if os.path.isdir(code_path):
|
| 51 |
+
all_codes.add(code)
|
| 52 |
+
# Validate code format and mapping coverage early
|
| 53 |
+
override_path = os.path.join(self.dataset_dir, 'iso2_to_name.json')
|
| 54 |
+
pat = re.compile(r'^[A-Z]{2}$')
|
| 55 |
+
invalid_codes = sorted([c for c in all_codes if not (pat.match(c) or c == 'XK')])
|
| 56 |
+
if invalid_codes:
|
| 57 |
+
raise ValueError(
|
| 58 |
+
"Country211: Found invalid ISO2 code folder names: {}. "
|
| 59 |
+
"Codes must be two uppercase letters (e.g., 'US', 'CN') or 'XK'. "
|
| 60 |
+
"Please rename these folders accordingly.".format(invalid_codes)
|
| 61 |
+
)
|
| 62 |
+
unknown_codes = sorted([c for c in all_codes if c not in iso_map])
|
| 63 |
+
if unknown_codes:
|
| 64 |
+
raise ValueError(
|
| 65 |
+
"Country211: ISO2 codes missing from mapping: {}. "
|
| 66 |
+
"Add them to {} as a JSON dict, e.g., {\"XX\": \"Country Name\"}.".format(
|
| 67 |
+
unknown_codes, override_path
|
| 68 |
+
)
|
| 69 |
+
)
|
| 70 |
+
# Sort by human name (fallback to code) for stable labels
|
| 71 |
+
def _code_to_name(c):
|
| 72 |
+
return iso_map.get(c, c)
|
| 73 |
+
sorted_codes = sorted(list(all_codes), key=lambda c: _code_to_name(c))
|
| 74 |
+
code_to_label = {c: i for i, c in enumerate(sorted_codes)}
|
| 75 |
+
|
| 76 |
+
train = self._read_split_dir(train_dir, code_to_label, iso_map)
|
| 77 |
+
val = self._read_split_dir(valid_dir, code_to_label, iso_map)
|
| 78 |
+
test = self._read_split_dir(test_dir, code_to_label, iso_map)
|
| 79 |
+
else:
|
| 80 |
+
# Fallbacks: JSON split or naive folder split
|
| 81 |
+
image_dir = os.path.join(self.dataset_dir, "images")
|
| 82 |
+
self.image_dir = image_dir if os.path.isdir(image_dir) else self.dataset_dir
|
| 83 |
+
split_path = os.path.join(self.dataset_dir, "split_zhou_Country211.json")
|
| 84 |
+
if os.path.exists(split_path):
|
| 85 |
+
train, val, test = OxfordPets.read_split(split_path, self.image_dir)
|
| 86 |
+
else:
|
| 87 |
+
train, val, test = DTD.read_and_split_data(self.image_dir)
|
| 88 |
+
OxfordPets.save_split(train, val, test, split_path, self.image_dir)
|
| 89 |
+
|
| 90 |
+
if num_shots >= 1:
|
| 91 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 92 |
+
if os.path.exists(preprocessed):
|
| 93 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 94 |
+
with open(preprocessed, "rb") as file:
|
| 95 |
+
data = pickle.load(file)
|
| 96 |
+
train, val = data["train"], data["val"]
|
| 97 |
+
else:
|
| 98 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 99 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 100 |
+
data = {"train": train, "val": val}
|
| 101 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 102 |
+
with open(preprocessed, "wb") as file:
|
| 103 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 104 |
+
|
| 105 |
+
subsample = subsample_classes
|
| 106 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample)
|
| 107 |
+
|
| 108 |
+
# Templates provided by user
|
| 109 |
+
self.templates = [
|
| 110 |
+
lambda c: f'a photo i took in {c}.',
|
| 111 |
+
lambda c: f'a photo i took while visiting {c}.',
|
| 112 |
+
lambda c: f'a photo from my home country of {c}.',
|
| 113 |
+
lambda c: f'a photo from my visit to {c}.',
|
| 114 |
+
lambda c: f'a photo showing the country of {c}.',
|
| 115 |
+
]
|
| 116 |
+
|
| 117 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 118 |
+
|
| 119 |
+
def _read_split_dir(self, split_dir, code_to_label, iso_map):
|
| 120 |
+
items = []
|
| 121 |
+
if not os.path.isdir(split_dir):
|
| 122 |
+
return items
|
| 123 |
+
codes = listdir_nohidden(split_dir)
|
| 124 |
+
for code in codes:
|
| 125 |
+
class_dir = os.path.join(split_dir, code)
|
| 126 |
+
if not os.path.isdir(class_dir):
|
| 127 |
+
continue
|
| 128 |
+
label = code_to_label.get(code)
|
| 129 |
+
if label is None:
|
| 130 |
+
raise ValueError(
|
| 131 |
+
f"Country211: Inconsistent label mapping for code '{code}' in split '{split_dir}'. "
|
| 132 |
+
f"This indicates an internal mismatch between discovered codes and label map."
|
| 133 |
+
)
|
| 134 |
+
try:
|
| 135 |
+
cname = iso_map[code]
|
| 136 |
+
except KeyError:
|
| 137 |
+
raise ValueError(
|
| 138 |
+
f"Country211: Missing ISO mapping for code '{code}'. "
|
| 139 |
+
f"Please add it to '{os.path.join(self.dataset_dir, 'iso2_to_name.json')}'."
|
| 140 |
+
)
|
| 141 |
+
for fname in listdir_nohidden(class_dir):
|
| 142 |
+
impath = os.path.join(class_dir, fname)
|
| 143 |
+
items.append(Datum(impath=impath, label=label, classname=cname))
|
| 144 |
+
return items
|
| 145 |
+
|
| 146 |
+
def _load_iso2_to_name(self):
|
| 147 |
+
"""Load ISO2->country name mapping, allow local JSON override.
|
| 148 |
+
Fallback to built-in mapping; unknown codes map to themselves at usage time.
|
| 149 |
+
"""
|
| 150 |
+
override_path = os.path.join(self.dataset_dir, 'iso2_to_name.json')
|
| 151 |
+
if os.path.exists(override_path):
|
| 152 |
+
try:
|
| 153 |
+
data = read_json(override_path)
|
| 154 |
+
if not isinstance(data, dict):
|
| 155 |
+
raise ValueError(
|
| 156 |
+
f"Country211: Expected a JSON object in '{override_path}', got {type(data)}"
|
| 157 |
+
)
|
| 158 |
+
# Validate keys as ISO2 (two uppercase letters) or 'XK', and values are non-empty strings
|
| 159 |
+
pat = re.compile(r'^[A-Z]{2}$')
|
| 160 |
+
bad_keys = [k for k in data.keys() if not (isinstance(k, str) and (pat.match(k) or k == 'XK'))]
|
| 161 |
+
bad_vals = [k for k, v in data.items() if not (isinstance(v, str) and v.strip())]
|
| 162 |
+
if bad_keys or bad_vals:
|
| 163 |
+
raise ValueError(
|
| 164 |
+
(
|
| 165 |
+
"Country211: Invalid iso2_to_name.json entries. "
|
| 166 |
+
f"Bad keys (must be ISO2 like 'US'): {bad_keys}. "
|
| 167 |
+
f"Bad values (must be non-empty strings) for keys: {bad_vals}."
|
| 168 |
+
)
|
| 169 |
+
)
|
| 170 |
+
return data
|
| 171 |
+
except Exception as e:
|
| 172 |
+
raise ValueError(f"Country211: Failed to load '{override_path}': {e}")
|
| 173 |
+
# Built-in mapping aligned with provided classes
|
| 174 |
+
return {
|
| 175 |
+
'AD': 'Andorra',
|
| 176 |
+
'AE': 'United Arab Emirates',
|
| 177 |
+
'AF': 'Afghanistan',
|
| 178 |
+
'AG': 'Antigua and Barbuda',
|
| 179 |
+
'AI': 'Anguilla',
|
| 180 |
+
'AL': 'Albania',
|
| 181 |
+
'AM': 'Armenia',
|
| 182 |
+
'AO': 'Angola',
|
| 183 |
+
'AQ': 'Antarctica',
|
| 184 |
+
'AR': 'Argentina',
|
| 185 |
+
'AT': 'Austria',
|
| 186 |
+
'AU': 'Australia',
|
| 187 |
+
'AW': 'Aruba',
|
| 188 |
+
'AX': 'Aland Islands',
|
| 189 |
+
'AZ': 'Azerbaijan',
|
| 190 |
+
'BA': 'Bosnia and Herzegovina',
|
| 191 |
+
'BB': 'Barbados',
|
| 192 |
+
'BD': 'Bangladesh',
|
| 193 |
+
'BE': 'Belgium',
|
| 194 |
+
'BF': 'Burkina Faso',
|
| 195 |
+
'BG': 'Bulgaria',
|
| 196 |
+
'BH': 'Bahrain',
|
| 197 |
+
'BJ': 'Benin',
|
| 198 |
+
'BM': 'Bermuda',
|
| 199 |
+
'BN': 'Brunei Darussalam',
|
| 200 |
+
'BO': 'Bolivia',
|
| 201 |
+
'BQ': 'Bonaire, Saint Eustatius and Saba',
|
| 202 |
+
'BR': 'Brazil',
|
| 203 |
+
'BS': 'Bahamas',
|
| 204 |
+
'BT': 'Bhutan',
|
| 205 |
+
'BW': 'Botswana',
|
| 206 |
+
'BY': 'Belarus',
|
| 207 |
+
'BZ': 'Belize',
|
| 208 |
+
'CA': 'Canada',
|
| 209 |
+
'CD': 'DR Congo',
|
| 210 |
+
'CF': 'Central African Republic',
|
| 211 |
+
'CH': 'Switzerland',
|
| 212 |
+
'CI': "Cote d'Ivoire",
|
| 213 |
+
'CK': 'Cook Islands',
|
| 214 |
+
'CL': 'Chile',
|
| 215 |
+
'CM': 'Cameroon',
|
| 216 |
+
'CN': 'China',
|
| 217 |
+
'CO': 'Colombia',
|
| 218 |
+
'CR': 'Costa Rica',
|
| 219 |
+
'CU': 'Cuba',
|
| 220 |
+
'CV': 'Cabo Verde',
|
| 221 |
+
'CW': 'Curacao',
|
| 222 |
+
'CY': 'Cyprus',
|
| 223 |
+
'CZ': 'Czech Republic',
|
| 224 |
+
'DE': 'Germany',
|
| 225 |
+
'DK': 'Denmark',
|
| 226 |
+
'DM': 'Dominica',
|
| 227 |
+
'DO': 'Dominican Republic',
|
| 228 |
+
'DZ': 'Algeria',
|
| 229 |
+
'EC': 'Ecuador',
|
| 230 |
+
'EE': 'Estonia',
|
| 231 |
+
'EG': 'Egypt',
|
| 232 |
+
'ES': 'Spain',
|
| 233 |
+
'ET': 'Ethiopia',
|
| 234 |
+
'FI': 'Finland',
|
| 235 |
+
'FJ': 'Fiji',
|
| 236 |
+
'FK': 'Falkland Islands',
|
| 237 |
+
'FO': 'Faeroe Islands',
|
| 238 |
+
'FR': 'France',
|
| 239 |
+
'GA': 'Gabon',
|
| 240 |
+
'GB': 'United Kingdom',
|
| 241 |
+
'GD': 'Grenada',
|
| 242 |
+
'GE': 'Georgia',
|
| 243 |
+
'GF': 'French Guiana',
|
| 244 |
+
'GG': 'Guernsey',
|
| 245 |
+
'GH': 'Ghana',
|
| 246 |
+
'GI': 'Gibraltar',
|
| 247 |
+
'GL': 'Greenland',
|
| 248 |
+
'GM': 'Gambia',
|
| 249 |
+
'GP': 'Guadeloupe',
|
| 250 |
+
'GR': 'Greece',
|
| 251 |
+
'GS': 'South Georgia and South Sandwich Is.',
|
| 252 |
+
'GT': 'Guatemala',
|
| 253 |
+
'GU': 'Guam',
|
| 254 |
+
'GY': 'Guyana',
|
| 255 |
+
'HK': 'Hong Kong',
|
| 256 |
+
'HN': 'Honduras',
|
| 257 |
+
'HR': 'Croatia',
|
| 258 |
+
'HT': 'Haiti',
|
| 259 |
+
'HU': 'Hungary',
|
| 260 |
+
'ID': 'Indonesia',
|
| 261 |
+
'IE': 'Ireland',
|
| 262 |
+
'IL': 'Israel',
|
| 263 |
+
'IM': 'Isle of Man',
|
| 264 |
+
'IN': 'India',
|
| 265 |
+
'IQ': 'Iraq',
|
| 266 |
+
'IR': 'Iran',
|
| 267 |
+
'IS': 'Iceland',
|
| 268 |
+
'IT': 'Italy',
|
| 269 |
+
'JE': 'Jersey',
|
| 270 |
+
'JM': 'Jamaica',
|
| 271 |
+
'JO': 'Jordan',
|
| 272 |
+
'JP': 'Japan',
|
| 273 |
+
'KE': 'Kenya',
|
| 274 |
+
'KG': 'Kyrgyz Republic',
|
| 275 |
+
'KH': 'Cambodia',
|
| 276 |
+
'KN': 'St. Kitts and Nevis',
|
| 277 |
+
'KP': 'North Korea',
|
| 278 |
+
'KR': 'South Korea',
|
| 279 |
+
'KW': 'Kuwait',
|
| 280 |
+
'KY': 'Cayman Islands',
|
| 281 |
+
'KZ': 'Kazakhstan',
|
| 282 |
+
'LA': 'Laos',
|
| 283 |
+
'LB': 'Lebanon',
|
| 284 |
+
'LC': 'St. Lucia',
|
| 285 |
+
'LI': 'Liechtenstein',
|
| 286 |
+
'LK': 'Sri Lanka',
|
| 287 |
+
'LR': 'Liberia',
|
| 288 |
+
'LT': 'Lithuania',
|
| 289 |
+
'LU': 'Luxembourg',
|
| 290 |
+
'LV': 'Latvia',
|
| 291 |
+
'LY': 'Libya',
|
| 292 |
+
'MA': 'Morocco',
|
| 293 |
+
'MC': 'Monaco',
|
| 294 |
+
'MD': 'Moldova',
|
| 295 |
+
'ME': 'Montenegro',
|
| 296 |
+
'MF': 'Saint-Martin',
|
| 297 |
+
'MG': 'Madagascar',
|
| 298 |
+
'MK': 'Macedonia',
|
| 299 |
+
'ML': 'Mali',
|
| 300 |
+
'MM': 'Myanmar',
|
| 301 |
+
'MN': 'Mongolia',
|
| 302 |
+
'MO': 'Macau',
|
| 303 |
+
'MQ': 'Martinique',
|
| 304 |
+
'MR': 'Mauritania',
|
| 305 |
+
'MT': 'Malta',
|
| 306 |
+
'MU': 'Mauritius',
|
| 307 |
+
'MV': 'Maldives',
|
| 308 |
+
'MW': 'Malawi',
|
| 309 |
+
'MX': 'Mexico',
|
| 310 |
+
'MY': 'Malaysia',
|
| 311 |
+
'MZ': 'Mozambique',
|
| 312 |
+
'NA': 'Namibia',
|
| 313 |
+
'NC': 'New Caledonia',
|
| 314 |
+
'NG': 'Nigeria',
|
| 315 |
+
'NI': 'Nicaragua',
|
| 316 |
+
'NL': 'Netherlands',
|
| 317 |
+
'NO': 'Norway',
|
| 318 |
+
'NP': 'Nepal',
|
| 319 |
+
'NZ': 'New Zealand',
|
| 320 |
+
'OM': 'Oman',
|
| 321 |
+
'PA': 'Panama',
|
| 322 |
+
'PE': 'Peru',
|
| 323 |
+
'PF': 'French Polynesia',
|
| 324 |
+
'PG': 'Papua New Guinea',
|
| 325 |
+
'PH': 'Philippines',
|
| 326 |
+
'PK': 'Pakistan',
|
| 327 |
+
'PL': 'Poland',
|
| 328 |
+
'PR': 'Puerto Rico',
|
| 329 |
+
'PS': 'Palestine',
|
| 330 |
+
'PT': 'Portugal',
|
| 331 |
+
'PW': 'Palau',
|
| 332 |
+
'PY': 'Paraguay',
|
| 333 |
+
'QA': 'Qatar',
|
| 334 |
+
'RE': 'Reunion',
|
| 335 |
+
'RO': 'Romania',
|
| 336 |
+
'RS': 'Serbia',
|
| 337 |
+
'RU': 'Russia',
|
| 338 |
+
'RW': 'Rwanda',
|
| 339 |
+
'SA': 'Saudi Arabia',
|
| 340 |
+
'SB': 'Solomon Islands',
|
| 341 |
+
'SC': 'Seychelles',
|
| 342 |
+
'SD': 'Sudan',
|
| 343 |
+
'SE': 'Sweden',
|
| 344 |
+
'SG': 'Singapore',
|
| 345 |
+
'SH': 'St. Helena',
|
| 346 |
+
'SI': 'Slovenia',
|
| 347 |
+
'SJ': 'Svalbard and Jan Mayen Islands',
|
| 348 |
+
'SK': 'Slovakia',
|
| 349 |
+
'SL': 'Sierra Leone',
|
| 350 |
+
'SM': 'San Marino',
|
| 351 |
+
'SN': 'Senegal',
|
| 352 |
+
'SO': 'Somalia',
|
| 353 |
+
'SS': 'South Sudan',
|
| 354 |
+
'SV': 'El Salvador',
|
| 355 |
+
'SX': 'Sint Maarten',
|
| 356 |
+
'SY': 'Syria',
|
| 357 |
+
'SZ': 'Eswatini',
|
| 358 |
+
'TG': 'Togo',
|
| 359 |
+
'TH': 'Thailand',
|
| 360 |
+
'TJ': 'Tajikistan',
|
| 361 |
+
'TL': 'Timor-Leste',
|
| 362 |
+
'TM': 'Turkmenistan',
|
| 363 |
+
'TN': 'Tunisia',
|
| 364 |
+
'TO': 'Tonga',
|
| 365 |
+
'TR': 'Turkey',
|
| 366 |
+
'TT': 'Trinidad and Tobago',
|
| 367 |
+
'TW': 'Taiwan',
|
| 368 |
+
'TZ': 'Tanzania',
|
| 369 |
+
'UA': 'Ukraine',
|
| 370 |
+
'UG': 'Uganda',
|
| 371 |
+
'US': 'United States',
|
| 372 |
+
'UY': 'Uruguay',
|
| 373 |
+
'UZ': 'Uzbekistan',
|
| 374 |
+
'VA': 'Vatican',
|
| 375 |
+
'VE': 'Venezuela',
|
| 376 |
+
'VG': 'British Virgin Islands',
|
| 377 |
+
'VI': 'United States Virgin Islands',
|
| 378 |
+
'VN': 'Vietnam',
|
| 379 |
+
'VU': 'Vanuatu',
|
| 380 |
+
'WS': 'Samoa',
|
| 381 |
+
'XK': 'Kosovo',
|
| 382 |
+
'YE': 'Yemen',
|
| 383 |
+
'ZA': 'South Africa',
|
| 384 |
+
'ZM': 'Zambia',
|
| 385 |
+
'ZW': 'Zimbabwe',
|
| 386 |
+
}
|
MTIL_datasets/dtd.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import random
|
| 4 |
+
|
| 5 |
+
from .utils import *
|
| 6 |
+
|
| 7 |
+
from .oxford_pets import OxfordPets
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class DescribableTextures(DatasetBase):
|
| 11 |
+
|
| 12 |
+
dataset_dir = "dtd"
|
| 13 |
+
|
| 14 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 15 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 16 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 17 |
+
self.image_dir = os.path.join(self.dataset_dir, "images")
|
| 18 |
+
self.split_path = os.path.join(self.dataset_dir, "split_zhou_DescribableTextures.json")
|
| 19 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 20 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 21 |
+
|
| 22 |
+
if os.path.exists(self.split_path):
|
| 23 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.image_dir)
|
| 24 |
+
else:
|
| 25 |
+
train, val, test = self.read_and_split_data(self.image_dir)
|
| 26 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.image_dir)
|
| 27 |
+
|
| 28 |
+
if num_shots >= 1:
|
| 29 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 30 |
+
|
| 31 |
+
if os.path.exists(preprocessed):
|
| 32 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 33 |
+
with open(preprocessed, "rb") as file:
|
| 34 |
+
data = pickle.load(file)
|
| 35 |
+
train, val = data["train"], data["val"]
|
| 36 |
+
else:
|
| 37 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 38 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 39 |
+
data = {"train": train, "val": val}
|
| 40 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 41 |
+
with open(preprocessed, "wb") as file:
|
| 42 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 43 |
+
|
| 44 |
+
subsample = subsample_classes
|
| 45 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample)
|
| 46 |
+
|
| 47 |
+
self.templates = [
|
| 48 |
+
lambda c: f'a photo of a {c} texture.',
|
| 49 |
+
lambda c: f'a photo of a {c} pattern.',
|
| 50 |
+
lambda c: f'a photo of a {c} thing.',
|
| 51 |
+
lambda c: f'a photo of a {c} object.',
|
| 52 |
+
lambda c: f'a photo of the {c} texture.',
|
| 53 |
+
lambda c: f'a photo of the {c} pattern.',
|
| 54 |
+
lambda c: f'a photo of the {c} thing.',
|
| 55 |
+
lambda c: f'a photo of the {c} object.',
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 59 |
+
|
| 60 |
+
@staticmethod
|
| 61 |
+
def read_and_split_data(image_dir, p_trn=0.5, p_val=0.2, ignored=[], new_cnames=None):
|
| 62 |
+
categories = listdir_nohidden(image_dir)
|
| 63 |
+
categories = [c for c in categories if c not in ignored]
|
| 64 |
+
categories.sort()
|
| 65 |
+
|
| 66 |
+
p_tst = 1 - p_trn - p_val
|
| 67 |
+
print(f"Splitting into {p_trn:.0%} train, {p_val:.0%} val, and {p_tst:.0%} test")
|
| 68 |
+
|
| 69 |
+
def _collate(ims, y, c):
|
| 70 |
+
items = []
|
| 71 |
+
for im in ims:
|
| 72 |
+
item = Datum(impath=im, label=y, classname=c)
|
| 73 |
+
items.append(item)
|
| 74 |
+
return items
|
| 75 |
+
|
| 76 |
+
train, val, test = [], [], []
|
| 77 |
+
for label, category in enumerate(categories):
|
| 78 |
+
category_dir = os.path.join(image_dir, category)
|
| 79 |
+
images = listdir_nohidden(category_dir)
|
| 80 |
+
images = [os.path.join(category_dir, im) for im in images]
|
| 81 |
+
random.shuffle(images)
|
| 82 |
+
n_total = len(images)
|
| 83 |
+
n_train = round(n_total * p_trn)
|
| 84 |
+
n_val = round(n_total * p_val)
|
| 85 |
+
n_test = n_total - n_train - n_val
|
| 86 |
+
assert n_train > 0 and n_val > 0 and n_test > 0
|
| 87 |
+
|
| 88 |
+
if new_cnames is not None and category in new_cnames:
|
| 89 |
+
category = new_cnames[category]
|
| 90 |
+
|
| 91 |
+
train.extend(_collate(images[:n_train], label, category))
|
| 92 |
+
val.extend(_collate(images[n_train : n_train + n_val], label, category))
|
| 93 |
+
test.extend(_collate(images[n_train + n_val :], label, category))
|
| 94 |
+
|
| 95 |
+
return train, val, test
|
MTIL_datasets/eurosat.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
|
| 4 |
+
from .utils import *
|
| 5 |
+
|
| 6 |
+
from .oxford_pets import OxfordPets
|
| 7 |
+
from .dtd import DescribableTextures as DTD
|
| 8 |
+
|
| 9 |
+
NEW_CNAMES = {
|
| 10 |
+
"AnnualCrop": "Annual Crop Land",
|
| 11 |
+
"Forest": "Forest",
|
| 12 |
+
"HerbaceousVegetation": "Herbaceous Vegetation Land",
|
| 13 |
+
"Highway": "Highway or Road",
|
| 14 |
+
"Industrial": "Industrial Buildings",
|
| 15 |
+
"Pasture": "Pasture Land",
|
| 16 |
+
"PermanentCrop": "Permanent Crop Land",
|
| 17 |
+
"Residential": "Residential Buildings",
|
| 18 |
+
"River": "River",
|
| 19 |
+
"SeaLake": "Sea or Lake",
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class EuroSAT(DatasetBase):
|
| 24 |
+
|
| 25 |
+
dataset_dir = "eurosat"
|
| 26 |
+
|
| 27 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 28 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 29 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 30 |
+
self.image_dir = os.path.join(self.dataset_dir, "2750")
|
| 31 |
+
self.split_path = os.path.join(self.dataset_dir, "split_zhou_EuroSAT.json")
|
| 32 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 33 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 34 |
+
|
| 35 |
+
if os.path.exists(self.split_path):
|
| 36 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.image_dir)
|
| 37 |
+
else:
|
| 38 |
+
train, val, test = DTD.read_and_split_data(self.image_dir, new_cnames=NEW_CNAMES)
|
| 39 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.image_dir)
|
| 40 |
+
|
| 41 |
+
if num_shots >= 1:
|
| 42 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 43 |
+
|
| 44 |
+
if os.path.exists(preprocessed):
|
| 45 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 46 |
+
with open(preprocessed, "rb") as file:
|
| 47 |
+
data = pickle.load(file)
|
| 48 |
+
train, val = data["train"], data["val"]
|
| 49 |
+
else:
|
| 50 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 51 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 52 |
+
data = {"train": train, "val": val}
|
| 53 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 54 |
+
with open(preprocessed, "wb") as file:
|
| 55 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 56 |
+
|
| 57 |
+
subsample = subsample_classes
|
| 58 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample)
|
| 59 |
+
|
| 60 |
+
self.templates = [
|
| 61 |
+
lambda c: f"a centered satellite photo of {c}.",
|
| 62 |
+
lambda c: f"a centered satellite photo of a {c}.",
|
| 63 |
+
lambda c: f"a centered satellite photo of the {c}.",
|
| 64 |
+
]
|
| 65 |
+
|
| 66 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 67 |
+
|
| 68 |
+
def update_classname(self, dataset_old):
|
| 69 |
+
dataset_new = []
|
| 70 |
+
for item_old in dataset_old:
|
| 71 |
+
cname_old = item_old.classname
|
| 72 |
+
cname_new = NEW_CLASSNAMES[cname_old]
|
| 73 |
+
item_new = Datum(impath=item_old.impath, label=item_old.label, classname=cname_new)
|
| 74 |
+
dataset_new.append(item_new)
|
| 75 |
+
return dataset_new
|
MTIL_datasets/fer2013.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import random
|
| 4 |
+
import warnings
|
| 5 |
+
from collections import defaultdict
|
| 6 |
+
|
| 7 |
+
from .utils import *
|
| 8 |
+
from .oxford_pets import OxfordPets
|
| 9 |
+
|
| 10 |
+
# Synonym sets per class (label index is the list index)
|
| 11 |
+
FER2013_CLASSES_SYNONYMS = [
|
| 12 |
+
['angry'],
|
| 13 |
+
['disgusted', 'disgust'],
|
| 14 |
+
['fearful', 'fear'],
|
| 15 |
+
['happy', 'smiling'],
|
| 16 |
+
['sad', 'depressed'],
|
| 17 |
+
['surprised', 'surprise', 'shocked', 'spooked'],
|
| 18 |
+
['neutral', 'bored'],
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
# Canonical class names are the first synonym in each list
|
| 22 |
+
FER2013_CANONICAL = [syns[0] for syns in FER2013_CLASSES_SYNONYMS]
|
| 23 |
+
|
| 24 |
+
# Prompt templates
|
| 25 |
+
FER2013_TEMPLATES = [
|
| 26 |
+
'a photo of a {} looking face.',
|
| 27 |
+
'a photo of a face showing the emotion: {}.',
|
| 28 |
+
'a photo of a face looking {}.',
|
| 29 |
+
'a face that looks {}.',
|
| 30 |
+
'they look {}.',
|
| 31 |
+
'look at how {} they are.',
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
FER2013_DEBUG = os.environ.get("FER2013_DEBUG", "0") not in ("0", "false", "False", "")
|
| 35 |
+
|
| 36 |
+
def _dbg(msg: str):
|
| 37 |
+
if FER2013_DEBUG:
|
| 38 |
+
print(f"[FER2013][DEBUG] {msg}")
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _norm(s: str) -> str:
|
| 42 |
+
s = s.lower().strip()
|
| 43 |
+
for ch in [" ", "_", "-", "."]:
|
| 44 |
+
s = s.replace(ch, "")
|
| 45 |
+
return s
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _build_syn_map():
|
| 49 |
+
m = {}
|
| 50 |
+
for y, syns in enumerate(FER2013_CLASSES_SYNONYMS):
|
| 51 |
+
for s in syns:
|
| 52 |
+
m[_norm(s)] = y
|
| 53 |
+
# add common canonical variants for safety
|
| 54 |
+
aliases = {
|
| 55 |
+
'disgust': 1,
|
| 56 |
+
'fear': 2,
|
| 57 |
+
'surprise': 5,
|
| 58 |
+
}
|
| 59 |
+
for k, v in aliases.items():
|
| 60 |
+
m[_norm(k)] = v
|
| 61 |
+
return m
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class FER2013(DatasetBase):
|
| 65 |
+
|
| 66 |
+
dataset_dir = "fer2013"
|
| 67 |
+
|
| 68 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 69 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 70 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 71 |
+
self.split_path = os.path.join(self.dataset_dir, "split_custom_FER2013.json")
|
| 72 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 73 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 74 |
+
|
| 75 |
+
train_dir = os.path.join(self.dataset_dir, "train")
|
| 76 |
+
test_dir = os.path.join(self.dataset_dir, "test")
|
| 77 |
+
if not os.path.isdir(train_dir) or not os.path.isdir(test_dir):
|
| 78 |
+
raise ValueError(
|
| 79 |
+
f"FER2013: expected train/test folders under '{self.dataset_dir}'. Got train={os.path.isdir(train_dir)}, test={os.path.isdir(test_dir)}"
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
# try cache
|
| 83 |
+
if os.path.exists(self.split_path):
|
| 84 |
+
try:
|
| 85 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.dataset_dir)
|
| 86 |
+
except Exception as e:
|
| 87 |
+
warnings.warn(f"FER2013: failed to read cached split; rebuilding. Error: {e}")
|
| 88 |
+
train, val, test = self._build_split(train_dir, test_dir)
|
| 89 |
+
try:
|
| 90 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.dataset_dir)
|
| 91 |
+
except Exception as e2:
|
| 92 |
+
warnings.warn(f"FER2013: failed to save split: {e2}")
|
| 93 |
+
else:
|
| 94 |
+
train, val, test = self._build_split(train_dir, test_dir)
|
| 95 |
+
try:
|
| 96 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.dataset_dir)
|
| 97 |
+
except Exception as e:
|
| 98 |
+
warnings.warn(f"FER2013: failed to save split: {e}")
|
| 99 |
+
|
| 100 |
+
if num_shots >= 1:
|
| 101 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 102 |
+
if os.path.exists(preprocessed):
|
| 103 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 104 |
+
with open(preprocessed, "rb") as file:
|
| 105 |
+
data = pickle.load(file)
|
| 106 |
+
train, val = data["train"], data["val"]
|
| 107 |
+
else:
|
| 108 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 109 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 110 |
+
data = {"train": train, "val": val}
|
| 111 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 112 |
+
with open(preprocessed, "wb") as file:
|
| 113 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 114 |
+
|
| 115 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample_classes)
|
| 116 |
+
self.templates = FER2013_TEMPLATES
|
| 117 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 118 |
+
|
| 119 |
+
def _build_split(self, train_dir, test_dir, p_val=0.2):
|
| 120 |
+
syn_map = _build_syn_map()
|
| 121 |
+
# read train per class
|
| 122 |
+
tr_items_by_label = defaultdict(list)
|
| 123 |
+
class_dirs = listdir_nohidden(train_dir, sort=True)
|
| 124 |
+
if not class_dirs:
|
| 125 |
+
warnings.warn(f"FER2013: no class folders found in {train_dir}")
|
| 126 |
+
for cls in class_dirs:
|
| 127 |
+
full = os.path.join(train_dir, cls)
|
| 128 |
+
if not os.path.isdir(full):
|
| 129 |
+
continue
|
| 130 |
+
key = _norm(cls)
|
| 131 |
+
y = syn_map.get(key)
|
| 132 |
+
if y is None:
|
| 133 |
+
warnings.warn(f"FER2013: unexpected class folder in train: '{cls}'")
|
| 134 |
+
continue
|
| 135 |
+
cname = FER2013_CANONICAL[y]
|
| 136 |
+
for fname in listdir_nohidden(full):
|
| 137 |
+
impath = os.path.join(full, fname)
|
| 138 |
+
tr_items_by_label[y].append(Datum(impath=impath, label=y, classname=cname))
|
| 139 |
+
|
| 140 |
+
# stratified split train->(train,val)
|
| 141 |
+
train, val = [], []
|
| 142 |
+
for y, items in tr_items_by_label.items():
|
| 143 |
+
random.shuffle(items)
|
| 144 |
+
n_val = max(1, round(len(items) * p_val)) if len(items) > 1 else 0
|
| 145 |
+
val.extend(items[:n_val])
|
| 146 |
+
train.extend(items[n_val:])
|
| 147 |
+
|
| 148 |
+
# read test
|
| 149 |
+
test = []
|
| 150 |
+
class_dirs = listdir_nohidden(test_dir, sort=True)
|
| 151 |
+
if not class_dirs:
|
| 152 |
+
warnings.warn(f"FER2013: no class folders found in {test_dir}")
|
| 153 |
+
for cls in class_dirs:
|
| 154 |
+
full = os.path.join(test_dir, cls)
|
| 155 |
+
if not os.path.isdir(full):
|
| 156 |
+
continue
|
| 157 |
+
key = _norm(cls)
|
| 158 |
+
y = syn_map.get(key)
|
| 159 |
+
if y is None:
|
| 160 |
+
warnings.warn(f"FER2013: unexpected class folder in test: '{cls}'")
|
| 161 |
+
continue
|
| 162 |
+
cname = FER2013_CANONICAL[y]
|
| 163 |
+
for fname in listdir_nohidden(full):
|
| 164 |
+
impath = os.path.join(full, fname)
|
| 165 |
+
test.append(Datum(impath=impath, label=y, classname=cname))
|
| 166 |
+
|
| 167 |
+
# basic sanity
|
| 168 |
+
if not train or not val or not test:
|
| 169 |
+
warnings.warn(f"FER2013: split sizes train={len(train)} val={len(val)} test={len(test)}")
|
| 170 |
+
return train, val, test
|
MTIL_datasets/fgvc_aircraft.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
|
| 4 |
+
from .utils import *
|
| 5 |
+
|
| 6 |
+
from .oxford_pets import OxfordPets
|
| 7 |
+
import torchvision.datasets
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class FGVCAircraft(DatasetBase):
|
| 11 |
+
|
| 12 |
+
dataset_dir = "fgvc_aircraft"
|
| 13 |
+
|
| 14 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 15 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 16 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 17 |
+
self.image_dir = os.path.join(self.dataset_dir, "images")
|
| 18 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 19 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 20 |
+
|
| 21 |
+
classnames = []
|
| 22 |
+
with open(os.path.join(self.dataset_dir, "variants.txt"), "r") as f:
|
| 23 |
+
lines = f.readlines()
|
| 24 |
+
for line in lines:
|
| 25 |
+
classnames.append(line.strip())
|
| 26 |
+
cname2lab = {c: i for i, c in enumerate(classnames)}
|
| 27 |
+
|
| 28 |
+
train = self.read_data(cname2lab, "images_variant_train.txt")
|
| 29 |
+
val = self.read_data(cname2lab, "images_variant_val.txt")
|
| 30 |
+
test = self.read_data(cname2lab, "images_variant_test.txt")
|
| 31 |
+
|
| 32 |
+
if num_shots >= 1:
|
| 33 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 34 |
+
|
| 35 |
+
if os.path.exists(preprocessed):
|
| 36 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 37 |
+
with open(preprocessed, "rb") as file:
|
| 38 |
+
data = pickle.load(file)
|
| 39 |
+
train, val = data["train"], data["val"]
|
| 40 |
+
else:
|
| 41 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 42 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 43 |
+
data = {"train": train, "val": val}
|
| 44 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 45 |
+
with open(preprocessed, "wb") as file:
|
| 46 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 47 |
+
|
| 48 |
+
subsample = subsample_classes
|
| 49 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample)
|
| 50 |
+
|
| 51 |
+
self.templates = [
|
| 52 |
+
lambda c: f"a photo of a {c}, a type of aircraft.",
|
| 53 |
+
lambda c: f"a photo of the {c}, a type of aircraft.",
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 57 |
+
|
| 58 |
+
def read_data(self, cname2lab, split_file):
|
| 59 |
+
filepath = os.path.join(self.dataset_dir, split_file)
|
| 60 |
+
items = []
|
| 61 |
+
|
| 62 |
+
with open(filepath, "r") as f:
|
| 63 |
+
lines = f.readlines()
|
| 64 |
+
for line in lines:
|
| 65 |
+
line = line.strip().split(" ")
|
| 66 |
+
imname = line[0] + ".jpg"
|
| 67 |
+
classname = " ".join(line[1:])
|
| 68 |
+
impath = os.path.join(self.image_dir, imname)
|
| 69 |
+
label = cname2lab[classname]
|
| 70 |
+
item = Datum(impath=impath, label=label, classname=classname)
|
| 71 |
+
items.append(item)
|
| 72 |
+
|
| 73 |
+
return items
|
MTIL_datasets/food101.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
|
| 4 |
+
from .utils import *
|
| 5 |
+
|
| 6 |
+
from .oxford_pets import OxfordPets
|
| 7 |
+
from .dtd import DescribableTextures as DTD
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Food101(DatasetBase):
|
| 12 |
+
|
| 13 |
+
dataset_dir = "food-101"
|
| 14 |
+
|
| 15 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 16 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 17 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 18 |
+
self.image_dir = os.path.join(self.dataset_dir, "images")
|
| 19 |
+
self.split_path = os.path.join(self.dataset_dir, "split_zhou_Food101.json")
|
| 20 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 21 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 22 |
+
|
| 23 |
+
if os.path.exists(self.split_path):
|
| 24 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.image_dir)
|
| 25 |
+
else:
|
| 26 |
+
train, val, test = DTD.read_and_split_data(self.image_dir)
|
| 27 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.image_dir)
|
| 28 |
+
|
| 29 |
+
if num_shots >= 1:
|
| 30 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 31 |
+
|
| 32 |
+
if os.path.exists(preprocessed):
|
| 33 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 34 |
+
with open(preprocessed, "rb") as file:
|
| 35 |
+
data = pickle.load(file)
|
| 36 |
+
train, val = data["train"], data["val"]
|
| 37 |
+
else:
|
| 38 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 39 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 40 |
+
data = {"train": train, "val": val}
|
| 41 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 42 |
+
with open(preprocessed, "wb") as file:
|
| 43 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 44 |
+
|
| 45 |
+
subsample = subsample_classes
|
| 46 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample)
|
| 47 |
+
|
| 48 |
+
self.templates = [
|
| 49 |
+
lambda c: f"a photo of a {c}, a type of food.",
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
super().__init__(train_x=train, val=val, test=test)
|
MTIL_datasets/gtsrb.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import random
|
| 4 |
+
import warnings
|
| 5 |
+
|
| 6 |
+
from .utils import *
|
| 7 |
+
from .oxford_pets import OxfordPets
|
| 8 |
+
|
| 9 |
+
# 43 classes for GTSRB with human-readable names
|
| 10 |
+
classes = [
|
| 11 |
+
'red and white circle 20 kph speed limit',
|
| 12 |
+
'red and white circle 30 kph speed limit',
|
| 13 |
+
'red and white circle 50 kph speed limit',
|
| 14 |
+
'red and white circle 60 kph speed limit',
|
| 15 |
+
'red and white circle 70 kph speed limit',
|
| 16 |
+
'red and white circle 80 kph speed limit',
|
| 17 |
+
'end / de-restriction of 80 kph speed limit',
|
| 18 |
+
'red and white circle 100 kph speed limit',
|
| 19 |
+
'red and white circle 120 kph speed limit',
|
| 20 |
+
'red and white circle red car and black car no passing',
|
| 21 |
+
'red and white circle red truck and black car no passing',
|
| 22 |
+
'red and white triangle road intersection warning',
|
| 23 |
+
'white and yellow diamond priority road',
|
| 24 |
+
'red and white upside down triangle yield right-of-way',
|
| 25 |
+
'stop',
|
| 26 |
+
'empty red and white circle',
|
| 27 |
+
'red and white circle no truck entry',
|
| 28 |
+
'red circle with white horizonal stripe no entry',
|
| 29 |
+
'red and white triangle with exclamation mark warning',
|
| 30 |
+
'red and white triangle with black left curve approaching warning',
|
| 31 |
+
'red and white triangle with black right curve approaching warning',
|
| 32 |
+
'red and white triangle with black double curve approaching warning',
|
| 33 |
+
'red and white triangle rough / bumpy road warning',
|
| 34 |
+
'red and white triangle car skidding / slipping warning',
|
| 35 |
+
'red and white triangle with merging / narrow lanes warning',
|
| 36 |
+
'red and white triangle with person digging / construction / road work warning',
|
| 37 |
+
'red and white triangle with traffic light approaching warning',
|
| 38 |
+
'red and white triangle with person walking warning',
|
| 39 |
+
'red and white triangle with child and person walking warning',
|
| 40 |
+
'red and white triangle with bicyle warning',
|
| 41 |
+
'red and white triangle with snowflake / ice warning',
|
| 42 |
+
'red and white triangle with deer warning',
|
| 43 |
+
'white circle with gray strike bar no speed limit',
|
| 44 |
+
'blue circle with white right turn arrow mandatory',
|
| 45 |
+
'blue circle with white left turn arrow mandatory',
|
| 46 |
+
'blue circle with white forward arrow mandatory',
|
| 47 |
+
'blue circle with white forward or right turn arrow mandatory',
|
| 48 |
+
'blue circle with white forward or left turn arrow mandatory',
|
| 49 |
+
'blue circle with white keep right arrow mandatory',
|
| 50 |
+
'blue circle with white keep left arrow mandatory',
|
| 51 |
+
'blue circle with white arrows indicating a traffic circle',
|
| 52 |
+
'white circle with gray strike bar indicating no passing for cars has ended',
|
| 53 |
+
'white circle with gray strike bar indicating no passing for trucks has ended',
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
DEBUG = os.environ.get("GTSRB_DEBUG", "0") not in ("0", "false", "False", "")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _dbg(msg: str):
|
| 61 |
+
if DEBUG:
|
| 62 |
+
print(f"[GTSRB][DEBUG] {msg}")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def _is_image_file(name):
|
| 66 |
+
name = name.lower()
|
| 67 |
+
return any(name.endswith(ext) for ext in ['.ppm', '.png', '.jpg', '.jpeg', '.bmp', '.webp'])
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class GTSRB(DatasetBase):
|
| 71 |
+
"""
|
| 72 |
+
German Traffic Sign Recognition Benchmark (GTSRB)
|
| 73 |
+
|
| 74 |
+
Expected structure:
|
| 75 |
+
<root>/gtsrb/
|
| 76 |
+
├─ 00000/*.ppm
|
| 77 |
+
├─ 00001/*.ppm
|
| 78 |
+
└─ ... up to 00042/
|
| 79 |
+
|
| 80 |
+
Note: The dataset has no official test split in this layout, so we will
|
| 81 |
+
randomly split each class into train/val/test (50%/20%/30%), cached to json.
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
dataset_dir = "gtsrb"
|
| 85 |
+
|
| 86 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 87 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 88 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 89 |
+
self.image_dir = self.dataset_dir
|
| 90 |
+
self.split_path = os.path.join(self.dataset_dir, "split_custom_GTSRB.json")
|
| 91 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 92 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 93 |
+
_dbg(f"dataset_dir={self.dataset_dir}")
|
| 94 |
+
_dbg(f"image_dir={self.image_dir}")
|
| 95 |
+
_dbg(f"split_path exists? {os.path.exists(self.split_path)}")
|
| 96 |
+
|
| 97 |
+
if os.path.exists(self.split_path):
|
| 98 |
+
try:
|
| 99 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.image_dir)
|
| 100 |
+
except Exception as e:
|
| 101 |
+
warnings.warn(f"GTSRB: failed to read split file '{self.split_path}'; rebuilding split. Error: {e}")
|
| 102 |
+
train, val, test = self.read_and_split_data(self.image_dir)
|
| 103 |
+
try:
|
| 104 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.image_dir)
|
| 105 |
+
except Exception as e2:
|
| 106 |
+
warnings.warn(f"GTSRB: failed to save rebuilt split to '{self.split_path}': {e2}")
|
| 107 |
+
else:
|
| 108 |
+
train, val, test = self.read_and_split_data(self.image_dir)
|
| 109 |
+
try:
|
| 110 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.image_dir)
|
| 111 |
+
except Exception as e:
|
| 112 |
+
warnings.warn(f"GTSRB: failed to save split to '{self.split_path}': {e}")
|
| 113 |
+
|
| 114 |
+
_dbg(f"loaded counts: train={len(train)}, val={len(val)}, test={len(test)}")
|
| 115 |
+
|
| 116 |
+
if num_shots >= 1:
|
| 117 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 118 |
+
if os.path.exists(preprocessed):
|
| 119 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 120 |
+
with open(preprocessed, "rb") as file:
|
| 121 |
+
data = pickle.load(file)
|
| 122 |
+
train, val = data["train"], data["val"]
|
| 123 |
+
else:
|
| 124 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 125 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 126 |
+
data = {"train": train, "val": val}
|
| 127 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 128 |
+
with open(preprocessed, "wb") as file:
|
| 129 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 130 |
+
_dbg(f"few-shot applied: train={len(train)}, val={len(val)} (shots={num_shots})")
|
| 131 |
+
|
| 132 |
+
# Ensure class subsampling behavior matches others
|
| 133 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample_classes)
|
| 134 |
+
_dbg(f"after subsample='{subsample_classes}': train={len(train)}, val={len(val)}, test={len(test)})")
|
| 135 |
+
|
| 136 |
+
# Prompt templates provided by user
|
| 137 |
+
self.templates = [
|
| 138 |
+
lambda c: f'a zoomed in photo of a "{c}" traffic sign.',
|
| 139 |
+
lambda c: f'a centered photo of a "{c}" traffic sign.',
|
| 140 |
+
lambda c: f'a close up photo of a "{c}" traffic sign.',
|
| 141 |
+
]
|
| 142 |
+
|
| 143 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 144 |
+
|
| 145 |
+
@staticmethod
|
| 146 |
+
def read_and_split_data(image_root, p_trn=0.5, p_val=0.2):
|
| 147 |
+
# Discover class directories (e.g., 00000 .. 00042)
|
| 148 |
+
try:
|
| 149 |
+
all_entries = listdir_nohidden(image_root, sort=True)
|
| 150 |
+
except Exception as e:
|
| 151 |
+
warnings.warn(f"GTSRB: failed to list directory '{image_root}': {e}")
|
| 152 |
+
raise
|
| 153 |
+
class_dirs = [d for d in all_entries if os.path.isdir(os.path.join(image_root, d))]
|
| 154 |
+
class_dirs.sort()
|
| 155 |
+
assert len(class_dirs) > 0, f"GTSRB: no class folders found under {image_root}"
|
| 156 |
+
_dbg(f"found {len(class_dirs)} class folders; head={class_dirs[:5]}")
|
| 157 |
+
|
| 158 |
+
# Map sorted class dirs to labels 0..N-1 and names from 'classes'
|
| 159 |
+
if len(class_dirs) != len(classes):
|
| 160 |
+
print(f"Warning: detected {len(class_dirs)} class folders but classes list has {len(classes)} entries. Proceeding with min overlap.")
|
| 161 |
+
num_labels = min(len(class_dirs), len(classes))
|
| 162 |
+
|
| 163 |
+
def _collate(paths, y, cname):
|
| 164 |
+
return [Datum(impath=p, label=y, classname=cname) for p in paths]
|
| 165 |
+
|
| 166 |
+
train, val, test = [], [], []
|
| 167 |
+
for y, cls_dir in enumerate(class_dirs[:num_labels]):
|
| 168 |
+
cname = classes[y]
|
| 169 |
+
cdir = os.path.join(image_root, cls_dir)
|
| 170 |
+
try:
|
| 171 |
+
files = listdir_nohidden(cdir, sort=False)
|
| 172 |
+
except Exception as e:
|
| 173 |
+
warnings.warn(f"GTSRB: failed to list class folder '{cdir}': {e}")
|
| 174 |
+
files = []
|
| 175 |
+
imgs = [os.path.join(cdir, f) for f in files if _is_image_file(f)]
|
| 176 |
+
random.shuffle(imgs)
|
| 177 |
+
n_total = len(imgs)
|
| 178 |
+
if n_total == 0:
|
| 179 |
+
warnings.warn(f"GTSRB: empty or unreadable class folder: {cdir}; skipping this class")
|
| 180 |
+
continue
|
| 181 |
+
if n_total < 5:
|
| 182 |
+
warnings.warn(f"GTSRB: very few images in class '{cls_dir}' (n={n_total}); splits may be unstable")
|
| 183 |
+
n_train = round(n_total * p_trn)
|
| 184 |
+
n_val = round(n_total * p_val)
|
| 185 |
+
n_test = n_total - n_train - n_val
|
| 186 |
+
if not (n_train > 0 and n_val > 0 and n_test > 0):
|
| 187 |
+
warnings.warn(f"GTSRB: split would create empty split for class {cls_dir} (n={n_total}); adjusting strategy to keep at least 1 per split")
|
| 188 |
+
# Fallback: enforce at least 1 per split if possible
|
| 189 |
+
if n_total >= 3:
|
| 190 |
+
n_train, n_val, n_test = 1, 1, n_total - 2
|
| 191 |
+
elif n_total == 2:
|
| 192 |
+
n_train, n_val, n_test = 1, 1, 0
|
| 193 |
+
else: # n_total == 1
|
| 194 |
+
n_train, n_val, n_test = 1, 0, 0
|
| 195 |
+
|
| 196 |
+
train.extend(_collate(imgs[:n_train], y, cname))
|
| 197 |
+
val.extend(_collate(imgs[n_train:n_train + n_val], y, cname))
|
| 198 |
+
test.extend(_collate(imgs[n_train + n_val:], y, cname))
|
| 199 |
+
_dbg(f"class {cls_dir} -> label {y}: total={n_total}, train={n_train}, val={n_val}, test={n_test}")
|
| 200 |
+
|
| 201 |
+
_dbg(f"aggregate sizes: train={len(train)}, val={len(val)}, test={len(test)}")
|
| 202 |
+
return train, val, test
|
MTIL_datasets/hatefulmemes.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import pickle
|
| 4 |
+
|
| 5 |
+
from .utils import *
|
| 6 |
+
from .oxford_pets import OxfordPets
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class HatefulMemes(DatasetBase):
|
| 10 |
+
"""
|
| 11 |
+
Hateful Memes dataset loader (image-only for MTIL).
|
| 12 |
+
|
| 13 |
+
Expected structure:
|
| 14 |
+
<root>/hatefulmemes/
|
| 15 |
+
├─ img/*.png|jpg
|
| 16 |
+
├─ train.jsonl
|
| 17 |
+
├─ dev.jsonl
|
| 18 |
+
└─ test.jsonl
|
| 19 |
+
|
| 20 |
+
JSONL lines example:
|
| 21 |
+
{"id":85362, "img":"img/85362.png", "label":0, "text":"..."}
|
| 22 |
+
|
| 23 |
+
We DO NOT use the 'text' field. Only image path and label are used.
|
| 24 |
+
|
| 25 |
+
Classes: ['meme', 'hatespeech meme']
|
| 26 |
+
Templates: ['a {}.']
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
dataset_dir = "hatefulmemes"
|
| 30 |
+
|
| 31 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 32 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 33 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 34 |
+
if not os.path.isdir(self.dataset_dir):
|
| 35 |
+
# allow alias without trailing 's'
|
| 36 |
+
alt = os.path.join(root, "hatefulmeme")
|
| 37 |
+
if os.path.isdir(alt):
|
| 38 |
+
self.dataset_dir = alt
|
| 39 |
+
else:
|
| 40 |
+
raise ValueError(
|
| 41 |
+
f"HatefulMemes: dataset folder not found at '{self.dataset_dir}' or '{alt}'"
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 45 |
+
mkdir_if_missing(split_fewshot_dir)
|
| 46 |
+
|
| 47 |
+
# paths
|
| 48 |
+
train_json = os.path.join(self.dataset_dir, "train.jsonl")
|
| 49 |
+
dev_json = os.path.join(self.dataset_dir, "dev.jsonl")
|
| 50 |
+
test_json = os.path.join(self.dataset_dir, "test.jsonl")
|
| 51 |
+
|
| 52 |
+
if not os.path.isfile(train_json) or not os.path.isfile(dev_json) or not os.path.isfile(test_json):
|
| 53 |
+
raise ValueError(
|
| 54 |
+
f"HatefulMemes: missing jsonl files. Expected train/dev/test at '{self.dataset_dir}'."
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
# fixed classes
|
| 58 |
+
classes = ["meme", "hatespeech meme"]
|
| 59 |
+
lab_to_name = {0: classes[0], 1: classes[1]}
|
| 60 |
+
|
| 61 |
+
# read splits
|
| 62 |
+
train = self._read_jsonl(train_json, lab_to_name)
|
| 63 |
+
val = self._read_jsonl(dev_json, lab_to_name)
|
| 64 |
+
test = self._read_jsonl(test_json, lab_to_name)
|
| 65 |
+
# Some public releases of Hateful Memes do not include labels for test.jsonl.
|
| 66 |
+
# In that case, fallback to use dev.jsonl for evaluation so zero-shot works.
|
| 67 |
+
if len(test) == 0 and len(val) > 0:
|
| 68 |
+
test = list(val)
|
| 69 |
+
|
| 70 |
+
# few-shot
|
| 71 |
+
if num_shots >= 1:
|
| 72 |
+
preprocessed = os.path.join(split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 73 |
+
if os.path.exists(preprocessed):
|
| 74 |
+
with open(preprocessed, "rb") as file:
|
| 75 |
+
data = pickle.load(file)
|
| 76 |
+
train, val = data["train"], data["val"]
|
| 77 |
+
else:
|
| 78 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 79 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 80 |
+
data = {"train": train, "val": val}
|
| 81 |
+
with open(preprocessed, "wb") as file:
|
| 82 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 83 |
+
|
| 84 |
+
# subsample behavior consistent with others
|
| 85 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample_classes)
|
| 86 |
+
|
| 87 |
+
# templates per user spec
|
| 88 |
+
self.templates = [
|
| 89 |
+
lambda c: f'a {c}.',
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 93 |
+
|
| 94 |
+
def _read_jsonl(self, filepath, lab_to_name):
|
| 95 |
+
items = []
|
| 96 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 97 |
+
for line in f:
|
| 98 |
+
line = line.strip()
|
| 99 |
+
if not line:
|
| 100 |
+
continue
|
| 101 |
+
try:
|
| 102 |
+
obj = json.loads(line)
|
| 103 |
+
except Exception:
|
| 104 |
+
continue
|
| 105 |
+
# fields: id, img, label, text (ignored)
|
| 106 |
+
img_rel = obj.get('img', '')
|
| 107 |
+
label = obj.get('label', None)
|
| 108 |
+
if img_rel is None or label is None:
|
| 109 |
+
continue
|
| 110 |
+
if label not in (0, 1):
|
| 111 |
+
raise ValueError(f"HatefulMemes: unexpected label {label} in {filepath}")
|
| 112 |
+
impath = os.path.join(self.dataset_dir, img_rel.replace('/', os.sep))
|
| 113 |
+
if not os.path.isfile(impath):
|
| 114 |
+
continue
|
| 115 |
+
cname = lab_to_name[label]
|
| 116 |
+
items.append(Datum(impath=impath, label=label, classname=cname))
|
| 117 |
+
return items
|
MTIL_datasets/imagenet_r.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import random
|
| 3 |
+
from typing import List, Tuple, Dict
|
| 4 |
+
|
| 5 |
+
from .utils import * # Datum, DatasetBase, listdir_nohidden, mkdir_if_missing
|
| 6 |
+
from .oxford_pets import OxfordPets
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
IMAGENETR_TEMPLATES: List[str] = [
|
| 10 |
+
'a bad photo of a {}.',
|
| 11 |
+
'a photo of many {}.',
|
| 12 |
+
'a sculpture of a {}.',
|
| 13 |
+
'a photo of the hard to see {}.',
|
| 14 |
+
'a low resolution photo of the {}.',
|
| 15 |
+
'a rendering of a {}.',
|
| 16 |
+
'graffiti of a {}.',
|
| 17 |
+
'a bad photo of the {}.',
|
| 18 |
+
'a cropped photo of the {}.',
|
| 19 |
+
'a tattoo of a {}.',
|
| 20 |
+
'the embroidered {}.',
|
| 21 |
+
'a photo of a hard to see {}.',
|
| 22 |
+
'a bright photo of a {}.',
|
| 23 |
+
'a photo of a clean {}.',
|
| 24 |
+
'a photo of a dirty {}.',
|
| 25 |
+
'a dark photo of the {}.',
|
| 26 |
+
'a drawing of a {}.',
|
| 27 |
+
'a photo of my {}.',
|
| 28 |
+
'the plastic {}.',
|
| 29 |
+
'a photo of the cool {}.',
|
| 30 |
+
'a close-up photo of a {}.',
|
| 31 |
+
'a black and white photo of the {}.',
|
| 32 |
+
'a painting of the {}.',
|
| 33 |
+
'a painting of a {}.',
|
| 34 |
+
'a pixelated photo of the {}.',
|
| 35 |
+
'a sculpture of the {}.',
|
| 36 |
+
'a bright photo of the {}.',
|
| 37 |
+
'a cropped photo of a {}.',
|
| 38 |
+
'a plastic {}.',
|
| 39 |
+
'a photo of the dirty {}.',
|
| 40 |
+
'a jpeg corrupted photo of a {}.',
|
| 41 |
+
'a blurry photo of the {}.',
|
| 42 |
+
'a photo of the {}.',
|
| 43 |
+
'a good photo of the {}.',
|
| 44 |
+
'a rendering of the {}.',
|
| 45 |
+
'a {} in a video game.',
|
| 46 |
+
'a photo of one {}.',
|
| 47 |
+
'a doodle of a {}.',
|
| 48 |
+
'a close-up photo of the {}.',
|
| 49 |
+
'a photo of a {}.',
|
| 50 |
+
'the origami {}.',
|
| 51 |
+
'the {} in a video game.',
|
| 52 |
+
'a sketch of a {}.',
|
| 53 |
+
'a doodle of the {}.',
|
| 54 |
+
'a origami {}.',
|
| 55 |
+
'a low resolution photo of a {}.',
|
| 56 |
+
'the toy {}.',
|
| 57 |
+
'a rendition of the {}.',
|
| 58 |
+
'a photo of the clean {}.',
|
| 59 |
+
'a photo of a large {}.',
|
| 60 |
+
'a rendition of a {}.',
|
| 61 |
+
'a photo of a nice {}.',
|
| 62 |
+
'a photo of a weird {}.',
|
| 63 |
+
'a blurry photo of a {}.',
|
| 64 |
+
'a cartoon {}.',
|
| 65 |
+
'art of a {}.',
|
| 66 |
+
'a sketch of the {}.',
|
| 67 |
+
'a embroidered {}.',
|
| 68 |
+
'a pixelated photo of a {}.',
|
| 69 |
+
'itap of the {}.',
|
| 70 |
+
'a jpeg corrupted photo of the {}.',
|
| 71 |
+
'a good photo of a {}.',
|
| 72 |
+
'a plushie {}.',
|
| 73 |
+
'a photo of the nice {}.',
|
| 74 |
+
'a photo of the small {}.',
|
| 75 |
+
'a photo of the weird {}.',
|
| 76 |
+
'the cartoon {}.',
|
| 77 |
+
'art of the {}.',
|
| 78 |
+
'a drawing of the {}.',
|
| 79 |
+
'a photo of the large {}.',
|
| 80 |
+
'a black and white photo of a {}.',
|
| 81 |
+
'the plushie {}.',
|
| 82 |
+
'a dark photo of a {}.',
|
| 83 |
+
'itap of a {}.',
|
| 84 |
+
'graffiti of the {}.',
|
| 85 |
+
'a toy {}.',
|
| 86 |
+
'itap of my {}.',
|
| 87 |
+
'a photo of a cool {}.',
|
| 88 |
+
'a photo of a small {}.',
|
| 89 |
+
'a tattoo of the {}.',
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class ImageNetR(DatasetBase):
|
| 94 |
+
|
| 95 |
+
dataset_dir = "imagenet-r"
|
| 96 |
+
|
| 97 |
+
def __init__(self, root: str, num_shots: int = 0, seed: int = 1, subsample_classes: str = 'all', test_ratio: float = 0.2):
|
| 98 |
+
"""
|
| 99 |
+
Expect directory structure:
|
| 100 |
+
{root}/imagenet-r/<wnid>/*.jpg|png|jpeg|bmp|webp
|
| 101 |
+
{root}/imagenet-r/classname.txt # lines: "<wnid> <human_readable_name>"
|
| 102 |
+
There is no official test split; we perform a per-class split into train/test, then
|
| 103 |
+
split train into train/val using OxfordPets.split_trainval.
|
| 104 |
+
"""
|
| 105 |
+
rnd = random.Random(seed)
|
| 106 |
+
|
| 107 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 108 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 109 |
+
self.split_path = os.path.join(self.dataset_dir, "split_custom_ImageNetR.json")
|
| 110 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 111 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 112 |
+
|
| 113 |
+
# Read class mapping from classname.txt
|
| 114 |
+
class_map_path = os.path.join(self.dataset_dir, "classname.txt")
|
| 115 |
+
if not os.path.exists(class_map_path):
|
| 116 |
+
raise FileNotFoundError(f"Class mapping file not found: {class_map_path}")
|
| 117 |
+
wnids, classnames = self._read_class_map(class_map_path)
|
| 118 |
+
self._wnids = wnids
|
| 119 |
+
self._classnames_ref = classnames
|
| 120 |
+
|
| 121 |
+
# Build or load split
|
| 122 |
+
if os.path.exists(self.split_path):
|
| 123 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.dataset_dir)
|
| 124 |
+
else:
|
| 125 |
+
trainval, test = self._read_from_folders(self.dataset_dir, wnids, classnames, rnd=rnd, test_ratio=test_ratio)
|
| 126 |
+
train, val = self._split_trainval_safe(trainval)
|
| 127 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.dataset_dir)
|
| 128 |
+
|
| 129 |
+
if num_shots >= 1:
|
| 130 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 131 |
+
if os.path.exists(preprocessed):
|
| 132 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 133 |
+
import pickle
|
| 134 |
+
with open(preprocessed, "rb") as file:
|
| 135 |
+
data = pickle.load(file)
|
| 136 |
+
train, val = data["train"], data["val"]
|
| 137 |
+
else:
|
| 138 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 139 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 140 |
+
data = {"train": train, "val": val}
|
| 141 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 142 |
+
import pickle
|
| 143 |
+
with open(preprocessed, "wb") as file:
|
| 144 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 145 |
+
|
| 146 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample_classes)
|
| 147 |
+
|
| 148 |
+
# Expose templates
|
| 149 |
+
self.templates = IMAGENETR_TEMPLATES
|
| 150 |
+
|
| 151 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 152 |
+
|
| 153 |
+
@staticmethod
|
| 154 |
+
def _read_class_map(filepath: str) -> Tuple[List[str], List[str]]:
|
| 155 |
+
wnids: List[str] = []
|
| 156 |
+
cnames: List[str] = []
|
| 157 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 158 |
+
for line in f:
|
| 159 |
+
line = line.strip()
|
| 160 |
+
if not line:
|
| 161 |
+
continue
|
| 162 |
+
parts = line.split()
|
| 163 |
+
wnid = parts[0]
|
| 164 |
+
cname = ' '.join(parts[1:]) if len(parts) > 1 else wnid
|
| 165 |
+
wnids.append(wnid)
|
| 166 |
+
cnames.append(cname)
|
| 167 |
+
return wnids, cnames
|
| 168 |
+
|
| 169 |
+
@staticmethod
|
| 170 |
+
def _split_trainval_safe(trainval: List[Datum], p_val: float = 0.2) -> Tuple[List[Datum], List[Datum]]:
|
| 171 |
+
from collections import defaultdict
|
| 172 |
+
tracker: Dict[int, List[int]] = defaultdict(list)
|
| 173 |
+
for idx, item in enumerate(trainval):
|
| 174 |
+
tracker[item.label].append(idx)
|
| 175 |
+
|
| 176 |
+
train, val = [], []
|
| 177 |
+
for label, idxs in tracker.items():
|
| 178 |
+
n = len(idxs)
|
| 179 |
+
if n <= 1:
|
| 180 |
+
for idx in idxs:
|
| 181 |
+
train.append(trainval[idx])
|
| 182 |
+
continue
|
| 183 |
+
n_val = max(1, int(round(n * p_val)))
|
| 184 |
+
if n_val >= n:
|
| 185 |
+
n_val = n - 1
|
| 186 |
+
random.shuffle(idxs)
|
| 187 |
+
for i, idx in enumerate(idxs):
|
| 188 |
+
if i < n_val:
|
| 189 |
+
val.append(trainval[idx])
|
| 190 |
+
else:
|
| 191 |
+
train.append(trainval[idx])
|
| 192 |
+
if len(val) == 0 and len(train) > 0:
|
| 193 |
+
val.append(train[-1])
|
| 194 |
+
train = train[:-1]
|
| 195 |
+
return train, val
|
| 196 |
+
|
| 197 |
+
def _read_from_folders(self, data_dir: str, wnids: List[str], classnames: List[str], rnd: random.Random, test_ratio: float) -> Tuple[List[Datum], List[Datum]]:
|
| 198 |
+
exts = {".png", ".jpg", ".jpeg", ".bmp", ".webp"}
|
| 199 |
+
class_to_label = {wnid: i for i, wnid in enumerate(wnids)}
|
| 200 |
+
label_to_cname = {i: classnames[i] for i in range(len(classnames))}
|
| 201 |
+
|
| 202 |
+
items_by_label: Dict[int, List[Datum]] = {i: [] for i in range(len(wnids))}
|
| 203 |
+
for wnid in wnids:
|
| 204 |
+
cdir = os.path.join(data_dir, wnid)
|
| 205 |
+
if not os.path.isdir(cdir):
|
| 206 |
+
# If a class listed in mapping has no folder, skip gracefully
|
| 207 |
+
continue
|
| 208 |
+
label = class_to_label[wnid]
|
| 209 |
+
cname = label_to_cname[label]
|
| 210 |
+
for fname in listdir_nohidden(cdir, sort=True):
|
| 211 |
+
fext = os.path.splitext(fname)[1].lower()
|
| 212 |
+
if fext not in exts:
|
| 213 |
+
continue
|
| 214 |
+
impath = os.path.join(cdir, fname)
|
| 215 |
+
items_by_label[label].append(Datum(impath=impath, label=label, classname=cname))
|
| 216 |
+
|
| 217 |
+
trainval: List[Datum] = []
|
| 218 |
+
test: List[Datum] = []
|
| 219 |
+
for label, items in items_by_label.items():
|
| 220 |
+
if not items:
|
| 221 |
+
continue
|
| 222 |
+
rnd.shuffle(items)
|
| 223 |
+
if len(items) == 1:
|
| 224 |
+
trainval.extend(items)
|
| 225 |
+
continue
|
| 226 |
+
n_test = max(1, int(round(len(items) * test_ratio)))
|
| 227 |
+
if n_test >= len(items):
|
| 228 |
+
n_test = len(items) - 1
|
| 229 |
+
test.extend(items[:n_test])
|
| 230 |
+
trainval.extend(items[n_test:])
|
| 231 |
+
return trainval, test
|
MTIL_datasets/kitti_distance.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import random
|
| 3 |
+
from typing import List, Tuple
|
| 4 |
+
|
| 5 |
+
from .utils import * # Datum, DatasetBase, listdir_nohidden, mkdir_if_missing
|
| 6 |
+
from .oxford_pets import OxfordPets
|
| 7 |
+
|
| 8 |
+
# Class names per user specification (ordered)
|
| 9 |
+
KITTI_DISTANCE_CLASSES: List[str] = [
|
| 10 |
+
'a photo i took of a car nearby',
|
| 11 |
+
'a photo i took with a car in the middle distance',
|
| 12 |
+
'a photo i took with a car faraway',
|
| 13 |
+
'a photo i took with no car.',
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
# Keep templates as strings with {}
|
| 17 |
+
KITTI_DISTANCE_TEMPLATES: List[str] = [
|
| 18 |
+
'{}',
|
| 19 |
+
]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class KittiDistance(DatasetBase):
|
| 23 |
+
|
| 24 |
+
dataset_dir = "kitti"
|
| 25 |
+
|
| 26 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all', test_ratio: float = 0.2):
|
| 27 |
+
"""
|
| 28 |
+
Predict the distance category of the closest car from KITTI labels.
|
| 29 |
+
|
| 30 |
+
Expected structure:
|
| 31 |
+
{root}/kitti/
|
| 32 |
+
image/000004.png
|
| 33 |
+
label/000004.txt
|
| 34 |
+
|
| 35 |
+
Label file format: KITTI object detection label lines.
|
| 36 |
+
We retain only lines with type == 'Car'.
|
| 37 |
+
For these, we read the 14th column (index 13, loc_z) as the camera-depth in meters.
|
| 38 |
+
We take the minimum positive loc_z across all 'Car' lines as z_min for the image.
|
| 39 |
+
If there is no valid positive loc_z for 'Car', we assign the 'no car' class.
|
| 40 |
+
|
| 41 |
+
Discretization into classes:
|
| 42 |
+
0: 0 < z_min < 10 -> 'nearby'
|
| 43 |
+
1: 10 <= z_min < 30 -> 'middle distance'
|
| 44 |
+
2: z_min >= 30 -> 'faraway'
|
| 45 |
+
3: no car -> 'no car.'
|
| 46 |
+
|
| 47 |
+
No official test set; we split randomly (per class) into train/test by test_ratio.
|
| 48 |
+
Then we split train into train/val using a safe per-class split.
|
| 49 |
+
Splits are saved to JSON for reproducibility.
|
| 50 |
+
"""
|
| 51 |
+
rnd = random.Random(seed)
|
| 52 |
+
|
| 53 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 54 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 55 |
+
|
| 56 |
+
# Image and label directories
|
| 57 |
+
image_dir = os.path.join(self.dataset_dir, 'image')
|
| 58 |
+
if not os.path.isdir(image_dir):
|
| 59 |
+
# Graceful fallback to standard KITTI naming variants if provided
|
| 60 |
+
alt1 = os.path.join(self.dataset_dir, 'image_2')
|
| 61 |
+
alt2 = os.path.join(self.dataset_dir, 'images')
|
| 62 |
+
if os.path.isdir(alt1):
|
| 63 |
+
image_dir = alt1
|
| 64 |
+
elif os.path.isdir(alt2):
|
| 65 |
+
image_dir = alt2
|
| 66 |
+
label_dir = os.path.join(self.dataset_dir, 'label')
|
| 67 |
+
if not os.path.isdir(label_dir):
|
| 68 |
+
alt_l1 = os.path.join(self.dataset_dir, 'label_2')
|
| 69 |
+
alt_l2 = os.path.join(self.dataset_dir, 'labels')
|
| 70 |
+
if os.path.isdir(alt_l1):
|
| 71 |
+
label_dir = alt_l1
|
| 72 |
+
elif os.path.isdir(alt_l2):
|
| 73 |
+
label_dir = alt_l2
|
| 74 |
+
|
| 75 |
+
if not os.path.isdir(image_dir):
|
| 76 |
+
raise FileNotFoundError(f"KittiDistance: image dir not found: {image_dir}")
|
| 77 |
+
if not os.path.isdir(label_dir):
|
| 78 |
+
raise FileNotFoundError(f"KittiDistance: label dir not found: {label_dir}")
|
| 79 |
+
|
| 80 |
+
self.image_dir = image_dir
|
| 81 |
+
self.label_dir = label_dir
|
| 82 |
+
|
| 83 |
+
self.split_path = os.path.join(self.dataset_dir, "split_custom_KittiDistance.json")
|
| 84 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 85 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 86 |
+
|
| 87 |
+
if os.path.exists(self.split_path):
|
| 88 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.image_dir)
|
| 89 |
+
else:
|
| 90 |
+
trainval, test = self._read_and_split(self.image_dir, self.label_dir, rnd=rnd, test_ratio=test_ratio)
|
| 91 |
+
train, val = self._split_trainval_safe(trainval)
|
| 92 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.image_dir)
|
| 93 |
+
|
| 94 |
+
if num_shots >= 1:
|
| 95 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 96 |
+
if os.path.exists(preprocessed):
|
| 97 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 98 |
+
import pickle
|
| 99 |
+
with open(preprocessed, "rb") as file:
|
| 100 |
+
data = pickle.load(file)
|
| 101 |
+
train, val = data["train"], data["val"]
|
| 102 |
+
else:
|
| 103 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 104 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 105 |
+
data = {"train": train, "val": val}
|
| 106 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 107 |
+
import pickle
|
| 108 |
+
with open(preprocessed, "wb") as file:
|
| 109 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 110 |
+
|
| 111 |
+
# Allow class subsampling if requested
|
| 112 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample_classes)
|
| 113 |
+
|
| 114 |
+
# Templates per user specification
|
| 115 |
+
self.templates = KITTI_DISTANCE_TEMPLATES
|
| 116 |
+
|
| 117 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 118 |
+
# Override inferred metadata to ensure stable 4-way classification regardless of train label coverage
|
| 119 |
+
self._classnames = KITTI_DISTANCE_CLASSES
|
| 120 |
+
self._lab2cname = {i: c for i, c in enumerate(KITTI_DISTANCE_CLASSES)}
|
| 121 |
+
self._num_classes = len(KITTI_DISTANCE_CLASSES)
|
| 122 |
+
|
| 123 |
+
def _read_and_split(self, image_dir: str, label_dir: str, rnd: random.Random, test_ratio: float) -> Tuple[List[Datum], List[Datum]]:
|
| 124 |
+
exts = {".png", ".jpg", ".jpeg", ".bmp", ".webp"}
|
| 125 |
+
# Collect items with computed labels
|
| 126 |
+
items_by_label = {i: [] for i in range(len(KITTI_DISTANCE_CLASSES))}
|
| 127 |
+
|
| 128 |
+
for fname in listdir_nohidden(image_dir, sort=True):
|
| 129 |
+
fext = os.path.splitext(fname)[1].lower()
|
| 130 |
+
if fext not in exts:
|
| 131 |
+
continue
|
| 132 |
+
impath = os.path.join(image_dir, fname)
|
| 133 |
+
stem = os.path.splitext(fname)[0]
|
| 134 |
+
label_path = os.path.join(label_dir, stem + '.txt')
|
| 135 |
+
label_idx = self._compute_label_from_kitti(label_path)
|
| 136 |
+
cname = KITTI_DISTANCE_CLASSES[label_idx]
|
| 137 |
+
items_by_label[label_idx].append(Datum(impath=impath, label=label_idx, classname=cname))
|
| 138 |
+
|
| 139 |
+
# Per-class balanced split into trainval/test
|
| 140 |
+
trainval, test = [], []
|
| 141 |
+
for label, items in items_by_label.items():
|
| 142 |
+
if not items:
|
| 143 |
+
continue
|
| 144 |
+
rnd.shuffle(items)
|
| 145 |
+
if len(items) == 1:
|
| 146 |
+
trainval.extend(items)
|
| 147 |
+
continue
|
| 148 |
+
n_test = max(1, int(round(len(items) * test_ratio)))
|
| 149 |
+
if n_test >= len(items):
|
| 150 |
+
n_test = len(items) - 1
|
| 151 |
+
test.extend(items[:n_test])
|
| 152 |
+
trainval.extend(items[n_test:])
|
| 153 |
+
|
| 154 |
+
return trainval, test
|
| 155 |
+
|
| 156 |
+
@staticmethod
|
| 157 |
+
def _compute_label_from_kitti(label_path: str) -> int:
|
| 158 |
+
"""
|
| 159 |
+
Parse KITTI label file; focus only on lines with type == 'Car'.
|
| 160 |
+
Extract the 14th column (index 13, loc_z) as meters; consider only positive values.
|
| 161 |
+
If no positive loc_z found -> class 'no car' (index 3).
|
| 162 |
+
|
| 163 |
+
Thresholds:
|
| 164 |
+
0: 0 < z < 10
|
| 165 |
+
1: 10 <= z < 30
|
| 166 |
+
2: z >= 30
|
| 167 |
+
"""
|
| 168 |
+
z_vals = []
|
| 169 |
+
if os.path.isfile(label_path):
|
| 170 |
+
with open(label_path, 'r') as f:
|
| 171 |
+
for line in f:
|
| 172 |
+
line = line.strip()
|
| 173 |
+
if not line:
|
| 174 |
+
continue
|
| 175 |
+
parts = line.split()
|
| 176 |
+
obj_type = parts[0]
|
| 177 |
+
if obj_type != 'Car':
|
| 178 |
+
continue
|
| 179 |
+
if len(parts) < 14:
|
| 180 |
+
# Not a valid KITTI detection line; skip
|
| 181 |
+
continue
|
| 182 |
+
try:
|
| 183 |
+
# parts[13] is loc_z (0-based index), per KITTI format
|
| 184 |
+
z = float(parts[13])
|
| 185 |
+
if z > 0:
|
| 186 |
+
z_vals.append(z)
|
| 187 |
+
except Exception:
|
| 188 |
+
continue
|
| 189 |
+
# Determine class index
|
| 190 |
+
if not z_vals:
|
| 191 |
+
return 3 # no car
|
| 192 |
+
z_min = min(z_vals)
|
| 193 |
+
if z_min < 10:
|
| 194 |
+
return 0
|
| 195 |
+
elif z_min < 30:
|
| 196 |
+
return 1
|
| 197 |
+
else:
|
| 198 |
+
return 2
|
| 199 |
+
|
| 200 |
+
@staticmethod
|
| 201 |
+
def _split_trainval_safe(trainval: List[Datum], p_val: float = 0.2) -> Tuple[List[Datum], List[Datum]]:
|
| 202 |
+
from collections import defaultdict
|
| 203 |
+
tracker = defaultdict(list)
|
| 204 |
+
for idx, item in enumerate(trainval):
|
| 205 |
+
tracker[item.label].append(idx)
|
| 206 |
+
|
| 207 |
+
train, val = [], []
|
| 208 |
+
for label, idxs in tracker.items():
|
| 209 |
+
n = len(idxs)
|
| 210 |
+
if n <= 1:
|
| 211 |
+
for idx in idxs:
|
| 212 |
+
train.append(trainval[idx])
|
| 213 |
+
continue
|
| 214 |
+
n_val = max(1, int(round(n * p_val)))
|
| 215 |
+
if n_val >= n:
|
| 216 |
+
n_val = n - 1
|
| 217 |
+
random.shuffle(idxs)
|
| 218 |
+
for i, idx in enumerate(idxs):
|
| 219 |
+
if i < n_val:
|
| 220 |
+
val.append(trainval[idx])
|
| 221 |
+
else:
|
| 222 |
+
train.append(trainval[idx])
|
| 223 |
+
|
| 224 |
+
if len(val) == 0 and len(train) > 0:
|
| 225 |
+
val.append(train[-1])
|
| 226 |
+
train = train[:-1]
|
| 227 |
+
|
| 228 |
+
return train, val
|
MTIL_datasets/mnist.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
|
| 4 |
+
from .utils import *
|
| 5 |
+
|
| 6 |
+
from .oxford_pets import OxfordPets
|
| 7 |
+
from .dtd import DescribableTextures as DTD
|
| 8 |
+
from .oxford_pets import OxfordPets
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import codecs
|
| 12 |
+
import numpy as np
|
| 13 |
+
import sys
|
| 14 |
+
|
| 15 |
+
classes = [
|
| 16 |
+
"0 - zero",
|
| 17 |
+
"1 - one",
|
| 18 |
+
"2 - two",
|
| 19 |
+
"3 - three",
|
| 20 |
+
"4 - four",
|
| 21 |
+
"5 - five",
|
| 22 |
+
"6 - six",
|
| 23 |
+
"7 - seven",
|
| 24 |
+
"8 - eight",
|
| 25 |
+
"9 - nine",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
class MNIST(DatasetBase):
|
| 29 |
+
|
| 30 |
+
dataset_dir = "mnist"
|
| 31 |
+
|
| 32 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 33 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 34 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 35 |
+
self.image_dir = self.dataset_dir
|
| 36 |
+
|
| 37 |
+
trainval_image_file = "train-images-idx3-ubyte"
|
| 38 |
+
trainval_data = read_image_file(os.path.join(self.image_dir, trainval_image_file)) # Size([60000, 28, 28]) torch.uint8
|
| 39 |
+
trainval_label_file = "train-labels-idx1-ubyte"
|
| 40 |
+
trainval_targets = read_label_file(os.path.join(self.image_dir, trainval_label_file)) # Size([60000]) torch.int64
|
| 41 |
+
# trainval_names =
|
| 42 |
+
|
| 43 |
+
test_image_file = "t10k-images-idx3-ubyte"
|
| 44 |
+
test_data = read_image_file(os.path.join(self.image_dir, test_image_file))
|
| 45 |
+
test_label_file = "t10k-labels-idx1-ubyte"
|
| 46 |
+
test_targets = read_label_file(os.path.join(self.image_dir, test_label_file))
|
| 47 |
+
|
| 48 |
+
trainval = []
|
| 49 |
+
for idx in range(trainval_data.size(0)):
|
| 50 |
+
item = Datum(impath=Image.fromarray(trainval_data[idx].numpy(), mode="L"),
|
| 51 |
+
label=int(trainval_targets[idx]), classname=classes[trainval_targets[idx]])
|
| 52 |
+
trainval.append(item)
|
| 53 |
+
|
| 54 |
+
test = []
|
| 55 |
+
for idx in range(test_data.size(0)):
|
| 56 |
+
item = Datum(impath=Image.fromarray(test_data[idx].numpy(), mode="L"),
|
| 57 |
+
label=int(test_targets[idx]), classname=classes[test_targets[idx]])
|
| 58 |
+
test.append(item)
|
| 59 |
+
|
| 60 |
+
train, val = OxfordPets.split_trainval(trainval)
|
| 61 |
+
|
| 62 |
+
if num_shots >= 1:
|
| 63 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 64 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 65 |
+
|
| 66 |
+
subsample = subsample_classes
|
| 67 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample)
|
| 68 |
+
|
| 69 |
+
self.templates = [
|
| 70 |
+
lambda c: f'a photo of the number: "{c}".',
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _flip_byte_order(t: torch.Tensor) -> torch.Tensor:
|
| 77 |
+
return (
|
| 78 |
+
t.contiguous().view(torch.uint8).view(*t.shape, t.element_size()).flip(-1).view(*t.shape[:-1], -1).view(t.dtype)
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
def get_int(b: bytes) -> int:
|
| 82 |
+
return int(codecs.encode(b, "hex"), 16)
|
| 83 |
+
|
| 84 |
+
SN3_PASCALVINCENT_TYPEMAP = {
|
| 85 |
+
8: torch.uint8,
|
| 86 |
+
9: torch.int8,
|
| 87 |
+
11: torch.int16,
|
| 88 |
+
12: torch.int32,
|
| 89 |
+
13: torch.float32,
|
| 90 |
+
14: torch.float64,
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
def read_sn3_pascalvincent_tensor(path: str, strict: bool = True) -> torch.Tensor:
|
| 94 |
+
"""Read a SN3 file in "Pascal Vincent" format (Lush file 'libidx/idx-io.lsh').
|
| 95 |
+
Argument may be a filename, compressed filename, or file object.
|
| 96 |
+
"""
|
| 97 |
+
# read
|
| 98 |
+
with open(path, "rb") as f:
|
| 99 |
+
data = f.read()
|
| 100 |
+
# parse
|
| 101 |
+
magic = get_int(data[0:4])
|
| 102 |
+
nd = magic % 256
|
| 103 |
+
ty = magic // 256
|
| 104 |
+
assert 1 <= nd <= 3
|
| 105 |
+
assert 8 <= ty <= 14
|
| 106 |
+
torch_type = SN3_PASCALVINCENT_TYPEMAP[ty]
|
| 107 |
+
s = [get_int(data[4 * (i + 1) : 4 * (i + 2)]) for i in range(nd)]
|
| 108 |
+
|
| 109 |
+
parsed = torch.frombuffer(bytearray(data), dtype=torch_type, offset=(4 * (nd + 1)))
|
| 110 |
+
if sys.byteorder == "little" and parsed.element_size() > 1:
|
| 111 |
+
parsed = _flip_byte_order(parsed)
|
| 112 |
+
|
| 113 |
+
assert parsed.shape[0] == np.prod(s) or not strict
|
| 114 |
+
return parsed.view(*s)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def read_label_file(path: str) -> torch.Tensor:
|
| 118 |
+
x = read_sn3_pascalvincent_tensor(path, strict=False)
|
| 119 |
+
if x.dtype != torch.uint8:
|
| 120 |
+
raise TypeError(f"x should be of dtype torch.uint8 instead of {x.dtype}")
|
| 121 |
+
if x.ndimension() != 1:
|
| 122 |
+
raise ValueError(f"x should have 1 dimension instead of {x.ndimension()}")
|
| 123 |
+
return x.long()
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def read_image_file(path: str) -> torch.Tensor:
|
| 127 |
+
x = read_sn3_pascalvincent_tensor(path, strict=False)
|
| 128 |
+
if x.dtype != torch.uint8:
|
| 129 |
+
raise TypeError(f"x should be of dtype torch.uint8 instead of {x.dtype}")
|
| 130 |
+
if x.ndimension() != 3:
|
| 131 |
+
raise ValueError(f"x should have 3 dimension instead of {x.ndimension()}")
|
| 132 |
+
return x
|
MTIL_datasets/oxford_flowers.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import random
|
| 4 |
+
from scipy.io import loadmat
|
| 5 |
+
from collections import defaultdict
|
| 6 |
+
|
| 7 |
+
from .utils import *
|
| 8 |
+
|
| 9 |
+
from .oxford_pets import OxfordPets
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class OxfordFlowers(DatasetBase):
|
| 13 |
+
|
| 14 |
+
dataset_dir = "oxford_flowers"
|
| 15 |
+
|
| 16 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 17 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 18 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 19 |
+
self.image_dir = os.path.join(self.dataset_dir, "jpg")
|
| 20 |
+
self.label_file = os.path.join(self.dataset_dir, "imagelabels.mat")
|
| 21 |
+
self.lab2cname_file = os.path.join(self.dataset_dir, "cat_to_name.json")
|
| 22 |
+
self.split_path = os.path.join(self.dataset_dir, "split_zhou_OxfordFlowers.json")
|
| 23 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 24 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 25 |
+
|
| 26 |
+
if os.path.exists(self.split_path):
|
| 27 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.image_dir)
|
| 28 |
+
else:
|
| 29 |
+
train, val, test = self.read_data()
|
| 30 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.image_dir)
|
| 31 |
+
|
| 32 |
+
if num_shots >= 1:
|
| 33 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 34 |
+
|
| 35 |
+
if os.path.exists(preprocessed):
|
| 36 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 37 |
+
with open(preprocessed, "rb") as file:
|
| 38 |
+
data = pickle.load(file)
|
| 39 |
+
train, val = data["train"], data["val"]
|
| 40 |
+
else:
|
| 41 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 42 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 43 |
+
data = {"train": train, "val": val}
|
| 44 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 45 |
+
with open(preprocessed, "wb") as file:
|
| 46 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 47 |
+
|
| 48 |
+
subsample = subsample_classes
|
| 49 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample)
|
| 50 |
+
|
| 51 |
+
self.templates = [
|
| 52 |
+
lambda c: f"a photo of a {c}, a type of flower.",
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 56 |
+
|
| 57 |
+
def read_data(self):
|
| 58 |
+
tracker = defaultdict(list)
|
| 59 |
+
label_file = loadmat(self.label_file)["labels"][0]
|
| 60 |
+
for i, label in enumerate(label_file):
|
| 61 |
+
imname = f"image_{str(i + 1).zfill(5)}.jpg"
|
| 62 |
+
impath = os.path.join(self.image_dir, imname)
|
| 63 |
+
label = int(label)
|
| 64 |
+
tracker[label].append(impath)
|
| 65 |
+
|
| 66 |
+
print("Splitting data into 50% train, 20% val, and 30% test")
|
| 67 |
+
|
| 68 |
+
def _collate(ims, y, c):
|
| 69 |
+
items = []
|
| 70 |
+
for im in ims:
|
| 71 |
+
item = Datum(impath=im, label=y - 1, classname=c)
|
| 72 |
+
items.append(item)
|
| 73 |
+
return items
|
| 74 |
+
|
| 75 |
+
lab2cname = read_json(self.lab2cname_file)
|
| 76 |
+
train, val, test = [], [], []
|
| 77 |
+
for label, impaths in tracker.items():
|
| 78 |
+
random.shuffle(impaths)
|
| 79 |
+
n_total = len(impaths)
|
| 80 |
+
n_train = round(n_total * 0.5)
|
| 81 |
+
n_val = round(n_total * 0.2)
|
| 82 |
+
n_test = n_total - n_train - n_val
|
| 83 |
+
assert n_train > 0 and n_val > 0 and n_test > 0
|
| 84 |
+
cname = lab2cname[str(label)]
|
| 85 |
+
train.extend(_collate(impaths[:n_train], label, cname))
|
| 86 |
+
val.extend(_collate(impaths[n_train : n_train + n_val], label, cname))
|
| 87 |
+
test.extend(_collate(impaths[n_train + n_val :], label, cname))
|
| 88 |
+
|
| 89 |
+
return train, val, test
|
MTIL_datasets/oxford_pets.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import math
|
| 4 |
+
import random
|
| 5 |
+
from collections import defaultdict
|
| 6 |
+
|
| 7 |
+
from .utils import *
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class OxfordPets(DatasetBase):
|
| 11 |
+
|
| 12 |
+
dataset_dir = "oxford_pets"
|
| 13 |
+
|
| 14 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 15 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 16 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 17 |
+
self.image_dir = os.path.join(self.dataset_dir, "images")
|
| 18 |
+
self.anno_dir = os.path.join(self.dataset_dir, "annotations")
|
| 19 |
+
self.split_path = os.path.join(self.dataset_dir, "split_zhou_OxfordPets.json")
|
| 20 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 21 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 22 |
+
|
| 23 |
+
if os.path.exists(self.split_path):
|
| 24 |
+
train, val, test = self.read_split(self.split_path, self.image_dir)
|
| 25 |
+
else:
|
| 26 |
+
trainval = self.read_data(split_file="trainval.txt")
|
| 27 |
+
test = self.read_data(split_file="test.txt")
|
| 28 |
+
train, val = self.split_trainval(trainval)
|
| 29 |
+
self.save_split(train, val, test, self.split_path, self.image_dir)
|
| 30 |
+
|
| 31 |
+
if num_shots >= 1:
|
| 32 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 33 |
+
|
| 34 |
+
if os.path.exists(preprocessed):
|
| 35 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 36 |
+
with open(preprocessed, "rb") as file:
|
| 37 |
+
data = pickle.load(file)
|
| 38 |
+
train, val = data["train"], data["val"]
|
| 39 |
+
else:
|
| 40 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 41 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 42 |
+
data = {"train": train, "val": val}
|
| 43 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 44 |
+
with open(preprocessed, "wb") as file:
|
| 45 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 46 |
+
|
| 47 |
+
subsample = subsample_classes
|
| 48 |
+
train, val, test = self.subsample_classes(train, val, test, subsample=subsample)
|
| 49 |
+
|
| 50 |
+
self.templates = [
|
| 51 |
+
lambda c: f"a photo of a {c}, a type of pet.",
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 55 |
+
|
| 56 |
+
def read_data(self, split_file):
|
| 57 |
+
filepath = os.path.join(self.anno_dir, split_file)
|
| 58 |
+
items = []
|
| 59 |
+
|
| 60 |
+
with open(filepath, "r") as f:
|
| 61 |
+
lines = f.readlines()
|
| 62 |
+
for line in lines:
|
| 63 |
+
line = line.strip()
|
| 64 |
+
imname, label, species, _ = line.split(" ")
|
| 65 |
+
breed = imname.split("_")[:-1]
|
| 66 |
+
breed = "_".join(breed)
|
| 67 |
+
breed = breed.lower()
|
| 68 |
+
imname += ".jpg"
|
| 69 |
+
impath = os.path.join(self.image_dir, imname)
|
| 70 |
+
label = int(label) - 1 # convert to 0-based index
|
| 71 |
+
item = Datum(impath=impath, label=label, classname=breed)
|
| 72 |
+
items.append(item)
|
| 73 |
+
|
| 74 |
+
return items
|
| 75 |
+
|
| 76 |
+
@staticmethod
|
| 77 |
+
def split_trainval(trainval, p_val=0.2):
|
| 78 |
+
p_trn = 1 - p_val
|
| 79 |
+
tracker = defaultdict(list)
|
| 80 |
+
for idx, item in enumerate(trainval):
|
| 81 |
+
label = item.label
|
| 82 |
+
tracker[label].append(idx)
|
| 83 |
+
|
| 84 |
+
train, val = [], []
|
| 85 |
+
for label, idxs in tracker.items():
|
| 86 |
+
n_val = round(len(idxs) * p_val)
|
| 87 |
+
assert n_val > 0
|
| 88 |
+
random.shuffle(idxs)
|
| 89 |
+
for n, idx in enumerate(idxs):
|
| 90 |
+
item = trainval[idx]
|
| 91 |
+
if n < n_val:
|
| 92 |
+
val.append(item)
|
| 93 |
+
else:
|
| 94 |
+
train.append(item)
|
| 95 |
+
|
| 96 |
+
return train, val
|
| 97 |
+
|
| 98 |
+
@staticmethod
|
| 99 |
+
def save_split(train, val, test, filepath, path_prefix):
|
| 100 |
+
def _extract(items):
|
| 101 |
+
out = []
|
| 102 |
+
for item in items:
|
| 103 |
+
impath = item.impath
|
| 104 |
+
label = item.label
|
| 105 |
+
classname = item.classname
|
| 106 |
+
impath = impath.replace(path_prefix, "")
|
| 107 |
+
if impath.startswith("/"):
|
| 108 |
+
impath = impath[1:]
|
| 109 |
+
out.append((impath, label, classname))
|
| 110 |
+
return out
|
| 111 |
+
|
| 112 |
+
train = _extract(train)
|
| 113 |
+
val = _extract(val)
|
| 114 |
+
test = _extract(test)
|
| 115 |
+
|
| 116 |
+
split = {"train": train, "val": val, "test": test}
|
| 117 |
+
|
| 118 |
+
write_json(split, filepath)
|
| 119 |
+
print(f"Saved split to {filepath}")
|
| 120 |
+
|
| 121 |
+
@staticmethod
|
| 122 |
+
def read_split(filepath, path_prefix):
|
| 123 |
+
def _convert(items):
|
| 124 |
+
out = []
|
| 125 |
+
for impath, label, classname in items:
|
| 126 |
+
impath = os.path.join(path_prefix, impath)
|
| 127 |
+
item = Datum(impath=impath, label=int(label), classname=classname)
|
| 128 |
+
out.append(item)
|
| 129 |
+
return out
|
| 130 |
+
|
| 131 |
+
print(f"Reading split from {filepath}")
|
| 132 |
+
split = read_json(filepath)
|
| 133 |
+
train = _convert(split["train"])
|
| 134 |
+
val = _convert(split["val"])
|
| 135 |
+
test = _convert(split["test"])
|
| 136 |
+
|
| 137 |
+
return train, val, test
|
| 138 |
+
|
| 139 |
+
@staticmethod
|
| 140 |
+
def subsample_classes(*args, subsample="all"):
|
| 141 |
+
assert subsample in ["all", "base", "new"]
|
| 142 |
+
|
| 143 |
+
if subsample == "all":
|
| 144 |
+
return args
|
| 145 |
+
|
| 146 |
+
dataset = args[0]
|
| 147 |
+
labels = set()
|
| 148 |
+
for item in dataset:
|
| 149 |
+
labels.add(item.label)
|
| 150 |
+
labels = list(labels)
|
| 151 |
+
labels.sort()
|
| 152 |
+
n = len(labels)
|
| 153 |
+
m = math.ceil(n / 2)
|
| 154 |
+
|
| 155 |
+
print(f"SUBSAMPLE {subsample.upper()} CLASSES!")
|
| 156 |
+
if subsample == "base":
|
| 157 |
+
selected = labels[:m]
|
| 158 |
+
else:
|
| 159 |
+
selected = labels[m:]
|
| 160 |
+
relabeler = {y: y_new for y_new, y in enumerate(selected)}
|
| 161 |
+
|
| 162 |
+
output = []
|
| 163 |
+
for dataset in args:
|
| 164 |
+
dataset_new = []
|
| 165 |
+
for item in dataset:
|
| 166 |
+
if item.label not in selected:
|
| 167 |
+
continue
|
| 168 |
+
item_new = Datum(
|
| 169 |
+
impath=item.impath,
|
| 170 |
+
label=relabeler[item.label],
|
| 171 |
+
classname=item.classname
|
| 172 |
+
)
|
| 173 |
+
dataset_new.append(item_new)
|
| 174 |
+
output.append(dataset_new)
|
| 175 |
+
|
| 176 |
+
return output
|
MTIL_datasets/pcam.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import random
|
| 4 |
+
from typing import List, Tuple
|
| 5 |
+
|
| 6 |
+
import h5py
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
from .utils import Datum, DatasetBase, mkdir_if_missing, read_json, write_json
|
| 10 |
+
from .oxford_pets import OxfordPets
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
PCAM_CLASSES = [
|
| 14 |
+
'lymph node tissue without metastatic tumor',
|
| 15 |
+
'metastatic tumor in lymph node tissue',
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
# Multiple domain-specific templates for histopathology microscopy images
|
| 19 |
+
PCAM_TEMPLATES = [
|
| 20 |
+
'a microscopy image patch of {}',
|
| 21 |
+
'a histopathology image of {}',
|
| 22 |
+
'a hematoxylin and eosin stained image of {}',
|
| 23 |
+
'a high-resolution histology patch of {}',
|
| 24 |
+
'a digital pathology slide patch of {}',
|
| 25 |
+
'this is a microscopy image of {}',
|
| 26 |
+
'this is a histopathology image of {}',
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class PCam(DatasetBase):
|
| 31 |
+
|
| 32 |
+
dataset_dir = 'pcam'
|
| 33 |
+
|
| 34 |
+
def __init__(self, root, num_shots: int = 0, seed: int = 1, subsample_classes: str = 'all', val_ratio: float = 0.2):
|
| 35 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 36 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 37 |
+
mkdir_if_missing(self.dataset_dir)
|
| 38 |
+
|
| 39 |
+
# HDF5 file paths
|
| 40 |
+
self.train_x_path = os.path.join(self.dataset_dir, 'camelyonpatch_level_2_split_train_x.h5')
|
| 41 |
+
self.train_y_path = os.path.join(self.dataset_dir, 'camelyonpatch_level_2_split_train_y.h5')
|
| 42 |
+
self.test_x_path = os.path.join(self.dataset_dir, 'camelyonpatch_level_2_split_test_x.h5')
|
| 43 |
+
self.test_y_path = os.path.join(self.dataset_dir, 'camelyonpatch_level_2_split_test_y.h5')
|
| 44 |
+
|
| 45 |
+
for p in [self.train_x_path, self.train_y_path, self.test_x_path, self.test_y_path]:
|
| 46 |
+
if not os.path.isfile(p):
|
| 47 |
+
raise FileNotFoundError(f"PCam: expected file not found: {p}")
|
| 48 |
+
|
| 49 |
+
self.split_path = os.path.join(self.dataset_dir, 'split_custom_PCam.json')
|
| 50 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, 'split_fewshot')
|
| 51 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 52 |
+
|
| 53 |
+
random.seed(seed)
|
| 54 |
+
np.random.seed(seed)
|
| 55 |
+
|
| 56 |
+
if os.path.exists(self.split_path):
|
| 57 |
+
train, val, test = self.read_split(self.split_path)
|
| 58 |
+
else:
|
| 59 |
+
trainval = self._read_train(self.train_x_path, self.train_y_path)
|
| 60 |
+
test = self._read_test(self.test_x_path, self.test_y_path)
|
| 61 |
+
train, val = self.split_trainval(trainval, p_val=val_ratio)
|
| 62 |
+
self.save_split(train, val, test, self.split_path)
|
| 63 |
+
|
| 64 |
+
if num_shots >= 1:
|
| 65 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 66 |
+
if os.path.exists(preprocessed):
|
| 67 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 68 |
+
with open(preprocessed, 'rb') as f:
|
| 69 |
+
data = pickle.load(f)
|
| 70 |
+
train, val = data['train'], data['val']
|
| 71 |
+
else:
|
| 72 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 73 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 74 |
+
data = {'train': train, 'val': val}
|
| 75 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 76 |
+
with open(preprocessed, 'wb') as f:
|
| 77 |
+
pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL)
|
| 78 |
+
|
| 79 |
+
# Optional class subsampling (kept for consistency with other datasets)
|
| 80 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample_classes)
|
| 81 |
+
|
| 82 |
+
# Templates per user specification
|
| 83 |
+
self.templates = PCAM_TEMPLATES
|
| 84 |
+
|
| 85 |
+
# Debug: print split sizes and label histograms
|
| 86 |
+
def _hist(items):
|
| 87 |
+
from collections import Counter
|
| 88 |
+
cnt = Counter([it.label for it in items])
|
| 89 |
+
# ensure keys present for both binary classes
|
| 90 |
+
out = {i: int(cnt.get(i, 0)) for i in range(len(PCAM_CLASSES))}
|
| 91 |
+
return out
|
| 92 |
+
|
| 93 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 94 |
+
# Ensure stable binary classification metadata
|
| 95 |
+
self._classnames = PCAM_CLASSES
|
| 96 |
+
self._lab2cname = {i: c for i, c in enumerate(PCAM_CLASSES)}
|
| 97 |
+
self._num_classes = len(PCAM_CLASSES)
|
| 98 |
+
|
| 99 |
+
@staticmethod
|
| 100 |
+
def _first_key(h5_path: str) -> str:
|
| 101 |
+
with h5py.File(h5_path, 'r') as f:
|
| 102 |
+
keys = list(f.keys())
|
| 103 |
+
if not keys:
|
| 104 |
+
raise RuntimeError(f"No datasets found in H5 file: {h5_path}")
|
| 105 |
+
return keys[0]
|
| 106 |
+
|
| 107 |
+
@staticmethod
|
| 108 |
+
def _read_labels(h5_path: str) -> np.ndarray:
|
| 109 |
+
key = PCam._first_key(h5_path)
|
| 110 |
+
with h5py.File(h5_path, 'r') as f:
|
| 111 |
+
y = f[key][...]
|
| 112 |
+
y = np.asarray(y).squeeze()
|
| 113 |
+
y = y.astype(np.int64)
|
| 114 |
+
return y
|
| 115 |
+
|
| 116 |
+
def _read_train(self, x_path: str, y_path: str) -> List[Datum]:
|
| 117 |
+
x_key = self._first_key(x_path)
|
| 118 |
+
y = self._read_labels(y_path)
|
| 119 |
+
items: List[Datum] = []
|
| 120 |
+
for i, label in enumerate(y.tolist()):
|
| 121 |
+
label_i = int(label)
|
| 122 |
+
classname = PCAM_CLASSES[label_i]
|
| 123 |
+
# Lazy image reference: ('h5', abs_h5_path, dataset_key, index)
|
| 124 |
+
abs_path = os.path.abspath(x_path)
|
| 125 |
+
impath = ('h5', abs_path, x_key, i)
|
| 126 |
+
items.append(Datum(impath=impath, label=label_i, classname=classname))
|
| 127 |
+
return items
|
| 128 |
+
|
| 129 |
+
def _read_test(self, x_path: str, y_path: str) -> List[Datum]:
|
| 130 |
+
x_key = self._first_key(x_path)
|
| 131 |
+
y = self._read_labels(y_path)
|
| 132 |
+
items: List[Datum] = []
|
| 133 |
+
for i, label in enumerate(y.tolist()):
|
| 134 |
+
label_i = int(label)
|
| 135 |
+
classname = PCAM_CLASSES[label_i]
|
| 136 |
+
abs_path = os.path.abspath(x_path)
|
| 137 |
+
impath = ('h5', abs_path, x_key, i)
|
| 138 |
+
items.append(Datum(impath=impath, label=label_i, classname=classname))
|
| 139 |
+
return items
|
| 140 |
+
|
| 141 |
+
@staticmethod
|
| 142 |
+
def split_trainval(trainval: List[Datum], p_val: float = 0.2) -> Tuple[List[Datum], List[Datum]]:
|
| 143 |
+
from collections import defaultdict
|
| 144 |
+
p_trn = 1 - p_val
|
| 145 |
+
print(f"Splitting PCam train into {p_trn:.0%} train and {p_val:.0%} val")
|
| 146 |
+
tracker = defaultdict(list)
|
| 147 |
+
for idx, item in enumerate(trainval):
|
| 148 |
+
tracker[item.label].append(idx)
|
| 149 |
+
train, val = [], []
|
| 150 |
+
for _, idxs in tracker.items():
|
| 151 |
+
n_val = max(1, round(len(idxs) * p_val))
|
| 152 |
+
random.shuffle(idxs)
|
| 153 |
+
for n, i in enumerate(idxs):
|
| 154 |
+
if n < n_val:
|
| 155 |
+
val.append(trainval[i])
|
| 156 |
+
else:
|
| 157 |
+
train.append(trainval[i])
|
| 158 |
+
return train, val
|
| 159 |
+
|
| 160 |
+
def save_split(self, train: List[Datum], val: List[Datum], test: List[Datum], filepath: str):
|
| 161 |
+
def _ser(items: List[Datum]):
|
| 162 |
+
out = []
|
| 163 |
+
for it in items:
|
| 164 |
+
impath = it.impath
|
| 165 |
+
if isinstance(impath, tuple) and len(impath) == 4 and impath[0] == 'h5':
|
| 166 |
+
tag, abs_path, key, idx = impath
|
| 167 |
+
# store relative path for portability
|
| 168 |
+
rel = os.path.relpath(abs_path, self.dataset_dir)
|
| 169 |
+
impath_ser = [tag, rel, key, int(idx)]
|
| 170 |
+
else:
|
| 171 |
+
raise ValueError('PCam expects H5 tuple paths')
|
| 172 |
+
out.append((impath_ser, int(it.label), it.classname))
|
| 173 |
+
return out
|
| 174 |
+
split = {
|
| 175 |
+
'train': _ser(train),
|
| 176 |
+
'val': _ser(val),
|
| 177 |
+
'test': _ser(test),
|
| 178 |
+
}
|
| 179 |
+
write_json(split, filepath)
|
| 180 |
+
print(f"Saved PCam split to {filepath}")
|
| 181 |
+
|
| 182 |
+
def read_split(self, filepath: str):
|
| 183 |
+
def _deser(items):
|
| 184 |
+
out = []
|
| 185 |
+
for impath_ser, label, classname in items:
|
| 186 |
+
if isinstance(impath_ser, (list, tuple)) and len(impath_ser) == 4 and impath_ser[0] == 'h5':
|
| 187 |
+
tag, rel, key, idx = impath_ser
|
| 188 |
+
fpath = os.path.join(self.dataset_dir, rel)
|
| 189 |
+
impath = (tag, fpath, key, int(idx))
|
| 190 |
+
else:
|
| 191 |
+
raise ValueError('PCam split contains invalid path entries')
|
| 192 |
+
out.append(Datum(impath=impath, label=int(label), classname=classname))
|
| 193 |
+
return out
|
| 194 |
+
split = read_json(filepath)
|
| 195 |
+
train = _deser(split['train'])
|
| 196 |
+
val = _deser(split['val'])
|
| 197 |
+
test = _deser(split['test'])
|
| 198 |
+
return train, val, test
|
MTIL_datasets/resisc.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import random
|
| 4 |
+
import warnings
|
| 5 |
+
|
| 6 |
+
from .utils import *
|
| 7 |
+
from .oxford_pets import OxfordPets
|
| 8 |
+
|
| 9 |
+
# Canonical class list for RESISC45 (order defines label ids 0..44)
|
| 10 |
+
RESISC_CLASSES = [
|
| 11 |
+
'airplane',
|
| 12 |
+
'airport',
|
| 13 |
+
'baseball diamond',
|
| 14 |
+
'basketball court',
|
| 15 |
+
'beach',
|
| 16 |
+
'bridge',
|
| 17 |
+
'chaparral',
|
| 18 |
+
'church',
|
| 19 |
+
'circular farmland',
|
| 20 |
+
'cloud',
|
| 21 |
+
'commercial area',
|
| 22 |
+
'dense residential',
|
| 23 |
+
'desert',
|
| 24 |
+
'forest',
|
| 25 |
+
'freeway',
|
| 26 |
+
'golf course',
|
| 27 |
+
'ground track field',
|
| 28 |
+
'harbor',
|
| 29 |
+
'industrial area',
|
| 30 |
+
'intersection',
|
| 31 |
+
'island',
|
| 32 |
+
'lake',
|
| 33 |
+
'meadow',
|
| 34 |
+
'medium residential',
|
| 35 |
+
'mobile home park',
|
| 36 |
+
'mountain',
|
| 37 |
+
'overpass',
|
| 38 |
+
'palace',
|
| 39 |
+
'parking lot',
|
| 40 |
+
'railway',
|
| 41 |
+
'railway station',
|
| 42 |
+
'rectangular farmland',
|
| 43 |
+
'river',
|
| 44 |
+
'roundabout',
|
| 45 |
+
'runway',
|
| 46 |
+
'sea ice',
|
| 47 |
+
'ship',
|
| 48 |
+
'snowberg',
|
| 49 |
+
'sparse residential',
|
| 50 |
+
'stadium',
|
| 51 |
+
'storage tank',
|
| 52 |
+
'tennis court',
|
| 53 |
+
'terrace',
|
| 54 |
+
'thermal power station',
|
| 55 |
+
'wetland',
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
# Prompt templates (strings) as provided
|
| 59 |
+
RESISC_TEMPLATES = [
|
| 60 |
+
'satellite imagery of {}.',
|
| 61 |
+
'aerial imagery of {}.',
|
| 62 |
+
'satellite photo of {}.',
|
| 63 |
+
'aerial photo of {}.',
|
| 64 |
+
'satellite view of {}.',
|
| 65 |
+
'aerial view of {}.',
|
| 66 |
+
'satellite imagery of a {}.',
|
| 67 |
+
'aerial imagery of a {}.',
|
| 68 |
+
'satellite photo of a {}.',
|
| 69 |
+
'aerial photo of a {}.',
|
| 70 |
+
'satellite view of a {}.',
|
| 71 |
+
'aerial view of a {}.',
|
| 72 |
+
'satellite imagery of the {}.',
|
| 73 |
+
'aerial imagery of the {}.',
|
| 74 |
+
'satellite photo of the {}.',
|
| 75 |
+
'aerial photo of the {}.',
|
| 76 |
+
'satellite view of the {}.',
|
| 77 |
+
'aerial view of the {}.',
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
RESISC_DEBUG = os.environ.get("RESISC_DEBUG", "0") not in ("0", "false", "False", "")
|
| 81 |
+
|
| 82 |
+
def _dbg(msg: str):
|
| 83 |
+
if RESISC_DEBUG:
|
| 84 |
+
print(f"[RESISC45][DEBUG] {msg}")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _norm_name(s: str) -> str:
|
| 88 |
+
s = s.lower().strip()
|
| 89 |
+
for ch in [" ", "_", "-", "."]:
|
| 90 |
+
s = s.replace(ch, "")
|
| 91 |
+
return s
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class RESISC45(DatasetBase):
|
| 95 |
+
|
| 96 |
+
dataset_dir = "resisc45"
|
| 97 |
+
|
| 98 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 99 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 100 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 101 |
+
self.image_dir = self.dataset_dir # images are under class subfolders directly
|
| 102 |
+
self.split_path = os.path.join(self.dataset_dir, "split_custom_RESISC45.json")
|
| 103 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 104 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 105 |
+
_dbg(f"dataset_dir={self.dataset_dir}")
|
| 106 |
+
|
| 107 |
+
if os.path.exists(self.split_path):
|
| 108 |
+
try:
|
| 109 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.image_dir)
|
| 110 |
+
except Exception as e:
|
| 111 |
+
warnings.warn(f"RESISC45: failed to read split file '{self.split_path}'; rebuilding. Error: {e}")
|
| 112 |
+
train, val, test = self.read_and_split_data(self.image_dir)
|
| 113 |
+
try:
|
| 114 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.image_dir)
|
| 115 |
+
except Exception as e2:
|
| 116 |
+
warnings.warn(f"RESISC45: failed to save rebuilt split to '{self.split_path}': {e2}")
|
| 117 |
+
else:
|
| 118 |
+
train, val, test = self.read_and_split_data(self.image_dir)
|
| 119 |
+
try:
|
| 120 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.image_dir)
|
| 121 |
+
except Exception as e:
|
| 122 |
+
warnings.warn(f"RESISC45: failed to save split to '{self.split_path}': {e}")
|
| 123 |
+
|
| 124 |
+
if num_shots >= 1:
|
| 125 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 126 |
+
if os.path.exists(preprocessed):
|
| 127 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 128 |
+
with open(preprocessed, "rb") as file:
|
| 129 |
+
data = pickle.load(file)
|
| 130 |
+
train, val = data["train"], data["val"]
|
| 131 |
+
else:
|
| 132 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 133 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 134 |
+
data = {"train": train, "val": val}
|
| 135 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 136 |
+
with open(preprocessed, "wb") as file:
|
| 137 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 138 |
+
|
| 139 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample_classes)
|
| 140 |
+
self.templates = RESISC_TEMPLATES
|
| 141 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 142 |
+
|
| 143 |
+
@staticmethod
|
| 144 |
+
def read_and_split_data(image_dir, p_trn=0.5, p_val=0.2):
|
| 145 |
+
try:
|
| 146 |
+
categories = listdir_nohidden(image_dir, sort=True)
|
| 147 |
+
except Exception as e:
|
| 148 |
+
warnings.warn(f"RESISC45: failed to list directory '{image_dir}': {e}")
|
| 149 |
+
raise
|
| 150 |
+
categories = [c for c in categories if os.path.isdir(os.path.join(image_dir, c))]
|
| 151 |
+
cat_norm = {_norm_name(c): c for c in categories}
|
| 152 |
+
|
| 153 |
+
missing = []
|
| 154 |
+
class_to_dir = {}
|
| 155 |
+
for cname in RESISC_CLASSES:
|
| 156 |
+
key = _norm_name(cname)
|
| 157 |
+
if key in cat_norm:
|
| 158 |
+
class_to_dir[cname] = cat_norm[key]
|
| 159 |
+
else:
|
| 160 |
+
missing.append(cname)
|
| 161 |
+
if missing:
|
| 162 |
+
warnings.warn(f"RESISC45: missing class folders for {len(missing)} classes: {missing[:5]}{' ...' if len(missing)>5 else ''}")
|
| 163 |
+
|
| 164 |
+
def _collate(paths, y, cname):
|
| 165 |
+
return [Datum(impath=p, label=y, classname=cname) for p in paths]
|
| 166 |
+
|
| 167 |
+
train, val, test = [], [], []
|
| 168 |
+
for y, cname in enumerate(RESISC_CLASSES):
|
| 169 |
+
if cname not in class_to_dir:
|
| 170 |
+
continue
|
| 171 |
+
cdir = os.path.join(image_dir, class_to_dir[cname])
|
| 172 |
+
try:
|
| 173 |
+
images = listdir_nohidden(cdir, sort=False)
|
| 174 |
+
except Exception as e:
|
| 175 |
+
warnings.warn(f"RESISC45: failed to list class folder '{cdir}': {e}")
|
| 176 |
+
images = []
|
| 177 |
+
images = [os.path.join(cdir, im) for im in images]
|
| 178 |
+
random.shuffle(images)
|
| 179 |
+
n_total = len(images)
|
| 180 |
+
if n_total == 0:
|
| 181 |
+
warnings.warn(f"RESISC45: empty class folder {cdir}; skipping")
|
| 182 |
+
continue
|
| 183 |
+
n_train = round(n_total * p_trn)
|
| 184 |
+
n_val = round(n_total * p_val)
|
| 185 |
+
n_test = n_total - n_train - n_val
|
| 186 |
+
if not (n_train > 0 and n_val > 0 and n_test > 0):
|
| 187 |
+
# Fallback to keep all splits non-empty where possible
|
| 188 |
+
if n_total >= 3:
|
| 189 |
+
n_train, n_val, n_test = 1, 1, n_total - 2
|
| 190 |
+
elif n_total == 2:
|
| 191 |
+
n_train, n_val, n_test = 1, 1, 0
|
| 192 |
+
else: # 1
|
| 193 |
+
n_train, n_val, n_test = 1, 0, 0
|
| 194 |
+
|
| 195 |
+
train.extend(_collate(images[:n_train], y, cname))
|
| 196 |
+
val.extend(_collate(images[n_train:n_train + n_val], y, cname))
|
| 197 |
+
test.extend(_collate(images[n_train + n_val:], y, cname))
|
| 198 |
+
|
| 199 |
+
return train, val, test
|
MTIL_datasets/sst2.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
|
| 4 |
+
from .utils import *
|
| 5 |
+
from .oxford_pets import OxfordPets
|
| 6 |
+
from .dtd import DescribableTextures as DTD
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class SST2(DatasetBase):
|
| 10 |
+
"""
|
| 11 |
+
SST2 (rendered) dataset loader for MTIL.
|
| 12 |
+
|
| 13 |
+
Expected structure (either of the following roots):
|
| 14 |
+
<root>/sst2/
|
| 15 |
+
├─ train/{negative,positive}/*.png
|
| 16 |
+
├─ valid/{negative,positive}/*.png
|
| 17 |
+
└─ test/{negative,positive}/*.png
|
| 18 |
+
|
| 19 |
+
<root>/rendered-sst2/ (alias supported)
|
| 20 |
+
├─ train/{negative,positive}/*.png
|
| 21 |
+
├─ valid/{negative,positive}/*.png
|
| 22 |
+
└─ test/{negative,positive}/*.png
|
| 23 |
+
|
| 24 |
+
Classes: ['negative', 'positive']
|
| 25 |
+
Templates: ['a {} review of a movie.']
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
dataset_dir = "sst2"
|
| 29 |
+
|
| 30 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 31 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 32 |
+
primary_dir = os.path.join(root, self.dataset_dir)
|
| 33 |
+
alt_dir = os.path.join(root, "rendered-sst2")
|
| 34 |
+
|
| 35 |
+
if os.path.isdir(primary_dir):
|
| 36 |
+
self.dataset_dir = primary_dir
|
| 37 |
+
elif os.path.isdir(alt_dir):
|
| 38 |
+
self.dataset_dir = alt_dir
|
| 39 |
+
else:
|
| 40 |
+
raise ValueError(
|
| 41 |
+
"SST2: dataset folder not found. Expected one of: '{}' or '{}'".format(primary_dir, alt_dir)
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 45 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 46 |
+
|
| 47 |
+
train_dir = os.path.join(self.dataset_dir, "train")
|
| 48 |
+
valid_dir = os.path.join(self.dataset_dir, "valid")
|
| 49 |
+
test_dir = os.path.join(self.dataset_dir, "test")
|
| 50 |
+
|
| 51 |
+
use_folder_splits = os.path.isdir(train_dir) and os.path.isdir(valid_dir) and os.path.isdir(test_dir)
|
| 52 |
+
|
| 53 |
+
# fixed class order and validation
|
| 54 |
+
classes = ["negative", "positive"]
|
| 55 |
+
class_to_label = {c: i for i, c in enumerate(classes)}
|
| 56 |
+
|
| 57 |
+
if use_folder_splits:
|
| 58 |
+
train = self._read_split_dir(train_dir, class_to_label)
|
| 59 |
+
val = self._read_split_dir(valid_dir, class_to_label)
|
| 60 |
+
test = self._read_split_dir(test_dir, class_to_label)
|
| 61 |
+
else:
|
| 62 |
+
# Fallbacks consistent with other datasets: look for a JSON split, else naive split
|
| 63 |
+
image_dir = os.path.join(self.dataset_dir, "images")
|
| 64 |
+
self.image_dir = image_dir if os.path.isdir(image_dir) else self.dataset_dir
|
| 65 |
+
split_path = os.path.join(self.dataset_dir, "split_zhou_SST2.json")
|
| 66 |
+
if os.path.exists(split_path):
|
| 67 |
+
train, val, test = OxfordPets.read_split(split_path, self.image_dir)
|
| 68 |
+
else:
|
| 69 |
+
train, val, test = DTD.read_and_split_data(self.image_dir)
|
| 70 |
+
OxfordPets.save_split(train, val, test, split_path, self.image_dir)
|
| 71 |
+
|
| 72 |
+
if num_shots >= 1:
|
| 73 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 74 |
+
if os.path.exists(preprocessed):
|
| 75 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 76 |
+
with open(preprocessed, "rb") as file:
|
| 77 |
+
data = pickle.load(file)
|
| 78 |
+
train, val = data["train"], data["val"]
|
| 79 |
+
else:
|
| 80 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 81 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 82 |
+
data = {"train": train, "val": val}
|
| 83 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 84 |
+
with open(preprocessed, "wb") as file:
|
| 85 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 86 |
+
|
| 87 |
+
subsample = subsample_classes
|
| 88 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample)
|
| 89 |
+
|
| 90 |
+
# Template per user spec
|
| 91 |
+
self.templates = [
|
| 92 |
+
lambda c: f'a {c} review of a movie.',
|
| 93 |
+
]
|
| 94 |
+
|
| 95 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 96 |
+
|
| 97 |
+
def _read_split_dir(self, split_dir, class_to_label):
|
| 98 |
+
items = []
|
| 99 |
+
if not os.path.isdir(split_dir):
|
| 100 |
+
return items
|
| 101 |
+
codes = listdir_nohidden(split_dir)
|
| 102 |
+
# Validate folders: must be subset of expected classes and cover at least one class
|
| 103 |
+
unexpected = sorted([c for c in codes if c not in class_to_label])
|
| 104 |
+
if unexpected:
|
| 105 |
+
raise ValueError(
|
| 106 |
+
f"SST2: Found unexpected class folders in '{split_dir}': {unexpected}. "
|
| 107 |
+
f"Expected only {list(class_to_label.keys())}."
|
| 108 |
+
)
|
| 109 |
+
for cls in codes:
|
| 110 |
+
class_dir = os.path.join(split_dir, cls)
|
| 111 |
+
if not os.path.isdir(class_dir):
|
| 112 |
+
continue
|
| 113 |
+
label = class_to_label.get(cls)
|
| 114 |
+
if label is None:
|
| 115 |
+
raise ValueError(
|
| 116 |
+
f"SST2: Inconsistent label mapping for class '{cls}' in split '{split_dir}'."
|
| 117 |
+
)
|
| 118 |
+
cname = cls
|
| 119 |
+
for fname in listdir_nohidden(class_dir):
|
| 120 |
+
impath = os.path.join(class_dir, fname)
|
| 121 |
+
items.append(Datum(impath=impath, label=label, classname=cname))
|
| 122 |
+
return items
|
MTIL_datasets/stanford_cars.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
from scipy.io import loadmat
|
| 4 |
+
|
| 5 |
+
from .utils import *
|
| 6 |
+
|
| 7 |
+
from .oxford_pets import OxfordPets
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class StanfordCars(DatasetBase):
|
| 11 |
+
|
| 12 |
+
dataset_dir = "stanford_cars"
|
| 13 |
+
|
| 14 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 15 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 16 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 17 |
+
self.split_path = os.path.join(self.dataset_dir, "split_zhou_StanfordCars.json")
|
| 18 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 19 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 20 |
+
|
| 21 |
+
if os.path.exists(self.split_path):
|
| 22 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.dataset_dir)
|
| 23 |
+
else:
|
| 24 |
+
trainval_file = os.path.join(self.dataset_dir, "devkit", "cars_train_annos.mat")
|
| 25 |
+
test_file = os.path.join(self.dataset_dir, "cars_test_annos_withlabels.mat")
|
| 26 |
+
meta_file = os.path.join(self.dataset_dir, "devkit", "cars_meta.mat")
|
| 27 |
+
trainval = self.read_data("cars_train", trainval_file, meta_file)
|
| 28 |
+
test = self.read_data("cars_test", test_file, meta_file)
|
| 29 |
+
train, val = OxfordPets.split_trainval(trainval)
|
| 30 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.dataset_dir)
|
| 31 |
+
|
| 32 |
+
if num_shots >= 1:
|
| 33 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 34 |
+
|
| 35 |
+
if os.path.exists(preprocessed):
|
| 36 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 37 |
+
with open(preprocessed, "rb") as file:
|
| 38 |
+
data = pickle.load(file)
|
| 39 |
+
train, val = data["train"], data["val"]
|
| 40 |
+
else:
|
| 41 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 42 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 43 |
+
data = {"train": train, "val": val}
|
| 44 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 45 |
+
with open(preprocessed, "wb") as file:
|
| 46 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 47 |
+
|
| 48 |
+
subsample = subsample_classes
|
| 49 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample)
|
| 50 |
+
|
| 51 |
+
self.templates = [
|
| 52 |
+
lambda c: f"a photo of a {c}, a type of car.",
|
| 53 |
+
lambda c: f"a photo of a {c}.",
|
| 54 |
+
lambda c: f"a photo of the {c}.",
|
| 55 |
+
lambda c: f"a photo of my {c}.",
|
| 56 |
+
lambda c: f"i love my {c}!",
|
| 57 |
+
lambda c: f"a photo of my dirty {c}.",
|
| 58 |
+
lambda c: f"a photo of my clean {c}.",
|
| 59 |
+
lambda c: f"a photo of my new {c}.",
|
| 60 |
+
lambda c: f"a photo of my old {c}.",
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 64 |
+
|
| 65 |
+
def read_data(self, image_dir, anno_file, meta_file):
|
| 66 |
+
anno_file = loadmat(anno_file)["annotations"][0]
|
| 67 |
+
meta_file = loadmat(meta_file)["class_names"][0]
|
| 68 |
+
items = []
|
| 69 |
+
|
| 70 |
+
for i in range(len(anno_file)):
|
| 71 |
+
imname = anno_file[i]["fname"][0]
|
| 72 |
+
impath = os.path.join(self.dataset_dir, image_dir, imname)
|
| 73 |
+
label = anno_file[i]["class"][0, 0]
|
| 74 |
+
label = int(label) - 1
|
| 75 |
+
classname = meta_file[label][0]
|
| 76 |
+
names = classname.split(" ")
|
| 77 |
+
year = names.pop(-1)
|
| 78 |
+
names.insert(0, year)
|
| 79 |
+
classname = " ".join(names)
|
| 80 |
+
item = Datum(impath=impath, label=label, classname=classname)
|
| 81 |
+
items.append(item)
|
| 82 |
+
|
| 83 |
+
return items
|
MTIL_datasets/stl10.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import math
|
| 3 |
+
import random
|
| 4 |
+
from typing import List, Tuple
|
| 5 |
+
|
| 6 |
+
from .utils import * # Datum, DatasetBase, listdir_nohidden
|
| 7 |
+
from .oxford_pets import OxfordPets
|
| 8 |
+
|
| 9 |
+
STL10_CLASSES: List[str] = [
|
| 10 |
+
'airplane',
|
| 11 |
+
'bird',
|
| 12 |
+
'car',
|
| 13 |
+
'cat',
|
| 14 |
+
'deer',
|
| 15 |
+
'dog',
|
| 16 |
+
'horse',
|
| 17 |
+
'monkey',
|
| 18 |
+
'ship',
|
| 19 |
+
'truck',
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
# keep templates as strings with {}
|
| 23 |
+
STL10_TEMPLATES: List[str] = [
|
| 24 |
+
'a photo of a {}.',
|
| 25 |
+
'a photo of the {}.',
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class STL10(DatasetBase):
|
| 30 |
+
|
| 31 |
+
dataset_dir = "stl10"
|
| 32 |
+
|
| 33 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all', test_ratio: float = 0.2):
|
| 34 |
+
"""
|
| 35 |
+
Expect directory structure:
|
| 36 |
+
{root}/stl10/<class_name>/*.png|jpg|jpeg|bmp|webp
|
| 37 |
+
No official test set provided; we split per-class into train/test.
|
| 38 |
+
Then we further split train into train/val using OxfordPets.split_trainval.
|
| 39 |
+
"""
|
| 40 |
+
rnd = random.Random(seed)
|
| 41 |
+
|
| 42 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 43 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 44 |
+
self.split_path = os.path.join(self.dataset_dir, "split_custom_STL10.json")
|
| 45 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 46 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 47 |
+
|
| 48 |
+
# If we have a saved split, reuse for reproducibility
|
| 49 |
+
if os.path.exists(self.split_path):
|
| 50 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.dataset_dir)
|
| 51 |
+
else:
|
| 52 |
+
trainval, test = self._read_from_folders(self.dataset_dir, rnd=rnd, test_ratio=test_ratio)
|
| 53 |
+
train, val = self._split_trainval_safe(trainval)
|
| 54 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.dataset_dir)
|
| 55 |
+
|
| 56 |
+
if num_shots >= 1:
|
| 57 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 58 |
+
if os.path.exists(preprocessed):
|
| 59 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 60 |
+
import pickle
|
| 61 |
+
with open(preprocessed, "rb") as file:
|
| 62 |
+
data = pickle.load(file)
|
| 63 |
+
train, val = data["train"], data["val"]
|
| 64 |
+
else:
|
| 65 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 66 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 67 |
+
data = {"train": train, "val": val}
|
| 68 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 69 |
+
import pickle
|
| 70 |
+
with open(preprocessed, "wb") as file:
|
| 71 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 72 |
+
|
| 73 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample_classes)
|
| 74 |
+
|
| 75 |
+
self.templates = STL10_TEMPLATES
|
| 76 |
+
|
| 77 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 78 |
+
|
| 79 |
+
def _read_from_folders(self, data_dir: str, rnd: random.Random, test_ratio: float) -> Tuple[List[Datum], List[Datum]]:
|
| 80 |
+
exts = {".png", ".jpg", ".jpeg", ".bmp", ".webp"}
|
| 81 |
+
class_to_label = {name: i for i, name in enumerate(STL10_CLASSES)}
|
| 82 |
+
|
| 83 |
+
items_by_label = {i: [] for i in range(len(STL10_CLASSES))}
|
| 84 |
+
for cname in STL10_CLASSES:
|
| 85 |
+
cdir = os.path.join(data_dir, cname)
|
| 86 |
+
if not os.path.isdir(cdir):
|
| 87 |
+
raise FileNotFoundError(f"Class folder not found: {cdir}")
|
| 88 |
+
for fname in listdir_nohidden(cdir, sort=True):
|
| 89 |
+
fext = os.path.splitext(fname)[1].lower()
|
| 90 |
+
if fext not in exts:
|
| 91 |
+
continue
|
| 92 |
+
impath = os.path.join(cdir, fname)
|
| 93 |
+
label = class_to_label[cname]
|
| 94 |
+
items_by_label[label].append(Datum(impath=impath, label=label, classname=cname))
|
| 95 |
+
|
| 96 |
+
trainval, test = [], []
|
| 97 |
+
for label, items in items_by_label.items():
|
| 98 |
+
if not items:
|
| 99 |
+
continue
|
| 100 |
+
rnd.shuffle(items)
|
| 101 |
+
if len(items) == 1:
|
| 102 |
+
# keep the only sample for training to preserve class presence in train
|
| 103 |
+
trainval.extend(items)
|
| 104 |
+
continue
|
| 105 |
+
n_test = max(1, int(round(len(items) * test_ratio)))
|
| 106 |
+
if n_test >= len(items):
|
| 107 |
+
n_test = len(items) - 1
|
| 108 |
+
test.extend(items[:n_test])
|
| 109 |
+
trainval.extend(items[n_test:])
|
| 110 |
+
|
| 111 |
+
return trainval, test
|
| 112 |
+
|
| 113 |
+
@staticmethod
|
| 114 |
+
def _split_trainval_safe(trainval: List[Datum], p_val: float = 0.2) -> Tuple[List[Datum], List[Datum]]:
|
| 115 |
+
from collections import defaultdict
|
| 116 |
+
tracker = defaultdict(list)
|
| 117 |
+
for idx, item in enumerate(trainval):
|
| 118 |
+
tracker[item.label].append(idx)
|
| 119 |
+
|
| 120 |
+
train, val = [], []
|
| 121 |
+
for label, idxs in tracker.items():
|
| 122 |
+
n = len(idxs)
|
| 123 |
+
if n <= 1:
|
| 124 |
+
# not enough to create a val sample for this class
|
| 125 |
+
for idx in idxs:
|
| 126 |
+
train.append(trainval[idx])
|
| 127 |
+
continue
|
| 128 |
+
n_val = max(1, int(round(n * p_val)))
|
| 129 |
+
if n_val >= n:
|
| 130 |
+
n_val = n - 1
|
| 131 |
+
random.shuffle(idxs)
|
| 132 |
+
for i, idx in enumerate(idxs):
|
| 133 |
+
if i < n_val:
|
| 134 |
+
val.append(trainval[idx])
|
| 135 |
+
else:
|
| 136 |
+
train.append(trainval[idx])
|
| 137 |
+
|
| 138 |
+
# If val ended up empty (degenerate tiny dataset), move one from train to val
|
| 139 |
+
if len(val) == 0 and len(train) > 0:
|
| 140 |
+
val.append(train[-1])
|
| 141 |
+
train = train[:-1]
|
| 142 |
+
|
| 143 |
+
return train, val
|
MTIL_datasets/sun397.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
|
| 4 |
+
from .utils import *
|
| 5 |
+
|
| 6 |
+
from .oxford_pets import OxfordPets
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class SUN397(DatasetBase):
|
| 10 |
+
|
| 11 |
+
dataset_dir = "sun397"
|
| 12 |
+
|
| 13 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 14 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 15 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 16 |
+
self.image_dir = os.path.join(self.dataset_dir, "SUN397")
|
| 17 |
+
self.split_path = os.path.join(self.dataset_dir, "split_zhou_SUN397.json")
|
| 18 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 19 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 20 |
+
|
| 21 |
+
if os.path.exists(self.split_path):
|
| 22 |
+
train, val, test = OxfordPets.read_split(self.split_path, self.image_dir)
|
| 23 |
+
else:
|
| 24 |
+
classnames = []
|
| 25 |
+
with open(os.path.join(self.dataset_dir, "ClassName.txt"), "r") as f:
|
| 26 |
+
lines = f.readlines()
|
| 27 |
+
for line in lines:
|
| 28 |
+
line = line.strip()[1:]
|
| 29 |
+
classnames.append(line)
|
| 30 |
+
cname2lab = {c: i for i, c in enumerate(classnames)}
|
| 31 |
+
trainval = self.read_data(cname2lab, "Training_01.txt")
|
| 32 |
+
test = self.read_data(cname2lab, "Testing_01.txt")
|
| 33 |
+
train, val = OxfordPets.split_trainval(trainval)
|
| 34 |
+
OxfordPets.save_split(train, val, test, self.split_path, self.image_dir)
|
| 35 |
+
|
| 36 |
+
if num_shots >= 1:
|
| 37 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 38 |
+
|
| 39 |
+
if os.path.exists(preprocessed):
|
| 40 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 41 |
+
with open(preprocessed, "rb") as file:
|
| 42 |
+
data = pickle.load(file)
|
| 43 |
+
train, val = data["train"], data["val"]
|
| 44 |
+
else:
|
| 45 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 46 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 47 |
+
data = {"train": train, "val": val}
|
| 48 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 49 |
+
with open(preprocessed, "wb") as file:
|
| 50 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 51 |
+
|
| 52 |
+
subsample = subsample_classes
|
| 53 |
+
train, val, test = OxfordPets.subsample_classes(train, val, test, subsample=subsample)
|
| 54 |
+
|
| 55 |
+
self.templates = [
|
| 56 |
+
lambda c: f"a photo of a {c}.",
|
| 57 |
+
lambda c: f"a photo of the {c}.",
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 61 |
+
|
| 62 |
+
def read_data(self, cname2lab, text_file):
|
| 63 |
+
text_file = os.path.join(self.dataset_dir, text_file)
|
| 64 |
+
items = []
|
| 65 |
+
|
| 66 |
+
with open(text_file, "r") as f:
|
| 67 |
+
lines = f.readlines()
|
| 68 |
+
for line in lines:
|
| 69 |
+
imname = line.strip()[1:]
|
| 70 |
+
classname = os.path.dirname(imname)
|
| 71 |
+
label = cname2lab[classname]
|
| 72 |
+
impath = os.path.join(self.image_dir, imname)
|
| 73 |
+
|
| 74 |
+
names = classname.split("/")[1:]
|
| 75 |
+
names = names[::-1]
|
| 76 |
+
classname = " ".join(names)
|
| 77 |
+
|
| 78 |
+
item = Datum(impath=impath, label=label, classname=classname)
|
| 79 |
+
items.append(item)
|
| 80 |
+
|
| 81 |
+
return items
|
MTIL_datasets/ucf101.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import random
|
| 4 |
+
import math
|
| 5 |
+
from collections import defaultdict
|
| 6 |
+
from typing import List, Tuple
|
| 7 |
+
|
| 8 |
+
from .utils import * # Datum, DatasetBase, mkdir_if_missing, read_json, write_json, listdir_nohidden
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# Canonical class names as provided
|
| 12 |
+
UCF101_CANONICAL: List[str] = [
|
| 13 |
+
'Apply Eye Makeup',
|
| 14 |
+
'Apply Lipstick',
|
| 15 |
+
'Archery',
|
| 16 |
+
'Baby Crawling',
|
| 17 |
+
'Balance Beam',
|
| 18 |
+
'Band Marching',
|
| 19 |
+
'Baseball Pitch',
|
| 20 |
+
'Basketball',
|
| 21 |
+
'Basketball Dunk',
|
| 22 |
+
'Bench Press',
|
| 23 |
+
'Biking',
|
| 24 |
+
'Billiards',
|
| 25 |
+
'Blow Dry Hair',
|
| 26 |
+
'Blowing Candles',
|
| 27 |
+
'Body Weight Squats',
|
| 28 |
+
'Bowling',
|
| 29 |
+
'Boxing Punching Bag',
|
| 30 |
+
'Boxing Speed Bag',
|
| 31 |
+
'Breast Stroke',
|
| 32 |
+
'Brushing Teeth',
|
| 33 |
+
'Clean And Jerk',
|
| 34 |
+
'Cliff Diving',
|
| 35 |
+
'Cricket Bowling',
|
| 36 |
+
'Cricket Shot',
|
| 37 |
+
'Cutting In Kitchen',
|
| 38 |
+
'Diving',
|
| 39 |
+
'Drumming',
|
| 40 |
+
'Fencing',
|
| 41 |
+
'Field Hockey Penalty',
|
| 42 |
+
'Floor Gymnastics',
|
| 43 |
+
'Frisbee Catch',
|
| 44 |
+
'Front Crawl',
|
| 45 |
+
'Golf Swing',
|
| 46 |
+
'Haircut',
|
| 47 |
+
'Hammer Throw',
|
| 48 |
+
'Hammering',
|
| 49 |
+
'Hand Stand Pushups',
|
| 50 |
+
'Handstand Walking',
|
| 51 |
+
'Head Massage',
|
| 52 |
+
'High Jump',
|
| 53 |
+
'Horse Race',
|
| 54 |
+
'Horse Riding',
|
| 55 |
+
'Hula Hoop',
|
| 56 |
+
'Ice Dancing',
|
| 57 |
+
'Javelin Throw',
|
| 58 |
+
'Juggling Balls',
|
| 59 |
+
'Jump Rope',
|
| 60 |
+
'Jumping Jack',
|
| 61 |
+
'Kayaking',
|
| 62 |
+
'Knitting',
|
| 63 |
+
'Long Jump',
|
| 64 |
+
'Lunges',
|
| 65 |
+
'Military Parade',
|
| 66 |
+
'Mixing',
|
| 67 |
+
'Mopping Floor',
|
| 68 |
+
'Nunchucks',
|
| 69 |
+
'Parallel Bars',
|
| 70 |
+
'Pizza Tossing',
|
| 71 |
+
'Playing Cello',
|
| 72 |
+
'Playing Daf',
|
| 73 |
+
'Playing Dhol',
|
| 74 |
+
'Playing Flute',
|
| 75 |
+
'Playing Guitar',
|
| 76 |
+
'Playing Piano',
|
| 77 |
+
'Playing Sitar',
|
| 78 |
+
'Playing Tabla',
|
| 79 |
+
'Playing Violin',
|
| 80 |
+
'Pole Vault',
|
| 81 |
+
'Pommel Horse',
|
| 82 |
+
'Pull Ups',
|
| 83 |
+
'Punch',
|
| 84 |
+
'Push Ups',
|
| 85 |
+
'Rafting',
|
| 86 |
+
'Rock Climbing Indoor',
|
| 87 |
+
'Rope Climbing',
|
| 88 |
+
'Rowing',
|
| 89 |
+
'Salsa Spin',
|
| 90 |
+
'Shaving Beard',
|
| 91 |
+
'Shotput',
|
| 92 |
+
'Skate Boarding',
|
| 93 |
+
'Skiing',
|
| 94 |
+
'Skijet',
|
| 95 |
+
'Sky Diving',
|
| 96 |
+
'Soccer Juggling',
|
| 97 |
+
'Soccer Penalty',
|
| 98 |
+
'Still Rings',
|
| 99 |
+
'Sumo Wrestling',
|
| 100 |
+
'Surfing',
|
| 101 |
+
'Swing',
|
| 102 |
+
'Table Tennis Shot',
|
| 103 |
+
'Tai Chi',
|
| 104 |
+
'Tennis Swing',
|
| 105 |
+
'Throw Discus',
|
| 106 |
+
'Trampoline Jumping',
|
| 107 |
+
'Typing',
|
| 108 |
+
'Uneven Bars',
|
| 109 |
+
'Volleyball Spiking',
|
| 110 |
+
'Walking With Dog',
|
| 111 |
+
'Wall Pushups',
|
| 112 |
+
'Writing On Board',
|
| 113 |
+
'Yo Yo',
|
| 114 |
+
]
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
UCF101_TEMPLATES: List[str] = [
|
| 118 |
+
'a photo of a person {}.',
|
| 119 |
+
'a video of a person {}.',
|
| 120 |
+
'a example of a person {}.',
|
| 121 |
+
'a demonstration of a person {}.',
|
| 122 |
+
'a photo of the person {}.',
|
| 123 |
+
'a video of the person {}.',
|
| 124 |
+
'a example of the person {}.',
|
| 125 |
+
'a demonstration of the person {}.',
|
| 126 |
+
'a photo of a person using {}.',
|
| 127 |
+
'a video of a person using {}.',
|
| 128 |
+
'a example of a person using {}.',
|
| 129 |
+
'a demonstration of a person using {}.',
|
| 130 |
+
'a photo of the person using {}.',
|
| 131 |
+
'a video of the person using {}.',
|
| 132 |
+
'a example of the person using {}.',
|
| 133 |
+
'a demonstration of the person using {}.',
|
| 134 |
+
'a photo of a person doing {}.',
|
| 135 |
+
'a video of a person doing {}.',
|
| 136 |
+
'a example of a person doing {}.',
|
| 137 |
+
'a demonstration of a person doing {}.',
|
| 138 |
+
'a photo of the person doing {}.',
|
| 139 |
+
'a video of the person doing {}.',
|
| 140 |
+
'a example of the person doing {}.',
|
| 141 |
+
'a demonstration of the person doing {}.',
|
| 142 |
+
'a photo of a person during {}.',
|
| 143 |
+
'a video of a person during {}.',
|
| 144 |
+
'a example of a person during {}.',
|
| 145 |
+
'a demonstration of a person during {}.',
|
| 146 |
+
'a photo of the person during {}.',
|
| 147 |
+
'a video of the person during {}.',
|
| 148 |
+
'a example of the person during {}.',
|
| 149 |
+
'a demonstration of the person during {}.',
|
| 150 |
+
'a photo of a person performing {}.',
|
| 151 |
+
'a video of a person performing {}.',
|
| 152 |
+
'a example of a person performing {}.',
|
| 153 |
+
'a demonstration of a person performing {}.',
|
| 154 |
+
'a photo of the person performing {}.',
|
| 155 |
+
'a video of the person performing {}.',
|
| 156 |
+
'a example of the person performing {}.',
|
| 157 |
+
'a demonstration of the person performing {}.',
|
| 158 |
+
'a photo of a person practicing {}.',
|
| 159 |
+
'a video of a person practicing {}.',
|
| 160 |
+
'a example of a person practicing {}.',
|
| 161 |
+
'a demonstration of a person practicing {}.',
|
| 162 |
+
'a photo of the person practicing {}.',
|
| 163 |
+
'a video of the person practicing {}.',
|
| 164 |
+
'a example of the person practicing {}.',
|
| 165 |
+
'a demonstration of the person practicing {}.',
|
| 166 |
+
]
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
class UCF101(DatasetBase):
|
| 170 |
+
"""
|
| 171 |
+
UCF101 midframes classification dataset adapter for MTIL.
|
| 172 |
+
Expected structure:
|
| 173 |
+
root/ucf101/
|
| 174 |
+
UCF-101-midframes/<Class_Folder>/<image>.jpg
|
| 175 |
+
split_zhou_UCF101.json
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
dataset_dir = "ucf101"
|
| 179 |
+
|
| 180 |
+
def __init__(self, root, num_shots=0, seed=1, subsample_classes='all'):
|
| 181 |
+
root = os.path.abspath(os.path.expanduser(root))
|
| 182 |
+
self.dataset_dir = os.path.join(root, self.dataset_dir)
|
| 183 |
+
self.image_dir = os.path.join(self.dataset_dir, "UCF-101-midframes")
|
| 184 |
+
self.split_path = os.path.join(self.dataset_dir, "split_zhou_UCF101.json")
|
| 185 |
+
self.split_fewshot_dir = os.path.join(self.dataset_dir, "split_fewshot")
|
| 186 |
+
mkdir_if_missing(self.split_fewshot_dir)
|
| 187 |
+
|
| 188 |
+
if os.path.exists(self.split_path):
|
| 189 |
+
train, val, test = self.read_split(self.split_path, self.image_dir)
|
| 190 |
+
else:
|
| 191 |
+
# Fallback: build from directory and split train/val; use val as test too.
|
| 192 |
+
trainval = self._read_from_dir()
|
| 193 |
+
train, val = self.split_trainval(trainval)
|
| 194 |
+
test = list(val)
|
| 195 |
+
self.save_split(train, val, test, self.split_path, self.image_dir)
|
| 196 |
+
|
| 197 |
+
if num_shots >= 1:
|
| 198 |
+
preprocessed = os.path.join(self.split_fewshot_dir, f"shot_{num_shots}-seed_{seed}.pkl")
|
| 199 |
+
if os.path.exists(preprocessed):
|
| 200 |
+
print(f"Loading preprocessed few-shot data from {preprocessed}")
|
| 201 |
+
with open(preprocessed, "rb") as file:
|
| 202 |
+
data = pickle.load(file)
|
| 203 |
+
train, val = data["train"], data["val"]
|
| 204 |
+
else:
|
| 205 |
+
train = self.generate_fewshot_dataset(train, num_shots=num_shots)
|
| 206 |
+
val = self.generate_fewshot_dataset(val, num_shots=min(num_shots, 4))
|
| 207 |
+
data = {"train": train, "val": val}
|
| 208 |
+
print(f"Saving preprocessed few-shot data to {preprocessed}")
|
| 209 |
+
with open(preprocessed, "wb") as file:
|
| 210 |
+
pickle.dump(data, file, protocol=pickle.HIGHEST_PROTOCOL)
|
| 211 |
+
|
| 212 |
+
# Optionally subsample classes (keep interface consistent)
|
| 213 |
+
train, val, test = self.subsample_classes(train, val, test, subsample=subsample_classes)
|
| 214 |
+
|
| 215 |
+
self.templates = UCF101_TEMPLATES
|
| 216 |
+
|
| 217 |
+
super().__init__(train_x=train, val=val, test=test)
|
| 218 |
+
|
| 219 |
+
@staticmethod
|
| 220 |
+
def subsample_classes(*args, subsample="all"):
|
| 221 |
+
assert subsample in ["all", "base", "new"]
|
| 222 |
+
|
| 223 |
+
if subsample == "all":
|
| 224 |
+
return args
|
| 225 |
+
|
| 226 |
+
dataset = args[0]
|
| 227 |
+
labels = set()
|
| 228 |
+
for item in dataset:
|
| 229 |
+
labels.add(item.label)
|
| 230 |
+
labels = list(labels)
|
| 231 |
+
labels.sort()
|
| 232 |
+
n = len(labels)
|
| 233 |
+
m = math.ceil(n / 2)
|
| 234 |
+
|
| 235 |
+
print(f"SUBSAMPLE {subsample.upper()} CLASSES!")
|
| 236 |
+
if subsample == "base":
|
| 237 |
+
selected = labels[:m]
|
| 238 |
+
else:
|
| 239 |
+
selected = labels[m:]
|
| 240 |
+
relabeler = {y: y_new for y_new, y in enumerate(selected)}
|
| 241 |
+
|
| 242 |
+
output = []
|
| 243 |
+
for dataset in args:
|
| 244 |
+
dataset_new = []
|
| 245 |
+
for item in dataset:
|
| 246 |
+
if item.label not in selected:
|
| 247 |
+
continue
|
| 248 |
+
item_new = Datum(
|
| 249 |
+
impath=item.impath,
|
| 250 |
+
label=relabeler[item.label],
|
| 251 |
+
classname=item.classname
|
| 252 |
+
)
|
| 253 |
+
dataset_new.append(item_new)
|
| 254 |
+
output.append(dataset_new)
|
| 255 |
+
|
| 256 |
+
return output
|
| 257 |
+
|
| 258 |
+
# ---- IO helpers (compatible with OxfordPets) ----
|
| 259 |
+
@staticmethod
|
| 260 |
+
def save_split(train, val, test, filepath, path_prefix):
|
| 261 |
+
def _extract(items):
|
| 262 |
+
out = []
|
| 263 |
+
for item in items:
|
| 264 |
+
impath = item.impath
|
| 265 |
+
label = item.label
|
| 266 |
+
classname = item.classname
|
| 267 |
+
impath = impath.replace(path_prefix, "")
|
| 268 |
+
if impath.startswith("/"):
|
| 269 |
+
impath = impath[1:]
|
| 270 |
+
out.append((impath, label, classname))
|
| 271 |
+
return out
|
| 272 |
+
|
| 273 |
+
train = _extract(train)
|
| 274 |
+
val = _extract(val)
|
| 275 |
+
test = _extract(test)
|
| 276 |
+
split = {"train": train, "val": val, "test": test}
|
| 277 |
+
write_json(split, filepath)
|
| 278 |
+
print(f"Saved split to {filepath}")
|
| 279 |
+
|
| 280 |
+
@staticmethod
|
| 281 |
+
def read_split(filepath, path_prefix):
|
| 282 |
+
def _convert(items):
|
| 283 |
+
out = []
|
| 284 |
+
for impath, label, classname in items:
|
| 285 |
+
impath = os.path.join(path_prefix, impath)
|
| 286 |
+
item = Datum(impath=impath, label=int(label), classname=classname)
|
| 287 |
+
out.append(item)
|
| 288 |
+
return out
|
| 289 |
+
|
| 290 |
+
print(f"Reading split from {filepath}")
|
| 291 |
+
split = read_json(filepath)
|
| 292 |
+
train = _convert(split["train"])
|
| 293 |
+
val = _convert(split["val"])
|
| 294 |
+
test = _convert(split["test"])
|
| 295 |
+
return train, val, test
|
| 296 |
+
|
| 297 |
+
@staticmethod
|
| 298 |
+
def split_trainval(trainval: List[Datum], p_val=0.2) -> Tuple[List[Datum], List[Datum]]:
|
| 299 |
+
p_trn = 1 - p_val
|
| 300 |
+
tracker = defaultdict(list)
|
| 301 |
+
for idx, item in enumerate(trainval):
|
| 302 |
+
tracker[item.label].append(idx)
|
| 303 |
+
train, val = [], []
|
| 304 |
+
for label, idxs in tracker.items():
|
| 305 |
+
n_val = max(1, round(len(idxs) * p_val))
|
| 306 |
+
random.shuffle(idxs)
|
| 307 |
+
for n, idx in enumerate(idxs):
|
| 308 |
+
item = trainval[idx]
|
| 309 |
+
if n < n_val:
|
| 310 |
+
val.append(item)
|
| 311 |
+
else:
|
| 312 |
+
train.append(item)
|
| 313 |
+
return train, val
|
| 314 |
+
|
| 315 |
+
# ---- Directory reader (fallback if JSON is missing) ----
|
| 316 |
+
def _read_from_dir(self) -> List[Datum]:
|
| 317 |
+
if not os.path.isdir(self.image_dir):
|
| 318 |
+
raise FileNotFoundError(f"Image directory not found: {self.image_dir}")
|
| 319 |
+
|
| 320 |
+
# Build canonical key mapping (lowercased, remove spaces/underscores)
|
| 321 |
+
def canon_key(s: str) -> str:
|
| 322 |
+
return ''.join(ch for ch in s.lower() if ch.isalnum())
|
| 323 |
+
|
| 324 |
+
canonical_to_label = {name: idx for idx, name in enumerate(UCF101_CANONICAL)}
|
| 325 |
+
key_to_canonical = {canon_key(name): name for name in UCF101_CANONICAL}
|
| 326 |
+
|
| 327 |
+
items: List[Datum] = []
|
| 328 |
+
class_dirs = listdir_nohidden(self.image_dir, sort=True)
|
| 329 |
+
for cls_dir in class_dirs:
|
| 330 |
+
cls_path = os.path.join(self.image_dir, cls_dir)
|
| 331 |
+
if not os.path.isdir(cls_path):
|
| 332 |
+
continue
|
| 333 |
+
# Try to map folder name to canonical class
|
| 334 |
+
k = canon_key(cls_dir.replace('_', ' '))
|
| 335 |
+
cname = key_to_canonical.get(k, None)
|
| 336 |
+
if cname is None:
|
| 337 |
+
# Try removing underscores without space
|
| 338 |
+
k2 = canon_key(cls_dir.replace('_', ''))
|
| 339 |
+
cname = key_to_canonical.get(k2, None)
|
| 340 |
+
if cname is None:
|
| 341 |
+
# As a last resort, use the folder name with underscores replaced
|
| 342 |
+
cname = cls_dir.replace('_', ' ').strip()
|
| 343 |
+
if cname not in canonical_to_label:
|
| 344 |
+
# Unknown class; skip
|
| 345 |
+
continue
|
| 346 |
+
label = canonical_to_label[cname]
|
| 347 |
+
# Collect images
|
| 348 |
+
for vid in listdir_nohidden(cls_path, sort=False):
|
| 349 |
+
vpath = os.path.join(cls_path, vid)
|
| 350 |
+
if os.path.isdir(vpath):
|
| 351 |
+
# some datasets might nest frames under video folder; include all frames
|
| 352 |
+
for frame in listdir_nohidden(vpath, sort=False):
|
| 353 |
+
impath = os.path.join(vpath, frame)
|
| 354 |
+
if os.path.isfile(impath):
|
| 355 |
+
items.append(Datum(impath=impath, label=label, classname=cname))
|
| 356 |
+
else:
|
| 357 |
+
# direct frames under class folder
|
| 358 |
+
if os.path.isfile(vpath):
|
| 359 |
+
items.append(Datum(impath=vpath, label=label, classname=cname))
|
| 360 |
+
return items
|
MTIL_datasets/utils.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import random
|
| 3 |
+
import os.path as osp
|
| 4 |
+
import tarfile
|
| 5 |
+
import zipfile
|
| 6 |
+
from collections import defaultdict
|
| 7 |
+
import gdown
|
| 8 |
+
import errno
|
| 9 |
+
import warnings
|
| 10 |
+
import json
|
| 11 |
+
import numpy as np
|
| 12 |
+
import h5py
|
| 13 |
+
from torch.utils.data import Dataset as TorchDataset
|
| 14 |
+
from PIL import Image
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class Datum:
|
| 18 |
+
def __init__(self, impath="", label=0, domain=0, classname=""):
|
| 19 |
+
self._impath = impath
|
| 20 |
+
self._label = label
|
| 21 |
+
self._domain = domain
|
| 22 |
+
self._classname = classname
|
| 23 |
+
|
| 24 |
+
@property
|
| 25 |
+
def impath(self):
|
| 26 |
+
return self._impath
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
def label(self):
|
| 30 |
+
return self._label
|
| 31 |
+
|
| 32 |
+
@property
|
| 33 |
+
def domain(self):
|
| 34 |
+
return self._domain
|
| 35 |
+
|
| 36 |
+
@property
|
| 37 |
+
def classname(self):
|
| 38 |
+
return self._classname
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class DatasetBase:
|
| 42 |
+
dataset_dir = ""
|
| 43 |
+
domains = []
|
| 44 |
+
|
| 45 |
+
def __init__(self, train_x=None, train_u=None, val=None, test=None):
|
| 46 |
+
self._train_x = train_x
|
| 47 |
+
self._train_u = train_u
|
| 48 |
+
self._val = val
|
| 49 |
+
self._test = test
|
| 50 |
+
self._num_classes = self.get_num_classes(train_x)
|
| 51 |
+
self._lab2cname, self._classnames = self.get_lab2cname(train_x)
|
| 52 |
+
|
| 53 |
+
@property
|
| 54 |
+
def train_x(self):
|
| 55 |
+
return self._train_x
|
| 56 |
+
|
| 57 |
+
@property
|
| 58 |
+
def train_u(self):
|
| 59 |
+
return self._train_u
|
| 60 |
+
|
| 61 |
+
@property
|
| 62 |
+
def val(self):
|
| 63 |
+
return self._val
|
| 64 |
+
|
| 65 |
+
@property
|
| 66 |
+
def test(self):
|
| 67 |
+
return self._test
|
| 68 |
+
|
| 69 |
+
@property
|
| 70 |
+
def lab2cname(self):
|
| 71 |
+
return self._lab2cname
|
| 72 |
+
|
| 73 |
+
@property
|
| 74 |
+
def classnames(self):
|
| 75 |
+
return self._classnames
|
| 76 |
+
|
| 77 |
+
@property
|
| 78 |
+
def num_classes(self):
|
| 79 |
+
return self._num_classes
|
| 80 |
+
|
| 81 |
+
@staticmethod
|
| 82 |
+
def get_num_classes(data_source):
|
| 83 |
+
label_set = set()
|
| 84 |
+
for item in data_source:
|
| 85 |
+
label_set.add(item.label)
|
| 86 |
+
return max(label_set) + 1
|
| 87 |
+
|
| 88 |
+
@staticmethod
|
| 89 |
+
def get_lab2cname(data_source):
|
| 90 |
+
container = set()
|
| 91 |
+
for item in data_source:
|
| 92 |
+
container.add((item.label, item.classname))
|
| 93 |
+
mapping = {label: classname for label, classname in container}
|
| 94 |
+
labels = list(mapping.keys())
|
| 95 |
+
labels.sort()
|
| 96 |
+
classnames = [mapping[label] for label in labels]
|
| 97 |
+
return mapping, classnames
|
| 98 |
+
|
| 99 |
+
@property
|
| 100 |
+
def template(self):
|
| 101 |
+
return self.templates[0]
|
| 102 |
+
|
| 103 |
+
def check_input_domains(self, source_domains, target_domains):
|
| 104 |
+
assert len(source_domains) > 0, "source_domains (list) is empty"
|
| 105 |
+
assert len(target_domains) > 0, "target_domains (list) is empty"
|
| 106 |
+
self.is_input_domain_valid(source_domains)
|
| 107 |
+
self.is_input_domain_valid(target_domains)
|
| 108 |
+
|
| 109 |
+
def is_input_domain_valid(self, input_domains):
|
| 110 |
+
for domain in input_domains:
|
| 111 |
+
if domain not in self.domains:
|
| 112 |
+
raise ValueError(
|
| 113 |
+
"Input domain must belong to {}, "
|
| 114 |
+
"but got [{}]".format(self.domains, domain)
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
def download_data(self, url, dst, from_gdrive=True):
|
| 118 |
+
if not osp.exists(osp.dirname(dst)):
|
| 119 |
+
os.makedirs(osp.dirname(dst))
|
| 120 |
+
|
| 121 |
+
if from_gdrive:
|
| 122 |
+
gdown.download(url, dst, quiet=False)
|
| 123 |
+
else:
|
| 124 |
+
raise NotImplementedError
|
| 125 |
+
|
| 126 |
+
print("Extracting file ...")
|
| 127 |
+
|
| 128 |
+
if dst.endswith(".zip"):
|
| 129 |
+
zip_ref = zipfile.ZipFile(dst, "r")
|
| 130 |
+
zip_ref.extractall(osp.dirname(dst))
|
| 131 |
+
zip_ref.close()
|
| 132 |
+
|
| 133 |
+
elif dst.endswith(".tar"):
|
| 134 |
+
tar = tarfile.open(dst, "r:")
|
| 135 |
+
tar.extractall(osp.dirname(dst))
|
| 136 |
+
tar.close()
|
| 137 |
+
|
| 138 |
+
elif dst.endswith(".tar.gz"):
|
| 139 |
+
tar = tarfile.open(dst, "r:gz")
|
| 140 |
+
tar.extractall(osp.dirname(dst))
|
| 141 |
+
tar.close()
|
| 142 |
+
|
| 143 |
+
else:
|
| 144 |
+
raise NotImplementedError
|
| 145 |
+
|
| 146 |
+
print("File extracted to {}".format(osp.dirname(dst)))
|
| 147 |
+
|
| 148 |
+
def generate_fewshot_dataset(
|
| 149 |
+
self, *data_sources, num_shots=-1, repeat=False
|
| 150 |
+
):
|
| 151 |
+
if num_shots < 1:
|
| 152 |
+
if len(data_sources) == 1:
|
| 153 |
+
return data_sources[0]
|
| 154 |
+
return data_sources
|
| 155 |
+
|
| 156 |
+
print(f"Creating a {num_shots}-shot dataset")
|
| 157 |
+
|
| 158 |
+
output = []
|
| 159 |
+
|
| 160 |
+
for data_source in data_sources:
|
| 161 |
+
tracker = self.split_dataset_by_label(data_source)
|
| 162 |
+
dataset = []
|
| 163 |
+
|
| 164 |
+
for label, items in tracker.items():
|
| 165 |
+
if len(items) >= num_shots:
|
| 166 |
+
sampled_items = random.sample(items, num_shots)
|
| 167 |
+
else:
|
| 168 |
+
if repeat:
|
| 169 |
+
sampled_items = random.choices(items, k=num_shots)
|
| 170 |
+
else:
|
| 171 |
+
sampled_items = items
|
| 172 |
+
dataset.extend(sampled_items)
|
| 173 |
+
|
| 174 |
+
output.append(dataset)
|
| 175 |
+
|
| 176 |
+
if len(output) == 1:
|
| 177 |
+
return output[0]
|
| 178 |
+
|
| 179 |
+
return output
|
| 180 |
+
|
| 181 |
+
def split_dataset_by_label(self, data_source):
|
| 182 |
+
output = defaultdict(list)
|
| 183 |
+
|
| 184 |
+
for item in data_source:
|
| 185 |
+
output[item.label].append(item)
|
| 186 |
+
|
| 187 |
+
return output
|
| 188 |
+
|
| 189 |
+
def split_dataset_by_domain(self, data_source):
|
| 190 |
+
output = defaultdict(list)
|
| 191 |
+
|
| 192 |
+
for item in data_source:
|
| 193 |
+
output[item.domain].append(item)
|
| 194 |
+
|
| 195 |
+
return output
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class DatasetWrapper(TorchDataset):
|
| 199 |
+
|
| 200 |
+
def __init__(self, data_source, transform=None, is_train=False):
|
| 201 |
+
self.data_source = data_source
|
| 202 |
+
self.transform = transform
|
| 203 |
+
self.is_train = is_train
|
| 204 |
+
self._h5_cache = {}
|
| 205 |
+
self._h5_info_printed = set()
|
| 206 |
+
|
| 207 |
+
def __len__(self):
|
| 208 |
+
return len(self.data_source)
|
| 209 |
+
|
| 210 |
+
def __getitem__(self, idx):
|
| 211 |
+
item = self.data_source[idx]
|
| 212 |
+
|
| 213 |
+
impath = item.impath
|
| 214 |
+
if isinstance(impath, str):
|
| 215 |
+
img0 = Image.open(impath).convert("RGB")
|
| 216 |
+
elif isinstance(impath, tuple) and len(impath) == 4 and impath[0] == 'h5':
|
| 217 |
+
_, fpath, key, index = impath
|
| 218 |
+
f = self._h5_cache.get(fpath)
|
| 219 |
+
if f is None:
|
| 220 |
+
f = h5py.File(fpath, 'r')
|
| 221 |
+
self._h5_cache[fpath] = f
|
| 222 |
+
# Print file info once
|
| 223 |
+
try:
|
| 224 |
+
keys = list(f.keys())
|
| 225 |
+
print(f"H5 open: {os.path.basename(fpath)} keys={keys[:5]}{'...' if len(keys) > 5 else ''}")
|
| 226 |
+
except Exception as e:
|
| 227 |
+
print(f"H5 open (keys) failed for {fpath}: {e}")
|
| 228 |
+
# Print dataset info per file the first time we see this path
|
| 229 |
+
if fpath not in self._h5_info_printed:
|
| 230 |
+
try:
|
| 231 |
+
ds = f[key]
|
| 232 |
+
print(f"H5 dataset: {os.path.basename(fpath)}[{key}] shape={getattr(ds, 'shape', '?')} dtype={getattr(ds, 'dtype', '?')}")
|
| 233 |
+
except Exception as e:
|
| 234 |
+
print(f"H5 dataset info failed for {fpath}[{key}]: {e}")
|
| 235 |
+
self._h5_info_printed.add(fpath)
|
| 236 |
+
arr = f[key][int(index)]
|
| 237 |
+
arr = np.asarray(arr)
|
| 238 |
+
# Convert CHW -> HWC if needed
|
| 239 |
+
if arr.ndim == 3 and arr.shape[0] in (1, 3) and arr.shape[-1] not in (1, 3):
|
| 240 |
+
arr = np.transpose(arr, (1, 2, 0))
|
| 241 |
+
# Ensure HWC and uint8
|
| 242 |
+
if arr.ndim == 3 and arr.shape[-1] in (1, 3):
|
| 243 |
+
pass
|
| 244 |
+
else:
|
| 245 |
+
raise ValueError(f"Unexpected H5 image shape: {arr.shape}")
|
| 246 |
+
if arr.dtype != np.uint8:
|
| 247 |
+
arr = arr.astype(np.uint8)
|
| 248 |
+
if arr.shape[-1] == 1:
|
| 249 |
+
img0 = Image.fromarray(arr.squeeze(-1), mode='L').convert('RGB')
|
| 250 |
+
else:
|
| 251 |
+
img0 = Image.fromarray(arr, mode='RGB')
|
| 252 |
+
else:
|
| 253 |
+
# Fallback: if already PIL Image
|
| 254 |
+
if isinstance(impath, Image.Image):
|
| 255 |
+
img0 = impath
|
| 256 |
+
else:
|
| 257 |
+
raise ValueError("Unsupported impath type in DatasetWrapper")
|
| 258 |
+
|
| 259 |
+
if self.transform:
|
| 260 |
+
img = self.transform(img0)
|
| 261 |
+
else:
|
| 262 |
+
img = img0
|
| 263 |
+
|
| 264 |
+
return img, item.label
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def check_isfile(fpath):
|
| 268 |
+
isfile = osp.isfile(fpath)
|
| 269 |
+
if not isfile:
|
| 270 |
+
warnings.warn('No file found at "{}"'.format(fpath))
|
| 271 |
+
return isfile
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def mkdir_if_missing(dirname):
|
| 275 |
+
if not osp.exists(dirname):
|
| 276 |
+
try:
|
| 277 |
+
os.makedirs(dirname)
|
| 278 |
+
except OSError as e:
|
| 279 |
+
if e.errno != errno.EEXIST:
|
| 280 |
+
raise
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def listdir_nohidden(path, sort=False):
|
| 284 |
+
items = [f for f in os.listdir(path) if not f.startswith(".")]
|
| 285 |
+
if sort:
|
| 286 |
+
items.sort()
|
| 287 |
+
return items
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def read_json(fpath):
|
| 291 |
+
with open(fpath, "r") as f:
|
| 292 |
+
obj = json.load(f)
|
| 293 |
+
return obj
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
def write_json(obj, fpath):
|
| 297 |
+
mkdir_if_missing(osp.dirname(fpath))
|
| 298 |
+
with open(fpath, "w") as f:
|
| 299 |
+
json.dump(obj, f, indent=4, separators=(",", ": "))
|
MTIL_datasets/voc2007.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import os.path as osp
|
| 3 |
+
from typing import List, Dict, Tuple
|
| 4 |
+
from collections import defaultdict
|
| 5 |
+
|
| 6 |
+
from .utils import Datum, DatasetBase, listdir_nohidden
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
VOC2007_CLASSES = [
|
| 10 |
+
'aeroplane', 'bicycle', 'bird', 'boat', 'bottle',
|
| 11 |
+
'bus', 'car', 'cat', 'chair', 'cow',
|
| 12 |
+
'dog', 'horse', 'motorbike', 'person', 'sheep',
|
| 13 |
+
'sofa', 'diningtable', 'pottedplant', 'train', 'tvmonitor',
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
VOC2007_TEMPLATES = [
|
| 17 |
+
'a photo of a {}.',
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class VOC2007(DatasetBase):
|
| 22 |
+
"""
|
| 23 |
+
VOC2007 multi-label classification dataset adapter for MTIL evaluation.
|
| 24 |
+
|
| 25 |
+
Expects directory structure:
|
| 26 |
+
data/VOC2007/
|
| 27 |
+
JPEGImages/
|
| 28 |
+
Main/
|
| 29 |
+
<class>_trainval.txt
|
| 30 |
+
<class>_test.txt
|
| 31 |
+
|
| 32 |
+
Each *_split.txt has lines: "<image_id> <label>", where label in {1, 0, -1}.
|
| 33 |
+
We treat 1 as positive and others as negative for that class.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
dataset_dir = 'VOC2007'
|
| 37 |
+
|
| 38 |
+
def __init__(self, root: str, seed: int = 32, single_label: bool = False):
|
| 39 |
+
self.root = os.path.expanduser(root)
|
| 40 |
+
self.dataset_dir = osp.join(self.root, self.dataset_dir)
|
| 41 |
+
self.images_dir = osp.join(self.dataset_dir, 'JPEGImages')
|
| 42 |
+
self.main_dir = osp.join(self.dataset_dir, 'Main')
|
| 43 |
+
|
| 44 |
+
classes = VOC2007_CLASSES
|
| 45 |
+
class_to_label = {c: i for i, c in enumerate(classes)}
|
| 46 |
+
|
| 47 |
+
if not single_label:
|
| 48 |
+
# Multi-label mode (for Zero-Shot evaluation)
|
| 49 |
+
test_list = self._read_split('test', classes)
|
| 50 |
+
test = self._build_data_list(test_list, class_to_label)
|
| 51 |
+
|
| 52 |
+
# Create a meta train set with one sample per class (integer labels) for proper initialization
|
| 53 |
+
# Try to point to a real image containing that class as positive; fallback to any available image or empty path
|
| 54 |
+
meta_train: List[Datum] = []
|
| 55 |
+
for ci, cname in enumerate(classes):
|
| 56 |
+
impath = None
|
| 57 |
+
for d in test:
|
| 58 |
+
try:
|
| 59 |
+
if isinstance(d.label, (list, tuple)) and ci < len(d.label) and int(d.label[ci]) == 1:
|
| 60 |
+
impath = d.impath
|
| 61 |
+
break
|
| 62 |
+
except Exception:
|
| 63 |
+
continue
|
| 64 |
+
if impath is None:
|
| 65 |
+
impath = test[0].impath if len(test) > 0 else ''
|
| 66 |
+
meta_train.append(Datum(impath=impath, label=ci, classname=cname))
|
| 67 |
+
|
| 68 |
+
train_x: List[Datum] = meta_train
|
| 69 |
+
val: List[Datum] = []
|
| 70 |
+
self.templates = VOC2007_TEMPLATES
|
| 71 |
+
super().__init__(train_x=train_x, val=val, test=test)
|
| 72 |
+
else:
|
| 73 |
+
# Single-label mode (for CIL downstream training/evaluation)
|
| 74 |
+
# Build trainval and test splits as single-label datasets by selecting the first positive class per image
|
| 75 |
+
trainval_multi = self._read_split('trainval', classes)
|
| 76 |
+
test_multi = self._read_split('test', classes)
|
| 77 |
+
train_x = self._build_single_label_list(trainval_multi, classes)
|
| 78 |
+
val = []
|
| 79 |
+
test = self._build_single_label_list(test_multi, classes)
|
| 80 |
+
self.templates = VOC2007_TEMPLATES
|
| 81 |
+
super().__init__(train_x=train_x, val=val, test=test)
|
| 82 |
+
|
| 83 |
+
def _read_split(self, split: str, classes: List[str]) -> Dict[str, List[int]]:
|
| 84 |
+
"""Return mapping: image_id -> multi-hot list for the given split."""
|
| 85 |
+
# Collect image ids present in this split
|
| 86 |
+
img_ids = set()
|
| 87 |
+
by_class_labels: Dict[str, Dict[str, int]] = {}
|
| 88 |
+
for cname in classes:
|
| 89 |
+
split_file = osp.join(self.main_dir, f"{cname}_{split}.txt")
|
| 90 |
+
if not osp.isfile(split_file):
|
| 91 |
+
# Try alternate common path name
|
| 92 |
+
split_file = osp.join(self.dataset_dir, 'ImageSets', 'Main', f"{cname}_{split}.txt")
|
| 93 |
+
if not osp.isfile(split_file):
|
| 94 |
+
# If missing, skip this class
|
| 95 |
+
continue
|
| 96 |
+
class_map: Dict[str, int] = {}
|
| 97 |
+
with open(split_file, 'r') as f:
|
| 98 |
+
for line in f:
|
| 99 |
+
parts = line.strip().split()
|
| 100 |
+
if len(parts) < 2:
|
| 101 |
+
continue
|
| 102 |
+
img_id, label_str = parts[0], parts[1]
|
| 103 |
+
try:
|
| 104 |
+
label = int(label_str)
|
| 105 |
+
except Exception:
|
| 106 |
+
continue
|
| 107 |
+
class_map[img_id] = 1 if label == 1 else 0
|
| 108 |
+
img_ids.add(img_id)
|
| 109 |
+
by_class_labels[cname] = class_map
|
| 110 |
+
|
| 111 |
+
# Aggregate into multi-hot vectors
|
| 112 |
+
result: Dict[str, List[int]] = {}
|
| 113 |
+
for img_id in img_ids:
|
| 114 |
+
vec = [0] * len(classes)
|
| 115 |
+
for ci, cname in enumerate(classes):
|
| 116 |
+
cmap = by_class_labels.get(cname, {})
|
| 117 |
+
vec[ci] = int(cmap.get(img_id, 0))
|
| 118 |
+
result[img_id] = vec
|
| 119 |
+
return result
|
| 120 |
+
|
| 121 |
+
def _build_data_list(self, id_to_vec: Dict[str, List[int]], class_to_label: Dict[str, int]) -> List[Datum]:
|
| 122 |
+
data: List[Datum] = []
|
| 123 |
+
for img_id, vec in id_to_vec.items():
|
| 124 |
+
# Try common image extensions
|
| 125 |
+
impath = None
|
| 126 |
+
for ext in ['.jpg', '.jpeg', '.png']:
|
| 127 |
+
p = osp.join(self.images_dir, img_id + ext)
|
| 128 |
+
if osp.isfile(p):
|
| 129 |
+
impath = p
|
| 130 |
+
break
|
| 131 |
+
if impath is None:
|
| 132 |
+
# As a fallback, if there's exactly one file starting with img_id
|
| 133 |
+
try:
|
| 134 |
+
candidates = [f for f in listdir_nohidden(self.images_dir) if f.startswith(img_id + '.')]
|
| 135 |
+
if candidates:
|
| 136 |
+
impath = osp.join(self.images_dir, candidates[0])
|
| 137 |
+
except Exception:
|
| 138 |
+
pass
|
| 139 |
+
if impath is None:
|
| 140 |
+
# Skip missing images
|
| 141 |
+
continue
|
| 142 |
+
# For multi-label, we store the full vector as the label
|
| 143 |
+
data.append(Datum(impath=impath, label=vec, classname=''))
|
| 144 |
+
return data
|
| 145 |
+
|
| 146 |
+
def _build_single_label_list(self, id_to_vec: Dict[str, List[int]], classes: List[str]) -> List[Datum]:
|
| 147 |
+
data: List[Datum] = []
|
| 148 |
+
for img_id, vec in id_to_vec.items():
|
| 149 |
+
# choose the first positive class; skip if none
|
| 150 |
+
try:
|
| 151 |
+
cls_idx = next((i for i, v in enumerate(vec) if int(v) == 1), None)
|
| 152 |
+
except Exception:
|
| 153 |
+
cls_idx = None
|
| 154 |
+
if cls_idx is None:
|
| 155 |
+
continue
|
| 156 |
+
# image path resolution
|
| 157 |
+
impath = None
|
| 158 |
+
for ext in ['.jpg', '.jpeg', '.png']:
|
| 159 |
+
p = osp.join(self.images_dir, img_id + ext)
|
| 160 |
+
if osp.isfile(p):
|
| 161 |
+
impath = p
|
| 162 |
+
break
|
| 163 |
+
if impath is None:
|
| 164 |
+
try:
|
| 165 |
+
candidates = [f for f in listdir_nohidden(self.images_dir) if f.startswith(img_id + '.')]
|
| 166 |
+
if candidates:
|
| 167 |
+
impath = osp.join(self.images_dir, candidates[0])
|
| 168 |
+
except Exception:
|
| 169 |
+
pass
|
| 170 |
+
if impath is None:
|
| 171 |
+
continue
|
| 172 |
+
data.append(Datum(impath=impath, label=int(cls_idx), classname=classes[cls_idx]))
|
| 173 |
+
return data
|
README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DFA-MoE: Tackling Dual Forgetting in Vision-Language Continual Learning
|
| 2 |
+
|
| 3 |
+
[](https://www.python.org/)
|
| 4 |
+
[](https://pytorch.org/)
|
| 5 |
+
|
| 6 |
+
Official PyTorch implementation of the paper "Don't Forget Why You Started: Tackling Dual Forgetting in Vision-Language Continual Learning" ICML2026.
|
| 7 |
+
|
| 8 |
+
## Abstract
|
| 9 |
+
|
| 10 |
+
Vision-Language Models (VLMs) are strong continual learners, but standard class-incremental learning often damages the pre-trained vision-language alignment that underpins zero-shot generalization. This repository studies that dual-forgetting problem from two perspectives: Incremental Knowledge Forgetting (IKF), which harms previously learned classes, and Pre-trained Knowledge Forgetting (PKF), which erodes the original zero-shot capabilities of the VLM. To address this issue, we implement the Dual-Forgetting-Aware Class-Incremental Learning (DFA-CIL) framework, the Similarity-Calibrated Retention (SCR) metric, and DFA-MoE, a functionally heterogeneous PEFT method that decouples alignment preservation from task adaptation.
|
| 11 |
+
|
| 12 |
+

|
| 13 |
+
|
| 14 |
+
## Key Contributions
|
| 15 |
+
|
| 16 |
+
- Dual-forgetting evaluation for VLM continual learning through the DFA-CIL protocol.
|
| 17 |
+
- Similarity-Calibrated Retention (SCR) utilizing similarity based weight to disentangle genuine foundational retention from the confounding effects of positive transfer.
|
| 18 |
+
- DFA-MoE with two functional pathways:
|
| 19 |
+
- Alignment Pathway: task-agnostic contrastive expert for PKF mitigation.
|
| 20 |
+
- Plasticity Pathway: task-specific experts with classification and auxiliary contrastive learning for IKF mitigation.
|
| 21 |
+
- Hierarchical routing with an inner router over task-specific experts and an outer router balancing alignment and plasticity outputs.
|
| 22 |
+
|
| 23 |
+
## Installation
|
| 24 |
+
|
| 25 |
+
### Setup Environment
|
| 26 |
+
|
| 27 |
+
```bash
|
| 28 |
+
git clone https://github.com/RL-MIND/DFA-MoE
|
| 29 |
+
cd DFA_MoE
|
| 30 |
+
pip install -r requirements.txt
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
## Supported Downstream Datasets
|
| 34 |
+
|
| 35 |
+
The current `configs/class` directory provides downstream class-incremental configs for the following dataset indices. These indices are the values used in `+train_dataset=[id]`.
|
| 36 |
+
|
| 37 |
+
- `0`: `FGVCAircraft`
|
| 38 |
+
- `1`: `Caltech101`
|
| 39 |
+
- `2`: `CIFAR100`
|
| 40 |
+
- `3`: `DescribableTextures`
|
| 41 |
+
- `4`: `EuroSAT`
|
| 42 |
+
- `5`: `OxfordFlowers`
|
| 43 |
+
- `6`: `Food101`
|
| 44 |
+
- `7`: `MNIST`
|
| 45 |
+
- `8`: `OxfordPets`
|
| 46 |
+
- `9`: `StanfordCars`
|
| 47 |
+
- `10`: `SUN397`
|
| 48 |
+
- `11`: `Country211`
|
| 49 |
+
- `14`: `GTSRB`
|
| 50 |
+
- `15`: `RESISC45`
|
| 51 |
+
- `16`: `FER2013`
|
| 52 |
+
- `17`: `UCF101`
|
| 53 |
+
- `18`: `CIFAR10`
|
| 54 |
+
- `19`: `STL10`
|
| 55 |
+
- `20`: `VOC2007`
|
| 56 |
+
- `21`: `ImageNetR`
|
| 57 |
+
- `22`: `KittiDistance`
|
| 58 |
+
- `24`: `CLEVRCount`
|
| 59 |
+
|
| 60 |
+
The datasets pool also contains:
|
| 61 |
+
|
| 62 |
+
- `12`: `SST2`
|
| 63 |
+
- `13`: `HatefulMemes`
|
| 64 |
+
- `23`: `PCam`
|
| 65 |
+
|
| 66 |
+
These datasets are not supported as `--downstream-dataset` for `calculate_sim.py` because they are binary classification datasets.
|
| 67 |
+
|
| 68 |
+
## Data Preparation
|
| 69 |
+
|
| 70 |
+
We will soon release all the datasets used in this work at [google drive](https://drive.google.com/drive/folders/1DxD6MixpyTcsLSv8Gah5V0Cp3yMupYZO?usp=sharing).
|
| 71 |
+
|
| 72 |
+
1. Download or prepare the datasets under a common `dataset_root`.
|
| 73 |
+
2. Update `dataset_root` in the command line when launching experiments.
|
| 74 |
+
3. Choose the dataset-specific config from `configs/class/`.
|
| 75 |
+
|
| 76 |
+
Example dataset config files:
|
| 77 |
+
|
| 78 |
+
- `configs/class/eurosat.yaml`
|
| 79 |
+
- `configs/class/flower.yaml`
|
| 80 |
+
- `configs/class/cifar100.yaml`
|
| 81 |
+
- `configs/class/aircraft.yaml`
|
| 82 |
+
|
| 83 |
+
## Quick Start
|
| 84 |
+
|
| 85 |
+
### Train a DFA-CIL Run
|
| 86 |
+
|
| 87 |
+
Example: EuroSAT with 5 class-incremental splits.
|
| 88 |
+
|
| 89 |
+
```bash
|
| 90 |
+
python main.py \
|
| 91 |
+
--config-path ./configs/class \
|
| 92 |
+
--config-name eurosat.yaml \
|
| 93 |
+
dataset_root="/path/to/data" \
|
| 94 |
+
+train_dataset=[4] \
|
| 95 |
+
+cil_splits=[5]
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
Example: OxfordFlowers with 17 splits.
|
| 99 |
+
|
| 100 |
+
```bash
|
| 101 |
+
python main.py \
|
| 102 |
+
--config-path ./configs/class \
|
| 103 |
+
--config-name flower.yaml \
|
| 104 |
+
dataset_root="/path/to/data" \
|
| 105 |
+
+train_dataset=[5] \
|
| 106 |
+
+cil_splits=[17]
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
## Important Configuration Options
|
| 111 |
+
|
| 112 |
+
This project uses Hydra-based configuration. Key parameters include:
|
| 113 |
+
|
| 114 |
+
```yaml
|
| 115 |
+
model_name: "ViT-B/16"
|
| 116 |
+
prompt_template: "a bad photo of a {}."
|
| 117 |
+
|
| 118 |
+
batch_size: 128
|
| 119 |
+
weight_decay: 0.0
|
| 120 |
+
ls: 0.0
|
| 121 |
+
|
| 122 |
+
epochs_a: 1
|
| 123 |
+
epochs_b: 1
|
| 124 |
+
|
| 125 |
+
lr_e1: 1.0e-5
|
| 126 |
+
lr_e2: 5.0e-4
|
| 127 |
+
text_lr_e2: 1.0e-5
|
| 128 |
+
lr_e2_router: 1.0e-3
|
| 129 |
+
lr_top_router: 1.0e-5
|
| 130 |
+
|
| 131 |
+
tau_con: 0.15
|
| 132 |
+
tau_b_con: 0.17
|
| 133 |
+
lambda_b_con: 0.001
|
| 134 |
+
|
| 135 |
+
num_task_experts: 2
|
| 136 |
+
e2_top_k: 2
|
| 137 |
+
moco_queue_size: 128
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
## Pre-task Zero-shot Baseline
|
| 141 |
+
|
| 142 |
+
To compute SCR correctly, the metric log must contain the original CLIP zero-shot baseline `A_k^0`. Enable this by setting:
|
| 143 |
+
|
| 144 |
+
```yaml
|
| 145 |
+
pre_task_zero_shot_eval: true
|
| 146 |
+
zero_shot_eval: true
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
or from the command line:
|
| 150 |
+
|
| 151 |
+
```bash
|
| 152 |
+
python main.py \
|
| 153 |
+
--config-path ./configs/class \
|
| 154 |
+
--config-name eurosat.yaml \
|
| 155 |
+
dataset_root="/path/to/data" \
|
| 156 |
+
+train_dataset=[4] \
|
| 157 |
+
+cil_splits=[5] \
|
| 158 |
+
pre_task_zero_shot_eval=true \
|
| 159 |
+
zero_shot_eval=true
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
This writes a `task: -1` entry with `zs_pre` into `metrics.json`, which is required by `calculate_SCR.py`.
|
| 163 |
+
|
| 164 |
+
## SCR Evaluation Workflow
|
| 165 |
+
|
| 166 |
+
### Step 1: Run continual learning and save `metrics.json`
|
| 167 |
+
|
| 168 |
+
Run `main.py` with `pre_task_zero_shot_eval=true` and `zero_shot_eval=true`.
|
| 169 |
+
|
| 170 |
+
### Step 2: Generate the similarity matrix
|
| 171 |
+
|
| 172 |
+
Example: EuroSAT with 5 splits.
|
| 173 |
+
|
| 174 |
+
```bash
|
| 175 |
+
python calculate_sim.py \
|
| 176 |
+
--dataset-root "/path/to/data" \
|
| 177 |
+
--downstream-dataset EuroSAT \
|
| 178 |
+
--cil-split 5 \
|
| 179 |
+
--output eurosat_similarity.json
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
This script:
|
| 183 |
+
|
| 184 |
+
- uses the original frozen CLIP model,
|
| 185 |
+
- produces a task-to-upstream similarity matrix for SCR.
|
| 186 |
+
|
| 187 |
+
### Step 3: Compute SCR
|
| 188 |
+
|
| 189 |
+
```bash
|
| 190 |
+
python calculate_SCR.py \
|
| 191 |
+
--metric-path /path/to/metrics.json \
|
| 192 |
+
--sim-json /path/to/eurosat_similarity_sim.json
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
`calculate_SCR.py` now strictly requires:
|
| 196 |
+
|
| 197 |
+
- a `task: -1` row with non-empty `zs_pre`,
|
| 198 |
+
- per-task `zs` results in `metrics.json`,
|
| 199 |
+
- a similarity JSON generated by `calculate_sim.py`.
|
| 200 |
+
|
| 201 |
+
## Reference Script
|
| 202 |
+
|
| 203 |
+
The repository also contains `run.sh` as a reference batch script.
|
| 204 |
+
|
| 205 |
+
## Notes
|
| 206 |
+
|
| 207 |
+
- `main.py`, `calculate_sim.py`, and `calculate_SCR.py` are the recommended entry points for experiments and evaluation.
|
| 208 |
+
|
| 209 |
+
## Citation
|
| 210 |
+
|
| 211 |
+
If you find this repository useful in your research, please cite the paper:
|
| 212 |
+
|
| 213 |
+
```bibtex
|
| 214 |
+
@inproceedings{kang2026dont,
|
| 215 |
+
title={Don't Forget Why You Started: Tackling Dual Forgetting in Vision-Language Continual Learning},
|
| 216 |
+
author={Kang, Borui and Gu, Jinrui and Feng, Tao and Fan, Qi and Shi, Yinghuan and Wang, Lei and Li, Wenbin and Gao, Yang},
|
| 217 |
+
booktitle={Proceedings of the 43rd International Conference on Machine Learning},
|
| 218 |
+
year={2026}
|
| 219 |
+
}
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
## Acknowledgement
|
| 223 |
+
|
| 224 |
+
This repository is built upon and modified from [MoE-Adapters4CL](https://github.com/JiazuoYu/MoE-Adapters4CL). We thank the original authors for making their code publicly available.
|
| 225 |
+
|
| 226 |
+
The dataset processing pipeline in this repository is also based on [DIKI](https://github.com/lloongx/DIKI). We thank the authors for releasing their implementation.
|
| 227 |
+
|
calculate_SCR.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
from typing import List, Dict, Any, Tuple
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
DATASET_NAME_ALIASES = {
|
| 9 |
+
"Aircraft": "FGVCAircraft",
|
| 10 |
+
"FGVCAircraft": "FGVCAircraft",
|
| 11 |
+
"DTD": "DescribableTextures",
|
| 12 |
+
"DescribableTextures": "DescribableTextures",
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def canonicalize_dataset_name(name: str) -> str:
|
| 17 |
+
return DATASET_NAME_ALIASES.get(str(name), str(name))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def format_matrix(mat: List[List[float]], decimals: int = 3) -> str:
|
| 21 |
+
if not mat:
|
| 22 |
+
return "[]"
|
| 23 |
+
fmt = f"{{:.{decimals}f}}"
|
| 24 |
+
lines = []
|
| 25 |
+
for row in mat:
|
| 26 |
+
row_str = ", ".join(fmt.format(v) for v in row)
|
| 27 |
+
lines.append(f"[{row_str}]")
|
| 28 |
+
return "[" + "\n" + ",\n".join(lines) + "\n]"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def load_lines(path: str) -> List[Dict[str, Any]]:
|
| 32 |
+
rows = []
|
| 33 |
+
with open(path, 'r', encoding='utf-8') as f:
|
| 34 |
+
for line in f:
|
| 35 |
+
line = line.strip()
|
| 36 |
+
if not line:
|
| 37 |
+
continue
|
| 38 |
+
try:
|
| 39 |
+
rows.append(json.loads(line))
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f"[WARN] skip invalid JSON line: {e}")
|
| 42 |
+
rows.sort(key=lambda r: int(r.get('task', 0)))
|
| 43 |
+
return rows
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def build_zs_matrix(rows: List[Dict[str, Any]]) -> Tuple[List[List[float]], List[str]]:
|
| 47 |
+
if not rows:
|
| 48 |
+
return [], []
|
| 49 |
+
|
| 50 |
+
baseline_row = None
|
| 51 |
+
task_rows = []
|
| 52 |
+
|
| 53 |
+
for r in rows:
|
| 54 |
+
task_id = int(r.get('task', 0))
|
| 55 |
+
zs_pre = r.get('zs_pre')
|
| 56 |
+
zs_task = r.get('zs')
|
| 57 |
+
zs_task_alt = r.get('zs_mtil')
|
| 58 |
+
|
| 59 |
+
if task_id == -1 and isinstance(zs_pre, dict) and len(zs_pre) > 0:
|
| 60 |
+
baseline_row = zs_pre
|
| 61 |
+
continue
|
| 62 |
+
if task_id >= 0:
|
| 63 |
+
if isinstance(zs_task, dict) and len(zs_task) > 0:
|
| 64 |
+
task_rows.append((task_id, zs_task))
|
| 65 |
+
elif isinstance(zs_task_alt, dict) and len(zs_task_alt) > 0:
|
| 66 |
+
task_rows.append((task_id, zs_task_alt))
|
| 67 |
+
|
| 68 |
+
if baseline_row is None:
|
| 69 |
+
raise ValueError(
|
| 70 |
+
"metric.json must contain a pre-task baseline row with `task: -1` and non-empty `zs_pre`."
|
| 71 |
+
)
|
| 72 |
+
if not task_rows:
|
| 73 |
+
return [], []
|
| 74 |
+
|
| 75 |
+
names = list(baseline_row.keys())
|
| 76 |
+
matrix: List[List[float]] = []
|
| 77 |
+
matrix.append([float(baseline_row.get(name, 0.0)) for name in names])
|
| 78 |
+
for _task_id, zs in sorted(task_rows, key=lambda item: item[0]):
|
| 79 |
+
row = [float(zs.get(name, 0.0)) for name in names]
|
| 80 |
+
matrix.append(row)
|
| 81 |
+
return matrix, names
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def load_similarity_json(path: str) -> Tuple[List[List[float]], List[str]]:
|
| 85 |
+
with open(path, 'r', encoding='utf-8') as f:
|
| 86 |
+
payload = json.load(f)
|
| 87 |
+
sim_mat_raw = payload.get("similarity_matrix", [])
|
| 88 |
+
sim_names_raw = payload.get("upstream_names", [])
|
| 89 |
+
sim_mat = [[float(x) for x in row] for row in sim_mat_raw]
|
| 90 |
+
sim_names = [canonicalize_dataset_name(x) for x in sim_names_raw]
|
| 91 |
+
return sim_mat, sim_names
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def compute_scores_with_matrix(
|
| 95 |
+
zs_mat: List[List[float]],
|
| 96 |
+
zs_names: List[str],
|
| 97 |
+
sim_mat: List[List[float]],
|
| 98 |
+
sim_names: List[str],
|
| 99 |
+
) -> List[float]:
|
| 100 |
+
if not zs_mat or not sim_mat:
|
| 101 |
+
return []
|
| 102 |
+
|
| 103 |
+
zs_names = [canonicalize_dataset_name(nm) for nm in zs_names]
|
| 104 |
+
zs_name_to_idx = {nm: i for i, nm in enumerate(zs_names)}
|
| 105 |
+
sim_name_to_idx = {nm: i for i, nm in enumerate(sim_names)}
|
| 106 |
+
common = [nm for nm in zs_names if nm in sim_name_to_idx]
|
| 107 |
+
if len(common) < len(zs_names):
|
| 108 |
+
missing = [nm for nm in zs_names if nm not in sim_name_to_idx]
|
| 109 |
+
print(f"[WARN] Missing similarity columns for: {missing}")
|
| 110 |
+
zs_cols = [zs_name_to_idx[nm] for nm in common]
|
| 111 |
+
sim_cols = [sim_name_to_idx[nm] for nm in common]
|
| 112 |
+
|
| 113 |
+
base = zs_mat[0]
|
| 114 |
+
scores: List[float] = []
|
| 115 |
+
T = min(len(sim_mat), max(0, len(zs_mat) - 1))
|
| 116 |
+
if T < (len(zs_mat) - 1):
|
| 117 |
+
print(f"[WARN] sim-matrix rows ({len(sim_mat)}) < tasks ({len(zs_mat) - 1}); truncating to {T}")
|
| 118 |
+
for t in range(T):
|
| 119 |
+
row = zs_mat[t + 1]
|
| 120 |
+
total = 0.0
|
| 121 |
+
sum_w = 0.0
|
| 122 |
+
sims = [float(sim_mat[t][sim_cols[k]]) for k in range(len(common))]
|
| 123 |
+
risks = [max(0.0, 1-s) for s in sims]
|
| 124 |
+
norm = sum(risks) if risks else 0.0
|
| 125 |
+
default_weight = 1.0 / max(1, len(common))
|
| 126 |
+
for k in range(len(common)):
|
| 127 |
+
i = zs_cols[k]
|
| 128 |
+
diff = float(row[i]) - float(base[i])
|
| 129 |
+
w = (risks[k] / norm) if norm > 0.0 else default_weight
|
| 130 |
+
total += diff * w
|
| 131 |
+
sum_w += w
|
| 132 |
+
avg = (total / sum_w) if sum_w > 0.0 else 0.0
|
| 133 |
+
scores.append(round(avg, 2))
|
| 134 |
+
return scores
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def compute_scores_grouped(
|
| 138 |
+
zs_mat: List[List[float]],
|
| 139 |
+
zs_names: List[str],
|
| 140 |
+
sim_mat: List[List[float]],
|
| 141 |
+
sim_names: List[str],
|
| 142 |
+
) -> Dict[str, Any]:
|
| 143 |
+
if not zs_mat or not sim_mat:
|
| 144 |
+
return {
|
| 145 |
+
"weighted": {"low": [], "mid": [], "high": []},
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
zs_names = [canonicalize_dataset_name(nm) for nm in zs_names]
|
| 149 |
+
zs_name_to_idx = {nm: i for i, nm in enumerate(zs_names)}
|
| 150 |
+
sim_name_to_idx = {nm: i for i, nm in enumerate(sim_names)}
|
| 151 |
+
common = [nm for nm in zs_names if nm in sim_name_to_idx]
|
| 152 |
+
if len(common) < len(zs_names):
|
| 153 |
+
missing = [nm for nm in zs_names if nm not in sim_name_to_idx]
|
| 154 |
+
print(f"[WARN] Missing similarity columns for grouped score: {missing}")
|
| 155 |
+
zs_cols = [zs_name_to_idx[nm] for nm in common]
|
| 156 |
+
sim_cols = [sim_name_to_idx[nm] for nm in common]
|
| 157 |
+
|
| 158 |
+
base = zs_mat[0]
|
| 159 |
+
T = min(len(sim_mat), max(0, len(zs_mat) - 1))
|
| 160 |
+
if T < (len(zs_mat) - 1):
|
| 161 |
+
print(f"[WARN] sim-matrix rows ({len(sim_mat)}) < tasks ({len(zs_mat) - 1}); truncating to {T}")
|
| 162 |
+
|
| 163 |
+
def calc_weighted(items):
|
| 164 |
+
total = sum(item["diff"] * item["weight"] for item in items)
|
| 165 |
+
sum_w = sum(item["weight"] for item in items)
|
| 166 |
+
return round((total / sum_w) if sum_w > 0.0 else 0.0, 2)
|
| 167 |
+
|
| 168 |
+
w_low, w_mid, w_high = [], [], []
|
| 169 |
+
|
| 170 |
+
for t in range(T):
|
| 171 |
+
row = zs_mat[t + 1]
|
| 172 |
+
sims = [float(sim_mat[t][sim_cols[k]]) for k in range(len(common))]
|
| 173 |
+
risks = [max(0.0, 1.0 - s) for s in sims]
|
| 174 |
+
norm = sum(risks) if risks else 0.0
|
| 175 |
+
default_weight = 1.0 / max(1, len(common))
|
| 176 |
+
triplets = []
|
| 177 |
+
for k in range(len(common)):
|
| 178 |
+
i = zs_cols[k]
|
| 179 |
+
sim_val = sims[k]
|
| 180 |
+
diff = float(row[i]) - float(base[i])
|
| 181 |
+
w = (risks[k] / norm) if norm > 0.0 else default_weight
|
| 182 |
+
triplets.append({
|
| 183 |
+
"sim": sim_val,
|
| 184 |
+
"diff": diff,
|
| 185 |
+
"weight": w,
|
| 186 |
+
"index": i + 1,
|
| 187 |
+
})
|
| 188 |
+
triplets.sort(key=lambda x: x["sim"])
|
| 189 |
+
n = len(triplets)
|
| 190 |
+
q = n // 3
|
| 191 |
+
low = triplets[:q]
|
| 192 |
+
mid = triplets[q:2 * q]
|
| 193 |
+
high = triplets[2 * q:]
|
| 194 |
+
|
| 195 |
+
w_low.append(calc_weighted(low))
|
| 196 |
+
w_mid.append(calc_weighted(mid))
|
| 197 |
+
w_high.append(calc_weighted(high))
|
| 198 |
+
|
| 199 |
+
return {
|
| 200 |
+
"weighted": {"low": w_low, "mid": w_mid, "high": w_high},
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def main() -> None:
|
| 205 |
+
parser = argparse.ArgumentParser(
|
| 206 |
+
description="Compute SCR metrics from metric.json and calculate_sim similarity JSON.",
|
| 207 |
+
)
|
| 208 |
+
parser.add_argument("--metric-path", type=str, default="metric.json", help="Path to the metric.json file")
|
| 209 |
+
parser.add_argument("--sim-json", type=str, required=True, help="Path to the similarity JSON generated by calculate_sim.py")
|
| 210 |
+
parser.add_argument("--zs-decimals", type=int, default=2, help="Decimals when printing ZS matrix")
|
| 211 |
+
args = parser.parse_args()
|
| 212 |
+
|
| 213 |
+
rows = load_lines(args.metric_path)
|
| 214 |
+
try:
|
| 215 |
+
zs_mat, zs_names = build_zs_matrix(rows)
|
| 216 |
+
except ValueError as e:
|
| 217 |
+
print(f"[ERROR] {e}")
|
| 218 |
+
return
|
| 219 |
+
sim_mat, sim_names = load_similarity_json(args.sim_json)
|
| 220 |
+
|
| 221 |
+
if not zs_mat:
|
| 222 |
+
print("[ERROR] No zero-shot matrix could be extracted from metric.json.")
|
| 223 |
+
return
|
| 224 |
+
if not sim_mat:
|
| 225 |
+
print("[ERROR] No similarity matrix could be extracted from the calculate_sim JSON.")
|
| 226 |
+
return
|
| 227 |
+
|
| 228 |
+
num_cols = len(zs_mat[0]) if zs_mat else 0
|
| 229 |
+
if any(len(row) != num_cols for row in zs_mat):
|
| 230 |
+
print("[ERROR] Extracted zero-shot matrix rows must all have the same length.")
|
| 231 |
+
return
|
| 232 |
+
|
| 233 |
+
sim_cols = len(sim_mat[0]) if sim_mat else 0
|
| 234 |
+
if any(len(row) != sim_cols for row in sim_mat):
|
| 235 |
+
print("[ERROR] similarity_matrix rows in the calculate_sim JSON must all have the same length.")
|
| 236 |
+
return
|
| 237 |
+
if sim_names and len(sim_names) != sim_cols:
|
| 238 |
+
print("[ERROR] upstream_names length must match similarity_matrix columns.")
|
| 239 |
+
return
|
| 240 |
+
if not sim_names:
|
| 241 |
+
sim_names = zs_names
|
| 242 |
+
|
| 243 |
+
print("1. Zero-shot matrix (baseline + tasks):")
|
| 244 |
+
print(format_matrix(zs_mat, decimals=args.zs_decimals))
|
| 245 |
+
|
| 246 |
+
scores_weighted = compute_scores_with_matrix(zs_mat, zs_names, sim_mat, sim_names)
|
| 247 |
+
avg_weighted = round(sum(scores_weighted) / len(scores_weighted), 2) if scores_weighted else 0.0
|
| 248 |
+
print("\nSCR:")
|
| 249 |
+
print(avg_weighted)
|
| 250 |
+
|
| 251 |
+
grouped = compute_scores_grouped(zs_mat, zs_names, sim_mat, sim_names)
|
| 252 |
+
|
| 253 |
+
def mean_or_zero(arr: list) -> float:
|
| 254 |
+
return round(sum(arr) / len(arr), 2) if arr else 0.0
|
| 255 |
+
|
| 256 |
+
weighted = grouped["weighted"]
|
| 257 |
+
low_w_mean = mean_or_zero(weighted["low"])
|
| 258 |
+
mid_w_mean = mean_or_zero(weighted["mid"])
|
| 259 |
+
high_w_mean = mean_or_zero(weighted["high"])
|
| 260 |
+
|
| 261 |
+
print("\nLow-Similarity SCR:", low_w_mean)
|
| 262 |
+
print("Mid-Similarity SCR:", mid_w_mean)
|
| 263 |
+
print("High-Similarity SCR:", high_w_mean)
|
| 264 |
+
|
| 265 |
+
if __name__ == "__main__":
|
| 266 |
+
main()
|
calculate_sim.py
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import os
|
| 3 |
+
from typing import List, Dict, Tuple
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import torch
|
| 7 |
+
from omegaconf import OmegaConf
|
| 8 |
+
|
| 9 |
+
# Project-local imports
|
| 10 |
+
from mtil_datasets import get_dataset as get_mtil_dataset
|
| 11 |
+
from continual_clip.mtil_cil import build_mtil_cil_scenarios
|
| 12 |
+
from continual_clip.clip_original import load as load_orig_clip, tokenize as tokenize_orig
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
DEFAULT_NAMES_ORDER1 = [
|
| 16 |
+
"FGVCAircraft", "Caltech101", "CIFAR100", "DescribableTextures", "EuroSAT",
|
| 17 |
+
"OxfordFlowers", "Food101", "MNIST", "OxfordPets", "StanfordCars",
|
| 18 |
+
"SUN397", "Country211", "SST2", "HatefulMemes", "GTSRB",
|
| 19 |
+
"RESISC45", "FER2013", "UCF101", "CIFAR10", "STL10",
|
| 20 |
+
"VOC2007", "ImageNetR", "KittiDistance", "PCam", "CLEVRCount",
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
DATASET_NAME_TO_CONFIG_STEM = {
|
| 25 |
+
"FGVCAircraft": "aircraft",
|
| 26 |
+
"Aircraft": "aircraft",
|
| 27 |
+
"Caltech101": "caltech",
|
| 28 |
+
"CIFAR100": "cifar100",
|
| 29 |
+
"DescribableTextures": "dtd",
|
| 30 |
+
"DTD": "dtd",
|
| 31 |
+
"EuroSAT": "eurosat",
|
| 32 |
+
"OxfordFlowers": "flower",
|
| 33 |
+
"Food101": "food",
|
| 34 |
+
"MNIST": "mnist",
|
| 35 |
+
"OxfordPets": "pet",
|
| 36 |
+
"StanfordCars": "car",
|
| 37 |
+
"SUN397": "sun",
|
| 38 |
+
"Country211": "conuntry",
|
| 39 |
+
"GTSRB": "gtsrb",
|
| 40 |
+
"RESISC45": "resisc45",
|
| 41 |
+
"FER2013": "fer2013",
|
| 42 |
+
"UCF101": "ucf",
|
| 43 |
+
"CIFAR10": "cifar10",
|
| 44 |
+
"STL10": "stl",
|
| 45 |
+
"VOC2007": "voc",
|
| 46 |
+
"ImageNetR": "image-r",
|
| 47 |
+
"KittiDistance": "kitti",
|
| 48 |
+
"CLEVRCount": "clevr",
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
UNSUPPORTED_DOWNSTREAM_DATASETS = {
|
| 53 |
+
"SST2",
|
| 54 |
+
"HatefulMemes",
|
| 55 |
+
"PCam",
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def resolve_downstream_seed(repo_root: str, downstream_name: str) -> int:
|
| 60 |
+
if downstream_name in UNSUPPORTED_DOWNSTREAM_DATASETS:
|
| 61 |
+
raise ValueError(
|
| 62 |
+
f"Downstream dataset '{downstream_name}' is currently unsupported as `--downstream-dataset` because no matching `configs/class/*.yaml` file is available."
|
| 63 |
+
)
|
| 64 |
+
config_stem = DATASET_NAME_TO_CONFIG_STEM.get(downstream_name)
|
| 65 |
+
if not config_stem:
|
| 66 |
+
raise ValueError(
|
| 67 |
+
f"No `configs/class/*.yaml` mapping is defined for downstream dataset '{downstream_name}'."
|
| 68 |
+
)
|
| 69 |
+
config_path = os.path.join(repo_root, "configs", "class", f"{config_stem}.yaml")
|
| 70 |
+
if not os.path.exists(config_path):
|
| 71 |
+
raise FileNotFoundError(
|
| 72 |
+
f"Expected downstream config file '{config_path}' for dataset '{downstream_name}'."
|
| 73 |
+
)
|
| 74 |
+
cfg = OmegaConf.load(config_path)
|
| 75 |
+
seed = OmegaConf.select(cfg, "seed")
|
| 76 |
+
if seed is None:
|
| 77 |
+
raise ValueError(
|
| 78 |
+
f"Config file '{config_path}' does not define a `seed` entry."
|
| 79 |
+
)
|
| 80 |
+
return int(seed)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def encode_texts(model, device, sentences: List[str], batch_size: int = 256) -> torch.Tensor:
|
| 84 |
+
feats_all = []
|
| 85 |
+
with torch.no_grad():
|
| 86 |
+
for i in range(0, len(sentences), batch_size):
|
| 87 |
+
chunk = sentences[i:i+batch_size]
|
| 88 |
+
tokens = tokenize_orig(chunk).to(device)
|
| 89 |
+
feats = model.encode_text(tokens)
|
| 90 |
+
feats = feats / (feats.norm(dim=-1, keepdim=True) + 1e-12)
|
| 91 |
+
feats_all.append(feats)
|
| 92 |
+
if not feats_all:
|
| 93 |
+
return torch.zeros((0, model.text_projection.shape[1]), device=device)
|
| 94 |
+
return torch.cat(feats_all, dim=0)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def build_text_class_prototypes(model, device, classnames: List[str], templates, batch_size: int = 256) -> torch.Tensor:
|
| 98 |
+
D = model.text_projection.shape[1]
|
| 99 |
+
class_vecs: List[torch.Tensor] = []
|
| 100 |
+
for cname in classnames:
|
| 101 |
+
sents: List[str] = []
|
| 102 |
+
if templates and len(templates) > 0:
|
| 103 |
+
for t in templates:
|
| 104 |
+
try:
|
| 105 |
+
sents.append(t(cname) if callable(t) else str(t).format(cname))
|
| 106 |
+
except Exception:
|
| 107 |
+
continue
|
| 108 |
+
else:
|
| 109 |
+
sents = [f"a photo of a {cname}."]
|
| 110 |
+
feats = encode_texts(model, device, sents, batch_size=batch_size)
|
| 111 |
+
if feats.numel() == 0:
|
| 112 |
+
v = torch.zeros(D, device=device)
|
| 113 |
+
else:
|
| 114 |
+
v = feats.mean(dim=0)
|
| 115 |
+
v = v / (v.norm() + 1e-12)
|
| 116 |
+
class_vecs.append(v)
|
| 117 |
+
if not class_vecs:
|
| 118 |
+
return torch.zeros((0, D), device=device)
|
| 119 |
+
return torch.stack(class_vecs, dim=0)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def normalize_labels_to_list(lab) -> List[int]:
|
| 123 |
+
if isinstance(lab, (int, np.integer)):
|
| 124 |
+
return [int(lab)]
|
| 125 |
+
import torch as _torch
|
| 126 |
+
if _torch.is_tensor(lab):
|
| 127 |
+
arr = lab.detach().cpu().numpy()
|
| 128 |
+
if arr.ndim == 0:
|
| 129 |
+
return [int(arr)]
|
| 130 |
+
if arr.ndim == 1 and arr.size > 1 and set(np.unique(arr)).issubset({0,1}):
|
| 131 |
+
return [int(x) for x in np.where(arr > 0.5)[0].tolist()]
|
| 132 |
+
return [int(x) for x in arr.flatten().tolist()]
|
| 133 |
+
if isinstance(lab, (list, tuple, np.ndarray)):
|
| 134 |
+
arr = np.asarray(lab)
|
| 135 |
+
if arr.ndim == 0:
|
| 136 |
+
return [int(arr)]
|
| 137 |
+
if arr.ndim == 1 and arr.size > 1 and set(np.unique(arr)).issubset({0,1}):
|
| 138 |
+
return [int(x) for x in np.where(arr > 0.5)[0].tolist()]
|
| 139 |
+
if arr.ndim == 1:
|
| 140 |
+
return [int(x) for x in arr.tolist()]
|
| 141 |
+
return [int(x) for x in np.where(arr.flatten() > 0.5)[0].tolist()]
|
| 142 |
+
return []
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def build_visual_class_prototypes(model, device, ds_wrapper, num_classes: int, max_per_class: int, batch_size: int, preprocess_eval) -> torch.Tensor:
|
| 146 |
+
D = None
|
| 147 |
+
sums: Dict[int, torch.Tensor] = {}
|
| 148 |
+
counts: Dict[int, int] = {i: 0 for i in range(num_classes)}
|
| 149 |
+
imgs_batch: List[torch.Tensor] = []
|
| 150 |
+
labels_batch_multi: List[List[int]] = []
|
| 151 |
+
with torch.no_grad():
|
| 152 |
+
for i in range(len(ds_wrapper)):
|
| 153 |
+
img, lab = ds_wrapper[i]
|
| 154 |
+
lab_ids = [lid for lid in normalize_labels_to_list(lab) if 0 <= lid < num_classes and counts[lid] < max_per_class]
|
| 155 |
+
if not lab_ids:
|
| 156 |
+
continue
|
| 157 |
+
if isinstance(img, torch.Tensor):
|
| 158 |
+
tensor_img = img
|
| 159 |
+
else:
|
| 160 |
+
tensor_img = preprocess_eval(img)
|
| 161 |
+
imgs_batch.append(tensor_img.unsqueeze(0))
|
| 162 |
+
labels_batch_multi.append(lab_ids)
|
| 163 |
+
if len(imgs_batch) >= max(1, batch_size):
|
| 164 |
+
batch = torch.cat(imgs_batch, dim=0).to(device)
|
| 165 |
+
feats = model.encode_image(batch)
|
| 166 |
+
feats = feats / (feats.norm(dim=-1, keepdim=True) + 1e-12)
|
| 167 |
+
if D is None:
|
| 168 |
+
D = int(feats.shape[1])
|
| 169 |
+
for f, ls in zip(feats, labels_batch_multi):
|
| 170 |
+
for l in ls:
|
| 171 |
+
if counts[l] >= max_per_class:
|
| 172 |
+
continue
|
| 173 |
+
if l not in sums:
|
| 174 |
+
sums[l] = f.detach().clone()
|
| 175 |
+
else:
|
| 176 |
+
sums[l] = sums[l] + f.detach()
|
| 177 |
+
counts[l] += 1
|
| 178 |
+
imgs_batch.clear()
|
| 179 |
+
labels_batch_multi.clear()
|
| 180 |
+
if all(counts[l] >= max_per_class for l in range(num_classes)):
|
| 181 |
+
break
|
| 182 |
+
if imgs_batch:
|
| 183 |
+
batch = torch.cat(imgs_batch, dim=0).to(device)
|
| 184 |
+
feats = model.encode_image(batch)
|
| 185 |
+
feats = feats / (feats.norm(dim=-1, keepdim=True) + 1e-12)
|
| 186 |
+
if D is None:
|
| 187 |
+
D = int(feats.shape[1])
|
| 188 |
+
for f, ls in zip(feats, labels_batch_multi):
|
| 189 |
+
for l in ls:
|
| 190 |
+
if counts[l] >= max_per_class:
|
| 191 |
+
continue
|
| 192 |
+
if l not in sums:
|
| 193 |
+
sums[l] = f.detach().clone()
|
| 194 |
+
else:
|
| 195 |
+
sums[l] = sums[l] + f.detach()
|
| 196 |
+
counts[l] += 1
|
| 197 |
+
imgs_batch.clear()
|
| 198 |
+
labels_batch_multi.clear()
|
| 199 |
+
class_vecs: List[torch.Tensor] = []
|
| 200 |
+
for lid in range(num_classes):
|
| 201 |
+
c = counts.get(lid, 0)
|
| 202 |
+
if c <= 0:
|
| 203 |
+
class_vecs.append(torch.zeros(int(D or 0), device=device))
|
| 204 |
+
else:
|
| 205 |
+
m = sums[lid] / float(c)
|
| 206 |
+
m = m / (m.norm() + 1e-12)
|
| 207 |
+
class_vecs.append(m)
|
| 208 |
+
if not class_vecs:
|
| 209 |
+
return torch.zeros((0, int(D or 0)), device=device)
|
| 210 |
+
return torch.stack(class_vecs, dim=0)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def build_visual_class_prototypes_subset(model, device, ds_wrapper, label_ids: List[int], max_per_class: int, batch_size: int, preprocess_eval) -> torch.Tensor:
|
| 214 |
+
D = None
|
| 215 |
+
sums: Dict[int, torch.Tensor] = {}
|
| 216 |
+
counts: Dict[int, int] = {lid: 0 for lid in label_ids}
|
| 217 |
+
label_set = set(label_ids)
|
| 218 |
+
imgs_batch: List[torch.Tensor] = []
|
| 219 |
+
labels_batch_multi: List[List[int]] = []
|
| 220 |
+
with torch.no_grad():
|
| 221 |
+
for i in range(len(ds_wrapper)):
|
| 222 |
+
img, lab = ds_wrapper[i]
|
| 223 |
+
lab_all = [lid for lid in normalize_labels_to_list(lab) if lid in label_set and counts.get(lid, 0) < max_per_class]
|
| 224 |
+
if not lab_all:
|
| 225 |
+
continue
|
| 226 |
+
tensor_img = img if isinstance(img, torch.Tensor) else preprocess_eval(img)
|
| 227 |
+
imgs_batch.append(tensor_img.unsqueeze(0))
|
| 228 |
+
labels_batch_multi.append(lab_all)
|
| 229 |
+
if len(imgs_batch) >= max(1, batch_size):
|
| 230 |
+
batch = torch.cat(imgs_batch, dim=0).to(device)
|
| 231 |
+
feats = model.encode_image(batch)
|
| 232 |
+
feats = feats / (feats.norm(dim=-1, keepdim=True) + 1e-12)
|
| 233 |
+
if D is None:
|
| 234 |
+
D = int(feats.shape[1])
|
| 235 |
+
for f, ls in zip(feats, labels_batch_multi):
|
| 236 |
+
for l in ls:
|
| 237 |
+
if counts[l] >= max_per_class:
|
| 238 |
+
continue
|
| 239 |
+
if l not in sums:
|
| 240 |
+
sums[l] = f.detach().clone()
|
| 241 |
+
else:
|
| 242 |
+
sums[l] = sums[l] + f.detach()
|
| 243 |
+
counts[l] += 1
|
| 244 |
+
imgs_batch.clear()
|
| 245 |
+
labels_batch_multi.clear()
|
| 246 |
+
if all(counts[lid] >= max_per_class for lid in label_ids):
|
| 247 |
+
break
|
| 248 |
+
if imgs_batch:
|
| 249 |
+
batch = torch.cat(imgs_batch, dim=0).to(device)
|
| 250 |
+
feats = model.encode_image(batch)
|
| 251 |
+
feats = feats / (feats.norm(dim=-1, keepdim=True) + 1e-12)
|
| 252 |
+
if D is None:
|
| 253 |
+
D = int(feats.shape[1])
|
| 254 |
+
for f, ls in zip(feats, labels_batch_multi):
|
| 255 |
+
for l in ls:
|
| 256 |
+
if counts[l] >= max_per_class:
|
| 257 |
+
continue
|
| 258 |
+
if l not in sums:
|
| 259 |
+
sums[l] = f.detach().clone()
|
| 260 |
+
else:
|
| 261 |
+
sums[l] = sums[l] + f.detach()
|
| 262 |
+
counts[l] += 1
|
| 263 |
+
imgs_batch.clear()
|
| 264 |
+
labels_batch_multi.clear()
|
| 265 |
+
class_vecs: List[torch.Tensor] = []
|
| 266 |
+
for lid in label_ids:
|
| 267 |
+
c = counts.get(lid, 0)
|
| 268 |
+
if c <= 0:
|
| 269 |
+
class_vecs.append(torch.zeros(int(D or 0), device=device))
|
| 270 |
+
else:
|
| 271 |
+
m = sums[lid] / float(c)
|
| 272 |
+
m = m / (m.norm() + 1e-12)
|
| 273 |
+
class_vecs.append(m)
|
| 274 |
+
if not class_vecs:
|
| 275 |
+
return torch.zeros((0, int(D or 0)), device=device)
|
| 276 |
+
return torch.stack(class_vecs, dim=0)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def cosine_distance_matrix(A: torch.Tensor, B: torch.Tensor) -> torch.Tensor:
|
| 280 |
+
S = (A @ B.t()).clamp(-1.0, 1.0)
|
| 281 |
+
return 1.0 - S
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def directed_covering_distance(T_src: torch.Tensor, V_src: torch.Tensor,
|
| 285 |
+
T_tgt: torch.Tensor, V_tgt: torch.Tensor) -> float:
|
| 286 |
+
if V_src.size(0) == 0 or V_tgt.size(0) == 0:
|
| 287 |
+
return 0.0
|
| 288 |
+
F_src = torch.cat([V_src, T_src], dim=1)
|
| 289 |
+
F_tgt = torch.cat([V_tgt, T_tgt], dim=1)
|
| 290 |
+
F_src = F_src / (F_src.norm(dim=-1, keepdim=True) + 1e-12)
|
| 291 |
+
F_tgt = F_tgt / (F_tgt.norm(dim=-1, keepdim=True) + 1e-12)
|
| 292 |
+
C = cosine_distance_matrix(F_src, F_tgt)
|
| 293 |
+
d_row_min = C.min(dim=1).values
|
| 294 |
+
return float(d_row_min.mean().item())
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def tasks_to_upstream_similarity_matrix(vision_model, device,
|
| 298 |
+
upstream_names: List[str], name_to_idx: Dict[str, int],
|
| 299 |
+
dataset_list, classes_names_list, templates_list,
|
| 300 |
+
downstream_name: str, cil_splits: int,
|
| 301 |
+
preprocess_eval,
|
| 302 |
+
max_images_per_class: int, vision_batch_size: int,
|
| 303 |
+
dataset_root: str,
|
| 304 |
+
beta: float,
|
| 305 |
+
clip_text_model=None,
|
| 306 |
+
seed: int = 32) -> Tuple[np.ndarray, List[str]]:
|
| 307 |
+
T_up_list: List[torch.Tensor] = []
|
| 308 |
+
V_up_list: List[torch.Tensor] = []
|
| 309 |
+
for nm in upstream_names:
|
| 310 |
+
i = name_to_idx[nm]
|
| 311 |
+
classnames = classes_names_list[i]
|
| 312 |
+
templates = templates_list[i]
|
| 313 |
+
ds_wrapper = dataset_list[i]
|
| 314 |
+
K = len(classnames)
|
| 315 |
+
T_k = build_text_class_prototypes(clip_text_model, device, classnames, templates, batch_size=256)
|
| 316 |
+
V_k = build_visual_class_prototypes(vision_model, device, ds_wrapper, K, max_per_class=int(max_images_per_class), batch_size=int(vision_batch_size), preprocess_eval=preprocess_eval)
|
| 317 |
+
T_up_list.append(T_k)
|
| 318 |
+
V_up_list.append(V_k)
|
| 319 |
+
|
| 320 |
+
ds_idx = name_to_idx[downstream_name]
|
| 321 |
+
cfg_ds = type("Cfg", (), {})()
|
| 322 |
+
cfg_ds.dataset = "MTIL"
|
| 323 |
+
cfg_ds.dataset_root = dataset_root
|
| 324 |
+
cfg_ds.MTIL_order_2 = False
|
| 325 |
+
cfg_ds.train_one_dataset = ds_idx
|
| 326 |
+
cfg_ds.seed = int(seed)
|
| 327 |
+
cfg_ds.use_validation = False
|
| 328 |
+
train_list, train_classes_names, train_templates, _ = get_mtil_dataset(
|
| 329 |
+
cfg_ds, 'train', transforms=preprocess_eval
|
| 330 |
+
)
|
| 331 |
+
test_list, _, _, _ = get_mtil_dataset(
|
| 332 |
+
cfg_ds, 'test', transforms=preprocess_eval
|
| 333 |
+
)
|
| 334 |
+
assert len(train_list) == 1 and len(test_list) == 1, "Expected single selected dataset for downstream"
|
| 335 |
+
classnames_single = train_classes_names[0]
|
| 336 |
+
templates_single = train_templates[0]
|
| 337 |
+
_, _, _, class_ids_per_task, _ = build_mtil_cil_scenarios(
|
| 338 |
+
train_list[0], test_list[0], classnames_single, cil_splits, seed=int(seed)
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
distances = np.zeros((cil_splits, len(upstream_names)), dtype=float)
|
| 342 |
+
for t, cls_ids in enumerate(class_ids_per_task):
|
| 343 |
+
cls_names_t = [classnames_single[c] for c in cls_ids]
|
| 344 |
+
T_t = build_text_class_prototypes(clip_text_model, device, cls_names_t, templates_single, batch_size=256)
|
| 345 |
+
ds_down = train_list[0]
|
| 346 |
+
V_t = build_visual_class_prototypes_subset(vision_model, device, ds_down, list(cls_ids), max_per_class=int(max_images_per_class), batch_size=int(vision_batch_size), preprocess_eval=preprocess_eval)
|
| 347 |
+
for j, nm in enumerate(upstream_names):
|
| 348 |
+
distances[t, j] = directed_covering_distance(T_t, V_t, T_up_list[j], V_up_list[j])
|
| 349 |
+
|
| 350 |
+
similarity = np.exp(-float(beta) * distances)
|
| 351 |
+
similarity = np.clip(similarity, 0.0, 1.0)
|
| 352 |
+
return similarity, upstream_names
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def main():
|
| 356 |
+
parser = argparse.ArgumentParser(description="Dataset similarity based on CLIP text and visual prototypes")
|
| 357 |
+
parser.add_argument("--dataset-root", type=str, default=os.environ.get("DATASET_ROOT", "data"))
|
| 358 |
+
parser.add_argument("--model-name", type=str, default="ViT-B/16")
|
| 359 |
+
parser.add_argument("--vision-batch-size", type=int, default=64)
|
| 360 |
+
parser.add_argument("--max-images-per-class", type=int, default=64, help="Maximum number of samples to use for each class when building visual prototypes. For efficiency, Chamfer distance is not computed using all samples in a dataset; instead, each class is sampled with at most this many examples.")
|
| 361 |
+
parser.add_argument("--beta", type=float, default=1.0, help="Similarity mapping exp(-beta * dist)")
|
| 362 |
+
parser.add_argument("--output", type=str, default="")
|
| 363 |
+
parser.add_argument("--downstream-dataset", type=str, default="", help="Name of downstream dataset (e.g., CIFAR100, PCam, FGVCAircraft, DescribableTextures)")
|
| 364 |
+
parser.add_argument("--cil-split", type=int, default=0, help="Number of CIL splits for downstream dataset; if >0, output [cil_split x 24] matrix")
|
| 365 |
+
args = parser.parse_args()
|
| 366 |
+
|
| 367 |
+
names = DEFAULT_NAMES_ORDER1
|
| 368 |
+
|
| 369 |
+
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
| 370 |
+
clip_model, _, preprocess_eval_clip = load_orig_clip(args.model_name, device=device, jit=False)
|
| 371 |
+
clip_model.eval()
|
| 372 |
+
|
| 373 |
+
vision_model = clip_model
|
| 374 |
+
preprocess_eval = preprocess_eval_clip
|
| 375 |
+
clip_text_model = clip_model
|
| 376 |
+
|
| 377 |
+
cfg = type("Cfg", (), {})()
|
| 378 |
+
cfg.dataset = "MTIL"
|
| 379 |
+
cfg.dataset_root = args.dataset_root
|
| 380 |
+
cfg.MTIL_order_2 = False
|
| 381 |
+
cfg.train_one_dataset = -1
|
| 382 |
+
cfg.seed = 32
|
| 383 |
+
cfg.use_validation = False
|
| 384 |
+
|
| 385 |
+
dataset_list, classes_names_list, templates_list, _ = get_mtil_dataset(
|
| 386 |
+
cfg, 'test', transforms=preprocess_eval
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
n = min(len(names), len(classes_names_list))
|
| 390 |
+
names = names[:n]
|
| 391 |
+
classes_names_list = classes_names_list[:n]
|
| 392 |
+
templates_list = templates_list[:n] if templates_list is not None else [None] * n
|
| 393 |
+
|
| 394 |
+
if args.downstream_dataset and int(args.cil_split) > 0:
|
| 395 |
+
repo_root = os.path.dirname(os.path.abspath(__file__))
|
| 396 |
+
alias_in = (args.downstream_dataset or '').strip()
|
| 397 |
+
alias_map = {
|
| 398 |
+
"Aircraft": "FGVCAircraft",
|
| 399 |
+
"FGVCAircraft": "FGVCAircraft",
|
| 400 |
+
"DTD": "DescribableTextures",
|
| 401 |
+
"DescribableTextures": "DescribableTextures",
|
| 402 |
+
}
|
| 403 |
+
ds_internal = alias_map.get(alias_in, alias_in) if alias_in else None
|
| 404 |
+
downstream_seed = resolve_downstream_seed(repo_root, ds_internal)
|
| 405 |
+
name_to_idx = {nm: idx for idx, nm in enumerate(names)}
|
| 406 |
+
if ds_internal not in name_to_idx:
|
| 407 |
+
raise ValueError(f"Downstream dataset '{args.downstream_dataset}' (mapped to '{ds_internal}') not found in MTIL names.")
|
| 408 |
+
cfg.seed = downstream_seed
|
| 409 |
+
dataset_list, classes_names_list, templates_list, _ = get_mtil_dataset(
|
| 410 |
+
cfg, 'test', transforms=preprocess_eval
|
| 411 |
+
)
|
| 412 |
+
n = min(len(names), len(classes_names_list))
|
| 413 |
+
names = names[:n]
|
| 414 |
+
classes_names_list = classes_names_list[:n]
|
| 415 |
+
templates_list = templates_list[:n] if templates_list is not None else [None] * n
|
| 416 |
+
upstream_names = [nm for nm in names if nm != ds_internal]
|
| 417 |
+
similarity_matrix, upstream_names = tasks_to_upstream_similarity_matrix(
|
| 418 |
+
vision_model, device,
|
| 419 |
+
upstream_names, name_to_idx,
|
| 420 |
+
dataset_list, classes_names_list, templates_list,
|
| 421 |
+
ds_internal, int(args.cil_split),
|
| 422 |
+
preprocess_eval,
|
| 423 |
+
int(args.max_images_per_class), int(args.vision_batch_size),
|
| 424 |
+
args.dataset_root, float(args.beta), clip_text_model=clip_text_model,
|
| 425 |
+
seed=downstream_seed,
|
| 426 |
+
)
|
| 427 |
+
print("DEFAULT_SIM_MATRIX = [")
|
| 428 |
+
fmt = "{:.3f}"
|
| 429 |
+
for i in range(similarity_matrix.shape[0]):
|
| 430 |
+
row_str = ", ".join(fmt.format(float(x)) for x in similarity_matrix[i])
|
| 431 |
+
print(f" [{row_str}],")
|
| 432 |
+
print("]")
|
| 433 |
+
names_py = ", ".join([f'"{n}"' for n in upstream_names])
|
| 434 |
+
print(f"DEFAULT_SIM_UPSTREAM_NAMES = [{names_py}]")
|
| 435 |
+
if args.output:
|
| 436 |
+
import json
|
| 437 |
+
out = {
|
| 438 |
+
"downstream": args.downstream_dataset,
|
| 439 |
+
"cil_split": int(args.cil_split),
|
| 440 |
+
"upstream_names": upstream_names,
|
| 441 |
+
"similarity_matrix": similarity_matrix.tolist(),
|
| 442 |
+
"beta": float(args.beta),
|
| 443 |
+
"seed": int(downstream_seed),
|
| 444 |
+
"max_images_per_class": int(args.max_images_per_class),
|
| 445 |
+
"vision_encoder": "clip",
|
| 446 |
+
"text_encoder": "clip",
|
| 447 |
+
}
|
| 448 |
+
base, ext = os.path.splitext(args.output)
|
| 449 |
+
out2 = base + "_sim" + ext
|
| 450 |
+
with open(out2, 'w', encoding='utf-8') as f:
|
| 451 |
+
json.dump(out, f, ensure_ascii=False, indent=2)
|
| 452 |
+
print(f"Saved similarity JSON to {out2}")
|
| 453 |
+
return
|
| 454 |
+
|
| 455 |
+
if __name__ == "__main__":
|
| 456 |
+
main()
|
class_orders/cifar100.yaml
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
class_order: [87, 0, 52, 58, 44, 91, 68, 97, 51, 15, 94, 92, 10, 72, 49, 78, 61, 14, 8, 86, 84, 96, 18, 24, 32, 45, 88, 11, 4, 67, 69, 66, 77, 47, 79, 93, 29, 50, 57, 83, 17, 81, 41, 12, 37, 59, 25, 20, 80, 73, 1, 28, 6, 46, 62, 82, 53, 9, 31, 75, 38, 63, 33, 74, 27, 22, 36, 3, 16, 21, 60, 19, 70, 90, 89, 43, 5, 42, 65, 76, 40, 30, 23, 85, 2, 95, 56, 48, 71, 64, 98, 13, 99, 7, 34, 55, 54, 26, 35, 39]
|
class_orders/imagenet100.yaml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class_order: [
|
| 2 |
+
68, 56, 78, 8, 23, 84, 90, 65, 74, 76, 40,
|
| 3 |
+
89, 3, 92, 55, 9, 26, 80, 43, 38, 58, 70,
|
| 4 |
+
77, 1, 85, 19, 17, 50, 28, 53, 13, 81, 45,
|
| 5 |
+
82, 6, 59, 83, 16, 15, 44, 91, 41, 72, 60,
|
| 6 |
+
79, 52, 20, 10, 31, 54, 37, 95, 14, 71, 96,
|
| 7 |
+
98, 97, 2, 64, 66, 42, 22, 35, 86, 24, 34,
|
| 8 |
+
87, 21, 99, 0, 88, 27, 18, 94, 11, 12, 47,
|
| 9 |
+
25, 30, 46, 62, 69, 36, 61, 7, 63, 75, 5, 32,
|
| 10 |
+
4, 51, 48, 73, 93, 39, 67, 29, 49, 57, 33
|
| 11 |
+
]
|
class_orders/imagenet1000.yaml
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class_order: [
|
| 2 |
+
54, 7, 894, 512, 126, 337, 988, 11, 284, 493, 133, 783, 192, 979, 622, 215, 240, 548, 238, 419, 274, 108,
|
| 3 |
+
928, 856, 494, 836, 473, 650, 85, 262, 508, 590, 390, 174, 637, 288, 658, 219, 912, 142, 852, 160, 704, 289,
|
| 4 |
+
123, 323, 600, 542, 999, 634, 391, 761, 490, 842, 127, 850, 665, 990, 597, 722, 748, 14, 77, 437, 394, 859,
|
| 5 |
+
279, 539, 75, 466, 886, 312, 303, 62, 966, 413, 959, 782, 509, 400, 471, 632, 275, 730, 105, 523, 224, 186,
|
| 6 |
+
478, 507, 470, 906, 699, 989, 324, 812, 260, 911, 446, 44, 765, 759, 67, 36, 5, 30, 184, 797, 159, 741, 954,
|
| 7 |
+
465, 533, 585, 150, 101, 897, 363, 818, 620, 824, 154, 956, 176, 588, 986, 172, 223, 461, 94, 141, 621, 659,
|
| 8 |
+
360, 136, 578, 163, 427, 70, 226, 925, 596, 336, 412, 731, 755, 381, 810, 69, 898, 310, 120, 752, 93, 39,
|
| 9 |
+
326, 537, 905, 448, 347, 51, 615, 601, 229, 947, 348, 220, 949, 972, 73, 913, 522, 193, 753, 921, 257, 957,
|
| 10 |
+
691, 155, 820, 584, 948, 92, 582, 89, 379, 392, 64, 904, 169, 216, 694, 103, 410, 374, 515, 484, 624, 409,
|
| 11 |
+
156, 455, 846, 344, 371, 468, 844, 276, 740, 562, 503, 831, 516, 663, 630, 763, 456, 179, 996, 936, 248,
|
| 12 |
+
333, 941, 63, 738, 802, 372, 828, 74, 540, 299, 750, 335, 177, 822, 643, 593, 800, 459, 580, 933, 306, 378,
|
| 13 |
+
76, 227, 426, 403, 322, 321, 808, 393, 27, 200, 764, 651, 244, 479, 3, 415, 23, 964, 671, 195, 569, 917,
|
| 14 |
+
611, 644, 707, 355, 855, 8, 534, 657, 571, 811, 681, 543, 313, 129, 978, 592, 573, 128, 243, 520, 887, 892,
|
| 15 |
+
696, 26, 551, 168, 71, 398, 778, 529, 526, 792, 868, 266, 443, 24, 57, 15, 871, 678, 745, 845, 208, 188,
|
| 16 |
+
674, 175, 406, 421, 833, 106, 994, 815, 581, 676, 49, 619, 217, 631, 934, 932, 568, 353, 863, 827, 425, 420,
|
| 17 |
+
99, 823, 113, 974, 438, 874, 343, 118, 340, 472, 552, 937, 0, 10, 675, 316, 879, 561, 387, 726, 255, 407,
|
| 18 |
+
56, 927, 655, 809, 839, 640, 297, 34, 497, 210, 606, 971, 589, 138, 263, 587, 993, 973, 382, 572, 735, 535,
|
| 19 |
+
139, 524, 314, 463, 895, 376, 939, 157, 858, 457, 935, 183, 114, 903, 767, 666, 22, 525, 902, 233, 250, 825,
|
| 20 |
+
79, 843, 221, 214, 205, 166, 431, 860, 292, 976, 739, 899, 475, 242, 961, 531, 110, 769, 55, 701, 532, 586,
|
| 21 |
+
729, 253, 486, 787, 774, 165, 627, 32, 291, 962, 922, 222, 705, 454, 356, 445, 746, 776, 404, 950, 241, 452,
|
| 22 |
+
245, 487, 706, 2, 137, 6, 98, 647, 50, 91, 202, 556, 38, 68, 649, 258, 345, 361, 464, 514, 958, 504, 826,
|
| 23 |
+
668, 880, 28, 920, 918, 339, 315, 320, 768, 201, 733, 575, 781, 864, 617, 171, 795, 132, 145, 368, 147, 327,
|
| 24 |
+
713, 688, 848, 690, 975, 354, 853, 148, 648, 300, 436, 780, 693, 682, 246, 449, 492, 162, 97, 59, 357, 198,
|
| 25 |
+
519, 90, 236, 375, 359, 230, 476, 784, 117, 940, 396, 849, 102, 122, 282, 181, 130, 467, 88, 271, 793, 151,
|
| 26 |
+
847, 914, 42, 834, 521, 121, 29, 806, 607, 510, 837, 301, 669, 78, 256, 474, 840, 52, 505, 547, 641, 987,
|
| 27 |
+
801, 629, 491, 605, 112, 429, 401, 742, 528, 87, 442, 910, 638, 785, 264, 711, 369, 428, 805, 744, 380, 725,
|
| 28 |
+
480, 318, 997, 153, 384, 252, 985, 538, 654, 388, 100, 432, 832, 565, 908, 367, 591, 294, 272, 231, 213,
|
| 29 |
+
196, 743, 817, 433, 328, 970, 969, 4, 613, 182, 685, 724, 915, 311, 931, 865, 86, 119, 203, 268, 718, 317,
|
| 30 |
+
926, 269, 161, 209, 807, 645, 513, 261, 518, 305, 758, 872, 58, 65, 146, 395, 481, 747, 41, 283, 204, 564,
|
| 31 |
+
185, 777, 33, 500, 609, 286, 567, 80, 228, 683, 757, 942, 134, 673, 616, 960, 450, 350, 544, 830, 736, 170,
|
| 32 |
+
679, 838, 819, 485, 430, 190, 566, 511, 482, 232, 527, 411, 560, 281, 342, 614, 662, 47, 771, 861, 692, 686,
|
| 33 |
+
277, 373, 16, 946, 265, 35, 9, 884, 909, 610, 358, 18, 737, 977, 677, 803, 595, 135, 458, 12, 46, 418, 599,
|
| 34 |
+
187, 107, 992, 770, 298, 104, 351, 893, 698, 929, 502, 273, 20, 96, 791, 636, 708, 267, 867, 772, 604, 618,
|
| 35 |
+
346, 330, 554, 816, 664, 716, 189, 31, 721, 712, 397, 43, 943, 804, 296, 109, 576, 869, 955, 17, 506, 963,
|
| 36 |
+
786, 720, 628, 779, 982, 633, 891, 734, 980, 386, 365, 794, 325, 841, 878, 370, 695, 293, 951, 66, 594, 717,
|
| 37 |
+
116, 488, 796, 983, 646, 499, 53, 1, 603, 45, 424, 875, 254, 237, 199, 414, 307, 362, 557, 866, 341, 19,
|
| 38 |
+
965, 143, 555, 687, 235, 790, 125, 173, 364, 882, 727, 728, 563, 495, 21, 558, 709, 719, 877, 352, 83, 998,
|
| 39 |
+
991, 469, 967, 760, 498, 814, 612, 715, 290, 72, 131, 259, 441, 924, 773, 48, 625, 501, 440, 82, 684, 862,
|
| 40 |
+
574, 309, 408, 680, 623, 439, 180, 652, 968, 889, 334, 61, 766, 399, 598, 798, 653, 930, 149, 249, 890, 308,
|
| 41 |
+
881, 40, 835, 577, 422, 703, 813, 857, 995, 602, 583, 167, 670, 212, 751, 496, 608, 84, 639, 579, 178, 489,
|
| 42 |
+
37, 197, 789, 530, 111, 876, 570, 700, 444, 287, 366, 883, 385, 536, 460, 851, 81, 144, 60, 251, 13, 953,
|
| 43 |
+
270, 944, 319, 885, 710, 952, 517, 278, 656, 919, 377, 550, 207, 660, 984, 447, 553, 338, 234, 383, 749,
|
| 44 |
+
916, 626, 462, 788, 434, 714, 799, 821, 477, 549, 661, 206, 667, 541, 642, 689, 194, 152, 981, 938, 854,
|
| 45 |
+
483, 332, 280, 546, 389, 405, 545, 239, 896, 672, 923, 402, 423, 907, 888, 140, 870, 559, 756, 25, 211, 158,
|
| 46 |
+
723, 635, 302, 702, 453, 218, 164, 829, 247, 775, 191, 732, 115, 331, 901, 416, 873, 754, 900, 435, 762,
|
| 47 |
+
124, 304, 329, 349, 295, 95, 451, 285, 225, 945, 697, 417
|
| 48 |
+
]
|
class_orders/tinyimagenet.yaml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class_order: [
|
| 2 |
+
131, 181, 22, 172, 144, 92, 97, 187, 58, 93, 6, 70, 106, 68,
|
| 3 |
+
153, 168, 179, 199, 29, 46, 9, 142, 134, 88, 193, 110, 26,
|
| 4 |
+
32, 117, 112, 17, 39, 166, 13, 94, 138, 109, 147, 51, 101,
|
| 5 |
+
59, 188, 116, 5, 170, 99, 100, 167, 180, 146, 65, 1, 104,
|
| 6 |
+
43, 38, 184, 123, 171, 137, 162, 71, 44, 95, 174, 12, 7,
|
| 7 |
+
54, 152, 21, 47, 28, 176, 34, 2, 132, 118, 42, 189, 150,
|
| 8 |
+
14, 165, 41, 192, 45, 82, 128, 63, 57, 197, 160, 53, 75,
|
| 9 |
+
108, 135, 121, 159, 183, 67, 169, 50, 87, 69, 89, 196,
|
| 10 |
+
115, 19, 148, 96, 86, 11, 8, 60, 33, 173, 78, 4, 119, 105,
|
| 11 |
+
182, 127, 177, 30, 186, 40, 49, 178, 76, 157, 161, 73, 164,
|
| 12 |
+
151, 31, 74, 191, 27, 125, 198, 81, 20, 155, 114, 139, 36,
|
| 13 |
+
61, 56, 145, 48, 16, 83, 62, 85, 126, 0, 102, 23, 3, 140,
|
| 14 |
+
15, 195, 133, 113, 190, 141, 52, 163, 156, 80, 111, 90, 175,
|
| 15 |
+
143, 120, 84, 18, 25, 79, 37, 154, 136, 64, 158, 24, 185,
|
| 16 |
+
72, 35, 129, 55, 149, 91, 122, 77, 103, 124, 130, 66, 10, 107, 194, 98
|
| 17 |
+
]
|
clip/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
This folder is a lightly modified version of https://github.com/openai/CLIP.
|
clip/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .clip import *
|
clip/adapter.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --------------------------------------------------------
|
| 2 |
+
# References:
|
| 3 |
+
# https://github.com/jxhe/unify-parameter-efficient-tuning
|
| 4 |
+
# --------------------------------------------------------
|
| 5 |
+
|
| 6 |
+
import math
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn as nn
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Adapter(nn.Module):
|
| 12 |
+
def __init__(self,
|
| 13 |
+
d_model=None,
|
| 14 |
+
bottleneck=None,
|
| 15 |
+
dropout=0.0,
|
| 16 |
+
init_option="lora",
|
| 17 |
+
adapter_scalar="1.0",
|
| 18 |
+
adapter_layernorm_option="in"):
|
| 19 |
+
super().__init__()
|
| 20 |
+
self.n_embd = d_model if d_model is None else d_model
|
| 21 |
+
self.down_size = bottleneck
|
| 22 |
+
|
| 23 |
+
#_before
|
| 24 |
+
self.adapter_layernorm_option = adapter_layernorm_option
|
| 25 |
+
|
| 26 |
+
self.adapter_layer_norm_before = None
|
| 27 |
+
if adapter_layernorm_option == "in" or adapter_layernorm_option == "out":
|
| 28 |
+
self.adapter_layer_norm_before = nn.LayerNorm(self.n_embd)
|
| 29 |
+
|
| 30 |
+
if adapter_scalar == "learnable_scalar":
|
| 31 |
+
self.scale = nn.Parameter(torch.ones(1))
|
| 32 |
+
else:
|
| 33 |
+
self.scale = float(adapter_scalar)
|
| 34 |
+
|
| 35 |
+
self.down_proj = nn.Linear(self.n_embd, 64)
|
| 36 |
+
self.non_linear_func = nn.ReLU()
|
| 37 |
+
self.up_proj = nn.Linear(self.down_size, self.n_embd)
|
| 38 |
+
|
| 39 |
+
self.dropout = dropout
|
| 40 |
+
if init_option == "bert":
|
| 41 |
+
raise NotImplementedError
|
| 42 |
+
elif init_option == "lora":
|
| 43 |
+
with torch.no_grad():
|
| 44 |
+
nn.init.kaiming_uniform_(self.down_proj.weight, a=math.sqrt(5))
|
| 45 |
+
nn.init.zeros_(self.up_proj.weight)
|
| 46 |
+
nn.init.zeros_(self.down_proj.bias)
|
| 47 |
+
nn.init.zeros_(self.up_proj.bias)
|
| 48 |
+
|
| 49 |
+
def forward(self, x, add_residual=True, residual=None):
|
| 50 |
+
|
| 51 |
+
residual = x if residual is None else residual
|
| 52 |
+
if self.adapter_layernorm_option == 'in': # none
|
| 53 |
+
x = self.adapter_layer_norm_before(x)
|
| 54 |
+
|
| 55 |
+
down = self.down_proj(x)
|
| 56 |
+
down = self.non_linear_func(down)
|
| 57 |
+
down = nn.functional.dropout(down, p=self.dropout, training=self.training)
|
| 58 |
+
up = self.up_proj(down)
|
| 59 |
+
|
| 60 |
+
up = up * self.scale
|
| 61 |
+
|
| 62 |
+
if self.adapter_layernorm_option == 'out': # none
|
| 63 |
+
up = self.adapter_layer_norm_before(up)
|
| 64 |
+
|
| 65 |
+
if add_residual:
|
| 66 |
+
output = up + residual
|
| 67 |
+
else:
|
| 68 |
+
output = up
|
| 69 |
+
return output
|
clip/bpe_simple_vocab_16e6.txt.gz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:924691ac288e54409236115652ad4aa250f48203de50a9e4722a6ecd48d6804a
|
| 3 |
+
size 1356917
|
clip/clip.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Code ported from https://github.com/openai/CLIP
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import os
|
| 5 |
+
import urllib
|
| 6 |
+
import warnings
|
| 7 |
+
from typing import Union, List
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from torchvision.transforms import Compose, Resize, CenterCrop, ToTensor, Normalize, RandomResizedCrop, InterpolationMode
|
| 11 |
+
from tqdm import tqdm
|
| 12 |
+
|
| 13 |
+
from clip.model import build_model
|
| 14 |
+
from clip.tokenizer import SimpleTokenizer as _Tokenizer
|
| 15 |
+
|
| 16 |
+
__all__ = ["available_models", "load", "tokenize"]
|
| 17 |
+
_tokenizer = _Tokenizer()
|
| 18 |
+
|
| 19 |
+
_MODELS = {
|
| 20 |
+
"RN50": "https://openaipublic.azureedge.net/clip/models/afeb0e10f9e5a86da6080e35cf09123aca3b358a0c3e3b6c78a7b63bc04b6762/RN50.pt",
|
| 21 |
+
"RN101": "https://openaipublic.azureedge.net/clip/models/8fa8567bab74a42d41c5915025a8e4538c3bdbe8804a470a72f30b0d94fab599/RN101.pt",
|
| 22 |
+
"RN50x4": "https://openaipublic.azureedge.net/clip/models/7e526bd135e493cef0776de27d5f42653e6b4c8bf9e0f653bb11773263205fdd/RN50x4.pt",
|
| 23 |
+
"RN50x16": "https://openaipublic.azureedge.net/clip/models/52378b407f34354e150460fe41077663dd5b39c54cd0bfd2b27167a4a06ec9aa/RN50x16.pt",
|
| 24 |
+
"ViT-B/32": "https://openaipublic.azureedge.net/clip/models/40d365715913c9da98579312b702a82c18be219cc2a73407c4526f58eba950af/ViT-B-32.pt",
|
| 25 |
+
"ViT-B/16": "https://openaipublic.azureedge.net/clip/models/5806e77cd80f8b59890b7e101eabd078d9fb84e6937f9e85e4ecb61988df416f/ViT-B-16.pt",
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _download(url: str, root: str = os.path.expanduser("~/.cache/clip")):
|
| 30 |
+
os.makedirs(root, exist_ok=True)
|
| 31 |
+
filename = os.path.basename(url)
|
| 32 |
+
|
| 33 |
+
expected_sha256 = url.split("/")[-2]
|
| 34 |
+
download_target = os.path.join(root, filename)
|
| 35 |
+
|
| 36 |
+
if os.path.exists(download_target) and not os.path.isfile(download_target):
|
| 37 |
+
raise RuntimeError(f"{download_target} exists and is not a regular file")
|
| 38 |
+
|
| 39 |
+
if os.path.isfile(download_target):
|
| 40 |
+
if hashlib.sha256(open(download_target, "rb").read()).hexdigest() == expected_sha256:
|
| 41 |
+
return download_target
|
| 42 |
+
else:
|
| 43 |
+
warnings.warn(f"{download_target} exists, but the SHA256 checksum does not match; re-downloading the file")
|
| 44 |
+
|
| 45 |
+
with urllib.request.urlopen(url) as source, open(download_target, "wb") as output:
|
| 46 |
+
with tqdm(total=int(source.info().get("Content-Length")), ncols=80, unit='iB', unit_scale=True) as loop:
|
| 47 |
+
while True:
|
| 48 |
+
buffer = source.read(8192)
|
| 49 |
+
if not buffer:
|
| 50 |
+
break
|
| 51 |
+
|
| 52 |
+
output.write(buffer)
|
| 53 |
+
loop.update(len(buffer))
|
| 54 |
+
|
| 55 |
+
if hashlib.sha256(open(download_target, "rb").read()).hexdigest() != expected_sha256:
|
| 56 |
+
raise RuntimeError(f"Model has been downloaded but the SHA256 checksum does not not match")
|
| 57 |
+
|
| 58 |
+
return download_target
|
| 59 |
+
|
| 60 |
+
def _convert_to_rgb(image):
|
| 61 |
+
return image.convert('RGB')
|
| 62 |
+
|
| 63 |
+
def _transform(n_px: int, is_train: bool):
|
| 64 |
+
normalize = Normalize((0.48145466, 0.4578275, 0.40821073), (0.26862954, 0.26130258, 0.27577711))
|
| 65 |
+
if is_train:
|
| 66 |
+
return Compose([
|
| 67 |
+
RandomResizedCrop(n_px, scale=(0.9, 1.0), interpolation=InterpolationMode.BICUBIC),
|
| 68 |
+
_convert_to_rgb,
|
| 69 |
+
ToTensor(),
|
| 70 |
+
normalize,
|
| 71 |
+
])
|
| 72 |
+
else:
|
| 73 |
+
return Compose([
|
| 74 |
+
Resize(n_px, interpolation=InterpolationMode.BICUBIC),
|
| 75 |
+
CenterCrop(n_px),
|
| 76 |
+
_convert_to_rgb,
|
| 77 |
+
ToTensor(),
|
| 78 |
+
normalize,
|
| 79 |
+
])
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def available_models() -> List[str]:
|
| 84 |
+
"""Returns the names of available CLIP models"""
|
| 85 |
+
return list(_MODELS.keys())
|
| 86 |
+
|
| 87 |
+
# def load(name: str, device: Union[str, torch.device] = "cuda" if torch.cuda.is_available() else "cpu", jit: bool = False, download_root: str = None):
|
| 88 |
+
# """Load a CLIP model
|
| 89 |
+
|
| 90 |
+
# Parameters
|
| 91 |
+
# ----------
|
| 92 |
+
# name : str
|
| 93 |
+
# A model name listed by `clip.available_models()`, or the path to a model checkpoint containing the state_dict
|
| 94 |
+
|
| 95 |
+
# device : Union[str, torch.device]
|
| 96 |
+
# The device to put the loaded model
|
| 97 |
+
|
| 98 |
+
# jit : bool
|
| 99 |
+
# Whether to load the optimized JIT model or more hackable non-JIT model (default).
|
| 100 |
+
|
| 101 |
+
# download_root: str
|
| 102 |
+
# path to download the model files; by default, it uses "~/.cache/clip"
|
| 103 |
+
|
| 104 |
+
# Returns
|
| 105 |
+
# -------
|
| 106 |
+
# model : torch.nn.Module
|
| 107 |
+
# The CLIP model
|
| 108 |
+
|
| 109 |
+
# preprocess : Callable[[PIL.Image], torch.Tensor]
|
| 110 |
+
# A torchvision transform that converts a PIL image into a tensor that the returned model can take as its input
|
| 111 |
+
# """
|
| 112 |
+
# if name in _MODELS:
|
| 113 |
+
# model_path = _download(_MODELS[name], download_root or os.path.expanduser("~/.cache/clip"))
|
| 114 |
+
# elif os.path.isfile(name):
|
| 115 |
+
# model_path = name
|
| 116 |
+
# else:
|
| 117 |
+
# raise RuntimeError(f"Model {name} not found; available models = {available_models()}")
|
| 118 |
+
|
| 119 |
+
# try:
|
| 120 |
+
# # loading JIT archive
|
| 121 |
+
# model = torch.jit.load(model_path, map_location=device if jit else "cpu").eval()
|
| 122 |
+
# state_dict = None
|
| 123 |
+
# except RuntimeError:
|
| 124 |
+
# # loading saved state dict
|
| 125 |
+
# if jit:
|
| 126 |
+
# warnings.warn(f"File {model_path} is not a JIT archive. Loading as a state dict instead")
|
| 127 |
+
# jit = False
|
| 128 |
+
# state_dict = torch.load(model_path, map_location="cpu")
|
| 129 |
+
|
| 130 |
+
# if not jit:
|
| 131 |
+
# model = build_model(state_dict or model.state_dict()).to(device)
|
| 132 |
+
# if str(device) == "cpu":
|
| 133 |
+
# model.float()
|
| 134 |
+
# return model, _transform(model.visual.input_resolution)
|
| 135 |
+
|
| 136 |
+
# # patch the device names
|
| 137 |
+
# device_holder = torch.jit.trace(lambda: torch.ones([]).to(torch.device(device)), example_inputs=[])
|
| 138 |
+
# device_node = [n for n in device_holder.graph.findAllNodes("prim::Constant") if "Device" in repr(n)][-1]
|
| 139 |
+
|
| 140 |
+
# def patch_device(module):
|
| 141 |
+
# try:
|
| 142 |
+
# graphs = [module.graph] if hasattr(module, "graph") else []
|
| 143 |
+
# except RuntimeError:
|
| 144 |
+
# graphs = []
|
| 145 |
+
|
| 146 |
+
# if hasattr(module, "forward1"):
|
| 147 |
+
# graphs.append(module.forward1.graph)
|
| 148 |
+
|
| 149 |
+
# for graph in graphs:
|
| 150 |
+
# for node in graph.findAllNodes("prim::Constant"):
|
| 151 |
+
# if "value" in node.attributeNames() and str(node["value"]).startswith("cuda"):
|
| 152 |
+
# node.copyAttributes(device_node)
|
| 153 |
+
|
| 154 |
+
# model.apply(patch_device)
|
| 155 |
+
# patch_device(model.encode_image)
|
| 156 |
+
# patch_device(model.encode_text)
|
| 157 |
+
|
| 158 |
+
# # patch dtype to float32 on CPU
|
| 159 |
+
# if str(device) == "cpu":
|
| 160 |
+
# float_holder = torch.jit.trace(lambda: torch.ones([]).float(), example_inputs=[])
|
| 161 |
+
# float_input = list(float_holder.graph.findNode("aten::to").inputs())[1]
|
| 162 |
+
# float_node = float_input.node()
|
| 163 |
+
|
| 164 |
+
# def patch_float(module):
|
| 165 |
+
# try:
|
| 166 |
+
# graphs = [module.graph] if hasattr(module, "graph") else []
|
| 167 |
+
# except RuntimeError:
|
| 168 |
+
# graphs = []
|
| 169 |
+
|
| 170 |
+
# if hasattr(module, "forward1"):
|
| 171 |
+
# graphs.append(module.forward1.graph)
|
| 172 |
+
|
| 173 |
+
# for graph in graphs:
|
| 174 |
+
# for node in graph.findAllNodes("aten::to"):
|
| 175 |
+
# inputs = list(node.inputs())
|
| 176 |
+
# for i in [1, 2]: # dtype can be the second or third argument to aten::to()
|
| 177 |
+
# if inputs[i].node()["value"] == 5:
|
| 178 |
+
# inputs[i].node().copyAttributes(float_node)
|
| 179 |
+
|
| 180 |
+
# model.apply(patch_float)
|
| 181 |
+
# patch_float(model.encode_image)
|
| 182 |
+
# patch_float(model.encode_text)
|
| 183 |
+
|
| 184 |
+
# model.float()
|
| 185 |
+
|
| 186 |
+
# return model, _transform(model.input_resolution.item())
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def load(name: str, device: Union[str, torch.device] = "cuda" if torch.cuda.is_available() else "cpu", jit=True, is_train=False, pretrained=True):
|
| 190 |
+
"""Load a CLIP model
|
| 191 |
+
Parameters
|
| 192 |
+
----------
|
| 193 |
+
name : str
|
| 194 |
+
A model name listed by `clip.available_models()`, or the path to a model checkpoint containing the state_dict
|
| 195 |
+
device : Union[str, torch.device]
|
| 196 |
+
The device to put the loaded model
|
| 197 |
+
jit : bool
|
| 198 |
+
Whether to load the optimized JIT model (default) or more hackable non-JIT model.
|
| 199 |
+
Returns
|
| 200 |
+
-------
|
| 201 |
+
model : torch.nn.Module
|
| 202 |
+
The CLIP model
|
| 203 |
+
preprocess : Callable[[PIL.Image], torch.Tensor]
|
| 204 |
+
A torchvision transform that converts a PIL image into a tensor that the returned model can take as its input
|
| 205 |
+
"""
|
| 206 |
+
if name in _MODELS:
|
| 207 |
+
model_path = _download(_MODELS[name])
|
| 208 |
+
elif os.path.isfile(name):
|
| 209 |
+
model_path = name
|
| 210 |
+
else:
|
| 211 |
+
raise RuntimeError(f"Model {name} not found; available models = {available_models()}")
|
| 212 |
+
|
| 213 |
+
try:
|
| 214 |
+
# loading JIT archive
|
| 215 |
+
model = torch.jit.load(model_path, map_location=device if jit else "cpu").eval()
|
| 216 |
+
state_dict = None
|
| 217 |
+
except RuntimeError:
|
| 218 |
+
# loading saved state dict
|
| 219 |
+
if jit:
|
| 220 |
+
warnings.warn(f"File {model_path} is not a JIT archive. Loading as a state dict instead")
|
| 221 |
+
jit = False
|
| 222 |
+
state_dict = torch.load(model_path, map_location="cpu")
|
| 223 |
+
|
| 224 |
+
if not jit:
|
| 225 |
+
try:
|
| 226 |
+
model = build_model(state_dict or model.state_dict()).to(device)
|
| 227 |
+
except KeyError:
|
| 228 |
+
sd = {k[7:]: v for k,v in state_dict["state_dict"].items()}
|
| 229 |
+
model = build_model(sd).to(device)
|
| 230 |
+
|
| 231 |
+
if str(device) == "cpu":
|
| 232 |
+
model.float()
|
| 233 |
+
return model, \
|
| 234 |
+
_transform(model.visual.input_resolution, is_train=True), \
|
| 235 |
+
_transform(model.visual.input_resolution, is_train=False)
|
| 236 |
+
|
| 237 |
+
# patch the device names
|
| 238 |
+
device_holder = torch.jit.trace(lambda: torch.ones([]).to(torch.device(device)), example_inputs=[])
|
| 239 |
+
device_node = [n for n in device_holder.graph.findAllNodes("prim::Constant") if "Device" in repr(n)][-1]
|
| 240 |
+
|
| 241 |
+
def patch_device(module):
|
| 242 |
+
graphs = [module.graph] if hasattr(module, "graph") else []
|
| 243 |
+
if hasattr(module, "forward1"):
|
| 244 |
+
graphs.append(module.forward1.graph)
|
| 245 |
+
|
| 246 |
+
for graph in graphs:
|
| 247 |
+
for node in graph.findAllNodes("prim::Constant"):
|
| 248 |
+
if "value" in node.attributeNames() and str(node["value"]).startswith("cuda"):
|
| 249 |
+
node.copyAttributes(device_node)
|
| 250 |
+
|
| 251 |
+
model.apply(patch_device)
|
| 252 |
+
patch_device(model.encode_image)
|
| 253 |
+
patch_device(model.encode_text)
|
| 254 |
+
|
| 255 |
+
# patch dtype to float32 on CPU
|
| 256 |
+
if str(device) == "cpu":
|
| 257 |
+
float_holder = torch.jit.trace(lambda: torch.ones([]).float(), example_inputs=[])
|
| 258 |
+
float_input = list(float_holder.graph.findNode("aten::to").inputs())[1]
|
| 259 |
+
float_node = float_input.node()
|
| 260 |
+
|
| 261 |
+
def patch_float(module):
|
| 262 |
+
graphs = [module.graph] if hasattr(module, "graph") else []
|
| 263 |
+
if hasattr(module, "forward1"):
|
| 264 |
+
graphs.append(module.forward1.graph)
|
| 265 |
+
|
| 266 |
+
for graph in graphs:
|
| 267 |
+
for node in graph.findAllNodes("aten::to"):
|
| 268 |
+
inputs = list(node.inputs())
|
| 269 |
+
for i in [1, 2]: # dtype can be the second or third argument to aten::to()
|
| 270 |
+
if inputs[i].node()["value"] == 5:
|
| 271 |
+
inputs[i].node().copyAttributes(float_node)
|
| 272 |
+
|
| 273 |
+
model.apply(patch_float)
|
| 274 |
+
patch_float(model.encode_image)
|
| 275 |
+
patch_float(model.encode_text)
|
| 276 |
+
|
| 277 |
+
model.float()
|
| 278 |
+
|
| 279 |
+
return model, \
|
| 280 |
+
_transform(model.input_resolution.item(), is_train=True), \
|
| 281 |
+
_transform(model.input_resolution.item(), is_train=False)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def tokenize(texts: Union[str, List[str]], context_length: int = 77) -> torch.LongTensor:
|
| 285 |
+
"""
|
| 286 |
+
Returns the tokenized representation of given input string(s)
|
| 287 |
+
Parameters
|
| 288 |
+
----------
|
| 289 |
+
texts : Union[str, List[str]]
|
| 290 |
+
An input string or a list of input strings to tokenize
|
| 291 |
+
context_length : int
|
| 292 |
+
The context length to use; all CLIP models use 77 as the context length
|
| 293 |
+
Returns
|
| 294 |
+
-------
|
| 295 |
+
A two-dimensional tensor containing the resulting tokens, shape = [number of input strings, context_length]
|
| 296 |
+
"""
|
| 297 |
+
if isinstance(texts, str):
|
| 298 |
+
texts = [texts]
|
| 299 |
+
|
| 300 |
+
sot_token = _tokenizer.encoder["<start_of_text>"]
|
| 301 |
+
eot_token = _tokenizer.encoder["<end_of_text>"]
|
| 302 |
+
all_tokens = [[sot_token] + _tokenizer.encode(text) + [eot_token] for text in texts]
|
| 303 |
+
result = torch.zeros(len(all_tokens), context_length, dtype=torch.long)
|
| 304 |
+
|
| 305 |
+
for i, tokens in enumerate(all_tokens):
|
| 306 |
+
if len(tokens) > context_length: # Truncate
|
| 307 |
+
tokens = tokens[:context_length]
|
| 308 |
+
result[i, :len(tokens)] = torch.tensor(tokens)
|
| 309 |
+
|
| 310 |
+
return result
|
clip/model.py
ADDED
|
@@ -0,0 +1,713 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections import OrderedDict
|
| 2 |
+
from typing import Tuple, Union
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import json
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn.functional as F
|
| 9 |
+
from torch import nn
|
| 10 |
+
from .adapter import Adapter
|
| 11 |
+
from torch.distributions.normal import Normal
|
| 12 |
+
from collections import Counter
|
| 13 |
+
|
| 14 |
+
global_taskid = 0
|
| 15 |
+
global_is_train=True
|
| 16 |
+
class SparseDispatcher(object):
|
| 17 |
+
"""Helper for implementing a mixture of experts.
|
| 18 |
+
The purpose of this class is to create input minibatches for the
|
| 19 |
+
experts and to combine the results of the experts to form a unified
|
| 20 |
+
output tensor.
|
| 21 |
+
There are two functions:
|
| 22 |
+
dispatch - take an input Tensor and create input Tensors for each expert.
|
| 23 |
+
combine - take output Tensors from each expert and form a combined output
|
| 24 |
+
Tensor. Outputs from different experts for the same batch element are
|
| 25 |
+
summed together, weighted by the provided "gates".
|
| 26 |
+
The class is initialized with a "gates" Tensor, which specifies which
|
| 27 |
+
batch elements go to which experts, and the weights to use when combining
|
| 28 |
+
the outputs. Batch element b is sent to expert e iff gates[b, e] != 0.
|
| 29 |
+
The inputs and outputs are all two-dimensional [batch, depth].
|
| 30 |
+
Caller is responsible for collapsing additional dimensions prior to
|
| 31 |
+
calling this class and reshaping the output to the original shape.
|
| 32 |
+
See common_layers.reshape_like().
|
| 33 |
+
Example use:
|
| 34 |
+
gates: a float32 `Tensor` with shape `[batch_size, num_experts]`
|
| 35 |
+
inputs: a float32 `Tensor` with shape `[batch_size, input_size]`
|
| 36 |
+
experts: a list of length `num_experts` containing sub-networks.
|
| 37 |
+
dispatcher = SparseDispatcher(num_experts, gates)
|
| 38 |
+
expert_inputs = dispatcher.dispatch(inputs)
|
| 39 |
+
expert_outputs = [experts[i](expert_inputs[i]) for i in range(num_experts)]
|
| 40 |
+
outputs = dispatcher.combine(expert_outputs)
|
| 41 |
+
The preceding code sets the output for a particular example b to:
|
| 42 |
+
output[b] = Sum_i(gates[b, i] * experts[i](inputs[b]))
|
| 43 |
+
This class takes advantage of sparsity in the gate matrix by including in the
|
| 44 |
+
`Tensor`s for expert i only the batch elements for which `gates[b, i] > 0`.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
def __init__(self, num_experts, gates):
|
| 48 |
+
"""Create a SparseDispatcher."""
|
| 49 |
+
|
| 50 |
+
self._gates = gates
|
| 51 |
+
self._num_experts = num_experts
|
| 52 |
+
|
| 53 |
+
sorted_experts, index_sorted_experts = torch.nonzero(gates).sort(0)
|
| 54 |
+
|
| 55 |
+
# drop indices
|
| 56 |
+
_, self._expert_index = sorted_experts.split(1, dim=1)
|
| 57 |
+
# get according batch index for each expert
|
| 58 |
+
self._batch_index = torch.nonzero(gates)[index_sorted_experts[:, 1], 0]
|
| 59 |
+
# calculate num samples that each expert gets
|
| 60 |
+
self._part_sizes = (gates > 0).sum(0).tolist()
|
| 61 |
+
# expand gates to match with self._batch_index
|
| 62 |
+
gates_exp = gates[self._batch_index.flatten()]
|
| 63 |
+
self._nonzero_gates = torch.gather(gates_exp, 1, self._expert_index)
|
| 64 |
+
|
| 65 |
+
def dispatch(self, inp):
|
| 66 |
+
"""Create one input Tensor for each expert.
|
| 67 |
+
The `Tensor` for a expert `i` contains the slices of `inp` corresponding
|
| 68 |
+
to the batch elements `b` where `gates[b, i] > 0`.
|
| 69 |
+
Args:
|
| 70 |
+
inp: a `Tensor` of shape "[batch_size, <extra_input_dims>]`
|
| 71 |
+
Returns:
|
| 72 |
+
a list of `num_experts` `Tensor`s with shapes
|
| 73 |
+
`[expert_batch_size_i, <extra_input_dims>]`.
|
| 74 |
+
"""
|
| 75 |
+
|
| 76 |
+
# assigns samples to experts whose gate is nonzero
|
| 77 |
+
|
| 78 |
+
inp_exp = inp[self._batch_index].squeeze(1)
|
| 79 |
+
return torch.split(inp_exp, self._part_sizes, dim=0)
|
| 80 |
+
|
| 81 |
+
def combine(self, expert_out, multiply_by_gates=True):
|
| 82 |
+
"""Sum together the expert output, weighted by the gates.
|
| 83 |
+
The slice corresponding to a particular batch element `b` is computed
|
| 84 |
+
as the sum over all experts `i` of the expert output, weighted by the
|
| 85 |
+
corresponding gate values. If `multiply_by_gates` is set to False, the
|
| 86 |
+
gate values are ignored.
|
| 87 |
+
Args:
|
| 88 |
+
expert_out: a list of `num_experts` `Tensor`s, each with shape
|
| 89 |
+
`[expert_batch_size_i, <extra_output_dims>]`.
|
| 90 |
+
multiply_by_gates: a boolean
|
| 91 |
+
Returns:
|
| 92 |
+
a `Tensor` with shape `[batch_size, <extra_output_dims>]`.
|
| 93 |
+
"""
|
| 94 |
+
# apply exp to expert outputs, so we are not longer in log space
|
| 95 |
+
|
| 96 |
+
stitched = torch.cat(expert_out, 0)
|
| 97 |
+
if multiply_by_gates:
|
| 98 |
+
stitched = stitched.mul(self._nonzero_gates) # 加权
|
| 99 |
+
|
| 100 |
+
zeros = torch.zeros(self._gates.size(0), expert_out[-1].size(1), device=stitched.device)
|
| 101 |
+
# combine samples that have been processed by the same k experts
|
| 102 |
+
|
| 103 |
+
combined = zeros.index_add(0, self._batch_index, stitched.float())
|
| 104 |
+
# add eps to all zero values in order to avoid nans when going back to log space
|
| 105 |
+
# back to log space
|
| 106 |
+
return combined
|
| 107 |
+
|
| 108 |
+
def expert_to_gates(self):
|
| 109 |
+
"""Gate values corresponding to the examples in the per-expert `Tensor`s.
|
| 110 |
+
Returns:
|
| 111 |
+
a list of `num_experts` one-dimensional `Tensor`s with type `tf.float32`
|
| 112 |
+
and shapes `[expert_batch_size_i]`
|
| 113 |
+
"""
|
| 114 |
+
# split nonzero gates for each expert
|
| 115 |
+
return torch.split(self._nonzero_gates, self._part_sizes, dim=0)
|
| 116 |
+
|
| 117 |
+
class Bottleneck(nn.Module):
|
| 118 |
+
expansion = 4
|
| 119 |
+
|
| 120 |
+
def __init__(self, inplanes, planes, stride=1):
|
| 121 |
+
super().__init__()
|
| 122 |
+
|
| 123 |
+
# all conv layers have stride 1. an avgpool is performed after the second convolution when stride > 1
|
| 124 |
+
self.conv1 = nn.Conv2d(inplanes, planes, 1, bias=False)
|
| 125 |
+
self.bn1 = nn.BatchNorm2d(planes)
|
| 126 |
+
|
| 127 |
+
self.conv2 = nn.Conv2d(planes, planes, 3, padding=1, bias=False)
|
| 128 |
+
self.bn2 = nn.BatchNorm2d(planes)
|
| 129 |
+
|
| 130 |
+
self.avgpool = nn.AvgPool2d(stride) if stride > 1 else nn.Identity()
|
| 131 |
+
|
| 132 |
+
self.conv3 = nn.Conv2d(planes, planes * self.expansion, 1, bias=False)
|
| 133 |
+
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
|
| 134 |
+
|
| 135 |
+
self.relu = nn.ReLU(inplace=True)
|
| 136 |
+
self.downsample = None
|
| 137 |
+
self.stride = stride
|
| 138 |
+
|
| 139 |
+
if stride > 1 or inplanes != planes * Bottleneck.expansion:
|
| 140 |
+
# downsampling layer is prepended with an avgpool, and the subsequent convolution has stride 1
|
| 141 |
+
self.downsample = nn.Sequential(OrderedDict([
|
| 142 |
+
("-1", nn.AvgPool2d(stride)),
|
| 143 |
+
("0", nn.Conv2d(inplanes, planes * self.expansion, 1, stride=1, bias=False)),
|
| 144 |
+
("1", nn.BatchNorm2d(planes * self.expansion))
|
| 145 |
+
]))
|
| 146 |
+
|
| 147 |
+
def forward(self, x: torch.Tensor):
|
| 148 |
+
identity = x
|
| 149 |
+
|
| 150 |
+
out = self.relu(self.bn1(self.conv1(x)))
|
| 151 |
+
out = self.relu(self.bn2(self.conv2(out)))
|
| 152 |
+
out = self.avgpool(out)
|
| 153 |
+
out = self.bn3(self.conv3(out))
|
| 154 |
+
|
| 155 |
+
if self.downsample is not None:
|
| 156 |
+
identity = self.downsample(x)
|
| 157 |
+
|
| 158 |
+
out += identity
|
| 159 |
+
out = self.relu(out)
|
| 160 |
+
return out
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
class AttentionPool2d(nn.Module):
|
| 164 |
+
def __init__(self, spacial_dim: int, embed_dim: int, num_heads: int, output_dim: int = None):
|
| 165 |
+
super().__init__()
|
| 166 |
+
self.positional_embedding = nn.Parameter(torch.randn(spacial_dim ** 2 + 1, embed_dim) / embed_dim ** 0.5)
|
| 167 |
+
self.k_proj = nn.Linear(embed_dim, embed_dim)
|
| 168 |
+
self.q_proj = nn.Linear(embed_dim, embed_dim)
|
| 169 |
+
self.v_proj = nn.Linear(embed_dim, embed_dim)
|
| 170 |
+
self.c_proj = nn.Linear(embed_dim, output_dim or embed_dim)
|
| 171 |
+
self.num_heads = num_heads
|
| 172 |
+
|
| 173 |
+
def forward(self, x):
|
| 174 |
+
x = x.reshape(x.shape[0], x.shape[1], x.shape[2] * x.shape[3]).permute(2, 0, 1) # NCHW -> (HW)NC
|
| 175 |
+
x = torch.cat([x.mean(dim=0, keepdim=True), x], dim=0) # (HW+1)NC
|
| 176 |
+
x = x + self.positional_embedding[:, None, :].to(x.dtype) # (HW+1)NC
|
| 177 |
+
x, _ = F.multi_head_attention_forward(
|
| 178 |
+
query=x, key=x, value=x,
|
| 179 |
+
embed_dim_to_check=x.shape[-1],
|
| 180 |
+
num_heads=self.num_heads,
|
| 181 |
+
q_proj_weight=self.q_proj.weight,
|
| 182 |
+
k_proj_weight=self.k_proj.weight,
|
| 183 |
+
v_proj_weight=self.v_proj.weight,
|
| 184 |
+
in_proj_weight=None,
|
| 185 |
+
in_proj_bias=torch.cat([self.q_proj.bias, self.k_proj.bias, self.v_proj.bias]),
|
| 186 |
+
bias_k=None,
|
| 187 |
+
bias_v=None,
|
| 188 |
+
add_zero_attn=False,
|
| 189 |
+
dropout_p=0,
|
| 190 |
+
out_proj_weight=self.c_proj.weight,
|
| 191 |
+
out_proj_bias=self.c_proj.bias,
|
| 192 |
+
use_separate_proj_weight=True,
|
| 193 |
+
training=self.training,
|
| 194 |
+
need_weights=False
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
return x[0]
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
class ModifiedResNet(nn.Module):
|
| 201 |
+
"""
|
| 202 |
+
A ResNet class that is similar to torchvision's but contains the following changes:
|
| 203 |
+
- There are now 3 "stem" convolutions as opposed to 1, with an average pool instead of a max pool.
|
| 204 |
+
- Performs anti-aliasing strided convolutions, where an avgpool is prepended to convolutions with stride > 1
|
| 205 |
+
- The final pooling layer is a QKV attention instead of an average pool
|
| 206 |
+
"""
|
| 207 |
+
|
| 208 |
+
def __init__(self, layers, output_dim, heads, input_resolution=224, width=64):
|
| 209 |
+
super().__init__()
|
| 210 |
+
self.output_dim = output_dim
|
| 211 |
+
self.input_resolution = input_resolution
|
| 212 |
+
|
| 213 |
+
# the 3-layer stem
|
| 214 |
+
self.conv1 = nn.Conv2d(3, width // 2, kernel_size=3, stride=2, padding=1, bias=False)
|
| 215 |
+
self.bn1 = nn.BatchNorm2d(width // 2)
|
| 216 |
+
self.conv2 = nn.Conv2d(width // 2, width // 2, kernel_size=3, padding=1, bias=False)
|
| 217 |
+
self.bn2 = nn.BatchNorm2d(width // 2)
|
| 218 |
+
self.conv3 = nn.Conv2d(width // 2, width, kernel_size=3, padding=1, bias=False)
|
| 219 |
+
self.bn3 = nn.BatchNorm2d(width)
|
| 220 |
+
self.avgpool = nn.AvgPool2d(2)
|
| 221 |
+
self.relu = nn.ReLU(inplace=True)
|
| 222 |
+
|
| 223 |
+
# residual layers
|
| 224 |
+
self._inplanes = width # this is a *mutable* variable used during construction
|
| 225 |
+
self.layer1 = self._make_layer(width, layers[0])
|
| 226 |
+
self.layer2 = self._make_layer(width * 2, layers[1], stride=2)
|
| 227 |
+
self.layer3 = self._make_layer(width * 4, layers[2], stride=2)
|
| 228 |
+
self.layer4 = self._make_layer(width * 8, layers[3], stride=2)
|
| 229 |
+
|
| 230 |
+
embed_dim = width * 32 # the ResNet feature dimension
|
| 231 |
+
self.attnpool = AttentionPool2d(input_resolution // 32, embed_dim, heads, output_dim)
|
| 232 |
+
|
| 233 |
+
def _make_layer(self, planes, blocks, stride=1):
|
| 234 |
+
layers = [Bottleneck(self._inplanes, planes, stride)]
|
| 235 |
+
|
| 236 |
+
self._inplanes = planes * Bottleneck.expansion
|
| 237 |
+
for _ in range(1, blocks):
|
| 238 |
+
layers.append(Bottleneck(self._inplanes, planes))
|
| 239 |
+
|
| 240 |
+
return nn.Sequential(*layers)
|
| 241 |
+
|
| 242 |
+
def forward(self, x):
|
| 243 |
+
def stem(x):
|
| 244 |
+
for conv, bn in [(self.conv1, self.bn1), (self.conv2, self.bn2), (self.conv3, self.bn3)]:
|
| 245 |
+
x = self.relu(bn(conv(x)))
|
| 246 |
+
x = self.avgpool(x)
|
| 247 |
+
return x
|
| 248 |
+
|
| 249 |
+
x = x.type(self.conv1.weight.dtype)
|
| 250 |
+
x = stem(x)
|
| 251 |
+
x = self.layer1(x)
|
| 252 |
+
x = self.layer2(x)
|
| 253 |
+
x = self.layer3(x)
|
| 254 |
+
x = self.layer4(x)
|
| 255 |
+
x = self.attnpool(x)
|
| 256 |
+
|
| 257 |
+
return x
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
class LayerNorm(nn.LayerNorm):
|
| 261 |
+
"""Subclass torch's LayerNorm to handle fp16."""
|
| 262 |
+
|
| 263 |
+
def forward(self, x: torch.Tensor):
|
| 264 |
+
orig_type = x.dtype
|
| 265 |
+
ret = super().forward(x.type(torch.float32))
|
| 266 |
+
return ret.type(orig_type)
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
class QuickGELU(nn.Module):
|
| 270 |
+
def forward(self, x: torch.Tensor):
|
| 271 |
+
return x * torch.sigmoid(1.702 * x)
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
class ResidualAttentionBlock(nn.Module):
|
| 275 |
+
def __init__(self, d_model: int, n_head: int, attn_mask: torch.Tensor = None, text_or_image=None):
|
| 276 |
+
super().__init__()
|
| 277 |
+
self.register_buffer("mean", torch.tensor([0.0]))
|
| 278 |
+
self.register_buffer("std", torch.tensor([1.0]))
|
| 279 |
+
self.attn = nn.MultiheadAttention(d_model, n_head)
|
| 280 |
+
self.ln_1 = LayerNorm(d_model)
|
| 281 |
+
self.mlp = nn.Sequential(OrderedDict([
|
| 282 |
+
("c_fc", nn.Linear(d_model, d_model * 4)),
|
| 283 |
+
("gelu", QuickGELU()),
|
| 284 |
+
("c_proj", nn.Linear(d_model * 4, d_model))
|
| 285 |
+
]))
|
| 286 |
+
self.ln_2 = LayerNorm(d_model)
|
| 287 |
+
self.attn_mask = attn_mask
|
| 288 |
+
self.is_train = global_is_train
|
| 289 |
+
self.step = 1
|
| 290 |
+
self.top_k = 2
|
| 291 |
+
self.ffn_num = 64
|
| 292 |
+
self.experts_num = 2
|
| 293 |
+
self.softmax = nn.Softmax(1)
|
| 294 |
+
self.softplus = nn.Softplus()
|
| 295 |
+
self.noisy_gating = True
|
| 296 |
+
self.adaptmlp_list = nn.ModuleList()
|
| 297 |
+
self.text_or_image = text_or_image
|
| 298 |
+
if text_or_image == 'text':
|
| 299 |
+
# print('text transformer')
|
| 300 |
+
self.choose_map_text = torch.zeros([ self.experts_num])
|
| 301 |
+
else:
|
| 302 |
+
# print('image transformer')
|
| 303 |
+
self.choose_map_image = torch.zeros([ self.experts_num])
|
| 304 |
+
self.router_list = nn.ParameterList()
|
| 305 |
+
self.w_noise_list = nn.ParameterList()
|
| 306 |
+
for i in range(self.step):
|
| 307 |
+
self.router_list.append(nn.Parameter(torch.zeros(d_model, self.experts_num), requires_grad=True))
|
| 308 |
+
self.w_noise_list.append(nn.Parameter(torch.zeros(d_model, self.experts_num), requires_grad=True))
|
| 309 |
+
for i in range(self.experts_num): #
|
| 310 |
+
self.adaptmlp = Adapter(d_model=d_model, dropout=0.1, bottleneck=self.ffn_num,
|
| 311 |
+
init_option='lora',
|
| 312 |
+
adapter_scalar=0.1,
|
| 313 |
+
adapter_layernorm_option='none',
|
| 314 |
+
)
|
| 315 |
+
self.adaptmlp_list.append(self.adaptmlp)
|
| 316 |
+
|
| 317 |
+
# self.taskid = None
|
| 318 |
+
def attention(self, x: torch.Tensor):
|
| 319 |
+
self.attn_mask = self.attn_mask.to(dtype=x.dtype, device=x.device) if self.attn_mask is not None else None
|
| 320 |
+
return self.attn(x, x, x, need_weights=False, attn_mask=self.attn_mask)[0]
|
| 321 |
+
|
| 322 |
+
def cv_squared(self, x):
|
| 323 |
+
"""The squared coefficient of variation of a sample.
|
| 324 |
+
Useful as a loss to encourage a positive distribution to be more uniform.
|
| 325 |
+
Epsilons added for numerical stability.
|
| 326 |
+
Returns 0 for an empty Tensor.
|
| 327 |
+
Args:
|
| 328 |
+
x: a `Tensor`.
|
| 329 |
+
Returns:
|
| 330 |
+
a `Scalar`.
|
| 331 |
+
"""
|
| 332 |
+
eps = 1e-10
|
| 333 |
+
# if only num_experts = 1
|
| 334 |
+
|
| 335 |
+
if x.shape[0] == 1:
|
| 336 |
+
return torch.tensor([0], device=x.device, dtype=x.dtype)
|
| 337 |
+
return x.float().var() / (x.float().mean()**2 + eps)
|
| 338 |
+
|
| 339 |
+
def _gates_to_load(self, gates):
|
| 340 |
+
"""Compute the true load per expert, given the gates.
|
| 341 |
+
The load is the number of examples for which the corresponding gate is >0.
|
| 342 |
+
Args:
|
| 343 |
+
gates: a `Tensor` of shape [batch_size, n]
|
| 344 |
+
Returns:
|
| 345 |
+
a float32 `Tensor` of shape [n]
|
| 346 |
+
"""
|
| 347 |
+
return (gates > 0).sum(0)
|
| 348 |
+
|
| 349 |
+
def _prob_in_top_k(self, clean_values, noisy_values, noise_stddev, noisy_top_values):
|
| 350 |
+
"""Helper function to NoisyTopKGating.
|
| 351 |
+
Computes the probability that value is in top k, given different random noise.
|
| 352 |
+
This gives us a way of backpropagating from a loss that balances the number
|
| 353 |
+
of times each expert is in the top k experts per example.
|
| 354 |
+
In the case of no noise, pass in None for noise_stddev, and the result will
|
| 355 |
+
not be differentiable.
|
| 356 |
+
Args:
|
| 357 |
+
clean_values: a `Tensor` of shape [batch, n].
|
| 358 |
+
noisy_values: a `Tensor` of shape [batch, n]. Equal to clean values plus
|
| 359 |
+
normally distributed noise with standard deviation noise_stddev.
|
| 360 |
+
noise_stddev: a `Tensor` of shape [batch, n], or None
|
| 361 |
+
noisy_top_values: a `Tensor` of shape [batch, m].
|
| 362 |
+
"values" Output of tf.top_k(noisy_top_values, m). m >= k+1
|
| 363 |
+
Returns:
|
| 364 |
+
a `Tensor` of shape [batch, n].
|
| 365 |
+
"""
|
| 366 |
+
# print('1231',clean_values) # 全nan
|
| 367 |
+
batch = clean_values.size(0)
|
| 368 |
+
m = noisy_top_values.size(1)
|
| 369 |
+
top_values_flat = noisy_top_values.flatten()
|
| 370 |
+
|
| 371 |
+
threshold_positions_if_in = torch.arange(batch, device=clean_values.device) * m + self.top_k
|
| 372 |
+
threshold_if_in = torch.unsqueeze(torch.gather(top_values_flat, 0, threshold_positions_if_in), 1)
|
| 373 |
+
is_in = torch.gt(noisy_values, threshold_if_in)
|
| 374 |
+
threshold_positions_if_out = threshold_positions_if_in - 1
|
| 375 |
+
threshold_if_out = torch.unsqueeze(torch.gather(top_values_flat, 0, threshold_positions_if_out), 1)
|
| 376 |
+
# is each value currently in the top k.
|
| 377 |
+
normal = Normal(self.mean, self.std)
|
| 378 |
+
#
|
| 379 |
+
|
| 380 |
+
prob_if_in = normal.cdf((clean_values - threshold_if_in)/noise_stddev)
|
| 381 |
+
prob_if_out = normal.cdf((clean_values - threshold_if_out)/noise_stddev)
|
| 382 |
+
prob = torch.where(is_in, prob_if_in, prob_if_out)
|
| 383 |
+
return prob
|
| 384 |
+
|
| 385 |
+
def noisy_top_k_gating(self, x, train, w_gate, w_noise, noise_epsilon=1e-2):
|
| 386 |
+
"""Noisy top-k gating.
|
| 387 |
+
See paper: https://arxiv.org/abs/1701.06538.
|
| 388 |
+
Args:
|
| 389 |
+
x: input Tensor with shape [batch_size, input_size]
|
| 390 |
+
train: a boolean - we only add noise at training time.
|
| 391 |
+
noise_epsilon: a float
|
| 392 |
+
Returns:
|
| 393 |
+
gates: a Tensor with shape [batch_size, num_experts]
|
| 394 |
+
load: a Tensor with shape [num_experts]
|
| 395 |
+
"""
|
| 396 |
+
|
| 397 |
+
clean_logits = x @ w_gate.to(x)
|
| 398 |
+
if self.noisy_gating and train:
|
| 399 |
+
raw_noise_stddev = x @ w_noise.to(x)
|
| 400 |
+
noise_stddev = ((self.softplus(raw_noise_stddev) + noise_epsilon))
|
| 401 |
+
noisy_logits = clean_logits + (torch.randn_like(clean_logits) * noise_stddev)
|
| 402 |
+
logits = noisy_logits
|
| 403 |
+
else:
|
| 404 |
+
logits = clean_logits
|
| 405 |
+
# calculate topk + 1 that will be needed for the noisy gates
|
| 406 |
+
top_logits, top_indices = logits.topk(min(self.top_k + 1, self.experts_num), dim=1)
|
| 407 |
+
top_k_logits = top_logits[:, :self.top_k]
|
| 408 |
+
top_k_indices = top_indices[:, :self.top_k]
|
| 409 |
+
top_k_gates = self.softmax(top_k_logits)
|
| 410 |
+
zeros = torch.zeros_like(logits)
|
| 411 |
+
gates = zeros.scatter(1, top_k_indices, top_k_gates)
|
| 412 |
+
if self.noisy_gating and self.top_k < self.experts_num and train: # 目前未用上
|
| 413 |
+
load = (self._prob_in_top_k(clean_logits, noisy_logits, noise_stddev, top_logits)).sum(0)
|
| 414 |
+
else:
|
| 415 |
+
load = self._gates_to_load(gates)
|
| 416 |
+
return gates, load
|
| 417 |
+
|
| 418 |
+
def forward(self, x: torch.Tensor):
|
| 419 |
+
x = x + self.attention(self.ln_1(x))
|
| 420 |
+
if global_taskid is not None:
|
| 421 |
+
x_re = x.permute(1, 0, 2)[:, 0, :]
|
| 422 |
+
gates, load = self.noisy_top_k_gating(x_re, self.is_train, self.router_list[global_taskid],
|
| 423 |
+
self.w_noise_list[global_taskid])
|
| 424 |
+
importance = gates.sum(0)
|
| 425 |
+
|
| 426 |
+
nonzero_indices = torch.nonzero(gates)
|
| 427 |
+
counter = Counter(nonzero_indices[:, 1].tolist())
|
| 428 |
+
for number, count in counter.items():
|
| 429 |
+
if self.text_or_image == 'text':
|
| 430 |
+
self.choose_map_text[number] = self.choose_map_text[number] + count
|
| 431 |
+
else:
|
| 432 |
+
self.choose_map_image[number] = self.choose_map_image[number] + count
|
| 433 |
+
dispatcher = SparseDispatcher(self.experts_num, gates)
|
| 434 |
+
expert_inputs = dispatcher.dispatch(x.permute(1, 0, 2).view(x.shape[1], -1))
|
| 435 |
+
expert_outputs = [self.adaptmlp_list[i](expert_inputs[i].view(expert_inputs[i].shape[0],
|
| 436 |
+
x.shape[0], x.shape[2]).to(x), add_residual=False)
|
| 437 |
+
for i in range(self.experts_num)]
|
| 438 |
+
|
| 439 |
+
i = 0
|
| 440 |
+
while i < len(expert_outputs):
|
| 441 |
+
if expert_outputs[i].shape[0] == 0:
|
| 442 |
+
expert_outputs.pop(i)
|
| 443 |
+
else:
|
| 444 |
+
expert_outputs[i] = expert_outputs[i].view(expert_outputs[i].shape[0], -1)
|
| 445 |
+
i += 1
|
| 446 |
+
|
| 447 |
+
y = dispatcher.combine(expert_outputs)
|
| 448 |
+
y = y.view(x.shape[1], x.shape[0], x.shape[2])
|
| 449 |
+
x = x + self.mlp(self.ln_2(x)) + y.permute(1, 0, 2)
|
| 450 |
+
else:
|
| 451 |
+
x = x + self.mlp(self.ln_2(x))
|
| 452 |
+
return x
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
class Transformer(nn.Module):
|
| 456 |
+
def __init__(self, width: int, layers: int, heads: int, attn_mask: torch.Tensor = None, text_or_image=None):
|
| 457 |
+
super().__init__()
|
| 458 |
+
self.width = width
|
| 459 |
+
self.layers = layers
|
| 460 |
+
self.resblocks = nn.Sequential(*[ResidualAttentionBlock(width, heads, attn_mask, text_or_image) for _ in range(layers)])
|
| 461 |
+
|
| 462 |
+
def forward(self, x: torch.Tensor):
|
| 463 |
+
return self.resblocks(x)
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
class VisualTransformer(nn.Module):
|
| 467 |
+
def __init__(self, input_resolution: int, patch_size: int, width: int, layers: int, heads: int, output_dim: int, text_or_image=None):
|
| 468 |
+
super().__init__()
|
| 469 |
+
self.input_resolution = input_resolution
|
| 470 |
+
self.output_dim = output_dim
|
| 471 |
+
# Added so this info is available. should not change anything.
|
| 472 |
+
self.patch_size = patch_size
|
| 473 |
+
self.width = width
|
| 474 |
+
self.layers = layers
|
| 475 |
+
self.heads = heads
|
| 476 |
+
|
| 477 |
+
self.conv1 = nn.Conv2d(in_channels=3, out_channels=width, kernel_size=patch_size, stride=patch_size, bias=False)
|
| 478 |
+
|
| 479 |
+
scale = width ** -0.5
|
| 480 |
+
self.class_embedding = nn.Parameter(scale * torch.randn(width))
|
| 481 |
+
self.positional_embedding = nn.Parameter(scale * torch.randn((input_resolution // patch_size) ** 2 + 1, width))
|
| 482 |
+
self.ln_pre = LayerNorm(width)
|
| 483 |
+
|
| 484 |
+
self.transformer = Transformer(width, layers, heads, text_or_image=text_or_image)
|
| 485 |
+
|
| 486 |
+
self.ln_post = LayerNorm(width)
|
| 487 |
+
self.proj = nn.Parameter(scale * torch.randn(width, output_dim))
|
| 488 |
+
|
| 489 |
+
def forward(self, x: torch.Tensor):
|
| 490 |
+
x = self.conv1(x)
|
| 491 |
+
x = x.reshape(x.shape[0], x.shape[1], -1)
|
| 492 |
+
x = x.permute(0, 2, 1)
|
| 493 |
+
x = torch.cat([self.class_embedding.to(x.dtype) + torch.zeros(x.shape[0], 1, x.shape[-1], dtype=x.dtype, device=x.device), x], dim=1) # shape = [*, grid ** 2 + 1, width]
|
| 494 |
+
x = x + self.positional_embedding.to(x.dtype)
|
| 495 |
+
x = self.ln_pre(x)
|
| 496 |
+
|
| 497 |
+
x = x.permute(1, 0, 2) # NLD -> LND
|
| 498 |
+
x = self.transformer(x)
|
| 499 |
+
x = x.permute(1, 0, 2) # LND -> NLD
|
| 500 |
+
|
| 501 |
+
x = self.ln_post(x[:, 0, :])
|
| 502 |
+
|
| 503 |
+
if self.proj is not None:
|
| 504 |
+
x = x @ self.proj
|
| 505 |
+
|
| 506 |
+
return x
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
class CLIP(nn.Module):
|
| 510 |
+
def __init__(self,
|
| 511 |
+
embed_dim: int,
|
| 512 |
+
# vision
|
| 513 |
+
image_resolution: int,
|
| 514 |
+
vision_layers: Union[Tuple[int, int, int, int], int],
|
| 515 |
+
vision_width: int,
|
| 516 |
+
vision_patch_size: int,
|
| 517 |
+
# text
|
| 518 |
+
context_length: int,
|
| 519 |
+
vocab_size: int,
|
| 520 |
+
transformer_width: int,
|
| 521 |
+
transformer_heads: int,
|
| 522 |
+
transformer_layers: int,
|
| 523 |
+
baseline = False
|
| 524 |
+
):
|
| 525 |
+
super().__init__()
|
| 526 |
+
self.baseline = baseline
|
| 527 |
+
|
| 528 |
+
self.context_length = context_length
|
| 529 |
+
|
| 530 |
+
if isinstance(vision_layers, (tuple, list)):
|
| 531 |
+
vision_heads = vision_width * 32 // 64
|
| 532 |
+
self.visual = ModifiedResNet(
|
| 533 |
+
layers=vision_layers,
|
| 534 |
+
output_dim=embed_dim,
|
| 535 |
+
heads=vision_heads,
|
| 536 |
+
input_resolution=image_resolution,
|
| 537 |
+
width=vision_width
|
| 538 |
+
)
|
| 539 |
+
else:
|
| 540 |
+
vision_heads = vision_width // 64
|
| 541 |
+
self.visual = VisualTransformer(
|
| 542 |
+
input_resolution=image_resolution,
|
| 543 |
+
patch_size=vision_patch_size,
|
| 544 |
+
width=vision_width,
|
| 545 |
+
layers=vision_layers,
|
| 546 |
+
heads=vision_heads,
|
| 547 |
+
output_dim=embed_dim,
|
| 548 |
+
text_or_image='image'
|
| 549 |
+
)
|
| 550 |
+
|
| 551 |
+
self.transformer = Transformer(
|
| 552 |
+
width=transformer_width,
|
| 553 |
+
layers=transformer_layers,
|
| 554 |
+
heads=transformer_heads,
|
| 555 |
+
attn_mask=self.build_attention_mask(),
|
| 556 |
+
text_or_image='text'
|
| 557 |
+
)
|
| 558 |
+
|
| 559 |
+
self.vocab_size = vocab_size
|
| 560 |
+
self.token_embedding = nn.Embedding(vocab_size, transformer_width)
|
| 561 |
+
self.positional_embedding = nn.Parameter(torch.empty(self.context_length, transformer_width))
|
| 562 |
+
self.ln_final = LayerNorm(transformer_width)
|
| 563 |
+
|
| 564 |
+
self.text_projection = nn.Parameter(torch.empty(transformer_width, embed_dim))
|
| 565 |
+
self.logit_scale = nn.Parameter(torch.ones([]) * np.log(1 / 0.07))
|
| 566 |
+
|
| 567 |
+
self.initialize_parameters()
|
| 568 |
+
|
| 569 |
+
def initialize_parameters(self):
|
| 570 |
+
nn.init.normal_(self.token_embedding.weight, std=0.02)
|
| 571 |
+
nn.init.normal_(self.positional_embedding, std=0.01)
|
| 572 |
+
self.logit_scale = nn.Parameter(torch.ones([]) * np.log(1 / 0.07))
|
| 573 |
+
|
| 574 |
+
if isinstance(self.visual, ModifiedResNet):
|
| 575 |
+
if self.visual.attnpool is not None:
|
| 576 |
+
std = self.visual.attnpool.c_proj.in_features ** -0.5
|
| 577 |
+
nn.init.normal_(self.visual.attnpool.q_proj.weight, std=std)
|
| 578 |
+
nn.init.normal_(self.visual.attnpool.k_proj.weight, std=std)
|
| 579 |
+
nn.init.normal_(self.visual.attnpool.v_proj.weight, std=std)
|
| 580 |
+
nn.init.normal_(self.visual.attnpool.c_proj.weight, std=std)
|
| 581 |
+
|
| 582 |
+
for resnet_block in [self.visual.layer1, self.visual.layer2, self.visual.layer3, self.visual.layer4]:
|
| 583 |
+
for name, param in resnet_block.named_parameters():
|
| 584 |
+
if name.endswith("bn3.weight"):
|
| 585 |
+
nn.init.zeros_(param)
|
| 586 |
+
|
| 587 |
+
proj_std = (self.transformer.width ** -0.5) * ((2 * self.transformer.layers) ** -0.5)
|
| 588 |
+
attn_std = self.transformer.width ** -0.5
|
| 589 |
+
fc_std = (2 * self.transformer.width) ** -0.5
|
| 590 |
+
for block in self.transformer.resblocks:
|
| 591 |
+
nn.init.normal_(block.attn.in_proj_weight, std=attn_std)
|
| 592 |
+
nn.init.normal_(block.attn.out_proj.weight, std=proj_std)
|
| 593 |
+
nn.init.normal_(block.mlp.c_fc.weight, std=fc_std)
|
| 594 |
+
nn.init.normal_(block.mlp.c_proj.weight, std=proj_std)
|
| 595 |
+
|
| 596 |
+
if self.text_projection is not None:
|
| 597 |
+
nn.init.normal_(self.text_projection, std=self.transformer.width ** -0.5)
|
| 598 |
+
|
| 599 |
+
def build_attention_mask(self):
|
| 600 |
+
# lazily create causal attention mask, with full attention between the vision tokens
|
| 601 |
+
# pytorch uses additive attention mask; fill with -inf
|
| 602 |
+
mask = torch.empty(self.context_length, self.context_length)
|
| 603 |
+
mask.fill_(float("-inf"))
|
| 604 |
+
mask.triu_(1) # zero out the lower diagonal
|
| 605 |
+
return mask
|
| 606 |
+
|
| 607 |
+
@property
|
| 608 |
+
def dtype(self):
|
| 609 |
+
return self.visual.conv1.weight.dtype
|
| 610 |
+
|
| 611 |
+
def encode_image(self, image):
|
| 612 |
+
return self.visual(image.type(self.dtype))
|
| 613 |
+
|
| 614 |
+
def encode_text(self, text):
|
| 615 |
+
|
| 616 |
+
x = self.token_embedding(text).type(self.dtype) # [batch_size, n_ctx, d_model]
|
| 617 |
+
|
| 618 |
+
x = x + self.positional_embedding.type(self.dtype)
|
| 619 |
+
x = x.permute(1, 0, 2) # NLD -> LND
|
| 620 |
+
x = self.transformer(x)
|
| 621 |
+
x = x.permute(1, 0, 2) # LND -> NLD
|
| 622 |
+
x = self.ln_final(x).type(self.dtype)
|
| 623 |
+
|
| 624 |
+
# take features from the eot embedding (eot_token is the highest number in each sequence)
|
| 625 |
+
x = x[torch.arange(x.shape[0]), text.argmax(dim=-1)] @ self.text_projection
|
| 626 |
+
|
| 627 |
+
return x
|
| 628 |
+
|
| 629 |
+
def forward(self, image, text, taskid, is_train):
|
| 630 |
+
global global_taskid, global_is_train
|
| 631 |
+
global_taskid = taskid
|
| 632 |
+
global_is_train = is_train
|
| 633 |
+
if image is None:
|
| 634 |
+
return self.encode_text(text)
|
| 635 |
+
elif text is None:
|
| 636 |
+
return self.encode_image(image)
|
| 637 |
+
image_features = self.encode_image(image)
|
| 638 |
+
text_features = self.encode_text(text)
|
| 639 |
+
|
| 640 |
+
image_features = image_features / image_features.norm(dim=-1, keepdim=True)
|
| 641 |
+
text_features = text_features / text_features.norm(dim=-1, keepdim=True)
|
| 642 |
+
|
| 643 |
+
# if self.baseline:
|
| 644 |
+
logit_scale = self.logit_scale.exp()
|
| 645 |
+
logits_per_image = logit_scale * image_features @ text_features.t()
|
| 646 |
+
logits_per_text = logits_per_image.t()
|
| 647 |
+
return logits_per_image, logits_per_text
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
|
| 651 |
+
def convert_weights(model: nn.Module):
|
| 652 |
+
"""Convert applicable model parameters to fp16"""
|
| 653 |
+
|
| 654 |
+
def _convert_weights_to_fp16(l):
|
| 655 |
+
if isinstance(l, (nn.Conv1d, nn.Conv2d, nn.Linear)):
|
| 656 |
+
l.weight.data = l.weight.data.half()
|
| 657 |
+
if l.bias is not None:
|
| 658 |
+
l.bias.data = l.bias.data.half()
|
| 659 |
+
|
| 660 |
+
if isinstance(l, nn.MultiheadAttention):
|
| 661 |
+
for attr in [*[f"{s}_proj_weight" for s in ["in", "q", "k", "v"]], "in_proj_bias", "bias_k", "bias_v"]:
|
| 662 |
+
tensor = getattr(l, attr)
|
| 663 |
+
if tensor is not None:
|
| 664 |
+
tensor.data = tensor.data.half()
|
| 665 |
+
|
| 666 |
+
for name in ["text_projection", "proj"]:
|
| 667 |
+
if hasattr(l, name):
|
| 668 |
+
attr = getattr(l, name)
|
| 669 |
+
if attr is not None:
|
| 670 |
+
attr.data = attr.data.half()
|
| 671 |
+
|
| 672 |
+
model.apply(_convert_weights_to_fp16)
|
| 673 |
+
|
| 674 |
+
|
| 675 |
+
def build_model(state_dict: dict):
|
| 676 |
+
vit = "visual.proj" in state_dict
|
| 677 |
+
|
| 678 |
+
if vit:
|
| 679 |
+
vision_width = state_dict["visual.conv1.weight"].shape[0]
|
| 680 |
+
vision_layers = len([k for k in state_dict.keys() if k.startswith("visual.") and k.endswith(".attn.in_proj_weight")])
|
| 681 |
+
vision_patch_size = state_dict["visual.conv1.weight"].shape[-1]
|
| 682 |
+
grid_size = round((state_dict["visual.positional_embedding"].shape[0] - 1) ** 0.5)
|
| 683 |
+
image_resolution = vision_patch_size * grid_size
|
| 684 |
+
else:
|
| 685 |
+
counts: list = [len(set(k.split(".")[2] for k in state_dict if k.startswith(f"visual.layer{b}"))) for b in [1, 2, 3, 4]]
|
| 686 |
+
vision_layers = tuple(counts)
|
| 687 |
+
vision_width = state_dict["visual.layer1.0.conv1.weight"].shape[0]
|
| 688 |
+
output_width = round((state_dict["visual.attnpool.positional_embedding"].shape[0] - 1) ** 0.5)
|
| 689 |
+
vision_patch_size = None
|
| 690 |
+
assert output_width ** 2 + 1 == state_dict["visual.attnpool.positional_embedding"].shape[0]
|
| 691 |
+
image_resolution = output_width * 32
|
| 692 |
+
|
| 693 |
+
embed_dim = state_dict["text_projection"].shape[1]
|
| 694 |
+
context_length = state_dict["positional_embedding"].shape[0]
|
| 695 |
+
vocab_size = state_dict["token_embedding.weight"].shape[0]
|
| 696 |
+
transformer_width = state_dict["ln_final.weight"].shape[0]
|
| 697 |
+
transformer_heads = transformer_width // 64
|
| 698 |
+
transformer_layers = len(set(k.split(".")[2] for k in state_dict if k.startswith(f"transformer.resblocks")))
|
| 699 |
+
|
| 700 |
+
model = CLIP(
|
| 701 |
+
embed_dim,
|
| 702 |
+
image_resolution, vision_layers, vision_width, vision_patch_size,
|
| 703 |
+
context_length, vocab_size, transformer_width, transformer_heads, transformer_layers
|
| 704 |
+
)
|
| 705 |
+
|
| 706 |
+
for key in ["input_resolution", "context_length", "vocab_size"]:
|
| 707 |
+
if key in state_dict:
|
| 708 |
+
del state_dict[key]
|
| 709 |
+
|
| 710 |
+
model.load_state_dict(state_dict, strict=False)
|
| 711 |
+
for p in model.parameters():
|
| 712 |
+
p.data = p.data.float()
|
| 713 |
+
return model.eval()
|
clip/tokenizer.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gzip
|
| 2 |
+
import html
|
| 3 |
+
import os
|
| 4 |
+
from functools import lru_cache
|
| 5 |
+
|
| 6 |
+
import ftfy
|
| 7 |
+
import regex as re
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@lru_cache()
|
| 11 |
+
def default_bpe():
|
| 12 |
+
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "bpe_simple_vocab_16e6.txt.gz")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@lru_cache()
|
| 16 |
+
def bytes_to_unicode():
|
| 17 |
+
"""
|
| 18 |
+
Returns list of utf-8 byte and a corresponding list of unicode strings.
|
| 19 |
+
The reversible bpe codes work on unicode strings.
|
| 20 |
+
This means you need a large # of unicode characters in your vocab if you want to avoid UNKs.
|
| 21 |
+
When you're at something like a 10B token dataset you end up needing around 5K for decent coverage.
|
| 22 |
+
This is a signficant percentage of your normal, say, 32K bpe vocab.
|
| 23 |
+
To avoid that, we want lookup tables between utf-8 bytes and unicode strings.
|
| 24 |
+
And avoids mapping to whitespace/control characters the bpe code barfs on.
|
| 25 |
+
"""
|
| 26 |
+
bs = list(range(ord("!"), ord("~")+1))+list(range(ord("¡"), ord("¬")+1))+list(range(ord("®"), ord("ÿ")+1))
|
| 27 |
+
cs = bs[:]
|
| 28 |
+
n = 0
|
| 29 |
+
for b in range(2**8):
|
| 30 |
+
if b not in bs:
|
| 31 |
+
bs.append(b)
|
| 32 |
+
cs.append(2**8+n)
|
| 33 |
+
n += 1
|
| 34 |
+
cs = [chr(n) for n in cs]
|
| 35 |
+
return dict(zip(bs, cs))
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def get_pairs(word):
|
| 39 |
+
"""Return set of symbol pairs in a word.
|
| 40 |
+
Word is represented as tuple of symbols (symbols being variable-length strings).
|
| 41 |
+
"""
|
| 42 |
+
pairs = set()
|
| 43 |
+
prev_char = word[0]
|
| 44 |
+
for char in word[1:]:
|
| 45 |
+
pairs.add((prev_char, char))
|
| 46 |
+
prev_char = char
|
| 47 |
+
return pairs
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def basic_clean(text):
|
| 51 |
+
text = ftfy.fix_text(text)
|
| 52 |
+
text = html.unescape(html.unescape(text))
|
| 53 |
+
return text.strip()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def whitespace_clean(text):
|
| 57 |
+
text = re.sub(r'\s+', ' ', text)
|
| 58 |
+
text = text.strip()
|
| 59 |
+
return text
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class SimpleTokenizer(object):
|
| 63 |
+
def __init__(self, bpe_path: str = default_bpe(), special_tokens=None):
|
| 64 |
+
self.byte_encoder = bytes_to_unicode()
|
| 65 |
+
self.byte_decoder = {v: k for k, v in self.byte_encoder.items()}
|
| 66 |
+
merges = gzip.open(bpe_path).read().decode("utf-8").split('\n')
|
| 67 |
+
merges = merges[1:49152-256-2+1]
|
| 68 |
+
merges = [tuple(merge.split()) for merge in merges]
|
| 69 |
+
vocab = list(bytes_to_unicode().values())
|
| 70 |
+
vocab = vocab + [v+'</w>' for v in vocab]
|
| 71 |
+
for merge in merges:
|
| 72 |
+
vocab.append(''.join(merge))
|
| 73 |
+
if not special_tokens:
|
| 74 |
+
special_tokens = ['<start_of_text>', '<end_of_text>']
|
| 75 |
+
else:
|
| 76 |
+
special_tokens = ['<start_of_text>', '<end_of_text>'] + special_tokens
|
| 77 |
+
vocab.extend(special_tokens)
|
| 78 |
+
self.encoder = dict(zip(vocab, range(len(vocab))))
|
| 79 |
+
self.decoder = {v: k for k, v in self.encoder.items()}
|
| 80 |
+
self.bpe_ranks = dict(zip(merges, range(len(merges))))
|
| 81 |
+
self.cache = {t:t for t in special_tokens}
|
| 82 |
+
special = "|".join(special_tokens)
|
| 83 |
+
self.pat = re.compile(special + r"""|'s|'t|'re|'ve|'m|'ll|'d|[\p{L}]+|[\p{N}]|[^\s\p{L}\p{N}]+""", re.IGNORECASE)
|
| 84 |
+
|
| 85 |
+
self.vocab_size = len(self.encoder)
|
| 86 |
+
self.all_special_ids = [self.encoder[t] for t in special_tokens]
|
| 87 |
+
|
| 88 |
+
def bpe(self, token):
|
| 89 |
+
if token in self.cache:
|
| 90 |
+
return self.cache[token]
|
| 91 |
+
word = tuple(token[:-1]) + ( token[-1] + '</w>',)
|
| 92 |
+
pairs = get_pairs(word)
|
| 93 |
+
|
| 94 |
+
if not pairs:
|
| 95 |
+
return token+'</w>'
|
| 96 |
+
|
| 97 |
+
while True:
|
| 98 |
+
bigram = min(pairs, key = lambda pair: self.bpe_ranks.get(pair, float('inf')))
|
| 99 |
+
if bigram not in self.bpe_ranks:
|
| 100 |
+
break
|
| 101 |
+
first, second = bigram
|
| 102 |
+
new_word = []
|
| 103 |
+
i = 0
|
| 104 |
+
while i < len(word):
|
| 105 |
+
try:
|
| 106 |
+
j = word.index(first, i)
|
| 107 |
+
new_word.extend(word[i:j])
|
| 108 |
+
i = j
|
| 109 |
+
except:
|
| 110 |
+
new_word.extend(word[i:])
|
| 111 |
+
break
|
| 112 |
+
|
| 113 |
+
if word[i] == first and i < len(word)-1 and word[i+1] == second:
|
| 114 |
+
new_word.append(first+second)
|
| 115 |
+
i += 2
|
| 116 |
+
else:
|
| 117 |
+
new_word.append(word[i])
|
| 118 |
+
i += 1
|
| 119 |
+
new_word = tuple(new_word)
|
| 120 |
+
word = new_word
|
| 121 |
+
if len(word) == 1:
|
| 122 |
+
break
|
| 123 |
+
else:
|
| 124 |
+
pairs = get_pairs(word)
|
| 125 |
+
word = ' '.join(word)
|
| 126 |
+
self.cache[token] = word
|
| 127 |
+
return word
|
| 128 |
+
|
| 129 |
+
def encode(self, text):
|
| 130 |
+
bpe_tokens = []
|
| 131 |
+
text = whitespace_clean(basic_clean(text)).lower()
|
| 132 |
+
for token in re.findall(self.pat, text):
|
| 133 |
+
token = ''.join(self.byte_encoder[b] for b in token.encode('utf-8'))
|
| 134 |
+
bpe_tokens.extend(self.encoder[bpe_token] for bpe_token in self.bpe(token).split(' '))
|
| 135 |
+
return bpe_tokens
|
| 136 |
+
|
| 137 |
+
def decode(self, tokens):
|
| 138 |
+
text = ''.join([self.decoder[token] for token in tokens])
|
| 139 |
+
text = bytearray([self.byte_decoder[c] for c in text]).decode('utf-8', errors="replace").replace('</w>', ' ')
|
| 140 |
+
return text
|
configs/class/aircraft.yaml
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
hydra:
|
| 2 |
+
run:
|
| 3 |
+
dir: ./experiments/Aircraft
|
| 4 |
+
job:
|
| 5 |
+
chdir: true
|
| 6 |
+
|
| 7 |
+
job_logging:
|
| 8 |
+
version: 1
|
| 9 |
+
formatters:
|
| 10 |
+
simple:
|
| 11 |
+
format: '%(message)s'
|
| 12 |
+
|
| 13 |
+
class_order: ""
|
| 14 |
+
dataset_root: ""
|
| 15 |
+
workdir: ""
|
| 16 |
+
log_path: "metrics.json"
|
| 17 |
+
model_name: "ViT-B/16"
|
| 18 |
+
prompt_template: "a bad photo of a {}."
|
| 19 |
+
zero_shot_eval: true
|
| 20 |
+
pre_task_zero_shot_eval: False
|
| 21 |
+
# zs_mtil_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] # indices of MTIL datasets (0..24) to evaluate in zero-shot; excludes any selected as downstream train_dataset
|
| 22 |
+
zs_mtil_indices: [1] # indices of MTIL datasets (0..24) to evaluate in zero-shot; excludes any selected as downstream train_dataset
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
batch_size: 128
|
| 26 |
+
increment: ${initial_increment}
|
| 27 |
+
initial_increment: 10
|
| 28 |
+
scenario: "class"
|
| 29 |
+
dataset: "cifar100"
|
| 30 |
+
|
| 31 |
+
# method: "lwf"
|
| 32 |
+
# lr: 7.1e-6
|
| 33 |
+
# ls: 0.2
|
| 34 |
+
# we:
|
| 35 |
+
# avg_freq:
|
| 36 |
+
# ref_dataset:
|
| 37 |
+
# ref_sentences:
|
| 38 |
+
|
| 39 |
+
weight_decay: 0.0
|
| 40 |
+
l2: 0
|
| 41 |
+
ce_method: 0
|
| 42 |
+
|
| 43 |
+
method: "MoE-Adapters"
|
| 44 |
+
lr: 1e-3
|
| 45 |
+
ls: 0.0
|
| 46 |
+
#we:
|
| 47 |
+
#avg_freq:
|
| 48 |
+
#ref_dataset:
|
| 49 |
+
#ref_sentences: random
|
| 50 |
+
|
| 51 |
+
# =====================
|
| 52 |
+
# DFA (two-expert + router) hyperparameters
|
| 53 |
+
# =====================
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# Training schedule
|
| 57 |
+
epochs_a: 1
|
| 58 |
+
epochs_b: 1
|
| 59 |
+
e2_top_k: 2 # top-k for E2 router
|
| 60 |
+
|
| 61 |
+
# Stage A (InfoNCE)
|
| 62 |
+
lr_e1: 1.0e-5
|
| 63 |
+
tau_con: 0.15 # contrastive temperature for InfoNCE
|
| 64 |
+
use_moco_queue: true # enable momentum queue for more negatives
|
| 65 |
+
moco_queue_size: 128 # queue size (default 4096, increases negative samples significantly)
|
| 66 |
+
|
| 67 |
+
# Stage B (Task expert + Router)
|
| 68 |
+
lr_e2: 5.0e-4
|
| 69 |
+
|
| 70 |
+
text_lr_e2: 1.0e-5
|
| 71 |
+
lr_e2_router: 1.0e-3
|
| 72 |
+
lr_top_router: 1.0e-5
|
| 73 |
+
lambda_b_con: 0.001
|
| 74 |
+
tau_b_con: 0.19
|
| 75 |
+
num_task_experts: 2
|
| 76 |
+
seed: 43
|
configs/class/caltech.yaml
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
hydra:
|
| 2 |
+
run:
|
| 3 |
+
dir: ./experiments/Caltech101
|
| 4 |
+
job:
|
| 5 |
+
chdir: true
|
| 6 |
+
|
| 7 |
+
job_logging:
|
| 8 |
+
version: 1
|
| 9 |
+
formatters:
|
| 10 |
+
simple:
|
| 11 |
+
format: '%(message)s'
|
| 12 |
+
|
| 13 |
+
class_order: ""
|
| 14 |
+
dataset_root: ""
|
| 15 |
+
workdir: ""
|
| 16 |
+
log_path: "metrics.json"
|
| 17 |
+
model_name: "ViT-B/16"
|
| 18 |
+
prompt_template: "a bad photo of a {}."
|
| 19 |
+
zero_shot_eval: true
|
| 20 |
+
pre_task_zero_shot_eval: False
|
| 21 |
+
zs_mtil_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] # indices of MTIL datasets (0..24) to evaluate in zero-shot; excludes any selected as downstream train_dataset
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
batch_size: 128
|
| 26 |
+
increment: ${initial_increment}
|
| 27 |
+
initial_increment: 10
|
| 28 |
+
scenario: "class"
|
| 29 |
+
dataset: "cifar100"
|
| 30 |
+
|
| 31 |
+
# method: "lwf"
|
| 32 |
+
# lr: 7.1e-6
|
| 33 |
+
# ls: 0.2
|
| 34 |
+
# we:
|
| 35 |
+
# avg_freq:
|
| 36 |
+
# ref_dataset:
|
| 37 |
+
# ref_sentences:
|
| 38 |
+
|
| 39 |
+
weight_decay: 0.0
|
| 40 |
+
l2: 0
|
| 41 |
+
ce_method: 0
|
| 42 |
+
|
| 43 |
+
method: "MoE-Adapters"
|
| 44 |
+
lr: 1e-3
|
| 45 |
+
ls: 0.0
|
| 46 |
+
#we:
|
| 47 |
+
#avg_freq:
|
| 48 |
+
#ref_dataset:
|
| 49 |
+
#ref_sentences: random
|
| 50 |
+
|
| 51 |
+
# =====================
|
| 52 |
+
# DFA (two-expert + router) hyperparameters
|
| 53 |
+
# =====================
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# Training schedule
|
| 57 |
+
epochs_a: 1
|
| 58 |
+
epochs_b: 1
|
| 59 |
+
e2_top_k: 2 # top-k for E2 router
|
| 60 |
+
|
| 61 |
+
# Stage A (InfoNCE)
|
| 62 |
+
lr_e1: 1.0e-5
|
| 63 |
+
tau_con: 0.15 # contrastive temperature for InfoNCE
|
| 64 |
+
use_moco_queue: true # enable momentum queue for more negatives
|
| 65 |
+
moco_queue_size: 128 # queue size (default 4096, increases negative samples significantly)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# Stage B (Task expert + Router)
|
| 71 |
+
lr_e2: 5.0e-4
|
| 72 |
+
|
| 73 |
+
text_lr_e2: 1.0e-5
|
| 74 |
+
lr_e2_router: 1.0e-3
|
| 75 |
+
lr_top_router: 1.0e-5
|
| 76 |
+
lambda_b_con: 0.001
|
| 77 |
+
tau_b_con: 0.19
|
| 78 |
+
num_task_experts: 2
|
| 79 |
+
seed: 43
|
configs/class/car.yaml
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
hydra:
|
| 2 |
+
run:
|
| 3 |
+
dir: ./experiments/StanfordCars
|
| 4 |
+
job:
|
| 5 |
+
chdir: true
|
| 6 |
+
|
| 7 |
+
job_logging:
|
| 8 |
+
version: 1
|
| 9 |
+
formatters:
|
| 10 |
+
simple:
|
| 11 |
+
format: '%(message)s'
|
| 12 |
+
|
| 13 |
+
class_order: ""
|
| 14 |
+
dataset_root: ""
|
| 15 |
+
workdir: ""
|
| 16 |
+
log_path: "metrics.json"
|
| 17 |
+
model_name: "ViT-B/16"
|
| 18 |
+
prompt_template: "a bad photo of a {}."
|
| 19 |
+
zero_shot_eval: true
|
| 20 |
+
pre_task_zero_shot_eval: False
|
| 21 |
+
zs_mtil_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] # indices of MTIL datasets (0..24) to evaluate in zero-shot; excludes any selected as downstream train_dataset
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
batch_size: 128
|
| 26 |
+
increment: ${initial_increment}
|
| 27 |
+
initial_increment: 10
|
| 28 |
+
scenario: "class"
|
| 29 |
+
dataset: "cifar100"
|
| 30 |
+
|
| 31 |
+
# method: "lwf"
|
| 32 |
+
# lr: 7.1e-6
|
| 33 |
+
# ls: 0.2
|
| 34 |
+
# we:
|
| 35 |
+
# avg_freq:
|
| 36 |
+
# ref_dataset:
|
| 37 |
+
# ref_sentences:
|
| 38 |
+
|
| 39 |
+
weight_decay: 0.0
|
| 40 |
+
l2: 0
|
| 41 |
+
ce_method: 0
|
| 42 |
+
|
| 43 |
+
method: "MoE-Adapters"
|
| 44 |
+
lr: 1e-3
|
| 45 |
+
ls: 0.0
|
| 46 |
+
#we:
|
| 47 |
+
#avg_freq:
|
| 48 |
+
#ref_dataset:
|
| 49 |
+
#ref_sentences: random
|
| 50 |
+
|
| 51 |
+
# =====================
|
| 52 |
+
# DFA (two-expert + router) hyperparameters
|
| 53 |
+
# =====================
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# Training schedule
|
| 57 |
+
epochs_a: 1
|
| 58 |
+
epochs_b: 1
|
| 59 |
+
e2_top_k: 2 # top-k for E2 router
|
| 60 |
+
|
| 61 |
+
# Stage A (InfoNCE)
|
| 62 |
+
lr_e1: 1.0e-5
|
| 63 |
+
tau_con: 0.15 # contrastive temperature for InfoNCE
|
| 64 |
+
use_moco_queue: true # enable momentum queue for more negatives
|
| 65 |
+
moco_queue_size: 128 # queue size (default 4096, increases negative samples significantly)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# Stage B (Task expert + Router)
|
| 71 |
+
lr_e2: 5.0e-4
|
| 72 |
+
|
| 73 |
+
text_lr_e2: 1.0e-5
|
| 74 |
+
lr_e2_router: 1.0e-3
|
| 75 |
+
lr_top_router: 1.0e-5
|
| 76 |
+
lambda_b_con: 0.001
|
| 77 |
+
tau_b_con: 0.17
|
| 78 |
+
num_task_experts: 2
|
| 79 |
+
seed: 43
|
configs/class/cifar10.yaml
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
hydra:
|
| 2 |
+
run:
|
| 3 |
+
dir: ./experiments/Cifar10
|
| 4 |
+
job:
|
| 5 |
+
chdir: true
|
| 6 |
+
|
| 7 |
+
job_logging:
|
| 8 |
+
version: 1
|
| 9 |
+
formatters:
|
| 10 |
+
simple:
|
| 11 |
+
format: '%(message)s'
|
| 12 |
+
|
| 13 |
+
class_order: ""
|
| 14 |
+
dataset_root: ""
|
| 15 |
+
workdir: ""
|
| 16 |
+
log_path: "metrics.json"
|
| 17 |
+
model_name: "ViT-B/16"
|
| 18 |
+
prompt_template: "a bad photo of a {}."
|
| 19 |
+
zero_shot_eval: true
|
| 20 |
+
pre_task_zero_shot_eval: False
|
| 21 |
+
zs_mtil_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] # indices of MTIL datasets (0..24) to evaluate in zero-shot; excludes any selected as downstream train_dataset
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
batch_size: 128
|
| 26 |
+
increment: ${initial_increment}
|
| 27 |
+
initial_increment: 10
|
| 28 |
+
scenario: "class"
|
| 29 |
+
dataset: "cifar100"
|
| 30 |
+
|
| 31 |
+
# method: "lwf"
|
| 32 |
+
# lr: 7.1e-6
|
| 33 |
+
# ls: 0.2
|
| 34 |
+
# we:
|
| 35 |
+
# avg_freq:
|
| 36 |
+
# ref_dataset:
|
| 37 |
+
# ref_sentences:
|
| 38 |
+
|
| 39 |
+
weight_decay: 0.0
|
| 40 |
+
l2: 0
|
| 41 |
+
ce_method: 0
|
| 42 |
+
|
| 43 |
+
method: "MoE-Adapters"
|
| 44 |
+
lr: 1e-3
|
| 45 |
+
ls: 0.0
|
| 46 |
+
#we:
|
| 47 |
+
#avg_freq:
|
| 48 |
+
#ref_dataset:
|
| 49 |
+
#ref_sentences: random
|
| 50 |
+
|
| 51 |
+
# =====================
|
| 52 |
+
# DFA (two-expert + router) hyperparameters
|
| 53 |
+
# =====================
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# Training schedule
|
| 57 |
+
epochs_a: 1
|
| 58 |
+
epochs_b: 1
|
| 59 |
+
e2_top_k: 2 # top-k for E2 router
|
| 60 |
+
|
| 61 |
+
# Stage A (InfoNCE)
|
| 62 |
+
lr_e1: 1.0e-5
|
| 63 |
+
tau_con: 0.15 # contrastive temperature for InfoNCE
|
| 64 |
+
use_moco_queue: true # enable momentum queue for more negatives
|
| 65 |
+
moco_queue_size: 128 # queue size (default 4096, increases negative samples significantly)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# Stage B (Task expert + Router)
|
| 71 |
+
lr_e2: 5.0e-4
|
| 72 |
+
|
| 73 |
+
text_lr_e2: 1.0e-5
|
| 74 |
+
lr_e2_router: 1.0e-3
|
| 75 |
+
lr_top_router: 1.0e-5
|
| 76 |
+
lambda_b_con: 0.001
|
| 77 |
+
tau_b_con: 0.19
|
| 78 |
+
num_task_experts: 2
|
| 79 |
+
seed: 43
|
configs/class/cifar100.yaml
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
hydra:
|
| 2 |
+
run:
|
| 3 |
+
dir: ./experiments/CIFAR100
|
| 4 |
+
job:
|
| 5 |
+
chdir: true
|
| 6 |
+
|
| 7 |
+
job_logging:
|
| 8 |
+
version: 1
|
| 9 |
+
formatters:
|
| 10 |
+
simple:
|
| 11 |
+
format: '%(message)s'
|
| 12 |
+
|
| 13 |
+
class_order: ""
|
| 14 |
+
dataset_root: ""
|
| 15 |
+
workdir: ""
|
| 16 |
+
log_path: "metrics.json"
|
| 17 |
+
model_name: "ViT-B/16"
|
| 18 |
+
prompt_template: "a bad photo of a {}."
|
| 19 |
+
zero_shot_eval: true
|
| 20 |
+
pre_task_zero_shot_eval: False
|
| 21 |
+
zs_mtil_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] # indices of MTIL datasets (0..24) to evaluate in zero-shot; excludes any selected as downstream train_dataset
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
batch_size: 64
|
| 26 |
+
increment: ${initial_increment}
|
| 27 |
+
initial_increment: 10
|
| 28 |
+
scenario: "class"
|
| 29 |
+
dataset: "cifar100"
|
| 30 |
+
|
| 31 |
+
# method: "lwf"
|
| 32 |
+
# lr: 7.1e-6
|
| 33 |
+
# ls: 0.2
|
| 34 |
+
# we:
|
| 35 |
+
# avg_freq:
|
| 36 |
+
# ref_dataset:
|
| 37 |
+
# ref_sentences:
|
| 38 |
+
|
| 39 |
+
weight_decay: 0.0
|
| 40 |
+
l2: 0
|
| 41 |
+
ce_method: 0
|
| 42 |
+
|
| 43 |
+
method: "MoE-Adapters"
|
| 44 |
+
lr: 1e-3
|
| 45 |
+
ls: 0.0
|
| 46 |
+
#we:
|
| 47 |
+
#avg_freq:
|
| 48 |
+
#ref_dataset:
|
| 49 |
+
#ref_sentences: random
|
| 50 |
+
|
| 51 |
+
# =====================
|
| 52 |
+
# DFA (two-expert + router) hyperparameters
|
| 53 |
+
# =====================
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# Training schedule
|
| 57 |
+
epochs_a: 1
|
| 58 |
+
epochs_b: 1
|
| 59 |
+
e2_top_k: 2 # top-k for E2 router
|
| 60 |
+
|
| 61 |
+
# Stage A (InfoNCE)
|
| 62 |
+
lr_e1: 1.0e-5
|
| 63 |
+
tau_con: 0.15 # contrastive temperature for InfoNCE
|
| 64 |
+
use_moco_queue: true # enable momentum queue for more negatives
|
| 65 |
+
moco_queue_size: 128 # queue size (default 4096, increases negative samples significantly)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# Stage B (Task expert + Router)
|
| 71 |
+
lr_e2: 5.0e-4
|
| 72 |
+
|
| 73 |
+
text_lr_e2: 1.0e-5
|
| 74 |
+
lr_e2_router: 1.0e-3
|
| 75 |
+
lr_top_router: 1.0e-5
|
| 76 |
+
lambda_b_con: 0.001
|
| 77 |
+
tau_b_con: 0.17
|
| 78 |
+
num_task_experts: 2
|
| 79 |
+
seed: 43
|
configs/class/clevr.yaml
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
hydra:
|
| 2 |
+
run:
|
| 3 |
+
dir: ./experiments/CLEVRCount
|
| 4 |
+
job:
|
| 5 |
+
chdir: true
|
| 6 |
+
|
| 7 |
+
job_logging:
|
| 8 |
+
version: 1
|
| 9 |
+
formatters:
|
| 10 |
+
simple:
|
| 11 |
+
format: '%(message)s'
|
| 12 |
+
|
| 13 |
+
class_order: ""
|
| 14 |
+
dataset_root: ""
|
| 15 |
+
workdir: ""
|
| 16 |
+
log_path: "metrics.json"
|
| 17 |
+
model_name: "ViT-B/16"
|
| 18 |
+
prompt_template: "a bad photo of a {}."
|
| 19 |
+
zero_shot_eval: true
|
| 20 |
+
pre_task_zero_shot_eval: False
|
| 21 |
+
zs_mtil_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] # indices of MTIL datasets (0..24) to evaluate in zero-shot; excludes any selected as downstream train_dataset
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
batch_size: 128
|
| 26 |
+
increment: ${initial_increment}
|
| 27 |
+
initial_increment: 10
|
| 28 |
+
scenario: "class"
|
| 29 |
+
dataset: "cifar100"
|
| 30 |
+
|
| 31 |
+
# method: "lwf"
|
| 32 |
+
# lr: 7.1e-6
|
| 33 |
+
# ls: 0.2
|
| 34 |
+
# we:
|
| 35 |
+
# avg_freq:
|
| 36 |
+
# ref_dataset:
|
| 37 |
+
# ref_sentences:
|
| 38 |
+
|
| 39 |
+
weight_decay: 0.0
|
| 40 |
+
l2: 0
|
| 41 |
+
ce_method: 0
|
| 42 |
+
|
| 43 |
+
method: "MoE-Adapters"
|
| 44 |
+
lr: 1e-3
|
| 45 |
+
ls: 0.0
|
| 46 |
+
#we:
|
| 47 |
+
#avg_freq:
|
| 48 |
+
#ref_dataset:
|
| 49 |
+
#ref_sentences: random
|
| 50 |
+
|
| 51 |
+
# =====================
|
| 52 |
+
# DFA (two-expert + router) hyperparameters
|
| 53 |
+
# =====================
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# Training schedule
|
| 57 |
+
epochs_a: 1
|
| 58 |
+
epochs_b: 1
|
| 59 |
+
e2_top_k: 2 # top-k for E2 router
|
| 60 |
+
|
| 61 |
+
# Stage A (InfoNCE)
|
| 62 |
+
lr_e1: 1.0e-5
|
| 63 |
+
tau_con: 0.15 # contrastive temperature for InfoNCE
|
| 64 |
+
use_moco_queue: true # enable momentum queue for more negatives
|
| 65 |
+
moco_queue_size: 128 # queue size (default 4096, increases negative samples significantly)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# Stage B (Task expert + Router)
|
| 71 |
+
lr_e2: 5.0e-4
|
| 72 |
+
|
| 73 |
+
text_lr_e2: 1.0e-5
|
| 74 |
+
lr_e2_router: 1.0e-3
|
| 75 |
+
lr_top_router: 1.0e-5
|
| 76 |
+
lambda_b_con: 0.001
|
| 77 |
+
tau_b_con: 0.19
|
| 78 |
+
num_task_experts: 2
|
| 79 |
+
seed: 43
|
configs/class/conuntry.yaml
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
hydra:
|
| 2 |
+
run:
|
| 3 |
+
dir: ./experiments/Country211
|
| 4 |
+
job:
|
| 5 |
+
chdir: true
|
| 6 |
+
|
| 7 |
+
job_logging:
|
| 8 |
+
version: 1
|
| 9 |
+
formatters:
|
| 10 |
+
simple:
|
| 11 |
+
format: '%(message)s'
|
| 12 |
+
|
| 13 |
+
class_order: ""
|
| 14 |
+
dataset_root: ""
|
| 15 |
+
workdir: ""
|
| 16 |
+
log_path: "metrics.json"
|
| 17 |
+
model_name: "ViT-B/16"
|
| 18 |
+
prompt_template: "a bad photo of a {}."
|
| 19 |
+
zero_shot_eval: true
|
| 20 |
+
pre_task_zero_shot_eval: False
|
| 21 |
+
zs_mtil_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] # indices of MTIL datasets (0..24) to evaluate in zero-shot; excludes any selected as downstream train_dataset
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
batch_size: 128
|
| 26 |
+
increment: ${initial_increment}
|
| 27 |
+
initial_increment: 10
|
| 28 |
+
scenario: "class"
|
| 29 |
+
dataset: "cifar100"
|
| 30 |
+
|
| 31 |
+
# method: "lwf"
|
| 32 |
+
# lr: 7.1e-6
|
| 33 |
+
# ls: 0.2
|
| 34 |
+
# we:
|
| 35 |
+
# avg_freq:
|
| 36 |
+
# ref_dataset:
|
| 37 |
+
# ref_sentences:
|
| 38 |
+
|
| 39 |
+
weight_decay: 0.0
|
| 40 |
+
l2: 0
|
| 41 |
+
ce_method: 0
|
| 42 |
+
|
| 43 |
+
method: "MoE-Adapters"
|
| 44 |
+
lr: 1e-3
|
| 45 |
+
ls: 0.0
|
| 46 |
+
#we:
|
| 47 |
+
#avg_freq:
|
| 48 |
+
#ref_dataset:
|
| 49 |
+
#ref_sentences: random
|
| 50 |
+
|
| 51 |
+
# =====================
|
| 52 |
+
# DFA (two-expert + router) hyperparameters
|
| 53 |
+
# =====================
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# Training schedule
|
| 57 |
+
epochs_a: 1
|
| 58 |
+
epochs_b: 1
|
| 59 |
+
e2_top_k: 2 # top-k for E2 router
|
| 60 |
+
|
| 61 |
+
# Stage A (InfoNCE)
|
| 62 |
+
lr_e1: 1.0e-5
|
| 63 |
+
tau_con: 0.15 # contrastive temperature for InfoNCE
|
| 64 |
+
use_moco_queue: true # enable momentum queue for more negatives
|
| 65 |
+
moco_queue_size: 128 # queue size (default 4096, increases negative samples significantly)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# Stage B (Task expert + Router)
|
| 71 |
+
lr_e2: 5.0e-4
|
| 72 |
+
|
| 73 |
+
text_lr_e2: 1.0e-5
|
| 74 |
+
lr_e2_router: 1.0e-3
|
| 75 |
+
lr_top_router: 1.0e-5
|
| 76 |
+
lambda_b_con: 0.001
|
| 77 |
+
tau_b_con: 0.19
|
| 78 |
+
num_task_experts: 2
|
| 79 |
+
seed: 43
|
configs/class/dtd.yaml
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
hydra:
|
| 2 |
+
run:
|
| 3 |
+
dir: ./experiments/DTD
|
| 4 |
+
job:
|
| 5 |
+
chdir: true
|
| 6 |
+
|
| 7 |
+
job_logging:
|
| 8 |
+
version: 1
|
| 9 |
+
formatters:
|
| 10 |
+
simple:
|
| 11 |
+
format: '%(message)s'
|
| 12 |
+
|
| 13 |
+
class_order: ""
|
| 14 |
+
dataset_root: ""
|
| 15 |
+
workdir: ""
|
| 16 |
+
log_path: "metrics.json"
|
| 17 |
+
model_name: "ViT-B/16"
|
| 18 |
+
prompt_template: "a bad photo of a {}."
|
| 19 |
+
zero_shot_eval: true
|
| 20 |
+
pre_task_zero_shot_eval: False
|
| 21 |
+
zs_mtil_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] # indices of MTIL datasets (0..24) to evaluate in zero-shot; excludes any selected as downstream train_dataset
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
batch_size: 128
|
| 26 |
+
increment: ${initial_increment}
|
| 27 |
+
initial_increment: 10
|
| 28 |
+
scenario: "class"
|
| 29 |
+
dataset: "cifar100"
|
| 30 |
+
|
| 31 |
+
# method: "lwf"
|
| 32 |
+
# lr: 7.1e-6
|
| 33 |
+
# ls: 0.2
|
| 34 |
+
# we:
|
| 35 |
+
# avg_freq:
|
| 36 |
+
# ref_dataset:
|
| 37 |
+
# ref_sentences:
|
| 38 |
+
|
| 39 |
+
weight_decay: 0.0
|
| 40 |
+
l2: 0
|
| 41 |
+
ce_method: 0
|
| 42 |
+
|
| 43 |
+
method: "MoE-Adapters"
|
| 44 |
+
lr: 1e-3
|
| 45 |
+
ls: 0.0
|
| 46 |
+
#we:
|
| 47 |
+
#avg_freq:
|
| 48 |
+
#ref_dataset:
|
| 49 |
+
#ref_sentences: random
|
| 50 |
+
|
| 51 |
+
# =====================
|
| 52 |
+
# DFA (two-expert + router) hyperparameters
|
| 53 |
+
# =====================
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# Training schedule
|
| 57 |
+
epochs_a: 1
|
| 58 |
+
epochs_b: 1
|
| 59 |
+
e2_top_k: 2 # top-k for E2 router
|
| 60 |
+
|
| 61 |
+
# Stage A (InfoNCE)
|
| 62 |
+
lr_e1: 1.0e-5
|
| 63 |
+
tau_con: 0.15 # contrastive temperature for InfoNCE
|
| 64 |
+
use_moco_queue: true # enable momentum queue for more negatives
|
| 65 |
+
moco_queue_size: 128 # queue size (default 4096, increases negative samples significantly)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
# Stage B (Task expert + Router)
|
| 71 |
+
lr_e2: 5.0e-4
|
| 72 |
+
|
| 73 |
+
text_lr_e2: 1.0e-5
|
| 74 |
+
lr_e2_router: 1.0e-3
|
| 75 |
+
lr_top_router: 1.0e-5
|
| 76 |
+
lambda_b_con: 0.001
|
| 77 |
+
tau_b_con: 0.19
|
| 78 |
+
num_task_experts: 2
|
| 79 |
+
seed: 43
|