天天看點

Python純手動搭建BP神經網絡(手寫數字識别)實驗介紹

來源:投稿 作者:張宇

編輯:學姐

實驗介紹

實驗要求:

實作一個手寫數字識别程式,如下圖所示,要求神經網絡包含一個隐層,隐層的神經元個數為15。

Python純手動搭建BP神經網絡(手寫數字識别)實驗介紹

整體思路:

主要參考西瓜書第五章神經網絡部分的介紹,使用批量梯度下降對神經網絡進行訓練。

讀取并處理資料

資料讀取思路:

  • 資料集中給出的圖檔為28*28的灰階圖,利用 plt.imread() 函數将圖檔讀取出來後為 28x28 的數組,如果使用神經網絡進行訓練的話,我們可以将每一個像素視為一個特征,是以将圖檔利用numpy.reshape 方法将圖檔化為 1x784 的數組。讀取全部資料并将其添加進入資料集 data 内,将對應的标簽按同樣的順序添加進labels内。
  • 然後使用np.random.permutation方法将索引打亂,利用傳入的測試資料所占比例test_ratio将資料劃分為測試集與訓練集。
  • 使用訓練樣本的均值和标準差對訓練資料、測試資料進行标準化。标準化這一步很重要,開始忽視了标準化的環節,神經網絡精度一直達不到效果。
  • 傳回資料集

這部分代碼如下:

# 讀取圖檔資料 參數:資料路徑 測試集所占的比例
def loadImageData(trainingDirName='data/', test_ratio=0.3):
    from os import listdir
    data = np.empty(shape=(0, 784))
    labels = np.empty(shape=(0, 1))

    for num in range(10):
        dirName = trainingDirName + '%s/' % (num)  # 擷取目前數字檔案路徑
        # print(listdir(dirName))
        nowNumList = [i for i in listdir(dirName) if i[-3:] == 'bmp']  # 擷取裡面的圖檔檔案
        labels = np.append(labels, np.full(shape=(len(nowNumList), 1), fill_value=num), axis=0)  # 将圖檔标簽加入
        for aNumDir in nowNumList:  # 将每一張圖檔讀入
            imageDir = dirName + aNumDir  # 構造圖檔路徑
            image = plt.imread(imageDir).reshape((1, 784))  # 讀取圖檔資料
            data = np.append(data, image, axis=0)  
    # 劃分資料集
    m = data.shape[0]
    shuffled_indices = np.random.permutation(m)  # 打亂資料
    test_set_size = int(m * test_ratio)
    test_indices = shuffled_indices[:test_set_size]
    train_indices = shuffled_indices[test_set_size:]

    trainData = data[train_indices]
    trainLabels = labels[train_indices]

    testData = data[test_indices]
    testLabels = labels[test_indices]

    # 對訓練樣本和測試樣本使用統一的均值 标準差進行歸一化
    tmean = np.mean(trainData)
    tstd = np.std(testData)

    trainData = (trainData - tmean) / tstd
    testData = (testData - tmean) / tstd
    return trainData, trainLabels, testData, testLabels
           

OneHot編碼:

由于神經網絡的特性,在進行多分類任務時,每一個神經元的輸出對應于一個類,是以将每一個訓練樣本的标簽轉化為OneHot形式(将0-9的資料映射在長度為10的向量中,每個樣本向量對應位置的值為1其餘為0)。

# 對輸出标簽進行OneHot編碼  參數:labels 待編碼的标簽  Label_class  編碼類數
def OneHotEncoder(labels,Label_class):
    one_hot_label = np.array([[int(i == int(labels[j])) for i in range(Label_class)] for j in range(len(labels))])
    return one_hot_label

           

訓練神經網絡

激活函數:

激活函數使用sigmoid函數,但是使用定義式在訓練過程中總是出現 overflow 警告,是以将函數進行了如下形式的轉化。

# sigmoid激活函數
def sigmoid(z):
    for r in range(z.shape[0]):
        for c in range(z.shape[1]):
            if z[r,c] >= 0:
                z[r,c] = 1 / (1 + np.exp(-z[r,c]))
            else :
                z[r,c] = np.exp(z[r,c]) / (1 + np.exp(z[r,c]))
    return z
           

