跟我一起學(xué)PyTorch-06:卷積神經(jīng)網(wǎng)絡(luò)CNN

以全連接層為基礎(chǔ)的深度神經(jīng)網(wǎng)絡(luò)DNN是整個(gè)深度學(xué)習(xí)的基石。要說(shuō)應(yīng)用最廣、影響最大的深度神經(jīng)網(wǎng)絡(luò),那就是卷積神經(jīng)網(wǎng)絡(luò)(Convolutional Neural Network,CNN)震庭。卷積神經(jīng)網(wǎng)絡(luò)雖然發(fā)布的時(shí)間較早,但直到2006年Hilton解決深度神經(jīng)網(wǎng)絡(luò)的訓(xùn)練問(wèn)題后才煥發(fā)生機(jī)你雌。卷積神經(jīng)網(wǎng)絡(luò)現(xiàn)在幾乎是圖像識(shí)別研究的標(biāo)配器联。

簡(jiǎn)單回顧卷積神經(jīng)網(wǎng)絡(luò)的發(fā)展歷程。日本科學(xué)家福島邦彥在1986年提出了Neocognitron(神經(jīng)認(rèn)知機(jī))婿崭,直接啟發(fā)了后來(lái)的卷積神經(jīng)網(wǎng)絡(luò)拨拓。Yann LeCun于1998年提出的卷積神經(jīng)網(wǎng)絡(luò)LeNet,首次使用了多層級(jí)聯(lián)的卷積結(jié)構(gòu)氓栈,可對(duì)手寫(xiě)數(shù)字進(jìn)行有效識(shí)別渣磷。2012年,Alex依靠卷積神經(jīng)網(wǎng)絡(luò)AlexNet奪得ILSVRC 2012比賽的冠軍授瘦,吹響了卷積神經(jīng)網(wǎng)絡(luò)研究的號(hào)角醋界。AlexNet成功應(yīng)用了ReLU、Dropout提完、最大池化形纺、LRN(Local Response Normalization,局部響應(yīng)歸一化)徒欣、GPU加速等新技術(shù)逐样,啟發(fā)了后續(xù)更多的技術(shù)創(chuàng)新,加速了卷積神經(jīng)網(wǎng)絡(luò)和深度學(xué)習(xí)的研究打肝。從此脂新,深度學(xué)習(xí)研究進(jìn)入了蓬勃發(fā)展的新階段。2014年Google提出的GoogleNet粗梭,運(yùn)用Inception Module這個(gè)可以反復(fù)堆疊争便、高效的卷積神經(jīng)結(jié)構(gòu),獲得了當(dāng)年ImageNet ILSVRC比賽的冠軍楼吃,同年的亞軍VGGNet全程使用3x3的卷積始花,成功訓(xùn)練了深度達(dá)19層的網(wǎng)絡(luò)。2015年孩锡,微軟提出了ResNet酷宵,包含殘差學(xué)習(xí)模塊,成功訓(xùn)練了152層的網(wǎng)絡(luò)躬窜,一舉拿下當(dāng)年的ILSVRC比賽的冠軍浇垦。

卷積神經(jīng)網(wǎng)絡(luò)技術(shù)的發(fā)展風(fēng)起云涌,盡管卷積神經(jīng)網(wǎng)絡(luò)最初是為了解決計(jì)算機(jī)視覺(jué)等問(wèn)題設(shè)計(jì)的荣挨,現(xiàn)在其應(yīng)用范圍不僅僅局限于圖像和視頻領(lǐng)域男韧,也可用于時(shí)間序列信號(hào),比如音頻信號(hào)等默垄。本章主要通過(guò)卷積神經(jīng)網(wǎng)絡(luò)在計(jì)算機(jī)視覺(jué)上的應(yīng)用來(lái)介紹卷積神經(jīng)網(wǎng)絡(luò)的原理以及如何使用PyTorch實(shí)現(xiàn)卷積神經(jīng)網(wǎng)絡(luò)此虑。

本章首先介紹人類(lèi)視覺(jué)和計(jì)算機(jī)視覺(jué)的基本原理,以及計(jì)算機(jī)視覺(jué)中特征的提取和選擇口锭;然后介紹卷積神經(jīng)網(wǎng)絡(luò)的主體思想和整體結(jié)構(gòu)朦前,并將詳細(xì)講解卷積層和池化層的網(wǎng)絡(luò)結(jié)構(gòu)、PyTorch對(duì)這些網(wǎng)絡(luò)的支持鹃操、如何設(shè)置每一層神經(jīng)網(wǎng)絡(luò)的配置韭寸,以及更加復(fù)雜的卷積神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu),如AlexNet荆隘、VGGNet恩伺、ResNet等;最后在MNIST數(shù)據(jù)集上通過(guò)PyTorch使用卷積神經(jīng)網(wǎng)絡(luò)實(shí)現(xiàn)圖片分類(lèi)椰拒。

1.計(jì)算機(jī)視覺(jué)

1.人類(lèi)視覺(jué)和計(jì)算機(jī)視覺(jué)

