2018-05-27

(轉自知乎)目標檢測近年來已經取得了很重要的進展,主流的算法主要分為兩個類型(參考RefineDet):(1)two-stage方法,如R-CNN系算法,其主要思路是先通過啟發式方法(selective search)或者CNN網絡(RPN)產生一系列稀疏的候選框,然后對這些候選框進行分類與回歸,two-stage方法的優勢是準確度高;(2)one-stage方法,如Yolo和SSD,其主要思路是均勻地在圖片的不同位置進行密集抽樣,抽樣時可以采用不同尺度和長寬比,然后利用CNN提取特征后直接進行分類與回歸,整個過程只需要一步,所以其優勢是速度快,但是均勻的密集采樣的一個重要缺點是訓練比較困難,這主要是因為正樣本與負樣本(背景)極其不均衡(參見Focal Loss),導致模型準確度稍低。不同算法的性能如圖1所示,可以看到兩類方法在準確度和速度上的差異。

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖1 不同檢測算法的性能對比</figcaption>

</figure>

本文講解的是SSD算法,其英文全名是Single Shot MultiBox Detector,名字取得不錯,Single shot指明了SSD算法屬于one-stage方法,MultiBox指明了SSD是多框預測。在上一篇文章中我們已經講了Yolo算法,從圖1也可以看到,SSD算法在準確度和速度(除了SSD512)上都比Yolo要好很多。圖2給出了不同算法的基本框架圖,對于Faster R-CNN,其先通過CNN得到候選框,然后再進行分類與回歸,而Yolo與SSD可以一步到位完成檢測。相比Yolo,SSD采用CNN來直接進行檢測,而不是像Yolo那樣在全連接層之后做檢測。其實采用卷積直接做檢測只是SSD相比Yolo的其中一個不同點,另外還有兩個重要的改變,一是SSD提取了不同尺度的特征圖來做檢測,大尺度特征圖(較靠前的特征圖)可以用來檢測小物體,而小尺度特征圖(較靠后的特征圖)用來檢測大物體;二是SSD采用了不同尺度和長寬比的先驗框(Prior boxes, Default boxes,在Faster R-CNN中叫做錨,Anchors)。Yolo算法缺點是難以檢測小目標,而且定位不準,但是這幾點重要改進使得SSD在一定程度上克服這些缺點。下面我們詳細講解SDD算法的原理,并最后給出如何用TensorFlow實現SSD算法。

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖2 不同算法的基本框架圖</figcaption>

</figure>

設計理念

SSD和Yolo一樣都是采用一個CNN網絡來進行檢測,但是卻采用了多尺度的特征圖,其基本架構如圖3所示。下面將SSD核心設計理念總結為以下三點:

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖3 SSD基本框架</figcaption>

</figure>

(1)采用多尺度特征圖用于檢測

所謂多尺度采用大小不同的特征圖,CNN網絡一般前面的特征圖比較大,后面會逐漸采用stride=2的卷積或者pool來降低特征圖大小,這正如圖3所示,一個比較大的特征圖和一個比較小的特征圖,它們都用來做檢測。這樣做的好處是比較大的特征圖來用來檢測相對較小的目標,而小的特征圖負責檢測大目標,如圖4所示,8x8的特征圖可以劃分更多的單元,但是其每個單元的先驗框尺度比較小。

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖4 不同尺度的特征圖</figcaption>

</figure>

(2)采用卷積進行檢測

與Yolo最后采用全連接層不同,SSD直接采用卷積對不同的特征圖來進行提取檢測結果。對于形狀為 [圖片上傳失敗...(image-aa9462-1527406250790)]

的特征圖,只需要采用 [圖片上傳失敗...(image-c432aa-1527406250790)]

這樣比較小的卷積核得到檢測值。

(3)設置先驗框

在Yolo中,每個單元預測多個邊界框,但是其都是相對這個單元本身(正方塊),但是真實目標的形狀是多變的,Yolo需要在訓練過程中自適應目標的形狀。而SSD借鑒了Faster R-CNN中anchor的理念,每個單元設置尺度或者長寬比不同的先驗框,預測的邊界框(bounding boxes)是以這些先驗框為基準的,在一定程度上減少訓練難度。一般情況下,每個單元會設置多個先驗框,其尺度和長寬比存在差異,如圖5所示,可以看到每個單元使用了4個不同的先驗框,圖片中貓和狗分別采用最適合它們形狀的先驗框來進行訓練,后面會詳細講解訓練過程中的先驗框匹配原則。

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖5 SSD的先驗框</figcaption>

