【機器學習】吳恩達機器學習視頻作業-(手撕)神經網絡

在這裏使用前饋神經網絡和反向傳播的方式對數據進行分類。這也是神經網絡最基礎的部分,剛瞭解相關內人的話,還是比較難的,特別是在公式推導中。在本部分中,構建一個神經網絡分類模型,就可以對手寫數字進行識別,無需構建多個二分類模型。
視頻講解:

徹底搞定機器學習算法理論與實戰——神經網絡入門-NeuralNetworks

視頻鏈接:https://www.bilibili.com/video/av96934866/

1 加載數據集與數據查看

本案例使用的數據集與使用邏輯迴歸進行多分類相同。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.io import loadmat   # 加載matlab數據
import matplotlib
import scipy.optimize as opt   # 優化函數
from sklearn.metrics import classification_report  # 評價報告
%matplotlib inline
def load_data(path, transpose=True):
    # 加載數據
    data = loadmat(path)
    y = data.get('y')   # (5000,1)
    y = y.reshape(y.shape[0])  # 構造列向量
    X = data.get('X')   # (5000, 400)
    if transpose:
        # 原始數據對圖像進行了翻轉,需要轉置回來
        X = np.array([im.reshape((20, 20)).T for im in X])
        # 再次將數據壓平
        X = np.array([im.reshape(400) for im in X])
    return X, y

查看原始數據並隨機繪製100張圖片

X, _ = load_data("ex4data1.mat")

def plot_100_image(X):
    """ 
    隨機選取100張圖片
    """
    size = int(np.sqrt(X.shape[1]))  # 獲取形狀
    # sample 100 image, reshape, reorg it
    sample_idx = np.random.choice(np.arange(X.shape[0]), 100)  # 100*400
    sample_images = X[sample_idx, :]   # 抽取圖片
    # 共享x,y軸
    fig, ax_array = plt.subplots(nrows=10, ncols=10, sharey=True, sharex=True, figsize=(8, 8))

    for r in range(10):
        for c in range(10):
            # 把一個數組、矩陣繪圖圖片:matshow
            ax_array[r, c].matshow(sample_images[10 * r + c].reshape((size, size)),
                                   cmap=matplotlib.cm.binary)  # cmap=matplotlib.cm.binary 設置圖片顏色
            plt.xticks(np.array([]))  # 設置x軸無標記
            plt.yticks(np.array([]))  # 設置y軸無標記

# 隨機查看100條數據
plot_100_image(X)

數據顯示
查看權重數據形狀

def load_weight(path):
    # 讀取權重數據 即參數信息
    data = loadmat(path)
    return data['Theta1'], data['Theta2']
t1, t2 = load_weight('ex4weights.mat')
t1.shape, t2.shape   # 查看權重形狀

參數的序列化和反序列化

def serialize(a, b):
    # 序列化數據
    return np.concatenate((np.ravel(a), np.ravel(b)))

def deserialize(seq):
    #  (25, 401), (10, 26)
    # 反序列化,分離參數theta1和theta2使用
    return seq[:25 * 401].reshape(25, 401), seq[25 * 401:].reshape(10, 26)

# 序列化2矩陣
# 在這個nn架構中,theta1(25,401),theta2(10,26),它們的梯度是delta1,delta2
theta = serialize(t1, t2)  # 扁平化參數,25*401+10*26=10285
theta.shape

2 數據預處理

標籤y轉成onehot編碼,特徵數據X插入數據列1

X_raw, y_raw = load_data('ex4data1.mat', transpose=False)  # 獲取實驗數據
X = np.insert(X_raw, 0, np.ones(X_raw.shape[0]), axis=1)#增加全部爲1的一列
X.shape # (5000, 401)


y_raw
# array([10, 10, 10, ...,  9,  9,  9], dtype=uint8)
# 將y進行onehot編碼
def expand_y(y):
    res = []
    for i in y:
        y_array = np.zeros(10)
        y_array[i - 1] = 1
        res.append(y_array)
    return np.array(res)

y = expand_y(y_raw)
y
"""
array([[0., 0., 0., ..., 0., 0., 1.],
       [0., 0., 0., ..., 0., 0., 1.],
       [0., 0., 0., ..., 0., 0., 1.],
       ...,
       [0., 0., 0., ..., 0., 1., 0.],
       [0., 0., 0., ..., 0., 1., 0.],
       [0., 0., 0., ..., 0., 1., 0.]])
"""

