神經(jīng)網(wǎng)絡(luò)中 BP 算法的原理與 Python 實現(xiàn)源碼解析

最近這段時間系統(tǒng)性的學習了BP算法后寫下了這篇學習筆記,因為能力有限彤敛,若有明顯錯誤,還請指出

目錄

  • 什么是梯度下降和鏈式求導法則
  • 神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu)
  • BP算法中的執(zhí)行流程(前向傳遞和逆向更新)
  • 輸出層和隱藏層權(quán)重以及偏置更新的推導
  • Python 實現(xiàn)源碼解析
  • 手寫數(shù)字識別實例
  • 訓練神經(jīng)網(wǎng)絡(luò)中有哪些難點(TODO)

梯度下降和鏈式求導法則

假設(shè)我們有一個函數(shù) J(w),如下圖所示抽米。

梯度下降示意圖

現(xiàn)在扶歪,我們要求當 w 等于什么的時候理肺,J(w) 能夠取到最小值。從圖中我們知道最小值在初始位置的左邊善镰,也就意味著如果想要使 J(w) 最小妹萨,w 的值需要減小。而初始位置的切線的斜率 a > 0(也即該位置對應(yīng)的導數(shù)大于0)炫欺,w = w – a 就能夠讓 w 的值減小乎完,循環(huán)求導更新 w 直到 J(w) 取得最小值。如果函數(shù) J(w) 包含多個變量品洛,那么就要分別對不同變量求偏導來更新不同變量的值树姨。

所謂的鏈式求導法則,就是求復合函數(shù)的導數(shù)

鏈式求導法則

放個例題毫别,會更加明白一點:


鏈式求導的例子

神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu)

神經(jīng)網(wǎng)絡(luò)由三部分組成娃弓,分別是最左邊的輸入層,隱藏層(實際應(yīng)用中遠遠不止一層)和最右邊的輸出層岛宦。層與層之間用線連接在一起台丛,每條連接線都有一個對應(yīng)的權(quán)重值 w,除了輸入層,一般來說每個神經(jīng)元還有對應(yīng)的偏置 b

神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu)圖

除了輸入層的神經(jīng)元挽霉,每個神經(jīng)元都會有加權(quán)求和得到的輸入值 z 和將 z 通過 Sigmoid 函數(shù)(也即是激活函數(shù))非線性轉(zhuǎn)化后的輸出值 a防嗡,他們之間的計算公式如下

神經(jīng)元輸出值 a 的計算公式

其中,公式里面的變量l和j表示的是第 l 層的第 j 個神經(jīng)元侠坎,ij 則表示從第 i 個神經(jīng)元到第 j 個神經(jīng)元之間的連線蚁趁,w 表示的是權(quán)重,b 表示的是偏置实胸,后面這些符號的含義大體上與這里描述的相似他嫡,所以不會再說明。下面的 Gif 動圖可以更加清楚每個神經(jīng)元輸入輸出值的計算方式(注意庐完,這里的動圖并沒有加上偏置钢属,但使用中一般都會加上 )

動圖顯示計算神經(jīng)元輸出值

使用激活函數(shù)的原因是因為線性模型(無法處理線性不可分的情況)的表達能力不夠,所以這里通常需要加入 Sigmoid 函數(shù)來加入非線性因素得到神經(jīng)元的輸出值门躯。

關(guān)于為什么線性函數(shù)模型表達能力不夠淆党,可以點擊這里查看知乎上面的討論

sigmoid 函數(shù)

可以看到 Sigmoid 函數(shù)的值域為 (0,1) ,若對于多分類任務(wù)讶凉,輸出層的每個神經(jīng)元可以表示是該分類的概率染乌。當然還存在其他的激活函數(shù),他們的用途和優(yōu)缺點也都各異懂讯。


BP 算法執(zhí)行的流程

