手寫(xiě)一個(gè)全連接神經(jīng)網(wǎng)絡(luò)用于數(shù)字識(shí)別

簡(jiǎn)介

本文的目的是不借助任何深度學(xué)習(xí)算法庫(kù)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的3層全連接神經(jīng)網(wǎng)絡(luò)(FCN),并將其用于MNIST手寫(xiě)數(shù)字識(shí)別任務(wù)虑绵。
文章將依次介紹感知器模型挤庇、S型神經(jīng)元寥粹、全連接神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)、梯度下降等內(nèi)容窝趣,并在最后給出完整代碼推正。為了避免打擊讀者的信心,這里可以提前告訴大家缸濒,最后的代碼除掉注釋在100行之內(nèi)足丢。是不是覺(jué)得很興奮?我們不借助其他神經(jīng)網(wǎng)絡(luò)庫(kù)庇配,僅僅使用少于100行的代碼就可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的全連接神經(jīng)網(wǎng)絡(luò)斩跌,并且它在MNIST數(shù)據(jù)集上的識(shí)別正確率可以達(dá)到96%以上。
接下來(lái)就讓我們開(kāi)始進(jìn)入正題吧讨永!

神經(jīng)網(wǎng)絡(luò)概覽

我們要設(shè)計(jì)的全連接神經(jīng)網(wǎng)絡(luò)的具體功能是滔驶,給定一張手寫(xiě)數(shù)字圖片,神經(jīng)網(wǎng)絡(luò)要能識(shí)別出來(lái)這個(gè)數(shù)字是幾卿闹。

聽(tīng)起來(lái)像很簡(jiǎn)單的任務(wù)是嗎揭糕?但是如果使用傳統(tǒng)的計(jì)算機(jī)程序來(lái)識(shí)別諸如上圖中的數(shù)字,就會(huì)明顯感受到視覺(jué)模式識(shí)別的困難锻霎。因?yàn)橐酝覀兙帉?xiě)的計(jì)算機(jī)程序往往擅長(zhǎng)處理各種提前制定好的規(guī)則著角。比如要讀取一張圖片,我們會(huì)先判斷圖片的格式旋恼,根據(jù)具體的格式解碼出圖像內(nèi)容再顯示出來(lái)吏口。而對(duì)于數(shù)字識(shí)別這個(gè)任務(wù),規(guī)則變得十分模糊與復(fù)雜冰更,如果我們?cè)囍屵@些識(shí)別規(guī)則變得越發(fā)精準(zhǔn)的時(shí)候产徊,就會(huì)很快陷入各種混亂的場(chǎng)景和異常情況中,這并不是一條可行的道路蜀细。

傳統(tǒng)的方式行不通舟铜,那我們?nèi)绾蝸?lái)解決這個(gè)問(wèn)題呢?首先回想一下小時(shí)候我們是怎么學(xué)習(xí)認(rèn)數(shù)字的奠衔,老師會(huì)把數(shù)字0~9挨個(gè)給我們看谆刨,然后告訴我們它們分別是幾。如果我們不小心認(rèn)錯(cuò)了归斤,老師會(huì)即時(shí)糾正我們痊夭,經(jīng)過(guò)一段時(shí)間的練習(xí)和記憶之后,我們就可以認(rèn)得數(shù)字了脏里。
神經(jīng)網(wǎng)絡(luò)也有異曲同工之妙她我,其思想是首先獲取大量的手寫(xiě)數(shù)字去建立一個(gè)訓(xùn)練數(shù)據(jù)集,然后開(kāi)發(fā)一個(gè)可以從這些訓(xùn)練數(shù)據(jù)中進(jìn)行學(xué)習(xí)的系統(tǒng),接著使用訓(xùn)練數(shù)據(jù)集去訓(xùn)練這個(gè)系統(tǒng)番舆,最后系統(tǒng)便能夠識(shí)別手寫(xiě)數(shù)字了根吁。換言之,神經(jīng)網(wǎng)絡(luò)使用訓(xùn)練樣本來(lái)自動(dòng)推斷出識(shí)別手寫(xiě)數(shù)字的規(guī)則合蔽。

根據(jù)上面的描述,可以歸納出構(gòu)成全連接網(wǎng)絡(luò)的幾個(gè)要素:

  • 訓(xùn)練數(shù)據(jù)集(MNIST)
  • 可以從訓(xùn)練樣本中自學(xué)習(xí)的系統(tǒng)(全連接網(wǎng)絡(luò)結(jié)構(gòu))
  • 訓(xùn)練系統(tǒng)的方法(梯度下降+反向傳播)

接下來(lái)會(huì)分別從這幾個(gè)方面來(lái)介紹介返。

MNIST

MNIST是一個(gè)手寫(xiě)數(shù)字集合拴事,包含了60000張測(cè)試圖片和10000張測(cè)試圖片,每張圖片分辨率為28x28圣蝎,像素點(diǎn)數(shù)值取值范圍0~255刃宵。MNIST 的名字來(lái)源于NIST——美國(guó)國(guó)家標(biāo)準(zhǔn)與技術(shù)研究所——收集的兩個(gè)數(shù)據(jù)集改進(jìn)后的子集,關(guān)于MNIST數(shù)據(jù)集更多信息可以自行搜索徘公。
MNIST數(shù)據(jù)集在機(jī)器學(xué)習(xí)牲证、深度學(xué)習(xí)領(lǐng)域的地位類(lèi)似于各類(lèi)編程語(yǔ)言里學(xué)的”Hello World“程序。

下面是MNIST數(shù)據(jù)集中的部分圖片:

我們?cè)O(shè)計(jì)的神經(jīng)網(wǎng)絡(luò)并不會(huì)直接使用原始圖像关面,而是會(huì)將其轉(zhuǎn)換成向量的形式坦袍,即每張?jiān)紙D像可以看做是一個(gè)28x28=784維的向量,向量中的每一維分別代表原始圖像中對(duì)應(yīng)像素點(diǎn)的灰度值等太。


全連接網(wǎng)絡(luò)結(jié)構(gòu)

感知器模型

