如何使用TensorFlow實(shí)現(xiàn)卷積神經(jīng)網(wǎng)絡(luò)

姓名:尤學(xué)強(qiáng)? ?學(xué)號(hào):17101223374

轉(zhuǎn)載自:http://mp.weixin.qq.com/s/C6cIDCGMr9t7BcCrY_5uBw

【嵌牛導(dǎo)讀】:深度學(xué)習(xí)

【嵌牛鼻子】:時(shí)間序列信號(hào),音頻信號(hào),文本數(shù)據(jù)

【嵌牛提問】:卷積神經(jīng)網(wǎng)絡(luò)的優(yōu)點(diǎn)?

【嵌牛正文】:

卷積神經(jīng)網(wǎng)絡(luò)簡介

卷積神經(jīng)網(wǎng)絡(luò)(Convolutional?Neural?Network,CNN)最初是為解決圖像識(shí)別等問題設(shè)計(jì)的,當(dāng)然其現(xiàn)在的應(yīng)用不僅限于圖像和視頻,也可用于時(shí)間序列信號(hào),比如音頻信號(hào)、文本數(shù)據(jù)等。在早期的圖像識(shí)別研究中,最大的挑戰(zhàn)是如何組織特征,因?yàn)閳D像數(shù)據(jù)不像其他類型的數(shù)據(jù)那樣可以通過人工理解來提取特征。在股票預(yù)測等模型中,我們可以從原始數(shù)據(jù)中提取過往的交易價(jià)格波動(dòng)、市盈率、市凈率、盈利增長等金融因子,這即是特征工程。但是在圖像中,我們很難根據(jù)人為理解提取出有效而豐富的特征。在深度學(xué)習(xí)出現(xiàn)之前,我們必須借助SIFT、HoG等算法提取具有良好區(qū)分性的特征,再集合SVM等機(jī)器學(xué)習(xí)算法進(jìn)行圖像識(shí)別。如圖5-1所示,SIFT對一定程度內(nèi)的縮放、平移、旋轉(zhuǎn)、視角改變、亮度調(diào)整等畸變,都具有不變性,是當(dāng)時(shí)最重要的圖像特征提取方法之一。可以說,在之前只能依靠SIFT等特征提取算法才能勉強(qiáng)進(jìn)行可靠的圖像識(shí)別。

圖5-1??SIFT、HoG等圖像特征提取方法

然而SIFT這類算法提取的特征還是有局限性的,在ImageNet?ILSVRC比賽的最好結(jié)果的錯(cuò)誤率也有26%以上,而且常年難以產(chǎn)生突破。卷積神經(jīng)網(wǎng)絡(luò)提取的特征則可以達(dá)到更好的效果,同時(shí)它不需要將特征提取和分類訓(xùn)練兩個(gè)過程分開,它在訓(xùn)練時(shí)就自動(dòng)提取了最有效的特征。CNN作為一個(gè)深度學(xué)習(xí)架構(gòu)被提出的最初訴求,是降低對圖像數(shù)據(jù)預(yù)處理的要求,以及避免復(fù)雜的特征工程。CNN可以直接使用圖像的原始像素作為輸入,而不必先使用SIFT等算法提取特征,減輕了使用傳統(tǒng)算法如SVM時(shí)必需要做的大量重復(fù)、煩瑣的數(shù)據(jù)預(yù)處理工作。和SIFT等算法類似,CNN訓(xùn)練的模型同樣對縮放、平移、旋轉(zhuǎn)等畸變具有不變性,有著很強(qiáng)的泛化性。CNN的最大特點(diǎn)在于卷積的權(quán)值共享結(jié)構(gòu),可以大幅減少神經(jīng)網(wǎng)絡(luò)的參數(shù)量,防止過擬合的同時(shí)又降低了神經(jīng)網(wǎng)絡(luò)模型的復(fù)雜度。CNN的權(quán)值共享其實(shí)也很像早期的延時(shí)神經(jīng)網(wǎng)絡(luò)(TDNN),只不過后者是在時(shí)間這一個(gè)維度上進(jìn)行權(quán)值共享,降低了學(xué)習(xí)時(shí)間序列信號(hào)的復(fù)雜度。

卷積神經(jīng)網(wǎng)絡(luò)的概念最早出自19世紀(jì)60年代科學(xué)家提出的感受野(Receptive?Field)。當(dāng)時(shí)科學(xué)家通過對貓的視覺皮層細(xì)胞研究發(fā)現(xiàn),每一個(gè)視覺神經(jīng)元只會(huì)處理一小塊區(qū)域的視覺圖像,即感受野。到了20世紀(jì)80年代,日本科學(xué)家提出神經(jīng)認(rèn)知機(jī)(Neocognitron)的概念,可以算作是卷積網(wǎng)絡(luò)最初的實(shí)現(xiàn)原型。神經(jīng)認(rèn)知機(jī)中包含兩類神經(jīng)元,用來抽取特征的S-cells,還有用來抗形變的C-cells,其中S-cells對應(yīng)我們現(xiàn)在主流卷積神經(jīng)網(wǎng)絡(luò)中的卷積核濾波操作,而C-cells則對應(yīng)激活函數(shù)、最大池化(Max-Pooling)等操作。同時(shí),CNN也是首個(gè)成功地進(jìn)行多層訓(xùn)練的網(wǎng)絡(luò)結(jié)構(gòu),即前面章節(jié)提到的LeCun的LeNet5,而全連接的網(wǎng)絡(luò)因?yàn)閰?shù)過多及梯度彌散等問題,在早期很難順利地進(jìn)行多層的訓(xùn)練。卷積神經(jīng)網(wǎng)絡(luò)可以利用空間結(jié)構(gòu)關(guān)系減少需要學(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)行組合和抽象形成更高階的特征,因此CNN可以應(yīng)對各種情況,理論上具有對圖像縮放、平移和旋轉(zhuǎn)的不變性。

一般的卷積神經(jīng)網(wǎng)絡(luò)由多個(gè)卷積層構(gòu)成,每個(gè)卷積層中通常會(huì)進(jìn)行如下幾個(gè)操作。

圖像通過多個(gè)不同的卷積核的濾波,并加偏置(bias),提取出局部特征,每一個(gè)卷積核會(huì)映射出一個(gè)新的2D圖像。

