神經網絡學習(七)MNIST手寫字識別 --- Python實現

系列博客是博主學習神經網絡中相關的筆記和一些個人理解,僅爲作者記錄筆記之用,不免有很多細節不對之處。

說明

上一節,我們介紹了MNIST手寫字的Matlab實現,本節我們看看它的一個簡單的Python實現(警告:博主是Python小白),本節代碼是參考了 Michael Nielsen的neural networks and deep learning相關代碼基礎上完成的。博主用的Python版本爲3.6。Michael Nielsen的代碼是2.7版本的,如果想在3.6版本下使用,需要修改幾個地方,

  1. 將 mnist_loader.py 中的 import cPickle 修改爲 import pickle
  2. 將 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’)
  3. training_data, validation_data, test_data需要用list()函數處理下
  4. 若干個print函數需要修改

本節全部代碼可以在這裏下載到(沒有積分的朋友可以私信我),

實現

我在前面的幾個程序中,對隨機梯度中的隨機理解有些偏差,我的理解是在每次更新過程(一次迭代)中隨機選取一些樣本進行模型更新,並且把一次迭代稱爲一個epoch,這個理解是不恰當的,且看下面這段話(摘自解析卷積神經網絡-深度學習實踐的手冊)

批處理的隨機梯度下降法(mini-batch SGD)在訓練模型階段隨機選取n 個樣本作爲一批(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%,但是訓練次數太多後就不穩定了,精度反而急劇下降。

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