在介紹全連接網(wǎng)絡(luò)結(jié)構(gòu)之前捂齐,我們先了解一下感知器模型。雖然我們最后的神經(jīng)網(wǎng)絡(luò)中并不包含這個(gè)模型缩抡,但是下面要介紹到的S型神經(jīng)元是由感知器模型演變過(guò)去的奠宜,因此值得花點(diǎn)時(shí)間來(lái)理解下感知器。

下圖是一個(gè)簡(jiǎn)單的感知器模型:
感知器模型

上圖中的感知器有3個(gè)輸入瞻想,x_1, x_2,x_3压真。通常可以有更多或者更少輸入蘑险。緊挨著的3條邊分別代表3個(gè)權(quán)重滴肿,w_1,w_2,w_3。圓圈里面是一個(gè)Step函數(shù)漠其,Step函數(shù)的圖像如下:

階躍函數(shù)

因此整個(gè)感知器模型的代數(shù)定義如下:

直觀地解釋就是嘴高,對(duì)于每一個(gè)輸入x_i,將其乘上對(duì)應(yīng)的權(quán)值w_i和屎,再累加起來(lái)得到一個(gè)中間結(jié)果\sum w_jx_j拴驮,再將\sum w_jx_j通過(guò)一個(gè)階躍函數(shù),如果\sum w_jx_j大于0柴信,則輸出1套啤;反之,輸出0。這就是一個(gè)感知器所要做的全部事情潜沦。
這是一個(gè)基本的數(shù)學(xué)模型萄涯,你可以將感知器看做是一種可以根據(jù)權(quán)重來(lái)做出決定的機(jī)器。我們可以通過(guò)手動(dòng)改變模型中的權(quán)重以及圓圈中的函數(shù)來(lái)得到不同的感知器模型唆鸡,它們對(duì)同一輸入可以做出不同的決策涝影。
我們將上述的簡(jiǎn)單感知器進(jìn)行組合,得到下面稍微復(fù)雜點(diǎn)的多層感知器模型:

多層感知器

在這個(gè)網(wǎng)絡(luò)中争占,第一列感知器——我們稱(chēng)其為第一層感知器——通過(guò)權(quán)衡輸入依據(jù)做出三個(gè)非常簡(jiǎn)單的決定燃逻。那第二層的感知器呢?每一個(gè)都在權(quán)衡第一層的決策結(jié)果并做出決定。以這種方式臂痕,一個(gè)第二層中的感知器可以比第一層中的做出更復(fù)雜和抽象的決策伯襟。在第三層中的感知器甚至能進(jìn)行更復(fù)雜的決策。以這種方式握童,一個(gè)多層的感知器網(wǎng)絡(luò)可以從事復(fù)雜巧妙的決策姆怪。

那這個(gè)模型跟我們的手寫(xiě)數(shù)字識(shí)別有什么關(guān)系呢?

換個(gè)角度來(lái)思考這個(gè)問(wèn)題澡绩,既然感知器的輸出是0或者1稽揭,那是不是意味著我們其實(shí)就可以用它來(lái)做單一手寫(xiě)數(shù)字圖像分類(lèi)呢?比如判斷一張圖像中的數(shù)字是不是”9“肥卡。如果圖片顯示的是“9”淀衣,就輸出1,否則輸出0召调。很顯然膨桥,我們的感知器一開(kāi)始并不知道如何判斷圖像是不是包含”9“,但是別忘了唠叛,我們可以修改權(quán)值和圓圈中的激活函數(shù)來(lái)讓感知器做出正確的分類(lèi)只嚣。

S型神經(jīng)元

如果我們對(duì)權(quán)值或者激活函數(shù)中的偏置做出的微小改動(dòng)就能夠引起輸出的微小變化,那我們就可以利用這一事實(shí)來(lái)修改權(quán)值和激活函數(shù)艺沼,讓網(wǎng)絡(luò)能夠表現(xiàn)得像我們想要的那樣册舞。假設(shè)網(wǎng)絡(luò)錯(cuò)誤地把一個(gè)“9”的圖像分類(lèi)為“8”,我們能夠計(jì)算出怎么對(duì)權(quán)重和偏置做些小的改動(dòng)障般,這樣網(wǎng)絡(luò)能夠接近于把圖像分類(lèi)為“9”调鲸。然后我們要重復(fù)這個(gè)工作,反復(fù)改動(dòng)權(quán)重和偏置來(lái)產(chǎn)生更好的輸出挽荡,此時(shí)網(wǎng)絡(luò)其實(shí)就是在自我學(xué)習(xí)藐石。
但是我們目前的感知器網(wǎng)絡(luò)無(wú)法做到這一點(diǎn)。因?yàn)楦兄鞯妮敵鲆词?定拟,要么是1于微。對(duì)權(quán)重和偏置的微小改動(dòng)可能會(huì)讓感知器的輸出發(fā)生大反轉(zhuǎn),比如從0變成1。在多層感知器模型里面株依,這樣的反轉(zhuǎn)可能使得與此感知器連接的其余網(wǎng)絡(luò)的行為完全改變驱证。因此,也許”9“可以被正確分類(lèi)恋腕,網(wǎng)絡(luò)在其它圖像上的行為很可能以一些很難控制的方式被完全改變抹锄。這使得逐步修改權(quán)重和偏置來(lái) 讓網(wǎng)絡(luò)接近期望行為變得困難。

因此我們引入一種稱(chēng)為S型神經(jīng)元的新的人工神經(jīng)元來(lái)克服這個(gè)問(wèn)題荠藤,它與感知器類(lèi)似祈远,但是對(duì)權(quán)值和激活函數(shù)的偏置的微小改動(dòng)只會(huì)引起輸出的微笑改變,而不會(huì)像感知器那樣完全反轉(zhuǎn)商源。

S型神經(jīng)元模型如下:
S型神經(jīng)元

可以看到它與感知器模型非常類(lèi)似,S 型神經(jīng)元有多個(gè)輸入谋减,x_i,x_2,...牡彻。這些輸入可以是0和1中的任意值。同樣S型神經(jīng)元對(duì)每個(gè)輸入都有權(quán)重出爹,w_1,w_2,...庄吼,和一個(gè)總的偏置b。但是它的輸出不再是0或者1严就,而是Sigmoid(w*x+b)总寻,這里的Sigmoid也寫(xiě)成\sigma\sigma的定義如下:

故上述模型的最終輸出為:

\sigma函數(shù)的圖像如下:

