基本的神经网络结构略去不谈,让我们从一个NLP实例开始。
设想一个判断上述语句中心词'Paris'是否是命名实体的任务。在此任务中,我们不仅要捕获词向量表示的窗口中单词的存在,而且想要获取单词之间的相互影响。例如,可能"只有在'in'是第二个单词的情况下第一个单词会是'museums' "这一点会有用。这样的非线性决策常常不能由输入直接传给softmax函数来解决,而需要一个中间层的处理。随后我们还可以使用另一个矩阵U来生成非标准化的分数,也就是:
其中F为激活函数。
如果我们使用4维的词向量,窗口大小为5维,一层8个Units,那么一层的参数维度:
我们使用最大间隔作为优化目标,也就是让正确的样本的分数比错误的样本的分数尽可能高。设正例分数为S,负例分数为Sc,那么目标函数就要最大化(S-Sc)或者最小化(Sc-S)。不过,这里我们只关注Sc>S的部分,也就是分类错误的部分,得到优化目标:
不过,我们这个优化目标不太“安全”,为了达到比较好的泛化能力,我们不仅要让正例分数比负例高,还需要在正负例之间设定一个间隔,假定这个间隔为1(感觉就是SVM里面的思路):
然后就是利用反向传播算法去更新参数了,老生常谈,略去。
让我们来看看官方笔记中给出的一些神经网络的技巧。
Gradient Check
梯度检查是一种数值逼近的梯度计算技术,虽然计算效率低下以至于不能直接用于神经网络的训练,但是它可以让我们对导数的正确性做有用而完整的检查。
centered difference formula:
e是一个很小的数,常常只有10^-5数量级,为在theta上添加一个微小的干扰项e后的输出,为添加干扰项-e后的输出。利用此公式可估计某个参数状态下的梯度。在实际应用中,这个公式比利用导数公式进行估计更准确:
直觉上来说,考虑点前后两方面的变化量自然要比只考虑前向变化要来的精确些。
根据泰勒定理,这个梯度估计的误差为e^2数量级,通常已经足够精确。
不过,由于利用此公式计算每次要进行两次前向传递,加上很多神经网络的参数非常多,所以计算效率很低,只能用来做检查。官方笔记中的梯度检查代码:
def eval _ numerical _ gradient(f, x):
"""
a naive implementation of numerical gradient of f at x
- f should be a function that takes a single argument
- x is the point (numpy array) to evaluate the gradient
at
"""
fx = f(x) # evaluate function value at original point
grad = np.zeros(x.shape)
h = 0.00001
# iterate over all indexes in x
it = np.nditer(x, flags=[’multi _ index’],
op _ flags=[’readwrite’])
while not it.finished:
# evaluate function at x+h
ix = it.multi _ index
old _ value = x[ix]
x[ix] = old _ value + h # increment by h
fxh _ left = f(x) # evaluate f(x + h)
x[ix] = old _ value - h # decrement by h
fxh _ right = f(x) # evaluate f(x - h)
x[ix] = old _ value # restore to previous value(very important!)
# compute the partial derivative
grad[ix] = (fxh _ left - fxh _ right) / (2 * h) # the slope
it.iternext() # step to next dimension
return grad
正则化:
为解决过拟合,也就是高方差问题,常常会在损失函数中添加正则化项:
官方笔记中说我们都用L2正则化,因为L1正则化容易得到稀疏解。。。。嗯?能得到稀疏解不是优势么。。
总之L1正则化倾向于选择较少较大的参数,L2倾向于选择较多较小的参数。还有就是L2更利于凸优化。
应用中主要问题i是正则化权重的选取,太大容易欠拟合,太小还是会过拟合。
Dropout
在一次迭代中随机选择一部分神经元进行计算,可以学到更多有意义的信息,防止过拟合,改善性能。直觉上的原因就是每次迭代只更新了一个小的神经网络,这样参数能够捕捉到更大更有效的变化趋势,最后将这些小神经网络的预测结果进行平均。
官方笔记中提了一个问题,为了使训练过程和测试过程的输出一致,需要在测试阶段的每个神经元上乘一个因数,那么这个因数是什么?
假设丢弃率为p,每次迭代中只有(1-p)的神经元在工作,这(1-p)的神经元的输出期望要与标注相适应,也就是说,每个神经元的输出实际上是正常情况下输出的(1/(1-p))倍,那么测试时只要乘(1-p)就好了。
激活函数:
官方笔记中并没有说各种激活函数的优劣,只是介绍了一下各个函数长什么样。
首先是之前一直在用的sigmoid函数:
这函数由于算梯度方便且易于理解,所以一直以来都用这个入门,但是导致了一个很严重的问题——梯度消失,直觉理解就是这个函数在[-1,1]之外的增长率太低了,导致多层神经网络中梯度传到前面几层时就太小了,根本train不动。
Tanh函数:
sigmoid函数的变体,拓展了值域。
还有一些变体,要么是为了计算更快,要么是为了增大Z比较大时的导数,只到ReLU的出现,用之前的函数的情况就比较少了。
ReLU:
现在最流行的激活函数,计算快,Z很大时梯度保持地很好。还有一种变体:
这种变体就是为了使Z为负时也能有一定的梯度。
数据预处理:
均值移除:
给定一个输出数据集X,首先通过减去平均值使X变为以0为中心的分布。这种方法的优点在于一般只在训练集中计算平均值,而训练之间做0分布利用了全部数据的信息。
Normalization:
将所有输出映射到相同的范围中去,使得输入数据得以平等化。其实之前用softmax计算概率也是一中normalization的思想。
Whitening;
是特征变得不相关且有值为1的方差。具体方法就是先做均值移除再做奇异值分解。
参数初始化:
一般神经网络中的参数初始化就是选取0附近的比较小的随机数,这种方法一般也工作的比较好。但是,有人提出了一种初始化方案:
n(l)即为fan-in,n(l+1)即为fan-out。这种初始化方式在反向传播过程中有保持激活梯度的作用,如果要使用sigmoid类函数时有用。
学习率:
就是
中的梯度权重。
学习率高,每次迭代的更新幅度大,但是有可能出现震荡或者错过最优解的情况。学习率小,虽然通常能达到更好的性能,但是很容易陷入局部最小值,且会受到计算资源的限制。为此,有人提出了一些动态更新学习率的方法,例如:
当迭代次数超过时,学习率就会越来越小。
惯性更新:
带惯性的参数更新策略,目的主要是能越过一些局部极小值尽量找到全局最优解,实现策略也简单:
v = mu * v - alpha * grad _ x
x += v
Adaptive Optimization Methods
自适应优化,基本思想是根据参数变化的情况来选择学习率,每个参数的学习率取决于该参数的梯度更新的历史,使得具有稀少更新历史的参数使用更大的学习速率更快地更新。 换句话说,过去未被更新的参数现在可能具有更高的学习速率。然后再看一下现在很流行的Adam:
m = beta1 * m + (1-beta1) * dx
v = beta2 * v + (1-beta2) * (dx ** 2)
x += - learning _ rate * m / (np.sqrt(v) + eps)
Adam不仅用了自适应优化策略,而且还融入了惯性更新的思想。
理论上来说Adam看起来很美好,但是Adam也有自身的问题:
可能不收敛——由于参数是时间窗口内的累计,学习率的变化并不是单调的,遇到一些波动较大的数据时,学习后期也可能会发生震荡。
可能错过全局最优解——Adam的最大优势也是它的劣势,那就是收敛非常快,所以收敛过程中理所当然地可能会错过最优解。
目前,有很多人会使用Adam做初期,SGD做后期的方法。
领取专属 10元无门槛券
私享最新 技术干货