視覺(jué)是人類(lèi)觀察和認(rèn)識(shí)世界非常重要的手段晶渠。據(jù)統(tǒng)計(jì),人類(lèi)從外部世界獲取的信息約80%是來(lái)自視覺(jué)耸三,既說(shuō)明視覺(jué)信息量巨大乱陡,同時(shí)又體現(xiàn)了視覺(jué)功能的重要性。同時(shí)仪壮,人類(lèi)視覺(jué)是如此的功能強(qiáng)大憨颠,在很短的時(shí)間里,迅速地辨識(shí)視線中的物體积锅,在人的視覺(jué)系統(tǒng)中爽彤,人的眼睛捕捉物體得到光信息。這些光信息經(jīng)過(guò)處理缚陷,運(yùn)送到大腦的視覺(jué)皮層适篙,分析得到以下信息:有關(guān)物體的空間、色彩箫爷、形狀和紋理等嚷节。有了這些信息聂儒,大腦做出對(duì)該物體的辨識(shí)。對(duì)于人類(lèi)而言硫痰,通過(guò)視覺(jué)來(lái)辨識(shí)數(shù)字衩婚、識(shí)別圖片中的物體或者找出圖片中人臉的輪廓是非常簡(jiǎn)單的事情。然而對(duì)于計(jì)算機(jī)而言效斑,讓計(jì)算機(jī)識(shí)別圖片中的內(nèi)容就不是一件容易的事情非春。計(jì)算機(jī)視覺(jué)希望借助計(jì)算機(jī)程序來(lái)處理、分析和理解圖片中的內(nèi)容缓屠,使得計(jì)算機(jī)可以從圖片中自動(dòng)識(shí)別各種不同模式的目標(biāo)和對(duì)象奇昙。

在深度學(xué)習(xí)出現(xiàn)之前,圖像識(shí)別的一般過(guò)程是敌完,前端為特征提取储耐,后端為模式識(shí)別算法。后端的模式識(shí)別算法包括K近鄰算法滨溉、支持向量機(jī)衡瓶、神經(jīng)網(wǎng)絡(luò)等晓避。對(duì)于不同的識(shí)別場(chǎng)景和越來(lái)越復(fù)雜的識(shí)別目標(biāo)宫补,尋找合適的前端特征顯得尤為重要植阴。

2.特征提取

對(duì)于特征提取,抽象于人的視覺(jué)原理勤家,提取有關(guān)輪廓腹尖、色彩、紋理伐脖、空間等相關(guān)的特征热幔。以色彩為例,它是一種現(xiàn)在仍然在廣泛使用的特征讼庇,稱為顏色直方圖特征绎巨,這是一種簡(jiǎn)單、直觀蠕啄、對(duì)實(shí)際圖片顏色進(jìn)行數(shù)字化表達(dá)的方式场勤。顏色的值用RGB三原色進(jìn)行表示,顏色直方圖的橫軸表示顏色的RGB值歼跟,表示該物品所有顏色的集合和媳,縱軸表示整個(gè)圖像具有每個(gè)顏色值像素的數(shù)量,這樣計(jì)算機(jī)可以對(duì)圖像進(jìn)行顏色表征哈街。

image.png

以紋理特征為例留瞳,橘子會(huì)有凸凹不平的紋理,而蘋(píng)果的紋理則非常光滑骚秦。這種局部的紋理刻畫(huà)她倘,如何通過(guò)特征抽象表示出來(lái)璧微?Gabor特征可以用來(lái)描述圖像紋理信息的特征,Gabor濾波器的頻率和方向與人類(lèi)的視覺(jué)系統(tǒng)類(lèi)似硬梁,特別適合于紋理表示與判別往毡。SIFT(Scale Invariant Feature Transform,尺寸不變特征變換)特征是一種檢測(cè)局部特征的算法靶溜,該算法通過(guò)把圖片中特征點(diǎn)用特征向量進(jìn)行描述,該特征向量具有對(duì)象縮放懒震、平移罩息、旋轉(zhuǎn)不變的特性,對(duì)于光照个扰、仿射和投影變換也有一定的不變性瓷炮。

image.png

形狀特征也是圖像特征的重要一類(lèi),HOG(Histogram of Oriented Gridients)特征就是其中的一種递宅。HOG是一種描述圖像局部梯度方向和梯度強(qiáng)度分布的特征娘香。其核心內(nèi)容是:在邊緣具體位置未知的情況下,邊緣方向的分布也可以很好地表示目標(biāo)的外形輪廓办龄。

image.png

上述特征提取算法提取的特征還是有局限的烘绽,盡管在顏色為黑白的數(shù)據(jù)集MNIST上的最好結(jié)果的錯(cuò)誤率為0.54%,但是在大型和復(fù)雜的數(shù)據(jù)ImageNet ILSVRC比賽的最好結(jié)果的錯(cuò)誤率也在26%以上俐填,而且難以突破安接。同時(shí),提取的特征只在特定的場(chǎng)合有效英融,場(chǎng)景變化后盏檐,需要重新提取特征和調(diào)整模型參數(shù)。卷積神經(jīng)網(wǎng)絡(luò)能夠自動(dòng)提取特征驶悟,不必人為地提取特征胡野,這樣提取的特征能夠達(dá)到更好的效果。同時(shí)痕鳍,它不需要將特征提取和分類(lèi)訓(xùn)練兩個(gè)過(guò)程分開(kāi)硫豆,在訓(xùn)練的過(guò)程中自動(dòng)提取特征、循環(huán)迭代笼呆、自動(dòng)選取最優(yōu)的特征够庙。

3.數(shù)據(jù)集

對(duì)于卷積神經(jīng)網(wǎng)絡(luò)的成功,計(jì)算機(jī)視覺(jué)領(lǐng)域的幾大數(shù)據(jù)集可謂功不可沒(méi)抄邀。在計(jì)算機(jī)視覺(jué)中有以下幾大基礎(chǔ)數(shù)據(jù)集耘眨。
(1)MNIST
MNIST數(shù)據(jù)集是用作手寫(xiě)體識(shí)別的數(shù)據(jù)集。MNIST數(shù)據(jù)集包括60000張訓(xùn)練圖片和10000張測(cè)試圖片境肾。如圖所示剔难。其中胆屿,每一張圖片都是0~9中的一個(gè)數(shù)字。圖片尺寸為28x28偶宫。由于數(shù)據(jù)集中數(shù)據(jù)相對(duì)比較簡(jiǎn)單非迹,人工標(biāo)注錯(cuò)誤率僅為0.2%。

