diff --git a/loss/__init__.py b/loss/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/loss/ada_scaling_loss.py b/loss/ada_scaling_loss.py new file mode 100644 index 0000000..c7b9c62 --- /dev/null +++ b/loss/ada_scaling_loss.py @@ -0,0 +1,62 @@ +import tensorflow as tf + + +def f1_reweight_loss(logits, labels, beta2): + """from paper: Adaptive Scaling for Sparse Detection in Information Extraction""" + m = tf.count_nonzero(labels, dtype=tf.float32) + batch_size = tf.shape(labels)[0] + n = tf.cast(batch_size, tf.float32) - m + probs = tf.nn.softmax(logits) # [batch_size, num_classes] + batch_idx = tf.range(batch_size) + label_with_idx = tf.concat([tf.expand_dims(t, 1) for t in [batch_idx, tf.cast(labels, tf.int32)]], 1) # [batch_size, 2] + golden_probs = tf.gather_nd(probs, label_with_idx) # [batch_size] + zeros = tf.zeros_like(golden_probs) + is_negative = tf.equal(labels, 0) + p1 = tf.reduce_sum(tf.where(is_negative, zeros, golden_probs)) # TP + p2 = tf.reduce_sum(tf.where(is_negative, golden_probs, zeros)) # TN + neg_weights = p1 / ((beta2 * m) + n - p2 + 1e-8) + ones = tf.ones_like(golden_probs) + weights = tf.where(is_negative, ones * neg_weights, ones) + return tf.losses.sparse_softmax_cross_entropy(labels, logits, weights) + + +def f1_reweight_loss_v2(logits, labels, beta2): + probs = tf.nn.softmax(logits) # [batch_size, num_classes] + labels = tf.cast(labels, tf.int32) + negative_idx = tf.where(tf.equal(labels, 0), tf.ones_like(labels, dtype=tf.float32), tf.zeros_like(labels, dtype=tf.float32)) + positive_idx = 1.0 - negative_idx + + batch_idx = tf.range(tf.shape(probs)[0]) + label_with_idx = tf.concat([tf.expand_dims(t, 1) for t in [batch_idx, labels]], 1) + golden_prob = tf.gather_nd(probs, label_with_idx) + m = tf.reduce_sum(positive_idx) + n = tf.reduce_sum(negative_idx) + p1 = tf.reduce_sum(positive_idx * golden_prob) + p2 = tf.reduce_sum(negative_idx * golden_prob) + neg_weight = p1 / ((beta2 * m) + n - p2 + 1e-8) + all_one = tf.ones(tf.shape(golden_prob)) + loss_weight = all_one * positive_idx + all_one * neg_weight * negative_idx + + loss = - loss_weight * tf.log(golden_prob + 1e-8) + return loss + + +def f1_reweight_sigmoid_cross_entropy(logits, labels, beta_square, label_smoothing=0, weights=None): + probs = tf.nn.sigmoid(logits) + if len(labels.shape.as_list()) == 1: + labels = tf.expand_dims(labels, -1) + labels = tf.to_float(labels) + batch_size = tf.shape(labels)[0] + batch_size_float = tf.to_float(batch_size) + num_pos = tf.reduce_sum(labels, axis=0) + num_neg = batch_size_float - num_pos + tp = tf.reduce_sum(probs, axis=0) + tn = batch_size_float - tp + neg_weight = tp / (beta_square * num_pos + num_neg - tn + 1e-8) + neg_weight_tile = tf.tile(tf.expand_dims(neg_weight, 0), [batch_size, 1]) + final_weights = tf.where(tf.equal(labels, 1.0), tf.ones_like(labels), neg_weight_tile) + if weights is not None: + if len(weights.shape.as_list()) == 1: + weights = tf.expand_dims(weights, -1) + final_weights *= weights + return tf.losses.sigmoid_cross_entropy(labels, logits, final_weights, label_smoothing=label_smoothing) diff --git a/loss/center_loss.py b/loss/center_loss.py new file mode 100755 index 0000000..fe31957 --- /dev/null +++ b/loss/center_loss.py @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# coding: utf-8 + +import tensorflow as tf + + +def get_center_loss(features, labels, alpha, num_classes): + """获取center loss及center的更新op + + Arguments: + features: Tensor,表征样本特征,一般使用某个fc层的输出,shape应该为[batch_size, feature_length]. + labels: Tensor,表征样本label,非one-hot编码,shape应为[batch_size]. + alpha: 0-1之间的数字,控制样本类别中心的学习率,细节参考原文. + num_classes: 整数,表明总共有多少个类别,网络分类输出有多少个神经元这里就取多少. + + Return: + loss: Tensor,可与softmax loss相加作为总的loss进行优化. + centers: Tensor,存储样本中心值的Tensor,仅查看样本中心存储的具体数值时有用. + centers_update_op: op,用于更新样本中心的op,在训练时需要同时运行该op,否则样本中心不会更新 + """ + # 获取特征的维数,例如256维 + len_features = features.get_shape()[1] + # 建立一个Variable,shape为[num_classes, len_features],用于存储整个网络的样本中心, + # 设置trainable=False是因为样本中心不是由梯度进行更新的 + centers = tf.get_variable('centers', [num_classes, len_features], dtype=tf.float32, trainable=False, + initializer=tf.contrib.layers.xavier_initializer()) + # initializer=tf.constant_initializer(0)) + # 将label展开为一维的,输入如果已经是一维的,则该动作其实无必要 + # labels = tf.reshape(labels, [-1]) + + # 根据样本label,获取mini-batch中每一个样本对应的中心值 + centers_batch = tf.gather(centers, labels) + # 计算loss + loss = tf.losses.mean_squared_error(features, centers_batch) + + # 当前mini-batch的特征值与它们对应的中心值之间的差 + diff = centers_batch - features + + # 获取mini-batch中同一类别样本出现的次数,了解原理请参考原文公式(4) + unique_label, unique_idx, unique_count = tf.unique_with_counts(labels) + appear_times = tf.gather(unique_count, unique_idx) + appear_times = tf.reshape(appear_times, [-1, 1]) + + diff = diff / tf.cast((1 + appear_times), tf.float32) + diff = alpha * diff + + centers_update_op = tf.scatter_sub(centers, labels, diff) + tf.add_to_collection(tf.GraphKeys.UPDATE_OPS, centers_update_op) + return loss, centers, centers_update_op + + +# from facenet +def center_loss(features, label, alfa, nrof_classes): + """Center loss based on the paper "A Discriminative Feature Learning Approach for Deep Face Recognition" + (http://ydwen.github.io/papers/WenECCV16.pdf) + """ + nrof_features = features.get_shape()[1] + centers = tf.get_variable('centers', [nrof_classes, nrof_features], dtype=tf.float32, + initializer=tf.constant_initializer(0), trainable=False) + label = tf.reshape(label, [-1]) + centers_batch = tf.gather(centers, label) + diff = (1 - alfa) * (centers_batch - features) + centers = tf.scatter_sub(centers, label, diff) + with tf.control_dependencies([centers]): + loss = tf.reduce_mean(tf.square(features - centers_batch)) + return loss, centers + + +def AM_logits_compute(embeddings, label_batch, args, nrof_classes): + ''' + loss head proposed in paper: + link: https://arxiv.org/abs/1801.05599 + embeddings : normalized embedding layer of Facenet, it's normalized value of output of resface + label_batch : ground truth label of current training batch + args: arguments from cmd line + nrof_classes: number of classes + ''' + m = 0.35 + s = 30 + + with tf.name_scope('AM_logits'): + kernel = tf.get_variable(name='kernel', dtype=tf.float32, shape=[args.embedding_size, nrof_classes], + initializer=tf.contrib.layers.xavier_initializer(uniform=False)) + kernel_norm = tf.nn.l2_normalize(kernel, 0, 1e-10, name='kernel_norm') + cos_theta = tf.matmul(embeddings, kernel_norm) + cos_theta = tf.clip_by_value(cos_theta, -1, 1) # for numerical steady + phi = cos_theta - m + label_onehot = tf.one_hot(label_batch, nrof_classes) + adjust_theta = s * tf.where(tf.equal(label_onehot, 1), phi, cos_theta) + + return adjust_theta + + +if __name__ == '__main__': + b = tf.constant([[1, 1], [2, 2], [3, 3]], dtype=tf.float32) + with tf.Session() as sess: + print(sess.run(tf.nn.l2_loss(b))) diff --git a/loss/circle_loss.py b/loss/circle_loss.py new file mode 100644 index 0000000..a91edb6 --- /dev/null +++ b/loss/circle_loss.py @@ -0,0 +1,92 @@ +# coding=utf-8 +import tensorflow as tf +import numpy as np + + +def circle_loss(pair_wise_cosine_matrix, pred_true_mask, + pred_neg_mask, + margin=0.25, + gamma=64): + """ + @:param pair_wise_cosine_matrix 所有样本对的相似度矩阵 + @:param pred_true_mask 正样本对的mask矩阵 + @:param pred_neg_mask 负样本对的mask矩阵 + https://github.com/zhen8838/Circle-Loss/blob/master/circle_loss.py + """ + O_p = 1 + margin + O_n = -margin + + Delta_p = 1 - margin + Delta_n = margin + + ap = tf.nn.relu(-tf.stop_gradient(pair_wise_cosine_matrix * pred_true_mask) + 1 + margin) + an = tf.nn.relu(tf.stop_gradient(pair_wise_cosine_matrix * pred_neg_mask) + margin) + + logit_p = -ap * (pair_wise_cosine_matrix - Delta_p) * gamma * pred_true_mask + logit_n = an * (pair_wise_cosine_matrix - Delta_n) * gamma * pred_neg_mask + + logit_p = logit_p - (1 - pred_true_mask) * 1e12 + logit_n = logit_n - (1 - pred_neg_mask) * 1e12 + + joint_neg_loss = tf.reduce_logsumexp(logit_n, axis=-1) + joint_pos_loss = tf.reduce_logsumexp(logit_p, axis=-1) + logits = tf.nn.softplus(joint_neg_loss + joint_pos_loss) + return logits + + +def _get_anchor_positive_triplet_mask(labels): + """Return a 2D mask where mask[a, p] is True iff a and p are distinct and have same label. + Args: + labels: tf.int32 `Tensor` with shape [batch_size] + Returns: + mask: tf.bool `Tensor` with shape [batch_size, batch_size] + """ + # Check that i and j are distinct + indices_equal = tf.cast(tf.eye(tf.shape(labels)[0]), tf.bool) + indices_not_equal = tf.logical_not(indices_equal) + + # Check if labels[i] == labels[j] + # Uses broadcasting where the 1st argument has shape (1, batch_size) and the 2nd (batch_size, 1) + labels_equal = tf.equal(tf.expand_dims(labels, 0), tf.expand_dims(labels, 1)) + + # Combine the two masks + mask = tf.logical_and(indices_not_equal, labels_equal) + + return mask + + +def _get_anchor_negative_triplet_mask(labels): + """Return a 2D mask where mask[a, n] is True iff a and n have distinct labels. + Args: + labels: tf.int32 `Tensor` with shape [batch_size] + Returns: + mask: tf.bool `Tensor` with shape [batch_size, batch_size] + """ + # Check if labels[i] != labels[k] + # Uses broadcasting where the 1st argument has shape (1, batch_size) and the 2nd (batch_size, 1) + labels_equal = tf.equal(tf.expand_dims(labels, 0), tf.expand_dims(labels, 1)) + + mask = tf.logical_not(labels_equal) + + return mask + + +input_tensor = tf.convert_to_tensor(np.random.random((10, 16)).astype(np.float32)) +input_tensor = tf.nn.l2_normalize(input_tensor, axis=-1) +labels = tf.convert_to_tensor([1, 0, 2, 2, 1, 1, 4, 0, 4, 1]) + +# [10, 10] +pair_wise_cosine_matrix = tf.matmul(input_tensor, tf.transpose(input_tensor)) + +positive_mask = _get_anchor_positive_triplet_mask(labels) +negative_mask = _get_anchor_negative_triplet_mask(labels) + +positive_mask = tf.cast(positive_mask, tf.float32) +negative_mask = tf.cast(negative_mask, tf.float32) + +loss = circle_loss(pair_wise_cosine_matrix, positive_mask, + negative_mask, + margin=0.25, + gamma=64) +sess = tf.Session() +print(sess.run([positive_mask, negative_mask, loss])) diff --git a/loss/cross_entropy_with_prior.py b/loss/cross_entropy_with_prior.py new file mode 100644 index 0000000..b4f8005 --- /dev/null +++ b/loss/cross_entropy_with_prior.py @@ -0,0 +1,15 @@ +# coding: utf-8 +import tensorflow as tf + + +def sparse_softmax_cross_entropy_with_prior(labels, logits, priors, tau=1.0): + """带先验分布的稀疏交叉熵: Long-Tail Learning via Logit Adjustment. priors: shape is [num_classes], 类别的先验概率分布""" + log_priors = tf.math.log(priors) + if len(log_priors.shape.as_list()) == 1: + log_priors = tf.expand_dims(log_priors, 0) + # print(log_priors.shape) + # print(logits.shape) + # print(labels.shape) + logits += tau * log_priors + return tf.losses.sparse_softmax_cross_entropy(labels, logits) + diff --git a/loss/focal_loss.py b/loss/focal_loss.py new file mode 100644 index 0000000..f68093e --- /dev/null +++ b/loss/focal_loss.py @@ -0,0 +1,124 @@ +# coding: utf-8 +"""Implements Focal loss.""" +# ____ __ ___ __ __ __ __ ____ ____ +# ( __)/ \ / __) / _\ ( ) ( ) / \ / ___)/ ___) +# ) _)( O )( (__ / \/ (_/\ / (_/\( O )\___ \\___ \ +# (__) \__/ \___)\_/\_/\____/ \____/ \__/ (____/(____/ +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import tensorflow as tf + + +def sigmoid_focal_loss_with_logits(y_true, y_pred, alpha=0.25, gamma=2.0): + """ + Implements the focal loss function. + Focal loss was first introduced in the RetinaNet paper + (https://arxiv.org/pdf/1708.02002.pdf). Focal loss is extremely useful for + classification when you have highly imbalanced classes. It down-weights + well-classified examples and focuses on hard examples. The loss value is + much high for a sample which is misclassified by the classifier as compared + to the loss value corresponding to a well-classified example. One of the + best use-cases of focal loss is its usage in object detection where the + imbalance between the background class and other classes is extremely high. + Args + y_true: true targets tensor (labels). + y_pred: predictions tensor (logits). + alpha: balancing factor. + gamma: modulating factor. + Returns: + Weighted loss float `Tensor`. If `reduction` is `NONE`,this has the + same shape as `y_true`; otherwise, it is scalar. + Raises: + ValueError: If the shape of `sample_weight` is invalid or value of + `gamma` is less than zero + """ + if gamma and gamma < 0: + raise ValueError( + "Value of gamma should be greater than or equal to zero") + + y_pred = tf.convert_to_tensor(y_pred) + y_true = tf.cast(y_true, y_pred.dtype) + + # Get the binary cross_entropy + bce = tf.nn.sigmoid_cross_entropy_with_logits(labels=y_true, logits=y_pred) + + # convert the predictions into probabilities + y_pred = tf.nn.sigmoid(y_pred) + + p_t = (y_true * y_pred) + ((1 - y_true) * (1 - y_pred)) + alpha_factor = 1 + modulating_factor = 1 + + if alpha: + alpha = tf.convert_to_tensor(alpha, dtype=tf.float32) + alpha_factor = y_true * alpha + ((1 - alpha) * (1 - y_true)) + + if gamma: + gamma = tf.convert_to_tensor(gamma, dtype=tf.float32) + modulating_factor = tf.pow((1 - p_t), gamma) + + # compute the final loss and return + return tf.reduce_mean(alpha_factor * modulating_factor * bce, axis=-1, keepdims=True) + + +def softmax_focal_loss_with_logits(logits, labels, alpha=None, sample_weights=None, gamma=2.0, epsilon=1.e-7): + """ + Args: + logits: [batch_size, num_class] + labels: [batch_size] not one-hot !!! + alpha: [num_class] 一般为其他类的样本比例,样本越多的类对应的alpha越小 + Returns: + -alpha*(1-y)^r * log(y) + 它是在哪实现 1- y 的? 通过gather选择的就是1-p,而不是通过计算实现的; + logits softmax之后是多个类别的概率,也就是二分类时候的1-P和P;多分类的时候不是1-p了; + + 怎么把alpha的权重加上去? + 通过gather把alpha选择后变成batch长度,同时达到了选择和维度变换的目的 + """ + labels = tf.cast(labels, dtype=tf.int32) + softmax = tf.reshape(tf.nn.softmax(logits), [-1]) # [batch_size * num_class] + batch_size, num_class = get_shape_list(logits) + # (N,) > (N,), 但是数值变换了,变成了每个label在 N * num_class 中的位置 + labels_shift = tf.range(0, batch_size) * num_class + labels + # (N * num_class,) > (N,) + prob = tf.gather(softmax, labels_shift) # 属于当前类的概率 + # 预防预测概率值为0的情况; (N,) + prob = tf.clip_by_value(prob, epsilon, 1. - epsilon) + weights = tf.pow(tf.subtract(1., prob), gamma) + + if alpha is not None: + alpha = tf.constant(alpha, dtype=tf.float32) # (num_class, 1) + # (num_class ,1) > (N,) + alpha_choice = tf.gather(alpha, labels) + weights = tf.multiply(alpha_choice, weights) + + if sample_weights is not None: + weights = tf.multiply(weights, sample_weights) + return tf.losses.sparse_softmax_cross_entropy(labels, logits, weights=weights) + +def get_shape_list(tensor): + """Returns a list of the shape of tensor, preferring static dimensions. + Args: + tensor: A tf.Tensor object to find the shape of. + + Returns: + A list of dimensions of the shape of tensor. All static dimensions will + be returned as python integers, and dynamic dimensions will be returned + as tf.Tensor scalars. + """ + shape = tensor.shape.as_list() + + non_static_indexes = [] + for (index, dim) in enumerate(shape): + if dim is None: + non_static_indexes.append(index) + + if not non_static_indexes: + return shape + + dyn_shape = tf.shape(tensor) + for index in non_static_indexes: + shape[index] = dyn_shape[index] + return shape diff --git a/loss/hierarchical_triplet.py b/loss/hierarchical_triplet.py new file mode 100644 index 0000000..38d3205 --- /dev/null +++ b/loss/hierarchical_triplet.py @@ -0,0 +1,373 @@ +import tensorflow as tf +from tensorflow.python.framework import dtypes +from tensorflow.python.ops import array_ops +from tensorflow.python.ops import math_ops + + +def hierarchical_triplet_loss(features, labels, embeddings, min_pos_id=0, beta=0.5, squared=False): + """Build the triplet loss over a batch of embeddings. + We generate all the valid triplets and average the loss over the positive ones. + Args: + features: features of the batch + labels: labels of the batch, of size (batch_size,) + embeddings: tensor of shape (batch_size, embed_dim). Embeddings should be l2 normalized. + beta: margin for triplet loss + squared: Boolean. If true, output is the pairwise squared euclidean distance matrix. + If false, output is the pairwise euclidean distance matrix. + Returns: + triplet_loss: scalar tensor containing the triplet loss + """ + # Get the pairwise distance matrix + # pairwise_dist = _pairwise_distances(embeddings, squared=squared) + pairwise_dist = pairwise_distance(embeddings, squared) + + # shape (batch_size, batch_size, 1) + anchor_positive_dist = tf.expand_dims(pairwise_dist, 2) + assert anchor_positive_dist.shape[2] == 1, "{}".format(anchor_positive_dist.shape) + # shape (batch_size, 1, batch_size) + anchor_negative_dist = tf.expand_dims(pairwise_dist, 1) + assert anchor_negative_dist.shape[1] == 1, "{}".format(anchor_negative_dist.shape) + + margin = _get_dynamic_margin(features, beta) + print("margin", margin.shape) + mask = _get_triplet_mask(labels, min_pos_id) + mask = tf.to_float(mask) + triplet_loss, fraction_positive_triplets = batch_all_triplet_loss_v2( + anchor_positive_dist, anchor_negative_dist, mask, margin) + tf.summary.scalar("loss/fraction_positive_triplets", fraction_positive_triplets) + + # mask_lvl2 = _get_triplet_mask(features["parent_id"]) + # mask_lvl2 = tf.to_float(mask_lvl2) - mask # only include the instances of the same parent but has different label + # margin_lvl2 = _get_dynamic_margin_v2(features) + # triplet_loss2, fraction_positive_triplets2 = batch_all_triplet_loss_v2( + # anchor_positive_dist, anchor_negative_dist, mask_lvl2, margin_lvl2) + # tf.summary.scalar("loss/fraction_positive_triplets2", fraction_positive_triplets) + # return triplet_loss + triplet_loss2 + return triplet_loss + + +def batch_all_triplet_loss_v2(anchor_positive_dist, anchor_negative_dist, mask, margin): + # Compute a 3D tensor of size (batch_size, batch_size, batch_size) + # triplet_loss[i, j, k] will contain the triplet loss of anchor=i, positive=j, negative=k + # Uses broadcasting where the 1st argument has shape (batch_size, batch_size, 1) + # and the 2nd (batch_size, 1, batch_size) + triplet_loss = anchor_positive_dist - anchor_negative_dist + margin + + # Put to zero the invalid triplets + # (where label(a) != label(p) or label(n) == label(a) or a == p) + triplet_loss = tf.multiply(mask, triplet_loss) + + # Remove negative losses (i.e. the easy triplets) + triplet_loss = tf.maximum(triplet_loss, 0.0) + + # Count number of positive triplets (where triplet_loss > 0) + valid_triplets = tf.to_float(tf.greater(triplet_loss, 1e-8)) + num_positive_triplets = tf.reduce_sum(valid_triplets) + num_valid_triplets = tf.reduce_sum(mask) + fraction_positive_triplets = num_positive_triplets / (num_valid_triplets + 1e-16) + + # Get final mean triplet loss over the positive valid triplets + triplet_loss = tf.reduce_sum(triplet_loss) / (num_positive_triplets + 1e-16) + + return triplet_loss, fraction_positive_triplets + + +def pairwise_distance(feature, squared=False, normalized=True): + """from the source code of `tf.contrib.losses.metric_learning.triplet_semihard_loss` + Computes the pairwise distance matrix with numerical stability. + output[i, j] = || feature[i, :] - feature[j, :] ||_2 + Args: + feature: 2-D Tensor of size [number of data, feature dimension]. + squared: Boolean, whether or not to square the pairwise distances. + normalized: Boolean, whether or not input feature has be l2 normalized. + Returns: + pairwise_distances: 2-D Tensor of size [number of data, number of data]. + """ + if normalized: + pairwise_distances_squared = 2.0 * (1.0 - math_ops.matmul(feature, array_ops.transpose(feature))) + else: + pairwise_distances_squared = math_ops.add( + math_ops.reduce_sum(math_ops.square(feature), axis=[1], keepdims=True), + math_ops.reduce_sum(math_ops.square(array_ops.transpose(feature)), axis=[0], keepdims=True))\ + - 2.0 * math_ops.matmul(feature, array_ops.transpose(feature)) + + # Deal with numerical inaccuracies. Set small negatives to zero. + pairwise_distances_squared = math_ops.maximum(pairwise_distances_squared, 0.0) + + # Optionally take the sqrt. + if squared: + pairwise_distances = pairwise_distances_squared + else: + # Get the mask where the zero distances are at. + error_mask = math_ops.less_equal(pairwise_distances_squared, 0.0) + pairwise_distances = math_ops.sqrt( + pairwise_distances_squared + + math_ops.cast(error_mask, dtypes.float32) * 1e-16) + # Undo conditionally adding 1e-16. + pairwise_distances = math_ops.multiply( + pairwise_distances, + math_ops.cast(math_ops.logical_not(error_mask), dtypes.float32)) + + num_data = array_ops.shape(feature)[0] + # Explicitly set diagonals to zero. + mask_offdiagonals = array_ops.ones_like(pairwise_distances) - array_ops.diag( + array_ops.ones([num_data])) + pairwise_distances = math_ops.multiply(pairwise_distances, mask_offdiagonals) + return pairwise_distances + + +def _pairwise_distances(embeddings, squared=False): + """Compute the 2D matrix of distances between all the embeddings. + Args: + embeddings: tensor of shape (batch_size, embed_dim) + squared: Boolean. If true, output is the pairwise squared euclidean distance matrix. + If false, output is the pairwise euclidean distance matrix. + Returns: + pairwise_distances: tensor of shape (batch_size, batch_size) + """ + # Get the dot product between all embeddings + # shape (batch_size, batch_size) + # dot_product = tf.matmul(embeddings, tf.transpose(embeddings)) + dot_product = tf.matmul(embeddings, embeddings, transpose_b=True) + + # Get squared L2 norm for each embedding. We can just take the diagonal of `dot_product`. + # This also provides more numerical stability (the diagonal of the result will be exactly 0). + # shape (batch_size,) + square_norm = tf.diag_part(dot_product) + + # Compute the pairwise distance matrix as we have: + # ||a - b||^2 = ||a||^2 - 2 + ||b||^2 + # shape (batch_size, batch_size) + distances = tf.expand_dims(square_norm, 1) - 2.0 * dot_product + tf.expand_dims(square_norm, 0) + + # Because of computation errors, some distances might be negative so we put everything >= 0.0 + distances = tf.maximum(distances, 0.0) + + if not squared: + # Because the gradient of sqrt is infinite when distances == 0.0 (ex: on the diagonal) + # we need to add a small epsilon where distances == 0.0 + mask = tf.to_float(tf.equal(distances, 0.0)) + distances = distances + mask * 1e-16 + + distances = tf.sqrt(distances) + + # Correct the epsilon added: set the distances on the mask to be exactly 0.0 + distances = distances * (1.0 - mask) + + return distances + + +def _get_triplet_mask(labels, minPosId): + """Return a 3D mask where mask[a, p, n] is True iff the triplet (a, p, n) is valid. + A triplet (i, j, k) is valid if: + - i, j, k are distinct + - labels[i] == labels[j] and labels[i] != labels[k] + Args: + labels: tf.int32 `Tensor` with shape [batch_size] + """ + # Check that i, j and k are distinct + indices_equal = tf.cast(tf.eye(tf.shape(labels)[0]), tf.bool) + indices_not_equal = tf.logical_not(indices_equal) + i_not_equal_j = tf.expand_dims(indices_not_equal, 2) + i_not_equal_k = tf.expand_dims(indices_not_equal, 1) + j_not_equal_k = tf.expand_dims(indices_not_equal, 0) + + distinct_indices = tf.logical_and(tf.logical_and(i_not_equal_j, i_not_equal_k), j_not_equal_k) + + # Check if labels[i] == labels[j] and labels[i] != labels[k] + label_equal = tf.equal(tf.expand_dims(labels, 0), tf.expand_dims(labels, 1)) # (batch, batch) + positive = tf.greater_equal(labels, minPosId) + positive_matric = tf.logical_and(tf.expand_dims(positive, 0), tf.expand_dims(positive, 1)) + positive_equal = tf.logical_and(label_equal, positive_matric) + + # i_equal_j = tf.expand_dims(label_equal, 2) + i_equal_j = tf.expand_dims(positive_equal, 2) + i_equal_k = tf.expand_dims(label_equal, 1) + + valid_labels = tf.logical_and(i_equal_j, tf.logical_not(i_equal_k)) + + # Combine the two masks + mask = tf.logical_and(distinct_indices, valid_labels) + + return mask + + +# def _get_dynamic_margin_v2(features, beta1=0.3, beta2=0.49): +def _get_dynamic_margin_v2(features, beta1=0.1, beta2=0.2): + """ return beta + d(labels[j], labels[k]) of shape (1, batch_size, batch_size) """ + grand_parent = features["grand_parent"] + grand_parents_equal = tf.equal(tf.expand_dims(grand_parent, 0), tf.expand_dims(grand_parent, 1)) + ones = tf.ones_like(grand_parents_equal, dtype=tf.float32) + margin = tf.where(grand_parents_equal, ones * beta1, ones * beta2) + return tf.expand_dims(margin, 0) + + +def _get_dynamic_margin(features, beta=0.5, beta1=0.35, beta2=0.15): + """ return beta + d(labels[j], labels[k]) of shape (1, batch_size, batch_size) """ + parents = features["parent_id"] + grand_parent = features["grand_parent"] + parents_equal = tf.equal(tf.expand_dims(parents, 0), tf.expand_dims(parents, 1)) + grand_parents_equal = tf.equal(tf.expand_dims(grand_parent, 0), tf.expand_dims(grand_parent, 1)) + print("parents_equal", parents_equal.shape) + print("grand_parents_equal", grand_parents_equal.shape) + ones = tf.ones_like(parents_equal, dtype=tf.float32) + zeros = tf.zeros_like(parents_equal, dtype=tf.float32) + part_margin = tf.where(parents_equal, zeros, ones * beta1) + margin = tf.where(grand_parents_equal, part_margin, ones * beta2) + beta + return tf.expand_dims(margin, 0) + + +def batch_all_triplet_loss(labels, embeddings, margin, squared=False): + """Build the triplet loss over a batch of embeddings. + We generate all the valid triplets and average the loss over the positive ones. + Args: + labels: labels of the batch, of size (batch_size,) + embeddings: tensor of shape (batch_size, embed_dim) + margin: margin for triplet loss + squared: Boolean. If true, output is the pairwise squared euclidean distance matrix. + If false, output is the pairwise euclidean distance matrix. + Returns: + triplet_loss: scalar tensor containing the triplet loss + """ + # Get the pairwise distance matrix + pairwise_dist = _pairwise_distances(embeddings, squared=squared) + + # shape (batch_size, batch_size, 1) + anchor_positive_dist = tf.expand_dims(pairwise_dist, 2) + assert anchor_positive_dist.shape[2] == 1, "{}".format(anchor_positive_dist.shape) + # shape (batch_size, 1, batch_size) + anchor_negative_dist = tf.expand_dims(pairwise_dist, 1) + assert anchor_negative_dist.shape[1] == 1, "{}".format(anchor_negative_dist.shape) + + # Compute a 3D tensor of size (batch_size, batch_size, batch_size) + # triplet_loss[i, j, k] will contain the triplet loss of anchor=i, positive=j, negative=k + # Uses broadcasting where the 1st argument has shape (batch_size, batch_size, 1) + # and the 2nd (batch_size, 1, batch_size) + triplet_loss = anchor_positive_dist - anchor_negative_dist + margin + + # Put to zero the invalid triplets + # (where label(a) != label(p) or label(n) == label(a) or a == p) + mask = _get_triplet_mask(labels) + mask = tf.to_float(mask) + triplet_loss = tf.multiply(mask, triplet_loss) + + # Remove negative losses (i.e. the easy triplets) + triplet_loss = tf.maximum(triplet_loss, 0.0) + + # Count number of positive triplets (where triplet_loss > 0) + valid_triplets = tf.to_float(tf.greater(triplet_loss, 1e-8)) + num_positive_triplets = tf.reduce_sum(valid_triplets) + num_valid_triplets = tf.reduce_sum(mask) + fraction_positive_triplets = num_positive_triplets / (num_valid_triplets + 1e-16) + + # Get final mean triplet loss over the positive valid triplets + triplet_loss = tf.reduce_sum(triplet_loss) / (num_positive_triplets + 1e-16) + + return triplet_loss, fraction_positive_triplets + + +def _get_anchor_positive_triplet_mask(labels): + """Return a 2D mask where mask[a, p] is True iff a and p are distinct and have same label. + Args: + labels: tf.int32 `Tensor` with shape [batch_size] + Returns: + mask: tf.bool `Tensor` with shape [batch_size, batch_size] + """ + # Check that i and j are distinct + indices_equal = tf.cast(tf.eye(tf.shape(labels)[0]), tf.bool) + indices_not_equal = tf.logical_not(indices_equal) + + # Check if labels[i] == labels[j] + # Uses broadcasting where the 1st argument has shape (1, batch_size) and the 2nd (batch_size, 1) + labels_equal = tf.equal(tf.expand_dims(labels, 0), tf.expand_dims(labels, 1)) + + # Combine the two masks + mask = tf.logical_and(indices_not_equal, labels_equal) + + return mask + + +def _get_anchor_negative_triplet_mask(labels): + """Return a 2D mask where mask[a, n] is True iff a and n have distinct labels. + Args: + labels: tf.int32 `Tensor` with shape [batch_size] + Returns: + mask: tf.bool `Tensor` with shape [batch_size, batch_size] + """ + # Check if labels[i] != labels[k] + # Uses broadcasting where the 1st argument has shape (1, batch_size) and the 2nd (batch_size, 1) + labels_equal = tf.equal(tf.expand_dims(labels, 0), tf.expand_dims(labels, 1)) + + mask = tf.logical_not(labels_equal) + + return mask + + +def batch_hard_triplet_loss(labels, embeddings, margin, squared=False): + """Build the triplet loss over a batch of embeddings. + For each anchor, we get the hardest positive and hardest negative to form a triplet. + Args: + labels: labels of the batch, of size (batch_size,) + embeddings: tensor of shape (batch_size, embed_dim) + margin: margin for triplet loss + squared: Boolean. If true, output is the pairwise squared euclidean distance matrix. + If false, output is the pairwise euclidean distance matrix. + Returns: + triplet_loss: scalar tensor containing the triplet loss + """ + # Get the pairwise distance matrix + pairwise_dist = _pairwise_distances(embeddings, squared=squared) + + # For each anchor, get the hardest positive + # First, we need to get a mask for every valid positive (they should have same label) + mask_anchor_positive = _get_anchor_positive_triplet_mask(labels) + mask_anchor_positive = tf.to_float(mask_anchor_positive) + + # We put to 0 any element where (a, p) is not valid (valid if a != p and label(a) == label(p)) + anchor_positive_dist = tf.multiply(mask_anchor_positive, pairwise_dist) + + # shape (batch_size, 1) + hardest_positive_dist = tf.reduce_max(anchor_positive_dist, axis=1, keepdims=True) + tf.summary.scalar("hardest_positive_dist", tf.reduce_mean(hardest_positive_dist)) + + # For each anchor, get the hardest negative + # First, we need to get a mask for every valid negative (they should have different labels) + mask_anchor_negative = _get_anchor_negative_triplet_mask(labels) + mask_anchor_negative = tf.to_float(mask_anchor_negative) + + # We add the maximum value in each row to the invalid negatives (label(a) == label(n)) + max_anchor_negative_dist = tf.reduce_max(pairwise_dist, axis=1, keepdims=True) + anchor_negative_dist = pairwise_dist + max_anchor_negative_dist * (1.0 - mask_anchor_negative) + + # shape (batch_size,) + hardest_negative_dist = tf.reduce_min(anchor_negative_dist, axis=1, keepdims=True) + tf.summary.scalar("hardest_negative_dist", tf.reduce_mean(hardest_negative_dist)) + + # Combine biggest d(a, p) and smallest d(a, n) into final triplet loss + triplet_loss = tf.maximum(hardest_positive_dist - hardest_negative_dist + margin, 0.0) + + # Get final mean triplet loss + triplet_loss = tf.reduce_mean(triplet_loss) + + return triplet_loss + + +if __name__ == '__main__': + a = tf.constant([[0.1, 0.3, 0.5], + [0.01, 0.34, 0.64], + [0.02, 0.34, 0.64], + [0.4, 0.44, 0.1]]) + norm_a = tf.nn.l2_normalize(a, axis=1) + # distance0 = pairwise_distance(norm_a, False, False) + distance1 = pairwise_distance(norm_a) + # distance2 = pairwise_distance(a, False, False) + distance3 = _pairwise_distances(a) + distance4 = _pairwise_distances(norm_a) + with tf.Session() as sess: + # print(sess.run(norm_a)) + # print(sess.run(tf.reduce_sum(tf.square(norm_a), axis=1))) + # print(sess.run(distance0)) + print(sess.run(distance1)) + # print(sess.run(distance2)) + print(sess.run(distance3)) + print(sess.run(distance4)) diff --git a/loss/ms_loss.py b/loss/ms_loss.py new file mode 100644 index 0000000..e3cdd5b --- /dev/null +++ b/loss/ms_loss.py @@ -0,0 +1,166 @@ +import tensorflow as tf +import modeling + + +def ms_loss(labels, embeddings, alpha=2.0, beta=50.0, lamb=1.0, eps=0.1, ms_mining=False, embed_normed=True): + """ + ref: http://openaccess.thecvf.com/content_CVPR_2019/papers/Wang_Multi-Similarity_Loss_With_General_Pair_Weighting_for_Deep_Metric_Learning_CVPR_2019_paper.pdf + official codes: https://github.com/MalongTech/research-ms-loss + """ + # make sure embedding should be l2-normalized + if not embed_normed: + embeddings = tf.nn.l2_normalize(embeddings, axis=1) + labels = tf.reshape(labels, [-1, 1]) + + embed_shape = modeling.get_shape_list(embeddings) + batch_size = embed_shape[0] + + adjacency = tf.equal(labels, tf.transpose(labels)) + adjacency_not = tf.logical_not(adjacency) + + mask_pos = tf.cast(adjacency, dtype=tf.float32) - tf.eye(batch_size, dtype=tf.float32) + mask_neg = tf.cast(adjacency_not, dtype=tf.float32) + + sim_mat = tf.matmul(embeddings, embeddings, transpose_a=False, transpose_b=True) + sim_mat = tf.maximum(sim_mat, 0.0) + + pos_mat = tf.multiply(sim_mat, mask_pos) + neg_mat = tf.multiply(sim_mat, mask_neg) + + if ms_mining: + max_val = tf.reduce_max(neg_mat, axis=1, keepdims=True) + tmp_max_val = tf.reduce_max(pos_mat, axis=1, keepdims=True) + min_val = tf.reduce_min(tf.multiply(sim_mat - tmp_max_val, mask_pos), axis=1, keepdims=True) + tmp_max_val + + max_val = tf.tile(max_val, [1, batch_size]) + min_val = tf.tile(min_val, [1, batch_size]) + + mask_pos = tf.where(pos_mat < max_val + eps, mask_pos, tf.zeros_like(mask_pos)) + mask_neg = tf.where(neg_mat > min_val - eps, mask_neg, tf.zeros_like(mask_neg)) + + pos_exp = tf.exp(-alpha * (pos_mat - lamb)) + pos_exp = tf.where(mask_pos > 0.0, pos_exp, tf.zeros_like(pos_exp)) + + neg_exp = tf.exp(beta * (neg_mat - lamb)) + neg_exp = tf.where(mask_neg > 0.0, neg_exp, tf.zeros_like(neg_exp)) + + pos_term = tf.log(1.0 + tf.reduce_sum(pos_exp, axis=1)) / alpha + neg_term = tf.log(1.0 + tf.reduce_sum(neg_exp, axis=1)) / beta + + loss = tf.reduce_mean(pos_term + neg_term) + return loss + + +def recall_at_k(labels, embeddings, k, embed_normed=True): + # make sure embedding should be l2-normalized + if not embed_normed: + embeddings = tf.nn.l2_normalize(embeddings, axis=1) + # batch_size = tf.size(labels) + embed_shape = modeling.get_shape_list(embeddings) + batch_size = embed_shape[0] + + sim_mat = tf.matmul(embeddings, embeddings, transpose_b=True) + sim_mat = sim_mat - tf.eye(batch_size) * 2.0 + + labels = tf.expand_dims(labels, -1) + mask = tf.equal(labels, tf.transpose(labels)) # shape: (batch_size, batch_size) + eye = tf.eye(batch_size, dtype=tf.bool) + mask = tf.logical_and(mask, tf.logical_not(eye)) + mask_pos = tf.where(mask, sim_mat, -tf.ones_like(sim_mat)) # shape: (batch_size, batch_size) + + if isinstance(k, int): + _, pos_top_k_idx = tf.nn.top_k(mask_pos, k) # shape: (batch_size, k) + return tf.metrics.recall_at_k(labels=tf.to_int64(pos_top_k_idx), predictions=sim_mat, k=k) + if any((isinstance(k, list), isinstance(k, tuple), isinstance(k, set))): + metrics = {} + for kk in k: + if k < 1: + continue + _, pos_top_k_idx = tf.nn.top_k(mask_pos, kk) + metrics["recall@"+str(kk)] = tf.metrics.recall_at_k(labels=tf.to_int64(pos_top_k_idx), predictions=sim_mat, k=kk) + return metrics + raise ValueError("k should be a `int` or a list/tuple/set of int.") + + +def get_matrix_mask_indices(matrix, num_rows=None): + if num_rows is None: + num_rows = modeling.get_shape_list(matrix)[0] + indices = tf.where(matrix) + num_indices = tf.shape(indices)[0] + elem_per_row = tf.bincount(tf.cast(indices[:, 0], tf.int32), minlength=num_rows) + max_elem_per_row = tf.reduce_max(elem_per_row) + row_start = tf.concat([[0], tf.cumsum(elem_per_row[:-1])], axis=0) + r = tf.range(max_elem_per_row) + idx = tf.expand_dims(row_start, 1) + r + idx = tf.minimum(idx, num_indices - 1) + result = tf.gather(indices[:, 1], idx) + # replace invalid elements with -1 + result = tf.where(tf.expand_dims(elem_per_row, 1) > r, result, -tf.ones_like(result)) + max_index_per_row = tf.reduce_max(result, axis=1, keepdims=True) + max_index_per_row = tf.tile(max_index_per_row, [1, max_elem_per_row]) + result = tf.where(result >= 0, result, max_index_per_row) + return result + + +def average_precision_at_k(labels, embeddings, k, embed_normed=True): + # make sure embedding should be l2-normalized + if not embed_normed: + embeddings = tf.nn.l2_normalize(embeddings, axis=1) + # batch_size = tf.size(labels) + embed_shape = modeling.get_shape_list(embeddings) + batch_size = embed_shape[0] + + sim_mat = tf.matmul(embeddings, embeddings, transpose_b=True) + sim_mat = sim_mat - tf.eye(batch_size) * 2.0 + + labels = tf.expand_dims(labels, -1) + mask = tf.equal(labels, tf.transpose(labels)) # shape: (batch_size, batch_size) + label_indices = get_matrix_mask_indices(mask) + if isinstance(k, int): + return tf.metrics.average_precision_at_k(label_indices, sim_mat, k) + if any((isinstance(k, list), isinstance(k, tuple), isinstance(k, set))): + metrics = {} + for kk in k: + if k < 1: + continue + metrics["MAP@"+str(kk)] = tf.metrics.average_precision_at_k(label_indices, sim_mat, kk) + return metrics + raise ValueError("k should be a `int` or a list/tuple/set of int.") + + +def knn(labels, embeddings, k, embed_normed=True): + # make sure embedding should be l2-normalized + if not embed_normed: + embeddings = tf.nn.l2_normalize(embeddings, axis=1) + + embed_shape = modeling.get_shape_list(embeddings) + batch_size = embed_shape[0] + sim_mat = tf.matmul(embeddings, embeddings, transpose_b=True) + sim_mat = sim_mat - tf.eye(batch_size) * 2.0 + + _, top_k_idx = tf.nn.top_k(sim_mat, k) + top_k_labels = tf.squeeze(tf.gather(labels, top_k_idx)) + + def knn_vote(v): + nearest_k_y, idx, votes = tf.unique_with_counts(v) + majority_idx = tf.argmax(votes) + predict_res = tf.gather(nearest_k_y, majority_idx) + return predict_res + + majority = tf.map_fn(knn_vote, top_k_labels) + return majority + + +def knn_metrics(labels, embeddings, k, embed_normed=True): + knn_result = knn(labels, embeddings, k, embed_normed) + accuracy = tf.metrics.accuracy(labels, knn_result) + + is_black = tf.where(labels < -100000, tf.ones_like(labels), tf.zeros_like(labels)) + predictions = tf.where(tf.equal(labels, knn_result), is_black, 1 - is_black) + precision = tf.metrics.precision(is_black, predictions) + recall = tf.metrics.recall(is_black, predictions) + return { + "knn_accuracy@" + str(k): accuracy, + "knn_precision@" + str(k): precision, + "knn_recall@" + str(k): recall + } diff --git a/loss/soft_triple.py b/loss/soft_triple.py new file mode 100644 index 0000000..70dedec --- /dev/null +++ b/loss/soft_triple.py @@ -0,0 +1,136 @@ +import tensorflow as tf + + +def soft_triple_loss(labels, embeddings, num_classes, num_centers=2, lamb=20.0, gamma=0.1, delta=0.01, tau=0.2): + """ + paper: SoftTriple Loss: Deep Metric Learning Without Triplet Sampling + ref: https://medium.com/@sebastianpinedaarango/implementation-of-softtriple-loss-dfff803bab7f + """ + embedding_size = embeddings.shape[-1] + centers = tf.get_variable("soft_triple_centers", shape=[num_classes * num_centers, embedding_size], + dtype=tf.float32, initializer=tf.contrib.layers.xavier_initializer(uniform=True)) + norm_centers = tf.math.l2_normalize(centers, axis=-1) + norm_embeddings = tf.math.l2_normalize(embeddings, axis=-1) + inner_logits = tf.matmul(norm_embeddings, norm_centers, transpose_b=True) + inner_logits = tf.reshape(inner_logits, [-1, num_centers, num_classes]) + inner_softmax = tf.math.softmax(inner_logits / gamma, axis=1) + sim_i_c = tf.reduce_sum(tf.multiply(inner_softmax, inner_logits), axis=1) + + one_hot_label = tf.one_hot(tf.cast(labels, tf.int32), num_classes, dtype=tf.float32) + logits = lamb * (sim_i_c - delta * one_hot_label) + + loss = tf.losses.softmax_cross_entropy(one_hot_label, logits) + + if tau > 0.0 and num_centers > 1: # do regularize, make adaptive number of centers + sim_centers = tf.matmul(norm_centers, norm_centers, transpose_b=True) + # Because of computation errors, some distances might be negative so we put everything >= 0.0 + dist_centers = tf.maximum(2.0 - 2.0 * sim_centers, 0.0) + + # Because the gradient of sqrt is infinite when distances == 0.0 (ex: on the diagonal) + # we need to add a small epsilon where distances == 0.0 + mask = tf.to_float(tf.equal(dist_centers, 0.0)) + dist_centers = dist_centers + mask * 1e-16 + dist_centers = tf.sqrt(dist_centers) + + # Correct the epsilon added: set the distances on the mask to be exactly 0.0 + dist_centers = dist_centers * (1.0 - mask) # shape: (C*K, C*K) + + checkerboard = tf.range(num_classes, dtype=tf.int32) + checkerboard = tf.one_hot(checkerboard, depth=num_classes, dtype=tf.float32) + checkerboard = tf.keras.backend.repeat_elements(checkerboard, num_centers, axis=0) + checkerboard = tf.keras.backend.repeat_elements(checkerboard, num_centers, axis=1) # shape: (C*K, C*K) + + dist_centers = tf.multiply(dist_centers, checkerboard) + mask = tf.ones_like(dist_centers, dtype=tf.float32) - tf.eye(num_classes * num_centers, dtype=tf.float32) + dist_centers = tf.multiply(dist_centers, mask) + reg_numerator = tau * tf.reduce_sum(dist_centers) / 2.0 + reg_denominator = num_classes * num_centers * (num_centers - 1.0) + loss_reg = reg_numerator / reg_denominator + loss += loss_reg + return loss, logits + + +def soft_triple(gt, embeddings, dim_features, num_class, num_centers=2, p_lambda=20.0, p_tau=0.2, p_gamma=0.1, + p_delta=0.01, with_reg=True): + """ + paper: SoftTriple Loss: Deep Metric Learning Without Triplet Sampling + code from: https://github.com/geonm/tf_SoftTriple_loss + """ + large_centers = tf.get_variable(name='feature_extractor/large_centers', + shape=[num_class * num_centers, dim_features], + dtype=tf.float32, + initializer=tf.contrib.layers.xavier_initializer(uniform=True), + trainable=True) + large_centers = tf.nn.l2_normalize(large_centers, axis=-1) + embeddings = tf.nn.l2_normalize(embeddings, axis=-1) + + large_logits = tf.matmul(embeddings, large_centers, transpose_b=True) # [batch_size, num_class * num_centers] + + batch_size = tf.shape(large_logits)[0] + + rs_large_logits = tf.reshape(large_logits, [batch_size, num_centers, num_class]) + + exp_rs_large_logits = tf.exp((1.0 / p_gamma) * rs_large_logits) + + sum_rs_large_logits = tf.reduce_sum(exp_rs_large_logits, axis=1, keepdims=True) + + coeff_large_logits = exp_rs_large_logits / sum_rs_large_logits + + rs_large_logits = tf.multiply(rs_large_logits, coeff_large_logits) + + logits = tf.reduce_sum(rs_large_logits, axis=1, keepdims=False) + + # get labels_map + gt = tf.reshape(gt, [-1]) # e.g., [0, 7, 3, 22, 39, ...] + + gt_int = tf.cast(gt, tf.int32) + + labels_map = tf.one_hot(gt_int, depth=num_class, dtype=tf.float32) + + # subtract p_delta + delta_map = p_delta * labels_map + + logits_delta = logits - delta_map + scaled_logits_delta = p_lambda * (logits_delta) + + # get xentropy loss + loss_xentropy = tf.nn.softmax_cross_entropy_with_logits(logits=scaled_logits_delta, labels=labels_map) + loss_xentropy = tf.reduce_mean(loss_xentropy, name='loss_xentropy') + + # get regularizer terms + loss_reg = 0.0 + if with_reg: + # get R function + # large_centers = [num_class * num_centers, dim_features] + sim_large_centers = tf.abs(tf.matmul(large_centers, large_centers, + transpose_b=True)) # [num_class * num_centers, num_class * num_centers] + + # check error + # sim_large_centers = tf.where(sim_large_centers > 1.0, tf.ones_like(sim_large_centers, dtype=tf.float32), sim_large_centers) + + dist_large_centers = tf.sqrt(tf.abs(2.0 - 2.0 * sim_large_centers) + 1e-10) + checkerboard = tf.range(num_class, dtype=tf.int32) + checkerboard = tf.one_hot(checkerboard, depth=num_class, dtype=tf.float32) + checkerboard = tf.keras.backend.repeat_elements(checkerboard, num_centers, axis=0) + checkerboard = tf.keras.backend.repeat_elements(checkerboard, num_centers, axis=1) + + dist_large_centers = tf.multiply(dist_large_centers, checkerboard) + + mask = tf.ones_like(dist_large_centers, dtype=tf.float32) - tf.eye(num_class * num_centers, dtype=tf.float32) + + dist_large_centers = p_tau * tf.multiply(dist_large_centers, mask) + + reg_numer = tf.reduce_sum(dist_large_centers) / 2.0 + + reg_denumer = num_class * num_centers * (num_centers - 1.0) + + loss_reg = reg_numer / reg_denumer + + # l2 reg loss + # reg_embeddings = tf.reduce_mean(tf.reduce_sum(tf.square(embeddings), 1)) + # reg_centers = tf.reduce_mean(tf.reduce_sum(tf.square(large_centers), 1)) + # loss_l2_reg = tf.multiply(0.25 * 0.002, reg_embeddings + reg_centers, name='loss_l2_reg') + + total_loss = loss_xentropy + loss_reg + + return total_loss diff --git a/loss/triplet_center.py b/loss/triplet_center.py new file mode 100644 index 0000000..45f2e55 --- /dev/null +++ b/loss/triplet_center.py @@ -0,0 +1,69 @@ +# coding: utf-8 +import tensorflow as tf + + +def triplet_center_loss(features, labels, num_classes, margin, alpha=0.5): + """获取triplet center loss及center的更新op + paper: Triplet-Center Loss for Multi-View 3D Object Retrieval + Arguments: + features: Tensor,表征样本特征,一般使用某个fc层的输出,shape应该为[batch_size, feature_length]. + features should be l2 normalized. + labels: Tensor,表征样本label,非one-hot编码,shape应为[batch_size]. + num_classes: 整数,表明总共有多少个类别,网络分类输出有多少个神经元这里就取多少. + margin: the margin of triplet loss + alpha: 0-1之间的数字,控制样本类别中心的学习率,细节参考原文. + + Return: + loss: Tensor,可与softmax loss相加作为总的loss进行优化. + centers_update_op: op, 用于更新样本中心的op,在训练时需要同时运行该op,否则样本中心不会更新 + for example: + ``` + with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)): + train_op = ... + ``` + """ + # 获取特征的维数,例如256维 + len_features = features.get_shape()[1] + # 建立一个Variable,shape为[num_classes, len_features],用于存储整个网络的样本中心, + # 设置trainable=False是因为样本中心不是由梯度进行更新的 + centers = tf.get_variable('centers', [num_classes, len_features], dtype=tf.float32, trainable=False, + initializer=tf.contrib.layers.xavier_initializer()) + + # ||a - b||^2 = ||a||^2 - 2 + ||b||^2 + square_a = tf.reduce_sum(tf.square(features), axis=1, keepdims=True) + square_b = tf.reduce_sum(tf.square(tf.transpose(centers)), axis=0, keepdims=True) + # shape (batch_size, num_classes) + distances = square_a + square_b - 2.0 * tf.matmul(features, centers, transpose_b=True) + + # Because of computation errors, some distances might be negative so we put everything >= 0.0 + distances = tf.maximum(0.5 * distances, 0.0) + + labels = tf.cast(labels, tf.int32) + anchor_positive_dist = tf.batch_gather(distances, tf.expand_dims(labels, -1)) # (batch_size, 1) + + max_distances = tf.reduce_max(distances, axis=1, keepdims=True) + mask = tf.one_hot(labels, num_classes) * max_distances + anchor_negative_dist = tf.reduce_min(distances + mask, axis=1, keepdims=True) + + # 计算loss + triplet_loss = tf.maximum(anchor_positive_dist - anchor_negative_dist + margin, 0.0) + + # Get final mean triplet loss + triplet_loss = tf.reduce_mean(triplet_loss) + + # 根据样本label,获取mini-batch中每一个样本对应的中心值 + centers_batch = tf.gather(centers, labels) + # 当前mini-batch的特征值与它们对应的中心值之间的差 + diff = centers_batch - features + + # 获取mini-batch中同一类别样本出现的次数,了解原理请参考原文公式(4) + unique_label, unique_idx, unique_count = tf.unique_with_counts(labels) + appear_times = tf.gather(unique_count, unique_idx) + appear_times = tf.reshape(appear_times, [-1, 1]) + + diff = diff / tf.cast((1 + appear_times), tf.float32) + diff = alpha * diff + + centers_update_op = tf.scatter_sub(centers, labels, diff) + tf.add_to_collection(tf.GraphKeys.UPDATE_OPS, centers_update_op) + return triplet_loss, centers_update_op diff --git a/rbo.py b/rbo.py new file mode 100644 index 0000000..9eff9d6 --- /dev/null +++ b/rbo.py @@ -0,0 +1,97 @@ +from __future__ import print_function +import math + + +def score(l1, l2, p=0.98): + """ + Calculates Ranked Biased Overlap (RBO) score. + l1 -- Ranked List 1 + l2 -- Ranked List 2 + """ + if l1 == None: l1 = [] + if l2 == None: l2 = [] + + sl, ll = sorted([(len(l1), l1), (len(l2), l2)]) + s, S = sl + l, L = ll + if s == 0: return 0 + + # Calculate the overlaps at ranks 1 through l + # (the longer of the two lists) + ss = set([]) # contains elements from the smaller list till depth i + ls = set([]) # contains elements from the longer list till depth i + x_d = {0: 0} + sum1 = 0.0 + for i in range(l): + x = L[i] + y = S[i] if i < s else None + d = i + 1 + + # if two elements are same then + # we don't need to add to either of the set + if x == y: + x_d[d] = x_d[d - 1] + 1.0 + # else add items to respective list + # and calculate overlap + else: + ls.add(x) + if y != None: ss.add(y) + x_d[d] = x_d[d - 1] + (1.0 if x in ss else 0.0) + (1.0 if y in ls else 0.0) + # calculate average overlap + sum1 += x_d[d] / d * pow(p, d) + + sum2 = 0.0 + for i in range(l - s): + d = s + i + 1 + sum2 += x_d[d] * (d - s) / (d * s) * pow(p, d) + + sum3 = ((x_d[l] - x_d[s]) / l + x_d[s] / s) * pow(p, l) + + # Equation 32 + rbo_ext = (1 - p) / p * (sum1 + sum2) + sum3 + return rbo_ext + + +def rbo_score(l1, l2, p): + if not l1 or not l2: + return 0 + s1 = set() + s2 = set() + max_depth = len(l1) + score = 0.0 + for d in range(max_depth): + s1.add(l1[d]) + s2.add(l2[d]) + avg_overlap = len(s1 & s2) / (d + 1) + score += math.pow(p, d) * avg_overlap + return (1 - p) * score + + +if __name__ == "__main__": + list1 = ['0', '1', '2', '3', '4', '5'] + list2 = ['1', '0', '2', '3', '4', '5'] + list3 = ['0', '1', '2', '3', '5', '4'] + list4 = ['2', '1', '0', '3', '5', '4'] + print(rbo_score(list1, list2, 0.01)) + print(rbo_score(list1, list3, 0.01)) + print(rbo_score(list1, list2, 0.5)) + print(rbo_score(list1, list3, 0.5)) + print(rbo_score(list1, list4, 0.5)) + print(rbo_score(list1, list2, 0.9)) + print(rbo_score(list1, list3, 0.9)) + print("-----------------------------------") + print(rbo_score(list1, list1, 0)) + print(rbo_score(list1, list1, 0.5)) + print(rbo_score(list1, list1, 0.9)) + print("-----------------------------------") + + list1 = ['0', '1', '3', '4', '5'] + list2 = ['1', '0', '3', '4', '5'] + list3 = ['0', '1', '3', '5', '4'] + print(rbo_score(list1, list2, 0.5)) + print(rbo_score(list1, list3, 0.5)) + # print score(list1, list2, p = 0.90) + + # list1 = ['012'] + # list2 = [] + # print rbo_score(list1, list2, p = 0.98)