</figure>

SSD的檢測值也與Yolo不太一樣。對于每個單元的每個先驗框,其都輸出一套獨立的檢測值,對應一個邊界框,主要分為兩個部分。第一部分是各個類別的置信度或者評分,值得注意的是SSD將背景也當做了一個特殊的類別,如果檢測目標共有 [圖片上傳失敗...(image-4d05d7-1527406250790)]

個類別,SSD其實需要預測 [圖片上傳失敗...(image-980aae-1527406250790)]

個置信度值,其中第一個置信度指的是不含目標或者屬于背景的評分。后面當我們說 [圖片上傳失敗...(image-a181c-1527406250790)]

個類別置信度時,請記住里面包含背景那個特殊的類別,即真實的檢測類別只有 [圖片上傳失敗...(image-f138b5-1527406250790)]

個。在預測過程中,置信度最高的那個類別就是邊界框所屬的類別,特別地,當第一個置信度值最高時,表示邊界框中并不包含目標。第二部分就是邊界框的location,包含4個值 [圖片上傳失敗...(image-ecaf3c-1527406250790)]

,分別表示邊界框的中心坐標以及寬高。但是真實預測值其實只是邊界框相對于先驗框的轉換值(paper里面說是offset,但是覺得transformation更合適,參見R-CNN)。先驗框位置用 [圖片上傳失敗...(image-bc7778-1527406250790)]

表示,其對應邊界框用 [圖片上傳失敗...(image-6d2e14-1527406250790)]

$表示,那么邊界框的預測值 [圖片上傳失敗...(image-be18e9-1527406250790)]

其實是 [圖片上傳失敗...(image-878765-1527406250790)]

相對于 [圖片上傳失敗...(image-bcd2fa-1527406250790)]

的轉換值:

[圖片上傳失敗...(image-3e4186-1527406250790)]

[圖片上傳失敗...(image-c8847b-1527406250790)]

習慣上,我們稱上面這個過程為邊界框的編碼(encode),預測時,你需要反向這個過程,即進行解碼(decode),從預測值 [圖片上傳失敗...(image-eb29a-1527406250790)]

中得到邊界框的真實位置 [圖片上傳失敗...(image-74146e-1527406250790)]

[圖片上傳失敗...(image-f9bb2d-1527406250790)]

[圖片上傳失敗...(image-91f8f-1527406250790)]

然而,在SSD的Caffe源碼實現中還有trick,那就是設置variance超參數來調整檢測值,通過bool參數variance_encoded_in_target來控制兩種模式,當其為True時,表示variance被包含在預測值中,就是上面那種情況。但是如果是False(大部分采用這種方式,訓練更容易?),就需要手動設置超參數variance,用來對 [圖片上傳失敗...(image-256fb8-1527406250790)]

的4個值進行放縮,此時邊界框需要這樣解碼:

[圖片上傳失敗...(image-9f704e-1527406250790)]

[圖片上傳失敗...(image-7424e6-1527406250790)]

綜上所述,對于一個大小 [圖片上傳失敗...(image-98ea8e-1527406250790)]

的特征圖,共有 [圖片上傳失敗...(image-deafa3-1527406250790)]

個單元,每個單元設置的先驗框數目記為 [圖片上傳失敗...(image-59fca0-1527406250790)]

,那么每個單元共需要 [圖片上傳失敗...(image-5a072f-1527406250790)]

個預測值,所有的單元共需要 [圖片上傳失敗...(image-2c23bf-1527406250790)]

個預測值,由于SSD采用卷積做檢測,所以就需要 [圖片上傳失敗...(image-313e3-1527406250790)]

個卷積核完成這個特征圖的檢測過程。

網絡結構

