ML:自己動(dòng)手實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)算法識(shí)別mnist手寫數(shù)字(準(zhǔn)確率92.11%)


  • 注意:這里的代碼都是在Jupyter Notebook中運(yùn)行,原始的.ipynb文件可以在我的GitHub主頁(yè)上下載 https://github.com/acphart/Hand_on_ML_Algorithm 其中的ANN_StochasticGradientDescent.ipynb就是這篇博客的文件,里面包括代碼、注釋以及交互式運(yùn)行結(jié)果,界面十分友好,讀者可以下載后直接在Jupyter Notebook中打開即可,在這里作者也強(qiáng)烈推薦使用Jupyter Notebook進(jìn)行學(xué)習(xí),這里面還有其他的機(jī)器學(xué)習(xí)算法實(shí)現(xiàn),這個(gè)代碼也在我的另外一個(gè)項(xiàng)目 GitHub:acphart/Deep_in_mnist 中,里面有更多的關(guān)于mnist手寫數(shù)字識(shí)別的代碼,喜歡可以順便給個(gè)star哦 ~~~

介紹

參考文獻(xiàn)

  • Michael A. Nielsen, “Neural Networks and Deep Learning”, Determination Press, 2015.

項(xiàng)目介紹

  • 這里實(shí)現(xiàn)了一個(gè)可以學(xué)習(xí)mnist手寫數(shù)字特征的人工神經(jīng)網(wǎng)絡(luò)類ANN
  • 此版本的ANN基于隨機(jī)梯度下降算法實(shí)現(xiàn),梯度由反向傳播算法計(jì)算,神經(jīng)元激活函數(shù)為sigmoid(),或稱logistic 函數(shù)。
  • 此項(xiàng)目主要為學(xué)習(xí)之用,在實(shí)現(xiàn)核心功能的前提下,盡量編寫簡(jiǎn)單、易于閱讀的代碼,暫時(shí)沒(méi)有考慮優(yōu)化。

反向傳播算法的公式證明

  • 這里花了很多冤枉時(shí)間,原本證明單個(gè)權(quán)重或者偏置的梯度公式不怎么麻煩,但是我吃著沒(méi)事做,硬是啃了兩三天想實(shí)現(xiàn)同時(shí)向網(wǎng)絡(luò)裝入一批訓(xùn)練數(shù)據(jù)并計(jì)算平均梯度,這樣可以利用numpy對(duì)運(yùn)算加速,結(jié)果最后在證公式、向量化運(yùn)算和debug中跪了 ~~
  • 最后其實(shí)還是從上面列出來(lái)的參考書中看到,一次送一個(gè)訓(xùn)練數(shù)據(jù)進(jìn)ann,只不過(guò)多了一層很小的循環(huán),整個(gè)過(guò)程就簡(jiǎn)潔多了 ~~
  • 由于這里篇幅原因,其實(shí)主要是要碼的公式太多。。。,詳細(xì)證明過(guò)程等有空專門寫一篇博客,然后在這里更新鏈接。

下一步計(jì)劃

  • 進(jìn)一步調(diào)參
  • 加入正則化,嘗試L1正則和L2正則
  • 改變神經(jīng)元激活函數(shù),嘗試雙曲正切函數(shù)tanh()和修正線性單元ReLU()
  • 搭建卷積神經(jīng)網(wǎng)絡(luò),使得網(wǎng)絡(luò)可以學(xué)習(xí)圖像結(jié)構(gòu)特征

步驟

1. 導(dǎo)入工具庫(kù)和準(zhǔn)備數(shù)據(jù)

  • 這里主要需要借助numpy進(jìn)行向量運(yùn)算
import random
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.core.interactiveshell import InteractiveShell

import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
import warnings
warnings.filterwarnings('ignore')

# InteractiveShell.ast_node_interactivity = 'all'
  • 數(shù)據(jù)讀入之后轉(zhuǎn)換成np.ndarray對(duì)象
  • 這里的all_mnist_data.csv是重新包裝后的所有原始mnist數(shù)據(jù),共70000個(gè)手寫數(shù)字,數(shù)據(jù)詳情及下載請(qǐng)閱讀我GitHub主頁(yè)上的介紹GitHub:acphart/Deep_in_mnist
