★★★ 本文源自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=wold−v。
显然,这里必须要多一个变量存储上周期的数据。方便起见可以通过重写一个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=wold−snewη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=wold−snewη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=wold−snewη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.