从零实现深度学习框架 给框架增加可变学习率策略

★★★ 本文源自AlStudio社区精品项目,【点击此处】查看更多精品内容 >>>


从零实现深度学习框架 给框架增加可变学习率策略

飞桨框架学习(LearnDL)是一个由Mr. Sun发起的活动,主旨在于以简单易懂的方式了解深度学习框架、构造深度学习框架乃至于改写深度学习框架。整体内容包括了入门级的名词解释乃至后续的框架实现工作,推荐新入门深度学习、对神经网络有些困惑、不知道如何给Paddle提PR、不知道如何参加黑客松、觉得平台上的交流充满“黑话”的同学一起参与学习~

本项目受启发于上述活动以及书目用Python实现深度学习框架,通过名词解释+代码的方式简要介绍深度学习/深度学习框架中的一些基础概念和一个简单的实现~

前置项目

可变学习步长

在开始之前,我也曾想过是否应该先从二元操作中跳出来,或者增加batchsize的功能,思来想去,还是先来说说优化器吧~

优化器的本质就是调节学习率。以一个“V”形状的一元函数为例,对于不在谷底的任何一点,导数值都是相同的,如果训练过程中不能逐渐降低学习率,则函数收敛到某个值后就不能很好收敛了。但实际上参数w所处的优化空间本就是一个黑盒子,为了能够在优化空间未知的情况下进行优化,一些自适应的补偿变化策略被提出来了。

本节会简单地给之前提到的框架添加一些自适应的学习率变化策略。

框架预处理

首先,在正式开始之前,对上次的代码进行少许修正,主要是针对一些变量名的书写进行规范化。

具体来说:

  • 统一了类中变量名为大写字母起头
  • 将可训练参数的指示符Flag更替为Trainable
  • 将lr更替为Learning_rate
  • 新增函数calculate,用于完成当前节点的计算部分,forward函数会依次调用父节点的forward以及本节点的calculate,提高代码重用性
  • 新增函数calculate,每个节点需要实现自身的calculate函数
  • 将错误的英文updata更新为update
  • Trainable的默认值为False
class Node(object):
    def __init__(self, Papa = None, Mama = None, Value = 0, Trainable = False, Learning_rate = 0.01):
        # 通常使用Father表示父节点,这里使用Papa和Mama纯粹因为更有趣一些
        self.Papa = Papa
        self.Mama = Mama
        self.Value = Value
        self.Trainable = Trainable
        self.Papa_grad = 0
        self.Mama_grad = 0
        self.Grad = 1
        self.Learning_rate = Learning_rate

    def update(self): # 参数更新
        if self.Trainable == True:
            self.Value = self.Value - self.Learning_rate*self.Grad

    def forward(self):
        if self.Papa != None: self.Papa.forward() # 基础节点的父节点不需要计算,但是非基础节点的父节点需要保证有值
        if self.Mama != None: self.Mama.forward()
        self.calculate()

    def calculate(self):
        self.Value = self.Value

    def backward(self):
        self.update()

# 加法节点
class AddNode(Node):
    def calculate(self):
        self.Value = self.Papa.Value + self.Mama.Value

    def backward(self):
        if self.Papa != None:
            self.Papa.Grad = self.Grad * 1
            self.Papa.backward()
        if self.Mama != None:
            self.Mama.Grad = self.Grad * 1
            self.Mama.backward()
        self.update()


# 乘法节点
class MulNode(Node):
    def calculate(self):
        self.Value = self.Papa.Value * self.Mama.Value

    def backward(self):
        if self.Papa != None:
            self.Papa.Grad = self.Grad * self.Mama.Value
            self.Papa.backward()
        if self.Mama != None:
            self.Mama.Grad = self.Grad * self.Papa.Value
            self.Mama.backward()
        self.update()

# 损失函数节点
class MSENode(Node):
    def calculate(self):
        self.Value = (self.Papa.Value - self.Mama.Value)**2

    def backward(self):
        if self.Papa != None:
            self.Papa.Grad = self.Grad * 2 * (self.Papa.Value - self.Mama.Value) * 1
            self.Papa.backward()
        if self.Mama != None:
            self.Mama.Grad = self.Grad * 2 * (self.Papa.Value - self.Mama.Value) * -1
            self.Mama.backward()
        self.update()

Momentum(冲量优化)

相对于基础的梯度更新 w n e w = w o l d − η g w_{new} = w_{old} - \eta g wnew=woldηg g g g是当前周期的梯度,Momentum优化器的参数还需要一个衰减系数 β \beta β,具体计算为

