The update focuses on several components: * We added "official" texts support and the corresponding Python examples. (Note, texts support in Pipelines is not supported yet.) * We introduced the `RetrievalResults` (`RR`) class — a container to store gallery items retrieved for given queries. `RR` provides a unified way to visualize predictions and compute metrics (if ground truths are known). It also simplifies post-processing, where an `RR` object is taken as input and another `RR_upd` is produced as output. Having these two objects allows comparison retrieval results visually or by metrics. Moreover, you can easily create a chain of such post-processors. * `RR` is memory optimized because of using batching: in other words, it doesn't store full matrix of query-gallery distances. (It doesn't make search approximate though). * We made `Model` and `Dataset` the only classes responsible for processing modality-specific logic. `Model` is responsible for interpreting its input dimensions: for example, `BxCxHxW` for images or `BxLxD` for sequences like texts. `Dataset` is responsible for preparing an item: it may use `Transforms` for images or `Tokenizer` for texts. Functions computing metrics like `calc_retrieval_metrics_rr`, `RetrievalResults`, `PairwiseReranker`, and other classes and functions are unified to work with any modality. * We added `IVisualizableDataset` having method `.visaulize()` that shows a single item. If implemented, `RetrievalResults` is able to show the layout of retrieved results. #### Migration from OML 2.* [Python API]: The easiest way to catch up with changes is to re-read the examples! * The recommended way of validation is to use `RetrievalResults` and functions like `calc_retrieval_metrics_rr`, `calc_fnmr_at_fmr_rr`, and others. The `EmbeddingMetrics` class is kept for use with PyTorch Lightning and inside Pipelines. Note, the signatures of `EmbeddingMetrics` methods have been slightly changed, see Lightning examples for that. * Since modality-specific logic is confined to `Dataset`, it doesn't output `PATHS_KEY`, `X1_KEY`, `X2_KEY`, `Y1_KEY`, and `Y2_KEY` anymore. Keys which are not modality-specific like `LABELS_KEY`, `IS_GALLERY`, `IS_QUERY_KEY`, `CATEGORIES_KEY` are still in use. * `inference_on_images` is now `inference` and works with any modality. * Slightly changed interfaces of `Datasets.` For example, we have `IQueryGalleryDataset` and `IQueryGalleryLabeledDataset` interfaces. The first has to be used for inference, the second one for validation. Also added `IVisualizableDataset` interface. * Removed some internals like `IMetricDDP`, `EmbeddingMetricsDDP`, `calc_distance_matrix`, `calc_gt_mask`, `calc_mask_to_ignore`, `apply_mask_to_ignore`. These changes shouldn't affect you. Also removed code related to a pipeline with precomputed triplets. #### Migration from OML 2.* [Pipelines]: * [Feature extraction](https://github.com/OML-Team/open-metric-learning/tree/main/pipelines/features_extraction): No changes, except for adding an optional argument — `mode_for_checkpointing = (min | max)`. It may be useful to switch between *the lower, the better* and *the greater, the better* type of metrics. * [Pairwise-postprocessing pipeline](https://github.com/OML-Team/open-metric-learning/tree/main/pipelines/postprocessing/pairwise_postprocessing): Slightly changed the name and arguments of the `postprocessor` sub config — `pairwise_images` is now `pairwise_reranker` and doesn't need transforms.
You may think *"If I need image embeddings I can simply train a vanilla classifier and take its penultimate layer"*. Well, it makes sense as a starting point. But there are several possible drawbacks: * If you want to use embeddings to perform searching you need to calculate some distance among them (for example, cosine or L2). Usually, **you don't directly optimize these distances during the training** in the classification setup. So, you can only hope that final embeddings will have the desired properties. * **The second problem is the validation process**. In the searching setup, you usually care how related your top-N outputs are to the query. The natural way to evaluate the model is to simulate searching requests to the reference set and apply one of the retrieval metrics. So, there is no guarantee that classification accuracy will correlate with these metrics. * Finally, you may want to implement a metric learning pipeline by yourself. **There is a lot of work**: to use triplet loss you need to form batches in a specific way, implement different kinds of triplets mining, tracking distances, etc. For the validation, you also need to implement retrieval metrics, which include effective embeddings accumulation during the epoch, covering corner cases, etc. It's even harder if you have several gpus and use DDP. You may also want to visualize your search requests by highlighting good and bad search results. Instead of doing it by yourself, you can simply use OML for your purposes.
[PML](https://github.com/KevinMusgrave/pytorch-metric-learning) is the popular library for Metric Learning, and it includes a rich collection of losses, miners, distances, and reducers; that is why we provide straightforward [examples](https://open-metric-learning.readthedocs.io/en/latest/feature_extraction/python_examples.html#usage-with-pytorch-metric-learning) of using them with OML. Initially, we tried to use PML, but in the end, we came up with our library, which is more pipeline / recipes oriented. That is how OML differs from PML: * OML has [Pipelines](https://github.com/OML-Team/open-metric-learning/tree/main/pipelines) which allows training models by preparing a config and your data in the required format (it's like converting data into COCO format to train a detector from [mmdetection](https://github.com/open-mmlab/mmdetection)). * OML focuses on end-to-end pipelines and practical use cases. It has config based examples on popular benchmarks close to real life (like photos of products of thousands ids). We found some good combinations of hyperparameters on these datasets, trained and published models and their configs. Thus, it makes OML more recipes oriented than PML, and its author [confirms](https://github.com/KevinMusgrave/pytorch-metric-learning/issues/169#issuecomment-670814393) this saying that his library is a set of tools rather the recipes, moreover, the examples in PML are mostly for CIFAR and MNIST datasets. * OML has the [Zoo](https://github.com/OML-Team/open-metric-learning#zoo) of pretrained models that can be easily accessed from the code in the same way as in `torchvision` (when you type `resnet50(pretrained=True)`). * OML is integrated with [PyTorch Lightning](https://www.pytorchlightning.ai/), so, we can use the power of its [Trainer](https://pytorch-lightning.readthedocs.io/en/stable/common/trainer.html). This is especially helpful when we work with DDP, so, you compare our [DDP example](https://open-metric-learning.readthedocs.io/en/latest/feature_extraction/python_examples.html#usage-with-pytorch-lightning) and the [PMLs one](https://github.com/KevinMusgrave/pytorch-metric-learning/blob/master/examples/notebooks/DistributedTripletMarginLossMNIST.ipynb). By the way, PML also has [Trainers](https://kevinmusgrave.github.io/pytorch-metric-learning/trainers/), but it's not widely used in the examples and custom `train` / `test` functions are used instead. We believe that having Pipelines, laconic examples, and Zoo of pretrained models sets the entry threshold to a really low value.
Metric Learning problem (also known as *extreme classification* problem) means a situation in which we have thousands of ids of some entities, but only a few samples for every entity. Often we assume that during the test stage (or production) we will deal with unseen entities which makes it impossible to apply the vanilla classification pipeline directly. In many cases obtained embeddings are used to perform search or matching procedures over them. Here are a few examples of such tasks from the computer vision sphere: * Person/Animal Re-Identification * Face Recognition * Landmark Recognition * Searching engines for online shops and many others.
* `embedding` - model's output (also known as `features vector` or `descriptor`). * `query` - a sample which is used as a request in the retrieval procedure. * `gallery set` - the set of entities to search items similar to `query` (also known as `reference` or `index`). * `Sampler` - an argument for `DataLoader` which is used to form batches * `Miner` - the object to form pairs or triplets after the batch was formed by `Sampler`. It's not necessary to form the combinations of samples only inside the current batch, thus, the memory bank may be a part of `Miner`. * `Samples`/`Labels`/`Instances` - as an example let's consider DeepFashion dataset. It includes thousands of fashion item ids (we name them `labels`) and several photos for each item id (we name the individual photo as `instance` or `sample`). All of the fashion item ids have their groups like "skirts", "jackets", "shorts" and so on (we name them `categories`). Note, we avoid using the term `class` to avoid misunderstanding. * `training epoch` - batch samplers which we use for combination-based losses usually have a length equal to `[number of labels in training dataset] / [numbers of labels in one batch]`. It means that we don't observe all of the available training samples in one epoch (as opposed to vanilla classification), instead, we observe all of the available labels.
It may be comparable with the current (2022 year) [SotA](https://paperswithcode.com/task/metric-learning) methods, for example, [Hyp-ViT](https://arxiv.org/pdf/2203.10833.pdf). *(Few words about this approach: it's a ViT architecture trained with contrastive loss, but the embeddings were projected into some hyperbolic space. As the authors claimed, such a space is able to describe the nested structure of real-world data. So, the paper requires some heavy math to adapt the usual operations for the hyperbolical space.)* We trained the same architecture with triplet loss, fixing the rest of the parameters: training and test transformations, image size, and optimizer. See configs in [Models Zoo](https://github.com/OML-Team/open-metric-learning#zoo). The trick was in heuristics in our miner and sampler: * [Category Balance Sampler](https://open-metric-learning.readthedocs.io/en/latest/contents/samplers.html#categorybalancesampler) forms the batches limiting the number of categories *C* in it. For instance, when *C = 1* it puts only jackets in one batch and only jeans into another one (just an example). It automatically makes the negative pairs harder: it's more meaningful for a model to realise why two jackets are different than to understand the same about a jacket and a t-shirt. * [Hard Triplets Miner](https://open-metric-learning.readthedocs.io/en/latest/contents/miners.html#hardtripletsminer) makes the task even harder keeping only the hardest triplets (with maximal positive and minimal negative distances). Here are *CMC@1* scores for 2 popular benchmarks. SOP dataset: Hyp-ViT — 85.9, ours — 86.6. DeepFashion dataset: Hyp-ViT — 92.5, ours — 92.1. Thus, utilising simple heuristics and avoiding heavy math we are able to perform on SotA level.
Recent research in SSL definitely obtained great results. The problem is that these approaches required an enormous amount of computing to train the model. But in our framework, we consider the most common case when the average user has no more than a few GPUs. At the same time, it would be unwise to ignore success in this sphere, so we still exploit it in two ways: * As a source of checkpoints that would be great to start training with. From publications and our experience, they are much better as initialisation than the default supervised model trained on ImageNet. Thus, we added the possibility to initialise your models using these pretrained checkpoints only by passing an argument in the config or the constructor. * As a source of inspiration. For example, we adapted the idea of a memory bank from *MoCo* for the *TripletLoss*.
No, you don't. OML is a framework-agnostic. Despite we use PyTorch Lightning as a loop runner for the experiments, we also keep the possibility to run everything on pure PyTorch. Thus, only the tiny part of OML is Lightning-specific and we keep this logic separately from other code (see `oml.lightning`). Even when you use Lightning, you don't need to know it, since we provide ready to use [Pipelines](https://github.com/OML-Team/open-metric-learning/blob/main/pipelines/). The possibility of using pure PyTorch and modular structure of the code leaves a room for utilizing OML with your favourite framework after the implementation of the necessary wrappers.
Yes. To run the experiment with [Pipelines](https://github.com/OML-Team/open-metric-learning/blob/main/pipelines/) you only need to write a converter to our format (it means preparing the `.csv` table with a few predefined columns). That's it! Probably we already have a suitable pre-trained model for your domain in our *Models Zoo*. In this case, you don't even need to train it.
Currently, we don't support exporting models to ONNX directly. However, you can use the built-in PyTorch capabilities to achieve this. For more information, please refer to this [issue](https://github.com/OML-Team/open-metric-learning/issues/592).
Losses | Miners ```python miner = AllTripletsMiner() miner = NHardTripletsMiner() miner = MinerWithBank() ... criterion = TripletLossWithMiner(0.1, miner) criterion = ArcFaceLoss() criterion = SurrogatePrecision() ``` | Samplers ```python labels = train.get_labels() l2c = train.get_label2category() sampler = BalanceSampler(labels) sampler = CategoryBalanceSampler(labels, l2c) sampler = DistinctCategoryBalanceSampler(labels, l2c) ``` |
Configs support ```yaml max_epochs: 10 sampler: name: balance args: n_labels: 2 n_instances: 2 ``` | Pre-trained models ```python model_hf = AutoModel.from_pretrained("roberta-base") tokenizer = AutoTokenizer.from_pretrained("roberta-base") extractor_txt = HFWrapper(model_hf) extractor_img = ViTExtractor.from_pretrained("vits16_dino") transforms, _ = get_transforms_for_pretrained("vits16_dino") ``` |
Post-processing ```python emb = inference(extractor, dataset) rr = RetrievalResults.from_embeddings(emb, dataset) postprocessor = AdaptiveThresholding() rr_upd = postprocessor.process(rr, dataset) ``` | Post-processing by NN | Paper ```python embeddings = inference(extractor, dataset) rr = RetrievalResults.from_embeddings(embeddings, dataset) postprocessor = PairwiseReranker(ConcatSiamese(), top_n=3) rr_upd = postprocessor.process(rr, dataset) ``` |
Logging ```python logger = TensorBoardPipelineLogger() logger = NeptunePipelineLogger() logger = WandBPipelineLogger() logger = MLFlowPipelineLogger() logger = ClearMLPipelineLogger() ``` |
PML ```python from pytorch_metric_learning import losses criterion = losses.TripletMarginLoss(0.2, "all") pred = ViTExtractor()(data) criterion(pred, gts) ``` |
Categories support ```python # train loader = DataLoader(CategoryBalanceSampler()) # validation rr = RetrievalResults.from_embeddings() m.calc_retrieval_metrics_rr(rr, query_categories) ``` | Misc metrics ```python embeddigs = inference(model, dataset) rr = RetrievalResults.from_embeddings(embeddings, dataset) m.calc_retrieval_metrics_rr(rr, precision_top_k=(5,)) m.calc_fnmr_at_fmr_rr(rr, fmr_vals=(0.1,)) m.calc_topological_metrics(embeddings, pcf_variance=(0.5,)) ``` |
Lightning ```python import pytorch_lightning as pl model = ViTExtractor.from_pretrained("vits16_dino") clb = MetricValCallback(EmbeddingMetrics(dataset)) module = ExtractorModule(model, criterion, optimizer) trainer = pl.Trainer(max_epochs=3, callbacks=[clb]) trainer.fit(module, train_loader, val_loader) ``` |
Lightning DDP ```python clb = MetricValCallback(EmbeddingMetrics(val)) module = ExtractorModuleDDP( model, criterion, optimizer, train, val ) ddp = {"devices": 2, "strategy": DDPStrategy()} trainer = pl.Trainer(max_epochs=3, callbacks=[clb], **ddp) trainer.fit(module) ``` |
IMAGES | TEXTS |
[comment]:train-val-img-start ```python from torch.optim import Adam from torch.utils.data import DataLoader from oml import datasets as d from oml.inference import inference from oml.losses import TripletLossWithMiner from oml.metrics import calc_retrieval_metrics_rr from oml.miners import AllTripletsMiner from oml.models import ViTExtractor from oml.registry import get_transforms_for_pretrained from oml.retrieval import RetrievalResults, AdaptiveThresholding from oml.samplers import BalanceSampler from oml.utils import get_mock_images_dataset model = ViTExtractor.from_pretrained("vits16_dino").to("cpu").train() transform, _ = get_transforms_for_pretrained("vits16_dino") df_train, df_val = get_mock_images_dataset(global_paths=True) train = d.ImageLabeledDataset(df_train, transform=transform) val = d.ImageQueryGalleryLabeledDataset(df_val, transform=transform) optimizer = Adam(model.parameters(), lr=1e-4) criterion = TripletLossWithMiner(0.1, AllTripletsMiner(), need_logs=True) sampler = BalanceSampler(train.get_labels(), n_labels=2, n_instances=2) def training(): for batch in DataLoader(train, batch_sampler=sampler): embeddings = model(batch["input_tensors"]) loss = criterion(embeddings, batch["labels"]) loss.backward() optimizer.step() optimizer.zero_grad() print(criterion.last_logs) def validation(): embeddings = inference(model, val, batch_size=4, num_workers=0) rr = RetrievalResults.from_embeddings(embeddings, val, n_items=3) rr = AdaptiveThresholding(n_std=2).process(rr) rr.visualize(query_ids=[2, 1], dataset=val, show=True) print(calc_retrieval_metrics_rr(rr, map_top_k=(3,), cmc_top_k=(1,))) training() validation() ``` [comment]:train-val-img-end | [comment]:train-val-txt-start ```python from torch.optim import Adam from torch.utils.data import DataLoader from transformers import AutoModel, AutoTokenizer from oml import datasets as d from oml.inference import inference from oml.losses import TripletLossWithMiner from oml.metrics import calc_retrieval_metrics_rr from oml.miners import AllTripletsMiner from oml.models import HFWrapper from oml.retrieval import RetrievalResults, AdaptiveThresholding from oml.samplers import BalanceSampler from oml.utils import get_mock_texts_dataset model = HFWrapper(AutoModel.from_pretrained("bert-base-uncased"), 768).to("cpu").train() tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") df_train, df_val = get_mock_texts_dataset() train = d.TextLabeledDataset(df_train, tokenizer=tokenizer) val = d.TextQueryGalleryLabeledDataset(df_val, tokenizer=tokenizer) optimizer = Adam(model.parameters(), lr=1e-4) criterion = TripletLossWithMiner(0.1, AllTripletsMiner(), need_logs=True) sampler = BalanceSampler(train.get_labels(), n_labels=2, n_instances=2) def training(): for batch in DataLoader(train, batch_sampler=sampler): embeddings = model(batch["input_tensors"]) loss = criterion(embeddings, batch["labels"]) loss.backward() optimizer.step() optimizer.zero_grad() print(criterion.last_logs) def validation(): embeddings = inference(model, val, batch_size=4, num_workers=0) rr = RetrievalResults.from_embeddings(embeddings, val, n_items=3) rr = AdaptiveThresholding(n_std=2).process(rr) rr.visualize(query_ids=[2, 1], dataset=val, show=True) print(calc_retrieval_metrics_rr(rr, map_top_k=(3,), cmc_top_k=(1,))) training() validation() ``` [comment]:train-val-txt-end |
Output```python {'active_tri': 0.125, 'pos_dist': 82.5, 'neg_dist': 100.5} # batch 1 {'active_tri': 0.0, 'pos_dist': 36.3, 'neg_dist': 56.9} # batch 2 {'cmc': {1: 0.75}, 'precision': {5: 0.75}, 'map': {3: 0.8}} ``` |
Output```python {'active_tri': 0.0, 'pos_dist': 8.5, 'neg_dist': 11.0} # batch 1 {'active_tri': 0.25, 'pos_dist': 8.9, 'neg_dist': 9.8} # batch 2 {'cmc': {1: 0.8}, 'precision': {5: 0.7}, 'map': {3: 0.9}} ``` |
[comment]:usage-retrieval-start ```python from oml.datasets import ImageQueryGalleryDataset from oml.inference import inference from oml.models import ViTExtractor from oml.registry import get_transforms_for_pretrained from oml.utils import get_mock_images_dataset from oml.retrieval import RetrievalResults, AdaptiveThresholding _, df_test = get_mock_images_dataset(global_paths=True) del df_test["label"] # we don't need gt labels for doing predictions extractor = ViTExtractor.from_pretrained("vits16_dino").to("cpu") transform, _ = get_transforms_for_pretrained("vits16_dino") dataset = ImageQueryGalleryDataset(df_test, transform=transform) embeddings = inference(extractor, dataset, batch_size=4, num_workers=0) rr = RetrievalResults.from_embeddings(embeddings, dataset, n_items=5) rr = AdaptiveThresholding(n_std=3.5).process(rr) rr.visualize(query_ids=[0, 1], dataset=dataset, show=True) # you get the ids of retrieved items and the corresponding distances print(rr) ``` [comment]:usage-retrieval-end
[comment]:usage-streaming-retrieval-start ```python import pandas as pd from oml.datasets import ImageBaseDataset from oml.inference import inference from oml.models import ViTExtractor from oml.registry import get_transforms_for_pretrained from oml.retrieval import RetrievalResults, ConstantThresholding from oml.utils import get_mock_images_dataset extractor = ViTExtractor.from_pretrained("vits16_dino").to("cpu") transform, _ = get_transforms_for_pretrained("vits16_dino") paths = pd.concat(get_mock_images_dataset(global_paths=True))["path"] galleries, queries1, queries2 = paths[:20], paths[20:22], paths[22:24] # gallery is huge and fixed, so we only process it once dataset_gallery = ImageBaseDataset(galleries, transform=transform) embeddings_gallery = inference(extractor, dataset_gallery, batch_size=4, num_workers=0) # queries come "online" in stream for queries in [queries1, queries2]: dataset_query = ImageBaseDataset(queries, transform=transform) embeddings_query = inference(extractor, dataset_query, batch_size=4, num_workers=0) # for the operation below we are going to provide integrations with vector search DB like QDrant or Faiss rr = RetrievalResults.from_embeddings_qg( embeddings_query=embeddings_query, embeddings_gallery=embeddings_gallery, dataset_query=dataset_query, dataset_gallery=dataset_gallery ) rr = ConstantThresholding(th=80).process(rr) rr.visualize_qg([0, 1], dataset_query=dataset_query, dataset_gallery=dataset_gallery, show=True) print(rr) ``` [comment]:usage-streaming-retrieval-end
```shell pip install open-metric-learning[nlp] ``` [comment]:zoo-text-start ```python from transformers import AutoModel, AutoTokenizer from oml.models import HFWrapper model = AutoModel.from_pretrained('bert-base-uncased').eval() tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased') extractor = HFWrapper(model=model, feat_dim=768) inp = tokenizer(text="Hello world", return_tensors="pt", add_special_tokens=True) embeddings = extractor(inp) ``` [comment]:zoo-text-end
[comment]:zoo-image-start ```python from oml.const import CKPT_SAVE_ROOT as CKPT_DIR, MOCK_DATASET_PATH as DATA_DIR from oml.models import ViTExtractor from oml.registry import get_transforms_for_pretrained model = ViTExtractor.from_pretrained("vits16_dino").eval() transforms, im_reader = get_transforms_for_pretrained("vits16_dino") img = im_reader(DATA_DIR / "images" / "circle_1.jpg") # put path to your image here img_tensor = transforms(img) # img_tensor = transforms(image=img)["image"] # for transforms from Albumentations features = model(img_tensor.unsqueeze(0)) # Check other available models: print(list(ViTExtractor.pretrained_models.keys())) # Load checkpoint saved on a disk: model_ = ViTExtractor(weights=CKPT_DIR / "vits16_dino.ckpt", arch="vits16", normalise_features=False) ``` [comment]:zoo-image-end