SSD采用VGG16作為基礎模型,然后在VGG16的基礎上新增了卷積層來獲得更多的特征圖以用于檢測。SSD的網絡結構如圖5所示。上面是SSD模型,下面是Yolo模型,可以明顯看到SSD利用了多尺度的特征圖做檢測。模型的輸入圖片大小是 [圖片上傳失敗...(image-2ce82c-1527406250790)]

(還可以是 [圖片上傳失敗...(image-f0281c-1527406250789)]

,其與前者網絡結構沒有差別,只是最后新增一個卷積層,本文不再討論)。

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖5 SSD網絡結構</figcaption>

</figure>

采用VGG16做基礎模型,首先VGG16是在ILSVRC CLS-LOC數據集預訓練。然后借鑒了DeepLab-LargeFOV,分別將VGG16的全連接層fc6和fc7轉換成 [圖片上傳失敗...(image-2f49cf-1527406250789)]

卷積層 conv6和 [圖片上傳失敗...(image-8516df-1527406250789)]

卷積層conv7,同時將池化層pool5由原來的變成(猜想是不想reduce特征圖大小),為了配合這種變化,采用了一種Atrous Algorithm,其實就是conv6采用擴展卷積或帶孔卷積(<u style="text-decoration: none; border-bottom: 1px dashed gray;">Dilation Conv</u>),其在不增加參數與模型復雜度的條件下指數級擴大卷積的視野,其使用擴張率(dilation rate)參數,來表示擴張的大小,如下圖6所示,(a)是普通的 [圖片上傳失敗...(image-822235-1527406250789)]

卷積,其視野就是 [圖片上傳失敗...(image-2b542f-1527406250789)]

,(b)是擴張率為1,此時視野變成 [圖片上傳失敗...(image-d1fb78-1527406250789)]

,(c)擴張率為3時,視野擴大為 [圖片上傳失敗...(image-7941e9-1527406250789)]

,但是視野的特征更稀疏了。Conv6采用 [圖片上傳失敗...(image-62841c-1527406250789)]

大小但dilation rate=6的擴展卷積。

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖6 擴展卷積</figcaption>

</figure>

然后移除dropout層和fc8層,并新增一系列卷積層,在檢測數據集上做finetuing。

其中VGG16中的Conv4_3層將作為用于檢測的第一個特征圖。conv4_3層特征圖大小是 [圖片上傳失敗...(image-23587e-1527406250789)]

,但是該層比較靠前,其norm較大,所以在其后面增加了一個L2 Normalization層(參見ParseNet),以保證和后面的檢測層差異不是很大,這個和Batch Normalization層不太一樣,其僅僅是對每個像素點在channle維度做歸一化,而Batch Normalization層是在[batch_size, width, height]三個維度上做歸一化。歸一化后一般設置一個可訓練的放縮變量gamma,使用TF可以這樣簡單實現:

# l2norm (not bacth norm, spatial normalization)
def l2norm(x, scale, trainable=True, scope="L2Normalization"):
    n_channels = x.get_shape().as_list()[-1]
    l2_norm = tf.nn.l2_normalize(x, [3], epsilon=1e-12)
    with tf.variable_scope(scope):
        gamma = tf.get_variable("gamma", shape=[n_channels, ], dtype=tf.float32,
                                initializer=tf.constant_initializer(scale),
                                trainable=trainable)
        return l2_norm * gamma

從后面新增的卷積層中提取Conv7,Conv8_2,Conv9_2,Conv10_2,Conv11_2作為檢測所用的特征圖,加上Conv4_3層,共提取了6個特征圖,其大小分別是 [圖片上傳失敗...(image-b7c182-1527406250789)]

,但是不同特征圖設置的先驗框數目不同(同一個特征圖上每個單元設置的先驗框是相同的,這里的數目指的是一個單元的先驗框數目)。先驗框的設置,包括尺度(或者說大小)和長寬比兩個方面。對于先驗框的尺度,其遵守一個線性遞增規則:隨著特征圖大小降低,先驗框尺度線性增加:

[圖片上傳失敗...(image-527c7d-1527406250789)]

其中 [圖片上傳失敗...(image-d25b43-1527406250789)]

指的特征圖個數,但卻是 [圖片上傳失敗...(image-27557c-1527406250789)]