v = η g − β v o l d v = \eta g - \beta v_{old} v=ηgβvold

w n e w = w o l d − v w_{new} = w_{old} - v wnew=woldv

显然,这里必须要多一个变量存储上周期的数据。方便起见可以通过重写一个node类实现这一优化策略~

class MomentumNode(Node):
    def __init__(self, Papa = None, Mama = None, Value = 0, Trainable = False, Learning_rate = 0.01):
        super().__init__(Papa = Papa, Mama = Mama, Value = Value, Trainable = Trainable, Learning_rate = Learning_rate)
        self.V = 0
        self.beta = 0.9 # 默认衰减系数为0.9

    def update(self):
        if self.Trainable == True:
            self.V = self.Learning_rate*self.Grad - self.beta*self.V
            self.Value = self.Value - self.V

x1 = Node(Value = 1.1) # 给一点小小的扰动
x2 = Node(Value = 2)
x3 = Node(Value = 3)
w1 = MomentumNode(Value = 1, Trainable=True)
w2 = Node(Value = 1, Trainable=True)
w3 = Node(Value = 1, Trainable=True)
m1 = MulNode(Papa = x1, Mama = w1)
m2 = MulNode(Papa = x2, Mama = w2)
m3 = MulNode(Papa = x3, Mama = w3)
a1 = AddNode(Papa = m1, Mama = m2)
a2 = AddNode(Papa = a1, Mama = m3)
y = Node(Value = 6) # 1+2+3 = 6, 这样MSE的结果为0
result = MSENode(Papa = a2, Mama = y, Value = 20)
print('计算前 result.value = ', result.Value)
result.forward()
print('计算后 result.value = ', result.Value)
result.backward()
print('第一次更新后 w1.value = ', w1.Value)
result.forward()
print('第一次更新后 result.value = ', result.Value)
result.backward()
print('第二次更新后 w1.value = ', w1.Value)
result.forward()
print('第二次更新后 result.value = ', result.Value)
result.backward()
print('第三次更新后 w1.value = ', w1.Value)
result.forward()
print('第三次更新后 result.value = ', result.Value)
result.backward()
print('第四次更新后 w1.value = ', w1.Value)
result.forward()
print('第四次更新后 result.value = ', result.Value)

计算前 result.value =  20
计算后 result.value =  0.009999999999999929
第一次更新后 w1.value =  0.9978
第一次更新后 result.value =  0.005123696399999996
第二次更新后 w1.value =  0.99820524
第二次更新后 result.value =  0.0028531583791212853
第三次更新后 w1.value =  0.996665394792
第三次更新后 result.value =  0.00143135432365771
第四次更新后 w1.value =  0.9972189241193136
第四次更新后 result.value =  0.0008182736317543128

框架结构更新

对比一下就会发现,虽然组网姿势完全相同,但是更新结果发生了一些微小的改变。

当然,如果我们每次都需要这么复杂地去改写Node节点似乎有点太麻烦了。

为了让框架更具有扩展性,这里对框架结构进行一些改进:

  • 增加optimizer类,用于更新节点
  • 节点调用update时,会调用自身的optimizer
class Optimizer(object):
    def __init__(self, Node):
        self.Node = Node

    def update(self):
        self.Node.Value = self.Node.Value - self.Node.Learning_rate*self.Node.Grad

class Node(object):
    def __init__(self, Papa = None, Mama = None, Value = 0, Trainable = False, Learning_rate = 0.01, Optimizer = Optimizer):
        # 通常使用Father表示父节点,这里使用Papa和Mama纯粹因为更有趣一些
        self.Papa = Papa
        self.Mama = Mama
        self.Value = Value
        self.Trainable = Trainable
        self.Papa_grad = 0
        self.Mama_grad = 0
        self.Grad = 1
        self.Learning_rate = Learning_rate
        if self.Trainable == True: # 暂时我们不考虑不可学习的参数转化为可学习参数的情况,对于不需要学习的参数不用生成对应的优化器
            self.Optimizer = Optimizer(self)

    def update(self): # 参数更新
        if self.Trainable == True:
            self.Optimizer.update()

    def forward(self):
        if self.Papa != None: self.Papa.forward() # 基础节点的父节点不需要计算,但是非基础节点的父节点需要保证有值
        if self.Mama != None: self.Mama.forward()
        self.calculate()

    def calculate(self):
        self.Value = self.Value

    def backward(self):
        self.update()