image.png

(2)CIFAR
CIFAR數(shù)據(jù)集是一個(gè)圖像分類(lèi)數(shù)據(jù)集纯趋。CIFAR數(shù)據(jù)集分為Cifar-10和Cifar-100兩個(gè)數(shù)據(jù)集憎兽。CIFAR數(shù)據(jù)集中的圖片為32x32的彩色圖片,這些圖片是由Alex Krizhenevsky教授吵冒、Vinod Nair博士和Geoffrey Hilton教授整理的纯命。Cifar-10數(shù)據(jù)集收集了來(lái)自10個(gè)不同種類(lèi)的60000張圖片,這些種類(lèi)有飛機(jī)痹栖、汽車(chē)亿汞、鳥(niǎo)、貓揪阿、鹿疗我、狗、青蛙南捂、馬吴裤、船和卡車(chē)。在Cifar-10數(shù)據(jù)集上溺健,人工標(biāo)注的正確率是94%嚼摩。

image.png

(3)ImageNet
ImageNet數(shù)據(jù)集是一個(gè)大型圖片數(shù)據(jù)集,由斯坦福大學(xué)的李飛飛教授帶頭整理而成矿瘦。在ImageNet中枕面,近1500萬(wàn)張圖片關(guān)聯(lián)到WordNet中20000個(gè)名詞同義詞集上。ImageNet每年舉行計(jì)算機(jī)視覺(jué)相關(guān)的競(jìng)賽——Image Large Scala Visual Recognition Challenge缚去,即ILSVRC潮秘。ImageNet數(shù)據(jù)集涵蓋了計(jì)算機(jī)視覺(jué)的各個(gè)研究方向,其用作圖像分類(lèi)的數(shù)據(jù)集是ILSVRC2012圖像分類(lèi)數(shù)據(jù)集易结。ILSVRC2012數(shù)據(jù)集和Cifar-10數(shù)據(jù)集一樣枕荞,識(shí)別圖像中主要物體,其中包含了來(lái)自1000個(gè)種類(lèi)的120萬(wàn)張圖片搞动,每張圖片只屬于一個(gè)種類(lèi)躏精,大小從幾K到幾M不等。卷積神經(jīng)網(wǎng)絡(luò)在此數(shù)據(jù)集上一戰(zhàn)成名鹦肿。

image.png

2.卷積神經(jīng)網(wǎng)絡(luò)

計(jì)算機(jī)視覺(jué)作為人工智能的重要領(lǐng)域矗烛,在2006年后取得了很多突破性的進(jìn)展。本章介紹的卷積神經(jīng)網(wǎng)絡(luò)就是這些突破性進(jìn)展背后的技術(shù)基礎(chǔ)箩溃。在前面章節(jié)中介紹的神經(jīng)網(wǎng)絡(luò)每?jī)蓪拥乃泄?jié)點(diǎn)都是兩兩相連的瞭吃,所以稱這種網(wǎng)絡(luò)結(jié)構(gòu)為全連接層網(wǎng)絡(luò)結(jié)構(gòu)碌嘀。可將只包含全連接層的神經(jīng)網(wǎng)絡(luò)稱為全連接神經(jīng)網(wǎng)絡(luò)歪架。

image.png

卷積神經(jīng)網(wǎng)絡(luò)利用卷積結(jié)構(gòu)減少需要學(xué)習(xí)的參數(shù)量股冗,從而提高反向傳播算法的訓(xùn)練效率。如下圖所示和蚪,在卷積神經(jīng)網(wǎng)絡(luò)中止状,第一個(gè)卷積層會(huì)直接接受圖像像素級(jí)的輸入,每一個(gè)卷積操作只處理一小塊圖像攒霹,進(jìn)行卷積操作后傳遞給后面的網(wǎng)絡(luò)怯疤,每一層卷積都會(huì)提取數(shù)據(jù)中最有效的特征。這種方法可以提取到圖像中最基礎(chǔ)的特征剔蹋,比如不同方向的拐角或者邊,而后進(jìn)行組合和抽象成更高階的特征辅髓,因此卷積神經(jīng)網(wǎng)絡(luò)對(duì)圖像縮放泣崩、平移和旋轉(zhuǎn)具有不變性。

image.png

在圖像處理中洛口,圖像是一個(gè)或者多個(gè)二維矩陣矫付,如前面提到的MNIST手寫(xiě)體圖片是一個(gè)28x28的二維矩陣。傳統(tǒng)的神經(jīng)網(wǎng)絡(luò)都是采用全連接的方式第焰,即輸入層到隱含層的神經(jīng)元都是全連接的买优,這樣導(dǎo)致參數(shù)量巨大,使得網(wǎng)絡(luò)訓(xùn)練耗時(shí)甚至難以訓(xùn)練挺举,并且容易過(guò)擬合杀赢,而卷積神經(jīng)網(wǎng)絡(luò)則通過(guò)局部連接、權(quán)值共享等方法避免這一困難湘纵。如下圖所示脂崔。

image.png

對(duì)于一個(gè)200x200的輸入圖像而言,如果下一個(gè)隱含層的神經(jīng)元數(shù)目為10000個(gè)梧喷,采用全連接則有200x200
x10000=400000000個(gè)權(quán)值參數(shù)砌左,如此巨大的參數(shù)幾乎難以訓(xùn)練;而采用局部連接铺敌,隱含層的每個(gè)神經(jīng)元僅與圖像中4x4的局部圖像相連接汇歹,那么此時(shí)的權(quán)值參數(shù)個(gè)數(shù)為4x4x10000=160000,大大減少了參數(shù)的個(gè)數(shù)偿凭。