損失函數:

使用均方誤差損失函數。其實我感覺在這個題目裡面直接使用預測精度也是可以的。

def cost(prediction, labels):
    return np.mean(np.power(prediction - labels,2))
           

訓練:

終于來到了緊張而又刺激的訓練環節。神經網絡從輸入層通過隐層傳遞的輸出層進而獲得網絡的輸出叫做正向傳播,而從輸出層根據梯度下降的方法進行調整權重及偏置的過程叫做反向傳播。

前向傳播每層之間将每個結點的輸出值乘對應的權重(對應函數中 omega1 omega2 )傳輸給下一層結點,下一層結點将上一層所有傳來的資料進行求和,并減去偏置 theta (此時資料對應函數中 h_in o_in )。最後通過激活函數輸出下一層(對應函數中 h_out o_out )。在前向傳播完成後計算了一次損失,友善後面進行分析。

反向傳播是用梯度下降法對權重和偏置進行更新,這裡是最主要的部分。根據西瓜書可以推出輸出層權重的調節量為 ,其中 為學習率, 分别為對應資料的真實值以及網絡的輸出, 為隐層的輸出值。這裡還要一個重要的地方在于如果将公式向量化的話需要重點關注輸出矩陣形狀以及每個矩陣資料之間的關系。代碼中d2 對應于公式中 這一部分,這裡需要對應值相乘。最後将這一部分與進行矩陣乘法,在乘學習率得到權重調節量,與權重相加即可(代碼中除了訓練集樣本個數m是因為 d2 與 h_out 的乘積将所有訓練樣本進行了累加,是以需要求平均)。

對于其他權重及偏置的調節方式與此類似,不做過多介紹(其實是因為明天要上課,太晚了得睡覺,有空補上這裡和其他不詳細的地方),詳見西瓜書。

# 訓練一輪ANN 參數:訓練資料 标簽 輸入層 隐層 輸出層size 輸入層隐層連接配接權重 隐層輸出層連接配接權重 偏置1  偏置2 學習率
def trainANN(X, y, input_size, hidden_size, output_size, omega1, omega2, theta1, theta2, learningRate):
    # 擷取樣本個數
    m = X.shape[0]
    # 将矩陣X,y轉換為numpy型矩陣
    X = np.matrix(X)
    y = np.matrix(y)

    # 前向傳播 計算各層輸出
    # 隐層輸入 shape=m*hidden_size
    h_in = np.matmul(X, omega1.T) - theta1.T
    # 隐層輸出 shape=m*hidden_size
    h_out = sigmoid(h_in)
    # 輸出層的輸入 shape=m*output_size
    o_in = np.matmul(h_out, omega2.T) - theta2.T
    # 輸出層的輸出 shape=m*output_size
    o_out = sigmoid(o_in)

    # 目前損失
    all_cost = cost(o_out, y)

    # 反向傳播
    # 輸出層參數更新
    d2 = np.multiply(np.multiply(o_out, (1 - o_out)), (y - o_out))
    omega2 += learningRate * np.matmul(d2.T, h_out) / m
    theta2 -= learningRate * np.sum(d2.T, axis=1) / m

    # 隐層參數更新
    d1 = np.multiply(h_out, (1 - h_out))
    omega1 += learningRate * (np.matmul(np.multiply(d1, np.matmul(d2, omega2)).T, X) / float(m))
    theta1 -= learningRate * (np.sum(np.multiply(d1, np.matmul(d2, omega2)).T, axis=1) / float(m))

    return omega1, omega2, theta1, theta2, all_cost

           

網絡測試

預測函數:

這裡比較簡單,前向傳播的部分和前面一樣,因為最後網絡輸出的為樣本x為每一類的機率,是以僅需要利用 np.argmax 函數求出機率值最大的下标即可,下标0-9正好對應數字的值。