Sigmoid 函數(shù)

為了理解和感知器模型的相似性梢为,假設(shè)z=w*x+b是一個(gè)很大的正數(shù)渐行,那么e^{-z} \approx 0, 而\sigma(z) \approx 1撕氧。即當(dāng)z=w*x+b很大并且為正匪蝙,S 型神經(jīng)元的輸出近似為 1,正好和感知器一樣宙枷。 相反地粟害,假設(shè)z=w*x+b是一個(gè)很大的負(fù)數(shù)蕴忆。那么e^{-z} \to \infty, \sigma(z) \approx 0。所以當(dāng)z=w*x+b是一個(gè)很大的負(fù)數(shù)悲幅,S 型神經(jīng)元的行為也非常近似一個(gè)感知器套鹅。只有在w*x+b取中間值時(shí),和感知器模型有比較大的偏離汰具。

我們應(yīng)該如何解釋一個(gè)S型神經(jīng)元的輸出呢? 很明顯感知器和 S 型神經(jīng)元之間一個(gè)很大的不同是S型神經(jīng)元不僅僅輸出0或1卓鹿,它可以輸出0和1之間的任何實(shí)數(shù)。假設(shè)我們希望網(wǎng)絡(luò)的輸出是表示”輸入圖像是一個(gè)9“或”輸入圖像不是一個(gè)9“的話留荔,此時(shí)網(wǎng)絡(luò)輸出0或者1會(huì)更容易表示這種情況减牺。此時(shí)我們可以采用以下約定,即以0.5為分界點(diǎn),將S型神經(jīng)元的輸出轉(zhuǎn)換成0或者1拔疚,具體表述如下:

一個(gè)簡(jiǎn)單的分類(lèi)手寫(xiě)數(shù)字的網(wǎng)絡(luò)

在理解基礎(chǔ)神經(jīng)元模型之后肥隆,我們就可以構(gòu)建較為復(fù)雜的神經(jīng)網(wǎng)絡(luò)。本文中我們將使用一個(gè)3層的神經(jīng)網(wǎng)絡(luò)來(lái)識(shí)別單個(gè)數(shù)字稚失,網(wǎng)絡(luò)結(jié)構(gòu)如下:
3層全連接網(wǎng)絡(luò)

網(wǎng)絡(luò)的第一層是輸入層栋艳,由于我們的輸入數(shù)據(jù)是28x28的圖像,將其攤平成28x28=784維的向量句各,所以輸入層包含有784個(gè)神經(jīng)元吸占。為了簡(jiǎn)化,上圖中忽略了 784 中大部分的輸入神經(jīng)元凿宾。輸入像素是灰度級(jí)的矾屯, 值為 0.0 表示白色,值為 1.0 表示黑色初厚,中間數(shù)值表示逐漸暗淡的灰色件蚕。
網(wǎng)絡(luò)的第二層是一個(gè)隱藏層,僅僅包含了15個(gè)神經(jīng)元产禾,當(dāng)然我們其實(shí)可以根據(jù)需要?jiǎng)討B(tài)調(diào)整這個(gè)參數(shù)以觀察不同隱藏層神經(jīng)元個(gè)數(shù)對(duì)最終結(jié)果的影響排作。
網(wǎng)絡(luò)的第三層是輸出層,包含有 10 個(gè)神經(jīng)元亚情。如果第一個(gè)神經(jīng)元激活妄痪,即輸出 ≈ 1,那么表明網(wǎng)絡(luò)認(rèn)為數(shù)字是一個(gè)”0“楞件。如果第二個(gè)神經(jīng)元激活衫生,就表明網(wǎng)絡(luò)認(rèn)為數(shù)字是一個(gè)”1“。依此類(lèi)推土浸。更確切地說(shuō)障簿, 我們把輸出神經(jīng)元的輸出賦予編號(hào) 0 到 9,并計(jì)算出哪個(gè)神經(jīng)元有最高的激活值栅迄。比如站故,如果編號(hào)為 6 的神經(jīng)元激活,那么我們的網(wǎng)絡(luò)會(huì)猜到輸入的數(shù)字是”6“毅舆,其它神經(jīng)元相同西篓。

梯度下降

有了訓(xùn)練數(shù)據(jù)集和定義好的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)之后,它將怎樣學(xué)習(xí)識(shí)別數(shù)字呢憋活?
對(duì)于上圖中的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)岂津,假設(shè)我們隨機(jī)初始化各個(gè)神經(jīng)元的權(quán)值,并且將手寫(xiě)數(shù)字圖片”9“傳入到網(wǎng)絡(luò)中悦即,那么可以預(yù)見(jiàn)網(wǎng)絡(luò)大概率并不會(huì)得到正確答案吮成,但是我們可以通過(guò)不斷調(diào)整權(quán)值來(lái)讓它輸出正確的結(jié)果橱乱。對(duì)于簡(jiǎn)單的模型也許這樣做是可行的,因?yàn)楫吘箍梢哉{(diào)整的權(quán)重?cái)?shù)據(jù)很少粱甫,但是對(duì)于我們定義的網(wǎng)絡(luò)泳叠,它其中包含了784x15+15x10+15+10 = 11935個(gè)參數(shù),手動(dòng)修改簡(jiǎn)直是一場(chǎng)噩夢(mèng)茶宵。

因此我們希望能夠設(shè)計(jì)出一種學(xué)習(xí)算法危纫,幫助我們自動(dòng)調(diào)整感知器中的權(quán)重和偏置,以至于網(wǎng)絡(luò)的輸出能夠擬合所有的訓(xùn)練輸入乌庶。為了量化這個(gè)目標(biāo)种蝶,我們定義一個(gè)損失函數(shù):