將前面卷積核的濾波輸出結(jié)果,進(jìn)行非線性的激活函數(shù)處理。目前最常見的是使用ReLU函數(shù),而以前Sigmoid函數(shù)用得比較多。

對激活函數(shù)的結(jié)果再進(jìn)行池化操作(即降采樣,比如將2×2的圖片降為1×1的圖片),目前一般是使用最大池化,保留最顯著的特征,并提升模型的畸變?nèi)萑棠芰Α?/p>

這幾個(gè)步驟就構(gòu)成了最常見的卷積層,當(dāng)然也可以再加上一個(gè)LRN(Local?Response?Normalization,局部響應(yīng)歸一化層)層,目前非常流行的Trick還有Batch?Normalization等。

一個(gè)卷積層中可以有多個(gè)不同的卷積核,而每一個(gè)卷積核都對應(yīng)一個(gè)濾波后映射出的新圖像,同一個(gè)新圖像中每一個(gè)像素都來自完全相同的卷積核,這就是卷積核的權(quán)值共享。那我們?yōu)槭裁匆蚕砭矸e核的權(quán)值參數(shù)呢?答案很簡單,降低模型復(fù)雜度,減輕過擬合并降低計(jì)算量。舉個(gè)例子,如圖5-2所示,如果我們的圖像尺寸是1000像素×1000像素,并且假定是黑白圖像,即只有一個(gè)顏色通道,那么一張圖片就有100萬個(gè)像素點(diǎn),輸入數(shù)據(jù)的維度也是100萬。接下來,如果連接一個(gè)相同大小的隱含層(100萬個(gè)隱含節(jié)點(diǎn)),那么將產(chǎn)生100萬×100萬=一萬億個(gè)連接。僅僅一個(gè)全連接層(Fully?Connected?Layer),就有一萬億連接的權(quán)重要去訓(xùn)練,這已經(jīng)超出了普通硬件的計(jì)算能力。我們必須減少需要訓(xùn)練的權(quán)重?cái)?shù)量,一是降低計(jì)算的復(fù)雜度,二是過多的連接會(huì)導(dǎo)致嚴(yán)重的過擬合,減少連接數(shù)可以提升模型的泛化性。

圖像在空間上是有組織結(jié)構(gòu)的,每一個(gè)像素點(diǎn)在空間上和周圍的像素點(diǎn)實(shí)際上是有緊密聯(lián)系的,但是和太遙遠(yuǎn)的像素點(diǎn)就不一定有什么關(guān)聯(lián)了。這就是前面提到的人的視覺感受野的概念,每一個(gè)感受野只接受一小塊區(qū)域的信號(hào)。這一小塊區(qū)域內(nèi)的像素是互相關(guān)聯(lián)的,每一個(gè)神經(jīng)元不需要接收全部像素點(diǎn)的信息,只需要接收局部的像素點(diǎn)作為輸入,而后將所有這些神經(jīng)元收到的局部信息綜合起來就可以得到全局的信息。這樣就可以將之前的全連接的模式修改為局部連接,之前隱含層的每一個(gè)隱含節(jié)點(diǎn)都和全部像素相連,現(xiàn)在我們只需要將每一個(gè)隱含節(jié)點(diǎn)連接到局部的像素節(jié)點(diǎn)。假設(shè)局部感受野大小是10×10,即每個(gè)隱含節(jié)點(diǎn)只與10×10個(gè)像素點(diǎn)相連,那么現(xiàn)在就只需要10×10×100萬=1億個(gè)連接,相比之前的1萬億縮小了10000倍。

圖5-2??全連接(左)和局部連接(右)

上面我們通過局部連接(Locally?Connect)的方法,將連接數(shù)從1萬億降低到1億,但仍然偏多,需要繼續(xù)降低參數(shù)量?,F(xiàn)在隱含層每一個(gè)節(jié)點(diǎn)都與10×10的像素相連,也就是每一個(gè)隱含節(jié)點(diǎn)都擁有100個(gè)參數(shù)。假設(shè)我們的局部連接方式是卷積操作,即默認(rèn)每一個(gè)隱含節(jié)點(diǎn)的參數(shù)都完全一樣,那我們的參數(shù)不再是1億,而是100。不論圖像有多大,都是這10×10=100個(gè)參數(shù),即卷積核的尺寸,這就是卷積對縮小參數(shù)量的貢獻(xiàn)。我們不需要再擔(dān)心有多少隱含節(jié)點(diǎn)或者圖片有多大,參數(shù)量只跟卷積核的大小有關(guān),這也就是所謂的權(quán)值共享。但是如果我們只有一個(gè)卷積核,我們就只能提取一種卷積核濾波的結(jié)果,即只能提取一種圖片特征,這不是我們期望的結(jié)果。好在圖像中最基本的特征很少,我們可以增加卷積核的數(shù)量來多提取一些特征。圖像中的基本特征無非就是點(diǎn)和邊,無論多么復(fù)雜的圖像都是點(diǎn)和邊組合而成的。人眼識(shí)別物體的方式也是從點(diǎn)和邊開始的,視覺神經(jīng)元接受光信號(hào)后,每一個(gè)神經(jīng)元只接受一個(gè)區(qū)域的信號(hào),并提取出點(diǎn)和邊的特征,然后將點(diǎn)和邊的信號(hào)傳遞給后面一層的神經(jīng)元,再接著組合成高階特征,比如三角形、正方形、直線、拐角等,再繼續(xù)抽象組合,得到眼睛、鼻子和嘴等五官,最后再將五官組合成一張臉,完成匹配識(shí)別。因此我們的問題就很好解決了,只要我們提供的卷積核數(shù)量足夠多,能提取出各種方向的邊或各種形態(tài)的點(diǎn),就可以讓卷積層抽象出有效而豐富的高階特征。每一個(gè)卷積核濾波得到的圖像就是一類特征的映射,即一個(gè)Feature?Map。一般來說,我們使用100個(gè)卷積核放在第一個(gè)卷積層就已經(jīng)很充足了。那這樣的話,如圖5-3所示,我們的參數(shù)量就是100×100=1萬個(gè),相比之前的1億又縮小了10000倍。因此,依靠卷積,我們就可以高效地訓(xùn)練局部連接的神經(jīng)網(wǎng)絡(luò)了。卷積的好處是,不管圖片尺寸如何,我們需要訓(xùn)練的權(quán)值數(shù)量只跟卷積核大小、卷積核數(shù)量有關(guān),我們可以使用非常少的參數(shù)量處理任意大小的圖片。每一個(gè)卷積層提取的特征,在后面的層中都會(huì)抽象組合成更高階的特征。而且多層抽象的卷積網(wǎng)絡(luò)表達(dá)能力更強(qiáng),效率更高,相比只使用一個(gè)隱含層提取全部高階特征,反而可以節(jié)省大量的參數(shù)。當(dāng)然,我們需要注意的是,雖然需要訓(xùn)練的參數(shù)量下降了,但是隱含節(jié)點(diǎn)的數(shù)量并沒有下降,隱含節(jié)點(diǎn)的數(shù)量只跟卷積的步長有關(guān)。如果步長為1,那么隱含節(jié)點(diǎn)的數(shù)量和輸入的圖像像素?cái)?shù)量一致;如果步長為5,那么每5×5的像素才需要一個(gè)隱含節(jié)點(diǎn),我們隱含節(jié)點(diǎn)的數(shù)量就是輸入像素?cái)?shù)量的1/25。

