1. 程式人生 > >【深度學習】Python實現簡單神經網路

【深度學習】Python實現簡單神經網路

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. 示例使用單隱藏層(即一共3層),故而需要兩個矩陣來儲存權重
  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 = 1 1 + e x y=\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開始封裝了一個單隱藏層(即一共3層)的簡單神經網路。如下為完整程式碼:

# 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. 神經網路中的可調節引數可以單獨封裝為