data = pd.read_csv('../dataset/all_mnist_data.csv').values

2. 定義神經(jīng)網(wǎng)絡(luò)類ANN

  • ANN目前提供給外部使用的就是以下四個(gè)方法

  • ANN其他方法為內(nèi)部方法調(diào)用

  • 所有方法的詳細(xì)文檔見方法定義處

  • ANN的使用方式如下:

  1. 構(gòu)建ANN實(shí)例并初始化:ann = ANN(sizes)
  2. 訓(xùn)練ANN:ann.SGD(train_data, epochs, batch_size, alpha[, test_data]);
  3. 評(píng)估ANN:ann.evaluate(test_data);
  4. 預(yù)測(cè):ann.predict(x)
class ANN(object):
    '''
    ANN:人工神經(jīng)網(wǎng)絡(luò)類:
    構(gòu)建ANN實(shí)例:ann = ANN(sizes),sizes含義見self__init__();
    訓(xùn)練ANN:ann.SGD(train_data, epochs, batch_size, alpha);
    評(píng)估ANN:ann.evaluate(test_X, test_y);
    預(yù)測(cè):ann.predict(x)。
    '''

    def __init__(self, sizes):
        '''
        sizes是ANN的規(guī)模;
        num_layers是ANN的層數(shù);
        biases是偏置矩陣列表,第l層的biases.shape設(shè)置為(nl, 1), nl為第l層的神經(jīng)元數(shù)目;
        weights是權(quán)重矩陣列表,第l層的weights.shape設(shè)置為(nl, nl-1), nl和nl-1分別是第l和l+1層的神經(jīng)元數(shù)目;
        如:sizes=[784, 30, 10]就是一個(gè)輸入層、隱藏層、輸出層分別是784,、30、10個(gè)神經(jīng)元的神經(jīng)網(wǎng)絡(luò);
        而相應(yīng)的biases為[(30,1), (10,1)],第一層沒(méi)有偏置值;weights為[(30, 784), (10, 30)]
        '''
        '''
        將biases.shape設(shè)置為(nl, 1),而不是(nl,)是為了能夠同時(shí)處理多個(gè)輸入;
        使用標(biāo)準(zhǔn)正態(tài)分布對(duì)權(quán)重進(jìn)行初始化,可以給我們的梯度下降算法一個(gè)起點(diǎn);
        但這不是最好的初始化方法,可以使用均值為0,標(biāo)準(zhǔn)差為1./sqrt(n)的正態(tài)分布進(jìn)行初始化。
        '''
        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:])]

    def feedforward(self, a):
        '''
        前向傳播,返回神經(jīng)網(wǎng)絡(luò)的輸出;
        輸入的a.shape is (n, 1),n是特征維度;
        返回的a.shape is (L, 1),L是輸出層神經(jīng)元個(gè)數(shù)。
        '''
        a = a.reshape(len(a), 1)
        for b, w in zip(self.biases, self.weights):
            a = sigmoid(np.dot(w, a) + b)
        return a

    def SGD(self, train_data, epochs, batch_size, alpha, 
            cv_data=None):
        '''
        隨機(jī)梯度下降:
        train_data是data_wrapper()包裝之后的訓(xùn)練數(shù)據(jù),數(shù)據(jù)格式見data_wrapper()函數(shù)定義處;
        epochs為迭代次數(shù);
        batch_size為采樣時(shí)的批量數(shù)據(jù)的大小;
        alpha是學(xué)習(xí)率;
        cv_data為可選參數(shù),是data_wrapper()包裝之后的交叉驗(yàn)證數(shù)據(jù),數(shù)據(jù)格式見data_wrapper()函數(shù)定義處;
        若給出了交叉驗(yàn)證數(shù)據(jù),則在每次訓(xùn)練后都會(huì)進(jìn)行性能評(píng)估,可用以跟蹤進(jìn)度,但會(huì)拖慢執(zhí)行速度。        
        '''            
        m = len(train_data)
        for j in range(1, epochs+1):
            '''
            在每個(gè)迭代期,首先將數(shù)據(jù)打亂,然后將它分成多個(gè)小批量數(shù)據(jù)batches;
            對(duì)于每一個(gè)小批量數(shù)據(jù)batch應(yīng)用一次梯度下降,通過(guò)調(diào)用self.grad_desent()完成。
            '''
            random.shuffle(train_data)
            batches = [train_data[k: k+batch_size] 
                       for k in range(0, m, batch_size)]
            for batch in batches:
                self.grad_desent(batch, alpha)
            
            if cv_data:
                '''若是給出了測(cè)試數(shù)據(jù),則進(jìn)行性能評(píng)估'''
                m_cv = len(cv_data)
                print("Epoch {0:<3d}: \t{1:>4d} / {2:<5d}".format(
                    j, self.evaluate(cv_data), m_cv))
            else:
                '''否則輸出此次迭代完成即可'''
                print("Epoch {0} complete".format(j))

    def grad_desent(self, batch, alpha):
        '''
        梯度下降,這里調(diào)用反向傳播函數(shù)self.backprop(),
        對(duì)batch中的每一個(gè)樣本計(jì)算梯度,然后適當(dāng)?shù)馗聶?quán)重和偏置。
        nabla_b 和nabla_w分別為偏置和權(quán)重的梯度;
        delta_nabla_b 和delta_nabla_w分別是單個(gè)訓(xùn)練數(shù)據(jù)(x, y)求得的梯度;
        '''
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        
        '''求得每一個(gè)訓(xùn)練數(shù)據(jù)(x, y)的梯度并累加到nabla_b和nabla_w中'''
        for x, y in batch:
            delta_nabla_b, delta_nabla_w = self.backprop(x, y)
            nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
            nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
        
        '''更新神經(jīng)網(wǎng)絡(luò)的權(quán)重和偏置'''
        self.weights = [w - (alpha/len(batch))*nw
                        for w, nw in zip(self.weights, nabla_w)]
        self.biases = [b - (alpha/len(batch))*nb
                       for b, nb in zip(self.biases, nabla_b)]

    def backprop(self, x, y):
        '''
        x.shape is (n,),  y.shape is (L,);
        反向傳播:計(jì)算梯度。
        nabla_b 和nabla_w分別為偏置和權(quán)重的梯度;
        activations為神經(jīng)元的激活值向量列表[a1, a2,...an],其中ai代表第i層神經(jīng)元的激活值向量;
        zs為神經(jīng)元的輸入權(quán)值向量列表[z2, z3,...zn],其中zi表示第i層神經(jīng)元的輸入權(quán)值向量;
        '''
        nabla_b = [np.zeros(b.shape) for b in self.biases]
        nabla_w = [np.zeros(w.shape) for w in self.weights]
        
        '''前向傳播,求得所有的激活值和輸入權(quán)重'''
        activation = x
        activations = [activation] 
        zs = [] 
        for b, w in zip(self.biases, self.weights): 
            z = np.dot(w, activation) + b
            zs.append(z)
            activation = sigmoid(z)
            activations.append(activation)
        
        '''反向傳播過(guò)程'''
        delta = (y - sigmoid(zs[-1])) * diff_sigm(zs[-1])
        nabla_b[-1] = delta
        nabla_w[-1] = np.dot(delta, activations[-2].T)
        for layer in range(2, self.num_layers):
            z = zs[-layer]
            delta = np.dot(self.weights[-layer+1].T, delta)
            nabla_b[-layer] = delta
            nabla_w[-layer] = np.dot(delta, activations[-layer-1].T)
        return (nabla_b, nabla_w)

    def evaluate(self, test_data):
        '''
        評(píng)價(jià)函數(shù),預(yù)測(cè)正確的個(gè)數(shù)。
        np.argmax函數(shù)返回?cái)?shù)組的最大值的序號(hào),實(shí)現(xiàn)從one-hot到數(shù)字的轉(zhuǎn)換;
        '''
        test_results = [(np.argmax(self.feedforward(x)), y)
                        for (x, y) in test_data]
        return sum(int(x == y) for (x, y) in test_results)
    
    def predict(self, x):
        '''預(yù)測(cè)函數(shù)'''
        return np.argmax(self.feedforward(x))