3 神經網絡構建

  • 輸入層是400 + 1(偏置)個神經元,對應數據的各個特徵。
  • 隱藏層只設置一層,有25 + 1(偏置)個神經元。
  • 輸出層有10個神經元,總共10類,最後選擇輸出層概率最大那個,決定是哪一類。

前向傳播圖形如下

前向傳播圖

激活函數 在神經網絡中有很多激活函數。這裏使用sigmoid函數對神經元進行激活。
sigmoid(z)=g(z)=11+ez sigmoid(z) = g(z) = \frac{1}{1+e^{-z}}

# 定義一個sigmoid函數
def sigmoid(z):
    # z可以是一個數,也可以是np中的矩陣
    return 1.0/(1+np.exp(-z))

前向傳播函數

# 前向傳播函數創建
def forward_propagate(theta, X):
    # theta 參數 一個行向量
    # X 5000x401的數據
    t1, t2 = deserialize(theta)  # t1:(25,401)  t2:(10,26)
    m = X.shape[0]    # 數據數目
    a1 = X    # 5000x401
    z2 = a1 @ t1.T   # 矩陣相乘
    a2 = np.insert(sigmoid(z2), 0, values=1, axis=1) # 5000x26
    z3 = a2 @ t2.T   # 矩陣相乘  5000x10
    h = sigmoid(z3)  # 經過激活函數輸出 5000x10
    return a1, z2, a2, z3, h  # 這些參數在反向迴歸時使用

查看前向傳播情況

_,_,_,_, h = forward_propagate(theta, X)
h

前向傳播結果

5 損失函數構建

J(θ)=1mi=1mk=1K[yk(i)log((hΘ(x(i)))k)(1yk(i))log(1(hΘ(x(i)))k)]+λ2m[j=125k=1400(Θj,k(1))2+j=110k=125(Θj,k(2))2]\begin{aligned} J(\theta)=& \frac{1}{m} \sum_{i=1}^{m} \sum_{k=1}^{K}\left[-y_{k}^{(i)} \log \left(\left(h_{\Theta}\left(x^{(i)}\right)\right)_{k}\right)-\left(1-y_{k}^{(i)}\right) \log \left(1-\left(h_{\Theta}\left(x^{(i)}\right)\right)_{k}\right)\right]+\\ & \frac{\lambda}{2 m}\left[\sum_{j=1}^{25} \sum_{k=1}^{400}\left(\Theta_{j, k}^{(1)}\right)^{2}+\sum_{j=1}^{10} \sum_{k=1}^{25}\left(\Theta_{j, k}^{(2)}\right)^{2}\right] \end{aligned}
公式說明:

  • 左邊部分爲常規的損失函數,右邊部分是正則化項

  • 以一條訓練數據爲例,則單條數據的損失函數爲:
    k=1K[yklog((hθ(x))k)(1yk)log(1(hθ(x))k)] \sum_{k=1}^{K}\left[-y_{k} \log \left(\left(h_{\theta}\left(x\right)\right)_{k}\right)-\left(1-y_{k}\right) \log \left(1-\left(h_{\theta}\left(x\right)\right)_{k}\right)\right ]
    這裏的k表示分類的數目。y已經經過one-hot編碼,一條訓練數據x對應着有10個數值,而每個數值有2中可能,即爲1或爲0(二分類)。實際中也只在y=1時計算出該數據的損失值,其它情況計算結果爲0。

  • 最後就是將所有數據的損失值相加求平均

  • 對於正則項,也以單條數據爲例,則有:(注:不考慮偏置項)
    λ2[j=125k=1400(Θj,k(1))2+j=110k=125(Θj,k(2))2] \frac{\lambda}{2}\left[ \sum_{j=1}^{25} \sum_{k=1}^{400}\left(\Theta_{j, k}^{(1)}\right)^{2}+\sum_{j=1}^{10} \sum_{k=1}^{25}\left(\Theta_{j,k}^{(2)}\right)^{2}\right]
    其中Θ(1)\Theta^{(1)}Θ2(2)\Theta_2^{(2)}分別表示輸入層到隱藏層、隱藏層到輸出層的參數。輸入層非配置項神經元有400個,隱藏層神經元有25個,則需要25×\times 400個參數。與此類似,數層神經元有10個,則隱藏層到輸出層之間的神經元個數:10×\times 25個。

  • 最後將所有數據的正則項計算結果加起來取平均即可,係數\frac{1}{2}是爲了求導時消掉2次項的係數。λ\lambda正則的正則項係數。

  • 正則項使用的是L2正則化,即L2範數

