经实践验证,激活函数有增加模型表达能力的作用; 如果没有激活函数,模型就全是线性运算, 而激活函数,是非线性运算,两者组合后,经实验证明,有了更强的表达能力 模型有更强的表达能力是什么意思? 线性回归公式理论上可以近似一切事物, 对于不同事物,完成这个过程花费的时间不一样,精度上限也不同; 增加一些非线性变换,模型可以更快地达到近似的上限, 同时,还能将这个上限提高一些,比如 线性变换花3天算出10天后会下雨, 增加非线性变换后,1小时就算出了10天后会下雨 线性变换在模型是一层,模型有层数再多,依然是线性变换,是线性的 随着空间维度的提升,它依然是线性,只是呈现的形态不同,线性,平面,超平面... 但现实事物之间的关系不是线性的 增加一些非线性关系,可以提升模型的表达能力 当然也不是任意的非线性关系都行, - 要考虑到计算机能算的动,越简单越好 - 要考虑到在数学上求导能成立,必须要靠梯度下降大法求极小值 - 要考虑工程上实现成本低,代价低,成本太高就不赚钱了 - 效果要有提升,模型的表达能力要变得更强... 综合以上因素,学者不断尝试,总结出了一些激活函数 |
![]() ![]() ![]() |
激活函数的位置 矩阵相乘是线性变换, 两个相连的线性变换,可简化为一个线性变换,一步到位 y=ax+b,z=cy+d,由x两步到z与一步到z,没有质的改变 所以,一个线性变换后面就跟一个激活函数,增加一些非线性的能力 不也是说,纯粹的线性变换不能解决问题, 比如transformer的每一层中,有以下几步: 1 求补码, 2 编码并按特征维度拆分, 3 矩阵相乘,即线性变换 4 做softmax, 5 求得分, 6 再矩阵相乘,即线性变换 7 层归一化 8 全连接, 9 激活函数处理, 10 全连接 1-7 是一个逻辑,这一通计算后数据分布肯定偏移了,第8步加一个归一化 线性变换与激活函数都是神经网络中的一个技巧,还可以有其他处理技巧 激活函数在每层中出现一次,且位于最后一个全连接的前面,或者是两个线性变换之间, 因为它的目的是为变换增加一些非线性能力 最好激活函数前面也是一个全连接,相当了,BN有自动学习参数,放ReLU也无大的问题 |
|
|
np.array([0 if ele < 0 else ele for ele in x]) ![]() import numpy as np import torch from torch import nn from torch.nn import functional as F class Activate(object): def __init__(self): pass @staticmethod def relu(X): X = np.array(X) shape = X.shape X = X.reshape(-1) x = np.array([0 if x < 0 else x for x in X]) x = x.reshape(shape) return x @staticmethod def relu2(X): X = torch.tensor(data=X,dtype=torch.float64) return X.relu() @staticmethod def relu3(X): X = torch.tensor(data=X) return F.relu(input=X,inplace=True) np.random.seed(73) A=np.random.randn(7,3) # print(A) """ [[ 0.57681305 2.1311088 2.44021967] [ 0.26332687 -1.49612065 -0.03673531] [ 0.43069579 -1.52947433 -0.73025968] [ 1.05131524 1.61979267 -1.60501337] [ 0.33100953 -0.21095236 0.2981767 ] [-1.14607352 0.57536202 -0.36390663] [ 0.03639919 -0.52056399 -0.01576433]] """ # B = Activate.relu(X=A) # print(B) """ [[0.57681305 2.1311088 2.44021967] [0.26332687 0. 0. ] [0.43069579 0. 0. ] [1.05131524 1.61979267 0. ] [0.33100953 0. 0.2981767 ] [0. 0.57536202 0. ] [0.03639919 0. 0. ]] """ # B = Activate.relu2(X=A) # print(B) """可以看出torch.relu()只保留了四位有效数字 tensor([[0.5768, 2.1311, 2.4402], [0.2633, 0.0000, 0.0000], [0.4307, 0.0000, 0.0000], [1.0513, 1.6198, 0.0000], [0.3310, 0.0000, 0.2982], [0.0000, 0.5754, 0.0000], [0.0364, 0.0000, 0.0000]], dtype=torch.float64) """ B = Activate.relu3(X=A) print(B) """F.relu同样改变了数据的精度 tensor([[0.5768, 2.1311, 2.4402], [0.2633, 0.0000, 0.0000], [0.4307, 0.0000, 0.0000], [1.0513, 1.6198, 0.0000], [0.3310, 0.0000, 0.2982], [0.0000, 0.5754, 0.0000], [0.0364, 0.0000, 0.0000]], dtype=torch.float64) """ |
以0为分界点, - 低于0的数据为0,直接就不用算了,因为任何数乘以0就是0 - 高于0,则导数为1,链式求导中,激活函数不会放大或缩小导函数中的值了,导函数值的大小只受参数变量的影响 - 0与1是极其特殊的两个数字,在这个简单的不能再简单的函数中,同时出现了,也有点神乎其技的感觉... 计算量低,效果也并不比sigmoid差,极受工程青睐 - 主要是当参数成千上万时,会有亿万个激活函数在运行 - 这就要求函数越简单越好 缺点是当x小于0时,直接置为0,在理论上好像不太好 |
|
|
|
nn.PReLU(num_parameters=10, init=0.25)
nn.PReLU(): a 可学习,根据数据变化而变化
PReLU(𝑥)=max(0,𝑥)+𝑎∗min(0,𝑥) 其中a是可学习的参数/权重, 当x大于等于0时,取x, 当x小于0时,是完全舍去还是按比例保留,通过学习而定 超参数: num_parameters (int):输入数据的通道数 init (float):超参数a的初始化值,默认0.25 示例: self.layer = nn.Sequential( nn.Conv2d(in_channels=3, out_channels=10, kernel_size=3, stride=1, padding=1), nn.BatchNorm2d(num_features=10), nn.PReLU(num_parameters=10, init=0.25), nn.MaxPool2d(kernel_size=2, stride=2) )
nn.RReLU()
RReLU中的 a是一个在 一个给定的范围内 随机抽取的值。
import numpy as np from matplotlib import pyplot as plt def linear(x): return 3 * x - 7 x = np.linspace(start=-10, stop=10, num=50) plt.plot(x, linear(x)) ![]() def sigmoid(x): return 1 / (1 + np.exp(-x)) plt.plot(x, sigmoid(x)) ![]() |
sigmoid函数将数据映射到[0,1] - 对应概率,可以将数据转换为概率 - 也有类似计算机非0即1的意味 - e函数求导方便 - 对定义域无限制,可以是任何实数,即不管数据范围如何,都到映射到[0,1] 缺点 - 指数函数在计算机中比线性函数计算量大 - 对于分布来说,以0为中心更好一些 - 当数据比较大时,sigmoid函数的梯度会变得很小 - 深度学习中求导是链式示求导, - 即损失函数在某个参数变量处的导数值是n个导函数的值相乘 - 本身就很小,再相乘,就更小了 - 最后梯度就消失了,w=w-lr*w.grad梯度下降变为梯度不变 - 如此参数就无法更新了,模型就训练不下去了 |
|
|
|
def tanh(x): return (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x)) ![]() |
双曲正切函数,相比sigmoid函数 - 数据以0为中心,[-1,1] 缺陷 - 同sigmoid一样,但它又不像sigmoid函数那样可以类比概率 - 所以应用的场景很少,基本不用了 |
|
|
|
神经网络】神经元ReLU、Leaky ReLU、PReLU和RReLU的比较