這里的w代表的是網(wǎng)絡(luò)中所有權(quán)重的集合,b是所有的偏置瞒大,n是訓(xùn)練輸入數(shù)據(jù)的個(gè)數(shù)螃征,y(x)是輸入x對(duì)應(yīng)的標(biāo)簽,a是表示當(dāng)輸入為x時(shí)神經(jīng)網(wǎng)絡(luò)輸出的向量透敌,求和則是在總的訓(xùn)練輸入x上進(jìn)行的盯滚。
我們把C稱(chēng)之為二次代價(jià)函數(shù),也稱(chēng)均方誤差或者M(jìn)SE拙泽。
通過(guò)觀察可以發(fā)現(xiàn),損失函數(shù)C(w,b)的值是非負(fù)的裸燎,因?yàn)榍蠛凸街械拿恳豁?xiàng)都是非負(fù)的顾瞻。如果代價(jià)函數(shù) C(w,b)的值相當(dāng)小,即 C(w,b) \approx 0德绿,那么意味著荷荤,對(duì)于所有的訓(xùn)練輸入xy(x) 接近于神經(jīng)網(wǎng)絡(luò)輸出a移稳。相反蕴纳, 當(dāng)C(w, b)很大時(shí)就不怎么好了,那意味著對(duì)于大量地輸入个粱,y(x)與輸出a相差很大古毛。因此我們的訓(xùn)練算法的目的,是要最小化代價(jià)函數(shù)C(w, b)都许,使C(w, b) ≈ 0稻薇。
現(xiàn)在我們忘掉之前講過(guò)的各種神經(jīng)元、網(wǎng)絡(luò)結(jié)構(gòu)胶征、MNIST等等塞椎,把注意力集中在一個(gè)問(wèn)題上,那就是如何最小化一個(gè)給定的多元函數(shù)睛低?我們使用一種被稱(chēng)之為梯度下降的技術(shù)來(lái)解決這樣的最小化問(wèn)題案狠。
假設(shè)我們要最小化某些函數(shù)服傍,如C(v)。它可以是任意的多元實(shí)值函數(shù)骂铁,v = v_1, v_2, . . .吹零。 注意我們用 v 代替了 wb 以強(qiáng)調(diào)它可能是任意的函數(shù),這里我們以只有兩個(gè)變量v_1,v_2的二元函數(shù)C(v_1,v_2)舉例从铲,圖像如下:

這里你也許會(huì)問(wèn)為什么拿二元函數(shù)舉例瘪校,主要原因是因?yàn)榭梢詫⑵淇梢暬绻瑑蓚€(gè)自變量以上的話名段,我們就無(wú)法畫(huà)出它對(duì)應(yīng)的圖像了阱扬。


從圖中可以一眼看到C的最小值在(0,0)處取得,但是通常函數(shù)C是一個(gè)復(fù)雜的多元函數(shù)伸辟,遠(yuǎn)不止2個(gè)自變量麻惶,因此看一眼就能找到最小值是不太現(xiàn)實(shí)的,此時(shí)梯度下降法就可以發(fā)揮用處了信夫。

梯度下降法的基本思想可以類(lèi)比為一個(gè)下山的過(guò)程窃蹋。假設(shè)這樣一個(gè)場(chǎng)景:一個(gè)人被困在山上,需要從山上下來(lái)(i.e. 找到山的最低點(diǎn)静稻,也就是山谷)警没。但此時(shí)山上的濃霧很大,導(dǎo)致可視度很低振湾。因此杀迹,下山的路徑就無(wú)法確定,他必須利用自己周?chē)男畔⑷フ业较律降穆窂窖禾隆_@個(gè)時(shí)候树酪,他就可以利用梯度下降算法來(lái)幫助自己下山。具體來(lái)說(shuō)就是大州,以他當(dāng)前的所處的位置為基準(zhǔn)续语,尋找這個(gè)位置最陡峭的地方,然后朝著山的高度下降的地方走厦画,同理疮茄,如果我們的目標(biāo)是上山,也就是爬到山頂根暑,那么此時(shí)應(yīng)該是朝著最陡峭的方向往上走娃豹。然后每走一段距離,都反復(fù)采用同一個(gè)方法购裙,最后就能成功的抵達(dá)山谷懂版。



我們同時(shí)可以假設(shè)這座山最陡峭的地方是無(wú)法通過(guò)肉眼立馬觀察出來(lái)的,而是需要一個(gè)復(fù)雜的工具來(lái)測(cè)量躏率,同時(shí)躯畴,這個(gè)人此時(shí)正好擁有測(cè)量出最陡峭方向的能力民鼓。所以,此人每走一段距離蓬抄,都需要一段時(shí)間來(lái)測(cè)量所在位置最陡峭的方向丰嘉,這是比較耗時(shí)的。那么為了在太陽(yáng)下山之前到達(dá)山底嚷缭,就要盡可能的減少測(cè)量方向的次數(shù)饮亏。這是一個(gè)兩難的選擇,如果測(cè)量的頻繁阅爽,可以保證下山的方向是絕對(duì)正確的路幸,但又非常耗時(shí),如果測(cè)量的過(guò)少付翁,又有偏離軌道的風(fēng)險(xiǎn)简肴。所以需要找到一個(gè)合適的測(cè)量方向的頻率,來(lái)確保下山的方向不錯(cuò)誤百侧,同時(shí)又不至于耗時(shí)太多砰识!

關(guān)于梯度下降法的詳細(xì)解釋可以參考這篇博客,寫(xiě)的非常詳細(xì)佣渴,強(qiáng)烈推薦辫狼。

回到我們的問(wèn)題中來(lái),首先把我們的函數(shù)C想象成一個(gè)山谷辛润,現(xiàn)在有一個(gè)人被困在了山上的某一處∨虼Γ現(xiàn)在我們讓這個(gè)人沿著v_1v_2方向移動(dòng)一個(gè)很小的量,即\Delta v_1\Delta v_2频蛔,微積分告訴我們函數(shù)C將會(huì)有如下變化:

為了使C往更小的方向變化灵迫,我們需要尋找一種選擇\Delta v_1\Delta v_2的方法使得\Delta C為負(fù)秦叛,即讓小人往下山的方向走晦溪。為了弄明白如何選擇,需要定義\Delta vv的變化的向量挣跋,即\Delta v = (\Delta v_1, \Delta v_2)^T, T是轉(zhuǎn)置符號(hào)三圆。我們也定義C的梯度為偏導(dǎo)數(shù)的向量,(\frac {\partial C}{\partial v_1},\frac {\partial C}{\partial v_2})^T避咆。我們使用\nabla C來(lái)表示梯度向量舟肉,即:

有了這些定義,\Delta C的表達(dá)式可以被重寫(xiě)成:

