【深度學習】多層感知機(一)Python從零開始實現雙層感知機

環境介紹

Ubuntu 18.04 + PyCharm 2018 + Anaconda3(Python3是大勢所趨)
Anaconda = 集成了常用包的Python,這裏不做過多介紹。
上述環境中,最好保持Python版本一致(Python3),其餘的關係不大。

定義神經網絡的框架

考慮一個神經網絡,很容易可以抽象出三種操作:

  1. 初始化函數:指定神經網絡的層數,每一層的節點個數等,即指定神經網絡的結構;
  2. 訓練函數:通過訓練數據集優化權重;
  3. 查詢函數:通過測試數據集測試訓練後的神經網絡。

爲此,給出如下神經網絡的類定義(神經網絡的框架),文件名爲neural_network.py

# coding=utf-8
# author: BebDong
# 10/23/18


# neural network definition
class NeuralNetwork:

    # initialise the neural network
    def __init__(self):
        pass

    # train the network using training data set
    def training(self):
        pass

    # query the network using test data set
    def query(self):
        pass

初始化

根據分析,編寫初始化函數__init__(),指定神經網絡的結構。

# initialise the neural network
def __init__(self, numInputNodes, numHiddenNodes, numOutputNodes, learningRate):
    # 單隱藏層示例,設置各層的節點個數
    self.numInputNodes = numInputNodes
    self.numHiddenNodes = numHiddenNodes
    self.numOutputNodes = numOutputNodes
        
    # 權重更新時的學習率
    self.learningRate = learningRate
    pass

創建網絡節點和鏈接

簡單均勻分佈隨機初始權重

網絡中最重要的部分就算鏈接權重,我們使用權重來得到輸出、反向傳播誤差、並優化權重本身來得到更加優化的結果。

  1. 示例使用單隱藏層(即一共2層,輸入層不做任何操作),故而需要兩個矩陣來存儲權重
  2. 輸入層和隱藏層權重矩陣大小爲numHiddenNodes*numInputNodes,隱藏層和輸出層權重矩陣大小爲numOutputNodes*numHiddenNodes
  3. 初始權重應該較小,隨機且不爲0(理解這一點,需要理解神經網絡的本質思想)
  4. 使用numpy包來生成隨機權重矩陣
  5. __init__()函數中定義
# 初始化權重: 加上偏移-0.5是爲了使權重分佈在(-0.5,0.5)
self.weightInputHidden = (numpy.random.rand(self.numHiddenNodes, self.numInputNodes) - 0.5)
self.weightHiddenOutput = (numpy.random.rand(self.numOutputNodes, self.numHiddenNodes) - 0.5)

正態分佈初始權重

對於設置鏈接的初始權重有一個經驗規則:在一個節點傳入鏈接數量平方根倒數的範圍內隨機採樣,即從均值爲0、標準方差等於節點傳入鏈接數量平方根倒數的正態分佈中進行採樣。
後文中,我們將採用這種方式。

# 正態分佈初始化權重
self.weightInputHidden = numpy.random.normal(0.0, pow(self.numHiddenNodes, -0.5),
                                                     (self.numHiddenNodes, self.numInputNodes))
self.weightHiddenOutput = numpy.random.normal(0.0, pow(self.numOutputNodes, -0.5),
                                                      (self.numOutputNodes, self.numHiddenNodes))

編寫查詢函數

查詢函數query()用於從訓練好的神經網絡處獲取輸出集進行預測。

  1. 網絡使用sigmoid激活函數,y=11+exy=\frac{1}{1+e^{-x}},在SciPy中定義爲expit()
  2. __init__()中定義激活函數,這樣可以方便地擴展激活函數或者改變激活函數
  3. 使用numpy進行矩陣運算
# 激活函數(lambda創建匿名函數)
self.activation_function = lambda x: scipy.special.expit(x)

至今的所有代碼如下:

# coding=utf-8
# author: BebDong
# 10/23/18

import numpy
import scipy.special