圖5-3??局部連接(左)和卷積操作(右)

我們再總結(jié)一下,卷積神經(jīng)網(wǎng)絡(luò)的要點(diǎn)就是局部連接(Local?Connection)、權(quán)值共享(Weight?Sharing)和池化層(Pooling)中的降采樣(Down-Sampling)。其中,局部連接和權(quán)值共享降低了參數(shù)量,使訓(xùn)練復(fù)雜度大大下降,并減輕了過擬合。同時(shí)權(quán)值共享還賦予了卷積網(wǎng)絡(luò)對平移的容忍性,而池化層降采樣則進(jìn)一步降低了輸出參數(shù)量,并賦予模型對輕度形變的容忍性,提高了模型的泛化能力。卷積神經(jīng)網(wǎng)絡(luò)相比傳統(tǒng)的機(jī)器學(xué)習(xí)算法,無須手工提取特征,也不需要使用諸如SIFT之類的特征提取算法,可以在訓(xùn)練中自動(dòng)完成特征的提取和抽象,并同時(shí)進(jìn)行模式分類,大大降低了應(yīng)用圖像識(shí)別的難度;相比一般的神經(jīng)網(wǎng)絡(luò),CNN在結(jié)構(gòu)上和圖片的空間結(jié)構(gòu)更為貼近,都是2D的有聯(lián)系的結(jié)構(gòu),并且CNN的卷積連接方式和人的視覺神經(jīng)處理光信號(hào)的方式類似。

大名鼎鼎的LeNet5?誕生于1994年,是最早的深層卷積神經(jīng)網(wǎng)絡(luò)之一,并且推動(dòng)了深度學(xué)習(xí)的發(fā)展。從1988年開始,在多次成功的迭代后,這項(xiàng)由Yann?LeCun完成的開拓性成果被命名為LeNet5。LeCun認(rèn)為,可訓(xùn)練參數(shù)的卷積層是一種用少量參數(shù)在圖像的多個(gè)位置上提取相似特征的有效方式,這和直接把每個(gè)像素作為多層神經(jīng)網(wǎng)絡(luò)的輸入不同。像素不應(yīng)該被使用在輸入層,因?yàn)閳D像具有很強(qiáng)的空間相關(guān)性,而使用圖像中獨(dú)立的像素直接作為輸入則利用不到這些相關(guān)性。

LeNet5當(dāng)時(shí)的特性有如下幾點(diǎn)。

每個(gè)卷積層包含三個(gè)部分:卷積、池化和非線性激活函數(shù)

使用卷積提取空間特征

降采樣(Subsample)的平均池化層(Average?Pooling)

雙曲正切(Tanh)或S型(Sigmoid)的激活函數(shù)

MLP作為最后的分類器

層與層之間的稀疏連接減少計(jì)算復(fù)雜度

LeNet5中的諸多特性現(xiàn)在依然在state-of-the-art卷積神經(jīng)網(wǎng)絡(luò)中使用,可以說LeNet5是奠定了現(xiàn)代卷積神經(jīng)網(wǎng)絡(luò)的基石之作。Lenet-5的結(jié)構(gòu)如圖5-4所示。它的輸入圖像為32×32的灰度值圖像,后面有三個(gè)卷積層,一個(gè)全連接層和一個(gè)高斯連接層。它的第一個(gè)卷積層C1包含6個(gè)卷積核,卷積核尺寸為5×5,即總共(5×5+1)×6=156個(gè)參數(shù),括號(hào)中的1代表1個(gè)bias,后面是一個(gè)2×2的平均池化層S2用來進(jìn)行降采樣,再之后是一個(gè)Sigmoid激活函數(shù)用來進(jìn)行非線性處理。而后是第二個(gè)卷積層C3,同樣卷積核尺寸是5×5,這里使用了16個(gè)卷積核,對應(yīng)16個(gè)Feature?Map。需要注意的是,這里的16個(gè)Feature?Map不是全部連接到前面的6個(gè)Feature?Map的輸出的,有些只連接了其中的幾個(gè)Feature?Map,這樣增加了模型的多樣性。下面的第二個(gè)池化層S4和第一個(gè)池化層S2一致,都是2×2的降采樣。接下來的第三個(gè)卷積層C5有120個(gè)卷積核,卷積大小同樣為5×5,因?yàn)檩斎雸D像的大小剛好也是5×5,因此構(gòu)成了全連接,也可以算作全連接層。F6層是一個(gè)全連接層,擁有84個(gè)隱含節(jié)點(diǎn),激活函數(shù)為Sigmoid。LeNet-5最后一層由歐式徑向基函數(shù)(Euclidean?Radial?Basis?Function)單元組成,它輸出最后的分類結(jié)果。

圖5-4??LeNet-5結(jié)構(gòu)示意圖

TensorFlow實(shí)現(xiàn)簡單的卷積網(wǎng)絡(luò)