盡管大大減少了參數(shù)個(gè)數(shù)产弹,但是參數(shù)數(shù)量依然較多。能否再進(jìn)一步減少參數(shù)呢弯囊?方法就是權(quán)值共享取视。一個(gè)卷積層可以有多個(gè)不同的卷積核硝皂,而每一個(gè)卷積核都對(duì)應(yīng)一個(gè)濾波后映射出的新圖像,同一個(gè)新圖像中每一個(gè)像素都來(lái)自完全相同的卷積核作谭,就是卷積核的權(quán)值共享稽物。具體的做法是,在局部連接中隱含層的每一個(gè)神經(jīng)元連接的是一個(gè)4x4的局部圖像折欠,因此有4x4個(gè)權(quán)值參數(shù)贝或,將這4x4個(gè)權(quán)值參數(shù)共享給剩下的神經(jīng)元,也就是說(shuō)隱含層中4x10000個(gè)神經(jīng)元的權(quán)值參數(shù)相同锐秦,那么此時(shí)不管隱含層神經(jīng)元的數(shù)目是多少咪奖,需要訓(xùn)練的參數(shù)就是這4x4個(gè)權(quán)值參數(shù)(也就是卷積核的大小)酱床,如下圖所示羊赵。

image.png

這大概就是卷積神經(jīng)網(wǎng)絡(luò)的神奇之處,盡管只有這么少的參數(shù)扇谣,依舊有出色的性能昧捷。但是,這樣僅提取了圖像的一種特征罐寨,如果要多提取一些特征靡挥,可以增加多個(gè)卷積核,不同的卷積核能夠得到圖像的不同映射下的特征鸯绿,稱為特征映射跋破。如果有100個(gè)卷積核,最終的權(quán)值參數(shù)也僅為100x100=10000個(gè)而已瓶蝴。另外毒返,偏置參數(shù)也是共享的,同一種濾波器共享一個(gè)偏置參數(shù)舷手。

總結(jié)一下饿悬,卷積神經(jīng)網(wǎng)絡(luò)的要點(diǎn)就是卷積層中的局部連接、權(quán)值共享和池化層中下采樣聚霜。局部連接狡恬、權(quán)值共享和下采樣降低了參數(shù)量,使得訓(xùn)練復(fù)雜度大大降低蝎宇,并降低了過(guò)擬合的風(fēng)險(xiǎn)弟劲。同時(shí)還賦予了卷積神經(jīng)網(wǎng)絡(luò)對(duì)于平移、形變姥芥、縮放的某種程度的不變性兔乞,提高了模型的泛化能力。

一般的卷積神經(jīng)網(wǎng)絡(luò)由卷積層、池化層庸追、全連接層霍骄、Softmax層組成,這四者構(gòu)成了常見(jiàn)的卷積神經(jīng)網(wǎng)絡(luò)淡溯。

(1)卷積層读整。卷積層是卷積神經(jīng)網(wǎng)絡(luò)最重要的部分,也是卷積神經(jīng)網(wǎng)絡(luò)得名的緣由咱娶。卷積層中每一個(gè)節(jié)點(diǎn)的輸入是上一層神經(jīng)網(wǎng)絡(luò)中的一小塊米间,卷積層試圖將神經(jīng)網(wǎng)絡(luò)中的每一小塊進(jìn)行更加深入的分析,從而得到抽象程度更高的特征膘侮。

(2)池化層屈糊。池化層的神經(jīng)網(wǎng)絡(luò)不會(huì)改變?nèi)S矩陣的深度,但是它將縮小矩陣的大小琼了。池化層將分辨率較高的圖片轉(zhuǎn)換為分辨率較低的圖片逻锐。

(3)全連接層。經(jīng)過(guò)多輪的卷積層和池化層處理后雕薪,卷積神經(jīng)網(wǎng)絡(luò)一般會(huì)接1到2層全連接層來(lái)給出最后的分類(lèi)結(jié)果昧诱。

(4)Softmax層。Softmax層主要用于分類(lèi)問(wèn)題蹦哼。

1.卷積層

下圖顯示了卷積神經(jīng)網(wǎng)絡(luò)中最重要的部分鳄哭,可稱之為卷積核(kernel)或?yàn)V波器(filter)要糊。在PyTorch文檔中將這個(gè)結(jié)構(gòu)稱為卷積核纲熏,因此這里我們也統(tǒng)稱為卷積核。如圖所示锄俄,卷積核將當(dāng)前層神經(jīng)網(wǎng)絡(luò)上的一個(gè)子節(jié)點(diǎn)矩陣轉(zhuǎn)換為下一層神經(jīng)網(wǎng)絡(luò)上的一個(gè)節(jié)點(diǎn)矩陣局劲。

image.png

在卷積層中,卷積核所處理的節(jié)點(diǎn)矩陣的長(zhǎng)奶赠、寬都是人工指定的鱼填,這個(gè)節(jié)點(diǎn)矩陣的尺寸稱為卷積核的尺寸。卷積核處理的深度和當(dāng)前層神經(jīng)網(wǎng)絡(luò)節(jié)點(diǎn)矩陣的深度是一致的毅戈,即便節(jié)點(diǎn)矩陣是三維的苹丸,卷積核的尺寸只需要指定兩個(gè)維度。一般而言苇经,卷積核的尺寸是3x3和5x5赘理。如上圖中,左邊表示輸入的數(shù)據(jù)扇单,輸入的數(shù)據(jù)尺寸為3x32x32商模,中間表示卷積核,右邊每一個(gè)小圓點(diǎn)表示一個(gè)神經(jīng)元,圖中有5個(gè)神經(jīng)元施流。假設(shè)卷積核尺寸為5x5响疚,卷積層中每個(gè)神經(jīng)元會(huì)有輸入數(shù)據(jù)中3x5x5區(qū)域的權(quán)重,一共75個(gè)權(quán)重瞪醋。這里再次強(qiáng)調(diào)下卷積核的深度必須為3忿晕,和輸入數(shù)據(jù)保持一致。

