动手造个轮子--mini-torch 反向传播实现

文章目录

前言

很高兴,在深度学习的学习过程当中,我终于准备踏上当前的旅程,从一起学习Java路线的Spring到源码一样。现在是时候来简单地实现一个简单的深度学习框架了。当然本文不是纯基础文,需要具备一定的深度学习基础,pytorch基础。

在这里我们的目标是,让这段测试代码完整的运行起来:

python 复制代码
import numpy as np

from core.surports.Variable import Variable
from core.function.Square import Square
from core.function.Exp import Exp
from core.surports.NumericalDifferentiation import NumericalDifferentiation


if __name__ == '__main__':

    # 这个是符合x --A(x)-->a--B(a)-->b--C(b)-->c 的调用函数
    def f(x)-> Variable:
        A = Square()
        B = Exp()
        C = Square()
        return C(B(A(x)))

    x = Variable(np.array(0.5))
    A = Square()
    B = Exp()
    C = Square()
    # 调用链路是 A -> B -> C
    # x --A(x)-->a--B(a)-->b--C(b)-->c
    a = A(x)
    b = B(a)
    c = C(b)

    # 此时对dc/dx = (dc/dc)(dc/db)* (db/da) * (da/dx)
    c.grad = Variable(np.array(1.0)) #(dc/dc)
    b.grad = C.backward(c.grad) # dc/db = (dc/dc)*(dc/db)
    a.grad = B.backward(b.grad) # dc/da = (dc/dc)*(dc/db)* (db/da)
    x.grad = A.backward(a.grad) # dc/dx = (dc/dc)(dc/db)* (db/da) * (da/dx)

    dy = NumericalDifferentiation()
    print(" x=0.5 时 复合函数导数是:", dy.numerical_difference(f, x))
    print(" x=0.5 时 反向传播得到的梯度是:", x.grad)

    #========================自动更新======================================
    # 这里必须重新声明变量,否则,梯度会错乱
    x = Variable(np.array(0.5))
    A = Square()
    B = Exp()
    C = Square()
    # x --A(x)-->a--B(a)-->b--C(b)-->c
    a = A(x)
    b = B(a)
    c = C(b)
    c.backward()
    print(" x=0.5 时 自动反向传播得到的梯度是:", x.grad)

导数与数值微分

导数是变化率的一种表示方式 比如某个物体的位置相对于时间的 化率就是位置的导数,民11 速度 连度相对于时间的变率就是速度的导数,即加速度 像这样 导数表示的是变化率,它被定义为在极短时间内的变化量 。

当然为什么我们需要求导,求取梯度,我想这里各位是知道的,沿梯度方向(凸优化)下进行求解。

但是在实际的工程运用当中,我们直接进行函数的求导是非常困难的,因此我们可以进行近似的求解。于是在这里我们引入:数值微分的实现

其中数学表达式非常简单:

基本的代码实现如下

python 复制代码
from typing import Union
from core.surports.Function import Function
from core.surports.Variable import Variable

class NumericalDifferentiation(object):

    """ 中心差分实现,用于近似拟合一阶导数"""
    def numerical_difference(self, fun:Union[Function,callable], input: Variable, epsilon: float = 1e-4) -> Variable:
        # 计算输入点附近的两个点
        input_minus_epsilon = Variable(input.data - epsilon)
        input_plus_epsilon = Variable(input.data + epsilon)
        # 计算这两个点的函数值
        fun_minus_epsilon = fun(input_minus_epsilon)
        fun_plus_epsilon = fun(input_plus_epsilon)
        # 使用中心差分法计算导数的近似值
        derivative = (fun_plus_epsilon.data - fun_minus_epsilon.data) / (2 * epsilon)
        # 返回导数的近似值作为一个 Variable 实例
        return Variable(derivative)

梯度与求导

在说明这个之前,我们还是需要在重复一下我们的链式求导法则:

我们假设有一个计算图是这样的:

写成数学表达式就是这样的:

于是这里我们就注意两个点:

  1. 正向传递
  2. 反向求导

这就意味着,当我们求取梯度的时候,我们需要从后往前得到。在计算过程当中,算到x,可以把中间变量都算到梯度。这是为什么,这里就不复述了。

所以为了实现这个操作,我们首先需要定义出我们的tensor,在这里是Variable

python 复制代码
class Variable(object):
    """ 将data进行基本封装 """
    def __init__(self, data):
        # 增加对Variable的判断,避免重复嵌套Variable
        if(isinstance(data,Variable)):
            self.data = data.data
        else:
            self.data = data

        # 增加限制,只能是np.ndarray 类型
        if data is not None:
            if not isinstance(data, np.ndarray):
                raise TypeError('{} is not supported'.format(type(data)))

        # 增加梯度实现
        self.grad = None
        # 增加计算图(获取上一个的函数调用者)
        self.creator = None

    """ 展示数据 """
    def __str__(self):
        return f"{self.data}"