# 資料預測
def predictionANN(X, omega1, omega2, theta1, theta2):
    # 擷取樣本個數
    m = X.shape[0]
    # 将矩陣X,y轉換為numpy型矩陣
    X = np.matrix(X)

    # 前向傳播 計算各層輸出
    # 隐層輸入 shape=m*hidden_size
    h_in = np.matmul(X, omega1.T) - theta1.T
    # 隐層輸出 shape=m*hidden_size
    h_out = sigmoid(h_in)
    # 輸出層的輸入 shape=m*output_size
    o_in = np.matmul(h_out, omega2.T) - theta2.T
    # 輸出層的輸出 shape=m*output_size
    o_out = np.argmax(sigmoid(o_in), axis=1)

    return o_out
           

準确率計算:

這裡将所有測試資料 X 進行預測,計算預測标簽 y-hat 與真實标簽 y 一緻個數的均值得出準确率。

# 計算模型準确率
def computeAcc(X, y, omega1, omega2, theta1, theta2):
    y_hat = predictionANN(X,omega1, omega2,theta1,theta2)
    return np.mean(y_hat == y)
           

主函數:

在主函數中通過調用上面的函數對網絡訓練預測精度,并使用 loss_list acc_list 兩個list儲存訓練過程中每一輪的精度與誤差,利用 acc_max 跟蹤最大精度的模型,并使用 pickle 将模型(其實就是神經網絡的參數)進行儲存,後面用到時可以讀取。同樣在訓練完成時我也将 loss_listacc_list 進行了儲存 (想的是可以利用訓練的資料做出一點好看的圖)。

最後部分将損失以及準确率随着訓練次數的趨勢進行了繪制。

結果如下:

Python純手動搭建BP神經網絡(手寫數字識别)實驗介紹

可以看出,模型隻是跑通了,效果并不好,訓練好幾個小時準确率隻有91%。原因可能是因為沒有對模型進行正則化、學習率沒有做動态調節等。

相反使用SVM模型準确率輕松可以達到97%以上。

# 擷取資料
from sklearn.datasets import fetch_openml
from sklearn.svm import SVC
mnist = fetch_openml('mnist_784', version=1, as_frame=False) # 預設傳回Pandas的DF類型
# sklearn加載的資料集類似字典結構
from sklearn.preprocessing import StandardScaler
X, y = mnist["data"], mnist["target"]
stder = StandardScaler() 
X = stder.fit_transform(X)
# 劃分訓練集與測試集
test_ratio = 0.3

shuffled_indices = np.random.permutation(X.shape[0]) # 打亂資料
test_set_size = int(X.shape[0] * test_ratio)
test_indices = shuffled_indices[:test_set_size]
train_indices = shuffled_indices[test_set_size:]

X_train, X_test, y_train, y_test = X[train_indices], X[test_indices], y[train_indices], y[test_indices]
# 訓練SVM

cls_svm = SVC(C=1.0, kernel='rbf')
cls_svm.fit(X_train,y_train)
y_pre = cls_svm.predict(X_test)
acc_rate = np.sum(y_pre == y_test) / float(y_pre.shape[0])
acc_rate
           
Python純手動搭建BP神經網絡(手寫數字識别)實驗介紹

瑟瑟發抖~~~

最後将全部代碼貼上:

import numpy as np
import matplotlib.pyplot as plt
import pickle

# 讀取圖檔資料
def loadImageData(trainingDirName='data/', test_ratio=0.3):
    from os import listdir
    data = np.empty(shape=(0, 784))
    labels = np.empty(shape=(0, 1))

    for num in range(10):
        dirName = trainingDirName + '%s/' % (num)  # 擷取目前數字檔案路徑
        # print(listdir(dirName))
        nowNumList = [i for i in listdir(dirName) if i[-3:] == 'bmp']  # 擷取裡面的圖檔檔案
        labels = np.append(labels, np.full(shape=(len(nowNumList), 1), fill_value=num), axis=0)  # 将圖檔标簽加入
        for aNumDir in nowNumList:  # 将每一張圖檔讀入
            imageDir = dirName + aNumDir  # 構造圖檔路徑
            image = plt.imread(imageDir).reshape((1, 784))  # 讀取圖檔資料
            data = np.append(data, image, axis=0)  
    # 劃分資料集
    m = data.shape[0]
    shuffled_indices = np.random.permutation(m)  # 打亂資料
    test_set_size = int(m * test_ratio)
    test_indices = shuffled_indices[:test_set_size]
    train_indices = shuffled_indices[test_set_size:]

    trainData = data[train_indices]
    trainLabels = labels[train_indices]

    testData = data[test_indices]
    testLabels = labels[test_indices]

    # 對訓練樣本和測試樣本使用統一的均值 标準差進行歸一化
    tmean = np.mean(trainData)
    tstd = np.std(testData)

    trainData = (trainData - tmean) / tstd
    testData = (testData - tmean) / tstd
    return trainData, trainLabels, testData, testLabels