3. 外部函數(shù)

  • ANN中會(huì)用到一些外部函數(shù),定義如下:

  • sigmoid(z):神經(jīng)元激活函數(shù)
    sigmoid(z) = \frac{1}{1 + e^{-z}}

  • diff_sigm(z)sigmoid(z)的導(dǎo)數(shù)
    sigmoid^{'}(z) = \frac{-e^{-z}}{(1 + e^{-z})^2} = -sigmoid(z)*(1-sigmoid(z))

  • vectorize(j):見函數(shù)注釋

  • data_wrapper(data, s_size):數(shù)據(jù)包裝函數(shù)

  • Kaggle上下載的數(shù)據(jù)源文件格式對(duì)于這里構(gòu)建神經(jīng)網(wǎng)絡(luò)其實(shí)不是很方便,所以將數(shù)據(jù)重新包裝了,使得每一個(gè)數(shù)據(jù)都是(x, y)元組,其中x為特征向量,y為期望結(jié)果,即真實(shí)值。其中訓(xùn)練數(shù)據(jù)train_datay還需要進(jìn)行向量化轉(zhuǎn)換成one-hot向量,便于訓(xùn)練ANN。

  • 并且因?yàn)樽约簩?shí)現(xiàn)的ANN沒(méi)法使用電腦上的GPU,速度很慢,所以包裝函數(shù)可以指定一個(gè)s_size參數(shù),選取數(shù)據(jù)的一個(gè)小子集進(jìn)行調(diào)參,同時(shí)根據(jù)子集大小的不同,其中劃分訓(xùn)練集、交叉驗(yàn)證集和測(cè)試集的大小也不同,子集很大時(shí),交叉驗(yàn)證集和測(cè)試集均固定為1000.

