本文譯自Olivier Moindrot的[blog](Triplet Loss and Online Triplet Mining in TensorFlow),英語好的可移步至其博客。
我們在之前的文章里介紹了[siamese network以及triplet network](Siamese network 孿生神經網絡--一個簡單神奇的結構)的基本概念,本文將介紹一下triplet network中triplet loss一些有趣的地方。
前言
在人臉識別領域,triplet loss通常用來學習人臉的向量表示。如果您對triplet loss不太了解推薦觀看Andrew Ng在Coursera上的deep learning specialization。
Triplet loss難于實現,本文將介紹triplet loss的定義以及triplet訓練時的策略。為什么要有訓練策略?所有的triplet組合太多了,都要訓練太inefficient,所以要挑一些比較好的triplet進行訓練,高效&效果好。
Triplet loss 和 triplet mining
為什么不用softmax,而使用triplet loss?
Triplet loss最早被用在人臉識別任務上,《FaceNet: A Unified Embedding for Face Recognition》 by Google。Google的研究人員提出了通過online triplet mining的方式訓練處人臉的新向量表示。接下來我們會詳細討論。
在有監督的機器學習領域,通常有固定的類別,這時就可以使用基于softmax的交叉熵損失函數進行訓練。但有時,類別是一個變量,此時使用triplet loss就能解決問題。在人臉識別,Quora question pair任務中,triplet loss的優勢在于細節區分,即當兩個輸入相似時,triplet loss能夠更好地對細節進行建模,相當于加入了兩個輸入差異性差異的度量,學習到輸入的更好表示,從而在上述兩個任務中有出色的表現。當然,triplet loss的缺點在于其收斂速度慢,有時不收斂。
Triplet loss的motivation是要讓屬于同一個人的人臉盡可能地“近”
(在embedding空間里),而與其他人臉盡可能地“遠”。
Triplet loss 定義
)
triplet loss的目標是:
兩個具有同樣標簽的樣本,他們在新的編碼空間里距離很近。
兩個具有不同標簽的樣本,他們在新的編碼空間里距離很遠。
進一步,我們希望兩個positive examples和一個negative example中,negative example與positive example的距離,大于positive examples之間的距離,或者大于某一個閾值:margin。
triplet loss定義在下面三元組概念之上:
an anchor(基準正例)
a positive of the same class as the anchor (正例)
a negative of a different class (負例)
對于(a,p,n)這個triplet(三元組),其triplet loss就可以寫作:
[圖片上傳失敗...(image-c02bb3-1523449975636)]
這時可以通過最小化上述損失函數,a與p之間的距離d(a,p)=0,而a與n之間的距離d(a,n)大于d(a,p)+margin。當negative example很好識別時,上述損失函數為0,否則是一個比較大的值。
Triplet mining
基于triplet loss的定義,可以將triplet(三元組)分為三類:
easy triplets(簡單三元組): triplet對應的損失為0的三元組,形式化定義為$d(a,n)>d(a,p)+margin$。
hard triplets(困難三元組): negative example 與anchor距離小于anchor與positive example的距離,形式化定義為$d(a,n)<d(a,p)$。
semi-hard triplets(一般三元組): negative example 與anchor距離大于anchor與positive example的距離,但還不至于使得loss為0,即$d(a,p)<d(a,n)<d(a,p)+margin$。
上述三種概念都是基于negative example與anchor和positive距離定義的。類似的,可以根據上述定義將negative examples分為3類:hard negatives, easy negatives, semi-hard negatives。如下圖所示,這個圖構建了編碼空間中三種negative examples與anchor和positive example之間的距離關系。
)
如何選擇triplet或者negative examples,對模型的效率有很大影響。在上述Facenet論文中,采用了隨機的semi-hard negative構建triplet進行訓練,取得了不錯的效果。
Offline和online triplet mining
通過上面的分析,可以看到,easy negative example比較容易識別,沒必要構建太多由easy negative example組成的triplet,否則會嚴重降低訓練效率。若都采用hard negative example,又可能會影響訓練效果。這時,就需要一定的方法進行triplet的挑選,也就是“mine the triplets”。
Offline triplet mining
離線方式的triplet mining將所有的訓練數據喂給神經網絡,得到每一個訓練樣本的編碼,根據編碼計算得到negative example與anchor和positive example之間的距離,根據這個距離判斷semi-hard triplets,hard triplets還是easy triplets。offline triplet mining 僅僅選擇select hard or semi-hard triplets,因為easy triplet太容易了,沒有必要訓練。
總得來說,這個方法不夠高效,因為最初要把所有的訓練數據喂給神經網絡,而且每過1個或幾個epoch,可能還要重新對negative examples進行分類。
Online triplet mining
Google的研究人員為解決上述問題,提出了online triplet mining的方法。該方法的motivation比較簡單,將B張圖片(一個batch)喂給神經網絡,得到B張圖片的embedding,將triplet的組合一共最多$B^3$個triplets,其中包含很多沒用的triplet(比如,三個negative examples和三個positive examples,這種稱作invalid triplets)。哪些是valid triplets呢?假設一個triplet$(B_i,B_j,B_k)$,如果樣本i和j有相同的label且不是同一個樣本,而樣本k具有不同的label,則稱其為valid triplet。
假設一個batch的數據包含P*K張人臉,P個人,每人K張圖片。
針對valid triplet的“挑選”,有以下兩個策略(來自論文[《In Defense of the Triplet Loss for Person Re-Identification》]([1703.07737] In Defense of the Triplet Loss for Person Re-Identification):
batch all: 計算所有的valid triplet,對hard 和 semi-hard triplets上的loss進行平均。
不考慮easy triplets,因為easy triplets的損失為0,平均會把整體損失縮小
將會產生PK(K-1)(PK-K)個triplet,即PK個anchor,對于每個anchor有k-1個可能的positive example,PK-K個可能的negative examples
batch hard: 對于每一個anchor,選擇hardest positive example(距離anchor最大的positive example)和hardest negative(距離anchor最大的negative example),
由此產生PK個triplet
這些triplet是最難分的
)
論文[《In Defense of the Triplet Loss for Person Re-Identification》]([1703.07737] In Defense of the Triplet Loss for Person Re-Identification)實驗結果表明,batch hard的表現是最好的。
那如何用tensorflow實現triplet loss呢?
offline triplets
很簡單,就是實現上面offline triplets的公式,tensorflow的實現如下:
anchor_output = ... # shape [None, 128]
positive_output = ... # shape [None, 128]
negative_output = ... # shape [None, 128]
d_pos = tf.reduce_sum(tf.square(anchor_output - positive_output), 1)
d_neg = tf.reduce_sum(tf.square(anchor_output - negative_output), 1)
loss = tf.maximum(0.0, margin + d_pos - d_neg)
loss = tf.reduce_mean(loss)
online triplets
batch all的實現方式
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)
anchor_positive_dist = tf.expand_dims(pairwise_dist, 2)
anchor_negative_dist = tf.expand_dims(pairwise_dist, 1)
# 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-16))
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
batch hard的實現方式
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)
# 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)
# 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
在minist等數據集上的效果都是棒棒噠。
總結
triplet loss的實現不是很簡單,比較tricky的地方是如何計算embedding的距離,以及怎樣識別并拋棄掉invalid和easy triplet。當然,如果您使用的是tensorflow,可以直接移步至github repository,有一份寫好的triplet loss在等著你。。。
可能有人會有疑惑,siamese network, triplet network的輸入都是成對的,或者triplet的三元組,怎么對一個樣本進行分類啊?神經網絡的優勢在于表示學習,自動的特征提取,所以,成對,或者triplet的輸入能讓神經網絡有更好的輸入表示,后面再接svm, logtistic regression就可以啦。
本文譯自Olivier Moindrot的[blog](Triplet Loss and Online Triplet Mining in TensorFlow),英語好的可移步至其博客。
我們在之前的文章里介紹了[siamese network以及triplet network](Siamese network 孿生神經網絡--一個簡單神奇的結構)的基本概念,本文將介紹一下triplet network中triplet loss一些有趣的地方。
前言
在人臉識別領域,triplet loss通常用來學習人臉的向量表示。如果您對triplet loss不太了解推薦觀看Andrew Ng在Coursera上的deep learning specialization。
Triplet loss難于實現,本文將介紹triplet loss的定義以及triplet訓練時的策略。為什么要有訓練策略?所有的triplet組合太多了,都要訓練太inefficient,所以要挑一些比較好的triplet進行訓練,高效&效果好。
Triplet loss 和 triplet mining
為什么不用softmax,而使用triplet loss?
Triplet loss最早被用在人臉識別任務上,《FaceNet: A Unified Embedding for Face Recognition》 by Google。Google的研究人員提出了通過online triplet mining的方式訓練處人臉的新向量表示。接下來我們會詳細討論。
在有監督的機器學習領域,通常有固定的類別,這時就可以使用基于softmax的交叉熵損失函數進行訓練。但有時,類別是一個變量,此時使用triplet loss就能解決問題。在人臉識別,Quora question pair任務中,triplet loss的優勢在于細節區分,即當兩個輸入相似時,triplet loss能夠更好地對細節進行建模,相當于加入了兩個輸入差異性差異的度量,學習到輸入的更好表示,從而在上述兩個任務中有出色的表現。當然,triplet loss的缺點在于其收斂速度慢,有時不收斂。
Triplet loss的motivation是要讓屬于同一個人的人臉盡可能地“近”
(在embedding空間里),而與其他人臉盡可能地“遠”。
Triplet loss 定義
)
triplet loss的目標是:
兩個具有同樣標簽的樣本,他們在新的編碼空間里距離很近。
兩個具有不同標簽的樣本,他們在新的編碼空間里距離很遠。
進一步,我們希望兩個positive examples和一個negative example中,negative example與positive example的距離,大于positive examples之間的距離,或者大于某一個閾值:margin。
triplet loss定義在下面三元組概念之上:
an anchor(基準正例)
a positive of the same class as the anchor (正例)
a negative of a different class (負例)
對于(a,p,n)這個triplet(三元組),其triplet loss就可以寫作:
[圖片上傳失敗...(image-fd03a2-1523449977468)]
這時可以通過最小化上述損失函數,a與p之間的距離d(a,p)=0,而a與n之間的距離d(a,n)大于d(a,p)+margin。當negative example很好識別時,上述損失函數為0,否則是一個比較大的值。
Triplet mining
基于triplet loss的定義,可以將triplet(三元組)分為三類:
easy triplets(簡單三元組): triplet對應的損失為0的三元組,形式化定義為$d(a,n)>d(a,p)+margin$。
hard triplets(困難三元組): negative example 與anchor距離小于anchor與positive example的距離,形式化定義為$d(a,n)<d(a,p)$。
semi-hard triplets(一般三元組): negative example 與anchor距離大于anchor與positive example的距離,但還不至于使得loss為0,即$d(a,p)<d(a,n)<d(a,p)+margin$。
上述三種概念都是基于negative example與anchor和positive距離定義的。類似的,可以根據上述定義將negative examples分為3類:hard negatives, easy negatives, semi-hard negatives。如下圖所示,這個圖構建了編碼空間中三種negative examples與anchor和positive example之間的距離關系。
)
如何選擇triplet或者negative examples,對模型的效率有很大影響。在上述Facenet論文中,采用了隨機的semi-hard negative構建triplet進行訓練,取得了不錯的效果。
Offline和online triplet mining
通過上面的分析,可以看到,easy negative example比較容易識別,沒必要構建太多由easy negative example組成的triplet,否則會嚴重降低訓練效率。若都采用hard negative example,又可能會影響訓練效果。這時,就需要一定的方法進行triplet的挑選,也就是“mine the triplets”。
Offline triplet mining
離線方式的triplet mining將所有的訓練數據喂給神經網絡,得到每一個訓練樣本的編碼,根據編碼計算得到negative example與anchor和positive example之間的距離,根據這個距離判斷semi-hard triplets,hard triplets還是easy triplets。offline triplet mining 僅僅選擇select hard or semi-hard triplets,因為easy triplet太容易了,沒有必要訓練。
總得來說,這個方法不夠高效,因為最初要把所有的訓練數據喂給神經網絡,而且每過1個或幾個epoch,可能還要重新對negative examples進行分類。
Online triplet mining
Google的研究人員為解決上述問題,提出了online triplet mining的方法。該方法的motivation比較簡單,將B張圖片(一個batch)喂給神經網絡,得到B張圖片的embedding,將triplet的組合一共最多$B^3$個triplets,其中包含很多沒用的triplet(比如,三個negative examples和三個positive examples,這種稱作invalid triplets)。哪些是valid triplets呢?假設一個triplet$(B_i,B_j,B_k)$,如果樣本i和j有相同的label且不是同一個樣本,而樣本k具有不同的label,則稱其為valid triplet。
假設一個batch的數據包含P*K張人臉,P個人,每人K張圖片。
針對valid triplet的“挑選”,有以下兩個策略(來自論文[《In Defense of the Triplet Loss for Person Re-Identification》]([1703.07737] In Defense of the Triplet Loss for Person Re-Identification):
batch all: 計算所有的valid triplet,對hard 和 semi-hard triplets上的loss進行平均。
不考慮easy triplets,因為easy triplets的損失為0,平均會把整體損失縮小
將會產生PK(K-1)(PK-K)個triplet,即PK個anchor,對于每個anchor有k-1個可能的positive example,PK-K個可能的negative examples
batch hard: 對于每一個anchor,選擇hardest positive example(距離anchor最大的positive example)和hardest negative(距離anchor最大的negative example),
由此產生PK個triplet
這些triplet是最難分的
)
論文[《In Defense of the Triplet Loss for Person Re-Identification》]([1703.07737] In Defense of the Triplet Loss for Person Re-Identification)實驗結果表明,batch hard的表現是最好的。
那如何用tensorflow實現triplet loss呢?
offline triplets
很簡單,就是實現上面offline triplets的公式,tensorflow的實現如下:
anchor_output = ... # shape [None, 128]
positive_output = ... # shape [None, 128]
negative_output = ... # shape [None, 128]
d_pos = tf.reduce_sum(tf.square(anchor_output - positive_output), 1)
d_neg = tf.reduce_sum(tf.square(anchor_output - negative_output), 1)
loss = tf.maximum(0.0, margin + d_pos - d_neg)
loss = tf.reduce_mean(loss)
online triplets
batch all的實現方式
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)
anchor_positive_dist = tf.expand_dims(pairwise_dist, 2)
anchor_negative_dist = tf.expand_dims(pairwise_dist, 1)
# 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-16))
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
batch hard的實現方式
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)
# 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)
# 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
在minist等數據集上的效果都是棒棒噠。
總結
triplet loss的實現不是很簡單,比較tricky的地方是如何計算embedding的距離,以及怎樣識別并拋棄掉invalid和easy triplet。當然,如果您使用的是tensorflow,可以直接移步至github repository,有一份寫好的triplet loss在等著你。。。
可能有人會有疑惑,siamese network, triplet network的輸入都是成對的,或者triplet的三元組,怎么對一個樣本進行分類啊?神經網絡的優勢在于表示學習,自動的特征提取,所以,成對,或者triplet的輸入能讓神經網絡有更好的輸入表示,后面再接svm, logtistic regression就可以啦。