在手工設(shè)定了神經(jīng)網(wǎng)絡(luò)的層數(shù)荷憋,每層的神經(jīng)元的個數(shù),學習率 η(下面會提到)后域醇,BP 算法會先隨機初始化每條連接線權(quán)重和偏置台谊,然后對于訓練集中的每個輸入 x 和輸出 y,BP 算法都會先執(zhí)行前向傳輸?shù)玫筋A(yù)測值譬挚,然后根據(jù)真實值與預(yù)測值之間的誤差執(zhí)行逆向反饋更新神經(jīng)網(wǎng)絡(luò)中每條連接線的權(quán)重和每層的偏好锅铅。在沒有到達停止條件的情況下重復上述過程。

其中减宣,停止條件可以是下面這三條

  • 權(quán)重的更新低于某個閾值的時候
  • 預(yù)測的錯誤率低于某個閾值
  • 達到預(yù)設(shè)一定的迭代次數(shù)

譬如說盐须,手寫數(shù)字識別中,一張手寫數(shù)字1的圖片儲存了28*28 = 784個像素點漆腌,每個像素點儲存著灰度值(值域為[0,255])贼邓,那么就意味著有784個神經(jīng)元作為輸入層,而輸出層有 10 個神經(jīng)元代表數(shù)字 09闷尿,每個神經(jīng)元取值為01塑径,代表著這張圖片是這個數(shù)字的概率。

每輸入一張圖片(也就是實例)填具,神經(jīng)網(wǎng)絡(luò)會執(zhí)行前向傳輸一層一層的計算到輸出層神經(jīng)元的值统舀,根據(jù)哪個輸出神經(jīng)元的值最大來預(yù)測輸入圖片所代表的手寫數(shù)字

然后根據(jù)輸出神經(jīng)元的值匆骗,計算出預(yù)測值與真實值之間的誤差,再逆向反饋更新神經(jīng)網(wǎng)絡(luò)中每條連接線的權(quán)重和每個神經(jīng)元的偏好

前向傳輸(Feed-Forward)

從輸入層=>隱藏層=>輸出層誉简,一層一層的計算所有神經(jīng)元輸出值的過程碉就。

逆向反饋(Back Propagation)

因為輸出層的值與真實的值會存在誤差,我們可以用均方誤差來衡量預(yù)測值和真實值之間的誤差

均方誤差

逆向反饋的目標就是讓E函數(shù)的值盡可能的小闷串,而每個神經(jīng)元的輸出值是由該點的連接線對應(yīng)的權(quán)重值和該層對應(yīng)的偏好所決定的瓮钥,因此,要讓誤差函數(shù)達到最小烹吵,我們就要調(diào)整w和b值碉熄, 使得誤差函數(shù)的值最小。

權(quán)重和偏置的更新公式

對目標函數(shù) E 求 w 和 b 的偏導可以得到 w 和 b 的更新量肋拔,下面拿求 w 偏導來做推導

其中 η 為學習率具被,取值通常為 0.1 ~ 0.3,可以理解為每次梯度所邁的步伐。注意到 w_hj 的值先影響到第 j 個輸出層神經(jīng)元的輸入值a只损,再影響到輸出值y,根據(jù)鏈式求導法則有

使用鏈式法則展開對權(quán)重求偏導

根據(jù)神經(jīng)元輸出值 a 的定義有

對函數(shù) z 求 w 的偏導

Sigmoid 求導數(shù)的式子如下七咧,從式子中可以發(fā)現(xiàn)其在計算機中實現(xiàn)也是非常的方便

Sigmoid 函數(shù)求導

所以

則權(quán)重 w 的更新量為

類似可得 b 的更新量為

但這兩個公式只能夠更新輸出層與前一層連接線的權(quán)重和輸出層的偏置跃惫,原因是因為 δ 值依賴了真實值y這個變量,但是我們只知道輸出層的真實值而不知道每層隱藏層的真實值艾栋,導致無法計算每層隱藏層的 δ 值爆存,所以我們希望能夠利用 l+1 層的 δ 值來計算 l 層的 δ 值,而恰恰通過一些列數(shù)學轉(zhuǎn)換后可以做到蝗砾,這也就是逆向反饋名字的由來先较,公式如下:

從式子中我們可以看到,我們只需要知道下一層的權(quán)重和神經(jīng)元輸出層的值就可以計算出上一層的 δ 值悼粮,我們只要通過不斷的利用上面這個式子就可以更新隱藏層的全部權(quán)重和偏置了