這個(gè)表達(dá)式解釋了為什么\nabla C 被稱(chēng)為梯度向量: \nabla Cv 的變化關(guān)聯(lián)為 C 的變化查库,正如我們期望的用梯度來(lái)表示路媚,但是這個(gè)方程真正讓我們興奮的是它讓我們看到了如何選取 \Delta v 才能讓 \Delta C 變?yōu)樨?fù)數(shù)。假設(shè)我們選取:

這里的\eta是一個(gè)很小的數(shù)樊销,稱(chēng)之為學(xué)習(xí)率整慎。那么\Delta C \approx -\eta \nabla C \cdot \nabla C = -\eta \| \nabla C\|^2脏款。由于\| \nabla C \|^2 \ge0,這就可以保證\Delta C \le 0裤园,即如果我們按照上式的規(guī)則去改變自變量v撤师,那么C就會(huì)一直減小,即小人會(huì)一直往下山的方向移動(dòng)拧揽。我們令v'代表改變后的新的自變量v剃盾,將上式展開(kāi)得到:

由此我們得到了自變量v的更新方式,可以使用它來(lái)計(jì)算下一次的移動(dòng)方向和距離淤袜。如果我們反復(fù)這么做痒谴,那么我們將持續(xù)減小C,正如我們希望的饮怯,得到一個(gè)全局的最小值闰歪。
總結(jié)一下,梯度下降算法工作的方式就是重復(fù)計(jì)算梯度 \nabla C蓖墅,然后沿著相反的方向移動(dòng)库倘。我們可以想象它像這樣:

這里需要介紹一下上面提到的學(xué)習(xí)率\eta,可以看到它的主要功能是用來(lái)控制小人每次移動(dòng)的步長(zhǎng)论矾。如果\eta太大教翩,那么v每次的變化很劇烈,即小人每次邁的步子很大贪壳,那么當(dāng)小人很快就要到谷底的時(shí)候饱亿,結(jié)果因?yàn)椴阶舆~得太大,錯(cuò)過(guò)了最低點(diǎn)闰靴,這肯定不是我們希望看到的彪笼。同理,我們也不希望\eta太小蚂且,那意味著v每次變化很小配猫,即小人邁的步子很小,行動(dòng)緩慢杏死,這就導(dǎo)致需要花很長(zhǎng)時(shí)間才能到達(dá)谷底泵肄。因此在真正的實(shí)現(xiàn)中,\eta通常是變化的淑翼。比如我們希望\eta在一開(kāi)始比較大腐巢,而快到谷底的時(shí)候稍微變小點(diǎn),即小人最開(kāi)始大步往山下走玄括,當(dāng)快到谷底的時(shí)候冯丙,小心翼翼地挪動(dòng)步伐,防止錯(cuò)過(guò)谷底遭京。

我們解釋了只有兩個(gè)自變量的函數(shù)C的梯度下降算法胃惜,但事實(shí)上风宁,即使C是一個(gè)具有多個(gè)變量的函數(shù)時(shí),梯度下降算法也能很好地工作蛹疯。假設(shè)C是一個(gè)具有m個(gè)變量v_1,v_2,...,v_m的多元函數(shù)戒财。那么對(duì)C中的自變量的變化\Delta v = (\Delta v_1, \Delta v_2,...,\Delta v_m)^T\Delta C會(huì)變成:

這里的梯度\nabla C是向量:

正如兩個(gè)自變量的情況捺弦,我們可以選取:

自變量v的更新規(guī)則為:

至此饮寞,梯度下降算法就介紹完畢了。

隨機(jī)梯度下降算法

我們?cè)趺丛谏窠?jīng)網(wǎng)絡(luò)中使用梯度下降算法去學(xué)習(xí)呢列吼?其思想就是利用梯度下降算法尋找使得我們的全連接神經(jīng)網(wǎng)絡(luò)的損失函數(shù)取得最小值時(shí)的權(quán)重w_kb_l幽崩。我估計(jì)你肯定忘了我們的損失函數(shù)是什么了,我將其重新寫(xiě)在下面:

相比之前的函數(shù)C寞钥,這里相當(dāng)于把原先的自變量v_1v_2換成了w_kb_l慌申,而梯度向量\nabla C則變成了\partial C / \partial w_k\partial C / \partial b_l。用這些分量來(lái)寫(xiě)梯度下降的更新規(guī)則理郑,可以得到:

注意不要認(rèn)為我們的損失函數(shù)也是只有兩個(gè)變量wb

你可能注意到了這一節(jié)的標(biāo)題叫做隨機(jī)梯度下降蹄溉,既然上一節(jié)已經(jīng)有了梯度下降,怎么又來(lái)個(gè)隨機(jī)梯度下降您炉?

請(qǐng)仔細(xì)看一下上面的損失函數(shù)柒爵,注意到了那個(gè)累加符號(hào)了嗎?我們給它換一個(gè)表達(dá)形式如下:

其中C_x代表的是一個(gè)訓(xùn)練樣本的損失函數(shù)赚爵,也就是是總體的損失函數(shù)是對(duì)每一個(gè)訓(xùn)練樣本的損失函數(shù)的值累加之后再求平均棉胀。故為了計(jì)算梯度\nabla C,我們需要遍歷整個(gè)訓(xùn)練數(shù)據(jù)集冀膝,對(duì)每一個(gè)訓(xùn)練樣本都計(jì)算梯度\nabla C唁奢,然后累加之后求平均值。這樣導(dǎo)致的最直接的問(wèn)題就是當(dāng)好訓(xùn)練集很大的時(shí)候窝剖,訓(xùn)練速度會(huì)非常的慢麻掸。
為了加速訓(xùn)練過(guò)程,在實(shí)際使用中枯芬,我們會(huì)采用隨機(jī)梯度下降的算法论笔。它的原理也非常樸素采郎,就是我們不采用整個(gè)數(shù)據(jù)集千所,而是隨機(jī)選取小量訓(xùn)練樣本來(lái)計(jì)算梯度\nabla C,進(jìn)而估算實(shí)際梯度\nabla C蒜埋。通過(guò)計(jì)算少量樣本的平均值我們就可以快速得到一個(gè)對(duì)于實(shí)際梯度\nabla C很好的估算淫痰,這有助于加速梯度下降,進(jìn)而加快訓(xùn)練過(guò)程整份。

