一、 Triplet loss
1、介绍
Triplet loss
最初是在 FaceNet: A Unified Embedding for Face Recognition and Clustering 论文中提出的,可以学到较好的人脸的embedding
- 为什么不适用
softmax
函数呢,softmax
最终的类别数是确定的,而Triplet loss
学到的是一个好的embedding
,相似的图像在embedding
空间里是相近的,可以判断是否是同一个人脸。2、原理
输入是一个三元组
<a, p, n>
a: anchor
p: positive
, 与a
是同一类别的样本n: negative
, 与a
是不同类别的样本
- 公式是: $$L = max(d(a, p) - d(a, n) + margin, 0)$$
- 所以最终的优化目标是拉近
a, p
的距离, 拉远a, n
的距离 easy triplets
: $L = 0$ 即 $d(a, p) +margin < d(a, n)$,这种情况不需要优化,天然a, p
的距离很近,a, n
的距离远hard triplets
: $d(a, n) < d(a, p)$, 即a, p
的距离远semi-hard triplets
: $d(a, p) < d(a, n) < d(a, p) + margin$, 即a, n
的距离靠的很近,但是有一个margin
- 所以最终的优化目标是拉近
FaceNet
中是随机选取semi-hard triplets
进行训练的, (也可以选择hard triplets
或者两者一起进行训练)
3、训练方法
3.1 offline
- 训练集所有数据经过计算得到对应的
embeddings
, 可以得到 很多<i, j, k>
的三元组,然后再计算triplet loss
- 效率不高,因为需要过一遍所有的数据得到三元组,然后训练反向更新网络
3.2
online
- 从训练集中抽取
B
个样本,然后计算B
个embeddings
,可以产生 $B^3$ 个triplets
(当然其中有不合法的,因为需要的是<a, p, n>
)
- 实际使用中采用此方法,又分为两种策略 (是在一篇行人重识别的论文中提到的 In Defense of the Triplet Loss for Person Re-Identification),假设 $B = PK$, 其中
P
个身份的人,每个身份的人K
张图片(一般K
取4
)Batch All
: 计算batch_size
中所有valid
的的hard triplet
和semi-hard triplet
, 然后取平均得到Loss
- 注意因为很多
easy triplets
的情况,所以平均会导致Loss
很小,所以是对所有 valid 的所有求平均 (下面代码中会介绍) - 可以产生 $PK(K-1)(PK-K)$个
triplets
PK
个anchor
K-1
个positive
PK-K
个negative
- 注意因为很多
Batch Hard
: 对于每一个anchor
, 选择距离最大的d(a, p)
和 距离最大的d(a, n)
- 所以公有 $PK$ 个 三元组
triplets
- 所以公有 $PK$ 个 三元组
二、 Tensorflow 中的实现
- 全部代码
Tensorflow
中有实现好的triplet loss
接口,这里自己实现,(实现起来还是有点绕的, 有一些小细节问题)- 使用
numpy
也仿照实现了,便于调试查看中间的结果, 全部代码1、Batch All
1.1 计算两两
embeddings
的距离 numpy
中的实现,便于调试理解, 点击查看- 输入大小是
(batch_size, vector_size)
大小的embeddings
向量 - 因为 $(a-b)^2 = a^2 -2ab + b^2$, 矩阵相乘 $embeddings \times embeddings^T$ 中包含
a*b
的值,对象线上是向量平方的值,所以可以直接使用矩阵计算 - 如果不使用平方,就开根号,
- 注意根号下不能为
0
,0
开根号是没有问题的,但是Tensorflow
梯度反向传播是就会导致无穷大,所以加上一个平滑项1e-16
,最后再修改回来。
- 注意根号下不能为
|
|
1.2 计算valid mask
numpy
中的实现, 点击查看- 上面得到了
(batch_size, batch_size)
大小的距离矩阵,然后就可以计算所有embeddings
组成的三元组<i, j, k>
损失 - 但是不是所有的三元组都是
valid
的, 要是<a, p, n>
的形式,所以计算一个3D
的mask
,然后乘上得到的(batch_size, batch_size, batch_size)
的所有三元组的损失即可,如何得到mask
呢 <i, j, k>
要满足i, j, k
不相等labels[i] == labels[j] and labels[i] != labels[k]
|
|
1.3 计算triplet loss
numpy
中的实现, 点击查看1.1
中计算得到了两两embeddings
的距离,大小(batch_size, batch_size)
, 需要得到所有三元组的triplet loss
, 即(batch_size, batch_size, batch_size)
大小- 为什么
triplet_loss = anchor_positive_dist - anchor_negative_dist + margin
可以得到所有(i, j, k)
的triplet loss
,- 如下图,
x0y
平面的是anchor_positive_dist
的距离矩阵(其实是3D
的, 想象一下) x0z
平面是anchor_negative_dist
的距离矩阵(也是3D
的)- 两个相减, 比如
0-0 = 0
就相当于i=0, j=0
的距离,减去j=0, k=0
的距离 - 以此类推,得到所有三元组的
loss
- 如下图,
|
|
2、Batch Hard
numpy
中的实现,点击查看- 因为最后只有$PK$个
triplet
, 从positive
中选择距离最大的,从negative
中选择距离最小的即可2.1 计算positive mask
- 满足
a!=p and a, p label一致即可
- 之后用
mask
乘上计算的pairwice_distances
, 然后取每行最大值即为每个样本对应positive
的最大距离123456789101112131415def _get_anchor_positive_triplet_mask(labels):'''得到合法的positive的mask, 即2D的矩阵,[a, p], a!=p and a和p相同labels------------------------------------------------Args:labels: 标签数据,shape = (batch_size, )Returns:mask: 合法的positive mask, shape = (batch_size, batch_size)'''indices_equal = tf.cast(tf.eye(tf.shape(labels)[0]), tf.bool)indices_not_equal = tf.logical_not(indices_equal) # (i, j)不相等labels_equal = tf.equal(tf.expand_dims(labels, 0), tf.expand_dims(labels, 1)) # labels相等,mask = tf.logical_and(indices_not_equal, labels_equal) # 取and即可return mask
2.2 计算negative mask
- 只需
[a, n]
对应的labels
不一致即可12345678910111213def _get_anchor_negative_triplet_mask(labels):'''得到negative的2D mask, [a, n] 只需a, n不同且有不同的labels------------------------------------------------Args:labels: 标签数据,shape = (batch_size, )Returns:mask: negative mask, shape = (batch_size, batch_size)'''labels_equal = tf.equal(tf.expand_dims(labels, 0), tf.expand_dims(labels, 1))mask = tf.logical_not(labels_equal)return mask
2.3 batch hard loss
- 计算最大
positive
距离时直接取valid
的每一行的最大值即可 - 计算最小
negative
距离时不能直接取每一行的最小值,因为invalid
位置的值为0
,所以可以在invalid
位置加上每一行的最大值,然后就可以取每一行的最小值了123456789101112131415161718192021222324252627282930def batch_hard_triplet_loss(labels, embeddings, margin, squared=False):'''batch hard triplet loss of a batch, 每个样本最大的positive距离 - 对应样本最小的negative距离------------------------------------Args:labels: 标签数据,shape = (batch_size,)embeddings: 提取的特征向量, shape = (batch_size, vector_size)margin: margin大小, scalarReturns:triplet_loss: scalar, 一个batch的损失值'''pairwise_distances = _pairwise_distance(embeddings)mask_anchor_positive = _get_anchor_positive_triplet_mask(labels)mask_anchor_positive = tf.to_float(mask_anchor_positive)anchor_positive_dist = tf.multiply(mask_anchor_positive, pairwise_distances)hardest_positive_dist = tf.reduce_max(anchor_positive_dist, axis=1, keepdims=True) # 取每一行最大的值即为最大positive距离tf.summary.scalar("hardest_positive_dis", tf.reduce_mean(hardest_positive_dist))'''取每一行最小值得时候,因为invalid [a, n]置为了0, 所以不能直接取,这里对应invalid位置加上每一行的最大值即可,然后再取最小的值'''mask_anchor_negative = _get_anchor_negative_triplet_mask(labels)mask_anchor_negative = tf.to_float(mask_anchor_negative)max_anchor_negative_dist = tf.reduce_max(pairwise_distances, axis=1, keepdims=True) # 每一样最大值anchor_negative_dist = pairwise_distances + max_anchor_negative_dist * (1.0 - mask_anchor_negative) # (1.0 - mask_anchor_negative)即为invalid位置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))triplet_loss = tf.maximum(hardest_positive_dist - hardest_negative_dist + margin, 0.0)triplet_loss = tf.reduce_mean(triplet_loss)return triplet_loss
三、具体使用
- 使用
mnist
数据集和triplet loss
训练,最后得到的embeddings
应该是同一类别的靠在一起 - 因为只有
10
个类别,所以直接随机取batch
大小的数据,这里batch_size=64
,- 注意如果类别很多时,就不能随机构建
batch
了, 需要选P
个类别,然后每个类别选K
张图
- 注意如果类别很多时,就不能随机构建
3.1 构建模型
- 上一篇介绍了 tensorflow的高级API, 这里使用
Estimator
构建模型 - 全部代码:点击查看
3.1.1 使用Estimator
params
指定超参数, 这里保存为json
格式的文件,- 配置为:
|
|
|
|
3.1.2 model_fn函数
- 下面都有对应注释
- 计算
embedding_mean_norm
中每一行embeding
公式为: $||A||_F = [\sum_{i,j} abs(a_{i,j})^2]^{1/2}$ , 然后再取均值
|
|
3.1.3 构建模型,得到embeddings
|
|
3.2 训练结果
3.2.1 batch all
python train_with_triplet_loss.py
- 可以在
tensorboard
中查看tensorboard --logdir experiment/model/
embeddings_mean_norm
- [ 可以看到是上升的,因为我们要学到可分性好的
embeddings
, 那么其方差应该是偏大的,均值应该是变大的 ]
- [ 可以看到是上升的,因为我们要学到可分性好的
fraction positive
- 这个是收敛的,因为随着优化占的比例是越来越少
- 这个是收敛的,因为随着优化占的比例是越来越少
loss
- 注意这里的
loss
一般不是收敛的,因为是计算的semi-hard
和hard
的距离均值,因为每次是先选择出semi-hard
和hard
的triplet
, 那么上次优化后的可能就选择不到了,所以loss
并不会收敛,但是fraction_postive_triplets
是收敛的,因为随着优化占的比例是越来越少的
- 注意这里的
3.2.2 batch hard
- embeddings mean norm
- positive and negative distance
- 这里我原以为应该是
negative
应该是增大的,positive
应该是减小的,但实际结果是positive
也是增大的,因为我们计算loss
是triplet_loss = tf.maximum(hardest_positive_dist - hardest_negative_dist + margin, 0.0)
, 只要negative
的距离大于positive + margin
就是0
了,所以只要满足就行, 用BN
训练的效果可能更好一点。(有什么其他看法的可以交流一下)
- 这里我原以为应该是
- loss
batch hard
的loss
就应该是收敛的了
3.3 可视化embedding
- 全部代码: 点击查看
- 之前在
tensorflow
工具中使用过: 点击查看 - 这里将可视化
embeddings
的训练数据都放在experiment/log
文件夹下- 另外我使用
tensorflow 1.11
出现问题,这里使用的版本是tensorflow 1.10
- 另外我使用
- 加载训练的模型,预测得到embeddings
|
|
- 获得label数据,保存为
metadata.tsv
文件
|
|
- 可视化
embedding
|
|
3.3.1 batch all
PCA
结果
3.3.2 batch hard
Reference
- https://omoindrot.github.io/triplet-loss#batch-hard-strategy
- https://github.com/omoindrot/tensorflow-triplet-loss
- https://github.com/lawlite19/Blog-Back-Up
- https://github.com/omoindrot/tensorflow-triplet-loss/issues/6
- FaceNet:https://arxiv.org/pdf/1503.03832.pdf
- Person Re-Identification: https://arxiv.org/pdf/1703.07737.pdf
- 本文链接: http://lawlite.me/2018/10/16/Triplet-Loss原理及其实现/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议 。转载请注明出处!