從零實現DNN 探究梯度降低的原理

摘要

對於DNN,大多數時間咱們都只須要利用成熟的第三方庫配置網絡結構和超參就能實現咱們想要的模型。
底層的求導、梯度降低永遠是一個黑盒,咱們不知道發生了什麼,只知道使用optimizer使loss函數收斂,獲得好的模型表現能力。node

本文從零開始,實現DNN網絡中須要的各類操做,拼湊出一個簡單的模型在iris數據集上驗證效果。python

梯度

什麼是梯度

梯度是一個向量,指向函數值降低最快的方向

若是將函數圖像視爲一個實體面,梯度近似於水自由流動的方向,向着這個方向走能最快獲得最的小函數值。
從數值上來說,梯度的每個份量分別對應該點在該軸方向上的偏導數算法

偏導數鏈式求導法則

想象一下這樣一個公式數組

對x求導得網絡

x的偏導數等於包含x項的導數之和
複合函數求導等於外層函數導數與內層函數導數之積 這個法則稱爲鏈式求導法則dom

矩陣求導法則

矩陣乘法其實是多項式乘法的另外一種直觀寫法機器學習

實際上是由兩個多項式函數組成函數

對x,y進行求偏導寫成矩陣的形式爲學習

該矩陣被稱爲jacobian矩陣測試

若e,f會被帶到外層函數中繼續參與計算 獲得最終結果z
根據鏈式求導法則,z對xy求導的jacobian矩陣爲

最終x,y的偏導數爲

假設矩陣(e f)的導數爲G 輸入矩陣(a b, c d)爲I 則x y的梯度應該爲

若a,b,c,d不是常數而是複合函數 則a,b,c,d也有對應的偏導數 其jacobian矩陣爲

實現

基本操做實現

使用了numpy庫用做矩陣操做,pandas庫用來讀取iris數據集,沒有使用現有機器學習庫

import numpy as np
import pandas as pd

# 定義各類操做的基類
class Operation:
    def __init__(self):
        self.inp = np.zeros(0)

    # 前向生成結果 inp爲輸入 返回值爲操做的計算結果
    def forward(self, inp):
        self.inp = inp
        
    # 反向求導生成梯度 grad爲出參的導數矩陣(鏈式求導法則的係數) 返回值爲入參的導數矩陣
    def backward(self, grad):
        pass

    # 模仿pytorch形式的調用方式
    def __call__(self, *args, **kwargs):
        return self.forward(args[0])
class Linear(Operation):

    def __init__(self, inp_size, out_size):
        super().__init__()
        self.k = np.random.rand(inp_size, out_size) - 0.5
        self.k_grad = np.zeros((inp_size, out_size)) - 0.5
        self.b = np.random.rand(out_size) - 0.5
        self.b_grad = np.zeros(out_size) - 0.5

    def forward(self, inp):
        super().forward(inp)
        return np.matmul(inp, self.k) + self.b

    def backward(self, grad):
        self.b_grad = grad.sum(axis=0) # b的jacobian矩陣
        self.k_grad = np.matmul(self.inp.transpose(), grad) # k的jacobian矩陣
        return np.matmul(grad, self.k.transpose()) # 入參的jacobian矩陣

線性函數y=kx+b
因爲操做包含兩個變量矩陣k,b 在backward方法中不只要對入參進行求導 也須要對這兩個參數進行求導 求導法則參照上文矩陣求導法則部分

class ReLU(Operation):

    def __init__(self):
        super().__init__()

    def forward(self, inp):
        super().forward(inp)
        inp[inp < 0] = 0
        return inp

    def backward(self, grad):
        grad[self.inp < 0] = 0
        return grad
 


非線性函數ReLU自己是一個max(0, x)前向操做將負數置爲0便可

反向求梯度操做,飽和部分梯度爲0 非飽和部分梯度爲1 將輸入中負數部分對應的出參導數置爲0即爲入參導數矩陣

class Sigmoid(Operation):

    def __init__(self):
        super().__init__()

    def forward(self, inp):
        super().forward(inp)
        return 1 / (np.exp(-inp) + 1)

    def backward(self, grad):
        output = 1 / (np.exp(-self.inp) + 1)
        return out(1-out) * grad


Sigmoid函數,這裏使用其主要目的是生成機率。
Sigmoid能夠將函數值映射到0-1之間,能夠將網絡計算的數值結果映射成機率結果 其導數爲

class L2Loss(Operation):

    def __init__(self, label):
        super().__init__()
        self.label = label

    def forward(self, inp):
        super().forward(inp)
        return np.power((inp - self.label), 2).sum()

    def backward(self, grad):
        return 2 * (self.inp - self.label) * grad
 

L2 loss 用來定義損失函數

DNN模型實現