再用小人下山的例子來(lái)對(duì)比一下梯度下降算法和隨機(jī)梯度下降算法待错。假設(shè)小人使用梯度下降算法下山籽孙,那意味著小人每走一步之前,都需要經(jīng)過(guò)精確的計(jì)算以便找到最陡峭的方向火俄,然后沿著這個(gè)方向邁出一步犯建,畢竟按照這個(gè)方向下山肯定是最快的。而如果小人使用隨機(jī)梯度算法下山的話瓜客,他并不會(huì)花那么長(zhǎng)的時(shí)間去找到最陡峭的方向适瓦,而是找到一個(gè)大概的方向就行了,然后就快速地邁出一步谱仪。畢竟只要方向基本不錯(cuò)玻熙,最終肯定也能到達(dá)谷底。

隨機(jī)梯度下降通過(guò)隨機(jī)選取小量的 m 個(gè)訓(xùn)練樣本來(lái)工作疯攒。我們將這些隨機(jī)的訓(xùn)練樣本標(biāo)記為 X_1, X_2, . . . , X_m嗦随,并把它們稱(chēng)為一個(gè)小批量數(shù)據(jù)(mini-batch)。假設(shè)樣本數(shù)量 m 足夠大敬尺,我們期望 \nabla C_{X_j} 的平均值大致相等于整個(gè)\nabla C_X 的平均值枚尼,即:

交換兩邊我們得到:

證實(shí)了我們可以僅僅計(jì)算隨機(jī)選取的小批量數(shù)據(jù)的梯度來(lái)估算整體梯度。
為了明確地和神經(jīng)網(wǎng)絡(luò)的學(xué)習(xí)聯(lián)系起來(lái)砂吞,假設(shè)w_kb_l表示我們神經(jīng)網(wǎng)絡(luò)中的權(quán)重和偏置姑原,隨機(jī)梯度下降通過(guò)隨機(jī)地選取小批量的訓(xùn)練樣本來(lái)工作,因此我們可以更改一下梯度下降的更新規(guī)則:

其中兩個(gè)求和符號(hào)是在當(dāng)前小批量數(shù)據(jù)中的所有訓(xùn)練樣本 X_j上進(jìn)行的呜舒。我們隨機(jī)挑選若干個(gè)小批量數(shù)據(jù)集锭汛,然后以此用這些小批量數(shù)據(jù)集去訓(xùn)練網(wǎng)絡(luò),當(dāng)用完了全部的小批量數(shù)據(jù)集之后袭蝗,這被稱(chēng)為完成了一次迭代周期(epoch)唤殴,然后開(kāi)始新一輪的訓(xùn)練迭代周期。

反向傳播

限于篇幅到腥,略過(guò)朵逝,詳情請(qǐng)看這篇博客

代碼實(shí)踐

本文采用的代碼來(lái)自Github乡范,但是原作者是基于Python2.7寫(xiě)的配名,我用python3將其改寫(xiě)了一遍,并且加上了很多注釋晋辆。
代碼鏈接在https://github.com/HeartbreakSurvivor/ClassicNetworks/tree/master/FCN
總共包含3個(gè)文件渠脉,分別是"mnist_loader.py"、"fc.py"瓶佳、"mnist.pkl.gz"芋膘。

下面展示實(shí)現(xiàn)了神經(jīng)網(wǎng)絡(luò)的fc.py的內(nèi)容:

import random
import numpy as np
import mnist_loader

def sigmoid(z):
    """
    Sigmoid激活函數(shù)定義
    """
    return 1.0/(1.0 + np.exp(-z))

def sigmoid_prime(z):
    """
    Sigmoid函數(shù)的導(dǎo)數(shù),關(guān)于Sigmoid函數(shù)的求導(dǎo)可以自行搜索。
    """
    return sigmoid(z)*(1-sigmoid(z))

