• 从零实现DNN 探究梯度下降的原理


    摘要

    对于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
    """
    # 对于模型从来没见过的测试数据,模型也可以准确的识别 表明模型拥有相当的泛化能力
  • 相关阅读:
    LUOGU P4113 [HEOI2012]采花
    LUOGU P4251 [SCOI2015]小凸玩矩阵
    bzoj 3230 相似子串——后缀数组
    bzoj 4453 cys就是要拿英魂!——后缀数组+单调栈+set
    洛谷 5061 秘密任务——二分图染色
    bzoj 4104 [Thu Summer Camp 2015]解密运算——思路
    bzoj 4319 cerc2008 Suffix reconstruction——贪心构造
    poj 3415 Common Substrings——后缀数组+单调栈
    CF 504E Misha and LCP on Tree——后缀数组+树链剖分
    bzoj 4278 [ONTAK2015]Tasowanie——后缀数组
  • 原文地址:https://www.cnblogs.com/wencheng256/p/10984143.html
Copyright © 2020-2023  润新知