Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/tensorflow_addons/losses/triplet.py: 25%
92 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-03 07:57 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-03 07:57 +0000
1# Copyright 2019 The TensorFlow Authors. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ==============================================================================
15"""Implements triplet loss."""
17import tensorflow as tf
18from tensorflow_addons.losses import metric_learning
19from tensorflow_addons.utils.keras_utils import LossFunctionWrapper
20from tensorflow_addons.utils.types import FloatTensorLike, TensorLike
21from typeguard import typechecked
22from typing import Optional, Union, Callable
25def _masked_maximum(data, mask, dim=1):
26 """Computes the axis wise maximum over chosen elements.
28 Args:
29 data: 2-D float `Tensor` of shape `[n, m]`.
30 mask: 2-D Boolean `Tensor` of shape `[n, m]`.
31 dim: The dimension over which to compute the maximum.
33 Returns:
34 masked_maximums: N-D `Tensor`.
35 The maximized dimension is of size 1 after the operation.
36 """
37 axis_minimums = tf.math.reduce_min(data, dim, keepdims=True)
38 masked_maximums = (
39 tf.math.reduce_max(
40 tf.math.multiply(data - axis_minimums, mask), dim, keepdims=True
41 )
42 + axis_minimums
43 )
44 return masked_maximums
47def _masked_minimum(data, mask, dim=1):
48 """Computes the axis wise minimum over chosen elements.
50 Args:
51 data: 2-D float `Tensor` of shape `[n, m]`.
52 mask: 2-D Boolean `Tensor` of shape `[n, m]`.
53 dim: The dimension over which to compute the minimum.
55 Returns:
56 masked_minimums: N-D `Tensor`.
57 The minimized dimension is of size 1 after the operation.
58 """
59 axis_maximums = tf.math.reduce_max(data, dim, keepdims=True)
60 masked_minimums = (
61 tf.math.reduce_min(
62 tf.math.multiply(data - axis_maximums, mask), dim, keepdims=True
63 )
64 + axis_maximums
65 )
66 return masked_minimums
69@tf.keras.utils.register_keras_serializable(package="Addons")
70@tf.function
71def triplet_semihard_loss(
72 y_true: TensorLike,
73 y_pred: TensorLike,
74 margin: FloatTensorLike = 1.0,
75 distance_metric: Union[str, Callable] = "L2",
76) -> tf.Tensor:
77 r"""Computes the triplet loss with semi-hard negative mining.
79 Usage:
81 >>> y_true = tf.convert_to_tensor([0, 0])
82 >>> y_pred = tf.convert_to_tensor([[0.0, 1.0], [1.0, 0.0]])
83 >>> tfa.losses.triplet_semihard_loss(y_true, y_pred, distance_metric="L2")
84 <tf.Tensor: shape=(), dtype=float32, numpy=2.4142137>
86 >>> # Calling with callable `distance_metric`
87 >>> distance_metric = lambda x: tf.linalg.matmul(x, x, transpose_b=True)
88 >>> tfa.losses.triplet_semihard_loss(y_true, y_pred, distance_metric=distance_metric)
89 <tf.Tensor: shape=(), dtype=float32, numpy=1.0>
91 Args:
92 y_true: 1-D integer `Tensor` with shape `[batch_size]` of
93 multiclass integer labels.
94 y_pred: 2-D float `Tensor` of embedding vectors. Embeddings should
95 be l2 normalized.
96 margin: Float, margin term in the loss definition.
97 distance_metric: `str` or a `Callable` that determines distance metric.
98 Valid strings are "L2" for l2-norm distance,
99 "squared-L2" for squared l2-norm distance,
100 and "angular" for cosine similarity.
102 A `Callable` should take a batch of embeddings as input and
103 return the pairwise distance matrix.
105 Returns:
106 triplet_loss: float scalar with dtype of `y_pred`.
107 """
108 labels = tf.convert_to_tensor(y_true, name="labels")
109 embeddings = tf.convert_to_tensor(y_pred, name="embeddings")
111 convert_to_float32 = (
112 embeddings.dtype == tf.dtypes.float16 or embeddings.dtype == tf.dtypes.bfloat16
113 )
114 precise_embeddings = (
115 tf.cast(embeddings, tf.dtypes.float32) if convert_to_float32 else embeddings
116 )
118 # Reshape label tensor to [batch_size, 1].
119 lshape = tf.shape(labels)
120 labels = tf.reshape(labels, [lshape[0], 1])
122 # Build pairwise squared distance matrix
124 if distance_metric == "L2":
125 pdist_matrix = metric_learning.pairwise_distance(
126 precise_embeddings, squared=False
127 )
129 elif distance_metric == "squared-L2":
130 pdist_matrix = metric_learning.pairwise_distance(
131 precise_embeddings, squared=True
132 )
134 elif distance_metric == "angular":
135 pdist_matrix = metric_learning.angular_distance(precise_embeddings)
137 else:
138 pdist_matrix = distance_metric(precise_embeddings)
140 # Build pairwise binary adjacency matrix.
141 adjacency = tf.math.equal(labels, tf.transpose(labels))
142 # Invert so we can select negatives only.
143 adjacency_not = tf.math.logical_not(adjacency)
145 batch_size = tf.size(labels)
147 # Compute the mask.
148 pdist_matrix_tile = tf.tile(pdist_matrix, [batch_size, 1])
149 mask = tf.math.logical_and(
150 tf.tile(adjacency_not, [batch_size, 1]),
151 tf.math.greater(
152 pdist_matrix_tile, tf.reshape(tf.transpose(pdist_matrix), [-1, 1])
153 ),
154 )
155 mask_final = tf.reshape(
156 tf.math.greater(
157 tf.math.reduce_sum(
158 tf.cast(mask, dtype=tf.dtypes.float32), 1, keepdims=True
159 ),
160 0.0,
161 ),
162 [batch_size, batch_size],
163 )
164 mask_final = tf.transpose(mask_final)
166 adjacency_not = tf.cast(adjacency_not, dtype=tf.dtypes.float32)
167 mask = tf.cast(mask, dtype=tf.dtypes.float32)
169 # negatives_outside: smallest D_an where D_an > D_ap.
170 negatives_outside = tf.reshape(
171 _masked_minimum(pdist_matrix_tile, mask), [batch_size, batch_size]
172 )
173 negatives_outside = tf.transpose(negatives_outside)
175 # negatives_inside: largest D_an.
176 negatives_inside = tf.tile(
177 _masked_maximum(pdist_matrix, adjacency_not), [1, batch_size]
178 )
179 semi_hard_negatives = tf.where(mask_final, negatives_outside, negatives_inside)
181 loss_mat = tf.math.add(margin, pdist_matrix - semi_hard_negatives)
183 mask_positives = tf.cast(adjacency, dtype=tf.dtypes.float32) - tf.linalg.diag(
184 tf.ones([batch_size])
185 )
187 # In lifted-struct, the authors multiply 0.5 for upper triangular
188 # in semihard, they take all positive pairs except the diagonal.
189 num_positives = tf.math.reduce_sum(mask_positives)
191 triplet_loss = tf.math.truediv(
192 tf.math.reduce_sum(
193 tf.math.maximum(tf.math.multiply(loss_mat, mask_positives), 0.0)
194 ),
195 num_positives,
196 )
198 if convert_to_float32:
199 return tf.cast(triplet_loss, embeddings.dtype)
200 else:
201 return triplet_loss
204@tf.keras.utils.register_keras_serializable(package="Addons")
205@tf.function
206def triplet_hard_loss(
207 y_true: TensorLike,
208 y_pred: TensorLike,
209 margin: FloatTensorLike = 1.0,
210 soft: bool = False,
211 distance_metric: Union[str, Callable] = "L2",
212) -> tf.Tensor:
213 r"""Computes the triplet loss with hard negative and hard positive mining.
215 Usage:
217 >>> y_true = tf.convert_to_tensor([0, 0])
218 >>> y_pred = tf.convert_to_tensor([[0.0, 1.0], [1.0, 0.0]])
219 >>> tfa.losses.triplet_hard_loss(y_true, y_pred, distance_metric="L2")
220 <tf.Tensor: shape=(), dtype=float32, numpy=1.0>
222 >>> # Calling with callable `distance_metric`
223 >>> distance_metric = lambda x: tf.linalg.matmul(x, x, transpose_b=True)
224 >>> tfa.losses.triplet_hard_loss(y_true, y_pred, distance_metric=distance_metric)
225 <tf.Tensor: shape=(), dtype=float32, numpy=0.0>
227 Args:
228 y_true: 1-D integer `Tensor` with shape `[batch_size]` of
229 multiclass integer labels.
230 y_pred: 2-D float `Tensor` of embedding vectors. Embeddings should
231 be l2 normalized.
232 margin: Float, margin term in the loss definition.
233 soft: Boolean, if set, use the soft margin version.
234 distance_metric: `str` or a `Callable` that determines distance metric.
235 Valid strings are "L2" for l2-norm distance,
236 "squared-L2" for squared l2-norm distance,
237 and "angular" for cosine similarity.
239 A `Callable` should take a batch of embeddings as input and
240 return the pairwise distance matrix.
242 Returns:
243 triplet_loss: float scalar with dtype of `y_pred`.
244 """
245 labels = tf.convert_to_tensor(y_true, name="labels")
246 embeddings = tf.convert_to_tensor(y_pred, name="embeddings")
248 convert_to_float32 = (
249 embeddings.dtype == tf.dtypes.float16 or embeddings.dtype == tf.dtypes.bfloat16
250 )
251 precise_embeddings = (
252 tf.cast(embeddings, tf.dtypes.float32) if convert_to_float32 else embeddings
253 )
255 # Reshape label tensor to [batch_size, 1].
256 lshape = tf.shape(labels)
257 labels = tf.reshape(labels, [lshape[0], 1])
259 # Build pairwise squared distance matrix.
260 if distance_metric == "L2":
261 pdist_matrix = metric_learning.pairwise_distance(
262 precise_embeddings, squared=False
263 )
265 elif distance_metric == "squared-L2":
266 pdist_matrix = metric_learning.pairwise_distance(
267 precise_embeddings, squared=True
268 )
270 elif distance_metric == "angular":
271 pdist_matrix = metric_learning.angular_distance(precise_embeddings)
273 else:
274 pdist_matrix = distance_metric(precise_embeddings)
276 # Build pairwise binary adjacency matrix.
277 adjacency = tf.math.equal(labels, tf.transpose(labels))
278 # Invert so we can select negatives only.
279 adjacency_not = tf.math.logical_not(adjacency)
281 adjacency_not = tf.cast(adjacency_not, dtype=tf.dtypes.float32)
282 # hard negatives: smallest D_an.
283 hard_negatives = _masked_minimum(pdist_matrix, adjacency_not)
285 batch_size = tf.size(labels)
287 adjacency = tf.cast(adjacency, dtype=tf.dtypes.float32)
289 mask_positives = tf.cast(adjacency, dtype=tf.dtypes.float32) - tf.linalg.diag(
290 tf.ones([batch_size])
291 )
293 # hard positives: largest D_ap.
294 hard_positives = _masked_maximum(pdist_matrix, mask_positives)
296 if soft:
297 triplet_loss = tf.math.log1p(tf.math.exp(hard_positives - hard_negatives))
298 else:
299 triplet_loss = tf.maximum(hard_positives - hard_negatives + margin, 0.0)
301 # Get final mean triplet loss
302 triplet_loss = tf.reduce_mean(triplet_loss)
304 if convert_to_float32:
305 return tf.cast(triplet_loss, embeddings.dtype)
306 else:
307 return triplet_loss
310@tf.keras.utils.register_keras_serializable(package="Addons")
311class TripletSemiHardLoss(LossFunctionWrapper):
312 """Computes the triplet loss with semi-hard negative mining.
314 The loss encourages the positive distances (between a pair of embeddings
315 with the same labels) to be smaller than the minimum negative distance
316 among which are at least greater than the positive distance plus the
317 margin constant (called semi-hard negative) in the mini-batch.
318 If no such negative exists, uses the largest negative distance instead.
319 See: https://arxiv.org/abs/1503.03832.
321 We expect labels `y_true` to be provided as 1-D integer `Tensor` with shape
322 `[batch_size]` of multi-class integer labels. And embeddings `y_pred` must be
323 2-D float `Tensor` of l2 normalized embedding vectors.
325 Args:
326 margin: Float, margin term in the loss definition. Default value is 1.0.
327 name: Optional name for the op.
328 """
330 @typechecked
331 def __init__(
332 self,
333 margin: FloatTensorLike = 1.0,
334 distance_metric: Union[str, Callable] = "L2",
335 name: Optional[str] = None,
336 **kwargs,
337 ):
338 super().__init__(
339 triplet_semihard_loss,
340 name=name,
341 reduction=tf.keras.losses.Reduction.NONE,
342 margin=margin,
343 distance_metric=distance_metric,
344 )
347@tf.keras.utils.register_keras_serializable(package="Addons")
348class TripletHardLoss(LossFunctionWrapper):
349 """Computes the triplet loss with hard negative and hard positive mining.
351 The loss encourages the maximum positive distance (between a pair of embeddings
352 with the same labels) to be smaller than the minimum negative distance plus the
353 margin constant in the mini-batch.
354 The loss selects the hardest positive and the hardest negative samples
355 within the batch when forming the triplets for computing the loss.
356 See: https://arxiv.org/pdf/1703.07737.
358 We expect labels `y_true` to be provided as 1-D integer `Tensor` with shape
359 `[batch_size]` of multi-class integer labels. And embeddings `y_pred` must be
360 2-D float `Tensor` of l2 normalized embedding vectors.
362 Args:
363 margin: Float, margin term in the loss definition. Default value is 1.0.
364 soft: Boolean, if set, use the soft margin version. Default value is False.
365 name: Optional name for the op.
366 """
368 @typechecked
369 def __init__(
370 self,
371 margin: FloatTensorLike = 1.0,
372 soft: bool = False,
373 distance_metric: Union[str, Callable] = "L2",
374 name: Optional[str] = None,
375 **kwargs,
376 ):
377 super().__init__(
378 triplet_hard_loss,
379 name=name,
380 reduction=tf.keras.losses.Reduction.NONE,
381 margin=margin,
382 soft=soft,
383 distance_metric=distance_metric,
384 )