# 對輸出标簽進行OneHot編碼
def OneHotEncoder(labels,Label_class):
    one_hot_label = np.array([[int(i == int(labels[j])) for i in range(Label_class)] for j in range(len(labels))])
    return one_hot_label

# sigmoid激活函數
def sigmoid(z):
    for r in range(z.shape[0]):
        for c in range(z.shape[1]):
            if z[r,c] >= 0:
                z[r,c] = 1 / (1 + np.exp(-z[r,c]))
            else :
                z[r,c] = np.exp(z[r,c]) / (1 + np.exp(z[r,c]))
    return z

# 計算均方誤差 參數:預測值 真實值
def cost(prediction, labels):
    return np.mean(np.power(prediction - labels,2))


# 訓練一輪ANN 參數:訓練資料 标簽 輸入層 隐層 輸出層size 輸入層隐層連接配接權重 隐層輸出層連接配接權重 偏置1  偏置2 學習率
def trainANN(X, y, input_size, hidden_size, output_size, omega1, omega2, theta1, theta2, learningRate):
    # 擷取樣本個數
    m = X.shape[0]
    # 将矩陣X,y轉換為numpy型矩陣
    X = np.matrix(X)
    y = np.matrix(y)

    # 前向傳播 計算各層輸出
    # 隐層輸入 shape=m*hidden_size
    h_in = np.matmul(X, omega1.T) - theta1.T
    # 隐層輸出 shape=m*hidden_size
    h_out = sigmoid(h_in)
    # 輸出層的輸入 shape=m*output_size
    o_in = np.matmul(h_out, omega2.T) - theta2.T
    # 輸出層的輸出 shape=m*output_size
    o_out = sigmoid(o_in)

    # 目前損失
    all_cost = cost(o_out, y)

    # 反向傳播
    # 輸出層參數更新
    d2 = np.multiply(np.multiply(o_out, (1 - o_out)), (y - o_out))
    omega2 += learningRate * np.matmul(d2.T, h_out) / m
    theta2 -= learningRate * np.sum(d2.T, axis=1) / m

    # 隐層參數更新
    d1 = np.multiply(h_out, (1 - h_out))
    omega1 += learningRate * (np.matmul(np.multiply(d1, np.matmul(d2, omega2)).T, X) / float(m))
    theta1 -= learningRate * (np.sum(np.multiply(d1, np.matmul(d2, omega2)).T, axis=1) / float(m))

    return omega1, omega2, theta1, theta2, all_cost


# 資料預測
def predictionANN(X, omega1, omega2, theta1, theta2):
    # 擷取樣本個數
    m = X.shape[0]
    # 将矩陣X,y轉換為numpy型矩陣
    X = np.matrix(X)

    # 前向傳播 計算各層輸出
    # 隐層輸入 shape=m*hidden_size
    h_in = np.matmul(X, omega1.T) - theta1.T
    # 隐層輸出 shape=m*hidden_size
    h_out = sigmoid(h_in)
    # 輸出層的輸入 shape=m*output_size
    o_in = np.matmul(h_out, omega2.T) - theta2.T
    # 輸出層的輸出 shape=m*output_size
    o_out = np.argmax(sigmoid(o_in), axis=1)

    return o_out


# 計算模型準确率
def computeAcc(X, y, omega1, omega2, theta1, theta2):
    y_hat = predictionANN(X,omega1, omega2,theta1,theta2)
    return np.mean(y_hat == y)


