Skip to content

Commit a2a9903

Browse files
authored
Add files via upload
1 parent 4e4c72d commit a2a9903

1 file changed

Lines changed: 191 additions & 0 deletions

File tree

TripletLoss.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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

Comments
 (0)