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

我叫馬里奧發表於2019-06-06

摘要

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

本文從零開始,實現DNN網路中需要的各種操作,拼湊出一個簡單的模型在iris資料集上驗證效果。

梯度

什麼是梯度

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

如果將函式影象視為一個實體面,梯度近似於水自由流動的方向,向著這個方向走能最快得到最的小函式值。
從數值上來講,梯度的每一個分量分別對應該點在該軸方向上的偏導數

偏導數鏈式求導法則

想象一下這樣一個公式

對x求導得

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

矩陣求導法則

矩陣乘法實際上是多項式乘法的另一種直觀寫法

其實是由兩個多項式函式組成

對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
"""
# 對於模型從來沒見過的測試資料,模型也可以準確的識別 表明模型擁有相當的泛化能力

相關文章