# 常規損失函數
def cost(theta, X, y):
    # theta 包含第一部分和第二部分的參數 1行(401*25+26*10)列的行向量
    # 訓練數據
    # 標籤
    m = X.shape[0]  # 獲取數據量
    _, _, _, _, h = forward_propagate(theta, X)
    return (-y*np.log(h) - (1 - y)*np.log(1 - h)).sum()/m
    

# 正則化損失函數定義損失函數
def regularized_cost(theta,  X, y, l):
    # theta 包含第一部分和第二部分的參數 1行(401*25+26*10)列的行向量
    # 訓練數據
    # 標籤
    # 正則項
    m = X.shape[0]      # 獲取訓練樣本的個數
    # 分離兩部分的參數值
    t1, t2 = deserialize(theta)
    # 正則項
    reg = l * (np.power(t1[:, 1:], 2).sum() + np.power(t2[:, 1:], 2).sum())/(2*m)
    return cost(theta, X, y) + reg

查看初始損失值

cost(theta, X, y), regularized_cost(theta, X, y, 1)
# (0.2876291651613189, 0.38376985909092365)

6 反向傳播算法

6.1 sigmoid函數的導數

先回顧一下分式函數求導法則:
[f(x)g(x)]=f(x)g(x)f(x)g(x)g2(x) \left[ \frac{f(x)}{g(x)} \right]^\prime = \frac{f^\prime(x)g(x) - f(x)g^\prime(x)}{g^2(x)}
那麼sigmoid函數的導數如下:
g(z)=11+ez=ez1+ez=ez(1+ez)e2z(1+ez)2=ez1+ez(1+ez11+ez)=g(z)(1g(z)) \begin{aligned} g (z) &= \frac{1}{1+e^{-z}}= \frac{e^z}{1+e^{-z}}\\ &= \frac{e^z(1+e^z) - e^{2z}}{(1+e^z)^2}\\ &= \frac{e^z}{1+e^z}\left( \frac{1+e^z - 1}{1+e^z} \right)\\ & = g(z)(1-g(z)) \end{aligned}

def gradient_sigmoid(z):
    # z 一個數或一個ndarray類型
    return sigmoid(z)*(1-sigmoid(z))

# 查看參數狀況
X.shape, y.shape, t1.shape, t2.shape, theta.shape
# ((5000, 401), (5000, 10), (25, 401), (10, 26), (10285,))

6.2 梯度下降法計算參數值theta

這一塊是整個程序最難的地方,數據的維度很難理得清。在吳恩達老師的視頻中也沒有進行具體的推導,也僅僅是羅列了相關結論。
只有一個隱藏層類型
可根據吳恩達老師給出的結論:
結論1
結論2
計算本模型中的參數θ\theta的偏導數,如下:
a(1)=xa^{(1)} = x,
z(2)=θ(1)a(1)z^{(2)} = \theta^{(1)}a^{(1)},
a(2)=g(z(2))a^{(2)}=g(z^{(2)}),
z(3)=θ(2)a(2)z^{(3)}=\theta^{(2)}a^{(2)},
a(3)=hΘ(x)=g(z(3))a^{(3)}=h_\Theta(x)=g(z^{(3)}),
δ(3)=a(3)y\delta^{(3)} = a^{(3)}-y,
δ(2)=θ(2)Tδ(3).g(a(2))\delta^{(2)} = {\theta^{(2)}}^T\delta^{(3)} .* g^\prime(a^{(2)}),
θ(2)J(Θ)=δ(3)Ta(2)\frac{\partial }{\partial \theta^{(2)}}J(\Theta) = {\delta^{(3)}}^Ta^{(2)},(需要注意維度)
θ(1)J(Θ)=δ(2)Ta(1)\frac{\partial }{\partial \theta^{(1)}}J(\Theta) = {\delta^{(2)}}^T a^{(1)}(需要注意不需要的偏置項)

