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

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.""" 

16 

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 

23 

24 

25def _masked_maximum(data, mask, dim=1): 

26 """Computes the axis wise maximum over chosen elements. 

27 

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. 

32 

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 

45 

46 

47def _masked_minimum(data, mask, dim=1): 

48 """Computes the axis wise minimum over chosen elements. 

49 

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. 

54 

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 

67 

68 

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. 

78 

79 Usage: 

80 

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> 

85 

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> 

90 

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. 

101 

102 A `Callable` should take a batch of embeddings as input and 

103 return the pairwise distance matrix. 

104 

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") 

110 

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 ) 

117 

118 # Reshape label tensor to [batch_size, 1]. 

119 lshape = tf.shape(labels) 

120 labels = tf.reshape(labels, [lshape[0], 1]) 

121 

122 # Build pairwise squared distance matrix 

123 

124 if distance_metric == "L2": 

125 pdist_matrix = metric_learning.pairwise_distance( 

126 precise_embeddings, squared=False 

127 ) 

128 

129 elif distance_metric == "squared-L2": 

130 pdist_matrix = metric_learning.pairwise_distance( 

131 precise_embeddings, squared=True 

132 ) 

133 

134 elif distance_metric == "angular": 

135 pdist_matrix = metric_learning.angular_distance(precise_embeddings) 

136 

137 else: 

138 pdist_matrix = distance_metric(precise_embeddings) 

139 

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) 

144 

145 batch_size = tf.size(labels) 

146 

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) 

165 

166 adjacency_not = tf.cast(adjacency_not, dtype=tf.dtypes.float32) 

167 mask = tf.cast(mask, dtype=tf.dtypes.float32) 

168 

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) 

174 

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) 

180 

181 loss_mat = tf.math.add(margin, pdist_matrix - semi_hard_negatives) 

182 

183 mask_positives = tf.cast(adjacency, dtype=tf.dtypes.float32) - tf.linalg.diag( 

184 tf.ones([batch_size]) 

185 ) 

186 

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) 

190 

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 ) 

197 

198 if convert_to_float32: 

199 return tf.cast(triplet_loss, embeddings.dtype) 

200 else: 

201 return triplet_loss 

202 

203 

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. 

214 

215 Usage: 

216 

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> 

221 

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> 

226 

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. 

238 

239 A `Callable` should take a batch of embeddings as input and 

240 return the pairwise distance matrix. 

241 

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") 

247 

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 ) 

254 

255 # Reshape label tensor to [batch_size, 1]. 

256 lshape = tf.shape(labels) 

257 labels = tf.reshape(labels, [lshape[0], 1]) 

258 

259 # Build pairwise squared distance matrix. 

260 if distance_metric == "L2": 

261 pdist_matrix = metric_learning.pairwise_distance( 

262 precise_embeddings, squared=False 

263 ) 

264 

265 elif distance_metric == "squared-L2": 

266 pdist_matrix = metric_learning.pairwise_distance( 

267 precise_embeddings, squared=True 

268 ) 

269 

270 elif distance_metric == "angular": 

271 pdist_matrix = metric_learning.angular_distance(precise_embeddings) 

272 

273 else: 

274 pdist_matrix = distance_metric(precise_embeddings) 

275 

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) 

280 

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) 

284 

285 batch_size = tf.size(labels) 

286 

287 adjacency = tf.cast(adjacency, dtype=tf.dtypes.float32) 

288 

289 mask_positives = tf.cast(adjacency, dtype=tf.dtypes.float32) - tf.linalg.diag( 

290 tf.ones([batch_size]) 

291 ) 

292 

293 # hard positives: largest D_ap. 

294 hard_positives = _masked_maximum(pdist_matrix, mask_positives) 

295 

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) 

300 

301 # Get final mean triplet loss 

302 triplet_loss = tf.reduce_mean(triplet_loss) 

303 

304 if convert_to_float32: 

305 return tf.cast(triplet_loss, embeddings.dtype) 

306 else: 

307 return triplet_loss 

308 

309 

310@tf.keras.utils.register_keras_serializable(package="Addons") 

311class TripletSemiHardLoss(LossFunctionWrapper): 

312 """Computes the triplet loss with semi-hard negative mining. 

313 

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. 

320 

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. 

324 

325 Args: 

326 margin: Float, margin term in the loss definition. Default value is 1.0. 

327 name: Optional name for the op. 

328 """ 

329 

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 ) 

345 

346 

347@tf.keras.utils.register_keras_serializable(package="Addons") 

348class TripletHardLoss(LossFunctionWrapper): 

349 """Computes the triplet loss with hard negative and hard positive mining. 

350 

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. 

357 

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. 

361 

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 """ 

367 

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 )