(1)梯度下降模型
梯度下降算法主要用于优化单个参数的取值,而反向传播算法给出了一个高效的方式在所有的参数上使用梯度下降算法,从而使得神经网络模型在训练数据上的损失函数尽可能小。反向传播算法是训练神经网络的核心算法,它可以根据定义好的损失函数优化神经网络中参数的取值,从而使神经网络的模型在训练数据集上的损失函数达到一个较小值。
假设用θ表示神经网络中的参数,J(θ)表示在给定的参数取值下,训练数据集上损失函数的大小,那么整个优化过程可以抽象为寻找一个参数θ,使得J(θ) 最小。因为目前没有一个通用的方法可以对任意损失函数直接求解最佳的参数取值,所以在实践中,梯度下降算法是最常用的神经网络优化方法。如图是梯度下降算法的原理。
x 轴表示参数θ的取值,y轴表示损失函数J(θ)的值。假设当前的参数和损失值对应图中小圆点的位置,那么梯度下降算法会将参数向x轴左侧移动,从而使得小圆点朝着箭头的方向移动。假设学习率为η,那么参数更新的公式为:
其中偏导部分就是该参数在小圆点处的梯度,总是沿着负梯度方向移动。
需要注意的是,梯度下降算法并不能保证优化的函数达到全局最优解。如图
当在小圆点位置,偏导等于0或者接近0时,参数就不会再进一步更新。如果 x 的初始值落在右侧深色的区间中,那么通过梯度下降得到的结果都会落到小黑点代表的局部最优解。由此可见在训练神经网络时,参数的初始值会很大程度影响最后得到的结果。只有当损失函数为凸函数时,梯度下降算法才能保证达到全局最优解。
除了不一定能达到全局最优外,梯度下降算法的另一个问题就是计算时间太长。因为要在全部训练数据上最小化损失,所以损失函数J(θ)是在所有训练数据上的损失和。
(2)神经网络进一步优化 -- 学习率设置
上述介绍了学习率在梯度下降算法中的使用,学习率决定了参数每次更新的幅度。学习率过大会造成摇摆,过小会造成训练时间过长,为了解决学习率的问题,TensorFlow提供了一种更加灵活的学习率设置方法 -- 指数衰减法。tf.train.exponential_decay函数实现了指数衰减学习率。
decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)
其中 decayed_learning_rate 为每一轮优化时使用的学习率,learning_rate 为事先设定的初始学习率,decay_rate为衰减系数,decay_steps为衰减速度。如下图显示了随着迭代轮数的增加,学习率逐步降低的过程,迭代轮数就是总训练样本数除以每一个 batch 中的训练样本数。
global_step = tf.Variable(0) #通过 exponential_decay 函数生成学习率,初始学习率为 0.1 learning_rate = tf.train.exponential_decay(0.1,global_step,100,0.96,staircase = True) #staircase = True 表示梯状衰减 #使用指数衰减的学习率,在 minimize 函数中传入global_step 将自动更新 global_step 参数,从而使得学习率也得到相应更新 learning_step = tf.train.GradientDescentOptimizer(learning_rate)\ .minimize(...my_loss...,global_step = global_step)
(3)神经网络进一步优化 -- 过拟合与正则化
过拟合,指的是当一个模型过为复杂后,它可以很好的“记忆”每一个训练数据中随机噪音的部分而忘了要去“学习”训练数据中通用的趋势。举一个极端的例子,如果一个模型中的参数比训练数据的总数还多,那么只要训练数据不冲突,这个模型完全可以记住所有训练数据的结果从而使得损失函数为0。
为了避免过拟合问题,一个非常常用的方法是正则化。正则化的思想就是在损失函数中加入刻画模型复杂度的指标。假设用于刻画模型在训练数据上表现的损失函数为 J(θ),那么在优化时不是直接优化J(θ),而是优化 J(θ) + λR(w)。其中 R(w) 刻画的是模型的复杂度,而 λ 表示模型复杂损失在总损失中的比例。一般来说模型复杂度只由权重 w 决定。常用的刻画模型复杂度的函数 R(w) 有两种,一种是 L1 正则化,计算公式是
另一种是 L2 正则化,计算公式是
无论是哪一种正则化方式,基本思想都是希望通过限制权重的大小,使得模型不能任意拟合训练数据中的随机噪音。但这两种正则化方式有很大的区别:
1. L1 正则化会让参数变得更稀疏,而 L2 正则化不会。所谓参数更稀疏就是会有更多的参数变为0。
2. L1 正则化不可导,L2 正则化可导。所以优化 L2 正则化损失函数更简洁,优化 L1 正则化损失函数更复杂。
w = tf.Variable(tf.random_normal([2,1],stddev = 1,seed = 1)) y = tf.matmul(x,w) loss = tf.reduce_mean(tf.square(y_ - y)) + tf.contrib.layers.l2_regularizer(lambda)(w)
在上述代码中,loss 为定义的损失函数,它由两部分组成。第一部分是前面介绍的均方差函数,它刻画了模型在训练数据上的表现。第二部分就是 L2 正则化。
weights = tf.constant([[1.0,-2.0],[-3.0,4.0]]) with tf.Session() as sess: #输出为 (|1|+|-2|+|-3|+|4|) * 0.5 = 5 其中 0.5 为正则化项的权重 print sess.run(tf.contrib.layers.l1_regularizer(0.5)(weights)) #输出为 (1^2 + (-2)^2 + (-3)^2 + (4)^2) /2 * 0.5 = 7.5 print sess.run(tf.contrib.layers.l2_regularizer(0.5)(weights))
以上代码显示了 L1 正则化和 L2 正则化的计算差别。但当神经网络的参数增多后,这样的方式首先会导致损失函数 loss 的定义很长,可读性差且容易出错。但更为主要的是,当网络结构复杂化之后定义网络结构的部分和计算损失函数的部分可能不在一个函数中,这样通过变量这种方式计算损失函数就不方便了。为了解决这个问题,可以利用TensorFlow中提供的集合,以下代码给出了通过集合计算一个 5 层神经网络带 L2 正则化的损失函数的计算方法。
import tensorflow as tf #获取一层神经网络边上的权重,并将这个权重的 L2 正则化损失加入名称为 'losses' 的集合中 def get_weight(shape,lambda): #生成一个变量 var = tf.Variable(tf.random_normal(shape),dtype = tf.float32) # add_to_collection 函数将这个新生成变量的 L2 正则化损失加入集合 # 这个函数的第一个参数 'losses' 是集合的名字,第二个参数是要加入集合的内容 tf.add_to_collection('losses',tf.contrib.layers.l2_regularizer(lambda)(var)) return var x = tf.placeholder(tf.float32,shape = (None,2)) y_ = tf.placeholder(tf.float32,shape = (None,1)) batch_size = 8 #定义了每一层网络节点中的个数 layer_dimension = [2,10,10,10,1] #神经网络的层数 n_layers = len(layer_dimension) #这个变量维护前向传播时最深层的节点,开始的时候是输入层 cur_layer = x #当前层的节点个数 in_dimension = layer_dimension[0] #通过 for 循环来生成 5 层全连接神经网络 for i in range(1,n_layers): out_dimension = layer_dimension[i] #下一层节点个数 #生成当前层中权重的变量,并将这个变量的 L2 正则化损失加入计算图上的集合 weight = get_weight([in_dimension,out_dimension],0.001) bias = tf.Variable(tf.constant(0.1,shape = [out_dimension])) #使用relu 激活函数 cur_layer = tf.nn.relu(tf.matmul(cur_layer,weight) + bias) #进入下一层之前将下一层的节点个数更新为当前节点个数 in_dimension = layer_dimension[i] #定义神经网络前向传播的同时已经将所有的 L2 正则化损失加入了图上的集合 #这里只需要计算刻画模型在数据上表现的损失函数 mse_loss = tf.reduce_mean(tf.square(y_ - cur_layer)) #将均方差损失函数加入集合 tf.add_to_collection('losses',mse_loss) # get_collection 返回一个列表,这个列表是所有的这个集合中的元素。 # 在这个样例中,这些元素就是损失函数的不同部分,将它们加起来就可以得到最终的损失函数 loss = tf.add_n(tf.get_collection('losses'))
(4)神经网络进一步优化 -- 滑动平均模型
另一个可以使模型在测试数据上更健壮的方法 -- 滑动平均模型。在采用随机梯度下降算法训练神经网络时,使用滑动平均模型在很多应用中都可以在一定程度上提高最终模型在测试数据上的表现。
在TensorFlow中提供了 tf.train.ExponentialMovingAverage 来实现滑动平均模型。在初始化时,需要提供一个衰减率(decay)。这个衰减率将用于控制模型更新的速度。滑动平均对每一个变量会维护一个影子变量,这个影子变量的初始值就是相应变量的初始值,而每次更新变量时,影子变量的值会更新成:
shadow_variable = decay * shadow_variable + (1 - decay)*variable (decay 为衰减率)
从公式可以看到,decay 决定了模型更新的速度,decay 越大则模型越趋于稳定。在实际应用中,decay 一般取非常接近 1 的数,比如 0.99 或 0.999。 为了使得模型在训练前期可以更新的更快,滑动平均还提供了 num_updates 参数来动态设置 decay 的大小。如果在滑动平均初始化时提供了 num_updates 参数,那么每次使用的衰减率将是
import tensorflow as tf #定义一个变量用于计算滑动平均,这个变量的初始值为 0 v1 = tf.Variable(0,dtype = tf.float32) #这里 step 变量模拟神经网络中迭代的轮数,可以用于动态控制衰减率 step = tf.Variable(0,trainable = False) #定义一个滑动平均类。初始化时给定了衰减率 0.99 和控制衰减率的变量 step ema = tf.train.ExponentialMovingAverage(0.99)(step) #定义一个更新变量滑动平均的操作,这里需要给定一个列表,每次执行这个操作时,这个列表中的变量都会更新 maintain_averages_op = ema.apply([v1]) with tf.Session() as sess: #初始化所有变量 tf.initialize_all_variables().run() #通过 ema.average(v1) 获取滑动平均之后的变量的取值。在初始化之后变量 v1 的值和 v1的滑动平均都是 0 print sess.run([v1,ema.average(v1)]) #更新 v1 的值到 5 sess.run(tf.assign(v1,5)) #更新 v1 的滑动平均值,衰减率为 min{0.99,(1+step)/(10+step) = 0.1} = 0.1 sess.run(maintain_averages_op) print sess.run([v1,ema.average(v1)]) #输出 [5.0,4.5] #更新 step 的值为 10000 sess.run(tf.assign(step,10000)) #更新 v1 的值为 10 sess.run(tf.assign(v1,10)) #更新 v1 的滑动平均值,衰减率为 min{0.99,(1+step)/(10+step)} = 0.99 #所以 v1 的滑动平均会被更新为 0.99*4.5 + 0.01*10 = 4.555 sess.run(maintain_averages_op) print sess.run([v1,ema.average(v1)]) #输出 [10.0,4.5549998] #再次更新滑动平均,得到的新滑动平均值为 0.99*4.555 + 0.01*10 = 4.60945 sess.run(maintain_averages_op) print sess.run([v1,ema.average(v1)]) #输出 [10.0,4.6094499]
通过上述代码可知,滑动平均模型是一个使得训练在基于后期时趋于稳定的一个模型。