文章目錄
環境介紹
Ubuntu 18.04 + PyCharm 2018 + Anaconda3(Python3是大勢所趨)
Anaconda = 集成了常用包的Python,這裏不做過多介紹。
上述環境中,最好保持Python版本一致(Python3),其餘的關係不大。
定義神經網絡的框架
考慮一個神經網絡,很容易可以抽象出三種操作:
- 初始化函數:指定神經網絡的層數,每一層的節點個數等,即指定神經網絡的結構;
- 訓練函數:通過訓練數據集優化權重;
- 查詢函數:通過測試數據集測試訓練後的神經網絡。
爲此,給出如下神經網絡的類定義(神經網絡的框架),文件名爲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
創建網絡節點和鏈接
簡單均勻分佈隨機初始權重
網絡中最重要的部分就算鏈接權重,我們使用權重來得到輸出、反向傳播誤差、並優化權重本身來得到更加優化的結果。
- 示例使用單隱藏層(即一共2層,輸入層不做任何操作),故而需要兩個矩陣來存儲權重
- 輸入層和隱藏層權重矩陣大小爲
numHiddenNodes*numInputNodes
,隱藏層和輸出層權重矩陣大小爲numOutputNodes*numHiddenNodes
- 初始權重應該較小,隨機且不爲0(理解這一點,需要理解神經網絡的本質思想)
- 使用numpy包來生成隨機權重矩陣
- 在
__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()
用於從訓練好的神經網絡處獲取輸出集進行預測。
- 網絡使用sigmoid激活函數,,在SciPy中定義爲
expit()
- 在
__init__()
中定義激活函數,這樣可以方便地擴展激活函數或者改變激活函數 - 使用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]))
程序運行正常並輸出類似如下結果:
程序運行的結果取決於:
- 隨機產生的初始權重;
- 網絡的大小和結構
編寫訓練函數
訓練函數training()
完成兩件事情:
- 第一階段,同
query()
根據輸入得到輸出 - 第二階段,反向傳播誤差更新鏈接權重
第一階段,同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()
我們選擇了訓練數據集的第一條記錄,繪製結果如下:
對輸入數據做必要的變換
目前,已經有了數據集和定義好的神經網絡,好像可以直接將數據丟給神經網路開始訓練了!?真的是這樣嗎?
思考:像素點的值取值範圍爲,觀察sigmoid函數的圖像,如下圖所示。當sigmoid的輸入過大或者過小時,激活函數的梯度極小,這將限制神經網絡的學習能力。所以需要將輸入顏色的值進行縮放,使得其分佈在激活函數梯度較大的舒適區域內,從而使神經網絡更好的工作。
這裏,將輸入顏色值從範圍縮放至,選擇0.01作爲起點,是爲了避免0值輸入會造成權重更新失敗的問題(梯度下降進行權重更新的時候,有一項是乘以輸入矩陣,有興趣的讀者可以自行推導公式)。
# 縮放輸入數據。0.01的偏移量避免0值輸入
scaled_inputs = (numpy.asfarray(all_pixels[1:])/255.0 * 0.99) + 0.01
考慮輸出數據
sigmoid函數的輸出範圍爲,如果我們想讓神經網絡輸出圖片的像素數組的話,其值在之間,看起來需要調整目標值以適應激活函數的範圍?
考慮另外一種方案,我們需要神經網絡判斷一個輸入圖片代表的是數字幾,即輸出一個區間的數字,共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
,編寫試驗代碼:
- 隱藏層節點個數不唯一,可以多次實驗進行調整
- 這裏訓練數據集僅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手寫數據集進行了實驗,結果可以說令人興奮。
- 要理解神經網絡中信號傳播的矩陣表示,利用矩陣運算可以極大的簡化代碼量;
- 要了解誤差反向傳播的原理,及sigmoid函數梯度下降更新權重的函數。(即誤差其實是權重的函數,這裏不再推導);
- 完整數據集的測試留給讀者自行完成;
- 神經網絡的可調節參數:學習率、網絡的結構(各層的節點數量),以及使用數據集進行多次訓練等等。這裏不再展示相關結果,感興趣的同學可以自行實驗;
- 神經網絡中的可調節參數可以單獨封裝爲
.yml
或者.xml
配置文件(其他類型也可),便於維護和調整; - 完整的項目代碼可以在github上下載:https://github.com/BebDong/NumRecognition
完整數據集測試及性能評估
- 下面的完整數據集測試同樣是一次性將數據讀入內存;
- 使用訓練樣本訓練兩次;
- 訓練次數不可太多,防止過度擬合。
# 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激活函數換成它的反函數,作爲信號反向傳播時的“激活函數”。很容易根據得到反函數,這個函數由scipy.special.logit()
提供。
有興趣的同學可以做做這個實驗看看會發生什麼!?