# neural network definition
class NeuralNetwork:

    # initialise the neural network
    def __init__(self, numInputNodes, numHiddenNodes, numOutputNodes, learningRate):
        # 單隱藏層示例,設置各層的節點個數
        self.numInputNodes = numInputNodes
        self.numHiddenNodes = numHiddenNodes
        self.numOutputNodes = numOutputNodes

        # 權重更新時的學習率
        self.learningRate = learningRate

        # 正態分佈初始化權重
        self.weightInputHidden = numpy.random.normal(0.0, pow(self.numHiddenNodes, -0.5),
                                                     (self.numHiddenNodes, self.numInputNodes))
        self.weightHiddenOutput = numpy.random.normal(0.0, pow(self.numOutputNodes, -0.5),
                                                      (self.numOutputNodes, self.numHiddenNodes))

        # 激活函數(lambda創建匿名函數)
        self.activation_function = lambda x: scipy.special.expit(x)
        pass

    # train the network using training data set
    def training(self):
        pass

    # query the network using test data set
    def query(self, inputs_list):
        # 將輸入一維數組轉化成二維,並轉置
        inputs = numpy.array(inputs_list, ndmin=2).T
        
        # 計算到達隱藏層的信號,即隱藏層輸入
        hidden_inputs = numpy.dot(self.weightInputHidden, inputs)
        # 計算隱藏層輸出,即經過sigmoid函數的輸出
        hidden_outputs = self.activation_function(hidden_inputs)
        
        # 計算到達輸出層的信號,即輸出層的輸入
        final_inputs = numpy.dot(self.weightHiddenOutput, hidden_outputs)
        # 計算最終的輸出
        final_outputs = self.activation_function(final_inputs)

        return final_outputs

階段性測試

到目前爲止,已經完成了神經網絡的初始化和query()的功能,按理說可以通過一個輸入得到一個輸出了。下面在編寫訓練函數之前先測試目前的所有代碼。
編寫一個測試文件test.py:

# coding=utf-8
# author: BebDong
# 10/23/18

import neural_network

input_nodes = 3
hidden_nodes = 3
output_nodes = 3

learning_rate = 0.3

n = neural_network.NeuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)

print(n.query([1.0, 0.5, -1.5]))

程序運行正常並輸出類似如下結果:
在這裏插入圖片描述
程序運行的結果取決於:

  1. 隨機產生的初始權重;
  2. 網絡的大小和結構

編寫訓練函數

訓練函數training()完成兩件事情:

  1. 第一階段,同query()根據輸入得到輸出
  2. 第二階段,反向傳播誤差更新鏈接權重

第一階段,同query()函數:

# 第一,同query()函數
inputs = numpy.array(inputs_list, ndmin=2).T
targets = numpy.array(targets_list, ndmin=2).T
hidden_inputs = numpy.dot(self.weightInputHidden, inputs)
hidden_outputs = self.activation_function(hidden_inputs)
final_inputs = numpy.dot(self.weightHiddenOutput, hidden_outputs)
final_outputs = self.activation_function(final_inputs)

第二階段,誤差反向傳播並更新權重。
首先計算各層的誤差:

# 計算誤差
output_errors = targets - final_outputs

# 反向傳播誤差到隱藏層
hidden_errors = numpy.dot(self.weightHiddenOutput.T, output_errors)

對於輸入層和隱藏層之間的鏈接權重,使用hidden_errors來更新,對於隱藏層和輸出層之間的權重,使用output_errors來進行更新。
接着使用梯度下降的方法來更新權重(公式此處不進行推導):

# 更新隱藏層和輸出層之間的權重
self.weightHiddenOutput += self.learningRate * numpy.dot((output_errors * final_outputs *
                                                                  (1.0 - final_outputs)),
                                                                 numpy.transpose(hidden_outputs))
# 更新輸入層和隱藏層之間的權重
self.weightInputHidden += self.learningRate * numpy.dot((hidden_errors * hidden_outputs *
                                                                 (1.0 - hidden_outputs)),
                                                                numpy.transpose(inputs))

神經網絡的所有代碼

到現在爲止,我們從0開始封裝了一個單隱藏層的簡單神經網絡。如下爲完整代碼:

# coding=utf-8
# author: BebDong
# 10/23/18

import numpy
import scipy.special


