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

发布于:2024-04-02 ⋅ 阅读:(74) ⋅ 点赞:(0)

前言

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

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


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 速度 连度相对于时间的变率就是速度的导数,即加速度 像这样 导数表示的是变化率,它被定义为在极短时间内的变化量 。
在这里插入图片描述

当然为什么我们需要求导,求取梯度,我想这里各位是知道的,沿梯度方向(凸优化)下进行求解。
在这里插入图片描述
但是在实际的工程运用当中,我们直接进行函数的求导是非常困难的,因此我们可以进行近似的求解。于是在这里我们引入:数值微分的实现
在这里插入图片描述
其中数学表达式非常简单:在这里插入图片描述
基本的代码实现如下

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

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}"

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

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 函数当前的梯度是当前的导数*传递过来的导数,因为链式求导。

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__

反向传播

之后就是我们的反向传播了,这个其实非常简单(当然是因为现在实现是非常简单)还是看到这个图:
在这里插入图片描述
这个计算图其实,就是一个链表节点,一个函数就是一个节点。
所以,我们就可以很轻松实现一个简单的计算图操作。那么重点还是看到我们对变量的实现:

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}"

之后是我们的函数类

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 方法。

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

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到