,因為第一層(Conv4_3層)是單獨設置的, [圖片上傳失敗...(image-ef0706-1527406250789)]

表示先驗框大小相對于圖片的比例,而 [圖片上傳失敗...(image-74686d-1527406250789)]

和 [圖片上傳失敗...(image-9682dc-1527406250789)]

表示比例的最小值與最大值,paper里面取0.2和0.9。對于第一個特征圖,其先驗框的尺度比例一般設置為 [圖片上傳失敗...(image-22c7b4-1527406250789)]

,那么尺度為 [圖片上傳失敗...(image-16049c-1527406250789)]

。對于后面的特征圖,先驗框尺度按照上面公式線性增加,但是先將尺度比例先擴大100倍,此時增長步長為 [圖片上傳失敗...(image-d23106-1527406250789)]

,這樣各個特征圖的 [圖片上傳失敗...(image-e84517-1527406250789)]

為 [圖片上傳失敗...(image-1cb4af-1527406250789)]

,將這些比例除以100,然后再乘以圖片大小,可以得到各個特征圖的尺度為 [圖片上傳失敗...(image-dc0526-1527406250789)]

,這種計算方式是參考SSD的Caffe源碼。綜上,可以得到各個特征圖的先驗框尺度 [圖片上傳失敗...(image-4ef710-1527406250789)]

。對于長寬比,一般選取 [圖片上傳失敗...(image-9e4f1c-1527406250789)]

,對于特定的長寬比,按如下公式計算先驗框的寬度與高度(后面的 [圖片上傳失敗...(image-b8e70c-1527406250789)]

均指的是先驗框實際尺度,而不是尺度比例):

[圖片上傳失敗...(image-5cba55-1527406250789)]

默認情況下,每個特征圖會有一個 [圖片上傳失敗...(image-8677e7-1527406250789)]

且尺度為 [圖片上傳失敗...(image-56a126-1527406250789)]

的先驗框,除此之外,還會設置一個尺度為 [圖片上傳失敗...(image-c1d015-1527406250789)]

且 [圖片上傳失敗...(image-b8481d-1527406250789)]

的先驗框,這樣每個特征圖都設置了兩個長寬比為1但大小不同的正方形先驗框。注意最后一個特征圖需要參考一個虛擬 [圖片上傳失敗...(image-f82bb3-1527406250789)]

來計算 [圖片上傳失敗...(image-8be899-1527406250789)]

。因此,每個特征圖一共有 [圖片上傳失敗...(image-be7fa-1527406250789)]

個先驗框 [圖片上傳失敗...(image-b6c666-1527406250789)]

,但是在實現時,Conv4_3,Conv10_2和Conv11_2層僅使用4個先驗框,它們不使用長寬比為 [圖片上傳失敗...(image-41dc14-1527406250789)]

的先驗框。每個單元的先驗框的中心點分布在各個單元的中心,即 [圖片上傳失敗...(image-7adf38-1527406250789)]

,其中 [圖片上傳失敗...(image-f11095-1527406250789)]

為特征圖的大小。

得到了特征圖之后,需要對特征圖進行卷積得到檢測結果,圖7給出了一個 [圖片上傳失敗...(image-3183e4-1527406250789)]

大小的特征圖的檢測過程。其中Priorbox是得到先驗框,前面已經介紹了生成規則。檢測值包含兩個部分:類別置信度和邊界框位置,各采用一次 [圖片上傳失敗...(image-3cf413-1527406250789)]

卷積來進行完成。令 [圖片上傳失敗...(image-7e788b-1527406250789)]

為該特征圖所采用的先驗框數目,那么類別置信度需要的卷積核數量為 [圖片上傳失敗...(image-6fc028-1527406250789)]

,而邊界框位置需要的卷積核數量為 [圖片上傳失敗...(image-77a566-1527406250789)]

。由于每個先驗框都會預測一個邊界框,所以SSD300一共可以預測 [圖片上傳失敗...(image-ab29fe-1527406250789)]

個邊界框,這是一個相當龐大的數字,所以說SSD本質上是密集采樣。

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖7 基于卷積得到檢測結果</figcaption>

</figure>

訓練過程