之后是关于求导的实现,首先关于求导的话,明白一点,进行了函数操作,我们才需要处理。所以我们可以这样:

python 复制代码
import numpy as np
from core.surports.Function import Function
from core.surports.Variable import Variable


class Exp(Function):
    def __init__(self):
        super(Exp, self).__init__()

    def forward(self, input:Variable) ->Variable:
        return Variable(np.exp(input.data))

    def backward(self, grad_output: Variable) -> Variable:
        return Variable(np.exp(self.input.data) * grad_output.data)

这个是我们定义的一个 e^x 函数当前的梯度是当前的导数*传递过来的导数,因为链式求导。

python 复制代码
class Function(object):

    """ call调用实现,负责调用forward函数 """
    def __call__(self, input:Variable)->Variable:

        # 这里需要记录状态,不能做深度复制,但是注意当前只能back一次
        self.input = input # x
        output = self.forward(input)# y
        self.output = output
        return output

    """ 这里完成基本函数实现,通过implement """
    def forward(self, input:Variable)->Variable:
        raise NotImplementedError

    """ 后面对反向传播的实现,完成梯度计算,让整个神经网络收敛 """
    def backward(self, grad_output:Variable)->Variable:
        raise NotImplementedError

    def __repr__(self):
        return self.__class__.__name__

反向传播

之后就是我们的反向传播了,这个其实非常简单(当然是因为现在实现是非常简单)还是看到这个图:

这个计算图其实,就是一个链表节点,一个函数就是一个节点。

所以,我们就可以很轻松实现一个简单的计算图操作。那么重点还是看到我们对变量的实现:

python 复制代码
class Variable(object):
    """ 将data进行基本封装 """
    def __init__(self, data):
        # 增加对Variable的判断,避免重复嵌套Variable
        if(isinstance(data,Variable)):
            self.data = data.data
        else:
            self.data = data

        # 增加限制,只能是np.ndarray 类型
        if data is not None:
            if not isinstance(data, np.ndarray):
                raise TypeError('{} is not supported'.format(type(data)))

        # 增加梯度实现
        self.grad = None
        # 增加计算图(获取上一个的函数调用者)
        self.creator = None

    """
    设置变量的来源,就是找到 A->B->C 当中,B from A C from B 然后链式依赖下去
    只有当进入函数操作时,才具备梯度,因此,这里我们要求func必须是Function类型,或者
    具备Function类型的组合的callable类型
    注意,我们是反向处理
    """
    def set_creator(self,func):
        self.creator = func

    # 增加backward实现操作
    def backward(self):
        if self.grad is None:
            self.grad = np.ones(self.data.shape)
        # 反向走,进入循环结构
        # 找到,最近操作的函数
        funcs = [self.creator]
        while funcs:
            func = funcs.pop()
            x,y = func.input,func.output
            x.grad = func.backward(y.grad)
            if(x.creator is not None):
                funcs.append(x.creator)

    """ 展示数据 """
    def __str__(self):
        return f"{self.data}"

之后是我们的函数类

python 复制代码
class Function(object):

    """ call调用实现,负责调用forward函数 """
    def __call__(self, input:Variable)->Variable:

        # 这里需要记录状态,不能做深度复制,但是注意当前只能back一次
        self.input = input # x
        output = self.forward(input)# y
        output.set_creator(self)# y 对应的操作函数 f
        self.output = output
        return output

    """ 这里完成基本函数实现,通过implement """
    def forward(self, input:Variable)->Variable:
        raise NotImplementedError

    """ 后面对反向传播的实现,完成梯度计算,让整个神经网络收敛 """
    def backward(self, grad_output:Variable)->Variable:
        raise NotImplementedError

    def __repr__(self):
        return self.__class__.__name__

这里我们在变量过来的 时候,记录了一下上一个函数是哪一个,然后方便调用backward 方法。

那么到这里一个简单的具备反向传播的功能就做好了。

相关推荐
mana飞侠26 分钟前
代码随想录算法训练营第67天:图论5[1]
算法·深度优先·图论
Mount〆27 分钟前
王道考研数据机构:中缀表达式转为后缀表达式
数据结构·算法
阿拉-M8328 分钟前
代码随想录Day72(图论Part08)
算法·图论
阿拉-M8331 分钟前
代码随想录Day71(图论Part07)
算法·图论
兔老大RabbitMQ1 小时前
mysql之比较两个表的数据
数据库·mysql·算法·oracle·哈希算法
aliceDingYM2 小时前
Linux python3.6安装mayavi报错
linux·python·ui
续亮~3 小时前
6、Redis系统-数据结构-05-整数
java·前端·数据结构·redis·算法
.生产的驴5 小时前
SpringBoot AOP切入点表达式
spring boot·后端·python
逆水寻舟6 小时前
算法学习记录2
python·学习·算法
羞儿6 小时前
【读点论文】基于二维伽马函数的光照不均匀图像自适应校正算法
人工智能·算法·计算机视觉