class FCN(object):
    """
    全連接網(wǎng)絡(luò)的純手工實(shí)現(xiàn)
    """
    def __init__(self, sizes):
        """
        :param sizes: 是一個(gè)列表,其中包含了神經(jīng)網(wǎng)絡(luò)每一層的神經(jīng)元的個(gè)數(shù)为朋,列表的長(zhǎng)度就是神經(jīng)網(wǎng)絡(luò)的層數(shù)臂拓。
        舉個(gè)例子,假如列表為[784,30,10]习寸,那么意味著它是一個(gè)3層的神經(jīng)網(wǎng)絡(luò)胶惰,第一層包含784個(gè)神經(jīng)元,第二層30個(gè)霞溪,最后一層10個(gè)童番。
        注意,神經(jīng)網(wǎng)絡(luò)的權(quán)重和偏置是隨機(jī)生成的威鹿,使用一個(gè)均值為0剃斧,方差為1的高斯分布。
        注意第一層被認(rèn)為是輸入層忽你,它是沒(méi)有偏置向量b和權(quán)重向量w的幼东。因?yàn)槠弥皇怯脕?lái)計(jì)算第二層之后的輸出
        """
        self._num_layers = len(sizes) # 記錄神經(jīng)網(wǎng)絡(luò)的層數(shù)
        # 為隱藏層和輸出層生成偏置向量b,還是以[784,30,10]為例科雳,那么一共會(huì)生成2個(gè)偏置向量b根蟹,分別屬于隱藏層和輸出層,大小分別為30x1,10x1糟秘。
        self._biases = [np.random.randn(y, 1) for y in sizes[1:]]
        # 為隱藏層和輸出層生成權(quán)重向量W, 以[784,30,10]為例简逮,這里會(huì)生成2個(gè)權(quán)重向量w,分別屬于隱藏層和輸出層尿赚,大小分別是30x784, 10x30散庶。
        self._weights = [np.random.randn(y, x) for x,y in zip(sizes[:-1], sizes[1:])]
        # print(self._biases[0].shape)
        # print(self._biases[1].shape)
        # print(self._weights[0].shape)
        # print(self._weights[1].shape)

    def feedforward(self, a):
        """
        前向計(jì)算,返回神經(jīng)網(wǎng)絡(luò)的輸出凌净。公式如下:
        output = sigmoid(w*x+b)
        以[784,30,10]為例悲龟,權(quán)重向量大小分別為[30x784, 10x30],偏置向量大小分別為[30x1, 10x1]
        輸入向量為 784x1.
        矩陣的計(jì)算過(guò)程為:
            30x784 * 784x1 = 30x1
            30x1 + 30x1 = 30x1

            10x30 * 30x1 = 10x1
            10x1 + 10x1 = 10x1
            故最后的輸出是10x1的向量冰寻,即代表了10個(gè)數(shù)字须教。
        :param a: 神經(jīng)網(wǎng)絡(luò)的輸入
        """
        for b, w in zip(self._biases, self._weights):
            a = sigmoid(np.dot(w, a) + b)
        return a

    def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):
        """
        使用小批量隨機(jī)梯度下降來(lái)訓(xùn)練網(wǎng)絡(luò)
        :param training_data: training data 是一個(gè)元素為(x, y)元祖形式的列表,代表了訓(xùn)練數(shù)據(jù)的輸入和輸出斩芭。
        :param epochs: 訓(xùn)練輪次
        :param mini_batch_size: 小批量訓(xùn)練樣本數(shù)據(jù)集大小
        :param eta: 學(xué)習(xí)率
        :param test_data: 如果test_data被指定轻腺,那么在每一輪迭代完成之后,都對(duì)測(cè)試數(shù)據(jù)集進(jìn)行評(píng)估划乖,計(jì)算有多少樣本被正確識(shí)別了贬养。但是這會(huì)拖慢訓(xùn)練速度。
        :return:
        """
        if test_data: n_test = len(test_data)
        n = len(training_data)
        for j in range(epochs):
            # 在每一次迭代之前迁筛,都將訓(xùn)練數(shù)據(jù)集進(jìn)行隨機(jī)打亂煤蚌,然后每次隨機(jī)選取若干個(gè)小批量訓(xùn)練數(shù)據(jù)集
            random.shuffle(training_data)
            mini_batches = [training_data[k:k+mini_batch_size] for k in range(0, n, mini_batch_size)]

            # 每次訓(xùn)練迭代周期中要使用完全部的小批量訓(xùn)練數(shù)據(jù)集
            for mini_batch in mini_batches:
                self.update_mini_batch(mini_batch, eta)

            # 如果test_data被指定,那么在每一輪迭代完成之后细卧,都對(duì)測(cè)試數(shù)據(jù)集進(jìn)行評(píng)估尉桩,計(jì)算有多少樣本被正確識(shí)別了
            if test_data:
                print("Epoch %d: accuracy rate: %.2f%%" % (j, self.evaluate(test_data)/n_test*100))
            else:
                print("Epoch {0} complete".format(j))

    def update_mini_batch(self, mini_batch, eta):
        """
        通過(guò)小批量隨機(jī)梯度下降以及反向傳播來(lái)更新神經(jīng)網(wǎng)絡(luò)的權(quán)重和偏置向量
        :param mini_batch: 隨機(jī)選擇的小批量
        :param eta: 學(xué)習(xí)率
        """
        nabla_b = [np.zeros(b.shape) for b in self._biases]
        nabla_w = [np.zeros(w.shape) for w in self._weights]
        for x,y in mini_batch:
            # 反向傳播算法,運(yùn)用鏈?zhǔn)椒▌t求得對(duì)b和w的偏導(dǎo)
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            # 對(duì)小批量訓(xùn)練數(shù)據(jù)集中的每一個(gè)求得的偏導(dǎo)數(shù)進(jìn)行累加
            nabla_b = [nb + dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw + dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]

        # 使用梯度下降得出的規(guī)則來(lái)更新權(quán)重和偏置向量
        self._weights = [w - (eta / len(mini_batch)) * nw
                        for w, nw in zip(self._weights, nabla_w)]
        self._biases = [b - (eta / len(mini_batch)) * nb
                       for b, nb in zip(self._biases, nabla_b)]

    def backprop(self, x, y):
        """
        反向傳播算法贪庙,計(jì)算損失對(duì)w和b的梯度
        :param x: 訓(xùn)練數(shù)據(jù)x
        :param y: 訓(xùn)練數(shù)據(jù)x對(duì)應(yīng)的標(biāo)簽
        :return: Return a tuple ``(nabla_b, nabla_w)`` representing the
                gradient for the cost function C_x.  ``nabla_b`` and
                ``nabla_w`` are layer-by-layer lists of numpy arrays, similar
                to ``self.biases`` and ``self.weights``.
        """
        nabla_b = [np.zeros(b.shape) for b in self._biases]
        nabla_w = [np.zeros(w.shape) for w in self._weights]
        # feedforward
        activation = x
        activations = [x]  # list to store all the activations, layer by layer
        zs = []  # list to store all the z vectors, layer by layer
        for b, w in zip(self._biases, self._weights):
            # z = wt*x+b note that the shape of z is the same as the bias
            z = np.dot(w, activation) + b
            zs.append(z)
            activation = sigmoid(z) #pass the result z to activator function  --> a = sigmoid(z)
            activations.append(activation)
        # backward pass
        delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
        nabla_b[-1] = delta
        nabla_w[-1] = np.dot(delta, activations[-2].transpose())
        for l in range(2, self._num_layers):
            z = zs[-l]
            sp = sigmoid_prime(z)
            delta = np.dot(self._weights[-l + 1].transpose(), delta) * sp
            nabla_b[-l] = delta
            nabla_w[-l] = np.dot(delta, activations[-l - 1].transpose())
        return (nabla_b, nabla_w)

    def evaluate(self, test_data):
        """
        返回神經(jīng)網(wǎng)絡(luò)對(duì)測(cè)試數(shù)據(jù)test_data的預(yù)測(cè)結(jié)果蜘犁,并且計(jì)算其中識(shí)別正確的個(gè)數(shù)
        因?yàn)樯窠?jīng)網(wǎng)絡(luò)的輸出是一個(gè)10x1的向量,我們需要知道哪一個(gè)神經(jīng)元被激活的程度最大止邮,
        因此使用了argmax函數(shù)以獲取激活值最大的神經(jīng)元的下標(biāo)这橙,那就是網(wǎng)絡(luò)輸出的最終結(jié)果。
        """
        test_results = [(np.argmax(self.feedforward(x)), y)
                        for (x, y) in test_data]
        return sum(int(x == y) for (x, y) in test_results)

    def cost_derivative(self, output_activations, y):
        """
        返回?fù)p失函數(shù)對(duì)a的的偏導(dǎo)數(shù)导披,損失函數(shù)定義 C = 1/2*||y(x)-a||^2
        求導(dǎo)的結(jié)果為:
            C' = y(x) - a
        """
        return (output_activations - y)