def sigmoid(z):
    """激活函數(shù)"""
    return 1.0/(1.0+np.exp(-z))

def diff_sigm(z):
    """激活函數(shù)sigmoid()的導(dǎo)數(shù)"""
    return -sigmoid(z)*(1-sigmoid(z))

def vectorize(j):
    '''
    將數(shù)字轉(zhuǎn)換成one-hot向量
    如: 2 => [0,0,1,0,0,0,0,0,0,0] 
    '''
    vector = np.zeros((10, 1))
    vector[int(j)] = 1.0
    return vector

def data_wrapper(data, s_size):
    '''
    對(duì)數(shù)據(jù)重新包裝:
    data是原始的train.csv數(shù)據(jù)的np.ndarray格式;
    s_size是抽取的樣本的大小。
    返回的是train_data, cv_data, test_data :
    train_data為訓(xùn)練集,格式為元組列表[tp0, tp1,...],
    tpi = (xi, yi),xi為784維的特征向量,yi為對(duì)應(yīng)數(shù)字的one-hot向量;
    cv_data為交叉驗(yàn)證集,格式同訓(xùn)練集,但yi是對(duì)應(yīng)的數(shù)字標(biāo)簽,非one-hot向量;
    test_data為測(cè)試集,格式同cv_data。
    '''
    if s_size<=40000:
        train_size = int(s_size*0.6)
        cv_size = int(s_size*0.8)
    else:
        train_size = s_size - 20000
        cv_size = s_size - 10000

    s_data = data[:s_size, ]
    
    s_X = s_data[:train_size, 1:]
    s_y = s_data[:train_size, 0]
    s_cv_X = s_data[train_size:cv_size, 1:]
    s_cv_y = s_data[train_size:cv_size, 0]
    s_test_X = s_data[cv_size: , 1:]
    s_test_y = s_data[cv_size: , 0]
    
    train_X = [np.reshape(x, (784, 1)) for x in s_X]
    train_y = [vectorize(y) for y in s_y]
    train_data = list(zip(train_X, train_y))
    cv_X = [np.reshape(x, (784, 1)) for x in s_cv_X]
    cv_data = list(zip(cv_X, s_cv_y))
    test_X = [np.reshape(x, (784, 1)) for x in s_test_X]
    test_data = list(zip(test_X, s_test_y))
    
    return (train_data, cv_data, test_data)

