文章目录
前言
很高兴,在深度学习的学习过程当中,我终于准备踏上当前的旅程,从一起学习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)
梯度与求导
在说明这个之前,我们还是需要在重复一下我们的链式求导法则:
我们假设有一个计算图是这样的:
写成数学表达式就是这样的:
于是这里我们就注意两个点:
- 正向传递
- 反向求导
这就意味着,当我们求取梯度的时候,我们需要从后往前得到。在计算过程当中,算到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 方法。
那么到这里一个简单的具备反向传播的功能就做好了。