「这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战」。
下文中用
tf
简写代表tensorflow
。
1.深度学习与深层神经网络
深度学习:一类通过多层非线性变换对高复杂性数据建模算法的合集。
深层神经网络:实现深度学习最常用的方法。
线性模型的局限性:
只通过线性变换,任意层的全连接神经网络和单层神经网络模型的表达能力没有任何区别,而且它们都是线性模型。然而线性模型能够解决的问题是有限的,这就是线性模型最大的局限性。
激活函数去实现线性化:
常用的几种非线性激活函数:
-
ReLU函数:
-
sigmoid函数:
-
tanh函数:
以下代码展示了如何通过TensorFlow实现ReLU函数加持的非线性前向传播算法:
a = tf.nn.relu(tf.matmul(x, w1) + biases1)
y = tf.nn.relu(tf.matmul(a, w2) + biases2)
复制代码
多层网络解决异或运算:
深层神经网络实际上有组合特征提取的功能。这个特性对于解决不易提取特征向量的问题(如图片识别、语音识别)有很大的帮助。
2.损失函数
2.1 经典损失函数
神经网络解决多分类的问题最常用的方法是,设置n个输出节点,每个节点对应着相应的类别,这样就能得到一个n维数组,理想情况下,如果该样本属于类别k,则该类别输出的值应该为1,其余类别应该为0。如:[1, 0, 0, 0, 0, 0, 0 , 0, 0, 0]。如何判断一个输出向量和期望的向量接近度?交叉熵是常用的评判方法之一,交叉熵刻画了两个概率分布之间的距离,它是分类问题中使用比较广的一种损失函数。
交叉熵:给定概率p,q,通过q来表示p的交叉熵为:
这里交叉熵刻画的是两个概率分布之间的距离,然而神经网络的输出却不一定是一个概率分布,Softmax变回归可以将神经网络向前传播得到的结果变成概率分布。
交叉熵代码解析:
cross_entroy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, le-10, 1.0)))
# 其中,y_代表正确结果,y代表预测结果
# clip_by_value可以将一个张量中的数值限制在一个范围内,本例中小于y的值都被换成y,大于le-10的值都被换成le-10
v1 = tf.constant([1.0, 2.0], [3.0, 4.0])
v2 = tf.constant([5.0, 6.0], [7.0, 8.0])
print tf.matmul(v1, v2).eval()
# 输出矩阵v1 v2相乘的结果
复制代码
一般在tf中,交叉熵跟softmax回归是一起使用的,所以tf对这两个功能进行了封装,提供了tf.nn.softmax_cross_entroy_with_logits函数:
cross_entroy = tf.nn.softmax_cross_entroy_with_logits(labels = y_, logits = y)
# 其中y代表了原始神经网络的输出结果,而y_给出了标准答案
复制代码
对于回归问题(对具体数值的预测),最常用的损失函数是均方误差(MSE,mean squared error)。定义如下:
其中yi为一个batch中第i个数据的正确答案,而yi'为神经网络给出的预测值,以下代码为如何实现:
mse = tf.reduce_mean(tf.square(y_ - y))
# 这里的减法也是两个矩阵中对应元素的减法
复制代码
2.2 自定义损失函数
loss = tf.reduce_sum(tf.where(tf.greater(v1 ,v2), (v1 - v2)*a, (v2 - v1)*b))
# tf.greater函数作用是比较大小
# tf.where有三个参数,第一个为选择依据,真就选第二个,假就选第三个
复制代码
3.神经网络优化算法
梯度下降算法、反向传播算法:
梯度下降算法主要用于单个参数的取值,而反向传播算法给出了一个高效的方式在所有参数上使用梯度下降算法。
3.1 梯度下降中心思想
假设用x表示神经网络中的参数,f(x)表示在给定的参数取值下,训练集上损失函数的大小,那么整个优化过程可以抽象为寻找这么一个x,使得f(x)的取值最小。
下面用一个例子来展示一下梯度下降算法的工作流程:假设参数的初始值为5,学习率为3,优化函数 (梯度就是求导):
轮数 | 当前轮参数值 | 梯度*学习率 | 更新后参数值 |
---|---|---|---|
1 | 5 | 2*5*0.3=3 | 2 |
2 | 2 | 2*2*0.3=1.2 | 0.8 |
3 | 0.8 | 2*0.8*0.3=0.48 | 0.32 |
4 | 0.32 | 2*0.32*0.3=0.192 | 0.128 |
5 | 0.128 | 2*0.128*0.3=0.0768 | 0.0512 |
神经网络算法的优化过程可以分为两阶段:
- 先通过前向传播算法计算得到预测值,并将预测值和真实值做对比,得出两者之间的差距;
- 在第二个阶段,通过反向传播算法计算损失函数对每一个参数的梯度,再根据梯度和学习率使用梯度下降算法更新每一个参数。
梯度下降算法的缺点:
- 未必会达到最优解:只有当损失函数为凸函数时,梯度下降算法才能给出全局最优解;
- 计算时间太长:因为要在全部训练数据上最小化损失,所以损失函数J(x)是在所有训练数据上的损失和,这样在每一轮迭代中都需要计算在全部训练数据上的损失函数;
- 为了加速训练过程,可以采用随机梯度下降算法,因为随机梯度下降算法每次优化的只是某一条训练数据上的参数;
- 一般在实际应用中,都采用上述二者相结合的算法--每次计算一小部分训练数据的损失函数。这一小部分数据被称之为一个batch。
神经网络的训练大致都遵循以下过程:
batch_size = n
# 每次读取一小部分数据作为当前的训练数据来执行反向传播算法
x = tf.placeholder(tf.float32, shape=(batch_size, 2), name='x-input')
y_ = tf.placeholder(tf.float32, shape=(batch_size, 1), name='y-input')
# 定义神经网络结构和优化算法
loss = ...
train_step = tf.train.AdamOptimizer(0.001).minimize(loss)
# 训练神经网络
with tf.Session() as sses:
# 参数初始化
...
# 迭代的更新参数
for i in range(STEPS)
#准备batch_size个训练数据。一般将所有训练数据随机打乱之后再选取可以得到更好的优化效果
current_X, current_Y = ...
sess.run(train_step, feed_dict={x:current_X, y_:current_Y})
复制代码
4.神经网络的进一步优化
4.1 学习率的设置
学习率:控制参数更新的速度。
学习率过大,会导致预测结果不准确,学习率过小,虽然收敛性很好,但是耗时,为了解决这个问题,更好地设置学习率,tf提供了--指数衰减法:tf.train.exponential_decay函数实现了指数衰减学习率。通过这个函数,可以先使用较大的学习率来快速得到一个比较优的解,然后随着迭代的继续逐步减小学习率,使得模型在训练后期更加稳定。
exponential_dacay函数会指数级地减小学习率,以下代码实现了它的功能:
decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)
# 其中decayed_learning_rate为每一轮优化时使用的学习率
# learning_rate为事先设定的初始学习率
# decay_rate为衰减系数
# decay_steps为衰减速度
复制代码
tf.train.exponential函数可以通过设置参数staircase选择不同的衰减方式,staircase默认为False,当它被设置成True时,global_step/decay_steps会被设置成整数,这使得学习率成为一个阶梯函数。在这样的设置下,decay_steps通常代表了完整的使用一遍训练数据所需要的迭代轮数。这个迭代数也就是总训练样本数据除以每一个batch中的训练样本数。这种设置的应用场景就是,每过一遍训练数据,学习率就减小一次,这可以使得训练数据集中的所有数据对模型训练有相等的作用。当使用连续的指数衰减学习率时,不同的训练数据有不同的学习率,而当学习率减小时,对应的训练数据对模型训练结果的影响也就小了。
下面给出了代码来示范tf.train.exponential_decay函数的使用:
global_step = tf.Variable(0)
# 通过exponential_decay函数生成学习率
learning_rate = tf.train.exponential_decay(0.1, global_step, 100, 0.96, staircase=True)
#使用指数衰减的学习率,在minimize函数中传入global_step将自动更新global_step参数,从而使得学习率也得到相应的更新
learning_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(...my loss..., global_step=global_step)
# 上述函数指定了初始学习率为0.1,因为指定了staircase为True,所以每训练100轮后学习率乘以0.96.一般来说初始学习率、衰减系数和衰减速度都是根据经验设置的。
复制代码
4.2 过拟合问题
所谓“过拟合”,指的是当一个模型过为复杂之后,它可以很好地“记忆”每一个训练数据中随机噪音的部分,而忘记要去“学习”训练数据中通用的趋势。
为了避免过拟合问题,一个非常常用的方法是正则化。正则化的思想就是在损失函数中加入刻画模型复杂程度的指标。假设用于刻画模型在训练数据上表现得损失函数为J(x),那么在优化时不是直接优化J(x),而是优化J(x) + bR(w),其中R(w)刻画的是模型的复杂程度,而b表示模型复杂损失在总损失中的比例。这里的x表示的是一个神经网络中所有的参数,它包括边上的权重w和偏置项b。一般来说,模型复杂度只由权重w决定。常用的函数有两种,一种是L1正则化,一种是L2正则化。无论哪种正则化方式,基本思想都是希望通过限制权重的大小,使得模型不能任意拟合训练数据张的噪音。
L1、L2也是有区别的,L1正则化会让参数变得稀疏,而L2则不会。(区别就是一个取绝对值,一个取平方值,当参数很小时,平方的影响忽略不计)以下代码展示了tf提供的L2正则化方法:
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为定义的损失函数,它由两部分组成
# 一部分是均方误差损失函数,刻画了模型在训练数据上的表现
# 第二部分就是正则化,它防止模型过渡模拟训练数据中的随机噪声
# lambda参数表示了正则化项的权重
# w为需要计算正则化损失的参数
# 以下代码展示了L1正则化
print sses.run(tf.contrib.layers.l1_regularizer(.5))(weights)//其中.5为正则化项的权重
复制代码
4.3 滑动平均模型
使模型在测试数据上更健壮的方法--滑动平均模型。
TensorFlow中提供了tf.train.ExponentialMovingAverage来实现滑动平均模型。在初始化这个函数前,要提供一个衰减率,decay。这个衰减率用于控制模型更新的速度。ExponentialMovingAverage对每一个变量会维护一个影子变量(shadow variable),这个影子变量的初始值就是相应变量的初始值,而每次运行变量更新时,影子变量的值会更新为:
其中shadow_variable为影子变量,variable为待更新的变量,decay为衰减率。从中可以得出,decay越大,模型越稳定。
ExponentialMovingAverage还提供了num_updates参数来动态设置decay的大小。
5.循环神经网络
循环神经网络--recurrent neural network,RNN
长短时记忆网络--long short-term memory,LSTM
循环神经网络的来源就是为了刻画一个当前的输出与之前信息的关系,从网络结构上看,循环神经网络会记忆之前的信息,并利用之前的信息影响后面节点的输出。也就是说循环神经网络的隐藏层之间的节点是有连接的,隐藏层的输入不仅包括输入层的输入,还包括上一时刻隐藏层的输出。
上图是一个典型的循环神经网络模型,从上图可以看出,在每个时刻t,循环神经网络会针对该时刻的输入结合当前模型的状态给出一个输出,并更新模型状态。循环结构的主体A的输入除了来自xt,还 有一个循环的边来提供上一时刻的隐藏状态ht-1,在每一个时刻,循环神经网络的模块A在读取了xt和ht-1后,会生成新的隐藏状态ht和本时刻的输出ot。由于模块A中的运算和变量在不同的时刻是相同的,因此循环神经网络理论上可以被看作是同一个神经网络结构被无限复制的结果。正如卷积神经网络在不同的空间位置共享参数,循环神经网络在不同的时间共享参数,从而用有限的参数处理任意长度的序列。
循环神经网络在展开后,可以看做一个有N个中间层的前馈神经网络,这个前馈神经网络没有循环链接,因此可以直接使用反向传播算法进行训练,而不需要其他特别的优化算法。这样的训练方法称为"沿时间反向传播",是训练循环神经网络最常见的方法。从循环神经网络的结构可以看出它最适合解决与时间序列相关的问题,主要应用在语音识别、语言模型、机器翻译以及时序分析等问题上。使用单层全连接神经网络作为循环体的循环神经网络结构图:
类似地,我们可以定义循环神经网络的损失函数,循环神经网络唯一的区别在于它每个时刻都有输出,所以循环神经网络的总损失为所有时刻(或部分时刻)上的损失函数的总和。定义完损失函数之后,就可以套用之前的优化框架,然后TensorFlow就可以自动完成模型训练的过程。理论上循环神经网络可以支持任意长度的序列,然而在实际问题中,如果序列过长,一方面会导致优化时出现梯度消散和梯度爆炸的问题,另一方面,展开后的前馈神经网络会占用过大的内存,所以实际中一般会规定一个最大长度,当序列长度超过规定长度之后会对序列进行截断。
6.长短时记忆网络(LSTM)结构
有时候不需要通过保存的上下文信息进行判断,有时候又需要之前某段时间的数据来进行判断,传统循环神经网络无法满足这种需求,这时就需要长短时记忆结构。与单一的循环神经网络结构不同,LSTM是一个拥有三个"门"结构的特殊网络结构。
所谓"门"的结构就是使用sigmoid神经网络和一个按位做乘法的操作,这两个操作合在一起就是一个"门"的结构。之所以称之为门,是因为使用sigmoid作为激活函数的全连接神经网络层会输出一个0到1之间的值,描述当前输入有多少信息量可以通过这个结构。当这个"门"打开时,sigmoid神经网络层输出为1,全部信息都可以通过,当这个"门"关闭时,sigmoid神经网络层输出为0,全部信息都无法通过。在LSTM中,遗忘门和输入门是最关键的部分。
遗忘门的作用是让循环神经网络忘记之前没有用的信息。遗忘门会根据当前的输入Xt和上一时刻输出Ht-1来决定哪一部分记忆需要被遗忘。假设状态c的维度为n。遗忘门会根据当前的输入xt和上一时刻输出ht-1用sigmoid计算一个维度为n的向量,它在每一个维度上的值都在(0,1)范围内。再将上一时刻的状态ct-1和该项量按位相乘,那么f取值接近0的维度就会被遗忘,f取值接近1的维度就会被保留。输入门作用就是在循环神经网络忘记部分记忆后,补充新的记忆。输入门会根据xt和ht-1决定哪些信息加入到状态ct-1中生成新的状态ct。具体每个门的公式定义如下:
其中 、 、 、 是4个维度为[2n, n]的参数矩阵。
import tensorflow as tf
# 定义一个LSTM结构。在TensorFlow中通过一句简单的命令就可以实现一个完整的LSTM结构。
# LSTM中使用的变量也会在函数中自动被声明
lstm = tf.nn.rnn_cell.BasicLSTMCell(lstm_hidden_size)
# 将LSTM中的状态初始化为全0数组。BasicLSTMCell类提供了zero_state函数来生成
# 全零的初始状态。state是一个包含两个张量的LSTMStateTuple类,其中state.c和state.h
# 分别对应了c状态和h状态
# 和其他神经网络类似,在优化循环神经网络时,每次也会使用一个batch的训练样本
state = lstm.zero_state(batch_size, tf.float32)
# 定义损失函数
loss = 0.0
# 虽然在测试时循环神经网络可以处理任意长度的序列,但是在训练中为了将循环网络展开成
# 前馈神经网络,我们需要知道训练数据的序列长度。以下代码中的num_steps就表示这个长度
for i in range(num_steps):
# 在第一个时刻声明LSTM结构中使用的变量,在之后的时刻都需要服用之前定义好的变量
if i > 0 : tf.get_variable_scope().reuse_variables()
# 每一步处理时间序列中的一个时刻。将当前输入currnt_input和前一时刻的状态state
# 传入定义的LSTM结构中可以得到当前LSTM的输出lstm_output和更新后的状态state。lstm_output
# 用于输出给其他层,state用于输出给下一时刻,它们在dropout等方面可以有不同的处理方式
lstm_output, state = lstm(current_input, state)
# 将当前时刻LSTM结构的输出传入一个全连接层得到最后的输出
final_output = fully_connected(lstm_output)
# 计算当前时刻输出的损失
los += calc_loss(final_output, expected_output)
# 接下来就是训练模型
复制代码
7.循环神经网络的变种
7.1 双向循环神经网络和深层循环神经网络
在有些问题中,当前时刻的输出不仅和之前的状态有关系,也和之后的状态有关系,这就需要使用双向循环神经网络。
双向循环神经网络的主体就是两个单项循环神经网络的结合。在每一个时刻t,输入会同时提供给这两个方向相反的循环神经网络。两个网络独立进行计算,各自产生该时刻的新状态和输出,而双向循环网络的最终输出是这两个单向循环神经网络的简单拼接。每一层网络中的循环体可以自由选择任意结构,如之前介绍的简单RNN、LSTM均可。
深层循环神经网络是另一个变种,为了增强模型的表达能力,可以在网络中设置多个循环层,将每层循环网络的输出传给下一层进行处理。对于深层循环神经网络,在一个L层的深层循环神经网络中,每一时刻的输入xi到输出ot之间有L个循环体,循环网络因此可以从L层输入中抽取更加高层的信息。以下代码展示了如何用MultiRNNCell类实现深层循环神经网络:
import tensorflow as tf
# 定义一个基本的LSTM结构作为循环体的基础结构。深层循环神经网络也支持使用其他循环体结构
lstm_cell = tf.nn.rnn_cell.BasicLSTMCell
# 通过MultiRNNCell类实现深层循环神经网络中每一时刻的前向传播过程。其中
# number_of_layers表示多少层。初始化MutiRNNCell,否则TensorFlow会在每一层之间共享参数
stacked_lstm = tf.nn.rnn_cell.MultiRNNCell(
lstm_cell(lstm_size) for _ in range(number_of_layers)
)
# 和经典循环神经网络一样,可以通过zero_state函数来获取初始状态
state = stacked_lstm.zero_state(batch_size, tf.float32)
# 计算每一时刻的前向传播结果
for i in range(len(num_steps)):
if i > 0: tf.get_variable_scope().reuse_variables()
stacked_lstm_output, state = stacked_lstm(current_input, state)
final_output = fully_connected(stacked_lstm_output)
loss += calc_loss(final_output, expected_output)
复制代码
7.2 循环神经网络的dropout
在神经网络结构中使用dropout方法会让神经网络更加健壮。循环神经网络一般只在不同层循环体结构之间使用dropout,而不在同一层的循环体结构之间使用。也就是说从时刻t-l传递到时刻t时,循环神经网络不会进行状态的dropout,而在同一时刻t中,不同层循环体之间会使用。
在TensorFlow中,使用tf.nn.rnn_cell.DropoutWrapper类可以实现dropout。