NN中每一个Affine层的权重参数的初始值是非常重要的,甚至会影响学习是否成功。但幸好我们已经有很多前辈总结出了比较好的初始值,以及判定一组初始值是否可以取得好的学习效果的方法。
权值衰减
有一种抑制过拟合,提高泛化能力的技术,叫做权值衰减,weight decay, 它的目的是减小NN的权重参数。
但是虽然较小权值参数可以获得好的学习效果避免过拟合,我们却不可以把权重的初始值设置为0!!! 这样将无法正确学习!!比如一个有一个隐层的NN,架构是2-2-2,即输入层隐层输出层分别有2,2,2个神经元,如果隐层的unit权重初始化为0,则输入 x 1 , x 2 x_1,x_2 x1,x2传到隐层两个unit的值都是这两个unit的偏置初始值 b 1 , b 2 b_1,b_2 b1,b2,再传到输出层的两个神经元获得的值只是输出层unit自己的偏置,这一路从第一步就已经把输入信息完全丢失了····还学个毛线
明确两点:
最好是NN每一层的所有权重参数取值不一样,因为取值一样两个神经元的相似性就太大了,干的一样的计算,计算结果也一样,实际上是在做无用功,学不到更多的东西;
有很多权重取相同的值的情况叫做“权重均一化”,这是非常不好的,这使得NN拥有许多不同权重的意义丧失了。所以实验中都是需要随机生成权重初始值的。
神经元激活值的分布有所偏向会造成NN“表现力受限”或梯度消失
NN把输入数据的信息逐层往后传,每经过一个隐层就得到一组激活函数值,作为下一个隐层的输入,所以在NN这种学习过程中,我们不希望一层学完了得到的激活值挤到一坨,即大多数激活值都是相同的值,那样的话,就算权重参数分布广学到的也都是一样的东西,还是学不好。
只有传递多样性的数据,才能使NN高效学习。
而权重的初始值是会影响到隐层激活值分布的, 下面展示不同权重初始值下NN每一层激活值的分布情况(直方图):
- w = np.random.randn(node_num, node_num) * 1(较大权重初始值) && sigmoid激活函数
标准差为1的高斯分布作为权重初始值
可以看出激活值多为0或1,因为sigmoid的输出大多接近0或1,则导数都是0,这样就会造成梯度消失,即在反向传播中计算的梯度的值越来越小,直到变为0(消失),并且层数加深会更严重
2. w = np.random.randn(node_num, node_num) * 1 && relu激活函数
梯度消失。
3. w = np.random.randn(node_num, node_num) * 1 && tanh激活函数
激活值集中在1附近,虽然不会出现梯度消失,但是倾向性严重,表现力受限,不好。
4. w = np.random.randn(node_num, node_num) * 0.01 && sigmoid
标准差变为0.01的高斯分布
激活值呈现靠近0.5的分布,虽然不用担心梯度消失了,但是激活值的分布不是平均的广泛地,而是有所倾向,这会使得NN的表现力差!!就像上面说的,如果几个unit输出的计算值是一样的,那它们的存在有什么意义呢?没有。1000个输出相同的神经元所做的事情实际上由1个神经元就可以做到了。
激活值非常有倾向性,限制NN的表现力,不好。
5. w = np.random.randn(node_num, node_num) * 0.01 && relu
后面的激活值集中在0附近,反向传播的梯度也会大多为0,学习根本无法进行,问题非常严重。
6. w = np.random.randn(node_num, node_num) * 0.01 && tanh
后面的激活值集中在0附近,反向传播的梯度也会大多为0,学习根本无法进行,问题非常严重。
7. w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num) && sigmoid
Xavier初始值
是Xavier Glorot论文中推荐的权重初始值,在很多深度学习框架中已经作为标准被使用。
它是以激活函数是线性函数为前提推导出来的。所以只适用于sigmoid和tanh, 他俩左右对称,在中间近似线性。但不适用于relu, relu做激活函数时会使用何凯明等人推荐的初始值。
他们为了使得各层激活值呈现相同的广度,进入了一个权重尺度。即:
如果前一层节点数为n,则初始值使用标准差为 1 n \frac{1}{\sqrt n} n1的高斯分布
所以前一层节点越多,权重初始值的标准差/尺度越小
从图像可以看到,每层激活值确实呈现了更广的分布,NN的表现力不受限制,有望高效学习
8. w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num) && relu
可以看到relu使用xavier初始值也取得了不错的分布,但是随着层数加深还是会倾向于0侧。因为relu的输入只要是负的,输出就是0,所以想办法尽量使得激活值正负相当,把affine层的输出的负值减少一些,所以就适量拉大了权重初始值的标准差,使用He初始值。
9. w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num) && tanh
使用tanh后,激活值分布呈现漂亮的吊钟型分布,解决了sigmoid中的歪斜
这是因为tanh是关于原点对称的s型曲线
而sigmoid却是关于(0,0.5)对称的S型曲线
用做激活函数的函数最好是具有关于原点对称的性质
11. w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num) && sigmoid
He初始值
前一层节点数为n,则初始值使用标准差为 2 n \sqrt\frac2 n n2的高斯分布
12. w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num) && relu
可以看出relu配合He初始值,每层是有相当的广度的
13. w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num) && tanh
最后用mnist数据集搭配5层NN试了一试,得到的效果:
he初始值学习的明显快的多,用的relu激活函数;
xavier差一些,用的sigmoid激活函数
用的是SGD
我做了个很有意思的实验, 既然He初始值是在xavier基础上改进的,拉大了标准差,那我再拉大一点呢???于是我把那个系数改为3,得到下图,注意绿色的He实际是系数为3的改进版;而黄色的Xavier才是He初始值的结果,果然学的更快!
当然这只是兴趣使然,没有理论依据,也许过分拉大标准差会带来某些弊端,只是在这个实验中没有体现出来,不然何凯明大佬怎么会不再拉大点呢。仅供娱乐,或者自己调参时可以改改看看效果。
总结
纵观实验结果,发现:
1. He初始值很好用!!搭配relu是深层网络的上上签,到后层也依旧保持很好的广度;搭配tanh可用于Deep NN; 甚至搭配sigmoid也能获得不多的广度
2. Xavier 搭配sigmoid,tanh, 但very Deep NN不能用tanh,更不要搭配relu
直方图代码:
# coding: utf-8
# weight_init_activation_histogram.py
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def ReLU(x):
return np.maximum(0, x)
def tanh(x):
return np.tanh(x)
input_data = np.random.randn(1000, 100) # 1000个输入数据
node_num = 100 # 各隐藏层的节点(神经元)数
hidden_layer_size = 5 # 隐藏层有5层
activations = {} # 激活值的结果保存在这里
x = input_data
for i in range(hidden_layer_size):
if i != 0:
x = activations[i-1]
# 改变初始值进行实验!
# w = np.random.randn(node_num, node_num) * 1
w = np.random.randn(node_num, node_num) * 0.01
# w = np.random.randn(node_num, node_num) * np.sqrt(1.0 / node_num)
# w = np.random.randn(node_num, node_num) * np.sqrt(2.0 / node_num)
a = np.dot(x, w)
# 将激活函数的种类也改变,来进行实验!
# z = sigmoid(a)
# z = ReLU(a)
z = tanh(a)
activations[i] = z
# 绘制直方图
for i, a in activations.items():
plt.subplot(1, len(activations), i+1)
plt.title(str(i+1) + "-layer")
if i != 0: plt.yticks([], [])
# plt.xlim(0.1, 1)
# plt.ylim(0, 7000)
plt.hist(a.flatten(), 30, range=(0,1))
plt.show()
不同权重初始值应用于mnist的部分代码:
# coding: utf-8
# weight_init_compare.py
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from util import smooth_curve
from MultiLayerNet import MultiLayerNet
from optimizer import SGD
# 0:读入MNIST数据==========
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True)
train_size = x_train.shape[0]
batch_size = 128
max_iterations = 2000
# 1:进行实验的设置==========
weight_init_types = {'std=0.01': 0.01, 'Xavier': 'sigmoid', 'He': 'relu'}
optimizer = SGD(lr=0.01)
networks = {}
train_loss = {}
for key, weight_type in weight_init_types.items():
networks[key] = MultiLayerNet(input_size=784, hidden_size_list=[100, 100, 100, 100],
output_size=10, weight_init_std=weight_type)
train_loss[key] = []
# 2:开始训练==========
for i in range(max_iterations):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
for key in weight_init_types.keys():
grads = networks[key].gradient(x_batch, t_batch)
optimizer.update(networks[key].params, grads)
loss = networks[key].loss(x_batch, t_batch)
train_loss[key].append(loss)
if i % 100 == 0:
print("===========" + "iteration:" + str(i) + "===========")
for key in weight_init_types.keys():
loss = networks[key].loss(x_batch, t_batch)
print(key + ":" + str(loss))
# 3.绘制图形==========
markers = {'std=0.01': 'o', 'Xavier': 's', 'He': 'D'}
x = np.arange(max_iterations)
for key in weight_init_types.keys():
plt.plot(x, smooth_curve(train_loss[key]), marker=markers[key], markevery=100, label=key)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.ylim(0, 2.5)
plt.legend()
plt.show()