本節(jié)將講解如何使用TensorFlow實(shí)現(xiàn)一個(gè)簡單的卷積神經(jīng)網(wǎng)絡(luò),使用的數(shù)據(jù)集依然是MNIST,預(yù)期可以達(dá)到99.2%左右的準(zhǔn)確率。本節(jié)將使用兩個(gè)卷積層加一個(gè)全連接層構(gòu)建一個(gè)簡單但是非常有代表性的卷積神經(jīng)網(wǎng)絡(luò),讀者應(yīng)該能通過這個(gè)例子掌握設(shè)計(jì)卷積神經(jīng)網(wǎng)絡(luò)的要點(diǎn)。

首先載入MNIST數(shù)據(jù)集,并創(chuàng)建默認(rèn)的Interactive?Session。本節(jié)代碼主要來自TensorFlow的開源實(shí)現(xiàn)。

接下來要實(shí)現(xiàn)的這個(gè)卷積神經(jīng)網(wǎng)絡(luò)會(huì)有很多的權(quán)重和偏置需要?jiǎng)?chuàng)建,因此我們先定義好初始化函數(shù)以便重復(fù)使用。我們需要給權(quán)重制造一些隨機(jī)的噪聲來打破完全對稱,比如截?cái)嗟恼龖B(tài)分布噪聲,標(biāo)準(zhǔn)差設(shè)為0.1。同時(shí)因?yàn)槲覀兪褂肦eLU,也給偏置增加一些小的正值(0.1)用來避免死亡節(jié)點(diǎn)(dead?neurons)。

卷積層、池化層也是接下來要重復(fù)使用的,因此也為他們分別定義創(chuàng)建函數(shù)。這里的tf.nn.conv2d是TensorFlow中的2維卷積函數(shù),參數(shù)中x是輸入,W是卷積的參數(shù),比如[5,5,1,32]:前面兩個(gè)數(shù)字代表卷積核的尺寸;第三個(gè)數(shù)字代表有多少個(gè)channel。因?yàn)槲覀冎挥谢叶葐紊?,所以?,如果是彩色的RGB圖片,這里應(yīng)該是3。最后一個(gè)數(shù)字代表卷積核的數(shù)量,也就是這個(gè)卷積層會(huì)提取多少類的特征。Strides代表卷積模板移動(dòng)的步長,都是1代表會(huì)不遺漏地劃過圖片的每一個(gè)點(diǎn)。Padding代表邊界的處理方式,這里的SAME代表給邊界加上Padding讓卷積的輸出和輸入保持同樣(SAME)的尺寸。tf.nn.max_pool是TensorFlow中的最大池化函數(shù),我們這里使用2×2的最大池化,即將一個(gè)2×2的像素塊降為1×1的像素。最大池化會(huì)保留原始像素塊中灰度值最高的那一個(gè)像素,即保留最顯著的特征。因?yàn)橄Mw上縮小圖片尺寸,因此池化層的strides也設(shè)為橫豎兩個(gè)方向以2為步長。如果步長還是1,那么我們會(huì)得到一個(gè)尺寸不變的圖片。

在正式設(shè)計(jì)卷積神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu)之前,先定義輸入的placeholder,x是特征,y_是真實(shí)的label。因?yàn)榫矸e神經(jīng)網(wǎng)絡(luò)會(huì)利用到空間結(jié)構(gòu)信息,因此需要將1D的輸入向量轉(zhuǎn)為2D的圖片結(jié)構(gòu),即從1×784的形式轉(zhuǎn)為原始的28×28的結(jié)構(gòu)。同時(shí)因?yàn)橹挥幸粋€(gè)顏色通道,故最終尺寸為[-1,28,28,1],前面的-1代表樣本數(shù)量不固定,最后的1代表顏色通道數(shù)量。這里我們使用的tensor變形函數(shù)是tf.reshape。

接下來定義我們的第一個(gè)卷積層。我們先使用前面寫好的函數(shù)進(jìn)行參數(shù)初始化,包括weights和bias,這里的[5,5,1,32]代表卷積核尺寸為5×5,1個(gè)顏色通道,32個(gè)不同的卷積核。然后使用conv2d函數(shù)進(jìn)行卷積操作,并加上偏置,接著再使用ReLU激活函數(shù)進(jìn)行非線性處理。最后,使用最大池化函數(shù)max_pool_2x2對卷積的輸出結(jié)果進(jìn)行池化操作。

現(xiàn)在定義第二個(gè)卷積層,這個(gè)卷積層基本和第一個(gè)卷積層一樣,唯一的不同是,卷積核的數(shù)量變成了64,也就是說這一層的卷積會(huì)提取64種特征。

因?yàn)榍懊娼?jīng)歷了兩次步長為2×2的最大池化,所以邊長已經(jīng)只有1/4了,圖片尺寸由28×28變成了7×7。而第二個(gè)卷積層的卷積核數(shù)量為64,其輸出的tensor尺寸即為7×7×64。我們使用tf.reshape函數(shù)對第二個(gè)卷積層的輸出tensor進(jìn)行變形,將其轉(zhuǎn)成1D的向量,然后連接一個(gè)全連接層,隱含節(jié)點(diǎn)為1024,并使用ReLU激活函數(shù)。

為了減輕過擬合,下面使用一個(gè)Dropout層,Dropout的用法第4章已經(jīng)講過,是通過一個(gè)placeholder傳入keep_prob比率來控制的。在訓(xùn)練時(shí),我們隨機(jī)丟棄一部分節(jié)點(diǎn)的數(shù)據(jù)來減輕過擬合,預(yù)測時(shí)則保留全部數(shù)據(jù)來追求最好的預(yù)測性能。

最后我們將Dropout層的輸出連接一個(gè)Softmax層,得到最后的概率輸出。

我們定義損失函數(shù)為cross?entropy,和之前一樣,但是優(yōu)化器使用Adam,并給予一個(gè)比較小的學(xué)習(xí)速率1e-4。

再繼續(xù)定義評測準(zhǔn)確率的操作。

下面開始訓(xùn)練過程。首先依然是初始化所有參數(shù),設(shè)置訓(xùn)練時(shí)Dropout的keep_prob比率為0.5。然后使用大小為50的mini-batch,共進(jìn)行20000次訓(xùn)練迭代,參與訓(xùn)練的樣本數(shù)量總共為100萬。其中每100次訓(xùn)練,我們會(huì)對準(zhǔn)確率進(jìn)行一次評測(評測時(shí)keep_prob設(shè)為1),用以實(shí)時(shí)監(jiān)測模型的性能。