# neural network definition
class NeuralNetwork:

    # initialise the neural network
    def __init__(self, numInputNodes, numHiddenNodes, numOutputNodes, learningRate):
        # 單隱藏層示例,設置各層的節點個數
        self.numInputNodes = numInputNodes
        self.numHiddenNodes = numHiddenNodes
        self.numOutputNodes = numOutputNodes

        # 權重更新時的學習率
        self.learningRate = learningRate

        # 正態分佈初始化權重
        self.weightInputHidden = numpy.random.normal(0.0, pow(self.numHiddenNodes, -0.5),
                                                     (self.numHiddenNodes, self.numInputNodes))
        self.weightHiddenOutput = numpy.random.normal(0.0, pow(self.numOutputNodes, -0.5),
                                                      (self.numOutputNodes, self.numHiddenNodes))

        # 激活函數(lambda創建匿名函數)
        self.activation_function = lambda x: scipy.special.expit(x)
        
        pass

    # train the network using training data set
    def training(self, inputs_list, targets_list):
        # 第一,同query()函數
        inputs = numpy.array(inputs_list, ndmin=2).T
        targets = numpy.array(targets_list, ndmin=2).T
        hidden_inputs = numpy.dot(self.weightInputHidden, inputs)
        hidden_outputs = self.activation_function(hidden_inputs)
        final_inputs = numpy.dot(self.weightHiddenOutput, hidden_outputs)
        final_outputs = self.activation_function(final_inputs)

        # 計算誤差
        output_errors = targets - final_outputs
        # 反向傳播誤差到隱藏層
        hidden_errors = numpy.dot(self.weightHiddenOutput.T, output_errors)

        # 更新隱藏層和輸出層之間的權重
        self.weightHiddenOutput += self.learningRate * numpy.dot((output_errors * final_outputs *
                                                                  (1.0 - final_outputs)),
                                                                 numpy.transpose(hidden_outputs))
        # 更新輸入層和隱藏層之間的權重
        self.weightInputHidden += self.learningRate * numpy.dot((hidden_errors * hidden_outputs *
                                                                 (1.0 - hidden_outputs)),
                                                                numpy.transpose(inputs))

        pass

    # query the network using test data set
    def query(self, inputs_list):
        # 將輸入一維數組轉化成二維,並轉置
        inputs = numpy.array(inputs_list, ndmin=2).T

        # 計算到達隱藏層的信號,即隱藏層輸入
        hidden_inputs = numpy.dot(self.weightInputHidden, inputs)
        # 計算隱藏層輸出,即經過sigmoid函數的輸出
        hidden_outputs = self.activation_function(hidden_inputs)

        # 計算到達輸出層的信號,即輸出層的輸入
        final_inputs = numpy.dot(self.weightHiddenOutput, hidden_outputs)
        # 計算最終的輸出
        final_outputs = self.activation_function(final_inputs)

        return final_outputs

識別手寫數字數據集MNIST

數據集介紹

完整數據集選擇和獲取

數據集網站:http://yann.lecun.com/exdb/mnist/
易用的數據格式:https://pjreddie.com/projects/mnist-in-csv/
原始數據網站提供的數據格式不易使用,爲此我們在實驗中使用他人提供的.csv格式的數據集,包含一個訓練數據集(60000樣本)和一個測試數據集(10000樣本)。
在這裏插入圖片描述

數據集解釋

打開數據集文件,可以得到如下格式:
在這裏插入圖片描述
第一列表示label列,即正確的答案,表示這張圖片代表的數字。後面的784列表示一個28*28像素的圖片,每個值表示每個像素點的像素值。

一種數據子集的選擇

使用較小的數據子集來提高計算機的執行時間效率,當確定算法和代碼有效之後,可以使用完整的數據集。
這裏我們選擇訓練子集(100樣本)和測試子集(10樣本)。

直觀的展示數據

編寫test.py,選擇數據集中的一條記錄,將這張手寫數字圖片繪製出來:

# coding=utf-8
# author: BebDong
# 10/23/18

import numpy
import matplotlib.pyplot as plt

# 直接使用plt.imshow無法顯示圖片,需要導入pylab包
import pylab

# 打開並讀取文件
data_file = open("mnist_dataset/mnist_train_100.csv")
data_list = data_file.readlines()
data_file.close()

# 拆分繪製28*28圖形
all_pixels = data_list[0].split(',')
image_array = numpy.asfarray(all_pixels[1:]).reshape((28, 28))
plt.figure("Image")
plt.imshow(image_array, cmap='gray', interpolation='None')
pylab.show()

我們選擇了訓練數據集的第一條記錄,繪製結果如下:
在這裏插入圖片描述

對輸入數據做必要的變換

目前,已經有了數據集和定義好的神經網絡,好像可以直接將數據丟給神經網路開始訓練了!?真的是這樣嗎?
思考:像素點的值取值範圍爲[0,255][0,255],觀察sigmoid函數的圖像,如下圖所示。當sigmoid的輸入過大或者過小時,激活函數的梯度極小,這將限制神經網絡的學習能力。所以需要將輸入顏色的值進行縮放,使得其分佈在激活函數梯度較大的舒適區域內,從而使神經網絡更好的工作。