在卷積層中趟章,還需要說(shuō)明神經(jīng)元的數(shù)量杏糙,以及它們的排列方式、滑動(dòng)步長(zhǎng)和邊界填充蚓土。

(1)卷積核的數(shù)量就是卷積層的輸出深度宏侍,如上圖所示的5個(gè)神經(jīng)元,該參數(shù)是用戶指定的蜀漆,和使用的卷積核數(shù)量一致谅河。
(2)卷積核計(jì)算運(yùn)算時(shí)必須指定滑動(dòng)步長(zhǎng)。比如步長(zhǎng)為1确丢,說(shuō)明卷積核每次移動(dòng)一個(gè)像素點(diǎn)绷耍;步長(zhǎng)為2,卷積核會(huì)滑動(dòng)2個(gè)像素點(diǎn)鲜侥」邮迹滑動(dòng)的操作使得輸出的數(shù)據(jù)變得更少。
(3)邊界填充如果為0描函,可以保證輸入和輸出在空間上尺寸一致崎苗;如果邊界填充大于0,可以確保在進(jìn)行卷積操作時(shí)不損失邊界信息舀寓。

那么胆数,輸出的尺寸最終如何計(jì)算呢?在PyTorch中互墓,可以用一個(gè)公式來(lái)計(jì)算必尼,就是floor((W-F+2P)/S+1)。其中floor表示向下取整操作篡撵,W表示輸入數(shù)據(jù)的大小判莉,F(xiàn)表示卷積層中卷積核的尺寸,S表示步長(zhǎng)育谬,P表示邊界填充0的數(shù)量券盅。比如輸入是5x5,卷積核是3x3斑司,步長(zhǎng)是1渗饮,填充的數(shù)量是0但汞,那么根據(jù)公式就能得到(5 - 3 + 2 x 0)/ 1 + 1 = 3,輸出的空間大小為3 x 3互站;如果步長(zhǎng)為2私蕾,那么(5 - 3 + 2 x 0)/ 2 + 1 = 2,輸出空間的大小為2 x 2胡桃。

以一維空間來(lái)說(shuō)明卷積操作踩叭,如下圖所示。其中翠胰,輸入數(shù)據(jù)大小為5容贝,卷積核的大小為3,填充為1之景,滑動(dòng)步長(zhǎng)分別為1和2的卷積操作結(jié)果如下:

image.png

在PyTorch中斤富,類(lèi)nn.Conv2d()是卷積核模塊。卷積核及其調(diào)用的例子如下:

nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0,dilation=1,groups=1,bias=True)

# 方形卷積核和等長(zhǎng)的步長(zhǎng)
m = nn.Conv2d(16,33,3,stride=2)
# 非方形卷積核锻狗,非等長(zhǎng)的步長(zhǎng)和邊界填充
m = nn.Conv2d(16,33,(3,5),stride=(2,1),padding=(4,2))
# 非方形卷積核满力,非等長(zhǎng)的步長(zhǎng),邊界填充和空間間隔
m = nn.Conv2d(16,33,(3,5),stride=(2,1),padding=(4,2),dilation=(3,1))
input = autograd.Variable(torch.randn(20,16,50,100))
output = m(input)

在nn.Conv2d()中轻纪,in_channels表示輸入數(shù)據(jù)體的深度油额,out_channels表示輸出數(shù)據(jù)體的深度,kernel_size表示卷積核的大小刻帚,stride表示滑動(dòng)步長(zhǎng)潦嘶,padding表示0邊界填充個(gè)數(shù),dilation表示數(shù)據(jù)體的空間間隔崇众,groups表示輸入數(shù)據(jù)體和輸出數(shù)據(jù)體在深度上的關(guān)聯(lián)掂僵,bias表示偏置。

2.池化層

通常會(huì)在卷積層后面插入池化層校摩,其作用是逐漸減低網(wǎng)絡(luò)的空間尺寸看峻,達(dá)到減少網(wǎng)絡(luò)中參數(shù)的數(shù)量阶淘,減少計(jì)算資源使用的目的衙吩,同時(shí)也能有效控制過(guò)擬合。

池化層一般有兩種形式:Max Pooling和Mean Pooling溪窒。下面以Max Pooling來(lái)說(shuō)明池化層的具體內(nèi)容。池化層操作不改變模型的深度澈蚌,對(duì)輸入數(shù)據(jù)在深度上切片作為輸入摹芙,不斷地滑動(dòng)窗口,取這些窗口的最大值作為輸出結(jié)果宛瞄,減少它的空間尺寸浮禾。池化層的效果如圖所示。

image.png

下圖說(shuō)明了池化層的具體計(jì)算過(guò)程。以窗口大小為2盈电,滑動(dòng)步長(zhǎng)為2舉例:每次都是從2x2的窗口中選擇最大的數(shù)值蝴簇,同時(shí)每次滑動(dòng)2個(gè)步長(zhǎng)進(jìn)入新的窗口。