在推導之前請先觀察下面這張圖

l 和 l+1 層的神經(jīng)元

首先我們看到 l 層的第 i 個神經(jīng)元與 l+1 層的所有神經(jīng)元都有連接闲勺,那么我們可以將 δ 展開成如下的式子

也即是說我們可以將 E 看做是 l+1 層所有神經(jīng)元輸入值的 z 函數(shù),而上面式子的 n 表示的是 l+1 層神經(jīng)元的數(shù)量扣猫,再進行化簡后就可以得到上面所說的式子菜循。

在這里的推導過程只解釋了關(guān)鍵的部分,如果要查看更加詳細的推導內(nèi)容申尤,可以點擊此處下載我在學習過程中參考的一篇 pdf 文檔癌幕,里面的推導過程非常詳細。另外也參考了周志華所寫的機器學習中的神經(jīng)網(wǎng)絡(luò)部分的內(nèi)容和neuralnetworksanddeeplearning 的內(nèi)容


源碼解析

源碼來自于 Michael Nielsen 大神的深度學習在線教程昧穿,但他的內(nèi)容都是英文的勺远,我結(jié)合了自己的理解和上面的理論知識對源碼進行了注釋。>>點擊此處查看整理的代碼和數(shù)字識別實例<<

使用 Python 實現(xiàn)的神經(jīng)網(wǎng)絡(luò)的代碼行數(shù)并不多时鸵,僅包含一個 Network 類胶逢,首先來看看該類的構(gòu)造方法

def __init__(self, sizes):
        """
        :param sizes: list類型,儲存每層神經(jīng)網(wǎng)絡(luò)的神經(jīng)元數(shù)目
                      譬如說:sizes = [2, 3, 2] 表示輸入層有兩個神經(jīng)元、
                      隱藏層有3個神經(jīng)元以及輸出層有2個神經(jīng)元
        """
        # 有幾層神經(jīng)網(wǎng)絡(luò) 
        self.num_layers = len(sizes)
        self.sizes = sizes
        # 除去輸入層宪塔,隨機產(chǎn)生每層中 y 個神經(jīng)元的 biase 值(0 - 1)
        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
        # 隨機產(chǎn)生每條連接線的 weight 值(0 - 1)
        self.weights = [np.random.randn(y, x)
                        for x, y in zip(sizes[:-1], sizes[1:])]

向前傳輸(FreedForward)的代碼

    def feedforward(self, a):
        """
        前向傳輸計算每個神經(jīng)元的值
        :param a: 輸入值
        :return: 計算后每個神經(jīng)元的值
        """
        for b, w in zip(self.biases, self.weights):
            # 加權(quán)求和以及加上 biase
            a = sigmoid(np.dot(w, a)+b)
        return a

源碼里使用的是隨機梯度下降(Stochastic Gradient Descent磁奖,簡稱 SGD),原理與梯度下降相似某筐,不同的是隨機梯度下降算法每次迭代只取數(shù)據(jù)集中一部分的樣本來更新 w 和 b 的值比搭,速度比梯度下降快,但是南誊,它不一定會收斂到局部極小值身诺,可能會在局部極小值附近徘徊。

    def SGD(self, training_data, epochs, mini_batch_size, eta,
            test_data=None):
        """
        隨機梯度下降
        :param training_data: 輸入的訓練集
        :param epochs: 迭代次數(shù)
        :param mini_batch_size: 小樣本數(shù)量
        :param eta: 學習率 
        :param test_data: 測試數(shù)據(jù)集
        """
        if test_data: n_test = len(test_data)
        n = len(training_data)
        for j in xrange(epochs):
            # 攪亂訓練集抄囚,讓其排序順序發(fā)生變化
            random.shuffle(training_data)
            # 按照小樣本數(shù)量劃分訓練集
            mini_batches = [
                training_data[k:k+mini_batch_size]
                for k in xrange(0, n, mini_batch_size)]
            for mini_batch in mini_batches:
                # 根據(jù)每個小樣本來更新 w 和 b霉赡,代碼在下一段
                self.update_mini_batch(mini_batch, eta)
            # 輸出測試每輪結(jié)束后,神經(jīng)網(wǎng)絡(luò)的準確度
            if test_data:
                print "Epoch {0}: {1} / {2}".format(
                    j, self.evaluate(test_data), n_test)
            else:
                print "Epoch {0} complete".format(j)