sigmoid函數圖像

這裏,將輸入顏色值從[0,255][0,255]範圍縮放至[0.01,1.0][0.01,1.0],選擇0.01作爲起點,是爲了避免0值輸入會造成權重更新失敗的問題(梯度下降進行權重更新的時候,有一項是乘以輸入矩陣,有興趣的讀者可以自行推導公式)。

# 縮放輸入數據。0.01的偏移量避免0值輸入
scaled_inputs = (numpy.asfarray(all_pixels[1:])/255.0 * 0.99) + 0.01

考慮輸出數據

sigmoid函數的輸出範圍爲(0,1)(0,1),如果我們想讓神經網絡輸出圖片的像素數組的話,其值在[0,255][0,255]之間,看起來需要調整目標值以適應激活函數的範圍?
考慮另外一種方案,我們需要神經網絡判斷一個輸入圖片代表的是數字幾,即輸出一個[0,9][0,9]區間的數字,共10個數字。所以可以設置輸出層節點個數爲10:如果答案是"0",則輸出層第一個節點激發,如果答案是"7",則輸出層的第8個節點激發。激發的意思是此節點數值明顯大於0。
使用這種方法,需要對訓練數據集做一定的調整,比如當label列爲"5"的時候,對應的目標輸出應該類似:[0.01, 0.01, 0.01, 0.01, 0.01, 0.99, 0.01, 0.01, 0.01, 0.01],即第6個節點被激發。

# 構建目標矩陣。sigmoid函數無法取端點值0或者1,使用0.01代替0,0.99代替1
output_nodes = 10
# 產生0值輸出矩陣
targets = numpy.zeros(output_nodes) + 0.01
# 將字符串轉換爲整數,並設置激發節點
targets[int(all_pixels[0])] = 0.99

編寫代碼,進行試驗

新建experiment.py,編寫試驗代碼:

  1. 隱藏層節點個數不唯一,可以多次實驗進行調整
  2. 這裏訓練數據集僅100條記錄,故一次性讀入內存。當數據集很大時,這樣的方法不可取
# coding=utf-8
# author: BebDong
# 2018.10.23

import neural_network as nn
import numpy

# 指定神經網絡的結構。隱藏層節點個數不唯一
input_nodes, hidden_nodes, output_nodes = 784, 100, 10

# 指定權重更新的學習率
learning_rate = 0.3

# 創建神經網絡的實例
network = nn.NeuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)

# 讀取訓練數據,只讀方式
training_data_file = open("mnist_dataset/mnist_train_100.csv", 'r')
# 當數據集很大時,應當分批讀入內存。這裏僅100條記錄,則一次性全部讀入內存
training_data_list = training_data_file.readlines()
training_data_file.close()

# 訓練神經網絡
for record in training_data_list:
    # 縮放輸入
    all_pixels = record.split(',')
    scaled_inputs = (numpy.asfarray(all_pixels[1:])/255.0 * 0.99) + 0.01
    # 創建目標輸出
    targets = numpy.zeros(output_nodes) + 0.01
    targets[int(all_pixels[0])] = 0.99
    network.training(scaled_inputs, targets)
    pass

# 讀取測試數據集
test_data_file = open("mnist_dataset/mnist_test_10.csv", 'r')
test_data_list = test_data_file.readlines()
test_data_file.close()

# 測試訓練好的神經網絡
# 初始化一個數據結構用於記錄神經網絡的表現
scorecard = []
# 遍歷測試數據集
for record in test_data_list:
    # 打印預期輸出
    all_pixels = record.split(',')
    correct_label = int(all_pixels[0])
    print("correct label: ", correct_label)
    # 查詢神經網絡
    inputs = (numpy.asfarray(all_pixels[1:])/255.0 * 0.99) + 0.01
    outputs = network.query(inputs)
    answer = numpy.argmax(outputs)
    print("network's answer: ", answer)
    # 更新神經網絡的表現
    if answer == correct_label:
        scorecard.append(1)
    else:
        scorecard.append(0)
        pass
    pass

# 打印得分
print(scorecard)
print("performance: ", sum(scorecard) / len(scorecard))

運行可以得到如下結果:
在這裏插入圖片描述
可以發現,在本次實驗中神經網絡的準確率達到了70%。在訓練樣本僅100的情況下,已經是一個很好的實驗結果。
另外,experiment.py中的代碼可以通過函數進行封裝,這樣可以使其更簡潔並且方便維護,有興趣的同學可以自己嘗試着封裝,這裏不再重複。