if __name__ == "__main__":
    # 獲取MNIST訓(xùn)練數(shù)據(jù)集屈扎、驗(yàn)證數(shù)據(jù)集、測(cè)試數(shù)據(jù)集
    training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
    # 定義一個(gè)3層全連接網(wǎng)絡(luò)撩匕,輸入層有784個(gè)神經(jīng)元鹰晨,隱藏層30個(gè)神經(jīng)元,輸出層10個(gè)神經(jīng)元
    fc = FCN([784, 30, 10])
    # 設(shè)置迭代次數(shù)30次止毕,mini-batch大小為10模蜡,學(xué)習(xí)率為3,并且設(shè)置測(cè)試集扁凛,即每一輪訓(xùn)練完成之后忍疾,都對(duì)模型進(jìn)行一次評(píng)估。
    # 這里的參數(shù)可以根據(jù)實(shí)際情況進(jìn)行修改
    fc.SGD(training_data, 30, 10, 3.0, test_data=test_data)

運(yùn)行結(jié)果如下:
運(yùn)行結(jié)果

上述代碼定義了一個(gè)3層的全連接神經(jīng)網(wǎng)絡(luò)谨朝,每層包含的神經(jīng)元個(gè)數(shù)分別為784卤妒、30、10字币。訓(xùn)練迭代周期為30荚孵,小批量數(shù)據(jù)集個(gè)數(shù)為10,學(xué)習(xí)率為3.0纬朝。并且設(shè)置了測(cè)試數(shù)據(jù)集收叶,即每完成一個(gè)訓(xùn)練迭代周期之后,都會(huì)將測(cè)試數(shù)據(jù)集運(yùn)用在神經(jīng)網(wǎng)絡(luò)上用來(lái)計(jì)算手寫(xiě)數(shù)字識(shí)別準(zhǔn)確率共苛,經(jīng)過(guò)30次的迭代周期之后判没,最終達(dá)到了95.25%的識(shí)別正確率。

因?yàn)樯窠?jīng)網(wǎng)絡(luò)的權(quán)重和偏置是隨機(jī)生成的隅茎,故每次試驗(yàn)結(jié)果并不一樣澄峰。感興趣的讀者可以在自己的電腦上跑一下代碼,并且可以修改一下隱藏層神經(jīng)元數(shù)量辟犀、迭代周期俏竞、學(xué)習(xí)率、小批量訓(xùn)練集的數(shù)量等,觀察一下神經(jīng)網(wǎng)絡(luò)計(jì)算出來(lái)的正確率會(huì)發(fā)生什么變化魂毁。


總結(jié)

本文篇幅很長(zhǎng)玻佩,但是若能仔細(xì)閱讀并且對(duì)文中提及的公式都手動(dòng)推導(dǎo)一遍,再結(jié)合代碼調(diào)試一下的話席楚,相信會(huì)對(duì)全連接神經(jīng)網(wǎng)絡(luò)的原理和訓(xùn)練優(yōu)化過(guò)程有較為深刻的理解咬崔。


后記

本文是自己學(xué)習(xí)《神經(jīng)網(wǎng)絡(luò)與深度學(xué)習(xí)》第一章內(nèi)容時(shí)做的筆記。原文講解的非常詳細(xì)生動(dòng)烦秩,但是篇幅也很長(zhǎng)垮斯,我提煉了出其中最主要的部分記錄下來(lái),方便自己查閱以及他人學(xué)習(xí)只祠。
個(gè)人覺(jué)得這本書(shū)寫(xiě)的非常不錯(cuò)兜蠕,以非常淺顯的語(yǔ)言與生動(dòng)的圖例解釋清楚了全連接神經(jīng)網(wǎng)絡(luò)的構(gòu)成、訓(xùn)練抛寝、優(yōu)化牺氨、調(diào)參等。GitHub上有對(duì)應(yīng)的中文版墩剖,感興趣的讀者也可以看這本猴凹。不過(guò)還是非常推薦閱讀英文原版,因?yàn)樵氖腔诰W(wǎng)頁(yè)的岭皂,有很多形象的插圖郊霎、動(dòng)畫(huà)以及在線程序演示等,可以使讀者加深對(duì)全連接網(wǎng)絡(luò)的印象爷绘。


參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末书劝,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子土至,更是在濱河造成了極大的恐慌购对,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陶因,死亡現(xiàn)場(chǎng)離奇詭異骡苞,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)楷扬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)解幽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人烘苹,你說(shuō)我怎么就攤上這事躲株。” “怎么了镣衡?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵霜定,是天一觀的道長(zhǎng)档悠。 經(jīng)常有香客問(wèn)我,道長(zhǎng)望浩,這世上最難降的妖魔是什么辖所? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮曾雕,結(jié)果婚禮上奴烙,老公的妹妹穿的比我還像新娘助被。我一直安慰自己剖张,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布揩环。 她就那樣靜靜地躺著搔弄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪丰滑。 梳的紋絲不亂的頭發(fā)上顾犹,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音褒墨,去河邊找鬼炫刷。 笑死,一個(gè)胖子當(dāng)著我的面吹牛郁妈,可吹牛的內(nèi)容都是我干的浑玛。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼噩咪,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼顾彰!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起胃碾,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤涨享,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后仆百,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體厕隧,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年俄周,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了栏账。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡栈源,死狀恐怖挡爵,靈堂內(nèi)的尸體忽然破棺而出勋篓,到底是詐尸還是另有隱情镶殷,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布全释,位于F島的核電站,受9級(jí)特大地震影響闭翩,放射性物質(zhì)發(fā)生泄漏挣郭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一疗韵、第九天 我趴在偏房一處隱蔽的房頂上張望兑障。 院中可真熱鬧,春花似錦蕉汪、人聲如沸流译。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)福澡。三九已至,卻和暖如春驹马,著一層夾襖步出監(jiān)牢的瞬間革砸,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工糯累, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留算利,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓泳姐,卻偏偏與公主長(zhǎng)得像效拭,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子仗岸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345