全部訓(xùn)練完成后,我們在最終的測試集上進(jìn)行全面的測試,得到整體的分類準(zhǔn)確率。

最后,這個(gè)CNN模型可以得到的準(zhǔn)確率約為99.2%,基本可以滿足對手寫數(shù)字識(shí)別準(zhǔn)確率的要求。相比之前MLP的2%錯(cuò)誤率,CNN的錯(cuò)誤率下降了大約60%。這其中主要的性能提升都來自于更優(yōu)秀的網(wǎng)絡(luò)設(shè)計(jì),即卷積網(wǎng)絡(luò)對圖像特征的提取和抽象能力。依靠卷積核的權(quán)值共享,CNN的參數(shù)量并沒有爆炸,降低計(jì)算量的同時(shí)也減輕了過擬合,因此整個(gè)模型的性能有較大的提升。本節(jié)我們只實(shí)現(xiàn)了一個(gè)簡單的卷積神經(jīng)網(wǎng)絡(luò),沒有復(fù)雜的Trick。接下來,我們將實(shí)現(xiàn)一個(gè)稍微復(fù)雜一些的卷積網(wǎng)絡(luò),而簡單的MNIST數(shù)據(jù)集已經(jīng)不適合用來評測其性能,我們將使用CIFAR-10數(shù)據(jù)集進(jìn)行訓(xùn)練,這也是深度學(xué)習(xí)可以大幅領(lǐng)先其他模型的一個(gè)數(shù)據(jù)集。

TensorFlow實(shí)現(xiàn)進(jìn)階的卷積網(wǎng)絡(luò)

本節(jié)使用的數(shù)據(jù)集是CIFAR-10,這是一個(gè)經(jīng)典的數(shù)據(jù)集,包含60000張32×32的彩色圖像,其中訓(xùn)練集50000張,測試集10000張。CIFAR-10如同其名字,一共標(biāo)注為10類,每一類圖片6000張。這10類分別是airplane、automobile、bird、cat、deer、dog、frog、horse、ship和truck,其中沒有任何重疊的情況,比如automobile只包括小型汽車,truck只包括卡車,也不會(huì)在一張圖片中同時(shí)出現(xiàn)兩類物體。它還有一個(gè)兄弟版本CIFAR-100,其中標(biāo)注了100類。這兩個(gè)數(shù)據(jù)集是前面章節(jié)提到的深度學(xué)習(xí)之父Geoffrey?Hinton和他的兩名學(xué)生Alex?Krizhevsky和Vinod?Nair收集的,圖片來源于80?million?tiny?images這個(gè)數(shù)據(jù)集,Hinton等人對其進(jìn)行了篩選和標(biāo)注。CIFAR-10數(shù)據(jù)集非常通用,經(jīng)常出現(xiàn)在各大會(huì)議的論文中用來進(jìn)行性能對比,也曾出現(xiàn)在Kaggle競賽而為大家所知。圖5-5所示為這個(gè)數(shù)據(jù)集的一些示例。

圖5-5??CIFAR-10數(shù)據(jù)集示例

許多論文中都在這個(gè)數(shù)據(jù)集上進(jìn)行了測試,目前state-of-the-art的工作已經(jīng)可以達(dá)到3.5%的錯(cuò)誤率了,但是需要訓(xùn)練很久,即使在GPU上也需要十幾個(gè)小時(shí)。CIFAR-10數(shù)據(jù)集上詳細(xì)的Benchmark和排名在classification?datasets?results上(http://rodrigob.github.io/are_we_there_yet/build/classification_datasets_results.html)。據(jù)深度學(xué)習(xí)三巨頭之一LeCun說,現(xiàn)有的卷積神經(jīng)網(wǎng)絡(luò)已經(jīng)可以對CIFAR-10進(jìn)行很好的學(xué)習(xí),這個(gè)數(shù)據(jù)集的問題已經(jīng)解決了。本節(jié)中實(shí)現(xiàn)的卷積神經(jīng)網(wǎng)絡(luò)沒有那么復(fù)雜(根據(jù)Alex描述的cuda-convnet模型做了些許修改得到),在只使用3000個(gè)batch(每個(gè)batch包含128個(gè)樣本)時(shí),可以達(dá)到73%左右的正確率。模型在GTX?1080單顯卡上大概只需要幾十秒的訓(xùn)練時(shí)間,如果在CPU上訓(xùn)練則會(huì)慢很多。如果使用100k個(gè)batch,并結(jié)合學(xué)習(xí)速度的decay(即每隔一段時(shí)間將學(xué)習(xí)速率下降一個(gè)比率),正確率最高可以到86%左右。模型中需要訓(xùn)練的參數(shù)約為100萬個(gè),而預(yù)測時(shí)需要進(jìn)行的四則運(yùn)算總量在2000萬次左右。在這個(gè)卷積神經(jīng)網(wǎng)絡(luò)模型中,我們使用了一些新的技巧。

對weights進(jìn)行了L2的正則化。

如圖5-6所示,我們對圖片進(jìn)行了翻轉(zhuǎn)、隨機(jī)剪切等數(shù)據(jù)增強(qiáng),制造了更多樣本。

在每個(gè)卷積-最大池化層后面使用了LRN層,增強(qiáng)了模型的泛化能力。

圖5-6??數(shù)據(jù)增強(qiáng)示例(水平翻轉(zhuǎn),隨機(jī)裁切)

我們首先下載TensorFlow?Models庫,以便使用其中提供CIFAR-10數(shù)據(jù)的類。

然后我們載入一些常用庫,比如NumPy和time,并載入TensorFlow?Models中自動(dòng)下載、讀取CIFAR-10數(shù)據(jù)的類。本節(jié)代碼主要來自TensorFlow的開源實(shí)現(xiàn)。

接著定義batch_size、訓(xùn)練輪數(shù)max_steps,以及下載CIFAR-10數(shù)據(jù)的默認(rèn)路徑。

這里定義初始化weight的函數(shù),和之前一樣依然使用tf.truncated_normal截?cái)嗟恼龖B(tài)分布來初始化權(quán)重。但是這里會(huì)給weight加一個(gè)L2的loss,相當(dāng)于做了一個(gè)L2的正則化處理。在機(jī)器學(xué)習(xí)中,不管是分類還是回歸任務(wù),都可能因特征過多而導(dǎo)致過擬合,一般可以通過減少特征或者懲罰不重要特征的權(quán)重來緩解這個(gè)問題。但是通常我們并不知道該懲罰哪些特征的權(quán)重,而正則化就是幫助我們懲罰特征權(quán)重的,即特征的權(quán)重也會(huì)成為模型的損失函數(shù)的一部分。可以理解為,為了使用某個(gè)特征,我們需要付出loss的代價(jià),除非這個(gè)特征非常有效,否則就會(huì)被loss上的增加覆蓋效果。這樣我們就可以篩選出最有效的特征,減少特征權(quán)重防止過擬合。這也即是奧卡姆剃刀法則,越簡單的東西越有效。一般來說,L1正則會(huì)制造稀疏的特征,大部分無用特征的權(quán)重會(huì)被置為0,而L2正則會(huì)讓特征的權(quán)重不過大,使得特征的權(quán)重比較平均。我們使用wl控制L2?loss的大小,使用tf.nn.l2_loss函數(shù)計(jì)算weight的L2?loss,再使用tf.multiply讓L2?loss乘以wl,得到最后的weight?loss。接著,我們使用tf.add_to_collection把weight?loss統(tǒng)一存到一個(gè)collection,這個(gè)collection名為“l(fā)osses”,它會(huì)在后面計(jì)算神經(jīng)網(wǎng)絡(luò)的總體loss時(shí)被用上。