池化層為什么有效匆帚?圖片特征具有局部不變性熬词,也就是說(shuō),即便通過(guò)下采樣也不會(huì)丟失圖片擁有的特征吸重。由于這種特性互拾,可以將圖片縮小再進(jìn)行卷積處理,大大降低卷積計(jì)算的時(shí)間嚎幸。最常用的池化層尺寸是2x2颜矿,滑動(dòng)步長(zhǎng)是2,對(duì)圖像進(jìn)行下采樣嫉晶,將其中75%的信息丟棄或衡,選擇其中最大的保留下來(lái),這樣也能達(dá)到去除一些噪聲信息的目的车遂。

image.png

在PyTorch中封断,池化層包括nn.MaxPool2d和nn.AvgPool2d等。下面以nn.MaxPool2d為例進(jìn)行說(shuō)明:

nn.MaxPool2d(kernel_size,stride=None,padding=0,dilation=1,return_indices=False,ceil_mode=False)

# 方形窗口尺寸為3舶担,等長(zhǎng)滑動(dòng)步長(zhǎng)為2
m = nn.MaxPool2d(3,stride=2)
# 非方形窗口坡疼,非等長(zhǎng)滑動(dòng)步長(zhǎng)
m = nn.MaxPool2d((3,2),stride=(2,1))
input = autograd.Variable(torch.randn(20,16,50,32))
output = m(input)

在nn.MaxPool2d中,kernel_size衣陶,stride柄瑰,padding,dilation參數(shù)在nn.Conv2d中已經(jīng)解釋過(guò)剪况,return_indices表示是否返回最大值所處的下標(biāo)教沾,ceil_mode表示使用方格代替層結(jié)構(gòu)。

3.經(jīng)典卷積神經(jīng)網(wǎng)絡(luò)

這里介紹三種經(jīng)典的卷積神經(jīng)網(wǎng)絡(luò):LeNet译断,AlexNet授翻,VGGNet。這三種卷積神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu)不算特別復(fù)雜孙咪,有興趣的也可以了解GoogleNet和ResNet堪唐。

(1)LeNet

LeNet具體指的是LeNet-5。LeNet-5模型是Yann LeCun教授于1998年提出的翎蹈,它是第一個(gè)成功應(yīng)用于數(shù)字識(shí)別的卷積神經(jīng)網(wǎng)絡(luò)淮菠。在MNIST數(shù)據(jù)集上,LeNet-5模型可以達(dá)到約99.2%的正確率荤堪。LeNet-5模型總共有7層合陵,包括2個(gè)卷積層枢赔,2個(gè)池化層,2個(gè)全連接層和1個(gè)輸出層拥知。下圖是LeNet-5的模型架構(gòu)糠爬。

image.png

LeNet-5在PyTorch中的實(shí)現(xiàn)如下:

class LeNet(nn.Module):
    def __init__(self):
        super(LeNet, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)
    def forward(self, x):
        out = F.relu(self.conv1(x))
        out = F.max_pool2d(out, 2)
        out = F.relu(self.conv2d(out))
        out = F.max_pool2d(out, 2)
        out = out.view(out.size(0), -1)
        out = F.relu(self.fc1(out))
        out = F.relu(self.fc2(out))
        out = self.fc3(out)
        return out

(2)AlexNet

2012年,Hilton的學(xué)生Alex Krizhevsky提出了卷積神經(jīng)網(wǎng)絡(luò)模型AlexNet举庶。AlexNet在卷積神經(jīng)網(wǎng)絡(luò)上成功運(yùn)用了ReLU执隧,Dropout和LRN等技巧。在ImageNet的競(jìng)賽上户侥,AlexNet以領(lǐng)先第二名10%的準(zhǔn)確率而奪得冠軍镀琉,成功地展示了深度學(xué)習(xí)的威力。AlexNet的網(wǎng)絡(luò)結(jié)構(gòu)如下圖所示蕊唐。

image.png

上圖看起來(lái)有點(diǎn)復(fù)雜屋摔,這是由于當(dāng)時(shí)GPU計(jì)算能力不強(qiáng),AlexNet使用了兩個(gè)GPU并行計(jì)算替梨,現(xiàn)在可以用一個(gè)GPU替換钓试。以單個(gè)GPU的AlexNet模型為例,包括5個(gè)卷積層副瀑,3個(gè)池化層弓熏,3個(gè)全連接層。其中卷積層和全連接層包含有ReLU層糠睡,在全連接層中還有Dropout層挽鞠。具體參數(shù)配置如下圖所示:

image.png

具體的參數(shù)配置可以查看PyTorch源代碼。下面給出PyTorch實(shí)現(xiàn)AlexNet模型的卷積神經(jīng)網(wǎng)絡(luò)程序狈孔。

class AlexNet(nn.module):
    def __init__(self, num_classes):
        super(AlexNet, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(64, 256, kernel_size=5, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(192, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
        )
        self.classifier = nn.Sequential(
            nn.Dropout(),
            nn.Linear(256 * 6 * 6, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes),
        )
        def forward(self, x):
            x = self.features(x)
            x = x.view(x.size(0), 256 * 6 * 6)
            x = self.classifier(x)
            return x

(3)VGGNet

VGGNet是牛津大學(xué)計(jì)算機(jī)視覺(jué)組和Google DeepMind公司的研究人員一起研發(fā)的卷積神經(jīng)網(wǎng)絡(luò)信认。通過(guò)堆疊3x3的小型卷積核和2x2的最大池化層,VGGNet成功構(gòu)筑了深達(dá)19層的卷積神經(jīng)網(wǎng)絡(luò)均抽。VGGNet取得了2014年ImageNet比賽的第二名嫁赏,由于拓展性強(qiáng),遷移到其他圖片數(shù)據(jù)上的泛化性比較好油挥,因此可用作遷移學(xué)習(xí)潦蝇。下圖顯示的是VGGNet各個(gè)級(jí)別的的網(wǎng)絡(luò)結(jié)構(gòu)圖。雖然從A到E每一級(jí)網(wǎng)絡(luò)逐層變深喘漏,但是網(wǎng)絡(luò)的參數(shù)量并沒(méi)有增長(zhǎng)很多护蝶,因?yàn)閰?shù)量主要都消耗在最后3個(gè)全連接層华烟。前面的卷積層參數(shù)很深翩迈,參數(shù)量并不是很多,但是在訓(xùn)練時(shí)計(jì)算量大盔夜,比較耗時(shí)负饲。D和E模型就是VGGNet-16和VGGNet-19堤魁。

