python手写神经网络之权重初始化——梯度消失、表达消失

基于《深度学习入门——基于Python的理论与实现》第六章,但是书上只有一段基础的展示代码,和一些刻板结论(xx激活用xx优化),没有太多过程分析,所以自己进行了扩展与实验,加入了激活前后的对比,和不同激活函数不同系数等等的对比。

 

 

关于权重初始化的理解:

在一个默认的情况下(w高斯分布初始化,sigmoid激活,x是一个1000*100的高斯分布的随机matrix),会产生这种两头高的分布,这个分布的主要缺点是,在sigmoid的输出0和输出1的区间,梯度近乎于零,梯度消失,无法训练。下面讨论的是解决它的一个方法,vanilla的方法就是w*=weight_init_std,其中std=0.1或者0.01。(x不变)

图一:std=1(上下分别为激活前后,注意看激活前的横轴量级和激活后的分布关系)

图二:std=0.1(注意看激活前的横轴量级和激活后的分布关系)

 

但是,当std=0.01时,w的scale太小了(下图),在0.1以内,趋零,梯度区间倒是很好,没有梯度消失问题,但是输出全集中在0.5这其实也是一个问题!之前没注意到的一点,其实这个初始化也是一个trade-off,除了有梯度消失问题(两头高),还有“表达消失”问题(中间高),所谓表达消失,就是因为权重都是0或者接近0,导致所有神经元输出雷同甚至相同,丧失对称性和表达能力(100个神经元干1个神经元的事)。理想的分布应该是不那么集中的。

 

之前只是听过全0初始化网络会对称(注意:w=0不是神经元死亡),对称的坏处是丧失神经网络的表达能力。但是没注意到这个解决梯度消失的初始化方案(降低初始化的方差)的极端就是表达消失和神经网络对称。反应到分布,就是从极端的两头高,变成极端的中间高(但是这很取决于激活函数,relu例外),这两张情形其实是一个trade-off,都要避免才是好的。

