Triplet Loss及tensorflow實現

本文譯自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 faces (Obama) 和 negative face (Macron)上的示意圖
Triplet loss 在 positive faces (Obama) 和 negative face (Macron)上的示意圖

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之間的距離關系。

三種negative examples與anchor和positive example之間的距離關系
三種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是最難分的

Online triplet loss
Online triplet loss

論文[《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 faces (Obama) 和 negative face (Macron)上的示意圖
Triplet loss 在 positive faces (Obama) 和 negative face (Macron)上的示意圖

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之間的距離關系。

三種negative examples與anchor和positive example之間的距離關系
三種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是最難分的

Online triplet loss
Online triplet loss

論文[《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就可以啦。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容