image.png

下面給出PyTorch實(shí)現(xiàn)VGGNet模型的卷積神經(jīng)網(wǎng)絡(luò)程序。

cfg = {
    'VGG11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'VGG13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'VGG16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
    'VGG19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}
class VGG(nn.Module):
    def __init__(self, vgg_name):
        super(VGG, self).__init__()
        self.features = self._make_layers(sfg[vgg_name])
        self.classifier = nn.Linear(512, 10)
    def forward(self, x):
        out = self.features(x)
        out = out.view(out.size(0), -1)
        out = self.classifier(out)
        return out
    def _make_layers(self, cfg):
        layers = []
        ln_channels = 3
        for x in cfg:
            if x == 'M':
                layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
            else:
                layers += [nn.Conv2d(in_channels, x, kernel_size=3, padding=1),
                           nn.BatchNorm2d(x),
                           nn.ReLU(inplace=True)]
                in_channels = x
            layers += [nn.AvgPool2d(kernel_size=1, stride=1)]
        return nn.Sequential(*layers) 

3.MNIST數(shù)據(jù)集上的卷積神經(jīng)網(wǎng)絡(luò)的實(shí)現(xiàn)

本節(jié)介紹如何使用PyTorch實(shí)現(xiàn)一個(gè)簡(jiǎn)單的卷積神經(jīng)網(wǎng)絡(luò)返十,使用的數(shù)據(jù)集是MNIST妥泉,預(yù)期可以達(dá)到97%的準(zhǔn)確率。該神經(jīng)網(wǎng)絡(luò)由2個(gè)卷積層和3個(gè)全連接層構(gòu)成洞坑。通過(guò)這個(gè)例子我們可以掌握設(shè)計(jì)卷積神經(jīng)網(wǎng)絡(luò)的特征以及參數(shù)的配置盲链。

1.配置庫(kù)和配置參數(shù)
import torch
from torch import nn,optim
import torch.nn.functional as F
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import transforms,datasets

# 配置參數(shù)
torch.manual_seed(1)    # 設(shè)置隨機(jī)種子,確保結(jié)果可重復(fù)
batch_size = 128        # 批處理大小
learning_rate = 1e-2    # 學(xué)習(xí)率
num_epoches = 10        # 訓(xùn)練次數(shù)
2.加載MNIST數(shù)據(jù)集
# 下載訓(xùn)練集MNIST(手寫(xiě)數(shù)字?jǐn)?shù)據(jù))
train_dataset = datasets.MNIST(root='./data',                    # 數(shù)據(jù)集保存路徑
                               train=True,                       # 訓(xùn)練數(shù)據(jù)集
                               transform=transforms.ToTensor(),  # 轉(zhuǎn)為T(mén)ensor
                               download=True)                    # 下載數(shù)據(jù)

test_dataset = datasets.MNIST(root='./data',
                               train=False,                      # 測(cè)試數(shù)據(jù)集
                               transform=transforms.ToTensor())

# 訓(xùn)練數(shù)據(jù)的加載方式:每次從train_dataset中隨機(jī)(shuffle=True)選擇batch_size個(gè)樣本作為一個(gè)批次返回迟杂,因此所選擇的數(shù)據(jù)可能會(huì)重復(fù)
train_loader = DataLoader(train_dataset,batch_size=batch_size,shuffle=True)
# 測(cè)試數(shù)據(jù)的加載方式:每次從test_dataset中選擇batch_size個(gè)不同的樣本作為一個(gè)批次返回刽沾,要覆蓋到所有測(cè)試樣本
test_loader = DataLoader(test_dataset,batch_size=batch_size,shuffle=False)
3.創(chuàng)建CNN模型
class Cnn(nn.Module):
    def __init__(self,in_dim,n_class): # 28*28*1
        super(Cnn,self).__init__()
        # 定義卷積層
        self.conv = nn.Sequential(
            nn.Conv2d(in_dim,6,3,stride=1,padding=1),  # 28*28
            nn.ReLU(True),
            nn.MaxPool2d(2,2),  # 14*14
            nn.Conv2d(6,16,5,stride=1,padding=0),  # 10*10*16
            nn.ReLU(True),
            nn.MaxPool2d(2,2))   # 5*5*16
        # 定義全連接層
        self.fc = nn.Sequential(
            nn.Linear(400,120),  # 400 = 5*5*16
            nn.Linear(120,84),   
            nn.Linear(84,n_class))
    # 前向傳播
    def forward(self,x):
        out = self.conv(x)
        out = out.view(out.size(0),400)  # 400 = 5*5*16
        out = self.fc(out)
        return out
    
model = Cnn(1,10)   # 圖片大小是28*28,10是數(shù)據(jù)的種類(lèi)

print(model)

輸出如下:

Cnn(
  (conv): Sequential(
    (0): Conv2d(1, 6, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU(inplace)
    (2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
    (4): ReLU(inplace)
    (5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (fc): Sequential(
    (0): Linear(in_features=400, out_features=120, bias=True)
    (1): Linear(in_features=120, out_features=84, bias=True)
    (2): Linear(in_features=84, out_features=10, bias=True)
  )
)
4.訓(xùn)練模型
# 定義損失:交叉熵?fù)p失
criterion = nn.CrossEntropyLoss() 
# 定義優(yōu)化器:隨機(jī)梯度下降SGD
optimizer = optim.SGD(model.parameters(),lr=learning_rate)

# 共訓(xùn)練num_epoches輪
for epoch in range(num_epoches): 
    running_loss = 0.0  # 當(dāng)前損失 
    running_acc = 0.0   # 當(dāng)前準(zhǔn)確度
    # 訓(xùn)練集:60000,批處理:128
    for i,data in enumerate(train_loader,1): 
        img,label = data 
        img = Variable(img) 
        label = Variable(label) 
        out = model(img)                              # 前向傳播
        loss = criterion(out,label)                   # 這個(gè)損失是當(dāng)前批次的平均損失
        running_loss += loss.item() * label.size(0)   # 累計(jì)損失大小排拷,乘積表示當(dāng)前批次的總損失
        _,pred=torch.max(out,1)                       # 多分類(lèi)問(wèn)題的類(lèi)別取概率最大的類(lèi)別
        num_correct = (pred == label).sum()           # 當(dāng)前批次預(yù)測(cè)正確的個(gè)數(shù)
        running_acc += num_correct.item()             # 累計(jì)預(yù)測(cè)正確的個(gè)數(shù)
        optimizer.zero_grad()                         # 梯度清零
        loss.backward()                               # 誤差反向傳播
        optimizer.step()                              # 梯度更新
    # 每訓(xùn)練一輪侧漓,打印一次信息
    print('Train {} epoch, Loss: {:.6f}, Acc: {:.6f}'.format(epoch+1,running_loss/(len(train_dataset)),running_acc/(len(train_dataset))))

輸出如下:

Train 1 epoch, Loss: 1.366457, Acc: 0.626950
Train 2 epoch, Loss: 0.411538, Acc: 0.877867
Train 3 epoch, Loss: 0.294756, Acc: 0.911117
Train 4 epoch, Loss: 0.232250, Acc: 0.930517
Train 5 epoch, Loss: 0.188249, Acc: 0.943533
Train 6 epoch, Loss: 0.159317, Acc: 0.952500
Train 7 epoch, Loss: 0.139562, Acc: 0.957750
Train 8 epoch, Loss: 0.125770, Acc: 0.962000
Train 9 epoch, Loss: 0.115610, Acc: 0.964783
Train 10 epoch, Loss: 0.108309, Acc: 0.966983
5.評(píng)估模型
# 由于訓(xùn)練和測(cè)試的BatchNorm,Drop等配置不同监氢,所以需要說(shuō)明是模型評(píng)估
model.eval()
eval_loss = 0
eval_acc = 0
for data in test_loader:
    img,label = data
    img = Variable(img)
    # 測(cè)試不需要 label = Variable(label)
    out = model(img)                             # 前向傳播
    loss = criterion(out,label)                  # 當(dāng)前批次的平均損失
    eval_loss += loss.item() * label.size(0)     # 累計(jì)損失
    _,pred = torch.max(out,1)                    # 多分類(lèi)問(wèn)題的類(lèi)別取概率最大的類(lèi)別
    num_correct = (pred==label).sum()            # 當(dāng)前批次預(yù)測(cè)正確的個(gè)數(shù)
    eval_acc += num_correct.item()               # 累計(jì)預(yù)測(cè)正確的個(gè)數(shù)

# 打印測(cè)試集上的評(píng)估結(jié)果 
print('Test Loss: {:.6f}, Acc: {:.6f}'.format(eval_loss/(len(test_dataset)),eval_acc * 1.0 / (len(test_dataset))))

輸出如下:

Test Loss: 0.091677, Acc: 0.972800
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末布蔗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子浪腐,更是在濱河造成了極大的恐慌纵揍,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件议街,死亡現(xiàn)場(chǎng)離奇詭異骡男,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)傍睹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)隔盛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人拾稳,你說(shuō)我怎么就攤上這事吮炕。” “怎么了访得?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵龙亲,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我悍抑,道長(zhǎng)鳄炉,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任搜骡,我火速辦了婚禮拂盯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘记靡。我一直安慰自己谈竿,他們只是感情好团驱,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著空凸,像睡著了一般嚎花。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上呀洲,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天紊选,我揣著相機(jī)與錄音,去河邊找鬼道逗。 笑死丛楚,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的憔辫。 我是一名探鬼主播趣些,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼贰您!你這毒婦竟也來(lái)了坏平?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤锦亦,失蹤者是張志新(化名)和其女友劉穎舶替,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體杠园,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡顾瞪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了抛蚁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陈醒。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖瞧甩,靈堂內(nèi)的尸體忽然破棺而出钉跷,到底是詐尸還是另有隱情,我是刑警寧澤肚逸,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布爷辙,位于F島的核電站,受9級(jí)特大地震影響朦促,放射性物質(zhì)發(fā)生泄漏膝晾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一务冕、第九天 我趴在偏房一處隱蔽的房頂上張望血当。 院中可真熱鬧,春花似錦、人聲如沸歹颓。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)巍扛。三九已至,卻和暖如春乏德,著一層夾襖步出監(jiān)牢的瞬間撤奸,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工喊括, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留胧瓜,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓郑什,卻偏偏與公主長(zhǎng)得像府喳,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蘑拯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容