梯度消失和表达消失受w的影响的变化,是会产生对立关系的,却又不是直接的因果对立关系(抽象就抽象在这,你又不能说它没关系,w是能影响激活前的值,从而影响到激活后的值得分布的,这样间接的因果关系)。(这里边有很多巧合,x轴对称激活函数中(tanh与sigmoid)好像是如此对立,relu好像又不同,relu的输入如果因为w而集中到0附近,relu的输出也集中在0附近,那么relu既会梯度消失,也会表达消失

 

“神经元死亡”这个概念也容易有误解,神经元死亡是神经元输出永久为零,但是输出其实是f(w*x)输出的值,它受多方面因素共同影响,不一定w=0输出就等于0(全0给sigmoid激活,输出是0.5)。可能略微抽象的是,既然w不是0,怎么就永久死亡了?为什么某一个特定的“死亡组合”就能永远生效呢?其实,“死亡”这个词,一个重要前提就是,“在当前训练集下”,只有在特定的x分布下,这个特定的w才能导致神经元死亡,但是这已经足够致命了,因为你没有无穷多的训练集,针对你的训练集死亡,就等于不再work了。

 

那么既然是一个trade-off,想两头都不得罪怎么办?肯定需要一个“动态”方案了,Xavier就是这种,他根据输入神经元和输出神经元的数量进行权重的初始化标准化工作(简化版的也可能只根据输入,比如本例)。直觉上也很好理解,有多少输入神经元和输出神经元,就对应多大量级的数据量,那么这个操作就根据数据量动态去操作。太详细的不说了。

另外,“He初始化”是什么?ReLU只有半轴输出,所以自然要在Xavier的系数中乘以2(根号下2/n),这样简单粗暴理解“He初始化”就好了

(其实有了各种BNs之后,这些trick多少有些派不上用场的感觉,但是从学习角度,研究一下还是有必要的)

 

 

简单发几组结果图示:

经验:不要光看图形,可能很多情况图的形状真的差不多,要看座标轴的数字!!

比较绕的一点是:这里打印的是每一层的激活值,不是w,而激活值到底是什么,除了w相关,和激活函数也有关。所以下面的图主要展示的是激活后的值,也就是整体分布的变化。

分布取向的不同:sigmoid和tanh是两头梯度消失(饱和,梯度0)。尤其是tanh,分布在0附近梯度正好(但是w),但是relu不同,relu的0分布代表梯度消失,relu的负轴无梯度,分布趋0代表梯度消失。

 

sigmoid的普通初始化(*0.01)与Xavier对比(单图内上下图分别为激活前后):

 

 

tanh的普通(0.01)和Xavier(得益于tanh的0均值化,效果要比sigmoid好):

ReLU的普通与Xavier与“He初始化”:(std=0.01横轴非常小,分布接近0,Xavier好很多,但是He初始化更好,注意横轴座标)

图一:纯1.0的高斯分布:输出集中在0附近,梯度消失,表达也不好,双输,这是relu不同于sigmoid的点。(因为w方差太大了,所以每一层的激活前分布的scale还是很大的,只是激活后的分布不好,那么如果w方差小,输出前后会怎样?)

 

这个图很难解释,因为我用了错误的hist range,看起来分布集中到0了?其实不是,只是分布到更广的范围了(因为0.1~1.0占比更少了,所以变得更“矮”了),所以说不算很向0集中。

 

 

图二,std=0.1,分布好了很多,激活前scale也更集中了,激活后也更分散了关键是怎么解释这个变化关系,为什么w的scale小了,激活后的值分布更均匀?首先,w的scale小了,那么激活前的值,也就是zi = a(i-1)*wi更集中在0附近,然后激活后呢?解释不了了,所以我需要改一下打印,加上weights,修正range(三行图的关系,w、z=w*x、a=f(z),x是前一列的a)

 

随着网络加深,第二个分布区间从一两千,变成了一万多,反而越来越多?怎么就导致activations的分布越来均匀?也不是,因为这里只看了0~1,其实看总量(下图),这个区间在增加,其实是从其他的分布区间剥削来的所以这是符合预期的结果。分布越来越集中,所以其实也是很差的表现!!!只是比std=1的情况集中一些。但是能说的上叫做好吗?也说不上(但是直觉上,std=1的情况太离散了,表达不太连续,可能不太好,这个情况的主要问题是,scale过大,波动过大的W,先不说每次训练会不会结果都很不同,这些极值之间的参数变化可能也很剧烈。另一方面,W方差过大,过拟合也会严重),这两个只是输出的数值量级不同,从梯度消失和表达消失上看,可能半斤八两。

 整体的正负分布都区别不大,只是w的量级不同,

 

 

 

图三,std=0.01,又变得糟糕起来,太小了。这意味着什么,5-layer的activations也就是输出,输出如果都是0.00000x,那么虽然经过softmax之后也许会大一点,然后和one-hot比一下,就是说,前向传播本身还能有东西(但是极小的值,肯定也伴随着很多精度损失,试想,如果10个类,每个类的输出都是1e-10,假如float精度只有1e-10,那么轻微波动,概率就巨幅的变动,可见,也不是个好现象)。但是反向传播呢?除了前向传播本身波动就很大之外,另一个问题也会出现——梯度消失,大规模的零值,(在ReLU中零值等于)梯度消失。除了零以外的值呢?途中5-layer还剩下一小点,但是量级已经很小了,随着层数加深,剩下的输出越来越少,最终输出全都变成零,那么反向传播的梯度也就全是零。梯度消失就越严重。

关于饱和和梯度消失问题,ReLU的特殊性:它必须要输出完全成为零的时候才是零梯度的状态,才是饱和状态,从不饱和到饱和是一个阶跃的过程,所以只能从数量级分析,量变引起质变。而sigmoid和tanh都是一个渐变的过程,相对来说更直观更容易理解,他们的不饱和到饱和是一个渐变的过程,直观解释就是分布越偏向两边饱和区,梯度越微小,最后反向相乘,导致梯度下溢消失。)

 

图四:Xavier初始化,乍一看,和std=0.1不相上下(也受限于例子,如果网络更深的话,效果会不同),但是如果仔细看最后一层分布第二高的bin的纵座标,Xavier优化接近2万,std=0.1是1万出头,而第一高的bin,他的纵座标反而降了一些,其实是优于std=0.1的,所以光看形状很难说明问题,重点要看刻度和座标!