根據(jù) backprop 方法得到的偏導數(shù)更新 w 和 b 的值

    def update_mini_batch(self, mini_batch, eta):
        """
        更新 w 和 b 的值
        :param mini_batch: 一部分的樣本
        :param eta: 學習率
        """
        # 根據(jù) biases 和 weights 的行列數(shù)創(chuàng)建對應(yīng)的全部元素值為 0 的空矩陣
        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:
            # 根據(jù)樣本中的每一個輸入 x 的其輸出 y幔托,計算 w 和 b 的偏導數(shù)
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            # 累加儲存偏導值 delta_nabla_b 和 delta_nabla_w 
            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)]
        # 更新根據(jù)累加的偏導值更新 w 和 b穴亏,這里因為用了小樣本,
        # 所以 eta 要除于小樣本的長度
        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)]

下面這塊代碼是源碼最核心的部分重挑,也即 BP 算法的實現(xiàn)嗓化,包含了前向傳輸和逆向反饋,前向傳輸在 Network 里有單獨一個方法(上面提到的 feedforward 方法)谬哀,那個方法是用于驗證訓練好的神經(jīng)網(wǎng)絡(luò)的精確度的刺覆,在下面有提到該方法。

    def backprop(self, x, y):
        """
        :param x:
        :param y:
        :return:
        """
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        # 前向傳輸
        activation = x
        # 儲存每層的神經(jīng)元的值的矩陣史煎,下面循環(huán)會 append 每層的神經(jīng)元的值
        activations = [x] 
        # 儲存每個未經(jīng)過 sigmoid 計算的神經(jīng)元的值
        zs = [] 
        for b, w in zip(self.biases, self.weights):
            z = np.dot(w, activation)+b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)
        # 求 δ 的值
        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 xrange(2, self.num_layers):
            # 從倒數(shù)第 l 層開始更新谦屑,-l 是 python 中特有的語法表示從倒數(shù)第 l 層開始計算
            # 下面這里利用 l+1 層的 δ 值來計算 l 的 δ 值
            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)

接下來則是 evaluate 的實現(xiàn),調(diào)用 feedforward 方法計算訓練好的神經(jīng)網(wǎng)絡(luò)的輸出層神經(jīng)元值(也即預(yù)測值)篇梭,然后比對正確值和預(yù)測值得到精確率氢橙。

    def evaluate(self, test_data):
        # 獲得預(yù)測結(jié)果
        test_results = [(np.argmax(self.feedforward(x)), y)
                        for (x, y) in test_data]
        # 返回正確識別的個數(shù)
        return sum(int(x == y) for (x, y) in test_results)

** 最后,我們可以利用這個源碼來訓練一個手寫數(shù)字識別的神經(jīng)網(wǎng)絡(luò)恬偷,并輸出評估的結(jié)果充蓝,代碼如下:**

import mnist_loader
import network

training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
net = network.Network([784, 30, 10])
net.SGD(training_data, 30, 10, 3.0, test_data = test_data)
# 輸出結(jié)果
# Epoch 0: 9038 / 10000
# Epoch 1: 9178 / 10000
# Epoch 2: 9231 / 10000
# ...
# Epoch 27: 9483 / 10000
# Epoch 28: 9485 / 10000
# Epoch 29: 9477 / 10000

可以看到,在經(jīng)過 30 輪的迭代后喉磁,識別手寫神經(jīng)網(wǎng)絡(luò)的精確度在 95% 左右谓苟,當然,設(shè)置不同的迭代次數(shù)协怒,學習率以取樣數(shù)對精度都會有影響涝焙,如何調(diào)參也是一門技術(shù)活,這個坑就后期再填吧孕暇。

總結(jié)

神經(jīng)網(wǎng)絡(luò)的優(yōu)點:

網(wǎng)絡(luò)實質(zhì)上實現(xiàn)了一個從輸入到輸出的映射功能仑撞,而數(shù)學理論已證明它具有實現(xiàn)任何復雜非線性映射的功能赤兴。這使得它特別適合于求解內(nèi)部機制復雜的問題
網(wǎng)絡(luò)能通過學習帶正確答案的實例集自動提取“合理的”求解規(guī)則,即具有自學習能力
網(wǎng)絡(luò)具有一定的推廣隧哮、概括能力

神經(jīng)網(wǎng)絡(luò)的缺點:

對初始權(quán)重非常敏感桶良,極易收斂于局部極小
容易 Over Fitting 和 Over Training
如何選擇隱藏層數(shù)和神經(jīng)元個數(shù)沒有一個科學的指導流程,有時候感覺就是靠猜

應(yīng)用領(lǐng)域:

常見的有圖像分類沮翔,自動駕駛陨帆,自然語言處理等


TODO

但其實想要訓練好一個神經(jīng)網(wǎng)絡(luò)還面臨著很多的坑(譬如下面四條)需要我在后面的簡書中不斷的填上

  1. 如何選擇超參數(shù)的值,譬如說神經(jīng)網(wǎng)絡(luò)的層數(shù)和每層的神經(jīng)元數(shù)量以及學習率
  2. 既然對初始化權(quán)重敏感采蚀,那該如何避免和修正
  3. Sigmoid 激活函數(shù)在深度神經(jīng)網(wǎng)絡(luò)中會面臨梯度消失問題該如何解決
  4. 避免 Overfitting 的 L1 和 L2正則化是什么

參考

[1] 周志華 機器學習
[2] 斯坦福大學機器學習在線課程
[[3] Parallel Distributed Processing (1986, by David E. Rumelhart, James L. McClelland), Chapter 8 Learning Internal Representations by Error Propagation](http://psych.stanford. edu/~jlm/papers/PDP/Volume%201/Chap8_PDP86.pdf)
[[4] How the backpropagation algorithm works](http://neuralnetworksanddeeplearning.com/chap2. html)
[[5] Backpropagation Algorithm](http://deeplearning.stanford.edu/wiki/index.php/Backpropagation_ Algorithm)
[6] 鏈式求導法則代芜,臺灣中華科技大學數(shù)位課程瓶摆,Youtube 視頻瘟芝,需要翻墻俱笛,順便安利一下他們的數(shù)學相關(guān)的視頻,因為做的都非常淺顯易懂

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末妆够,一起剝皮案震驚了整個濱河市识啦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌神妹,老刑警劉巖袁滥,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異灾螃,居然都是意外死亡,警方通過查閱死者的電腦和手機揩徊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門腰鬼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人塑荒,你說我怎么就攤上這事熄赡。” “怎么了齿税?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵彼硫,是天一觀的道長。 經(jīng)常有香客問我凌箕,道長拧篮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任牵舱,我火速辦了婚禮串绩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘芜壁。我一直安慰自己礁凡,他們只是感情好高氮,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著顷牌,像睡著了一般剪芍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上窟蓝,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天罪裹,我揣著相機與錄音,去河邊找鬼疗锐。 笑死坊谁,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的滑臊。 我是一名探鬼主播口芍,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼雇卷!你這毒婦竟也來了鬓椭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤关划,失蹤者是張志新(化名)和其女友劉穎小染,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贮折,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡裤翩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了调榄。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片踊赠。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖每庆,靈堂內(nèi)的尸體忽然破棺而出筐带,到底是詐尸還是另有隱情,我是刑警寧澤缤灵,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布伦籍,位于F島的核電站,受9級特大地震影響腮出,放射性物質(zhì)發(fā)生泄漏帖鸦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一胚嘲、第九天 我趴在偏房一處隱蔽的房頂上張望富蓄。 院中可真熱鬧,春花似錦慢逾、人聲如沸立倍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽口注。三九已至变擒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間寝志,已是汗流浹背娇斑。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留材部,地道東北人毫缆。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像乐导,于是被迫代替她去往敵國和親苦丁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

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