總結

本文通過實現一個簡單的三層神經網絡,介紹了神經網絡的基本實現過程,並使用Python和MNIST手寫數據集進行了實驗,結果可以說令人興奮。

  1. 要理解神經網絡中信號傳播的矩陣表示,利用矩陣運算可以極大的簡化代碼量;
  2. 要了解誤差反向傳播的原理,及sigmoid函數梯度下降更新權重的函數。(即誤差其實是權重的函數,這裏不再推導);
  3. 完整數據集的測試留給讀者自行完成;
  4. 神經網絡的可調節參數:學習率、網絡的結構(各層的節點數量),以及使用數據集進行多次訓練等等。這裏不再展示相關結果,感興趣的同學可以自行實驗;
  5. 神經網絡中的可調節參數可以單獨封裝爲.yml或者.xml配置文件(其他類型也可),便於維護和調整;
  6. 完整的項目代碼可以在github上下載:https://github.com/BebDong/NumRecognition

完整數據集測試及性能評估

  1. 下面的完整數據集測試同樣是一次性將數據讀入內存;
  2. 使用訓練樣本訓練兩次;
  3. 訓練次數不可太多,防止過度擬合。
# coding=utf-8
# author: BebDong
# 2018.10.23

import neural_network as nn
import numpy
import time

# 便於計算執行時間
start = time.process_time()

# 指定神經網絡的結構。隱藏層節點個數不唯一
input_nodes, hidden_nodes, output_nodes = 784, 100, 10

# 指定權重更新的學習率
learning_rate = 0.3

# 創建神經網絡的實例
network = nn.NeuralNetwork(input_nodes, hidden_nodes, output_nodes, learning_rate)

# 讀取訓練數據,只讀方式
training_data_file = open("mnist_dataset/mnist_train.csv", 'r')
# 當數據集很大時,應當分批讀入內存。這裏僅100條記錄,則一次性全部讀入內存
training_data_list = training_data_file.readlines()
training_data_file.close()

# 訓練神經網絡,epochs次
epochs = 2
for e in range(epochs):
    for record in training_data_list:
        # 縮放輸入
        all_pixels = record.split(',')
        scaled_inputs = (numpy.asfarray(all_pixels[1:]) / 255.0 * 0.99) + 0.01
        # 創建目標輸出
        targets = numpy.zeros(output_nodes) + 0.01
        targets[int(all_pixels[0])] = 0.99
        network.training(scaled_inputs, targets)
        pass
    pass

# 讀取測試數據集
test_data_file = open("mnist_dataset/mnist_test.csv", 'r')
test_data_list = test_data_file.readlines()
test_data_file.close()

# 測試訓練好的神經網絡
# 初始化一個數據結構用於記錄神經網絡的表現
scorecard = []
# 遍歷測試數據集
for record in test_data_list:
    # 打印預期輸出
    all_pixels = record.split(',')
    correct_label = int(all_pixels[0])
    # 查詢神經網絡
    inputs = (numpy.asfarray(all_pixels[1:])/255.0 * 0.99) + 0.01
    outputs = network.query(inputs)
    answer = numpy.argmax(outputs)
    # 更新神經網絡的表現
    if answer == correct_label:
        scorecard.append(1)
    else:
        scorecard.append(0)
        pass
    pass

# 打印得分及運行時間
print("time: ", time.process_time()-start)
print("performance: ", sum(scorecard) / len(scorecard))

我的運行結果,準確率大概在95%左右:
在這裏插入圖片描述

一個有趣的想法

我們都知道神經網絡就像一個黑盒子,我們無法知道它的內部如何工作,我們往往只關注答案是否準確本身。

神經網絡學習到的知識通過鏈接中權重來反映。跟人腦不同的是,這種反映實質上不能稱之爲對於這個問題的理解或者智慧。個人認爲,僅僅只能將這種學習到的知識看做對樣本空間特徵的一種固化。

如果我們將信號的傳播方向反過來,我們從輸出層輸入一個標籤,看看輸入層會輸出一個什麼樣的圖像呢?

這裏需要將sigmoid激活函數換成它的反函數,作爲信號反向傳播時的“激活函數”。很容易根據y=11+exy=\frac{1}{1+e^{-x}}得到反函數x=ln[y1y]x=ln[\frac{y}{1-y}],這個函數由scipy.special.logit()提供。

有興趣的同學可以做做這個實驗看看會發生什麼!?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章