(1)先驗框匹配
在訓練過程中,首先要確定訓練圖片中的ground truth(真實目標)與哪個先驗框來進行匹配,與之匹配的先驗框所對應的邊界框將負責預測它。在Yolo中,ground truth的中心落在哪個單元格,該單元格中與其IOU最大的邊界框負責預測它。但是在SSD中卻完全不一樣,SSD的先驗框與ground truth的匹配原則主要有兩點。首先,對于圖片中每個ground truth,找到與其IOU最大的先驗框,該先驗框與其匹配,這樣,可以保證每個ground truth一定與某個先驗框匹配。通常稱與ground truth匹配的先驗框為正樣本(其實應該是先驗框對應的預測box,不過由于是一一對應的就這樣稱呼了),反之,若一個先驗框沒有與任何ground truth進行匹配,那么該先驗框只能與背景匹配,就是負樣本。一個圖片中ground truth是非常少的, 而先驗框卻很多,如果僅按第一個原則匹配,很多先驗框會是負樣本,正負樣本極其不平衡,所以需要第二個原則。第二個原則是:對于剩余的未匹配先驗框,若某個ground truth的 [圖片上傳失敗...(image-620469-1527406250789)]

大于某個閾值(一般是0.5),那么該先驗框也與這個ground truth進行匹配。這意味著某個ground truth可能與多個先驗框匹配,這是可以的。但是反過來卻不可以,因為一個先驗框只能匹配一個ground truth,如果多個ground truth與某個先驗框 [圖片上傳失敗...(image-9becd4-1527406250789)]

大于閾值,那么先驗框只與IOU最大的那個先驗框進行匹配。第二個原則一定在第一個原則之后進行,仔細考慮一下這種情況,如果某個ground truth所對應最大 [圖片上傳失敗...(image-1eb66-1527406250789)]

小于閾值,并且所匹配的先驗框卻與另外一個ground truth的 [圖片上傳失敗...(image-e30d4c-1527406250789)]

大于閾值,那么該先驗框應該匹配誰,答案應該是前者,首先要確保某個ground truth一定有一個先驗框與之匹配。但是,這種情況我覺得基本上是不存在的。由于先驗框很多,某個ground truth的最大 [圖片上傳失敗...(image-32118b-1527406250789)]

肯定大于閾值,所以可能只實施第二個原則既可以了,這里的TensorFlow版本就是只實施了第二個原則,但是這里的Pytorch兩個原則都實施了。圖8為一個匹配示意圖,其中綠色的GT是ground truth,紅色為先驗框,FP表示負樣本,TP表示正樣本。

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖8 先驗框匹配示意圖</figcaption>

</figure>

盡管一個ground truth可以與多個先驗框匹配,但是ground truth相對先驗框還是太少了,所以負樣本相對正樣本會很多。為了保證正負樣本盡量平衡,SSD采用了hard negative mining,就是對負樣本進行抽樣,抽樣時按照置信度誤差(預測背景的置信度越小,誤差越大)進行降序排列,選取誤差的較大的top-k作為訓練的負樣本,以保證正負樣本比例接近1:3。

(2)損失函數
訓練樣本確定了,然后就是損失函數了。損失函數定義為位置誤差(locatization loss, loc)與置信度誤差(confidence loss, conf)的加權和:

[圖片上傳失敗...(image-87add8-1527406250789)]

其中 [圖片上傳失敗...(image-c0426a-1527406250789)]

是先驗框的正樣本數量。這里 [圖片上傳失敗...(image-333887-1527406250789)]

為一個指示參數,當 [圖片上傳失敗...(image-25f788-1527406250789)]

時表示第 [圖片上傳失敗...(image-11e50f-1527406250789)]

個先驗框與第 [圖片上傳失敗...(image-9b6c5a-1527406250789)]

個ground truth匹配,并且ground truth的類別為 [圖片上傳失敗...(image-2b8389-1527406250789)]

。 [圖片上傳失敗...(image-8a2760-1527406250789)]

為類別置信度預測值。 [圖片上傳失敗...(image-6a341b-1527406250789)]

為先驗框的所對應邊界框的位置預測值,而 [圖片上傳失敗...(image-631625-1527406250789)]

是ground truth的位置參數。對于位置誤差,其采用Smooth L1 loss,定義如下:

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