下面使用cifar10類下載數(shù)據(jù)集,并解壓、展開到其默認(rèn)位置。

再使用cifar10_input類中的distorted_inputs函數(shù)產(chǎn)生訓(xùn)練需要使用的數(shù)據(jù),包括特征及其對應(yīng)的label,這里返回的是已經(jīng)封裝好的tensor,每次執(zhí)行都會(huì)生成一個(gè)batch_size的數(shù)量的樣本。需要注意的是我們對數(shù)據(jù)進(jìn)行了Data?Augmentation(數(shù)據(jù)增強(qiáng))。具體的實(shí)現(xiàn)細(xì)節(jié),讀者可以查看cifar10_input.distorted_inputs函數(shù),其中的數(shù)據(jù)增強(qiáng)操作包括隨機(jī)的水平翻轉(zhuǎn)(tf.image.random_flip_left_right)、隨機(jī)剪切一塊24×24大小的圖片(tf.random_crop)、設(shè)置隨機(jī)的亮度和對比度(tf.image.random_brightness、tf.image.random_contrast),以及對數(shù)據(jù)進(jìn)行標(biāo)準(zhǔn)化tf.image.per_image_whitening(對數(shù)據(jù)減去均值,除以方差,保證數(shù)據(jù)零均值,方差為1)。通過這些操作,我們可以獲得更多的樣本(帶噪聲的),原來的一張圖片樣本可以變?yōu)槎鄰垐D片,相當(dāng)于擴(kuò)大樣本量,對提高準(zhǔn)確率非常有幫助。需要注意的是,我們對圖像進(jìn)行數(shù)據(jù)增強(qiáng)的操作需要耗費(fèi)大量CPU時(shí)間,因此distorted_inputs使用了16個(gè)獨(dú)立的線程來加速任務(wù),函數(shù)內(nèi)部會(huì)產(chǎn)生線程池,在需要使用時(shí)會(huì)通過TensorFlow?queue進(jìn)行調(diào)度。

我們再使用cifar10_input.inputs函數(shù)生成測試數(shù)據(jù),這里不需要進(jìn)行太多處理,不需要對圖片進(jìn)行翻轉(zhuǎn)或修改亮度、對比度,不過需要裁剪圖片正中間的24×24大小的區(qū)塊,并進(jìn)行數(shù)據(jù)標(biāo)準(zhǔn)化操作。

這里創(chuàng)建輸入數(shù)據(jù)的placeholder,包括特征和label。在設(shè)定placeholder的數(shù)據(jù)尺寸時(shí)需要注意,因?yàn)閎atch_size在之后定義網(wǎng)絡(luò)結(jié)構(gòu)時(shí)被用到了,所以數(shù)據(jù)尺寸中的第一個(gè)值即樣本條數(shù)需要被預(yù)先設(shè)定,而不能像以前一樣可以設(shè)為None。而數(shù)據(jù)尺寸中的圖片尺寸為24×24,即是裁剪后的大小,而顏色通道數(shù)則設(shè)為3,代表圖片是彩色有RGB三條通道。

做好了準(zhǔn)備工作,接下來開始創(chuàng)建第一個(gè)卷積層。先使用之前寫好的variable_with_weight_loss函數(shù)創(chuàng)建卷積核的參數(shù)并進(jìn)行初始化。第一個(gè)卷積層使用5×5的卷積核大小,3個(gè)顏色通道,64個(gè)卷積核,同時(shí)設(shè)置weight初始化函數(shù)的標(biāo)準(zhǔn)差為0.05。我們不對第一個(gè)卷積層的weight進(jìn)行L2的正則,因此wl(weight?loss)這一項(xiàng)設(shè)為0。下面使用tf.nn.conv2d函數(shù)對輸入數(shù)據(jù)image_holder進(jìn)行卷積操作,這里的步長stride均設(shè)為1,padding模式為SAME。把這層的bias全部初始化為0,再將卷積的結(jié)果加上bias,最后使用一個(gè)ReLU激活函數(shù)進(jìn)行非線性化。在ReLU激活函數(shù)之后,我們使用一個(gè)尺寸為3×3且步長為2×2的最大池化層處理數(shù)據(jù),注意這里最大池化的尺寸和步長不一致,這樣可以增加數(shù)據(jù)的豐富性。再之后,我們使用tf.nn.lrn函數(shù),即LRN對結(jié)果進(jìn)行處理。LRN最早見于Alex那篇用CNN參加ImageNet比賽的論文,Alex在論文中解釋LRN層模仿了生物神經(jīng)系統(tǒng)的“側(cè)抑制”機(jī)制,對局部神經(jīng)元的活動(dòng)創(chuàng)建競爭環(huán)境,使得其中響應(yīng)比較大的值變得相對更大,并抑制其他反饋較小的神經(jīng)元,增強(qiáng)了模型的泛化能力。Alex在ImageNet數(shù)據(jù)集上的實(shí)驗(yàn)表明,使用LRN后CNN在Top1的錯(cuò)誤率可以降低1.4%,因此在其經(jīng)典的AlexNet中使用了LRN層。LRN對ReLU這種沒有上限邊界的激活函數(shù)會(huì)比較有用,因?yàn)樗鼤?huì)從附近的多個(gè)卷積核的響應(yīng)(Response)中挑選比較大的反饋,但不適合Sigmoid這種有固定邊界并且能抑制過大值的激活函數(shù)。

