- GitHub获取源码:github.com/flying-fore…
折与曲的相会——激活函数
1 前语
- “自适应线性单元”可以不断地将一个一次函数的输入和输出“喂”给它,它就可以自动地找到一次函数y=wx+b中适宜的参数值w和b。核算图通过前向传达和反向传达,开端展示了它的奇特之处。
- 但在实践遇到的问题中,输入与输出之间往往并不是简略的线性联系,它们之间的函数联系可能是二次的、指数、甚至分段的。此刻”自适应线性单元“就不足以满足咱们的需求了。而”激活函数“,将为核算图带来一种拟合这些非线性函数联系的才能。
- 一同为了得到关于激活函数愈加明晰和形象化的认知,本节咱们还将运用matplotlib对拟合进程进行一些可视化的展示。
- 本节任务:在核算图中参加激活函数relu,拟合二次函数y=x^2在区间[0,2]的一小段曲线。
可视化作用:
图1:二次函数拟合动画 |
2 激活函数
关于”激活函数“这个称号(非专业解说),首要咱们可以看阶跃函数。当输入超越0这个阈值时,输出就从0跳到了1,0是一个非激活的状况,而1是一个激活的状况。这个和生物领域中神经元间的突触有一定相似性,当突触间的兴奋性神经元递质超越某个阈值后,下一个神经元才会进入兴奋状况持续传递信号。
图2:阶跃函数的图画 |
2.1 Relu
人们发明了许多各式各样的激活函数,它们有着不同的特色,而Relu是其间比较常用的一种。Relu是一个简略的分段函数,它的中心思维是通过多段折线来靠近曲线,折线段越多、越短,拟合作用就越好,理论上运用relu几乎可以较好地任何曲线。
图3:Relu函数图画 |
Relu节点的完结:
# ourdl/ops/ops.py
class Relu(Op):
def compute(self):
assert len(self.parents) == 1
self.value = self.parents[0].value if self.parents[0].value >= 0 else 0
def get_parent_grad(self, parent):
return 1. if self.parents[0].value > 0 else 0 # 发现relu的导函数便是step
@staticmethod
def relu(x: float):
'''静态办法 --> 在核算图之外运用relu'''
return x if x >= 0. else 0.
在前向传达的进程中,它承受一个父节点的输入,并发生一个输出。咱们还运用装饰器@staticmethod
,完结了一个静态办法relu(x)
,这样咱们也可以在核算图之外直接调用relu函数了,例如可以在运用matplotlib制作函数图画时用到。
2.3 LeakyRelu
和加法节点、乘法节点等节点一样,激活函数也是核算图中的一个运算节点,需求在该节点类中完结对应的get_parent_grad()
办法对父节点进行求导。Relu函数在输入小于0时函数值都是0,对应的导数也是0,这种状况下参数就不会进行更新了。
人们提出了一种对Relu函数的修正方案,那便是LeakyRelu。在输入大于等于0的部分函数值不变,依然是x;但是在输入小于0的部分取0.1x,这样在反向传达的进程中,节点的输入小于0时,尽管导数只有0.1,但并没有直接消失,参数依然可以进行更新。(这儿0.1是一个”超参数“,也可以取其它值)
在我的一些尝试中,运用Relu函数时练习进程会卡住一直无法拟合,但LeakyRelu可以一定程度上缓解问题,依然可以拟合只是比较慢。
图4:LeakyRelu的函数图画 |
LeakyRelu节点的完结:
# ourdl/ops/ops.py
class LeakyRelu(Op):
'''消除了relu中导数为0的状况'''
def compute(self):
assert len(self.parents) == 1
t = self.parents[0].value
self.value = t if t >= 0 else t * 0.1
def get_parent_grad(self, parent):
return 1. if self.parents[0].value > 0 else 0.1 # 发现relu的导函数便是step
@staticmethod
def relu(x: float):
'''静态办法 --> 在核算图之外运用leakyrelu'''
return x if x >= 0. else x * 0.1
超参数”0.1″直接写死在代码中了,因为它一般并不需求改变。
3 拟合曲线的尝试
3.1 规划核算图
图5:核算图的规划 |
这个核算图中明确地画出了一切的节点,看起来有一些复杂。从全体看,核算图包括了三次改换:
输入–>线性改换–>激活函数–>线性改换–>输出
上述三次改换各自的含义是什么?核算图为什么规划成这个姿态?这都是很重要的问题。
其实在图一中大致就能得到答案。
- 第一次改换,发生两条不同的直线,它们有着不同的斜率,更重要的是:它们与x轴有着不同的交点。
- 第2次改换,运用激活函数relu(图一中是LeakyRelu),两条直线都在与x轴的交点处折断,得到两条折线。
- 第三次改换,两条折线线性叠加,由于它们折断点不同,故得到是一个三段折线。
而我所希望的,便是运用三段的折线去尽可能地贴合二次函数的曲线。
3.2 完结练习进程
1、核算图的建立:
# example/01_esay/04_relu与二次拟合.py
import sys
sys.path.append('../..') # 父目录的父目录
from ourdl.core import Varrible
from ourdl.ops import Mul, Add
from ourdl.ops.loss import ValueLoss
from ourdl.ops import LeakyRelu as Relu
import matplotlib.pyplot as plt
import numpy as np
import random
# 1.1 线性改换一
x = Varrible()
w_11 = Varrible()
w_12 = Varrible()
mul_11 = Mul([x, w_11])
mul_12 = Mul([x, w_12])
b_11 = Varrible()
b_12 = Varrible()
add_11 = Add([mul_11, b_11])
add_12 = Add([mul_12, b_12])
# 1.2 激活函数 --> 非线性改换
relu_11 = Relu([add_11])
relu_12 = Relu([add_12])
# 1.3 线性改换二
w_21 = Varrible()
w_22 = Varrible()
mul_21 = Mul([relu_11, w_21])
mul_22 = Mul([relu_12, w_22])
b_21 = Varrible()
add_21 = Add([mul_21, mul_22, b_21])
# 1.4 丢失函数
label = Varrible()
loss = ValueLoss([label, add_21])
在完结核算图的规划后,建立的进程比较简略,便是一些节点的创立和衔接。由于节点的数量比较多,因而稍有些繁琐,后边咱们会完结一些对象和办法用于批量创立和衔接节点以及核算图的封装,简化核算图的建立进程。一个个节点地创立也有其优势——灵敏。
2、初始化核算图参数
# example/01_esay/04_relu与二次拟合.py
# 2 参数初始化
params = [w_11, w_12, b_11, b_12, w_21, w_22, b_21]
for param in params:
param.set_value(random.uniform(-1, 1))
print([param.value for param in params])
这儿调用了random
库,运用均匀分布进行参数的随机初始化。将一切需求练习的参数参加到了一个列表params
中,便利批量进行初始化以及后边的参数更新。
有时为了比照屡次练习的作用,需求进行固定的初始化(初始化有时可以很大程度地影响练习作用),你可以手动地指定这些参数的初始值,例如
values = [-0.1571950013426796, -0.1070365984042347, 0.3791639008324807, 0.31960284774415215, 0.4263410176300597, 0.5097967360623379, 0.7597168751185974]
for i in range(len(params)):
params[i].set_value(values[i])
如果你运用的是Relu激活函数而不是LeakyRelu,一同选用随机参数初始化,你将发现你的练习时而成功时而失败。
3、结构练习数据
# example/01_esay/04_relu与二次拟合.py
# 3 生成数据
data_x = [random.uniform(0, 2) for i in range(1500)] # 如同实数比离散的[0, 1, 2]要好
data_label = [x * x for x in data_x]
运用均匀分布,在[0, 2]的范围内生成了1500个随机值,作为输入的x。然后运用了列表推导式得到对应的二次函数输出值,作为核算图中的标签。
4、练习进程
# example/01_esay/04_relu与二次拟合.py
# 4 开端练习
losses = []
for i in range(len(data_x)):
x.set_value(data_x[i])
label.set_value(data_label[i])
loss.forward()
for param in params:
param.get_grad()
param.update(lr=0.01)
if i % 100 == 0:
print(f'[{i}]:loss={loss.value},', [param.value for param in params])
losses.append(loss.value)
loss.clear()
# 5 画出练习进程中loss的改变曲线
show_x = [i for i in range(len(losses))]
show_y = [_ for _ in losses]
plt.plot(show_x, show_y)
plt.show()
练习进程与上一节“自适应线性单元”基本相同。在第三步结构练习数据时,咱们生成了1500个数据样本,因而制作曲线来调查练习进程中的丢失改变会愈加直观。咱们运用losses
列表记录了每次参数更新后的丢失值,并运用matplotlib库制作丢失的改变曲线。
3.3 练习作用
这儿咱们就完结了练习进程的一切代码编写,让咱们运转一下代码看看作用吧!
图6:丢失改变曲线 |
可以看到跟着练习进程的进行,丢失值呈下降的趋势,并渐渐趋于平稳。丢失值越低表示着模型的输出越准确,咱们的模型看起来如同练习得还不错。
但只看丢失函数其实仍是不太直观,咱们可以直接将模型所表示的函数,与二次函数y=x^2画在一同,看看它们究竟贴得近不近。
3.4 练习进程的可视化动画
当然,我觉得只看一个最终的贴合成果还不够,甚至只看输出的成果也依然不能很明晰地了解练习的进程。所以我决定将中心节点的输出也画出来,并跟着练习的进程以动画的形式出现。
作用我们现已看过啦!便是图1所示的动画。
# example/01_esay/04_relu与二次拟合.py
# 4 开端练习
# 4.1 创立画布
fig = plt.figure(figsize=(15,4))
ax = fig.subplots(1,3,sharex=True,sharey=False) # ax是包括一行三列,总共三块子画布的列表
# 4.2 练习,一同制作动画
losses = []
for i in range(len(data_x)):
x.set_value(data_x[i])
label.set_value(data_label[i])
loss.forward()
for param in params:
param.get_grad()
param.update(lr=0.01)
if i % 200 == 0:
print(f'[{i}]:loss={loss.value},', [param.value for param in params])
losses.append(loss.value)
loss.clear()
if i % 40 == 0:
show_ax_mul(ax) # 用于制作多图动画
首要咱们修改了 4、练习进程 部分的代码,创立画布,然后将画布对象传递给show_ax_mul()
函数,制作图画。在show_ax_mul()
每次制作完结后,调用plt.pause()
让画面暂停下(否则画面会一闪而逝啥也看不清),然后清空画布便利下次绘图。
画布的制作、清空都是在后台的,因而“清空画布”操作不会直接清空现已画出的函数图画。在下次制作图画时,才会将本来的图画覆盖掉。
通过反复的制作——清空,就形成了动画的作用。这样制作的效率比较低,我们也可以自行搜索其它的动画制作办法。
# example/01_esay/04_relu与二次拟合.py
def show_ax_mul(ax):
'''
用于制作多图动画n
'''
# 1 画实在的二次函数曲线
show_x = np.linspace(-0.5, 2.5, 30, endpoint=True)
show_y = [x_one * x_one for x_one in show_x]
ax[2].plot(show_x, show_y)
# 2 画模型拟合的曲线
show_x = np.linspace(-0.5, 2.5, 10, endpoint=True)
shows = {'add1':[], 'add2':[], 'mul1':[], 'mul2':[], 'y':[]}
for x_one in show_x:
x.set_value(x_one)
add_21.clear()
add_21.forward()
shows['y'].append(add_21.value)
shows['add1'].append(add_11.value)
shows['add2'].append(add_12.value)
shows['mul1'].append(mul_21.value)
shows['mul2'].append(mul_22.value)
ax[2].plot(show_x, shows['y'])
ax[0].plot(show_x, shows['add1'])
ax[0].plot(show_x, shows['add2'])
ax[1].plot(show_x, shows['mul1'])
ax[1].plot(show_x, shows['mul2'])
y_0 = [0 for _ in show_x] # 水平的参阅线
ax[2].plot(show_x, y_0)
ax[0].plot(show_x, y_0)
ax[1].plot(show_x, y_0)
plt.pause(0.01)
for i in range(ax.shape[0]):
ax[i].cla()
4 补充:参数初始化的影响
参数的初始化对练习作用的影响真的很大!它可以影响练习的速度,最终成功拟合的方法,以及是否可以成功拟合。我们可以尝试着调整各个初始化参数的正负以及大小,调查它们对应练习作用的影响,并思考这样的影响是怎样发生的。
一同,由于咱们的核算图还比较简略,也便利进行比较透彻的思考。
比照 图7 与 图8 的拟合成果,发现它们尽管有所差异,但是都较好的拟合了二次函数在[0, 2]的这一段曲线。这种差异是丢失函数曲线中所无法表现出来的信息,这便是可视化的含义之一。
图8的练习迭代了1500次,而 图7 迭代了10000次以上,两次练习的差异只是便是参数的初始化不同。跟着核算图参数规模的增大,对应参数初始化的灵敏程度会没有那么高。但运用不同的随机分布、不同的数值范围进行初始化,有时练习作用仍会有较大的距离。
# 图8对应的参数初始化
values = [-0.1571950013426796, -0.1070365984042347, 0.3791639008324807, 0.31960284774415215, 0.4263410176300597, 0.5097967360623379, 0.7597168751185974]
# 图7对应的参数初始化
values = [-0.4571950013426796, -0.4070365984042347, 0.3791639008324807, 0.31960284774415215, 0.4263410176300597, 0.5097967360623379, 0.7597168751185974]
图7:实践的拟合方法 |
图8:我期待的拟合方法 |