(0,1)区间 

图五:He初始化,横轴,明显(0,1)分布比Xavier还要均匀一些,范围更大一些。

 

(0,1)区间 

总的来说,He初始化0,1区间更均匀稳定,分布的区域也大一些。

 

更多的实验结果就不发了,改参数,观察,就可以。

(好险,改了又改,差点就不能自恰了,好在仔细扣了座标,各种分析,座标真的重要,这个是你简单看一些教程和slides不会注意到的东西,多动手,多观察)

 

代码实现:

两个参数:用来对比普通权重初始化和Xavier或者“He初始化”的区别,还有具体看哪个激活函数的分布

plain_weight_init = True
activation = 'relu'

代码根目录(这本书的其他实践和其他博客的相关代码都在这个根目录下):https://github.com/huqinwei/python_deep_learning_introduction/chap06_weight_init_activation_histogram.py

代码已经有变动,不更新到这了,这段代码当做一个简化版。

#这个还是要好好练一下的,手写,各种激活的打印和监视。

# from book_dir.common.multi_layer_net import MultiLayerNet#没用上!!!!!!
import numpy as np
import matplotlib.pyplot as plt
import math

def sigmoid(x):
    return 1.0 / (1 + np.exp(-x))
def tanh(x):#自己平移sigmoid做的tanh和库里的tanh,和网上标准公式实现的tanh,是否有区别,
    return (2.0 / (1 + np.exp(-2*x))) - 1
def tanh2(x):#公式推导,其实是等价的
    y=(math.e**(x)-math.e**(-x))/(math.e**(x)+math.e**(-x))
    return y
def relu(x):
    return np.maximum(0,x)

x = np.random.randn(1000,100)
node_num = 100
hidden_layer_size = 5
activations = {}
before_activations = {}
hist_demo = False
plain_weight_init = True
activation = 'relu'

for i in range(hidden_layer_size):
    if i != 0:#有一层没算
        x = activations[i-1]#那么第一层呢,直接用原定义,注意这是裸写的网络,不是类!

    w = np.random.randn(node_num, node_num)
    if plain_weight_init:
        weight_init_std = 0.01#1,0.1,0.01,0.001#量级越小,后边就越消失
        w = w * weight_init_std
        
    else:
        if activation == 'tanh':
            w = w / np.sqrt(node_num)
        elif activation == 'sigmoid':
            w = w / np.sqrt(node_num)
        elif activation == 'relu':#作为对比,relu最好也用weight_init_std=0.01跑一次
            w = w / np.sqrt(node_num) * np.sqrt(2)#可以注释掉根号2对比,差距明显,这个很好解释,因为relu只有正半轴


    z = np.dot(x,w)
    if activation == 'tanh':
        a = tanh(z)
    elif activation == 'sigmoid':
        a = sigmoid(z)
    elif activation == 'relu':
        a = relu(z)
    else:
        a = sigmoid(z)
    activations[i] = a
    before_activations[i] = z

layer_nums = len(activations)
for i,z in before_activations.items():
    plt.subplot(2,layer_nums,i+1)#i从0起,plot从1起
    plt.title(str(i+1) + "-layer")
    zf = z.flatten()

    if activation == 'tanh':
        plt.hist(zf,30,range=(-1,1))#用tanh可以看到分布更好看一些,钟形
    elif activation == 'sigmoid':
        plt.hist(zf,30,range=(0,1))
    elif activation == 'relu':
        plt.hist(zf,range=(-1,1))



for i,a in activations.items():
    plt.subplot(2,layer_nums,i+1 + layer_nums)#i从0起,plot从1起
    plt.title(str(i+1) + "-layer")
    af = a.flatten()

    if activation == 'tanh':
        plt.hist(af,30,range=(-1,1))#用tanh可以看到分布更好看一些,钟形
    elif activation == 'sigmoid':
        plt.hist(af,30,range=(0,1))
    elif activation == 'relu':
        plt.hist(af,30)#根据hist的功能,0右侧是大于0,那么relu不可能0左侧没东西吧?0不要面子的吗????记错了,左闭右开,那么其实是[0,0.x],其实是包含0的,也包含极小的非零值


plt.show()








 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章