現(xiàn)在來創(chuàng)建第二個(gè)卷積層,這里的步驟和第一步很像,區(qū)別如下。上一層的卷積核數(shù)量為64(即輸出64個(gè)通道),所以本層卷積核尺寸的第三個(gè)維度即輸入的通道數(shù)也需要調(diào)整為64;還有一個(gè)需要注意的地方是這里的bias值全部初始化為0.1,而不是0。最后,我們調(diào)換了最大池化層和LRN層的順序,先進(jìn)行LRN層處理,再使用最大池化層。

在兩個(gè)卷積層之后,將使用一個(gè)全連接層,這里需要先把前面兩個(gè)卷積層的輸出結(jié)果全部flatten,使用tf.reshape函數(shù)將每個(gè)樣本都變成一維向量。我們使用get_shape函數(shù),獲取數(shù)據(jù)扁平化之后的長度。接著使用variable_with_weight_loss函數(shù)對全連接層的weight進(jìn)行初始化,這里隱含節(jié)點(diǎn)數(shù)為384,正態(tài)分布的標(biāo)準(zhǔn)差設(shè)為0.04,bias的值也初始化為0.1。需要注意的是我們希望這個(gè)全連接層不要過擬合,因此設(shè)了一個(gè)非零的weight?loss值0.04,讓這一層的所有參數(shù)都被L2正則所約束。最后我們依然使用ReLU激活函數(shù)進(jìn)行非線性化。

接下來的這個(gè)全連接層和前一層很像,只不過其隱含節(jié)點(diǎn)數(shù)下降了一半,只有192個(gè),其他的超參數(shù)保持不變。

下面是最后一層,依然先創(chuàng)建這一層的weight,其正態(tài)分布標(biāo)準(zhǔn)差設(shè)為上一個(gè)隱含層的節(jié)點(diǎn)數(shù)的倒數(shù),并且不計(jì)入L2的正則。需要注意的是,這里不像之前那樣使用softmax輸出最后結(jié)果,這是因?yàn)槲覀儼裺oftmax的操作放在了計(jì)算loss的部分。我們不需要對inference的輸出進(jìn)行softmax處理就可以獲得最終分類結(jié)果(直接比較inference輸出的各類的數(shù)值大小即可),計(jì)算softmax主要是為了計(jì)算loss,因此softmax操作整合到后面是比較合適的。

到這里就完成了整個(gè)網(wǎng)絡(luò)inference的部分。梳理整個(gè)網(wǎng)絡(luò)結(jié)構(gòu)可以得到表5-1。從上到下,依次是整個(gè)卷積神經(jīng)網(wǎng)絡(luò)從輸入到輸出的流程??梢杂^察到,其實(shí)設(shè)計(jì)CNN主要就是安排卷積層、池化層、全連接層的分布和順序,以及其中超參數(shù)的設(shè)置、Trick的使用等。設(shè)計(jì)性能良好的CNN是有一定規(guī)律可循的,但是想要針對某個(gè)問題設(shè)計(jì)最合適的網(wǎng)絡(luò)結(jié)構(gòu),是需要大量實(shí)踐摸索的。

表5-1??卷積神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)表

完成了模型inference部分的構(gòu)建,接下來計(jì)算CNN的loss。這里依然使用cross?entropy,需要注意的是我們把softmax的計(jì)算和cross?entropy?loss的計(jì)算合在了一起,即tf.nn.sparse_softmax_cross_entropy_with_logits。這里使用tf.reduce_mean對cross?entropy計(jì)算均值,再使用tf.add_to_collection把cross?entropy的loss添加到整體losses的collection中。最后,使用tf.add_n將整體losses的collection中的全部loss求和,得到最終的loss,其中包括cross?entropy?loss,還有后兩個(gè)全連接層中weight的L2?loss。

接著將logits節(jié)點(diǎn)和label_placeholder傳入loss函數(shù)獲得最終的loss。

優(yōu)化器依然選擇Adam?Optimizer,學(xué)習(xí)速率設(shè)為1e-3。

使用tf.nn.in_top_k函數(shù)求輸出結(jié)果中top?k的準(zhǔn)確率,默認(rèn)使用top?1,也就是輸出分?jǐn)?shù)最高的那一類的準(zhǔn)確率。

使用tf.InteractiveSession創(chuàng)建默認(rèn)的session,接著初始化全部模型參數(shù)。

這一步是啟動(dòng)前面提到的圖片數(shù)據(jù)增強(qiáng)的線程隊(duì)列,這里一共使用了16個(gè)線程來進(jìn)行加速。注意,如果這里不啟動(dòng)線程,那么后續(xù)的inference及訓(xùn)練的操作都是無法開始的。

現(xiàn)在正式開始訓(xùn)練。在每一個(gè)step的訓(xùn)練過程中,我們需要先使用session的run方法執(zhí)行images_train、labels_train的計(jì)算,獲得一個(gè)batch的訓(xùn)練數(shù)據(jù),再將這個(gè)batch的數(shù)據(jù)傳入train_op和loss的計(jì)算。我們記錄每一個(gè)step花費(fèi)的時(shí)間,每隔10個(gè)step會(huì)計(jì)算并展示當(dāng)前的loss、每秒鐘能訓(xùn)練的樣本數(shù)量,以及訓(xùn)練一個(gè)batch數(shù)據(jù)所花費(fèi)的時(shí)間,這樣就可以比較方便地監(jiān)控整個(gè)訓(xùn)練過程。在GTX?1080上,每秒鐘可以訓(xùn)練大約1800個(gè)樣本,如果batch_size為128,則每個(gè)batch大約需要0.066s。損失loss在一開始大約為4.6,在經(jīng)過了3000步訓(xùn)練后會(huì)下降到1.0附近。

