系列博客是博主學習神經網絡中相關的筆記和一些個人理解,僅爲作者記錄筆記之用,不免有很多細節不對之處。
說明
上一節,我們介紹了MNIST手寫字的Matlab實現,本節我們看看它的一個簡單的Python實現(警告:博主是Python小白),本節代碼是參考了 Michael Nielsen的neural networks and deep learning相關代碼基礎上完成的。博主用的Python版本爲3.6。Michael Nielsen的代碼是2.7版本的,如果想在3.6版本下使用,需要修改幾個地方,
- 將 mnist_loader.py 中的 import cPickle 修改爲 import pickle
- 將 mnist_loader.py 中 的
load_data()
函數中 training_data, validation_data, test_data = cPickle.load(f) 語句修改爲 training_data, validation_data, test_data = pickle.load(f,encoding = ‘latin1’) - training_data, validation_data, test_data需要用
list()
函數處理下 - 若干個print函數需要修改
本節全部代碼可以在這裏下載到(沒有積分的朋友可以私信我),
實現
我在前面的幾個程序中,對隨機梯度中的隨機理解有些偏差,我的理解是在每次更新過程(一次迭代)中隨機選取一些樣本進行模型更新,並且把一次迭代稱爲一個epoch,這個理解是不恰當的,且看下面這段話(摘自解析卷積神經網絡-深度學習實踐的手冊)
批處理的隨機梯度下降法(mini-batch SGD)在訓練模型階段隨機選取 個樣本作爲一批(batch)樣本,先通過前饋運算得到預測並計算其誤差,後通過梯度下降法更新參數,梯度從後往前逐層反饋,直至更新到網絡的第一層參數,這樣的一個參數更新過程稱爲一個”批處理過程”(mini-batch)。不同批處理之間按照無放回抽樣遍歷所有訓練樣本集樣本,遍歷一次訓練樣本稱爲“一輪”(epoch)。
通過上面這段話可知,一個epoch內無放回遍歷所有樣本,我在之前的程序中把一個epoch理解爲一次模型更新了,並且樣本選取屬於有放回進行的,這樣導致的一個後果是有些數據可能始終都沒有參與到模型更新中,造成數據的浪費。(以後的程序更正這個問題,鑑於前面的程序分類效果還OK,博主太懶不再更正了)
Michael Nielsen 的相關代碼參見neural networks and deep learning,這裏就不做過多介紹了。爲了與他的 network(他有三個版本的,第一個版本爲 network ,兩個優化版本爲 network2 和 network3)相區別,我自己編寫的神經網絡稱爲gnetwork.
下面是運行腳本
import mnist_loader
import matplotlib.pyplot as plt
import time
import network
import gnetwork
#數據載入
training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
test_data = list(test_data)
training_data = list(training_data)
#Michael Nielsen的network
net = network.Network([784,30,10])
start = time.clock()
net.SGD(training_data, 30, 10, 3.0, test_data=test_data)
print("Time elapsed: ",(time.clock()-start))
#我的network
gnet = gnetwork.gNetwork([784,30,10])
start = time.clock()
gnet.SGD(training_data, 30, 10, 3.0, test_data=test_data)
print("Time elapsed: ",(time.clock()-start))
#繪圖
plt.figure()
plt.plot(net.accuracy,color = "r",label = "Nielsen's network")
plt.plot(gnet.accuracy,color = "b",label = "Guo's network")
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.legend()
下面是運行結果
下面是 mini_batch_size = 15 的結果
上面的兩張圖是單次結果,多次試驗兩者基本沒有差。進行對比僅僅是爲了驗證我們的程序是不是可以達到正常分類水平。Michael Nielsen 稱他的第一個版本的 network 識別率可以超過 96%。我運行了多次,始終沒有出現超過96%的情況(這充分說明:調參有多麼重要!)
Michael Nielsen 的 network 是沒有進行優化的,運行效率還是要低一些的。我自己編寫的這個的效率會高一些,下面有個簡單的對比
mini_batch_size | Michael Nielsen | Guo chf |
---|---|---|
10 | 147.2 | 34.8 |
15 | 149.3 | 25.9 |
下面看看 gNetwork 的實現,Python 實現與前面的 Matlab 實現大同小異,
"""
gnetwork.py
~~~~~~~~~~
採用隨機梯度法進行前饋神經網絡訓練的模塊,梯度訓練使用BP算法
"""
#### Libraries
# Standard library
import random
# Third-party libraries
import numpy as np
class gNetwork(object):
def __init__(self, sizes):
"""
sizes:list類型,表示網絡結構,比如[2,3,1]表示輸入層2個神經元,
一個包含3個神經元的隱層,1個輸出層
"""
self.num_layers = len(sizes)
self.sizes = sizes
#初始化網絡
self.biases = [np.random.randn(y, 1) for y in sizes[1:]]
self.weights = [np.random.randn(y, x)
for x, y in zip(sizes[:-1], sizes[1:])]
#爲了方便代碼編寫,weights和biases與網絡結構相同,輸入層爲空即可
self.biases.insert(0,[])
self.weights.insert(0,[])
self.accuracy=[];
def feedforward(self,a,z):
"""Return the output of the network"""
#z=w*x+b,a=sigma(z)
for ik in range(1,self.num_layers):
z[ik] = np.dot(self.weights[ik], a[ik-1])+self.biases[ik]
a[ik] = sigmoid(z[ik])
def SGD(self, training_data, epochs, mini_batch_size, eta,test_data=None):
#中間變量,weights和biases的導數,以及激活值a和帶權輸入z
nabla_biases = self.biases[:]
nabla_weights = self.weights[:]
a = [[] for i in range(self.num_layers)]
z = [[] for i in range(self.num_layers)]
#測試數據
if test_data:
n_test = len(test_data)
x_test = np.squeeze(np.array([d[0] for d in test_data]).transpose())
y_test = np.squeeze(np.array([d[1] for d in test_data]).transpose())
n = len(training_data)
#主循環
for j in range(epochs):
#對樣本數據隨機排列
random.shuffle(training_data)
#mini_batch
mini_batches = [
training_data[k:k+mini_batch_size]
for k in range(0, n, mini_batch_size)]
for mini_batch in mini_batches:
#將tuple的數據處理成矩陣形式,這樣可以一次計算所有的mini_batch
#比在mini_batch中循環單個樣本快很多
x_train = np.squeeze(np.array([x[0] for x in mini_batch])).transpose()
y_train = np.squeeze(np.array([y[1] for y in mini_batch])).transpose()
a[0] = x_train
#前饋
self.feedforward(a,z)
#反向傳播誤差,並更新模型
self.update_mini_batch(y_train,a,z,nabla_weights,nabla_biases,mini_batch_size,eta)
if test_data:
yp = self.evaluate(x_test,y_test)
self.accuracy.append(yp/n_test)
print("Epoch {%d}: {%d} / {%d}" %(j, self.evaluate(x_test,y_test), n_test))
else:
print("Epoch {%d} complete" %(j))
def update_mini_batch(self,y_train,a,z,nabla_weights,nabla_biases,mini_batch_size,eta):
#計算輸出層,公式BP1
delta = (a[-1]-y_train)*sigmoid_prime(z[-1])
nabla_biases[-1][:,0] = np.mean(delta,axis = 1)
nabla_weights[-1] = np.dot(delta,a[-2].transpose())/mini_batch_size
#反向傳播誤差
for m in range(2,self.num_layers):
#公式BP2
delta = np.dot(self.weights[-m+1].transpose(),delta)*sigmoid_prime(z[-m])
#公式BP3
nabla_biases[-m][:,0] = np.mean(delta,axis = 1)
#公式BP4
nabla_weights[-m] = np.dot(delta,a[-m-1].transpose())/mini_batch_size
#更新模型
for m in range(1,self.num_layers):
self.weights[m] = self.weights[m] - eta*nabla_weights[m];
self.biases[m] = self.biases[m] - eta*nabla_biases[m];
def evaluate(self, x_test,y_test):
a = x_test[:]
for ik in range(1,self.num_layers):
a = sigmoid(np.dot(self.weights[ik], a)+self.biases[ik])
test_result = np.argmax(a,axis = 0);
return sum(int(x == y) for (x, y) in zip(test_result,y_test))
#### Miscellaneous functions
def sigmoid(z):
"""The sigmoid function."""
return 1.0/(1.0+np.exp(-z))
def sigmoid_prime(z):
"""Derivative of the sigmoid function."""
s = sigmoid(z)
return s*(1-s)
Michael Nielsen 的 network 計算思路是:先把整個訓練樣本分爲若干個mini_batch,再對 mini_batch 內的每個樣本進行循序,計算每個樣本所對應的誤差,最後對 mini_batch 內的樣本求和取平均更新模型。
上面的這段代碼是一次性計算完整個 mini_batch 的誤差,不做 for
循環,從而提高了計算效率。
提升一下
下面咱們進一步提高下分類精度,如咱們討論的那樣,利用ReLU
激活函數代替Sigmoid
函數,但是需要注意的是,在模型參數初始化的使用rand()
函數而不是randn()
函數。具體程序如下
"""
gnetwork1.py
~~~~~~~~~~
採用隨機梯度法進行前饋神經網絡訓練的模塊,梯度訓練使用BP算法
中間層採用ReLU激活函數,輸出層採用Sigmoid函數
"""
#### Libraries
# Standard library
import random
# Third-party libraries
import numpy as np
class gNetwork(object):
def __init__(self, sizes):
self.num_layers = len(sizes)
self.sizes = sizes
#初始化網絡,注意下這裏
self.biases = [np.random.rand(y, 1)-0.5 for y in sizes[1:]]
self.weights = [np.random.rand(y, x)-0.5
for x, y in zip(sizes[:-1], sizes[1:])]
#爲了方便代碼編寫,weights和biases與網絡結構相同,輸入層爲空即可
self.biases.insert(0,[])
self.weights.insert(0,[])
self.accuracy=[];
def feedforward(self,a,z):
"""Return the output of the network"""
#z=w*x+b,a=sigma(z)
for ik in range(1,self.num_layers-1):
z[ik] = np.dot(self.weights[ik], a[ik-1])+self.biases[ik]
a[ik] = relu(z[ik])
ik = ik+1
z[ik] = np.dot(self.weights[ik], a[ik-1])+self.biases[ik]
a[ik] = sigmoid(z[ik])
def SGD(self, training_data, epochs, mini_batch_size, eta,test_data=None):
#中間變量,weights和biases的導數,以及激活值a和帶權輸入z
nabla_biases = self.biases[:]
nabla_weights = self.weights[:]
a = [[] for i in range(self.num_layers)]
z = [[] for i in range(self.num_layers)]
#測試數據
if test_data:
n_test = len(test_data)
x_test = np.squeeze(np.array([d[0] for d in test_data]).transpose())
y_test = np.squeeze(np.array([d[1] for d in test_data]).transpose())
n = len(training_data)
#主循環
for j in range(epochs):
#對樣本數據隨機排列
random.shuffle(training_data)
#mini_batch
mini_batches = [
training_data[k:k+mini_batch_size]
for k in range(0, n, mini_batch_size)]
for mini_batch in mini_batches:
#將tuple的數據處理成矩陣形式,這樣可以一次計算所有的mini_batch
#比在mini_batch中循環單個樣本快很多
x_train = np.squeeze(np.array([x[0] for x in mini_batch])).transpose()
y_train = np.squeeze(np.array([y[1] for y in mini_batch])).transpose()
a[0] = x_train
#前饋
self.feedforward(a,z)
#反向傳播誤差,並更新模型
self.update_mini_batch(y_train,a,z,nabla_weights,nabla_biases,mini_batch_size,eta)
if test_data:
yp = self.evaluate(x_test,y_test)
self.accuracy.append(yp/n_test)
print("Epoch {%d}: {%d} / {%d}" %(j, self.evaluate(x_test,y_test), n_test))
else:
print("Epoch {%d} complete" %(j))
def update_mini_batch(self,y_train,a,z,nabla_weights,nabla_biases,mini_batch_size,eta):
#計算輸出層,公式BP1
delta = (a[-1]-y_train)*sigmoid_prime(z[-1])
nabla_biases[-1][:,0] = np.mean(delta,axis = 1)
nabla_weights[-1] = np.dot(delta,a[-2].transpose())/mini_batch_size
#反向傳播誤差
for m in range(2,self.num_layers):
#公式BP2
delta = np.dot(self.weights[-m+1].transpose(),delta)*relu_prime(z[-m])
#公式BP3
nabla_biases[-m][:,0] = np.mean(delta,axis = 1)
#公式BP4
nabla_weights[-m] = np.dot(delta,a[-m-1].transpose())/mini_batch_size
#更新模型
for m in range(1,self.num_layers):
self.weights[m] = self.weights[m] - eta*nabla_weights[m];
self.biases[m] = self.biases[m] - eta*nabla_biases[m];
def evaluate(self, x_test,y_test):
a = x_test[:]
for ik in range(1,self.num_layers-1):
a = relu(np.dot(self.weights[ik], a)+self.biases[ik])
ik = ik+1
a = sigmoid(np.dot(self.weights[ik], a)+self.biases[ik])
test_result = np.argmax(a,axis = 0);
return sum(int(x == y) for (x, y) in zip(test_result,y_test))
#### Miscellaneous functions
def sigmoid(z):
"""The sigmoid function."""
return 1.0/(1.0+np.exp(-z))
def sigmoid_prime(z):
"""Derivative of the sigmoid function."""
s = sigmoid(z)
return s*(1-s)
def relu(z):
"""The Rectifier Linear Unit function."""
a = z[:]
a[a<0] = 0
return a
def relu_prime(z):
"""Derivative of the ReLU function."""
a = z-z
a[z>0] = 1
return a
運行腳本如下:
import mnist_loader
import matplotlib.pyplot as plt
import time
import gnetwork1
#數據載入
training_data, validation_data, test_data = mnist_loader.load_data_wrapper()
test_data = list(test_data)
training_data = list(training_data)
#gnetwork1
gnet1 = gnetwork1.gNetwork([784,30,10])
start = time.clock()
gnet1.SGD(training_data,60,100,3.0, test_data=test_data)
print("Time elapsed: ",(time.clock()-start))
#繪圖
plt.figure()
plt.plot(gnet1.accuracy,color = "r",label = "network:[784,30,10]")
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim([0.94,0.98])
plt.grid(1)
plt.legend()
運行結果如下:
在20個epoch以內可以達到96.0%的識別率,最高可以達到96.5%。設置多個隱層後,分辨率稍有提高,偶爾可達到97.0%,但是訓練次數太多後就不穩定了,精度反而急劇下降。