3. 搭建ANN實(shí)例并訓(xùn)練網(wǎng)絡(luò)

  • 在這里體會(huì)到不能用GPU的痛苦了,也體會(huì)到調(diào)參的痛苦了 ~~

  • 先用大一點(diǎn)的學(xué)習(xí)率訓(xùn)練3代,再逐步減小學(xué)習(xí)率,同時(shí)查看測(cè)試結(jié)果

train_data, cv_data, test_data = data_wrapper(data, 70000)

ann = ANN([784, 100, 10])
'''SGD(self, train_data, epochs, batch_size, alpha, cv_data=None)'''
ann.SGD(train_data, 3, 30, 2.0, cv_data)
print('accuracy: ', ann.evaluate(test_data)/len(test_data))
Epoch 1  :  8880 / 10000
Epoch 2  :  9076 / 10000
Epoch 3  :  9124 / 10000
accuracy:  0.909
ann.SGD(train_data, 3, 30, 1.0, cv_data)
print('accuracy: ', ann.evaluate(test_data)/len(test_data))
Epoch 1  :  9212 / 10000
Epoch 2  :  9225 / 10000
Epoch 3  :  9242 / 10000
accuracy:  0.916
ann.SGD(train_data, 3, 30, 0.5, cv_data)
print('accuracy: ', ann.evaluate(test_data)/len(test_data))
Epoch 1  :  9245 / 10000
Epoch 2  :  9247 / 10000
Epoch 3  :  9243 / 10000
accuracy:  0.9175
ann.SGD(train_data, 3, 30, 0.2, cv_data)
print('accuracy: ', ann.evaluate(test_data)/len(test_data))
Epoch 1  :  9259 / 10000
Epoch 2  :  9269 / 10000
Epoch 3  :  9262 / 10000
accuracy:  0.9211
  • 調(diào)了幾次參數(shù),發(fā)現(xiàn)這么弄效果還不錯(cuò),我們的準(zhǔn)確率就到0.9211了

查看預(yù)測(cè)效果

def show_pic(ax, image, y_, label=None):
    '''
    作圖函數(shù):
    ax為Matplotlib.Axes對(duì)象;
    image為單個(gè)的mnist手寫數(shù)字特征向量,image.shape is (784,);
    y_為預(yù)測(cè)值;
    label為image對(duì)應(yīng)的真實(shí)數(shù)字標(biāo)簽。
    '''
    img = image.reshape(28, 28)
    ax.imshow(img, cmap='Greys')
    ax.axis('off')
    ax.text(28, 24, str(y_), color='r', fontsize=18)
    if label != None:
        ax.text(28, 12, str(label), color='black', fontsize=18)
  • 這里把前n * n 個(gè)識(shí)別錯(cuò)誤的手寫數(shù)字顯示出來(lái)
  • 每個(gè)手寫數(shù)字右上方黑色數(shù)字是真實(shí)值,右下方紅色數(shù)字是ann預(yù)測(cè)值
  • 結(jié)果顯示,這些錯(cuò)誤識(shí)別的數(shù)字有一部分的確不好辨認(rèn),但大部分我們?nèi)诉€是可以辨認(rèn)出來(lái)的。
  • 所以網(wǎng)絡(luò)還有改進(jìn)的空間 ~~
n = 10
fig, ax = plt.subplots(n, n, sharex=True, sharey=True)
ax = ax.flatten()
fig.set_size_inches(15, 10)
ax_id = 0
i = 0
while ax_id < n*n :
    x = test_data[i][0]
    y = test_data[i][1]
    y_ = ann.predict(x)
    if y_ != y:
        show_pic(ax[ax_id], x, y_, int(y))
        ax_id = ax_id + 1
    i = i + 1
        
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容