接下來評測模型在測試集上的準(zhǔn)確率。測試集一共有10000個(gè)樣本,但是需要注意的是,我們依然要像訓(xùn)練時(shí)那樣使用固定的batch_size,然后一個(gè)batch一個(gè)batch地輸入測試數(shù)據(jù)。我們先計(jì)算一共要多少個(gè)batch才能將全部樣本評測完。同時(shí),在每一個(gè)step中使用session的run方法獲取images_test、labels_test的batch,再執(zhí)行top_k_op計(jì)算模型在這個(gè)batch的top?1上預(yù)測正確的樣本數(shù)。最后匯總所有預(yù)測正確的結(jié)果,求得全部測試樣本中預(yù)測正確的數(shù)量。

最后將準(zhǔn)確率的評測結(jié)果計(jì)算并打印出來。

最終,在CIFAR-10數(shù)據(jù)集上,通過一個(gè)短時(shí)間小迭代次數(shù)的訓(xùn)練,可以達(dá)到大致73%的準(zhǔn)確率。持續(xù)增加max_steps,可以期望準(zhǔn)確率逐漸增加。如果max_steps比較大,則推薦使用學(xué)習(xí)速率衰減(decay)的SGD進(jìn)行訓(xùn)練,這樣訓(xùn)練過程中能達(dá)到的準(zhǔn)確率峰值會(huì)比較高,大致接近86%。而其中L2正則及LRN層的使用都對模型準(zhǔn)確率有提升作用,他們都可以從某些方面提升模型的泛化性。

數(shù)據(jù)增強(qiáng)(Data?Augmentation)在我們的訓(xùn)練中作用很大,它可以給單幅圖增加多個(gè)副本,提高圖片的利用率,防止對某一張圖片結(jié)構(gòu)的學(xué)習(xí)過擬合。這剛好是利用了圖片數(shù)據(jù)本身的性質(zhì),圖片的冗余信息量比較大,因此可以制造不同的噪聲并讓圖片依然可以被識(shí)別出來。如果神經(jīng)網(wǎng)絡(luò)可以克服這些噪聲并準(zhǔn)確識(shí)別,那么它的泛化性必然會(huì)很好。數(shù)據(jù)增強(qiáng)大大增加了樣本量,而數(shù)據(jù)量的大小恰恰是深度學(xué)習(xí)最看重的,深度學(xué)習(xí)可以在圖像識(shí)別上領(lǐng)先其他算法的一大因素就是它對海量數(shù)據(jù)的利用效率非常高。用其他算法,可能在數(shù)據(jù)量大到一定程度時(shí),準(zhǔn)確率就不再上升了,而深度學(xué)習(xí)只要提供足夠多的樣本,準(zhǔn)確率基本可以持續(xù)提升,所以說它是最適合大數(shù)據(jù)的算法。如圖5-6所示,傳統(tǒng)的機(jī)器學(xué)習(xí)算法在獲取了一定量的數(shù)據(jù)后,準(zhǔn)確率上升曲線就接近瓶頸,而神經(jīng)網(wǎng)絡(luò)則可以持續(xù)上升到更高的準(zhǔn)確率才接近瓶頸。規(guī)模越大越復(fù)雜的神經(jīng)網(wǎng)絡(luò)模型,可以達(dá)到的準(zhǔn)確率水平越高,但是也相應(yīng)地需要更多的數(shù)據(jù)才能訓(xùn)練好,在數(shù)據(jù)量小時(shí)反而容易過擬合。我們可以看到Large?NN在數(shù)據(jù)量小的時(shí)候,并不比常規(guī)算法好,直到數(shù)據(jù)量持續(xù)擴(kuò)大才慢慢超越了常規(guī)算法、Small?NN和Medium?NN,并在最后達(dá)到了一個(gè)非常高的準(zhǔn)確率。根據(jù)Alex在cuda-convnet上的測試結(jié)果,如果不對CIFAR-10數(shù)據(jù)使用數(shù)據(jù)增強(qiáng),那么錯(cuò)誤率最低可以下降到17%;使用數(shù)據(jù)增強(qiáng)后,錯(cuò)誤率可以下降到11%左右,模型性能的提升非常顯著。

圖5-6??傳統(tǒng)機(jī)器學(xué)習(xí)算法和深度學(xué)習(xí)在不同數(shù)據(jù)量下的表現(xiàn)

從本章的例子中可以發(fā)現(xiàn),卷積層一般需要和一個(gè)池化層連接,卷積加池化的組合目前已經(jīng)是做圖像識(shí)別時(shí)的一個(gè)標(biāo)準(zhǔn)組件了。卷積網(wǎng)絡(luò)最后的幾個(gè)全連接層的作用是輸出分類結(jié)果,前面的卷積層主要做特征提取的工作,直到最后的全連接層才開始對特征進(jìn)行組合匹配,并進(jìn)行分類。卷積層的訓(xùn)練相對于全連接層更復(fù)雜,訓(xùn)練全連接層基本是進(jìn)行一些矩陣乘法運(yùn)算,而目前卷積層的訓(xùn)練基本依賴于cuDNN的實(shí)現(xiàn)(另有nervana公司的neon也占有一席之地)。其中的算法相對復(fù)雜,有些方法(比如Facebook開源的算法)還會(huì)涉及傅里葉變換。同時(shí),卷積層的使用有很多Trick,除了本章提到的方法,實(shí)際上有很多方法可以防止CNN過擬合,加快收斂速度或者提高泛化性,這些會(huì)在后續(xù)章節(jié)中講解。

經(jīng)過6年多的發(fā)展,LSGO軟件技術(shù)團(tuán)隊(duì)在地理信息系統(tǒng)、數(shù)據(jù)統(tǒng)計(jì)分析、計(jì)算機(jī)視覺領(lǐng)域積累了豐富的研發(fā)經(jīng)驗(yàn),也建立了人才培養(yǎng)的完備體系。

本團(tuán)隊(duì)希望能與其他科研團(tuán)隊(duì)進(jìn)行交流合作,并共同成長進(jìn)步。

本微信公眾平臺(tái)長期系統(tǒng)化提供有關(guān)機(jī)器學(xué)習(xí)、軟件研發(fā)、教育及學(xué)習(xí)方法、數(shù)學(xué)建模的知識(shí),并將以上知識(shí)轉(zhuǎn)化為實(shí)踐。拒絕知識(shí)碎片化、耐心打磨技能、解決實(shí)際問題是我們的宗旨和追求。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容