# 运行测试代码
x1 = Node(Value = 1.1) # 给一点小小的扰动
x2 = Node(Value = 2)
x3 = Node(Value = 3)
w1 = Node(Value = 1, Trainable=True)
w2 = Node(Value = 1, Trainable=True)
w3 = Node(Value = 1, Trainable=True)
m1 = MulNode(Papa = x1, Mama = w1)
m2 = MulNode(Papa = x2, Mama = w2)
m3 = MulNode(Papa = x3, Mama = w3)
a1 = AddNode(Papa = m1, Mama = m2)
a2 = AddNode(Papa = a1, Mama = m3)
y = Node(Value = 6) # 1+2+3 = 6, 这样MSE的结果为0
result = MSENode(Papa = a2, Mama = y, Value = 20)
print('计算前 result.value = ', result.Value)
result.forward()
print('计算后 result.value = ', result.Value)
result.backward()
print('第一次更新后 w1.value = ', w1.Value)
result.forward()
print('第一次更新后 result.value = ', result.Value)
result.backward()
print('第二次更新后 w1.value = ', w1.Value)
result.forward()
print('第二次更新后 result.value = ', result.Value)
result.backward()
print('第三次更新后 w1.value = ', w1.Value)
result.forward()
print('第三次更新后 result.value = ', result.Value)
result.backward()
print('第四次更新后 w1.value = ', w1.Value)
result.forward()
print('第四次更新后 result.value = ', result.Value)
计算前 result.value =  20
计算后 result.value =  0.009999999999999929
第一次更新后 w1.value =  0.9978
第一次更新后 result.value =  0.005123696399999996
第二次更新后 w1.value =  0.99622524
第二次更新后 result.value =  0.002625226479937307
第三次更新后 w1.value =  0.995098026792
第三次更新后 result.value =  0.00134508634644392
第四次更新后 w1.value =  0.9942911675777136
第四次更新后 result.value =  0.0006891814070964006

可以看到不指定优化器,我们的节点会自动构造随机梯度下降优化器(SGD),输出结果和从零实现深度学习框架 基础框架的构建中的示例一致。

基于Optimizer的Momentum优化器实现

class Momentum(Optimizer):
    def __init__(self, Node, Beta = 0.9):
        super().__init__(Node)
        self.Beta = Beta # 衰减系数
        self.V = 0

    def update(self):
        self.V = self.Node.Learning_rate*self.Node.Grad - self.Beta*self.V
        self.Node.Value = self.Node.Value - self.V
# 运行测试代码
x1 = Node(Value = 1.1) # 给一点小小的扰动
x2 = Node(Value = 2)
x3 = Node(Value = 3)
w1 = Node(Value = 1, Trainable=True, Optimizer= lambda Node:Momentum(Node, Beta=0.9))
w2 = Node(Value = 1, Trainable=True)
w3 = Node(Value = 1, Trainable=True)
m1 = MulNode(Papa = x1, Mama = w1)
m2 = MulNode(Papa = x2, Mama = w2)
m3 = MulNode(Papa = x3, Mama = w3)
a1 = AddNode(Papa = m1, Mama = m2)
a2 = AddNode(Papa = a1, Mama = m3)
y = Node(Value = 6) # 1+2+3 = 6, 这样MSE的结果为0
result = MSENode(Papa = a2, Mama = y, Value = 20)
print('计算前 result.value = ', result.Value)
result.forward()
print('计算后 result.value = ', result.Value)
result.backward()
print('第一次更新后 w1.value = ', w1.Value)
result.forward()
print('第一次更新后 result.value = ', result.Value)
result.backward()
print('第二次更新后 w1.value = ', w1.Value)
result.forward()
print('第二次更新后 result.value = ', result.Value)
result.backward()
print('第三次更新后 w1.value = ', w1.Value)
result.forward()
print('第三次更新后 result.value = ', result.Value)
result.backward()
print('第四次更新后 w1.value = ', w1.Value)
result.forward()
print('第四次更新后 result.value = ', result.Value)
计算前 result.value =  20
计算后 result.value =  0.009999999999999929
第一次更新后 w1.value =  0.9978
第一次更新后 result.value =  0.005123696399999996
第二次更新后 w1.value =  0.99820524
第二次更新后 result.value =  0.0028531583791212853
第三次更新后 w1.value =  0.996665394792
第三次更新后 result.value =  0.00143135432365771
第四次更新后 w1.value =  0.9972189241193136
第四次更新后 result.value =  0.0008182736317543128