</figure>

<figure style="margin: 1.6em 0px 1em; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

</figure>

由于 [圖片上傳失敗...(image-cb6729-1527406250789)]

的存在,所以位置誤差僅針對正樣本進行計算。值得注意的是,要先對ground truth的 [圖片上傳失敗...(image-f3f8b1-1527406250789)]

進行編碼得到 [圖片上傳失敗...(image-5d4f77-1527406250789)]

,因為預測值 [圖片上傳失敗...(image-3a1001-1527406250789)]

也是編碼值,若設置variance_encoded_in_target=True,編碼時要加上variance:

[圖片上傳失敗...(image-19a70e-1527406250789)]

[圖片上傳失敗...(image-6b1a6-1527406250789)]

對于置信度誤差,其采用softmax loss:

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

</figure>

權重系數 [圖片上傳失敗...(image-1698e8-1527406250789)]

通過交叉驗證設置為1。

(3)數據擴增

采用數據擴增(Data Augmentation)可以提升SSD的性能,主要采用的技術有水平翻轉(horizontal flip),隨機裁剪加顏色扭曲(random crop & color distortion),隨機采集塊域(Randomly sample a patch)(獲取小目標訓練樣本),如下圖所示:

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

</figure>

<figure style="margin: 1.6em 0px 1em; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">圖9 數據擴增方案</figcaption>

</figure>

其它的訓練細節如學習速率的選擇詳見論文,這里不再贅述。

預測過程

預測過程比較簡單,對于每個預測框,首先根據類別置信度確定其類別(置信度最大者)與置信度值,并過濾掉屬于背景的預測框。然后根據置信度閾值(如0.5)過濾掉閾值較低的預測框。對于留下的預測框進行解碼,根據先驗框得到其真實的位置參數(解碼后一般還需要做clip,防止預測框位置超出圖片)。解碼之后,一般需要根據置信度進行降序排列,然后僅保留top-k(如400)個預測框。最后就是進行NMS算法,過濾掉那些重疊度較大的預測框。最后剩余的預測框就是檢測結果了。

性能評估

首先整體看一下SSD在VOC2007,VOC2012及COCO數據集上的性能,如表1所示。相比之下,SSD512的性能會更好一些。加*的表示使用了image expansion data augmentation(通過zoom out來創造小的訓練樣本)技巧來提升SSD在小目標上的檢測效果,所以性能會有所提升。

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">表1 SSD在不同數據集上的性能</figcaption>

</figure>

SSD與其它檢測算法的對比結果(在VOC2007數據集)如表2所示,基本可以看到,SSD與Faster R-CNN有同樣的準確度,并且與Yolo具有同樣較快地檢測速度。

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">表2 SSD與其它檢測算法的對比結果(在VOC2007數據集)</figcaption>

</figure>

文章還對SSD的各個trick做了更為細致的分析,表3為不同的trick組合對SSD的性能影響,從表中可以得出如下結論:

  • 數據擴增技術很重要,對于mAP的提升很大;
  • 使用不同長寬比的先驗框可以得到更好的結果;

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">表3 不同的trick組合對SSD的性能影響</figcaption>

</figure>

同樣的,采用多尺度的特征圖用于檢測也是至關重要的,這可以從表4中看出:

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

<figcaption style="margin-top: 0.35556em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">表4 多尺度特征圖對SSD的影響</figcaption>

</figure>

TensorFlow上的實現

SSD在很多框架上都有了開源的實現,這里基于balancap的TensorFlow版本來實現SSD的Inference過程。這里實現的是SSD300,與paper里面不同的是,這里采用 [圖片上傳失敗...(image-8a3cbe-1527406250789)]

。首先定義SSD的參數:

self.ssd_params = SSDParams(img_shape=(300, 300),   # 輸入圖片大小
                                    num_classes=21,     # 類別數+背景
                                    no_annotation_label=21,
                                    feat_layers=["block4", "block7", "block8", "block9", "block10", "block11"],   # 要進行檢測的特征圖name
                                    feat_shapes=[(38, 38), (19, 19), (10, 10), (5, 5), (3, 3), (1, 1)],  # 特征圖大小
                                    anchor_size_bounds=[0.15, 0.90],  # 特征圖尺度范圍
                                    anchor_sizes=[(21., 45.),
                                                  (45., 99.),
                                                  (99., 153.),
                                                  (153., 207.),
                                                  (207., 261.),
                                                  (261., 315.)],  # 不同特征圖的先驗框尺度(第一個值是s_k,第2個值是s_k+1)
                                    anchor_ratios=[[2, .5],
                                                   [2, .5, 3, 1. / 3],
                                                   [2, .5, 3, 1. / 3],
                                                   [2, .5, 3, 1. / 3],
                                                   [2, .5],
                                                   [2, .5]], # 特征圖先驗框所采用的長寬比(每個特征圖都有2個正方形先驗框)
                                    anchor_steps=[8, 16, 32, 64, 100, 300],  # 特征圖的單元大小
                                    anchor_offset=0.5,                       # 偏移值,確定先驗框中心
                                    normalizations=[20, -1, -1, -1, -1, -1],  # l2 norm
                                    prior_scaling=[0.1, 0.1, 0.2, 0.2]       # variance
                                    )

然后構建整個網絡,注意對于stride=2的conv不要使用TF自帶的padding="same",而是手動pad,這是為了與Caffe一致:

def _built_net(self):
        """Construct the SSD net"""
        self.end_points = {}  # record the detection layers output
        self._images = tf.placeholder(tf.float32, shape=[None, self.ssd_params.img_shape[0],
                                                        self.ssd_params.img_shape[1], 3])
        with tf.variable_scope("ssd_300_vgg"):
            # original vgg layers
            # block 1
            net = conv2d(self._images, 64, 3, scope="conv1_1")
            net = conv2d(net, 64, 3, scope="conv1_2")
            self.end_points["block1"] = net
            net = max_pool2d(net, 2, scope="pool1")
            # block 2
            net = conv2d(net, 128, 3, scope="conv2_1")
            net = conv2d(net, 128, 3, scope="conv2_2")
            self.end_points["block2"] = net
            net = max_pool2d(net, 2, scope="pool2")
            # block 3
            net = conv2d(net, 256, 3, scope="conv3_1")
            net = conv2d(net, 256, 3, scope="conv3_2")
            net = conv2d(net, 256, 3, scope="conv3_3")
            self.end_points["block3"] = net
            net = max_pool2d(net, 2, scope="pool3")
            # block 4
            net = conv2d(net, 512, 3, scope="conv4_1")
            net = conv2d(net, 512, 3, scope="conv4_2")
            net = conv2d(net, 512, 3, scope="conv4_3")
            self.end_points["block4"] = net
            net = max_pool2d(net, 2, scope="pool4")
            # block 5
            net = conv2d(net, 512, 3, scope="conv5_1")
            net = conv2d(net, 512, 3, scope="conv5_2")
            net = conv2d(net, 512, 3, scope="conv5_3")
            self.end_points["block5"] = net
            print(net)
            net = max_pool2d(net, 3, stride=1, scope="pool5")
            print(net)

            # additional SSD layers
            # block 6: use dilate conv
            net = conv2d(net, 1024, 3, dilation_rate=6, scope="conv6")
            self.end_points["block6"] = net
            #net = dropout(net, is_training=self.is_training)
            # block 7
            net = conv2d(net, 1024, 1, scope="conv7")
            self.end_points["block7"] = net
            # block 8
            net = conv2d(net, 256, 1, scope="conv8_1x1")
            net = conv2d(pad2d(net, 1), 512, 3, stride=2, scope="conv8_3x3",
                         padding="valid")
            self.end_points["block8"] = net
            # block 9
            net = conv2d(net, 128, 1, scope="conv9_1x1")
            net = conv2d(pad2d(net, 1), 256, 3, stride=2, scope="conv9_3x3",
                         padding="valid")
            self.end_points["block9"] = net
            # block 10
            net = conv2d(net, 128, 1, scope="conv10_1x1")
            net = conv2d(net, 256, 3, scope="conv10_3x3", padding="valid")
            self.end_points["block10"] = net
            # block 11
            net = conv2d(net, 128, 1, scope="conv11_1x1")
            net = conv2d(net, 256, 3, scope="conv11_3x3", padding="valid")
            self.end_points["block11"] = net

            # class and location predictions
            predictions = []
            logits = []
            locations = []
            for i, layer in enumerate(self.ssd_params.feat_layers):
                cls, loc = ssd_multibox_layer(self.end_points[layer], self.ssd_params.num_classes,
                                              self.ssd_params.anchor_sizes[i],
                                              self.ssd_params.anchor_ratios[i],
                                              self.ssd_params.normalizations[i], scope=layer+"_box")
                predictions.append(tf.nn.softmax(cls))
                logits.append(cls)
                locations.append(loc)
            return predictions, logits, locations