# 常規方法梯度下降算法
def backprop_regular_gradient(theta, X, y):
    t1, t2 = deserialize(theta)  # 獲取兩個參數值
    m = X.shape[0]
    delta1 = np.zeros_like(t1)
    delta2 = np.zeros_like(t2)
    a1, z2, a2, z3, h = forward_propagate(theta, X)
    # 使用單挑數據梯度值疊加計算方法
    for i in range(m):
        a1i = a1[i, :]  # (1, 401) 行向量
        z2i = z2[i, :]  # (1, 25)  行向量
        a2i = a2[i, :]  # (1, 26)  行向量
        hi = h[i, :]    # (1, 10)  行向量
        yi = y[i, :]    # (1, 10)  行向量
        
        d3i = hi - yi   # (1, 10)
        z2i = np.insert(z2i, 0, 1)  # 增加偏置項
        d2i = t2.T @ d3i * gradient_sigmoid(z2i)   # (1,26)
        # 注意np向量的轉置 這裏使用matrix時,行向量轉置會成爲2維向量,如果使用array,行向量轉置還是行向量
        delta2 += np.matrix(d3i).T @ np.matrix(a2i) # (1, 10).T @ (1, 26).T
        # 注意在當前層會增加上一層沒有的偏置項
        delta1 += np.matrix(d2i[1:]).T @ np.matrix(a1i)  # (1, 25).t @ (1, 401)
    
    delta1 = delta1/m
    delta2 = delta2/m
    
    return serialize(delta1, delta2)

# 查看計算情況
d1, d2 = deserialize(backprop_regular_gradient(theta, X, y))
d1.shape, d2.shape
# ((25, 401), (10, 26))

6.3 正則化梯度

Θij(l)J(Θ)=Dij(l)=1mΔij(l) for j=0 \frac{\partial}{\partial \Theta_{i j}^{(l)}} J(\Theta) =D_{i j}^{(l)} =\frac{1}{m} \Delta_{i j}^{(l)} \quad\quad \quad\quad \text { for } j=0
Θij(l)J(Θ)=Dij(l)=1mΔij(l)+λmΘij(l) for j1 \frac{\partial}{\partial \Theta_{i j}^{(l)}} J(\Theta) =D_{i j}^{(l)} =\frac{1}{m} \Delta_{i j}^{(l)}+\frac{\lambda}{m} \Theta_{i j}^{(l)} \quad\text { for } j \geq 1

# 正則化參數的梯度
def regularized_gradient(theta, X, y, l=1):
    m = X.shape[0]
    # 常規梯度下降計算
    delta1, delta2 = deserialize(backprop_regular_gradient(theta, X, y))
    t1, t2 = deserialize(theta)
    t1[:, 0] = 0  # 0 列表示不要正則化的列
    reg_term_d1 = (l / m) * t1
    delta1 = delta1 + reg_term_d1
 
    t2[:, 0] = 0  # 0 列表示不要正則化的列
    reg_term_d2 = (l / m) * t2
    delta2 = delta2 + reg_term_d2

    return serialize(delta1, delta2)

7 模型訓練

def nn_training(X, y):
    # 初始化模型參數theta
    init_theta = np.random.uniform(-0.12, 0.12, 10285)  # 25*401 + 10*26
    res = opt.minimize(fun=regularized_cost,
                       x0=init_theta,
                       args=(X, y, 1),
                       method='TNC',
                       jac=regularized_gradient,
                       options={'maxiter': 400})
    return res

res = nn_training(X, y)  # 運行時間長
res

訓練結果

8 查看正確率

def show_accuracy(theta, X, y):
    _, _, _, _, h = forward_propagate(theta, X)
    y_pred = np.argmax(h, axis=1) + 1  # 確定預測類
    # 打印報告
    print(classification_report(y, y_pred, digits=4))

show_accuracy(res.x, X, y_raw)

模型預測結果

9 顯示隱藏層

def plot_hidden_layer(theta):
    """
    theta: (10285, )
    """
    final_theta1, _ = deserialize(theta)
    hidden_layer = final_theta1[:, 1:]  # ger rid of bias term theta

    fig, ax_array = plt.subplots(nrows=5, ncols=5, sharey=True, sharex=True, figsize=(5, 5))

    for r in range(5):
        for c in range(5):
            ax_array[r, c].matshow(hidden_layer[5 * r + c].reshape((20, 20)),
                                   cmap=matplotlib.cm.binary)
            plt.xticks(np.array([]))
            plt.yticks(np.array([]))
plot_hidden_layer(res.x)

隱藏層結果

總結

曾嘗試使用矩陣求導的方式去推導本模型中的反向傳播方法,但是涉及的參數過多,很容易混亂。最後使用的是吳恩達老師給出的結論,然後根據本模型去推出適合本模型中的數據。當然本模型也還存在一定的缺陷,這個在今後深入深度學習後再進行改進和使用更加簡潔的方法進行處理。
程序源碼可在訂閱號"AIAS編程有道"中回覆“機器學習”即可獲取。

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