From f3e3a161a4a4ee08c88abbc8392580cdfae41766 Mon Sep 17 00:00:00 2001 From: ir2718 Date: Thu, 17 Oct 2024 18:49:21 +0200 Subject: [PATCH 01/13] implement SmoothAP loss --- docs/imgs/smooth_ap_approx_equation.png | Bin 0 -> 61610 bytes docs/imgs/smooth_ap_loss_equation.png | Bin 0 -> 20175 bytes docs/imgs/smooth_ap_sigmoid_equation.png | Bin 0 -> 29364 bytes docs/losses.md | 31 ++++ .../losses/__init__.py | 1 + .../losses/smooth_ap.py | 91 ++++++++++ tests/losses/test_smooth_ap_loss.py | 170 ++++++++++++++++++ 7 files changed, 293 insertions(+) create mode 100644 docs/imgs/smooth_ap_approx_equation.png create mode 100644 docs/imgs/smooth_ap_loss_equation.png create mode 100644 docs/imgs/smooth_ap_sigmoid_equation.png create mode 100644 src/pytorch_metric_learning/losses/smooth_ap.py create mode 100644 tests/losses/test_smooth_ap_loss.py diff --git a/docs/imgs/smooth_ap_approx_equation.png b/docs/imgs/smooth_ap_approx_equation.png new file mode 100644 index 0000000000000000000000000000000000000000..0b37c27adbc9e74bc5ab536c36b4a59c7b8c99e8 GIT binary patch literal 61610 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 literal 0 HcmV?d00001 diff --git a/docs/imgs/smooth_ap_loss_equation.png b/docs/imgs/smooth_ap_loss_equation.png new file mode 100644 index 0000000000000000000000000000000000000000..25f061c8d957e00ad22d74d8880b1b95c06bb2cf GIT binary patch literal 20175 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%0AIgh@`i*_1&I` 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 U7SY}z>> loss = SmoothAP(0.01, 60, 6, 256) + >>> input = torch.randn(60, 256, requires_grad=True).to("cuda:0") + >>> output = loss(input) + >>> output.backward() + """ + + def __init__(self, anneal, batch_size, num_id, feat_dims): + """ + Parameters + ---------- + anneal : float + the temperature of the sigmoid that is used to smooth the ranking function + batch_size : int + the batch size being used + num_id : int + the number of different classes that are represented in the batch + feat_dims : int + the dimension of the input feature embeddings + """ + super(SmoothAP, self).__init__() + + assert(batch_size%num_id==0) + + self.anneal = anneal + self.batch_size = batch_size + self.num_id = num_id + self.feat_dims = feat_dims + + def forward(self, preds): + """Forward pass for all input predictions: preds - (batch_size x feat_dims) """ + + + # ------ differentiable ranking of all retrieval set ------ + # compute the mask which ignores the relevance score of the query to itself + mask = 1.0 - torch.eye(self.batch_size) + mask = mask.unsqueeze(dim=0).repeat(self.batch_size, 1, 1) + # compute the relevance scores via cosine similarity of the CNN-produced embedding vectors + sim_all = compute_aff(preds) + sim_all_repeat = sim_all.unsqueeze(dim=1).repeat(1, self.batch_size, 1) + # compute the difference matrix + sim_diff = sim_all_repeat - sim_all_repeat.permute(0, 2, 1) + # pass through the sigmoid + sim_sg = sigmoid(sim_diff, temp=self.anneal) * mask.to(TEST_DEVICE) + # compute the rankings + sim_all_rk = torch.sum(sim_sg, dim=-1) + 1 + + + # ------ differentiable ranking of only positive set in retrieval set ------ + # compute the mask which only gives non-zero weights to the positive set + xs = preds.view(self.num_id, int(self.batch_size / self.num_id), self.feat_dims) + pos_mask = 1.0 - torch.eye(int(self.batch_size / self.num_id)) + pos_mask = pos_mask.unsqueeze(dim=0).unsqueeze(dim=0).repeat(self.num_id, int(self.batch_size / self.num_id), 1, 1) + + + # compute the relevance scores + sim_pos = torch.bmm(xs, xs.permute(0, 2, 1)) + sim_pos_repeat = sim_pos.unsqueeze(dim=2).repeat(1, 1, int(self.batch_size / self.num_id), 1) + # compute the difference matrix + sim_pos_diff = sim_pos_repeat - sim_pos_repeat.permute(0, 1, 3, 2) + # pass through the sigmoid + sim_pos_sg = sigmoid(sim_pos_diff, temp=self.anneal) * pos_mask.to(TEST_DEVICE) + # compute the rankings of the positive set + sim_pos_rk = torch.sum(sim_pos_sg, dim=-1) + 1 + + # sum the values of the Smooth-AP for all instances in the mini-batch + ap = torch.zeros(1).to(TEST_DEVICE) + group = int(self.batch_size / self.num_id) + for ind in range(self.num_id): + pos_divide = torch.sum(sim_pos_rk[ind] / (sim_all_rk[(ind * group):((ind + 1) * group), (ind * group):((ind + 1) * group)])) + ap = ap + ((pos_divide / group) / self.batch_size) + + return (1-ap) + +class TestSmoothAPLoss(unittest.TestCase): + def test_smooth_ap_loss(self): + for dtype in TEST_DTYPES: + for seed in TEST_SEEDS: + torch.manual_seed(seed) + loss = SmoothAP( + HYPERPARAMETERS["temp"], + HYPERPARAMETERS["batch_size"], + HYPERPARAMETERS["num_id"], + HYPERPARAMETERS["feat_dims"] + ) + rand_tensor = torch.randn(HYPERPARAMETERS["batch_size"], HYPERPARAMETERS["feat_dims"], requires_grad=True).to(TEST_DEVICE).to(dtype) + # The original code uses a model that normalizes the output vector + input_ = F.normalize(rand_tensor, p=2.0, dim=-1) + output = loss(input_) + + loss2 = SmoothAPLoss(temperature=HYPERPARAMETERS["temp"]) + # The original code assumes the label is in this format + labels = [] + for i in range(HYPERPARAMETERS["batch_size"] // HYPERPARAMETERS["num_id"]): + labels.extend([i for _ in range(HYPERPARAMETERS["num_id"])]) + + labels = torch.tensor(labels) + output2 = loss2.compute_loss( + rand_tensor, labels, None, rand_tensor, labels + )["loss"]["losses"] + self.assertTrue(torch.isclose(output, output2)) \ No newline at end of file From a1be0079016de7e574cc8c6a7d476e5abc032311 Mon Sep 17 00:00:00 2001 From: ir2718 Date: Thu, 17 Oct 2024 18:54:09 +0200 Subject: [PATCH 02/13] format code --- .../losses/__init__.py | 2 +- .../losses/smooth_ap.py | 39 +++++++----- tests/losses/test_smooth_ap_loss.py | 63 +++++++++++++------ 3 files changed, 66 insertions(+), 38 deletions(-) diff --git a/src/pytorch_metric_learning/losses/__init__.py b/src/pytorch_metric_learning/losses/__init__.py index 7ff54b09..96403a3b 100644 --- a/src/pytorch_metric_learning/losses/__init__.py +++ b/src/pytorch_metric_learning/losses/__init__.py @@ -30,6 +30,7 @@ from .ranked_list_loss import RankedListLoss from .self_supervised_loss import SelfSupervisedLoss from .signal_to_noise_ratio_losses import SignalToNoiseRatioContrastiveLoss +from .smooth_ap import SmoothAPLoss from .soft_triple_loss import SoftTripleLoss from .sphereface_loss import SphereFaceLoss from .subcenter_arcface_loss import SubCenterArcFaceLoss @@ -37,4 +38,3 @@ from .triplet_margin_loss import TripletMarginLoss from .tuplet_margin_loss import TupletMarginLoss from .vicreg_loss import VICRegLoss -from .smooth_ap import SmoothAPLoss \ No newline at end of file diff --git a/src/pytorch_metric_learning/losses/smooth_ap.py b/src/pytorch_metric_learning/losses/smooth_ap.py index cb536322..2cd5824d 100644 --- a/src/pytorch_metric_learning/losses/smooth_ap.py +++ b/src/pytorch_metric_learning/losses/smooth_ap.py @@ -1,20 +1,18 @@ import torch import torch.nn.functional as F +from ..distances import CosineSimilarity from ..utils import common_functions as c_f from ..utils import loss_and_miner_utils as lmu from .base_metric_loss_function import BaseMetricLossFunction -from ..distances import CosineSimilarity + class SmoothAPLoss(BaseMetricLossFunction): """ - Implementation of the SmoothAP loss: https://arxiv.org/abs/2007.12163 + Implementation of the SmoothAP loss: https://arxiv.org/abs/2007.12163 """ - def __init__( - self, - temperature=0.01, - **kwargs - ): + + def __init__(self, temperature=0.01, **kwargs): super().__init__(**kwargs) c_f.assert_distance_type(self, CosineSimilarity) self.temperature = temperature @@ -38,10 +36,11 @@ def compute_loss(self, embeddings, labels, iices_tuple, ref_emb, ref_labels): nonzero_counts = counts[nonzero_indices] if nonzero_counts.unique().size(0) != 1: raise ValueError( - "All classes must have the same number of elements in the labels.\n" \ + "All classes must have the same number of elements in the labels.\n" "The given labels have the following number of elements: {}.\n" - "You can achieve this using the samplers.MPerClassSampler class and setting the batch_size and m." \ - .format(nonzero_counts.cpu().tolist()) + "You can achieve this using the samplers.MPerClassSampler class and setting the batch_size and m.".format( + nonzero_counts.cpu().tolist() + ) ) batch_size = embeddings.size(0) @@ -62,25 +61,31 @@ def compute_loss(self, embeddings, labels, iices_tuple, ref_emb, ref_labels): num_classes_batch, batch_size // num_classes_batch, embeddings.size(-1) ).permute(0, 2, 1) pos_mask = 1.0 - torch.eye(batch_size // num_classes_batch) - pos_mask = pos_mask.unsqueeze(dim=0).unsqueeze(dim=0).repeat( - num_classes_batch, batch_size // num_classes_batch, 1, 1 + pos_mask = ( + pos_mask.unsqueeze(dim=0) + .unsqueeze(dim=0) + .repeat(num_classes_batch, batch_size // num_classes_batch, 1, 1) ) sims_pos = F.cosine_similarity(xs[:, :, None, :], xs[:, :, :, None]) - sims_pos_repeat = sims_pos.unsqueeze(dim=2).repeat(1, 1, batch_size // num_classes_batch, 1) + sims_pos_repeat = sims_pos.unsqueeze(dim=2).repeat( + 1, 1, batch_size // num_classes_batch, 1 + ) sims_pos_diff = sims_pos_repeat - sims_pos_repeat.permute(0, 1, 3, 2) - sims_pos_sigm = F.sigmoid(sims_pos_diff / self.temperature) * pos_mask.to(sims_diff.device) + sims_pos_sigm = F.sigmoid(sims_pos_diff / self.temperature) * pos_mask.to( + sims_diff.device + ) sims_pos_ranks = torch.sum(sims_pos_sigm, dim=-1) + 1 ap = torch.zeros(1).to(embeddings.device) g = batch_size // num_classes_batch for i in range(num_classes_batch): pos_divide = torch.sum( - sims_pos_ranks[i] / sims_ranks[i * g:(i + 1) * g, i * g:(i + 1) * g] + sims_pos_ranks[i] / sims_ranks[i * g : (i + 1) * g, i * g : (i + 1) * g] ) ap = ap + (pos_divide / g) / batch_size - + loss = 1 - ap return { "loss": { @@ -88,4 +93,4 @@ def compute_loss(self, embeddings, labels, iices_tuple, ref_emb, ref_labels): "indices": None, "reduction_type": "already_reduced", } - } \ No newline at end of file + } diff --git a/tests/losses/test_smooth_ap_loss.py b/tests/losses/test_smooth_ap_loss.py index bab3e662..1a332377 100644 --- a/tests/losses/test_smooth_ap_loss.py +++ b/tests/losses/test_smooth_ap_loss.py @@ -15,10 +15,11 @@ } TEST_SEEDS = [42, 1234, 5642, 9999, 3459] + # Original implementation of the SmoothAP loss taken from: # https://github.com/Andrew-Brown1/Smooth_AP/blob/master/src/Smooth_AP_loss.py def sigmoid(tensor, temp=1.0): - """ temperature controlled sigmoid + """temperature controlled sigmoid takes as input a torch tensor (tensor) and passes it through a sigmoid, controlled by temperature: temp """ @@ -89,7 +90,7 @@ def __init__(self, anneal, batch_size, num_id, feat_dims): """ super(SmoothAP, self).__init__() - assert(batch_size%num_id==0) + assert batch_size % num_id == 0 self.anneal = anneal self.batch_size = batch_size @@ -97,12 +98,11 @@ def __init__(self, anneal, batch_size, num_id, feat_dims): self.feat_dims = feat_dims def forward(self, preds): - """Forward pass for all input predictions: preds - (batch_size x feat_dims) """ - + """Forward pass for all input predictions: preds - (batch_size x feat_dims)""" # ------ differentiable ranking of all retrieval set ------ # compute the mask which ignores the relevance score of the query to itself - mask = 1.0 - torch.eye(self.batch_size) + mask = 1.0 - torch.eye(self.batch_size) mask = mask.unsqueeze(dim=0).repeat(self.batch_size, 1, 1) # compute the relevance scores via cosine similarity of the CNN-produced embedding vectors sim_all = compute_aff(preds) @@ -114,17 +114,21 @@ def forward(self, preds): # compute the rankings sim_all_rk = torch.sum(sim_sg, dim=-1) + 1 - # ------ differentiable ranking of only positive set in retrieval set ------ # compute the mask which only gives non-zero weights to the positive set xs = preds.view(self.num_id, int(self.batch_size / self.num_id), self.feat_dims) pos_mask = 1.0 - torch.eye(int(self.batch_size / self.num_id)) - pos_mask = pos_mask.unsqueeze(dim=0).unsqueeze(dim=0).repeat(self.num_id, int(self.batch_size / self.num_id), 1, 1) - - + pos_mask = ( + pos_mask.unsqueeze(dim=0) + .unsqueeze(dim=0) + .repeat(self.num_id, int(self.batch_size / self.num_id), 1, 1) + ) + # compute the relevance scores sim_pos = torch.bmm(xs, xs.permute(0, 2, 1)) - sim_pos_repeat = sim_pos.unsqueeze(dim=2).repeat(1, 1, int(self.batch_size / self.num_id), 1) + sim_pos_repeat = sim_pos.unsqueeze(dim=2).repeat( + 1, 1, int(self.batch_size / self.num_id), 1 + ) # compute the difference matrix sim_pos_diff = sim_pos_repeat - sim_pos_repeat.permute(0, 1, 3, 2) # pass through the sigmoid @@ -136,23 +140,40 @@ def forward(self, preds): ap = torch.zeros(1).to(TEST_DEVICE) group = int(self.batch_size / self.num_id) for ind in range(self.num_id): - pos_divide = torch.sum(sim_pos_rk[ind] / (sim_all_rk[(ind * group):((ind + 1) * group), (ind * group):((ind + 1) * group)])) + pos_divide = torch.sum( + sim_pos_rk[ind] + / ( + sim_all_rk[ + (ind * group) : ((ind + 1) * group), + (ind * group) : ((ind + 1) * group), + ] + ) + ) ap = ap + ((pos_divide / group) / self.batch_size) - return (1-ap) - + return 1 - ap + + class TestSmoothAPLoss(unittest.TestCase): def test_smooth_ap_loss(self): for dtype in TEST_DTYPES: for seed in TEST_SEEDS: torch.manual_seed(seed) loss = SmoothAP( - HYPERPARAMETERS["temp"], - HYPERPARAMETERS["batch_size"], - HYPERPARAMETERS["num_id"], - HYPERPARAMETERS["feat_dims"] + HYPERPARAMETERS["temp"], + HYPERPARAMETERS["batch_size"], + HYPERPARAMETERS["num_id"], + HYPERPARAMETERS["feat_dims"], + ) + rand_tensor = ( + torch.randn( + HYPERPARAMETERS["batch_size"], + HYPERPARAMETERS["feat_dims"], + requires_grad=True, + ) + .to(TEST_DEVICE) + .to(dtype) ) - rand_tensor = torch.randn(HYPERPARAMETERS["batch_size"], HYPERPARAMETERS["feat_dims"], requires_grad=True).to(TEST_DEVICE).to(dtype) # The original code uses a model that normalizes the output vector input_ = F.normalize(rand_tensor, p=2.0, dim=-1) output = loss(input_) @@ -160,11 +181,13 @@ def test_smooth_ap_loss(self): loss2 = SmoothAPLoss(temperature=HYPERPARAMETERS["temp"]) # The original code assumes the label is in this format labels = [] - for i in range(HYPERPARAMETERS["batch_size"] // HYPERPARAMETERS["num_id"]): + for i in range( + HYPERPARAMETERS["batch_size"] // HYPERPARAMETERS["num_id"] + ): labels.extend([i for _ in range(HYPERPARAMETERS["num_id"])]) labels = torch.tensor(labels) output2 = loss2.compute_loss( rand_tensor, labels, None, rand_tensor, labels )["loss"]["losses"] - self.assertTrue(torch.isclose(output, output2)) \ No newline at end of file + self.assertTrue(torch.isclose(output, output2)) From 12141c32f8fac5d43cf626dbe1e9f1f8f448bc2e Mon Sep 17 00:00:00 2001 From: ir2718 Date: Tue, 3 Dec 2024 23:44:41 +0100 Subject: [PATCH 03/13] change transpose to general case --- src/pytorch_metric_learning/distances/dot_product_similarity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pytorch_metric_learning/distances/dot_product_similarity.py b/src/pytorch_metric_learning/distances/dot_product_similarity.py index 2e0b4b01..74be22f5 100644 --- a/src/pytorch_metric_learning/distances/dot_product_similarity.py +++ b/src/pytorch_metric_learning/distances/dot_product_similarity.py @@ -9,7 +9,7 @@ def __init__(self, **kwargs): assert self.is_inverted def compute_mat(self, query_emb, ref_emb): - return torch.matmul(query_emb, ref_emb.t()) + return torch.matmul(query_emb, ref_emb.transpose(-1, -2)) def pairwise_distance(self, query_emb, ref_emb): return torch.sum(query_emb * ref_emb, dim=1) From 6c3d4c1e1dbf300dd060b36c1e30404e472db0fe Mon Sep 17 00:00:00 2001 From: ir2718 Date: Tue, 3 Dec 2024 23:47:58 +0100 Subject: [PATCH 04/13] swap to using distance function inside loss class --- src/pytorch_metric_learning/losses/smooth_ap.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pytorch_metric_learning/losses/smooth_ap.py b/src/pytorch_metric_learning/losses/smooth_ap.py index 2cd5824d..6df256f2 100644 --- a/src/pytorch_metric_learning/losses/smooth_ap.py +++ b/src/pytorch_metric_learning/losses/smooth_ap.py @@ -22,7 +22,7 @@ def get_default_distance(self): # Implementation is based on the original repository: # https://github.com/Andrew-Brown1/Smooth_AP/blob/master/src/Smooth_AP_loss.py#L87 - def compute_loss(self, embeddings, labels, iices_tuple, ref_emb, ref_labels): + def compute_loss(self, embeddings, labels, indices_tuple, ref_emb, ref_labels): # The loss expects labels such that there is the same number of elements for each class # The number of classes is not important, nor their order, but the number of elements must be the same, eg. # @@ -49,9 +49,8 @@ def compute_loss(self, embeddings, labels, iices_tuple, ref_emb, ref_labels): mask = 1.0 - torch.eye(batch_size) mask = mask.unsqueeze(dim=0).repeat(batch_size, 1, 1) - sims = F.cosine_similarity( - embeddings[None, :, :], embeddings[:, None, :], dim=-1 - ) + sims = self.distance(embeddings) + sims_repeat = sims.unsqueeze(dim=1).repeat(1, batch_size, 1) sims_diff = sims_repeat - sims_repeat.permute(0, 2, 1) sims_sigm = F.sigmoid(sims_diff / self.temperature) * mask.to(sims_diff.device) @@ -59,7 +58,7 @@ def compute_loss(self, embeddings, labels, iices_tuple, ref_emb, ref_labels): xs = embeddings.view( num_classes_batch, batch_size // num_classes_batch, embeddings.size(-1) - ).permute(0, 2, 1) + ) pos_mask = 1.0 - torch.eye(batch_size // num_classes_batch) pos_mask = ( pos_mask.unsqueeze(dim=0) @@ -67,7 +66,10 @@ def compute_loss(self, embeddings, labels, iices_tuple, ref_emb, ref_labels): .repeat(num_classes_batch, batch_size // num_classes_batch, 1, 1) ) - sims_pos = F.cosine_similarity(xs[:, :, None, :], xs[:, :, :, None]) + # Circumvent the shape check in forward method + xs_norm = self.distance.maybe_normalize(xs, dim=-1) + sims_pos = self.distance.compute_mat(xs_norm, xs_norm) + sims_pos_repeat = sims_pos.unsqueeze(dim=2).repeat( 1, 1, batch_size // num_classes_batch, 1 ) From 2edf3e1954ebc2cfe1e4abff0490ff773507c3f0 Mon Sep 17 00:00:00 2001 From: ir2718 Date: Sun, 8 Dec 2024 16:16:48 +0100 Subject: [PATCH 05/13] add reduce option --- .../losses/smooth_ap.py | 19 ++++++++++--------- tests/losses/test_smooth_ap_loss.py | 4 ++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/pytorch_metric_learning/losses/smooth_ap.py b/src/pytorch_metric_learning/losses/smooth_ap.py index 6df256f2..2f1c8bff 100644 --- a/src/pytorch_metric_learning/losses/smooth_ap.py +++ b/src/pytorch_metric_learning/losses/smooth_ap.py @@ -80,19 +80,20 @@ def compute_loss(self, embeddings, labels, indices_tuple, ref_emb, ref_labels): ) sims_pos_ranks = torch.sum(sims_pos_sigm, dim=-1) + 1 - ap = torch.zeros(1).to(embeddings.device) g = batch_size // num_classes_batch + ap = torch.zeros(batch_size).to(embeddings.device) for i in range(num_classes_batch): - pos_divide = torch.sum( - sims_pos_ranks[i] / sims_ranks[i * g : (i + 1) * g, i * g : (i + 1) * g] - ) - ap = ap + (pos_divide / g) / batch_size + for j in range(g): + pos_rank = sims_pos_ranks[i, j] + all_rank = sims_ranks[i * g + j, i * g: (i + 1) * g] + ap[i * g + j] = torch.sum(pos_rank / all_rank) / g loss = 1 - ap + return { - "loss": { + "ap_loss": { "losses": loss, - "indices": None, - "reduction_type": "already_reduced", + "indices": c_f.torch_arange_from_size(loss), + "reduction_type": "element", } - } + } \ No newline at end of file diff --git a/tests/losses/test_smooth_ap_loss.py b/tests/losses/test_smooth_ap_loss.py index 1a332377..1c7a1463 100644 --- a/tests/losses/test_smooth_ap_loss.py +++ b/tests/losses/test_smooth_ap_loss.py @@ -187,7 +187,7 @@ def test_smooth_ap_loss(self): labels.extend([i for _ in range(HYPERPARAMETERS["num_id"])]) labels = torch.tensor(labels) - output2 = loss2.compute_loss( + output2 = loss2.forward( rand_tensor, labels, None, rand_tensor, labels - )["loss"]["losses"] + ) self.assertTrue(torch.isclose(output, output2)) From 23b4cf1ad42d232a27688c369278183f544e4afe Mon Sep 17 00:00:00 2001 From: ir2718 Date: Sun, 8 Dec 2024 16:30:50 +0100 Subject: [PATCH 06/13] format code --- src/pytorch_metric_learning/losses/smooth_ap.py | 8 +++++--- tests/losses/test_smooth_ap_loss.py | 4 +--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pytorch_metric_learning/losses/smooth_ap.py b/src/pytorch_metric_learning/losses/smooth_ap.py index 2f1c8bff..0512112b 100644 --- a/src/pytorch_metric_learning/losses/smooth_ap.py +++ b/src/pytorch_metric_learning/losses/smooth_ap.py @@ -14,7 +14,7 @@ class SmoothAPLoss(BaseMetricLossFunction): def __init__(self, temperature=0.01, **kwargs): super().__init__(**kwargs) - c_f.assert_distance_type(self, CosineSimilarity) + # c_f.assert_distance_type(self, CosineSimilarity) self.temperature = temperature def get_default_distance(self): @@ -50,6 +50,8 @@ def compute_loss(self, embeddings, labels, indices_tuple, ref_emb, ref_labels): mask = mask.unsqueeze(dim=0).repeat(batch_size, 1, 1) sims = self.distance(embeddings) + if not self.distance.is_inverted: + sims *= -1 sims_repeat = sims.unsqueeze(dim=1).repeat(1, batch_size, 1) sims_diff = sims_repeat - sims_repeat.permute(0, 2, 1) @@ -85,7 +87,7 @@ def compute_loss(self, embeddings, labels, indices_tuple, ref_emb, ref_labels): for i in range(num_classes_batch): for j in range(g): pos_rank = sims_pos_ranks[i, j] - all_rank = sims_ranks[i * g + j, i * g: (i + 1) * g] + all_rank = sims_ranks[i * g + j, i * g : (i + 1) * g] ap[i * g + j] = torch.sum(pos_rank / all_rank) / g loss = 1 - ap @@ -96,4 +98,4 @@ def compute_loss(self, embeddings, labels, indices_tuple, ref_emb, ref_labels): "indices": c_f.torch_arange_from_size(loss), "reduction_type": "element", } - } \ No newline at end of file + } diff --git a/tests/losses/test_smooth_ap_loss.py b/tests/losses/test_smooth_ap_loss.py index 1c7a1463..e92f595b 100644 --- a/tests/losses/test_smooth_ap_loss.py +++ b/tests/losses/test_smooth_ap_loss.py @@ -187,7 +187,5 @@ def test_smooth_ap_loss(self): labels.extend([i for _ in range(HYPERPARAMETERS["num_id"])]) labels = torch.tensor(labels) - output2 = loss2.forward( - rand_tensor, labels, None, rand_tensor, labels - ) + output2 = loss2.forward(rand_tensor, labels, None, rand_tensor, labels) self.assertTrue(torch.isclose(output, output2)) From ef0f87c6a4c875a20b3f2f928750a32263e922ca Mon Sep 17 00:00:00 2001 From: ir2718 Date: Sun, 8 Dec 2024 17:33:48 +0100 Subject: [PATCH 07/13] add checks for inputs --- src/pytorch_metric_learning/losses/smooth_ap.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pytorch_metric_learning/losses/smooth_ap.py b/src/pytorch_metric_learning/losses/smooth_ap.py index 0512112b..28adbbc9 100644 --- a/src/pytorch_metric_learning/losses/smooth_ap.py +++ b/src/pytorch_metric_learning/losses/smooth_ap.py @@ -14,7 +14,7 @@ class SmoothAPLoss(BaseMetricLossFunction): def __init__(self, temperature=0.01, **kwargs): super().__init__(**kwargs) - # c_f.assert_distance_type(self, CosineSimilarity) + c_f.assert_distance_type(self, CosineSimilarity) self.temperature = temperature def get_default_distance(self): @@ -31,6 +31,9 @@ def compute_loss(self, embeddings, labels, indices_tuple, ref_emb, ref_labels): # The following label is NOT valid: # [ B,B,B A,A,A,A, C,C,C ] # + c_f.labels_required(labels) + c_f.ref_not_supported(embeddings, labels, ref_emb, ref_labels) + counts = torch.bincount(labels) nonzero_indices = torch.nonzero(counts, as_tuple=True)[0] nonzero_counts = counts[nonzero_indices] @@ -50,8 +53,6 @@ def compute_loss(self, embeddings, labels, indices_tuple, ref_emb, ref_labels): mask = mask.unsqueeze(dim=0).repeat(batch_size, 1, 1) sims = self.distance(embeddings) - if not self.distance.is_inverted: - sims *= -1 sims_repeat = sims.unsqueeze(dim=1).repeat(1, batch_size, 1) sims_diff = sims_repeat - sims_repeat.permute(0, 2, 1) From 44bbb39ad35f00a0da1863fd7250902705d0a34d Mon Sep 17 00:00:00 2001 From: ir2718 Date: Sun, 8 Dec 2024 18:05:31 +0100 Subject: [PATCH 08/13] add mining --- src/pytorch_metric_learning/losses/smooth_ap.py | 5 +++-- tests/losses/test_smooth_ap_loss.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pytorch_metric_learning/losses/smooth_ap.py b/src/pytorch_metric_learning/losses/smooth_ap.py index 28adbbc9..b0e441f3 100644 --- a/src/pytorch_metric_learning/losses/smooth_ap.py +++ b/src/pytorch_metric_learning/losses/smooth_ap.py @@ -33,7 +33,7 @@ def compute_loss(self, embeddings, labels, indices_tuple, ref_emb, ref_labels): # c_f.labels_required(labels) c_f.ref_not_supported(embeddings, labels, ref_emb, ref_labels) - + counts = torch.bincount(labels) nonzero_indices = torch.nonzero(counts, as_tuple=True)[0] nonzero_counts = counts[nonzero_indices] @@ -91,7 +91,8 @@ def compute_loss(self, embeddings, labels, indices_tuple, ref_emb, ref_labels): all_rank = sims_ranks[i * g + j, i * g : (i + 1) * g] ap[i * g + j] = torch.sum(pos_rank / all_rank) / g - loss = 1 - ap + miner_weights = lmu.convert_to_weights(indices_tuple, labels, dtype=ap.dtype) + loss = (1 - ap) * miner_weights return { "ap_loss": { diff --git a/tests/losses/test_smooth_ap_loss.py b/tests/losses/test_smooth_ap_loss.py index e92f595b..422e9423 100644 --- a/tests/losses/test_smooth_ap_loss.py +++ b/tests/losses/test_smooth_ap_loss.py @@ -187,5 +187,5 @@ def test_smooth_ap_loss(self): labels.extend([i for _ in range(HYPERPARAMETERS["num_id"])]) labels = torch.tensor(labels) - output2 = loss2.forward(rand_tensor, labels, None, rand_tensor, labels) + output2 = loss2.forward(rand_tensor, labels) self.assertTrue(torch.isclose(output, output2)) From b7d9eb668cb9fc0246d1791bf315c121f553e974 Mon Sep 17 00:00:00 2001 From: Luca Marini Date: Wed, 2 Apr 2025 14:31:00 +0200 Subject: [PATCH 09/13] add check that each pair should be either positive or negative --- src/pytorch_metric_learning/losses/generic_pair_loss.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/pytorch_metric_learning/losses/generic_pair_loss.py b/src/pytorch_metric_learning/losses/generic_pair_loss.py index 6996490f..b26c7fda 100644 --- a/src/pytorch_metric_learning/losses/generic_pair_loss.py +++ b/src/pytorch_metric_learning/losses/generic_pair_loss.py @@ -28,6 +28,7 @@ def mat_based_loss(self, mat, indices_tuple): pos_mask, neg_mask = torch.zeros_like(mat), torch.zeros_like(mat) pos_mask[a1, p] = 1 neg_mask[a2, n] = 1 + self._assert_either_pos_or_neg(pos_mask, neg_mask) return self._compute_loss(mat, pos_mask, neg_mask) def pair_based_loss(self, mat, indices_tuple): @@ -38,3 +39,11 @@ def pair_based_loss(self, mat, indices_tuple): if len(a2) > 0: neg_pair = mat[a2, n] return self._compute_loss(pos_pair, neg_pair, indices_tuple) + + @staticmethod + def _assert_either_pos_or_neg(pos_mask, neg_mask): + pos_indices = set(pos_mask.flatten().nonzero().flatten().tolist()) + neg_indices = set(neg_mask.flatten().nonzero().flatten().tolist()) + assert ( + pos_indices.isdisjoint(neg_indices) + ), "Each pair should be either be positive or negative" From e9d6a90b26a1007868d1bea35c238461e02f7e38 Mon Sep 17 00:00:00 2001 From: Luca Marini Date: Sat, 5 Apr 2025 00:59:25 +0200 Subject: [PATCH 10/13] more efficient implementation --- src/pytorch_metric_learning/losses/generic_pair_loss.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pytorch_metric_learning/losses/generic_pair_loss.py b/src/pytorch_metric_learning/losses/generic_pair_loss.py index b26c7fda..8c3145f8 100644 --- a/src/pytorch_metric_learning/losses/generic_pair_loss.py +++ b/src/pytorch_metric_learning/losses/generic_pair_loss.py @@ -42,8 +42,4 @@ def pair_based_loss(self, mat, indices_tuple): @staticmethod def _assert_either_pos_or_neg(pos_mask, neg_mask): - pos_indices = set(pos_mask.flatten().nonzero().flatten().tolist()) - neg_indices = set(neg_mask.flatten().nonzero().flatten().tolist()) - assert ( - pos_indices.isdisjoint(neg_indices) - ), "Each pair should be either be positive or negative" + assert not torch.any((pos_mask != 0) & (neg_mask != 0)), "Each pair should be either be positive or negative" From 4f3e1acfc044683c4ab0ab90f270f543ed5353e8 Mon Sep 17 00:00:00 2001 From: marcpaga Date: Tue, 8 Apr 2025 11:51:56 +0200 Subject: [PATCH 11/13] Fix kwargs not passed to SubCenterArcFaceLoss --- .../losses/subcenter_arcface_loss.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pytorch_metric_learning/losses/subcenter_arcface_loss.py b/src/pytorch_metric_learning/losses/subcenter_arcface_loss.py index fc1f2210..25628210 100644 --- a/src/pytorch_metric_learning/losses/subcenter_arcface_loss.py +++ b/src/pytorch_metric_learning/losses/subcenter_arcface_loss.py @@ -1,4 +1,5 @@ import math +from copy import deepcopy import numpy as np import torch @@ -13,9 +14,12 @@ class SubCenterArcFaceLoss(ArcFaceLoss): """ def __init__(self, *args, margin=28.6, scale=64, sub_centers=3, **kwargs): - num_classes, embedding_size = kwargs["num_classes"], kwargs["embedding_size"] + num_classes = deepcopy(kwargs["num_classes"]) + embedding_size = deepcopy(kwargs["embedding_size"]) + del kwargs["num_classes"] + del kwargs["embedding_size"] super().__init__( - num_classes * sub_centers, embedding_size, margin=margin, scale=scale + num_classes=num_classes * sub_centers, embedding_size=embedding_size, margin=margin, scale=scale, **kwargs ) self.sub_centers = sub_centers self.num_classes = num_classes From 26cdade1478af01602cae7f2f151ec3593d56946 Mon Sep 17 00:00:00 2001 From: marcpaga Date: Fri, 11 Apr 2025 10:40:17 +0200 Subject: [PATCH 12/13] add unittest for SubCenterArcFaceLoss with DoNothingReducer --- tests/losses/test_subcenter_arcface_loss.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/losses/test_subcenter_arcface_loss.py b/tests/losses/test_subcenter_arcface_loss.py index a4d324f4..f8bdc944 100644 --- a/tests/losses/test_subcenter_arcface_loss.py +++ b/tests/losses/test_subcenter_arcface_loss.py @@ -6,6 +6,7 @@ import torch.nn.functional as F from pytorch_metric_learning.losses import ArcFaceLoss, SubCenterArcFaceLoss +from pytorch_metric_learning.reducers import DoNothingReducer from .. import TEST_DEVICE, TEST_DTYPES @@ -142,3 +143,19 @@ def test_inference_subcenter_arcface(self): ) == 0 ) + + def test_reducer_subcenter_arcface(self): + + arcfaceloss = SubCenterArcFaceLoss( + num_classes=10, + sub_centers=3, + embedding_size=64, + reducer=DoNothingReducer(), + ) + + emb = torch.randn(4, 64) + result = arcfaceloss(emb, torch.arange(4)) + + self.assertTrue(isinstance(result, dict)) + self.assertTrue(result['loss']['losses'].shape[0] == 4) + \ No newline at end of file From 61d47c9903fb2ef9c2628bb07d9ae70d4aae5559 Mon Sep 17 00:00:00 2001 From: KevinMusgrave Date: Sun, 20 Apr 2025 00:02:47 -0400 Subject: [PATCH 13/13] remove outer miner test from test_cross_batch_memory.py --- tests/losses/test_cross_batch_memory.py | 32 ------------------------- 1 file changed, 32 deletions(-) diff --git a/tests/losses/test_cross_batch_memory.py b/tests/losses/test_cross_batch_memory.py index 5c3d47af..f1c2d8c3 100644 --- a/tests/losses/test_cross_batch_memory.py +++ b/tests/losses/test_cross_batch_memory.py @@ -238,7 +238,6 @@ def test_loss(self): batch_size = 32 for inner_loss in [ContrastiveLoss(), MultiSimilarityLoss()]: inner_miner = MultiSimilarityMiner(0.3) - outer_miner = MultiSimilarityMiner(0.2) self.loss = CrossBatchMemory( loss=inner_loss, embedding_size=self.embedding_size, @@ -267,10 +266,6 @@ def test_loss(self): labels = torch.randint(0, num_labels, (batch_size,)).to(TEST_DEVICE) loss = self.loss(embeddings, labels) loss_with_miner = self.loss_with_miner(embeddings, labels) - oa1, op, oa2, on = outer_miner(embeddings, labels) - loss_with_miner_and_input_indices = self.loss_with_miner2( - embeddings, labels, (oa1, op, oa2, on) - ) all_embeddings = torch.cat([all_embeddings, embeddings]) all_labels = torch.cat([all_labels, labels]) @@ -308,33 +303,6 @@ def test_loss(self): torch.isclose(loss_with_miner, correct_loss_with_miner) ) - # loss with inner and outer miner - indices_tuple = inner_miner( - embeddings, labels, all_embeddings, all_labels - ) - a1, p, a2, n = lmu.remove_self_comparisons( - indices_tuple, - self.loss_with_miner2.curr_batch_idx, - self.loss_with_miner2.memory_size, - ) - a1 = torch.cat([oa1, a1]) - p = torch.cat([op, p]) - a2 = torch.cat([oa2, a2]) - n = torch.cat([on, n]) - correct_loss_with_miner_and_input_indice = inner_loss( - embeddings, - labels, - (a1, p, a2, n), - all_embeddings, - all_labels, - ) - self.assertTrue( - torch.isclose( - loss_with_miner_and_input_indices, - correct_loss_with_miner_and_input_indice, - ) - ) - def test_queue(self): for test_enqueue_mask in [False, True]: for dtype in TEST_DTYPES: