From 700eae71eef13c70d7c689015ecf6ae5d92b4025 Mon Sep 17 00:00:00 2001 From: HauserA Date: Mon, 27 Jan 2020 19:10:09 +0100 Subject: [PATCH 01/13] triplet hard loss --- tensorflow_addons/losses/triplet.py | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tensorflow_addons/losses/triplet.py b/tensorflow_addons/losses/triplet.py index 9ef4c704b2..061676a918 100644 --- a/tensorflow_addons/losses/triplet.py +++ b/tensorflow_addons/losses/triplet.py @@ -142,6 +142,54 @@ def triplet_semihard_loss(y_true, y_pred, margin=1.0): return triplet_loss + +@tf.keras.utils.register_keras_serializable(package="Addons") +@tf.function +def triplet_hard_loss(y_true, y_pred, margin=1.0): + """Computes the triplet loss with hard negative mining. + + Args: + y_true: 1-D integer `Tensor` with shape [batch_size] of + multiclass integer labels. + y_pred: 2-D float `Tensor` of embedding vectors. Embeddings should + be l2 normalized. + margin: Float, margin term in the loss definition. + """ + labels, embeddings = y_true, y_pred + # Reshape label tensor to [batch_size, 1]. + lshape = tf.shape(labels) + labels = tf.reshape(labels, [lshape[0], 1]) + + # Build pairwise squared distance matrix. + pdist_matrix = metric_learning.pairwise_distance(embeddings, squared=True) + # Build pairwise binary adjacency matrix. + adjacency = tf.math.equal(labels, tf.transpose(labels)) + # Invert so we can select negatives only. + adjacency_not = tf.math.logical_not(adjacency) + + adjacency_not = tf.cast(adjacency_not, dtype=tf.dtypes.float32) + # hard negatives: smallest D_an. + hard_negatives = _masked_minimum(pdist_matrix, adjacency_not) + + batch_size = tf.size(labels) + + adjacency = tf.cast(adjacency, dtype=tf.dtypes.float32) + + mask_positives = tf.cast( + adjacency, dtype=tf.dtypes.float32) - tf.linalg.diag( + tf.ones([batch_size])) + + # hard positives: largest D_ap. + hard_positives = _masked_maximum(pdist_matrix, mask_positives) + + triplet_loss = tf.maximum(hard_positives - hard_negatives + margin, 0.0) + + # Get final mean triplet loss + triplet_loss = tf.reduce_mean(triplet_loss) + + return triplet_loss + + @tf.keras.utils.register_keras_serializable(package="Addons") class TripletSemiHardLoss(tf.keras.losses.Loss): """Computes the triplet loss with semi-hard negative mining. @@ -175,3 +223,38 @@ def get_config(self): } base_config = super().get_config() return {**base_config, **config} + + +@tf.keras.utils.register_keras_serializable(package='Addons') +class TripletHardLoss(tf.keras.losses.Loss): + """Computes the triplet loss with hard negative and hard positive mining. + + The loss encourages the maximum positive distance (between a pair of embeddings + with the same labels) to be smaller than the minimum negative distance plus the + margin constant in the mini-batch. + The loss selects the hardest positive and the hardest negative samples + within the batch when forming the triplets for computing the loss. + See: https://arxiv.org/pdf/1703.07737.pdf + + We expect labels `y_true` to be provided as 1-D integer `Tensor` with shape + [batch_size] of multi-class integer labels. And embeddings `y_pred` must be + 2-D float `Tensor` of l2 normalized embedding vectors. + + Args: + margin: Float, margin term in the loss definition. Default value is 1.0. + name: Optional name for the op. + """ + + def __init__(self, margin=1.0, name=None, **kwargs): + super().__init__(name=name, reduction=tf.keras.losses.Reduction.NONE) + self.margin = margin + + def call(self, y_true, y_pred): + return triplet_hard_loss(y_true, y_pred, self.margin) + + def get_config(self): + config = { + "margin": self.margin, + } + base_config = super().get_config() + return {**base_config, **config} From cf408ade877646628b18f42b9c2a2c6c16ceea01 Mon Sep 17 00:00:00 2001 From: HauserA Date: Mon, 27 Jan 2020 19:18:33 +0100 Subject: [PATCH 02/13] triplet hard test --- tensorflow_addons/losses/triplet_hard_test.py | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 tensorflow_addons/losses/triplet_hard_test.py diff --git a/tensorflow_addons/losses/triplet_hard_test.py b/tensorflow_addons/losses/triplet_hard_test.py new file mode 100644 index 0000000000..d2d29b7fb3 --- /dev/null +++ b/tensorflow_addons/losses/triplet_hard_test.py @@ -0,0 +1,114 @@ +# Copyright 2019 The TensorFlow Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Tests for triplet hard loss.""" + +import numpy as np +import tensorflow as tf + +from tensorflow_addons.losses import triplet +from tensorflow_addons.utils import test_utils + +def pairwise_distance_np(feature, squared=False): + """Computes the pairwise distance matrix in numpy. + + Args: + feature: 2-D numpy array of size [number of data, feature dimension] + squared: Boolean. If true, output is the pairwise squared euclidean + distance matrix; else, output is the pairwise euclidean distance + matrix. + + Returns: + pairwise_distances: 2-D numpy array of size + [number of data, number of data]. + """ + triu = np.triu_indices(feature.shape[0], 1) + upper_tri_pdists = np.linalg.norm(feature[triu[1]] - feature[triu[0]], axis=1) + if squared: + upper_tri_pdists **= 2.0 + num_data = feature.shape[0] + pairwise_distances = np.zeros((num_data, num_data)) + pairwise_distances[np.triu_indices(num_data, 1)] = upper_tri_pdists + # Make symmetrical. + pairwise_distances = ( + pairwise_distances + + pairwise_distances.T + - np.diag(pairwise_distances.diagonal()) + ) + return pairwise_distances + +@test_utils.run_all_in_graph_and_eager_modes +class TripletHardLossTest(tf.test.TestCase): + def test_unweighted(self): + num_data = 20 + feat_dim = 6 + margin = 1.0 + num_classes = 4 + + embedding = np.random.rand(num_data, feat_dim).astype(np.float32) + labels = np.random.randint(0, num_classes, size=(num_data)) + + # Reshape labels to compute adjacency matrix. + labels_reshaped = np.reshape( + labels.astype(np.float32), (labels.shape[0], 1)) + # Compute the loss in NP. + adjacency = np.equal(labels_reshaped, labels_reshaped.T) + + pdist_matrix = pairwise_distance_np(embedding, squared=True) + loss_np = 0.0 + for i in range(num_data): + pos_distances = [] + neg_distances = [] + for j in range(num_data): + if adjacency[i][j] == 0: + neg_distances.append(pdist_matrix[i][j]) + if adjacency[i][j] > 0.0 and i != j: + pos_distances.append(pdist_matrix[i][j]) + + # if their are no positive pairs, distance is 0 + if len(pos_distances) == 0: + pos_distances.append(0) + + # Sort by distance. + neg_distances.sort() + min_neg_distance = neg_distances[0] + pos_distances.sort(reverse=True) + max_pos_distance = pos_distances[0] + + loss_np += np.maximum( + 0.0, margin - min_neg_distance + max_pos_distance) + + loss_np /= num_data + + # Compute the loss in TF. + y_true = tf.constant(labels) + y_pred = tf.constant(embedding) + cce_obj = triplet.TripletHardLoss() + loss = cce_obj(y_true, y_pred) + self.assertAlmostEqual(self.evaluate(loss), loss_np, 3) + + def test_keras_model_compile(self): + model = tf.keras.models.Sequential([ + tf.keras.layers.Input(shape=(784,)), + tf.keras.layers.Dense(10), + ]) + model.compile(loss="Addons>triplet_hard_loss", optimizer="adam") + + def test_serialization(self): + loss = triplet.TripletHardLoss() + new_loss = tf.keras.losses.deserialize(tf.keras.losses.serialize(loss)) + + +if __name__ == '__main__': + tf.test.main() From 3e2ef27efa108889613a1233c9e30e73137b2f2f Mon Sep 17 00:00:00 2001 From: HauserA Date: Mon, 27 Jan 2020 19:19:22 +0100 Subject: [PATCH 03/13] format with black --- tensorflow_addons/losses/triplet.py | 9 ++++----- tensorflow_addons/losses/triplet_hard_test.py | 17 ++++++++--------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/tensorflow_addons/losses/triplet.py b/tensorflow_addons/losses/triplet.py index 061676a918..600a2b859b 100644 --- a/tensorflow_addons/losses/triplet.py +++ b/tensorflow_addons/losses/triplet.py @@ -142,7 +142,6 @@ def triplet_semihard_loss(y_true, y_pred, margin=1.0): return triplet_loss - @tf.keras.utils.register_keras_serializable(package="Addons") @tf.function def triplet_hard_loss(y_true, y_pred, margin=1.0): @@ -175,9 +174,9 @@ def triplet_hard_loss(y_true, y_pred, margin=1.0): adjacency = tf.cast(adjacency, dtype=tf.dtypes.float32) - mask_positives = tf.cast( - adjacency, dtype=tf.dtypes.float32) - tf.linalg.diag( - tf.ones([batch_size])) + mask_positives = tf.cast(adjacency, dtype=tf.dtypes.float32) - tf.linalg.diag( + tf.ones([batch_size]) + ) # hard positives: largest D_ap. hard_positives = _masked_maximum(pdist_matrix, mask_positives) @@ -225,7 +224,7 @@ def get_config(self): return {**base_config, **config} -@tf.keras.utils.register_keras_serializable(package='Addons') +@tf.keras.utils.register_keras_serializable(package="Addons") class TripletHardLoss(tf.keras.losses.Loss): """Computes the triplet loss with hard negative and hard positive mining. diff --git a/tensorflow_addons/losses/triplet_hard_test.py b/tensorflow_addons/losses/triplet_hard_test.py index d2d29b7fb3..20bdddb8a6 100644 --- a/tensorflow_addons/losses/triplet_hard_test.py +++ b/tensorflow_addons/losses/triplet_hard_test.py @@ -20,6 +20,7 @@ from tensorflow_addons.losses import triplet from tensorflow_addons.utils import test_utils + def pairwise_distance_np(feature, squared=False): """Computes the pairwise distance matrix in numpy. @@ -48,6 +49,7 @@ def pairwise_distance_np(feature, squared=False): ) return pairwise_distances + @test_utils.run_all_in_graph_and_eager_modes class TripletHardLossTest(tf.test.TestCase): def test_unweighted(self): @@ -60,8 +62,7 @@ def test_unweighted(self): labels = np.random.randint(0, num_classes, size=(num_data)) # Reshape labels to compute adjacency matrix. - labels_reshaped = np.reshape( - labels.astype(np.float32), (labels.shape[0], 1)) + labels_reshaped = np.reshape(labels.astype(np.float32), (labels.shape[0], 1)) # Compute the loss in NP. adjacency = np.equal(labels_reshaped, labels_reshaped.T) @@ -86,8 +87,7 @@ def test_unweighted(self): pos_distances.sort(reverse=True) max_pos_distance = pos_distances[0] - loss_np += np.maximum( - 0.0, margin - min_neg_distance + max_pos_distance) + loss_np += np.maximum(0.0, margin - min_neg_distance + max_pos_distance) loss_np /= num_data @@ -99,10 +99,9 @@ def test_unweighted(self): self.assertAlmostEqual(self.evaluate(loss), loss_np, 3) def test_keras_model_compile(self): - model = tf.keras.models.Sequential([ - tf.keras.layers.Input(shape=(784,)), - tf.keras.layers.Dense(10), - ]) + model = tf.keras.models.Sequential( + [tf.keras.layers.Input(shape=(784,)), tf.keras.layers.Dense(10),] + ) model.compile(loss="Addons>triplet_hard_loss", optimizer="adam") def test_serialization(self): @@ -110,5 +109,5 @@ def test_serialization(self): new_loss = tf.keras.losses.deserialize(tf.keras.losses.serialize(loss)) -if __name__ == '__main__': +if __name__ == "__main__": tf.test.main() From d60a443e802c5527fe184289edd7bc235535b92b Mon Sep 17 00:00:00 2001 From: HauserA Date: Mon, 27 Jan 2020 19:51:47 +0100 Subject: [PATCH 04/13] FaceNet reference --- tensorflow_addons/losses/triplet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tensorflow_addons/losses/triplet.py b/tensorflow_addons/losses/triplet.py index 600a2b859b..5a490980fd 100644 --- a/tensorflow_addons/losses/triplet.py +++ b/tensorflow_addons/losses/triplet.py @@ -233,7 +233,7 @@ class TripletHardLoss(tf.keras.losses.Loss): margin constant in the mini-batch. The loss selects the hardest positive and the hardest negative samples within the batch when forming the triplets for computing the loss. - See: https://arxiv.org/pdf/1703.07737.pdf + See: https://arxiv.org/abs/1503.03832 We expect labels `y_true` to be provided as 1-D integer `Tensor` with shape [batch_size] of multi-class integer labels. And embeddings `y_pred` must be From caac6a0afa708d0a94bfb674fc2681ef33440b3c Mon Sep 17 00:00:00 2001 From: HauserA Date: Mon, 27 Jan 2020 20:42:05 +0100 Subject: [PATCH 05/13] spelling correction --- tensorflow_addons/losses/triplet_hard_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tensorflow_addons/losses/triplet_hard_test.py b/tensorflow_addons/losses/triplet_hard_test.py index 20bdddb8a6..f22fab1c6a 100644 --- a/tensorflow_addons/losses/triplet_hard_test.py +++ b/tensorflow_addons/losses/triplet_hard_test.py @@ -77,7 +77,7 @@ def test_unweighted(self): if adjacency[i][j] > 0.0 and i != j: pos_distances.append(pdist_matrix[i][j]) - # if their are no positive pairs, distance is 0 + # if there are no positive pairs, distance is 0 if len(pos_distances) == 0: pos_distances.append(0) From 5225610e4e87fb7e22ea62a51d5fcf99e1b726e4 Mon Sep 17 00:00:00 2001 From: HauserA Date: Mon, 27 Jan 2020 21:42:00 +0100 Subject: [PATCH 06/13] soft margin version --- tensorflow_addons/losses/triplet.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tensorflow_addons/losses/triplet.py b/tensorflow_addons/losses/triplet.py index 5a490980fd..e9f96ec0d2 100644 --- a/tensorflow_addons/losses/triplet.py +++ b/tensorflow_addons/losses/triplet.py @@ -144,7 +144,7 @@ def triplet_semihard_loss(y_true, y_pred, margin=1.0): @tf.keras.utils.register_keras_serializable(package="Addons") @tf.function -def triplet_hard_loss(y_true, y_pred, margin=1.0): +def triplet_hard_loss(y_true, y_pred, margin=1.0, soft=False): """Computes the triplet loss with hard negative mining. Args: @@ -153,6 +153,7 @@ def triplet_hard_loss(y_true, y_pred, margin=1.0): y_pred: 2-D float `Tensor` of embedding vectors. Embeddings should be l2 normalized. margin: Float, margin term in the loss definition. + soft: Boolean, if set, use the soft margin version. """ labels, embeddings = y_true, y_pred # Reshape label tensor to [batch_size, 1]. @@ -181,7 +182,10 @@ def triplet_hard_loss(y_true, y_pred, margin=1.0): # hard positives: largest D_ap. hard_positives = _masked_maximum(pdist_matrix, mask_positives) - triplet_loss = tf.maximum(hard_positives - hard_negatives + margin, 0.0) + if soft: + triplet_loss = tf.math.log1p(tf.math.exp(hard_positives - hard_negatives)) + else: + triplet_loss = tf.maximum(hard_positives - hard_negatives + margin, 0.0) # Get final mean triplet loss triplet_loss = tf.reduce_mean(triplet_loss) @@ -233,7 +237,7 @@ class TripletHardLoss(tf.keras.losses.Loss): margin constant in the mini-batch. The loss selects the hardest positive and the hardest negative samples within the batch when forming the triplets for computing the loss. - See: https://arxiv.org/abs/1503.03832 + See: https://arxiv.org/pdf/1703.07737. We expect labels `y_true` to be provided as 1-D integer `Tensor` with shape [batch_size] of multi-class integer labels. And embeddings `y_pred` must be @@ -241,15 +245,17 @@ class TripletHardLoss(tf.keras.losses.Loss): Args: margin: Float, margin term in the loss definition. Default value is 1.0. + soft: Boolean, if set, use the soft margin version. Default value is False. name: Optional name for the op. """ - def __init__(self, margin=1.0, name=None, **kwargs): + def __init__(self, margin=1.0, soft=False, name=None, **kwargs): super().__init__(name=name, reduction=tf.keras.losses.Reduction.NONE) self.margin = margin + self.soft = soft def call(self, y_true, y_pred): - return triplet_hard_loss(y_true, y_pred, self.margin) + return triplet_hard_loss(y_true, y_pred, self.margin, self.soft) def get_config(self): config = { From 80dd84f0e327642c2edcbccb52c392026a3aa462 Mon Sep 17 00:00:00 2001 From: HauserA Date: Mon, 27 Jan 2020 21:42:14 +0100 Subject: [PATCH 07/13] test for soft margin version --- tensorflow_addons/losses/triplet_hard_test.py | 80 +++++++++++++------ 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/tensorflow_addons/losses/triplet_hard_test.py b/tensorflow_addons/losses/triplet_hard_test.py index f22fab1c6a..d212839444 100644 --- a/tensorflow_addons/losses/triplet_hard_test.py +++ b/tensorflow_addons/losses/triplet_hard_test.py @@ -50,6 +50,44 @@ def pairwise_distance_np(feature, squared=False): return pairwise_distances +def triplet_hard_loss_np(labels, embedding, margin, soft=False): + + num_data = embedding.shape[0] + # Reshape labels to compute adjacency matrix. + labels_reshaped = np.reshape(labels.astype(np.float32), (labels.shape[0], 1)) + # Compute the loss in NP. + adjacency = np.equal(labels_reshaped, labels_reshaped.T) + + pdist_matrix = pairwise_distance_np(embedding, squared=True) + loss_np = 0.0 + for i in range(num_data): + pos_distances = [] + neg_distances = [] + for j in range(num_data): + if adjacency[i][j] == 0: + neg_distances.append(pdist_matrix[i][j]) + if adjacency[i][j] > 0.0 and i != j: + pos_distances.append(pdist_matrix[i][j]) + + # if there are no positive pairs, distance is 0 + if len(pos_distances) == 0: + pos_distances.append(0) + + # Sort by distance. + neg_distances.sort() + min_neg_distance = neg_distances[0] + pos_distances.sort(reverse=True) + max_pos_distance = pos_distances[0] + + if soft: + loss_np += np.log1p(np.exp(max_pos_distance - min_neg_distance)) + else: + loss_np += np.maximum(0.0, max_pos_distance - min_neg_distance + margin) + + loss_np /= num_data + return loss_np + + @test_utils.run_all_in_graph_and_eager_modes class TripletHardLossTest(tf.test.TestCase): def test_unweighted(self): @@ -61,40 +99,30 @@ def test_unweighted(self): embedding = np.random.rand(num_data, feat_dim).astype(np.float32) labels = np.random.randint(0, num_classes, size=(num_data)) - # Reshape labels to compute adjacency matrix. - labels_reshaped = np.reshape(labels.astype(np.float32), (labels.shape[0], 1)) - # Compute the loss in NP. - adjacency = np.equal(labels_reshaped, labels_reshaped.T) + loss_np = triplet_hard_loss_np(labels, embedding, margin) - pdist_matrix = pairwise_distance_np(embedding, squared=True) - loss_np = 0.0 - for i in range(num_data): - pos_distances = [] - neg_distances = [] - for j in range(num_data): - if adjacency[i][j] == 0: - neg_distances.append(pdist_matrix[i][j]) - if adjacency[i][j] > 0.0 and i != j: - pos_distances.append(pdist_matrix[i][j]) - - # if there are no positive pairs, distance is 0 - if len(pos_distances) == 0: - pos_distances.append(0) + # Compute the loss in TF. + y_true = tf.constant(labels) + y_pred = tf.constant(embedding) + cce_obj = triplet.TripletHardLoss() + loss = cce_obj(y_true, y_pred) + self.assertAlmostEqual(self.evaluate(loss), loss_np, 3) - # Sort by distance. - neg_distances.sort() - min_neg_distance = neg_distances[0] - pos_distances.sort(reverse=True) - max_pos_distance = pos_distances[0] + def test_unweighted_soft(self): + num_data = 20 + feat_dim = 6 + margin = 1.0 + num_classes = 4 - loss_np += np.maximum(0.0, margin - min_neg_distance + max_pos_distance) + embedding = np.random.rand(num_data, feat_dim).astype(np.float32) + labels = np.random.randint(0, num_classes, size=(num_data)) - loss_np /= num_data + loss_np = triplet_hard_loss_np(labels, embedding, margin, soft=True) # Compute the loss in TF. y_true = tf.constant(labels) y_pred = tf.constant(embedding) - cce_obj = triplet.TripletHardLoss() + cce_obj = triplet.TripletHardLoss(soft=True) loss = cce_obj(y_true, y_pred) self.assertAlmostEqual(self.evaluate(loss), loss_np, 3) From 0ed6426ca690119b62485b482604304a11b1ec54 Mon Sep 17 00:00:00 2001 From: HauserA Date: Mon, 27 Jan 2020 21:51:50 +0100 Subject: [PATCH 08/13] Update description of loss function --- tensorflow_addons/losses/triplet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tensorflow_addons/losses/triplet.py b/tensorflow_addons/losses/triplet.py index e9f96ec0d2..fe6819572f 100644 --- a/tensorflow_addons/losses/triplet.py +++ b/tensorflow_addons/losses/triplet.py @@ -145,7 +145,7 @@ def triplet_semihard_loss(y_true, y_pred, margin=1.0): @tf.keras.utils.register_keras_serializable(package="Addons") @tf.function def triplet_hard_loss(y_true, y_pred, margin=1.0, soft=False): - """Computes the triplet loss with hard negative mining. + """Computes the triplet loss with hard negative and hard positive mining. Args: y_true: 1-D integer `Tensor` with shape [batch_size] of From 93c5e9d13d3272cfe1b0e43618c5ee91e4ab2838 Mon Sep 17 00:00:00 2001 From: HauserA Date: Tue, 28 Jan 2020 17:26:11 +0100 Subject: [PATCH 09/13] moved tests into triplet_test.py --- tensorflow_addons/losses/triplet_hard_test.py | 141 ------------------ tensorflow_addons/losses/triplet_test.py | 87 +++++++++++ 2 files changed, 87 insertions(+), 141 deletions(-) delete mode 100644 tensorflow_addons/losses/triplet_hard_test.py diff --git a/tensorflow_addons/losses/triplet_hard_test.py b/tensorflow_addons/losses/triplet_hard_test.py deleted file mode 100644 index d212839444..0000000000 --- a/tensorflow_addons/losses/triplet_hard_test.py +++ /dev/null @@ -1,141 +0,0 @@ -# Copyright 2019 The TensorFlow Authors. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ============================================================================== -"""Tests for triplet hard loss.""" - -import numpy as np -import tensorflow as tf - -from tensorflow_addons.losses import triplet -from tensorflow_addons.utils import test_utils - - -def pairwise_distance_np(feature, squared=False): - """Computes the pairwise distance matrix in numpy. - - Args: - feature: 2-D numpy array of size [number of data, feature dimension] - squared: Boolean. If true, output is the pairwise squared euclidean - distance matrix; else, output is the pairwise euclidean distance - matrix. - - Returns: - pairwise_distances: 2-D numpy array of size - [number of data, number of data]. - """ - triu = np.triu_indices(feature.shape[0], 1) - upper_tri_pdists = np.linalg.norm(feature[triu[1]] - feature[triu[0]], axis=1) - if squared: - upper_tri_pdists **= 2.0 - num_data = feature.shape[0] - pairwise_distances = np.zeros((num_data, num_data)) - pairwise_distances[np.triu_indices(num_data, 1)] = upper_tri_pdists - # Make symmetrical. - pairwise_distances = ( - pairwise_distances - + pairwise_distances.T - - np.diag(pairwise_distances.diagonal()) - ) - return pairwise_distances - - -def triplet_hard_loss_np(labels, embedding, margin, soft=False): - - num_data = embedding.shape[0] - # Reshape labels to compute adjacency matrix. - labels_reshaped = np.reshape(labels.astype(np.float32), (labels.shape[0], 1)) - # Compute the loss in NP. - adjacency = np.equal(labels_reshaped, labels_reshaped.T) - - pdist_matrix = pairwise_distance_np(embedding, squared=True) - loss_np = 0.0 - for i in range(num_data): - pos_distances = [] - neg_distances = [] - for j in range(num_data): - if adjacency[i][j] == 0: - neg_distances.append(pdist_matrix[i][j]) - if adjacency[i][j] > 0.0 and i != j: - pos_distances.append(pdist_matrix[i][j]) - - # if there are no positive pairs, distance is 0 - if len(pos_distances) == 0: - pos_distances.append(0) - - # Sort by distance. - neg_distances.sort() - min_neg_distance = neg_distances[0] - pos_distances.sort(reverse=True) - max_pos_distance = pos_distances[0] - - if soft: - loss_np += np.log1p(np.exp(max_pos_distance - min_neg_distance)) - else: - loss_np += np.maximum(0.0, max_pos_distance - min_neg_distance + margin) - - loss_np /= num_data - return loss_np - - -@test_utils.run_all_in_graph_and_eager_modes -class TripletHardLossTest(tf.test.TestCase): - def test_unweighted(self): - num_data = 20 - feat_dim = 6 - margin = 1.0 - num_classes = 4 - - embedding = np.random.rand(num_data, feat_dim).astype(np.float32) - labels = np.random.randint(0, num_classes, size=(num_data)) - - loss_np = triplet_hard_loss_np(labels, embedding, margin) - - # Compute the loss in TF. - y_true = tf.constant(labels) - y_pred = tf.constant(embedding) - cce_obj = triplet.TripletHardLoss() - loss = cce_obj(y_true, y_pred) - self.assertAlmostEqual(self.evaluate(loss), loss_np, 3) - - def test_unweighted_soft(self): - num_data = 20 - feat_dim = 6 - margin = 1.0 - num_classes = 4 - - embedding = np.random.rand(num_data, feat_dim).astype(np.float32) - labels = np.random.randint(0, num_classes, size=(num_data)) - - loss_np = triplet_hard_loss_np(labels, embedding, margin, soft=True) - - # Compute the loss in TF. - y_true = tf.constant(labels) - y_pred = tf.constant(embedding) - cce_obj = triplet.TripletHardLoss(soft=True) - loss = cce_obj(y_true, y_pred) - self.assertAlmostEqual(self.evaluate(loss), loss_np, 3) - - def test_keras_model_compile(self): - model = tf.keras.models.Sequential( - [tf.keras.layers.Input(shape=(784,)), tf.keras.layers.Dense(10),] - ) - model.compile(loss="Addons>triplet_hard_loss", optimizer="adam") - - def test_serialization(self): - loss = triplet.TripletHardLoss() - new_loss = tf.keras.losses.deserialize(tf.keras.losses.serialize(loss)) - - -if __name__ == "__main__": - tf.test.main() diff --git a/tensorflow_addons/losses/triplet_test.py b/tensorflow_addons/losses/triplet_test.py index 6760397a68..edb8ab140a 100644 --- a/tensorflow_addons/losses/triplet_test.py +++ b/tensorflow_addons/losses/triplet_test.py @@ -50,6 +50,44 @@ def pairwise_distance_np(feature, squared=False): return pairwise_distances +def triplet_hard_loss_np(labels, embedding, margin, soft=False): + + num_data = embedding.shape[0] + # Reshape labels to compute adjacency matrix. + labels_reshaped = np.reshape(labels.astype(np.float32), (labels.shape[0], 1)) + # Compute the loss in NP. + adjacency = np.equal(labels_reshaped, labels_reshaped.T) + + pdist_matrix = pairwise_distance_np(embedding, squared=True) + loss_np = 0.0 + for i in range(num_data): + pos_distances = [] + neg_distances = [] + for j in range(num_data): + if adjacency[i][j] == 0: + neg_distances.append(pdist_matrix[i][j]) + if adjacency[i][j] > 0.0 and i != j: + pos_distances.append(pdist_matrix[i][j]) + + # if there are no positive pairs, distance is 0 + if len(pos_distances) == 0: + pos_distances.append(0) + + # Sort by distance. + neg_distances.sort() + min_neg_distance = neg_distances[0] + pos_distances.sort(reverse=True) + max_pos_distance = pos_distances[0] + + if soft: + loss_np += np.log1p(np.exp(max_pos_distance - min_neg_distance)) + else: + loss_np += np.maximum(0.0, max_pos_distance - min_neg_distance + margin) + + loss_np /= num_data + return loss_np + + @test_utils.run_all_in_graph_and_eager_modes class TripletSemiHardLossTest(tf.test.TestCase): def test_unweighted(self): @@ -114,5 +152,54 @@ def test_serialization(self): new_loss = tf.keras.losses.deserialize(tf.keras.losses.serialize(loss)) +@test_utils.run_all_in_graph_and_eager_modes +class TripletHardLossTest(tf.test.TestCase): + def test_unweighted(self): + num_data = 20 + feat_dim = 6 + margin = 1.0 + num_classes = 4 + + embedding = np.random.rand(num_data, feat_dim).astype(np.float32) + labels = np.random.randint(0, num_classes, size=(num_data)) + + loss_np = triplet_hard_loss_np(labels, embedding, margin) + + # Compute the loss in TF. + y_true = tf.constant(labels) + y_pred = tf.constant(embedding) + cce_obj = triplet.TripletHardLoss() + loss = cce_obj(y_true, y_pred) + self.assertAlmostEqual(self.evaluate(loss), loss_np, 3) + + def test_unweighted_soft(self): + num_data = 20 + feat_dim = 6 + margin = 1.0 + num_classes = 4 + + embedding = np.random.rand(num_data, feat_dim).astype(np.float32) + labels = np.random.randint(0, num_classes, size=(num_data)) + + loss_np = triplet_hard_loss_np(labels, embedding, margin, soft=True) + + # Compute the loss in TF. + y_true = tf.constant(labels) + y_pred = tf.constant(embedding) + cce_obj = triplet.TripletHardLoss(soft=True) + loss = cce_obj(y_true, y_pred) + self.assertAlmostEqual(self.evaluate(loss), loss_np, 3) + + def test_keras_model_compile(self): + model = tf.keras.models.Sequential( + [tf.keras.layers.Input(shape=(784,)), tf.keras.layers.Dense(10),] + ) + model.compile(loss="Addons>triplet_hard_loss", optimizer="adam") + + def test_serialization(self): + loss = triplet.TripletHardLoss() + new_loss = tf.keras.losses.deserialize(tf.keras.losses.serialize(loss)) + + if __name__ == "__main__": tf.test.main() From ee67565627864f0d3c1f386bac6a287d0aeca20d Mon Sep 17 00:00:00 2001 From: HauserA Date: Tue, 28 Jan 2020 17:32:49 +0100 Subject: [PATCH 10/13] Added triplet_hard_loss, TripletHardLoss to init --- tensorflow_addons/losses/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tensorflow_addons/losses/__init__.py b/tensorflow_addons/losses/__init__.py index fba1f3138a..c238285d00 100644 --- a/tensorflow_addons/losses/__init__.py +++ b/tensorflow_addons/losses/__init__.py @@ -19,7 +19,12 @@ from tensorflow_addons.losses.giou_loss import giou_loss, GIoULoss from tensorflow_addons.losses.lifted import lifted_struct_loss, LiftedStructLoss from tensorflow_addons.losses.sparsemax_loss import sparsemax_loss, SparsemaxLoss -from tensorflow_addons.losses.triplet import triplet_semihard_loss, TripletSemiHardLoss +from tensorflow_addons.losses.triplet import ( + triplet_semihard_loss, + triplet_hard_loss, + TripletSemiHardLoss, + TripletHardLoss, +) from tensorflow_addons.losses.quantiles import pinball_loss, PinballLoss # Temporarily disable for windows From c7921140b3049b39ec7affd4906293075fc06c2c Mon Sep 17 00:00:00 2001 From: HauserA Date: Tue, 28 Jan 2020 19:16:05 +0100 Subject: [PATCH 11/13] flake8 unused variable --- tensorflow_addons/losses/triplet_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tensorflow_addons/losses/triplet_test.py b/tensorflow_addons/losses/triplet_test.py index edb8ab140a..d6cab02c03 100644 --- a/tensorflow_addons/losses/triplet_test.py +++ b/tensorflow_addons/losses/triplet_test.py @@ -198,7 +198,7 @@ def test_keras_model_compile(self): def test_serialization(self): loss = triplet.TripletHardLoss() - new_loss = tf.keras.losses.deserialize(tf.keras.losses.serialize(loss)) + tf.keras.losses.deserialize(tf.keras.losses.serialize(loss)) if __name__ == "__main__": From d75ca61fcac7b1ae228472279ea23e76d43a603b Mon Sep 17 00:00:00 2001 From: HauserA Date: Tue, 28 Jan 2020 19:21:15 +0100 Subject: [PATCH 12/13] added typechecks for __init__ --- tensorflow_addons/losses/triplet.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tensorflow_addons/losses/triplet.py b/tensorflow_addons/losses/triplet.py index fe6819572f..c7a05c76d6 100644 --- a/tensorflow_addons/losses/triplet.py +++ b/tensorflow_addons/losses/triplet.py @@ -16,6 +16,7 @@ import tensorflow as tf from tensorflow_addons.losses import metric_learning +from typeguard import typechecked def _masked_maximum(data, mask, dim=1): @@ -249,7 +250,10 @@ class TripletHardLoss(tf.keras.losses.Loss): name: Optional name for the op. """ - def __init__(self, margin=1.0, soft=False, name=None, **kwargs): + @typechecked + def __init__( + self, margin: float = 1.0, soft: bool = False, name: str = None, **kwargs + ): super().__init__(name=name, reduction=tf.keras.losses.Reduction.NONE) self.margin = margin self.soft = soft From 0941df9480540a475231fc2cad569e3e7595f6b6 Mon Sep 17 00:00:00 2001 From: HauserA Date: Wed, 29 Jan 2020 22:53:14 +0100 Subject: [PATCH 13/13] serialize self.soft --- tensorflow_addons/losses/triplet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tensorflow_addons/losses/triplet.py b/tensorflow_addons/losses/triplet.py index c7a05c76d6..0e375b08c0 100644 --- a/tensorflow_addons/losses/triplet.py +++ b/tensorflow_addons/losses/triplet.py @@ -264,6 +264,7 @@ def call(self, y_true, y_pred): def get_config(self): config = { "margin": self.margin, + "soft": self.soft, } base_config = super().get_config() return {**base_config, **config}