class Dnn:

    # 模型定義 input->hidden_layer[ReLU]->output[Sigmoid]
    def __init__(self, inp_size, hidden_nodes, batch_size):
        self.l1 = Linear(inp_size, hidden_nodes)
        self.relu = ReLU()
        self.l2 = Linear(hidden_nodes, 1)
        self.sigmoid = Sigmoid()
        self.loss = L2Loss([])
        self.batch_size = batch_size

    # 計算模型輸出
    def forward(self, inp):
        x = self.l1(inp)
        x = self.relu(x)
        x = self.l2(x)
        return self.sigmoid(x)

    # 計算損失函數
    def loss_value(self, inp, label):
        output = self.forward(inp)
        self.loss = L2Loss(label)
        return self.loss(output)

    # 反向傳播計算梯度
    def backward(self):
        x = self.loss.backward(1)
        x = self.sigmoid.backward(x)
        x = self.l2.backward(x)
        x = self.relu.backward(x)
        return self.l1.backward(x)

    # 梯度降低 步長爲l 這裏使用固定方向固定步長的梯度降低 並無使用更復雜的隨機梯度降低
    # 注意:沒使用隨機梯度降低等優化算法,若是初始狀態得位置很差,有可能出現沒法收斂的狀況
    def optm(self, l):
        self.l1.k -= l * self.l1.k_grad
        self.l2.k -= l * self.l2.k_grad
        self.l1.b -= l * self.l1.b_grad
        self.l2.b -= l * self.l2.b_grad

    def __call__(self, *args, **kwargs):
        return self.forward(args[0])

實驗

datas = pd.read_csv("/data1/iris.data")
# 把原本的多分類問題轉變成2分類問題方便實現
datas.loc[datas.query("label == 'Iris-setosa'").index, "label_bool"] = 1
datas.loc[datas.query("label != 'Iris-setosa'").index, "label_bool"] = 0

datas.sample(10) # sample用於隨機抽樣 返回數據集中隨機n行
"""
f1    f2    f3    f4    label    label_bool
121    5.6    2.8    4.9    2.0    Iris-virginica    0.0
147    6.5    3.0    5.2    2.0    Iris-virginica    0.0
53    5.5    2.3    4.0    1.3    Iris-versicolor    0.0
22    4.6    3.6    1.0    0.2    Iris-setosa    1.0
129    7.2    3.0    5.8    1.6    Iris-virginica    0.0
57    4.9    2.4    3.3    1.0    Iris-versicolor    0.0
52    6.9    3.1    4.9    1.5    Iris-versicolor    0.0
11    4.8    3.4    1.6    0.2    Iris-setosa    1.0
20    5.4    3.4    1.7    0.2    Iris-setosa    1.0
70    5.9    3.2    4.8    1.8    Iris-versicolor    0.0
"""
datas = datas[["f1", "f2", "f3", "f4", "label_bool"]]
# 劃分訓練集和測試集
train = datas.head(130)
test = datas.tail(20)

m = Dnn(4, 20, 15)
# 迭代1000次 步長0.1 batch大小15 每100次迭代輸出一次loss
m = Dnn(4, 20, 15)
for i in range(0, 1000):
    t = train.sample(15)
    tf = t[["f1", "f2", "f3", "f4"]]
    m(tf.values)
    loss = m.loss_value(tf.values, t["label_bool"].values.reshape(15, 1))
    if i % 100  == 0:
        print(loss)
    m.backward()
    m.optm(0.1)
"""
9.906133281506982
5.999831306129411
2.9998719755558625
2.9998576731173774
3.999596848200819
3.9935063085696276
0.014295955296485221
0.0009333518819264006
0.0010388260442712738
0.0010148543295591939
"""
# 從訓練結果上來看是收斂了的 接下來看泛化能力如何
t = test.sample(15)
tf = t[["f1", "f2", "f3", "f4"]]

m(tf.values)
"""
array([[1.06040557e-03],
       [9.95898652e-01],
       [8.77277996e-05],
       [3.03117186e-05],
       [4.66427694e-04],
       [9.96013317e-01],
       [8.69158240e-04],
       [9.99175120e-01],
       [1.97615481e-06],
       [6.21071987e-06],
       [9.93056596e-01],
       [9.93803044e-01],
       [1.84890487e-03],
       [3.39268278e-04],
       [4.31708336e-05]])
"""
t
"""
    f1    f2    f3    f4    label_bool
62    6.0    2.2    4.0    1.0    0.0
1    4.9    3.0    1.4    0.2    1.0
68    6.2    2.2    4.5    1.5    0.0
149    5.9    3.0    5.1    1.8    0.0
58    6.6    2.9    4.6    1.3    0.0
20    5.4    3.4    1.7    0.2    1.0
89    5.5    2.5    4.0    1.3    0.0
4    5.0    3.6    1.4    0.2    1.0
143    6.8    3.2    5.9    2.3    0.0
114    5.8    2.8    5.1    2.4    0.0
26    5.0    3.4    1.6    0.4    1.0
45    4.8    3.0    1.4    0.3    1.0
69    5.6    2.5    3.9    1.1    0.0
55    5.7    2.8    4.5    1.3    0.0
133    6.3    2.8    5.1    1.5    0.0
"""
m.loss_value(tf.values, t["label_bool"].values.reshape(15, 1))
"""
0.0001256497782966175
"""
# 對於模型歷來沒見過的測試數據,模型也能夠準確的識別 代表模型擁有至關的泛化能力
相關文章
相關標籤/搜索