调试处理
一般最需要调试的是学习率α;其次是momentum的超参数β(一般为0.9)、mini-batch的大小、隐藏单元的数量n[l];接下来才是层数L、学习率衰减(可能需要选择多个学习率的值)。当应用Adam算法时,从不调试β1、β2、ε,总是分别选择0.9,0.999,10-8。
如果有两个超参数,常用的做法是在网格中取样点,看看选择哪个参数效果最好。
上图是一个5×5的网格。当参数的数量相对较少时,这个方法很实用。但更推荐的做法是:
随机取值,然后用这些随机选择的点试验超参数的效果。
如果有三个超参数,那么搜索的不是一个方格,而是一个立方体,超参数三代表第三维,然后在三维立方体中取值。
给超参数取值时,另一个惯例是采用由粗糙到精细的策略,比如在上个图中进行了取值,也许可以发现效果最好的某个点,也许这个点周围的其他一些点效果也很好,接下来就要放大这块小区域,然后在其中更密集地取值或随机取值。
通过试验超参数的不同取值,可以选择对于训练集目标而言的最优值或对于开发集而言的最优值或在超参搜索过程中最想优化的东西。
为超参数选择合适的范围
随机取值可以提升搜索效率,但随机取值并不是在有效值范围内的随机均匀取值,而是选择合适的标尺去探究这些超参数。
假设要搜索超参数学习速率α,假设认为其值最小是0.0001,最大是1。如果画一条从0.0001到1的数轴,沿其随机均匀取值,那么90%的数值将会落在0.1到1之间,那么在0.1到1之间就会占用90%的资源;而在0.0001到0.1之间只有10%的搜索资源。相反用对数标尺搜索超参数的方式会更合理。因此,不使用线性轴,分别依次取0.0001,0.001,0.01,0.1,1:
在对数轴上均匀随机取点。这样在0.0001到0.001之间,0.001到0.01之间等等就会有更多的搜索资源可用。
在python中,可以:
r = -4 * np.random.rand()
α = 10^r
由第一行代码可知r的范围为[-4, 0],因此α会在10-4和100之间。
总结:当在10a和10b之间取值时,所要做的就是在[a, b]区间随机均匀地给r取值,然后将超参数设置为10r,这就是在对数轴上取值的过程。
当要给用于计算指数的加权平均值β选择范围时:
假设认为β是0.9到0.999之间的某个值。当计算指数的加权平均值时,取0.9就像在10个值(1/(1-0.9)=10)中计算平均值,有点类似于之间介绍的计算10天的温度平均值;而取0.999就是在1000个值(1/(1-0.999)=1000)中取平均。
如果想在0.9到0.999区间搜索,不能用线性轴取值,不可随机均匀地在此区间取值。
所以解决这个问题的关键在于需要探究1-β,此值在0.1到0.001这个区间中,所以给1-β取值0.1,0.01,0.001,应用之前的方法,0.1=10-1,0.001=10-3,所以应该在[-3, -1]里随机均匀地给r取值。
其中,1-β=10r,所以β=1-10r,这就是这个超参数特定的选择范围内的取值。
当β接近1时,所得结果的灵敏度会变化。如果β在0.9到0.9005之间取值,无关紧要,结果几乎不会变化,因为这个大概是10个值取平均;如果β在0.999到0.9995之间取值,会对算法产生巨大影响,因为这里是基于1000个值取平均,然后变成了2000个值取平均。因此,当β接近1时,β会对细微的变化变得很敏感,使用线性轴取值误差会很大。所以整个取值中,需要更加密集地在β接近1的区间内取值。
超参数训练的实践:Pandas VS Caviar
需要每隔几个月重新测试或评估超参数,以确保对数值依然很满意。
关于如何搜索超参数的问题,一般有两种思想:
1.
照看模型,像Pandas,一个模型类似于一个熊猫宝宝,剩下熊猫宝宝然后对它悉心照顾。适用于有庞大的数据组,但没有许多计算资源或足够的CPU和GPU的情况。也就是说,只可以一次负担起试验一个模型或一小批模型。这种情况下,即使在试验中,每天都在观察,及时可以调整改良模型。
2.
同时试验多种模型,像Caviar,鱼类繁殖产生许多卵,但不对任何一个多加照顾,只是希望其中一个或一群能够表现出色,像鱼子酱。设置好了超参数,让其自己运行数天,然后获得了学习曲线,可以是损失函数J或实验误差的损失或数据设置误差的损失。同时可以有别的不同超参数设定的模型,它们也会生成学习曲线。用这种方法可以同时试验许多不同的参数设定,然后最后快速选择工作效果最好的那个。
正则化网络的激活函数
Batch归一化算法可以让参数搜索问题变得很容易,使神经网络对超参数的选择更加稳定,超参数的范围会更庞大,工作效果也很好。
Batch归一化的使用方法为:
在神经网络中,已知一些中间值,假设有一些隐藏单元值,从z(1)到z(m)(这些来源于隐藏层z[l](i))。接下来计算平均值;然后取每个z(i)值,使其规范化,也就是[z(i) - 均值μ] / [标准偏差σ2 + ε]1/2,这样z的每一个分量都含有平均值0和方差1。
将Batch Norm拟合进神经网络
batch归一化是发生在计算z和a之间的。
在tf中,无需自己去实现batch归一化,用函数就可实现:
tf.nn.batch-normalization
实践中,batch归一化通常和训练集的mini-batch一起使用。使用batch归一化的方法就是:用第一个mini-batchX{1}计算z[1],再继续计算第二个mini-batchX{2},接着batch归一化会减去均值,除以标准差,就得到了z~[1],再应用激活函数得到a[1];然后用w[2]、b[2]计算z[2]…所有这一切都是为了在第一个mini-batch上进行一步梯度下降法。
然后在第二个mini-batch上做类似的操作。
实际使用batch归一化的时候,可以把b[l]看作是0,因为它的值并不会影响结果。那么参数z[l] = w[l]a[l-1],归一化的z[l]norm = z~ = γ[l]z[l] + β[l]。其中z[l]、b[l]、β[l]、γ[l]的维数是(n[l], 1)。
假设在使用mini-batch梯度下降法,在运行t从1到mini-batch数量的for循环,可以对每个隐藏层mini-batch X{t}应用正向prop,用batch归一化替代z[l]为z~[l]。
接下来,确保在这个mini-batch中,z值有归一化的均值和方差z~[l]。然后使用反向prop计算dw[l]、db[l] 及dβ[l]、dγ[l]。
最后,更新这些参数,w[l] = w[l] - αdw[l],β[l] = β[l] - αdβ[l]、γ[l] = γ[l] - αdγ[l]。也可以使用其他算法来更新。
Batch Norm为什么奏效?
batch归一化有用的另一个原因就是它可以使权重比你的网络更滞后或更深层,比如第10层的权重相比于神经网络中前层的网络的权重更能经受得住变化。
covariate shift的想法是:如果已经学习了从x到y的映射,如果x的分布改变了,那么可能需要重新训练学习算法,这种做法同样适用于真实函数。
测试时的Batch Norm
batch归一化将数据以mini-batch的形式逐一处理,但在测试时可能需要对每一个样本逐一处理。
这些是用来执行batch归一化的等式:
其中,m表示这个mini-batch中的样本数量,而不是整个训练集。μ和σ2是在整个mini-batch上进行计算的,但在测试时,不能将一个mini-batch中的6428或2056个样本同时处理,因此需要其他方式来得到μ和σ2。当只有一个样本时,样本的均值和方差是没有意义的。那么为了将神经网络运用于测试,就需要单独估算μ和σ2。在典型的batch归一化运用中,可以用一个指数加权平均来估算,这个平均数涵盖了所有mini-batch。
假设在L层,有mini-batch X{1}、X{2}以及对应的y值等。当训练第一个mini-batch时,就得到了μ{1}[l];当训练第二个mini-batch时,就得到了μ{2}[l];…这些指数平均值就成为了对这一隐藏层的μ均值的估值。同理去估计σ2。
因此在用不同的mini-batch训练神经网络的同时,可以查看每一层的μ和σ2的平均数的实时数值。最后在测试时,对应上面后两个等式就可以计算了。
总结:在训练时,μ和σ2是在整个mini-batch上计算出来的,包含了像是64或28或其他一定数量的样本。但在测试时,需要根据训练集估计μ和σ2,从而逐一处理样本。估算的方法有很多,理论上可以在最终的网络中运行整个训练集来得到μ和σ2,但实际中,通常运用指数加权平均来追踪在训练过程中看到的μ和σ2的值;还可以用指数加权平均,有时也叫流动平均来粗略估算μ和σ2。然后用测试中μ和σ2的值来进行所需的隐藏单元z值的调整。如果使用的是某种深度学习框架,通常会有默认的估算μ和σ2的方式。
Softmax回归
它是logistic回归的一般形式,能在识别某一分类时做出预测,可以识别多类分类中的一个,而不仅仅是识别两个分类。比如你不仅仅想识别猫,还想识别猫、狗、小鸡,如果不属于以上任何一类,就分到“其他”类。
用C表示输入的被分入的类别总个数,0、1、n-1。如果有四类,猫、狗、鸡、其他,那么就用0、1、2、3去代表它们。那么就需要建立一个输出层有4个的神经网络(输出单元有4个):
因此n,也就是输出层L层的单元数量等于4,一般也等于C。
希望输出层单元的数字告诉我们这4中类型中每一个的概率有多大,所以输出层的第一个节点输出的应该是输入X的情况下,输出“其他”类的概率;第二个节点输出的是输入X的情况下,输出猫类的概率;第三个节点输出的是输入X的情况下,输出狗类的概率;第四个节点输出的是输入X的情况下,输出小鸡类的概率。因此,上面的y hat必须输出四个数字告诉这四种概率,因此它是一个4×1维的向量,而且这四个数字加起来应该等于1。
让网络做到这一点的标准模型需要用到Softmax层以及输出层来生成输出。在神经网络的最后一层需要像往常一样计算各层的线性部分z[L] = z[L]a[L-1] +b[L],算出了z[L]之后需要应用Softmax激活函数,其作用如下:
首先计算一个临时变量t = ez[L],这适用于每个元素,在本例中,z[L]是一个(4, 1)维向量,因此t也是一个(4, 1)维向量。
然后输出a[L],它基本上就是向量t,但是会归一化,使和为1:
训练一个Softmax分类器
输出层计算出的z[L]如下:
C=4,如果输出层的激活函数g[L]是softmax激活函数,那么输出如下:
简单来说就是用临时变量t将它归一化,使总和为1,于是这个输出就是a[L]。
在向量z中,最大的元素是5,而最大的概率也是它所对应的那个位置处的概率。
与之相反,hardmax会把向量z变成[1 0 0 0]T,hardmax函数会观察z的元素,然后在z中最大元素的位置放1,其他位置设置为0。这个函数会让最大的元素的输出为1,其他元素的输出都为0。
softmax回归或softmax激活函数将logistic激活函数推广到C类,而不仅仅是两类。如果C=2,那么softmax就变回了logistic回归。
怎样训练带有softmax输出层的神经网络?
在softmax中,一般使用的损失函数L(y hat, y)是负的j从1到C的yjlogy hatj的和,它可以帮助我们找到训练集中的真实类别,然后试图使该类别相应的概率尽可能地高。这是单个训练样本的损失。
整个训练集的损失J就是整个训练集损失的总和,把训练算法对所有训练样本的预测都加起来。
若C=4,那么y是一个4×1向量,y hat也是一个4×1向量,如果使用向量化实现Y = [y(1), [y(2), …,[y(m)]4×m,同样y hat也是4×m维。
如果需要反向传播计算,需要下列公式:
但我们使用的编程框架可以自动为我们进行反向传播的导数计算。
深度学习框架
选择框架的标准为:便于编程;运行速度;是否真的开放。
Tensorflow
假设损失函数J(w) = w2 - 10w + 25,很显然当w为5时它的值最小,但如果不知道这点,可以使用tensorflow将其最小化。
import numpy as np
import tensorflow as tf
w = tf.Variable(0, dtype=tf.float32)
cost = tf.add(tf.add(w**2, tf.multiply(-10., w)))
train = tf.train.GradientDescentOptimizer(0.01).minimize(cost)#用0.01的学习率,目标是最小化损失
#以下为惯用表达
init = tf.global_variables_initializer()
session = tf.Session()
session.run(init)#初始化全局变量
print(session.run(w))
第一行代码将w初始化为0,因为w是想要优化的参数,所以将其称为变量Variable;第二行代码定义损失函数,一般使用add和multiply之类的函数,也可以写成代码cost = w**2 - 10*w + 25
,一旦w被定义为了tf变量,其平方、乘法和加减运算都重载了反向传播和梯度运算;第三行代码定义train为学习算法,它用梯度下降法优化器使损失函数最小化。代码sess.run(w)
评估了w。
最后结果会是0,因为我们还什么都没有运行。
最后三行代码还可以用以下代码来代替:
with tf.Session() as session:
session.run(init)
print(session.run(w))
接着继续编写:
session.run(train)#运行一步梯度下降法
print(session.run(w))
再次输出评估的w的值,输出结果为0.1。
for i in range(1000):
session.run(train)
print(session.run(w))
现在运行梯度下降1000次迭代,再让其输出,结果为4.99999,与真实值5已经很接近了。
如何将训练数据加入tf程序呢?
coefficients = np.array([[1.], [-10.], [25.]])
w = tf.Variable(0, dtype=tf.float32)
x = tf.placeholder(tf.float32, [3, 1])#让x成为[3, 1]数组
cost = x[0][0]*w**2 + x[1][0]*w + x[2][0]
tf.train.GradientDescentOptimizer(0.01).minimize(cost)
init = tf.global_variables_initializer()
session = tf.Session()
session.run(init)#初始化全局变量
print(session.run(w))
session.run(train, feed_dict={x:coefficients})
print(session.run(w))
for i in range(1000):
session.run(train, feed_dict={x:coefficients})
print(session.run(w))
cost这个二次方程的三个项前都有固定的系数,可以把这些数字1,-10,25变成数据,将上面定义cost的代码更换。
现在x变成了控制这个二次函数系数的数据,而前面的placeholder函数告诉tf稍后会为x提供数值,所以在最初需要定义一个数组coefficients,然后在session那里将数组训练。
以上会得到和之前一样的代码,但现在如果想要改变这个二次函数的系数,只需要在一开始改变数组的值即可。
tf中的placeholder是一个之后会赋值的变量,这种方式便于把训练数据加入损失方程,用feed_dict={x:coefficients}
来加入数据。如果在做mini-batch梯度下降,在每次迭代时需要插入不同的mini-batch,就可以使用feed_dict来喂入训练集的不同子集,把不同的mini-batch喂入损失函数需要数据的地方。