alue = 0.0008182736317543128

可以看到上述代码和直接通过继承Node创建对应节点的效果是一样的。

这里使用了一个小技巧:通过lambda创建一个临时函数。

具体的语法规范如下:

函数名 = lambda 输入变量 : 表达式

函数名是一个具有输入变量为传入参数的函数。

如果你不需要修改一些默认参数值,直接使用Optimizer = Momentum也是可以的~

以下面两个写法为例,结果是完全一致的

# case 1
Optimizer = lambda Node:Momentum(Node, Beta=0.9)
opt = Optimizer(Node)

# case 2
opt = Momentum(Node, Beta=0.9)

给OurDL增加更多优化器

在上文中,我们给自研框架OurDL进行了一些改进和框架结构调整。并且基于已有的框架,增加优化器非常的简单,只需要继承Optimizer,改写init,改写update方法即可。

以下几个实现我可能理解的有点问题,无法保证一定正确,大家仅作参考,避免被我误导

AdaGrad

更新公式如下:

s n e w = s o l d + g r a d 2 s_{new} = s_{old} + grad^2 snew=sold+grad2

w n e w = w o l d − η g r a d s n e w w_{new} = w_{old} - \frac{\eta grad}{\sqrt{s_{new}}} wnew=woldsnew ηgrad

class AdaGrad(Optimizer):
    def __init__(self, Node, Eps = 10**-10):
        super().__init__(Node)
        self.S = 0
        self.Eps = Eps # 极小数,避免除零

    def update(self):
        self.S = self.S + self.Node.Grad**2
        self.Node.Value = self.Node.Value - self.Node.Learning_rate * self.Node.Grad / (self.S + self.Eps) ** 0.5

PMSProp

更新公式如下:

s n e w = β s o l d + ( 1 − β ) g r a d 2 s_{new} = \beta s_{old} + (1-\beta) grad^2 snew=βsold+(1β)grad2

w n e w = w o l d − η g r a d s n e w w_{new} = w_{old} - \frac{\eta grad}{\sqrt{s_{new}}} wnew=woldsnew ηgrad

class RMSProp(Optimizer):
    def __init__(self, Node, Beta = 0.9, Eps = 10**-10):
        super().__init__(Node)
        self.S = 0
        self.Beta = Beta
        self.Eps = Eps # 极小数,避免除零

    def update(self):
        self.S = self.Beta * self.S + (1 - self.Beta) * self.Node.Grad**2
        self.Node.Value = self.Node.Value - self.Node.Learning_rate * self.Node.Grad / (self.S + self.Eps) ** 0.5

Adam

更新公式如下:

v n e w = β 1 v o l d + ( 1 − β 1 ) g v_{new} = \beta_1 v_{old} + (1 - \beta_1)g vnew=β1vold+(1β1)g

s n e w = β 2 s o l d + ( 1 − β 2 ) g r a d 2 s_{new} = \beta_2 s_{old} + (1-\beta_2) grad^2 snew=β2sold+(1β2)grad2

w n e w = w o l d − η v n e w s n e w w_{new} = w_{old} - \frac{\eta v_{new}}{\sqrt{s_{new}}} wnew=woldsnew ηvnew

class Adam(Optimizer):
    def __init__(self, Node, Beta1 = 0.9, Beta2 = 0.99, Eps = 10**-10):
        super().__init__(Node)
        self.S = 0
        self.V = 0
        self.Beta1 = Beta1
        self.Beta2 = Beta2
        self.Eps = Eps # 极小数,避免除零

    def update(self):
        self.V = self.Beta1 * self.V + (1 - self.Beta1) * self.Node.Grad
        self.S = self.Beta2 * self.S + (1 - self.Beta2) * self.Node.Grad**2
        self.Node.Value = self.Node.Value - self.Node.Learning_rate * self.V / (self.S + self.Eps) ** 0.5

结语

本项目延续之前的工作内容,给自研框架OurDL进行了一些结构化的改进,并且让这个框架支持不同的优化器~关于各个优化器对应的结果可以自行修改示例代码观察,可以看到更替了优化器之后收敛速度明显提高。

最后,把上面的代码复制粘贴到OurDL.py文件里就可以快乐通过import的方式使用了~

Fork本项目,即可看到最新的OurDL.py文件~

请点击此处查看本环境基本用法.

Please click here for more detailed instructions.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值