對于特征圖的檢測,這里單獨定義了一個組合層ssd_multibox_layer,其主要是對特征圖進行兩次卷積,分別得到類別置信度與邊界框位置:

# multibox layer: get class and location predicitions from detection layer
    def ssd_multibox_layer(x, num_classes, sizes, ratios, normalization=-1, scope="multibox"):
        pre_shape = x.get_shape().as_list()[1:-1]
        pre_shape = [-1] + pre_shape
        with tf.variable_scope(scope):
            # l2 norm
            if normalization > 0:
                x = l2norm(x, normalization)
                print(x)
            # numbers of anchors
            n_anchors = len(sizes) + len(ratios)
            # location predictions
            loc_pred = conv2d(x, n_anchors*4, 3, activation=None, scope="conv_loc")
            loc_pred = tf.reshape(loc_pred, pre_shape + [n_anchors, 4])
            # class prediction
            cls_pred = conv2d(x, n_anchors*num_classes, 3, activation=None, scope="conv_cls")
            cls_pred = tf.reshape(cls_pred, pre_shape + [n_anchors, num_classes])
            return cls_pred, loc_pred

對于先驗框,可以基于numpy生成,定義在ssd_anchors.py文件中,結合先驗框與檢測值,對邊界框進行過濾與解碼:

classes, scores, bboxes = self._bboxes_select(predictions, locations)

這里將得到過濾得到的邊界框,其中classes, scores, bboxes分別表示類別,置信度值以及邊界框位置。

基于訓練好的權重文件在這里下載,這里對SSD進行測試:

ssd_net = SSD()
classes, scores, bboxes = ssd_net.detections()
images = ssd_net.images()

sess = tf.Session()
# Restore SSD model.
ckpt_filename = './ssd_checkpoints/ssd_vgg_300_weights.ckpt'
sess.run(tf.global_variables_initializer())
saver = tf.train.Saver()
saver.restore(sess, ckpt_filename)

img = cv2.imread('./demo/dog.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img_prepocessed = preprocess_image(img)   # 預處理圖片,主要是歸一化和resize
rclasses, rscores, rbboxes = sess.run([classes, scores, bboxes],
                                      feed_dict={images: img_prepocessed})
rclasses, rscores, rbboxes = process_bboxes(rclasses, rscores, rbboxes)  # 處理預測框,包括clip,sort,nms

plt_bboxes(img, rclasses, rscores, rbboxes)  # 繪制檢測結果

詳細的代碼放在GitHub上了,然后看一下一個自然圖片的檢測效果:

<figure style="margin: 1em 0px; color: rgb(26, 26, 26); font-family: -apple-system, system-ui, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", "Source Han Sans SC", "Noto Sans CJK SC", "WenQuanYi Micro Hei", sans-serif; font-size: 16px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: pre-wrap; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">
image

</figure>

如果你想實現SSD的train過程,你可以參考附錄里面的Caffe,TensorFlow以及Pytorch實現。

小結

SSD在Yolo的基礎上主要改進了三點:多尺度特征圖,利用卷積進行檢測,設置先驗框。這使得SSD在準確度上比Yolo更好,而且對于小目標檢測效果也相對好一點。由于很多實現細節都包含在源碼里面,文中有描述不準或者錯誤的地方在所難免,歡迎交流指正。

參考文獻

  1. SSD: Single Shot MultiBox Detector
  2. SSD Slide
  3. SSD Caffe
  4. SSD TensorFlow
  5. SSD Pytorch
  6. leonardoaraujosantos Artificial Inteligence online book
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容