"""Console script for clip_benchmark.""" import argparse import ast import csv import json import os import pathlib import random import sys from copy import copy from itertools import product from functools import partial # Determine the parent directory of the clip_benchmark package parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) # Add the parent directory to sys.path sys.path.insert(0, parent_dir) import torch from clip_benchmark.datasets.builder import (build_dataset, dataset_collection, get_dataset_collate_fn, get_dataset_collection_from_file, get_dataset_default_task, is_video_dataset, is_audio_dataset) from clip_benchmark.metrics import (linear_probe, multiclass_retrieval, visualization, zeroshot_classification, zeroshot_retrieval) from clip_benchmark.model_collection import (get_model_collection_from_file, model_collection) class ParseKwargs(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): # kw = {} kw = getattr(namespace, self.dest, {}) for value in values: key, value = value.split("=") try: kw[key] = ast.literal_eval(value) except ValueError: kw[key] = str( value ) # fallback to string (avoid need to escape on command line) setattr(namespace, self.dest, kw) from dataclasses import dataclass import core.vision_encoder.pe as pe import core.vision_encoder.transforms as transforms from core.audio_visual_encoder import PEAudioVisual, PEAudioVisualTransform @dataclass class Visualization: enabled: bool = False delete_post_ln: bool = False extract_layer: int = None attn_pooling: str = None # "K", "V" attn_rollout: bool = False def get_parser_args(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() parser_eval = subparsers.add_parser("eval", help="Evaluate") parser_eval.add_argument( "--dataset", type=str, default="cifar10", nargs="+", help="Dataset(s) to use for the benchmark. Can be the name of a dataset, or a collection name ('vtab', 'vtab+', 'imagenet_robustness', 'retrieval') or path of a text file where each line is a dataset name", ) parser_eval.add_argument( "--dataset_root", default="root", type=str, help="dataset root folder where the datasets are downloaded. Can be in the form of a template depending on dataset name, e.g., --dataset_root='datasets/{dataset}'. This is useful if you evaluate on multiple datasets.", ) parser_eval.add_argument( "--split", type=str, default="test", help="Dataset split to use" ) parser_eval.add_argument( "--test_split", dest="split", action="store", type=str, default="test", help="Dataset split to use", ) parser_eval.add_argument( "--train_split", type=str, nargs="+", default="train", help="Dataset(s) train split names", ) mutually_exclusive = parser_eval.add_mutually_exclusive_group() mutually_exclusive.add_argument( "--val_split", default=None, type=str, nargs="+", help="Dataset(s) validation split names. Mutually exclusive with val_proportion.", ) mutually_exclusive.add_argument( "--val_proportion", default=None, type=float, nargs="+", help="what is the share of the train dataset will be used for validation part, if it doesn't predefined. Mutually exclusive with val_split", ) parser_eval.add_argument( "--model", type=str, nargs="+", default=["ViT-B-32-quickgelu"], help="Model architecture to use from OpenCLIP", ) parser_eval.add_argument( "--pretrained", type=str, nargs="+", default=["laion400m_e32"], help="Model checkpoint name to use from OpenCLIP", ) parser_eval.add_argument( "--pretrained_model", type=str, default="", nargs="+", help="Pre-trained model(s) to use. Can be the full model name where `model` and `pretrained` are comma separated (e.g., --pretrained_model='ViT-B-32-quickgelu,laion400m_e32'), a model collection name ('openai' or 'openclip_base' or 'openclip_multilingual' or 'openclip_all'), or path of a text file where each line is a model fullname where model and pretrained are comma separated (e.g., ViT-B-32-quickgelu,laion400m_e32). --model and --pretrained are ignored if --pretrained_model is used.", ) parser_eval.add_argument( "--task", type=str, default="auto", choices=[ "zeroshot_classification", "zeroshot_retrieval", "multiclass_retreival", "linear_probe", "auto", ], help="Task to evaluate on. With --task=auto, the task is automatically inferred from the dataset.", ) parser_eval.add_argument( "--no_amp", action="store_false", dest="amp", default=True, help="whether to use mixed precision", ) parser_eval.add_argument("--num_workers", default=4, type=int) parser_eval.add_argument( "--recall_k", default=[1, 5, 10], type=int, help="for retrieval, select the k for Recall@K metric. ", nargs="+", ) parser_eval.add_argument( "--fewshot_k", default=-1, type=int, help="for linear probe, how many shots. -1 = whole dataset.", ) parser_eval.add_argument( "--fewshot_epochs", default=10, type=int, help="for linear probe, how many epochs.", ) parser_eval.add_argument( "--fewshot_lr", default=0.1, type=float, help="for linear probe, what is the learning rate.", ) parser_eval.add_argument( "--skip_load", action="store_true", help="for linear probes, when everything is cached, no need to load model.", ) parser_eval.add_argument( "--distributed", action="store_true", help="evaluation in parallel" ) parser_eval.add_argument("--seed", default=0, type=int, help="random seed.") parser_eval.add_argument("--batch_size", default=64, type=int) parser_eval.add_argument( "--normalize", default=True, type=bool, help="features normalization" ) parser_eval.add_argument( "--model_cache_dir", default=None, type=str, help="directory to where downloaded models are cached", ) parser_eval.add_argument( "--feature_root", default="features", type=str, help="feature root folder where the features are stored.", ) parser_eval.add_argument( "--annotation_file", default="", type=str, help="text annotation file for retrieval datasets. Only needed for when `--task` is `zeroshot_retrieval`.", ) parser_eval.add_argument( "--custom_classname_file", default=None, type=str, help="use custom json file with classnames for each dataset, where keys are dataset names and values are list of classnames.", ) parser_eval.add_argument( "--custom_template_file", default=None, type=str, help="use custom json file with prompts for each dataset, where keys are dataset names and values are list of prompts. For instance, to use CuPL prompts, use --custom_template_file='cupl_prompts.json'", ) parser_eval.add_argument( "--dump_classnames", default=False, action="store_true", help="dump classnames to the results json file.", ) parser_eval.add_argument( "--dump_templates", default=False, action="store_true", help="dump templates to the results json file.", ) parser_eval.add_argument( "--name", default=None, type=str, help="Overwrite the name used." ) parser_eval.add_argument( "--image-mean", type=float, nargs="+", default=None, metavar="MEAN", help="Override default image mean value of dataset", ) parser_eval.add_argument( "--image-std", type=float, nargs="+", default=None, metavar="STD", help="Override default image std deviation of of dataset", ) parser_eval.add_argument( "--force-preprocess-cfg", nargs="*", default={}, action=ParseKwargs ) parser_eval.add_argument( "--force-vision-cfg", nargs="*", default={}, action=ParseKwargs, help="Overwrite fields of the vision cfg with the args. Only specified kwdargs will be updated.", ) parser_eval.add_argument( "--num-frames", default=8, type=int, help="number of frames to use for video datasets", ) parser_eval.add_argument( "--reweight-retrieval", default=True, action=argparse.BooleanOptionalAction, help="use the softmax trick to reweight the retrieval scores", ) parser_eval.add_argument( "--reweight-scale", default=1.0, type=float, help="Scale the scores prior to doing the softmax trick to reweight" ) parser_eval.add_argument( "--visualize", nargs="*", default={}, action=ParseKwargs, help="Visualization tool. Spits out visualizations as just bytes in the console. To be read by bento.", ) parser_eval.add_argument( "--language", default="en", type=str, nargs="+", help="language(s) of classname and prompts to use for zeroshot classification.", ) parser_eval.add_argument( "--output", default="result.json", type=str, help="output file where to dump the metrics. Can be in form of a template, e.g., --output='{dataset}_{pretrained}_{model}_{language}_{task}.json'", ) parser_eval.add_argument( "--quiet", dest="verbose", action="store_false", help="suppress verbose messages", ) parser_eval.add_argument( "--save_clf", default=None, type=str, help="optionally save the classification layer output by the text tower", ) parser_eval.add_argument( "--load_clfs", nargs="+", default=[], type=str, help="optionally load and average mutliple layers output by text towers.", ) parser_eval.add_argument( "--skip_existing", default=False, action="store_true", help="whether to skip an evaluation if the output file exists.", ) parser_eval.add_argument( "--model_type", default="open_clip", type=str, help="clip model type" ) parser_eval.add_argument( "--wds_cache_dir", default=None, type=str, help="optional cache directory for webdataset only", ) parser_eval.set_defaults(which="eval") parser_build = subparsers.add_parser("build", help="Build CSV from evaluations") parser_build.add_argument( "files", type=str, nargs="+", help="path(s) of JSON result files" ) parser_build.add_argument( "--output", type=str, default="benchmark.csv", help="CSV output file" ) parser_build.set_defaults(which="build") args = parser.parse_args() return parser, args def main(): parser, base = get_parser_args() if not hasattr(base, "which"): parser.print_help() return if base.which == "eval": main_eval(base) elif base.which == "build": main_build(base) def main_build(base): # Build a benchmark single CSV file from a set of evaluations (JSON files) rows = [] fieldnames = set() def process_file(path: str): data = json.load(open(path)) row = {} row.update(data["metrics"]) row.update(data) del row["metrics"] row["model_fullname"] = row["model"] + " " + row["pretrained"] for field in row.keys(): fieldnames.add(field) rows.append(row) for path in base.files: if os.path.isdir(path): files = [ os.path.join(path, f) for f in os.listdir(path) if f.endswith(".json") ] for file in files: process_file(file) else: process_file(path) with open(base.output, "w") as csvfile: writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() for row in rows: writer.writerow(row) def main_eval(base): # Get list of pre-trained models to evaluate pretrained_model = _as_list(base.pretrained_model) if pretrained_model: models = [] for name in pretrained_model: if os.path.isfile(name): # if path, read file, each line is a pre-trained model models.extend(get_model_collection_from_file(name)) elif name in model_collection: # if part of `model_collection`, retrieve from it models.extend(model_collection[name]) else: # if not, assume it is in the form of `model,pretrained` model, pretrained = name.split(",") models.append((model, pretrained)) else: models = list(product(base.model, base.pretrained)) # Get list of datasets to evaluate on datasets = [] for name in _as_list(base.dataset): if os.path.isfile(name): # If path, read file, each line is a dataset name datasets.extend(get_dataset_collection_from_file(name)) elif name in dataset_collection: # if part of `dataset_collection`, retrieve from it datasets.extend(dataset_collection[name]) else: # if not, assume it is simply the name of the dataset datasets.append(name) train_splits = _as_list(base.train_split) train_splits = _single_option_to_multiple_datasets( train_splits, datasets, "train_split" ) proportions, val_splits = None, None if base.val_split is not None: val_splits = _as_list(base.val_split) val_splits = _single_option_to_multiple_datasets( val_splits, datasets, "val_split" ) if base.val_proportion is not None: proportions = _as_list(base.val_proportion) proportions = _single_option_to_multiple_datasets( proportions, datasets, "val_proportion" ) dataset_info = {} for i in range(len(datasets)): dataset_info[datasets[i]] = { "train_split": train_splits[i], "val_split": val_splits[i] if val_splits is not None else None, "proportion": proportions[i] if proportions is not None else None, } # Get list of languages to evaluate on languages = _as_list(base.language) if base.verbose: print(f"Models: {models}") print(f"Datasets: {datasets}") print(f"Languages: {languages}") runs = product(models, datasets, languages) if base.distributed: local_rank, rank, world_size = world_info_from_env() runs = list(runs) # randomize runs so that runs are balanced across gpus random.seed(base.seed) random.shuffle(runs) runs = [r for i, r in enumerate(runs) if i % world_size == rank] for (model, pretrained), (dataset), (language) in runs: # We iterative over all possible model/dataset/languages args = copy(base) args.model = model args.pretrained = pretrained args.dataset = dataset args.language = language args.train_split = dataset_info[dataset]["train_split"] args.val_split = dataset_info[dataset]["val_split"] args.val_proportion = dataset_info[dataset]["proportion"] run(args) def _as_list(l): if not l: return [] return [l] if type(l) != list else l def _single_option_to_multiple_datasets(cur_option, datasets, name): cur_len = len(cur_option) ds_len = len(datasets) if cur_len != ds_len: # If user wants to use same value for all datasets if cur_len == 1: return [cur_option[0]] * ds_len else: raise ValueError(f"The incommensurable number of {name}") else: return cur_option def get_basename_and_parent_folder(path): """ Returns the basename and the folder two parents above. Args: path (str): The input path. Returns: str: The basename and the folder two parents above. """ p = pathlib.Path(path) parent_folder = ( p.parents[1].name if len(p.parents) >= 2 else "" ) # Get the name of the parent folder two levels up basename = p.stem # Get the basename return f"{parent_folder}-{basename}" def run(args): """Console script for clip_benchmark.""" if torch.cuda.is_available(): if args.distributed: local_rank, rank, world_size = world_info_from_env() device = "cuda:%d" % local_rank torch.cuda.set_device(device) else: device = "cuda" args.device = device else: args.device = "cpu" # set seed. torch.manual_seed(args.seed) task = args.task if args.dataset.startswith("wds/"): dataset_name = args.dataset.replace("wds/", "", 1) else: dataset_name = args.dataset if task == "auto": task = get_dataset_default_task(dataset_name) local_model_flag = os.path.isfile(args.pretrained) or os.path.isdir(args.pretrained) pretrained_slug = ( get_basename_and_parent_folder(args.pretrained) if local_model_flag else args.pretrained ) pretrained_slug_full_path = ( args.pretrained.replace("/", "_") if os.path.isfile(args.pretrained) else args.pretrained ) dataset_slug = dataset_name.replace("/", "_") output = args.output.format( model=args.model, pretrained=( pretrained_slug if args.name is None else f"{pretrained_slug}-{args.name}" ), pretrained_full_path=pretrained_slug_full_path, task=task, dataset=dataset_slug, language=args.language, num_frames=args.num_frames, ) # hack to replace timm/hf model output = output.replace("timm/", "timm_") if os.path.exists(output) and args.skip_existing: if args.verbose: print(f"Skip {output}, exists already.") return if args.verbose: print( f"Running '{task}' on '{dataset_name}' with the model '{args.pretrained}' on language '{args.language}'" ) dataset_root = args.dataset_root.format( dataset=dataset_name, dataset_cleaned=dataset_name.replace("/", "-") ) if args.skip_load: model, transform, collate_fn, dataloader = None, None, None, None else: if args.model.startswith("pe-av"): # Load PE-AV model model = PEAudioVisual.from_config(args.model, pretrained=True).cuda() transform = PEAudioVisualTransform.from_config(args.model) tokenizer = partial(transform.tokenizer, padding=True, return_tensors="pt") else: model_name = args.model model = pe.CLIP.from_config(model_name, pretrained=True) # Downloads from HF model = model.cuda() transform = transforms.get_image_transform(model.image_size) tokenizer = transforms.get_text_tokenizer(model.context_length) model.eval() dataset = build_dataset( dataset_name=args.dataset, root=dataset_root, transform=transform, split=args.split, annotation_file=args.annotation_file, download=True, language=args.language, task=task, custom_template_file=args.custom_template_file, custom_classname_file=args.custom_classname_file, wds_cache_dir=args.wds_cache_dir, num_frames=args.num_frames, ) if hasattr(dataset, "collate_fn"): collate_fn = dataset.collate_fn else: collate_fn = get_dataset_collate_fn(args.dataset) if hasattr(transform, "collate_fn"): # Union the dataloader's collate fn (which deals with images) with clipbench's collate fn (which deals with text) def collate_union(collate_img, collate_text): def collate(batch): img_batch = collate_img([(x[0], torch.zeros(1)) for x in batch]) text_batch = collate_text( [[torch.zeros(3, 224, 224)] + list(x[1:]) for x in batch] ) return tuple(list(img_batch[:1]) + list(text_batch[1:])) return collate collate_fn = collate_union(transform.collate_fn, collate_fn) def test(*args, **kwargs): breakpoint() return collate_fn(*args, **kwargs) if args.verbose: try: print(f"Dataset size: {len(dataset)}") except TypeError: print("IterableDataset has no len()") print(f"Dataset split: {args.split}") if hasattr(dataset, "classes") and dataset.classes: try: print(f"Dataset classes: {dataset.classes}") print(f"Dataset number of classes: {len(dataset.classes)}") except AttributeError: print("Dataset has no classes.") if args.dataset.startswith("wds/"): dataloader = torch.utils.data.DataLoader( dataset.batched(args.batch_size, collation_fn=collate_fn), batch_size=None, shuffle=False, num_workers=args.num_workers, ) else: dataloader = torch.utils.data.DataLoader( dataset, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, collate_fn=collate_fn, ) if task == "zeroshot_classification": zeroshot_templates = ( dataset.templates if hasattr(dataset, "templates") else None ) if args.verbose: print(f"Zero-shot templates: {zeroshot_templates}") classnames = dataset.classes if hasattr(dataset, "classes") else None assert ( zeroshot_templates is not None and classnames is not None ), "Dataset does not support classification" metrics = zeroshot_classification.evaluate( model, dataloader, tokenizer, classnames, zeroshot_templates, video_dataset=is_video_dataset(args.dataset), device=args.device, amp=args.amp, verbose=args.verbose, save_clf=args.save_clf, load_clfs=args.load_clfs, args=args, ) elif task == "zeroshot_retrieval": metrics = zeroshot_retrieval.evaluate( model, dataloader, tokenizer, video_dataset=is_video_dataset(args.dataset), recall_k_list=args.recall_k, device=args.device, amp=args.amp, args=args, audio_dataset=is_audio_dataset(args.dataset), transform=transform, ) elif task == "multiclass_retrieval": metrics = multiclass_retrieval.evaluate( model, dataloader, tokenizer, device=args.device, amp=args.amp, args=args, retrieval_template=dataset.retrieval_template, ) elif task == "linear_probe": # we also need the train and validation splits for linear probing. train_dataset = None train_dataset = build_dataset( dataset_name=args.dataset, root=dataset_root, transform=transform, split=args.train_split, annotation_file=args.annotation_file, download=True, ) if args.val_split is not None: val_dataset = build_dataset( dataset_name=args.dataset, root=dataset_root, transform=transform, split=args.val_split, annotation_file=args.annotation_file, download=True, ) elif args.val_proportion is not None: train_dataset, val_dataset = torch.utils.data.random_split( train_dataset, [1 - args.val_proportion, args.val_proportion] ) else: val_dataset = None train_dataloader = torch.utils.data.DataLoader( train_dataset, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, collate_fn=collate_fn, pin_memory=True, ) if val_dataset is not None: val_dataloader = torch.utils.data.DataLoader( val_dataset, batch_size=args.batch_size, shuffle=False, num_workers=args.num_workers, collate_fn=collate_fn, pin_memory=True, ) else: val_dataloader = None metrics = linear_probe.evaluate( model, train_dataloader, dataloader, args.fewshot_k, args.batch_size, args.num_workers, args.fewshot_lr, args.fewshot_epochs, (args.model + "-" + args.pretrained + "-" + args.dataset).replace("/", "_"), args.seed, args.feature_root, val_dataloader=val_dataloader, device=args.device, normalize=args.normalize, amp=args.amp, verbose=args.verbose, ) else: raise ValueError( "Unsupported task: {}. task should be `zeroshot_classification`, `zeroshot_retrieval`, `multiclass_retrieval`, `linear_probe`, or `captioning`".format( task ) ) dump = { "dataset": args.dataset, "model": args.model, "pretrained": args.pretrained, "task": task, "metrics": metrics, "language": args.language, "name": args.name if args.name is not None else "", } if hasattr(dataset, "classes") and dataset.classes and args.dump_classnames: dump["classnames"] = dataset.classes if hasattr(dataset, "templates") and dataset.templates and args.dump_templates: dump["templates"] = dataset.templates if args.verbose: print(f"Dump results to: {output}") os.makedirs(os.path.dirname(output), exist_ok=True) with open(output, "w") as f: json.dump(dump, f) return 0 def world_info_from_env(): # from openclip local_rank = 0 for v in ( "LOCAL_RANK", "MPI_LOCALRANKID", "SLURM_LOCALID", "OMPI_COMM_WORLD_LOCAL_RANK", ): if v in os.environ: local_rank = int(os.environ[v]) break global_rank = 0 for v in ("RANK", "PMI_RANK", "SLURM_PROCID", "OMPI_COMM_WORLD_RANK"): if v in os.environ: global_rank = int(os.environ[v]) break world_size = 1 for v in ("WORLD_SIZE", "PMI_SIZE", "SLURM_NTASKS", "OMPI_COMM_WORLD_SIZE"): if v in os.environ: world_size = int(os.environ[v]) break return local_rank, global_rank, world_size if __name__ == "__main__": sys.exit(main()) # pragma: no cover