|
| 1 | +from __future__ import absolute_import |
| 2 | +from __future__ import division |
| 3 | +from __future__ import print_function |
| 4 | + |
| 5 | +from tensorflow.python.framework import dtypes |
| 6 | +from tensorflow.python.framework import ops |
| 7 | +from tensorflow.python.framework import sparse_tensor |
| 8 | +from tensorflow.python.framework import tensor_shape |
| 9 | +from tensorflow.python.ops import array_ops |
| 10 | +from tensorflow.python.ops import control_flow_ops |
| 11 | +from tensorflow.python.ops import logging_ops |
| 12 | +from tensorflow.python.ops import math_ops |
| 13 | +from tensorflow.python.ops import nn |
| 14 | +from tensorflow.python.ops import script_ops |
| 15 | +from tensorflow.python.ops import sparse_ops |
| 16 | +from tensorflow.python.summary import summary |
| 17 | +from sklearn import metrics |
| 18 | + |
| 19 | + |
| 20 | +def pairwise_distance(feature, squared=False): |
| 21 | + """Computes the pairwise distance matrix with numerical stability. |
| 22 | + output[i, j] = || feature[i, :] - feature[j, :] ||_2 |
| 23 | + Args: |
| 24 | + feature: 2-D Tensor of size [number of data, feature dimension]. |
| 25 | + squared: Boolean, whether or not to square the pairwise distances. |
| 26 | + Returns: |
| 27 | + pairwise_distances: 2-D Tensor of size [number of data, number of data]. |
| 28 | + """ |
| 29 | + pairwise_distances_squared = math_ops.add( |
| 30 | + math_ops.reduce_sum(math_ops.square(feature), axis=[1], keepdims=True), |
| 31 | + math_ops.reduce_sum( |
| 32 | + math_ops.square(array_ops.transpose(feature)), |
| 33 | + axis=[0], |
| 34 | + keepdims=True)) - 2.0 * math_ops.matmul(feature, |
| 35 | + array_ops.transpose(feature)) |
| 36 | + |
| 37 | + # Deal with numerical inaccuracies. Set small negatives to zero. |
| 38 | + pairwise_distances_squared = math_ops.maximum(pairwise_distances_squared, 0.0) |
| 39 | + # Get the mask where the zero distances are at. |
| 40 | + error_mask = math_ops.less_equal(pairwise_distances_squared, 0.0) |
| 41 | + |
| 42 | + # Optionally take the sqrt. |
| 43 | + if squared: |
| 44 | + pairwise_distances = pairwise_distances_squared |
| 45 | + else: |
| 46 | + pairwise_distances = math_ops.sqrt( |
| 47 | + pairwise_distances_squared + math_ops.to_float(error_mask) * 1e-16) |
| 48 | + |
| 49 | + # Undo conditionally adding 1e-16. |
| 50 | + pairwise_distances = math_ops.multiply( |
| 51 | + pairwise_distances, math_ops.to_float(math_ops.logical_not(error_mask))) |
| 52 | + |
| 53 | + num_data = array_ops.shape(feature)[0] |
| 54 | + # Explicitly set diagonals to zero. |
| 55 | + mask_offdiagonals = array_ops.ones_like(pairwise_distances) - array_ops.diag( |
| 56 | + array_ops.ones([num_data])) |
| 57 | + pairwise_distances = math_ops.multiply(pairwise_distances, mask_offdiagonals) |
| 58 | + return pairwise_distances |
| 59 | + |
| 60 | +def pairwise_cosine_distance(feature): |
| 61 | + # normalize each row |
| 62 | + normalized = nn.l2_normalize(feature, axis = 1) |
| 63 | + |
| 64 | + # multiply row i with row j using transpose |
| 65 | + # element wise product |
| 66 | + prod = math_ops.matmul(normalized, normalized, |
| 67 | + adjoint_b = True # transpose second matrix |
| 68 | + ) |
| 69 | + |
| 70 | + dist = 1 - prod |
| 71 | + return dist |
| 72 | + |
| 73 | +def _build_multilabel_adjacency(labels): |
| 74 | + """ |
| 75 | + Since that we assume labels share at least one concepts are similar and don't |
| 76 | + share any concepts are dissimilar, so we can compute c @ c.T, and zero elements |
| 77 | + are dissimilar pairs, otherwise similar. |
| 78 | + :param labels: labels of [batch_size, class_num] |
| 79 | + :return: a [batch_size, batch_size] adjacency matrix |
| 80 | + """ |
| 81 | + adj = labels @ array_ops.transpose(labels) |
| 82 | + return math_ops.greater(adj, 0) |
| 83 | + |
| 84 | +def masked_maximum(data, mask, dim=1): |
| 85 | + """Computes the axis wise maximum over chosen elements. |
| 86 | + Args: |
| 87 | + data: 2-D float `Tensor` of size [n, m]. |
| 88 | + mask: 2-D Boolean `Tensor` of size [n, m]. |
| 89 | + dim: The dimension over which to compute the maximum. |
| 90 | + Returns: |
| 91 | + masked_maximums: N-D `Tensor`. |
| 92 | + The maximized dimension is of size 1 after the operation. |
| 93 | + """ |
| 94 | + axis_minimums = math_ops.reduce_min(data, dim, keepdims=True) |
| 95 | + masked_maximums = math_ops.reduce_max( |
| 96 | + math_ops.multiply(data - axis_minimums, mask), dim, |
| 97 | + keepdims=True) + axis_minimums |
| 98 | + return masked_maximums |
| 99 | + |
| 100 | + |
| 101 | +def masked_minimum(data, mask, dim=1): |
| 102 | + """Computes the axis wise minimum over chosen elements. |
| 103 | + Args: |
| 104 | + data: 2-D float `Tensor` of size [n, m]. |
| 105 | + mask: 2-D Boolean `Tensor` of size [n, m]. |
| 106 | + dim: The dimension over which to compute the minimum. |
| 107 | + Returns: |
| 108 | + masked_minimums: N-D `Tensor`. |
| 109 | + The minimized dimension is of size 1 after the operation. |
| 110 | + """ |
| 111 | + axis_maximums = math_ops.reduce_max(data, dim, keepdims=True) |
| 112 | + masked_minimums = math_ops.reduce_min( |
| 113 | + math_ops.multiply(data - axis_maximums, mask), dim, |
| 114 | + keepdims=True) + axis_maximums |
| 115 | + return masked_minimums |
| 116 | + |
| 117 | + |
| 118 | +def triplet_semihard_loss_multilabel(labels, embeddings, use_cos=False, margin=1.0): |
| 119 | + """Computes the triplet loss with semi-hard negative mining. |
| 120 | + The loss encourages the positive distances (between a pair of embeddings with |
| 121 | + the same labels) to be smaller than the minimum negative distance among |
| 122 | + which are at least greater than the positive distance plus the margin constant |
| 123 | + (called semi-hard negative) in the mini-batch. If no such negative exists, |
| 124 | + uses the largest negative distance instead. |
| 125 | + See: https://arxiv.org/abs/1503.03832. |
| 126 | + Args: |
| 127 | + labels: tensor of shape [batch_size, class_num] for multi-label samples |
| 128 | + embeddings: 2-D float `Tensor` of embedding vectors. Embeddings should |
| 129 | + be l2 normalized. |
| 130 | + use_cos: metric of embedding, cosine similarity or l2 distance |
| 131 | + margin: Float, margin term in the loss definition. |
| 132 | + Returns: |
| 133 | + triplet_loss: tf.float32 scalar. |
| 134 | + """ |
| 135 | + # Reshape [batch_size] label tensor to a [batch_size, 1] label tensor. |
| 136 | + |
| 137 | + # Build pairwise squared distance matrix. |
| 138 | + pdist_matrix = pairwise_cosine_distance(embeddings) if use_cos else pairwise_distance(embeddings, squared=True) |
| 139 | + # Build pairwise binary adjacency matrix. |
| 140 | + adjacency = _build_multilabel_adjacency(labels) |
| 141 | + # Invert so we can select negatives only. |
| 142 | + adjacency_not = math_ops.logical_not(adjacency) |
| 143 | + |
| 144 | + batch_size = labels.get_shape().as_list()[0] |
| 145 | + |
| 146 | + # Compute the mask. |
| 147 | + pdist_matrix_tile = array_ops.tile(pdist_matrix, [batch_size, 1]) |
| 148 | + mask = math_ops.logical_and( |
| 149 | + array_ops.tile(adjacency_not, [batch_size, 1]), |
| 150 | + math_ops.greater( |
| 151 | + pdist_matrix_tile, array_ops.reshape( |
| 152 | + array_ops.transpose(pdist_matrix), [-1, 1]))) |
| 153 | + mask_final = array_ops.reshape( |
| 154 | + math_ops.greater( |
| 155 | + math_ops.reduce_sum( |
| 156 | + math_ops.cast(mask, dtype=dtypes.float32), 1, keepdims=True), |
| 157 | + 0.0), [batch_size, batch_size]) |
| 158 | + mask_final = array_ops.transpose(mask_final) |
| 159 | + |
| 160 | + adjacency_not = math_ops.cast(adjacency_not, dtype=dtypes.float32) |
| 161 | + mask = math_ops.cast(mask, dtype=dtypes.float32) |
| 162 | + |
| 163 | + # negatives_outside: smallest D_an where D_an > D_ap. |
| 164 | + negatives_outside = array_ops.reshape( |
| 165 | + masked_minimum(pdist_matrix_tile, mask), [batch_size, batch_size]) |
| 166 | + negatives_outside = array_ops.transpose(negatives_outside) |
| 167 | + |
| 168 | + # negatives_inside: largest D_an. |
| 169 | + negatives_inside = array_ops.tile( |
| 170 | + masked_maximum(pdist_matrix, adjacency_not), [1, batch_size]) |
| 171 | + semi_hard_negatives = array_ops.where( |
| 172 | + mask_final, negatives_outside, negatives_inside) |
| 173 | + |
| 174 | + loss_mat = math_ops.add(margin, pdist_matrix - semi_hard_negatives) |
| 175 | + |
| 176 | + mask_positives = math_ops.cast( |
| 177 | + adjacency, dtype=dtypes.float32) - array_ops.diag( |
| 178 | + array_ops.ones([batch_size])) |
| 179 | + |
| 180 | + # In lifted-struct, the authors multiply 0.5 for upper triangular |
| 181 | + # in semihard, they take all positive pairs except the diagonal. |
| 182 | + num_positives = math_ops.reduce_sum(mask_positives) |
| 183 | + |
| 184 | + triplet_loss = math_ops.truediv( |
| 185 | + math_ops.reduce_sum( |
| 186 | + math_ops.maximum( |
| 187 | + math_ops.multiply(loss_mat, mask_positives), 0.0)), |
| 188 | + num_positives, |
| 189 | + name='triplet_semihard_loss') |
| 190 | + |
| 191 | + return triplet_loss |
0 commit comments