if __name__ == '__main__':
    # 載入模型資料
    trainData, trainLabels, testData, testLabels = loadImageData()

    # 初始化設定
    input_size = 784
    hidden_size = 15
    output_size = 10
    lamda = 1

    # 将網絡參數進行随機初始化
    omega1 = np.matrix((np.random.random(size=(hidden_size,input_size)) - 0.5) * 0.25) # 15*784
    omega2 = np.matrix((np.random.random(size=(output_size,hidden_size)) - 0.5) * 0.25)  # 10*15

    # 初始化兩個偏置
    theta1 = np.matrix((np.random.random(size=(hidden_size,1)) - 0.5) * 0.25) # 15*1
    theta2 = np.matrix((np.random.random(size=(output_size,1)) - 0.5) * 0.25) # 10*1
    # 學習率
    learningRate = 0.1
    # 資料集
    m = trainData.shape[0] # 樣本個數
    X = np.matrix(trainData) # 輸入資料 m*784
    y_onehot=OneHotEncoder(trainLabels,10) # 标簽 m*10

    iters_num = 20000  # 設定循環的次數
    loss_list = []
    acc_list = []
    acc_max = 0.0  # 最大精度 在精度達到最大時儲存模型
    acc_max_iters = 0

    for i in range(iters_num):
        omega1, omega2, theta1, theta2, loss = trainANN(X, y_onehot, input_size, hidden_size, output_size, omega1,
                                                        omega2, theta1, theta2, learningRate)
        loss_list.append(loss)
        acc_now = computeAcc(testData, testLabels, omega1, omega2, theta1, theta2)  # 計算精度
        acc_list.append(acc_now)
        if acc_now > acc_max:  # 如果精度達到最大 儲存模型
            acc_max = acc_now
            acc_max_iters = i  # 儲存坐标 友善在圖上标注
            # 儲存模型參數
            f = open(r"./best_model", 'wb')
            pickle.dump((omega1, omega2, theta1, theta2), f, 0)
            f.close()
        if i % 100 == 0:  # 每訓練100輪列印一次精度資訊
            print("%d  Now accuracy : %f"%(i,acc_now))

    # 儲存訓練資料 友善分析
    f = open(r"./loss_list", 'wb')
    pickle.dump(loss_list, f, 0)
    f.close()

    f = open(r"./acc_list", 'wb')
    pickle.dump(loss_list, f, 0)
    f.close()

    # 繪制圖形
    plt.figure(figsize=(13, 6))
    plt.subplot(121)
    x1 = np.arange(len(loss_list))
    plt.plot(x1, loss_list, "r")
    plt.xlabel(r"Number of iterations", fontsize=16)
    plt.ylabel(r"Mean square error", fontsize=16)
    plt.grid(True, which='both')

    plt.subplot(122)
    x2 = np.arange(len(acc_list))
    plt.plot(x2, acc_list, "r")
    plt.xlabel(r"Number of iterations", fontsize=16)
    plt.ylabel(r"Accuracy", fontsize=16)
    plt.grid(True, which='both')
    plt.annotate('Max accuracy:%f' % (acc_max),  # 标注最大精度值
                 xy=(acc_max_iters, acc_max),
                 xytext=(acc_max_iters * 0.7, 0.5),
                 arrowprops=dict(facecolor='black', shrink=0.05),
                 ha="center",
                 fontsize=15,
                 )
    plt.plot(np.linspace(acc_max_iters, acc_max_iters, 200), np.linspace(0, 1, 200), "y--", linewidth=2, )  # 最大精度疊代次數
    plt.plot(np.linspace(0, len(acc_list), 200), np.linspace(acc_max, acc_max, 200), "y--", linewidth=2)  # 最大精度

    plt.scatter(acc_max_iters, acc_max, s=180, facecolors='#FFAAAA')  # 标注最大精度點
    plt.axis([0, len(acc_list), 0, 1.0])  # 設定坐标範圍
    plt.savefig("ANN_plot")  # 儲存圖檔
    plt.show()
           

深度學習神經網絡系列🚀🚀🚀

關注下方《學姐帶你玩AI》一起學習

碼字不易,歡迎大家點贊評論收藏!