Recurrent Neural Networks(RNN) 循环神经网络初探

1. 针对机器学习/深度神经网络“记忆能力”的讨论

0x1:数据规律的本质是能表明此类数据的通用模式 - 数据挖掘的本质是在进行模式提取

数据的本质是存储信息的介质,而模式(pattern)是信息的一种表现形式。在一个数据集中,模式有不少不一样的表现形式,无论是在传统的机器学习训练的过程,仍是是深度学习的训练过程,本质上都是在进行模式提取。php

而从信息论的角度来看,模式提取也能够理解为一种信息压缩过程,经过将信息从一种形式压缩为另外一种形式。压缩的过程不可避免会形成信息丢失。html

笔者这里列举几种典型的体现模式提取思想的算法。git

1. 矢量图表示法

1)像素图表示法 - 最原始的信息记录方法

像素这个概念咱们都很是熟悉,像素是表示每一个图像的基本单位。github

传统的bmp位图亦称为点阵图像绘制图像,是由称做像素(图片元素)的单个点组成的。这些点能够进行不一样的排列和染色以构成图样。web

当放大位图时,能够看见赖以构成整个图像的无数单个方块。扩大位图尺寸的效果是扩大单个像素,从而使线条和形状显得良莠不齐。算法

同时,缩小位图尺寸也会使原图变形,由于此举是经过减小像素来使整个图像变小的。shell

一样,因为位图图像是以排列的像素集合体形式建立的,因此不能单独操做(如移动)局部位图。能够想象一下,位图的移动相似数组的平移,成本很是高。编程

2)矢量图表示法 - 记录信息不如记录生成原理

矢量图,也称为面向对象的图像绘图图像,在数学上定义为一系列由线链接的点。数组

矢量文件中的图形元素称为对象。每一个对象都是一个自成一体的实体,它具备颜色、形状、轮廓、大小和屏幕位置等属性。安全

矢量图使用直线和曲线来描述图形,这些图形的元素是一些点、线、矩形、多边形、圆和弧线等等,它们都是经过数学公式计算得到的。

例如一幅花的矢量图形其实是由线段造成外框轮廓,由外框的颜色以及外框所封闭的颜色决定花显示出的颜色。

矢量图最大的好处是“存储成本小”,由于矢量图并不须要存储原始文件的全部像素信息,而只要存储有限的用于生成原始像素图像的生成算法便可(很有机器学习模型参数的感受),所以,矢量图能够无限放大而不会增长额外的存储成本。

2. K-mean聚类

1)聚类前的数据集(原始信息)是怎么样的?

2)聚类后获得的压缩后信息是怎么样的?

原始数据集通过K-mean以后,获得的压缩后信息为 N 个聚类中心,N 由算法操做者决定。

3)Kmeans压缩后信息如何表明原始的信息?

聚类获得的 N 个聚类中心就是 K-means模型的模型参数。

从某种程度上来讲,这个 N 个聚类中心就能够表明原始的样本信息。

训练获得的Kmeans模型能够用于新样本的标注(预测),当输入待检测样本的时候,Kmeans根据必定的搜索算法,搜索已知的聚类中心,将待检测样本打标为最靠近的那个类别。

这样,Kmeans经过 N 个聚类中心,实现了原始信息的模式提取,或者说核心信息压缩。

3. 线性回归模型学习

一个典型的例子

在一元线性回归的场景中,线性回归模型经过训练获得:Y = ax + b。

经过2个参数【a,b】,对本来庞大的样本点集进行了描述。

从上面3个例子中,能够看到一个共通点,即“信息压缩”,它们的本质都是抽象出了一种形式化表达,而这种表达就表明了一种模式,这种模式能够表明本来的海量样本集中的某种形式的规律。

至于如何提取这种模式规律,以及提以后的模型规律如何被运用进行后续的新的预测,就是不一样机器学习算法的变化所在了。

0x2:神经网络是如何记忆和存储数据中的模式规律的?

机器学习的神经是参考人脑神经网络的构造而创造的,那人脑神经网络又是如何识别、存储、记忆天天看到的咱们所谓的有用的知识的呢?

这块内容笔者只是查询了网上的一些讨论资料,彻底跨专业了,也只是看科普看了一些大概,目前业内彷佛并无一个准确的定义,不少的讨论彷佛是在实验观测和理论假设猜想之上。

可是我看下来,有几个观点不少业内学者提到:

1. 记忆并非一个直接经过bit方式存储在大脑内,而是经过一些的神经细胞的结构来存储,即结构及记忆知识,换言之,大脑并非直接存储知识自己,而是存储知识的概念结构,也就是所谓的模式;
2. 脑内反映某外界客观物体,是由被该外界刺激激活的全部皮层细胞组成的,这些同时被激活的神经元称做“细胞集合”,假如这些细胞相互链接,细胞集合内的链接持续激活,对外界客观物体的内部反应就能做为短时程记忆始终保存,若是细胞集合能持续激活很长一段时间,那么细胞间相互链接更有效的神经元就会链接在一块儿,更紧密的链接就会使细胞集合再次兴奋,记忆的巩固就可能发生。换句话说,要记忆某种信息模式,就必须生成相应的神经网结构,而且经过不断的刺激使之固定下来;
3. 仅仅集团内的一部分细胞的破坏并不能消除记忆,记忆的痕迹普遍分布于细胞集合的细胞链接内。 

Relevant Link: 

https://arxiv.org/pdf/1706.05394.pdf
http://www.360doc.com/content/17/0622/19/16619343_665595088.shtml 
https://www.zhihu.com/question/20264424 

 

2. 循环神经网络RNN(Recurrent Neural Network)介绍

循环神经网络(recurrent neural network)或 RNN 是一类用于处理了序列数据的神经网络。咱们这个章节来针对RNN的一些基本概念展开讨论。

0x1:共享参数思想

咱们先从参数共享机制提及,这是RNN循环神经网络的一个核心特色,也是RNN可以拥有某些强大性能的缘由之一。

参数共享机制使得神经网络对序列数据中的模式具有了必定的平移不变泛化能力,以及模式记忆能力。

1. 从传统全链接前馈网络的特征表征提及 - 不包含参数共享机制

不单是对DNN,传统的机器学习模型,也全都要求输入的特征向量是一个定长的vector。
对这种定长的 feature vector,不一样的特征维度之间是正交独立的,即打乱顺序是不会影响最后的检测结果。因此开发者在进行特征工程的时候,并不须要考虑特征之间的序列关系,只要把特征“堆”到一块儿便可。
传统的全链接前馈网络会给每一个输入特征分配一个单独的参数,不一样特征对应的参数是单独调整的。

须要注意的是,在NLP场景中,传统机器学习算法常常和词袋编码结合使用。词袋编码虽然不具有共享参数能力,可是由于词袋编码自己就就丢弃原始文本中的时序信息,即词袋特征对原始文本中的序列顺序变化并不敏感,所以从某种程度上来讲,词袋编码具有必定的对时序文本中特征平移的泛化能力。

2. 卷积网络的共享参数 - 感知域平移参数共享

一种捕获文本中相同单词在不一样位置的特征的方法是,1 维时间序列上使用卷积

这种卷积方法是时延神经网络的基础 (Lang and Hinton, 1988; Waibel et al., 1989; Lang et al., 1990)

卷积操做容许网络跨时间共享参数,可是浅层的。卷积的输出是一个序列,其中输出中的每一项是相邻几项输入的函数。

参数共享的概念体如今每一个时间步中使用的相同卷积核。

3. RNN中使用参数共享机制实现特征位置平移不变性

从多层网络到循环网络,循环网络吸取了20世纪80年代机器学习和统计模型早期思想的优势:在模型的不一样部分共享参数。参数共享使得RNN模型可以扩展到不一样长度的序列样本并进行泛化。

若是咱们在每一个时间点都有一个单独的参数,不但不能泛化到训练时没有见过的序列长度,也不能在时间上共享不一样序列长度和不一样位置的统计强度。当信息的特定部分会在序列内多个位置出现时,这样的共享尤其重要。

例如,考虑这两句话:“I went to Nepal in 2009’’ “In 2009, I went to Nepal.”。

若是咱们让一个机器学习模型读取这两个句子,并提取叙述者去Nepal的年份,不管 “2009’’ 是做为句子的第六个单词仍是第二个单词出现,咱们都但愿模型能认出 “2009’’ 做为相关资料片断。

相比传统DNN网络,循环神经网络在几个时间步内共享相同的权重,不须要分别学习句子每一个位置的全部语言规则。

相比于CNN卷积网络,循环神经网络以不一样的方式共享参数。输出的每一项是前一项的函数。输出的每一项对先前的输出应用相同的更新规则(参数共享)而产生。这种循环方式致使参数经过很深的计算图共享。

此外, 须要注意的是,所谓的时间序列没必要是字面上现实世界中流逝的时间。有时,它仅表示序列中的位置。RNN 也能够应用于跨越两个维度的空间数据(如图像)

当应用于涉及时间的数据, 而且将整个序列提供给网络以前就能观察到整个序列时,该网络可具备关于时间向后的链接。

笔者思考:RNN和CNN这种神经网络具有参数共享机制,能够对特征的位置平移实现泛化能力,那是否是就意味着RNN全面优于传统机器学习算法呢?笔者认为答案是否认的!

由于并非全部的数学建模场景中,特征都呈现时序关系的。整体上来讲,特征工程获得的特征分为两大类:1)正交独立的特征集,例如说描述一个桌子的一组物理参数;2)时序特征,例如某人发出的一段声音的声纹信号。

对于正交独立的特征集,时序模型就不必定能发挥其自己的做用,甚至还会起反效果。正交独立的特征集更适合使用传统机器学习模型进行几率分布/模型参数的训练评估。

在具体的AI项目中,咱们会遇到各类各样的具体问题,首先要思考的是,从哪些角度入手进行特征工程,若是是正交独立特征集,选择哪些特征?若是是时序特征,选取什么数据抽取时序特征?

4. 参数共享的假设前提

在循环网络中使用的参数共享的前提是相同参数可用于不一样时间步的假设。也 就是说,假设给定时刻 t 的变量后,时刻 t + 1 变量的条件几率分布是 平稳的 (stationary),这意味着以前的时间步与下个时间步之间的关系并不依赖于 t

0x2:RNN网络具有的时序记忆能力

在不少项目场景中,针对当前样本的判断不仅仅仅限于当前样本,而须要结合历史的样本进行时序依赖判断。

以突显识别举例来讲(实际上RNN并不局限于图像时序数据):

若是咱们看到一个沙滩的场景,咱们应该在接下来的帧数中加强沙滩活动:若是图像中的人在海水中,那么这个图像可能会被标记为“游泳”;若是图像中的人闭着眼睛躺在沙滩上,那么这个图像可能会被标记为“日光浴”。

若是若是咱们可以记得Bob刚刚抵达一家超市的话,那么即便没有任何特别的超市特征,Bob手拿一块培根的图像均可能会被标记为“购物”而不是“烹饪”。

所以,咱们但愿让咱们的模型可以跟踪事物的各类状态:

  • 在检测完每一个图像后,模型会输出一个标签,这个标签对应该图像的识别结果(即RNN每一个时间步输出的 y 值)。同时模型对世界的认识也会有所更新(更新隐状态)。例如,模型可能会学习自主地去发现并跟踪相关的信息,如位置信息(场景发生的地点是在家中仍是在沙滩上?)、时间(若是场景中包含月亮的图像,模型应该记住该场景发生在晚上)和电影进度(这个图像是第一帧仍是第100帧?)

  • 在向模型输入新的图像时,模型应该结合它收集到的历史信息,对当前的输入图片进行更合理的综合判断。

循环神经网络(RNN),它不只可以完成简单地图像输入和事件输出行为,还能保持对世界的记忆(给不一样信息分配的权重),以帮助改进本身的分类功能。

0x3: RNN的图灵完备性

"循环"两个字,表达了RNN的核心特征, 即系统的输出会保留在网络里和系统下一刻的输入一块儿共同决定下一刻的输出。这就把动力学的本质体现了出来, 循环正对应动力学系统的反馈概念,能够刻画复杂的历史依赖。

另外一个角度看也符合著名的图灵机原理。 即此刻的状态包含上一刻的历史,又是下一刻变化的依据。

这其实包含了可编程神经网络的核心概念,即当你有一个未知的过程,但你能够测量到输入和输出, 你假设当这个过程经过RNN的时候,它是能够本身学会这样的输入输出规律的, 并且所以具备预测能力。 在这点上说, RNN是图灵完备的。

下面列举了一些可能的图灵操做规则:

1. 图1即CNN的架构
2. 图2是把单一输入转化为序列输出,例如把图像转化成一行文字
3. 图三是把序列输入转化为单个输出,好比情感测试,测量一段话正面或负面的情绪
4. 图四是把序列转化为序列,最典型的是机器翻译 
5. 图5是无时差(注意输入和输出的"时差")的序列到序列转化, 好比给一个录像中的每一帧贴标签(每个中间状态都输出一个output)

0x4:长期依赖的挑战

学习循环网络长期依赖的数学挑战在于“梯度消失”和“梯度爆炸”。根本问题是,通过许多阶段传播后的梯度倾向于消失(大部分状况)或爆炸(不多,但对优化过程影响很大)。

即便咱们假设循环网络是参数稳定的(可存储记忆,且梯度不爆炸),但长期依赖的困难来自比短时间相互做用指数小的权重(涉及许多 Jacobian 相乘)。

循环网络涉及相同函数的屡次组合,每一个时间步一次。这些组合能够致使极端非线性行为,以下图所示:

当组合许多非线性函数(如这里所示的线性 tanh 层)时,获得的结果是高度非线性的。
在大多数状况下,导数不是过大,就是太小,以及在增长和减少之间的屡次交替。
此处,咱们绘制从 100 维隐藏状态降到单个维度的线性投影,绘制于 y 轴上。x 轴是 100 维空间中沿着随机方向的初始状态的坐标。所以,咱们能够将该图视为高维函数的线性截面。曲线显示每一个时间步以后的函数,或者等价地,转换函数被组合必定次数以后。

特别地,循环神经网络所使用的函数组合有点像矩阵乘法。咱们能够认为,循环联系:

是一个很是简单的、缺乏非线性激活函数和输入 x 的循环神经网络。这种递推关系本质上描述了幂法。它能够被简化为:

而当 W 符合下列形式的特征分:

其中 Q 正交,循环性可进一步简化为

特征值提高到 t 次后,致使幅值不到一的特征值衰减到零,而幅值大于一的特征值就会激增。任何不与最大特征向量对齐的 h(0) 的部分将最终被丢弃。

RNN 梯度消失和爆炸问题是由不一样研究人员独立发现 (Hochreiter, 1991a; Bengio et al., 1993, 1994b)。有人可能会但愿经过简单地停留在梯度不消失或爆炸的参数空间来避免这个问题。不幸的是,为了储存记忆并对小扰动具备鲁棒性,RNN 必须进入参数空间中的梯度消失区域。
具体来讲,每当模型可以表示长期依赖时,长期相互做用的梯度幅值就会变得指数小,这将直接致使长期依赖的权重调整速度极度放缓,这也是RNN很难学到长序列特征的缘由。
同时,因为长期依赖关系的信号很容易被短时间相关性产生的最小波动隐藏,于是学习长期依赖可能须要很长的时间。
实践中实验代表,当咱们增长了须要捕获的依赖关系的跨度, 基于梯度的优化变得愈来愈困难,SGD 在长度仅为 10 或 20 的序列上成功训练传统 RNN 的几率迅速变为 0。
咱们这里讨论几种缓解Long Term依赖问题的方法。

1. 多时间尺度的策略

处理长期依赖的一种方法是设计工做在多个时间尺度的模型,使模型的某些部分在细粒度时间尺度上操做并能处理小细节;而其余部分在粗时间尺度上操做,并能把遥远过去的信息更有效地传递过来。

这种循环网络的依赖链是在时间单元间“跳跃的”,相似于咱们经常会将不一样感知域的CNN Filter进行Stacking以得到综合的效果,多时间尺度的目的也是同样,但愿同时兼顾短程依赖和长程依赖的序列特征模式。

细粒度时间尺度不用特殊设计,就是原始的RNN结构。粗时间尺度须要特殊设计,目前存在多种同时构建粗细时间尺度的策略。

1)在时间轴增长跳跃链接

增长从遥远过去的变量到目前变量的直接链接是获得粗时间尺度的一种方法。使用这样跳跃链接的想法能够追溯到Lin et al. (1996),紧接是向前馈网络引入延迟的想法 (Lang and Hinton, 1988)。但其实增长跳跃的本质就是引入延时,本该被传入邻接时间步的输出被延迟传入了以后 N 步时间步中。

在普通的循环网络中,循环从时刻 t 的单元链接 到时刻 t + 1 单元。构造较长的延迟循环网络是可能的。

对于梯度爆炸和梯度消失问题,引入了 d 延时的循环链接能够减轻这个问题。由于引入 d 延时后,导数指数减少的速度变为 τ/d相关,而不是 τ。

既然同时存在延迟链接单步链接,梯度仍可能成 t 指数爆炸,只是问题会有所缓解。

2)渗漏单元

得到导数乘积接近 1 的另外一方式是设置线性自链接单元,而且这些链接的权重接近 1。
咱们对某些 v 值应用更新累积一个滑动平均值 μ(t), 其中 α 是一个从 μ(t−1) 到 μ(t) 线性自链接的例子。

当 α 接近 1 时,滑动平均值能记住过去很长一段时间的信息,而当 α 接近 0,关于过去的信息被迅速丢弃。

线性自链接的隐藏单元能够模拟滑动平均的行为。这种隐藏单元称为渗漏单元(leaky unit)

d 时间步的跳跃链接能够确保单元总能被 d 个时间步前的那个值影响。使用权重接近 1 的线性自链接是确保该单元能够访问过去值的不一样方式。

线性自链接经过调节实值 α 更平滑灵活地调整这种效果,比整数的跳跃长度更“柔顺”。

咱们能够经过两种基本策略设置渗漏单元使用的时间常数。

1. 一种策略是手动将其固定为常数,例如在初始化时从某些分布采样它们的值;
2. 另外一种策略是使时间常数成为自由变量,并学习出来;

3)删除链接

处理长期依赖另外一种方法是在多个时间尺度组织 RNN 状态的想法。

一个根本的理论是:信息在较慢的时间尺度上更容易长距离流动。这很容易理解,若是时间尺度很小,信息在每一个时间步要迅速的被决策是保留仍是舍弃,以及保留和舍弃的比例。时间尺度变慢,意味着决策的次数减小,信息保留的几率就增大了。

这个想法与以前讨论的时间维度上的跳跃链接不一样:

1. 该方法涉及主动删除长度为 1 的链接并用更长的链接替换它们。以这种方式修改的单元被迫在长时间尺度上运做;
2. 而经过时间跳跃链接是添加边,收到这种新链接的单元,能够学习在长时间尺度上运做,但也能够选择专一于本身其余的短时间链接;

0x5:长短时间记忆和门控RNN

像渗漏单元同样,门控 RNN 想法也是基于生成经过时间的路径,其中导数既不消失也不发生爆炸。

渗漏单元经过手动选择常量的链接权重或参数化的链接权重来达到这一目的。门控 RNN 将其推广为在每一个时间步均可能改变的链接权重。

渗漏单元容许网络在较长持续时间内积累信息(诸如用于特定特征或类的线索)。然而,一旦该信息被使用了,让神经网络遗忘旧的状态多是有帮助的。

例如,若是一个序列是由子序列组成,咱们但愿渗漏单元能在各子序列内积累线索,可是当进入新的子序列前能够忘记旧的子序列的线索信息,咱们须要一种忘记旧状态的机制。

咱们但愿神经网络学会决定什么时候清除状态,而不是手动决定。这就是门控 RNN 要作的事。

1. LSTM

引入自循环的巧妙构思,以产生梯度长时间持续流动的路径是初始长短时间记忆 (long short-term memory, LSTM)模型的核心贡献。

其中一个关键扩展是使自循环的权重视上下文而定,而不是固定的

咱们前面说过,RNN的核心思想就是参数共享,LSTM也一样遵照这个核心思想,所不一样的是,LSTM并非从头至尾参数一直共享,而是在某个“时间区间”内进行共享,在整个时间步链路上权重会动态调整。

在这种状况下,即便是具备固定参数的 LSTM,累积的时间尺度也能够因输入序列而改变,由于时间常数是模型自己的输出。

2. LSTM的核心思想

1. 容许网络动态地控制时间尺度,即信息在时间步链条上的存活时间是动态控制的;
2. 容许网络动态控制不一样单元的遗忘行为;

Relevant Link:

https://blog.csdn.net/cf2suds8x8f0v/article/details/79244587
https://blog.csdn.net/qq_36279445/article/details/72724649

 

3. 循环神经网络计算图(Computational Graph)

本章咱们将计算图的思想扩展到包括循环。

这些周期表明了变量自身的值在将来某一时间步会对自身值的影响。这样的计算图容许咱们定义循环神经网络。

0x1:计算图定义

咱们使用图中的每个节点来表示一个变量。

变量能够是标量、向量、矩阵、张量、或者甚至是另外一类型的变量。

为了形式化咱们的图形,咱们还需引入操做(operation)这一律念。操做是指一个或多个变量的简单函数。

咱们的图形语言伴随着一组被容许的操做。咱们能够经过将多个操做复合在一块儿来描述更为复杂的函数。

若是变量 y 是变量 x 经过一个操做计算获得的,那么咱们画一条从 x 到 y 的有向边。咱们有时用操做的名称来注释输出的节点,当上下文很明确时,有时也会省略这个标注。

1. 不一样操做对应的计算图举例

使用 × 操做计算 z = xy 的图

用于逻辑回归预测 yˆ = σ(x⊤w + b) 的图。一些中间表达式在代数表达式中没有名称,但在图形中却须要。咱们简单地将 第 i 个这样的变量命名为 u(i)

表达式 H = max{0, XW + b} 的计算图,在给定包含小批量输入数据的设计矩阵 X 时,它计算整流线性单元激活的设计矩阵 H。

对变量实施多个操做也是可能的。该计算图对线性回归模型的权重 w 实施多个操做。这个权重不只用于预测 yˆ,也用于权重衰减罚项 λ∑ w2。这就是所谓的结构化风险评估。

0x2:展开RNN计算图

计算图是形式化一组计算结构的方式,如那些涉及将输入和参数映射到输出和损失的计算。

咱们对展开(unfolding)递归或循环计算获得的重复结构进行解释,这些重复结构一般对应于一个事件链展开(unfolding)这个计算图将更好地可视化深度网络结构中的参数共享。

1. 动态系统的经典形式计算图

例如,考虑下式:

,其中,称为系统的状态。

s 在时刻 t 的定义须要参考时刻 t-1 时一样的定义,所以上式是循环的

对有限时间步 τ, τ - 1 次应用这个定义能够展开这个图。例如 τ = 3,咱们对上式进行展开,能够获得:

以这种方式重复应用定义,展开等式,就能获得不涉及循环的表达式。

如今咱们可使用传统的有向无环图(和HMM同样都是有向无环图几率图模型)呈现上式的表达。

每一个节点表示在某个时刻 t 的状态,而且函数 f t 处的状态映射到 t + 1 处的状态。全部时间步都使用相同的参数(用于参数化 f 的相同 θ 值)

2. 存在外部驱动信号的动态系统的计算图

做为另外一个例子,让咱们考虑由外部信号驱动的动态系统:

从公式上看,当前状态包含了整个过去序列的信息。可是这个历史信息是有损的

当训练循环网络根据过去预测将来时,网络一般要学会使用 h(t) 做为过去序列的有损摘要。

此摘要通常而言必定是有损的,由于其映射任意长度的序列到一固定长度的向量 h(t)。

根据不一样的训练准则,摘要可能选择性地精确保留过去序列的某些方面

例如,若是在统计语言建模中使用RNN,一般给定前一个词预测下一个词,可能没有必要存储时刻 t 前输入序列中的全部信息,而仅仅存储足够预测句子其他部分的信息(相似HMM)。

最苛刻的状况是咱们要求 h(t) 足够丰富,并能大体恢复输入序列,如自编码器框架。

上面公式能够用两种不一样的方式绘制,以下图:

1)回路图表示法

一种方法是为可能在模型的物理实现中存在的部分赋予一个节点,如生物神经网络。在这个观点下,网络定义了实时操做的回路,如上图左侧,其当前状态能够影响其将来的状态。

咱们使用 回路图的黑色方块代表在时刻 t 的状态到时刻 t + 1 的状态单个时刻延迟中的相互做用。

2)展开表示法

另外一个绘制 RNN 的方法是展开的计算图,其中每个组件由许多不一样的变量表示,每一个时间步一个变量,表示在该时间点组件的状态。每一个时间步的每一个变量绘制为计算图的一个独立节点,如上图右侧。

咱们所说的展开是将左图中的回路映射为右图中包含重复组件的计算图的操做。目前,展开图的大小取决于序列长度。

3. 展开计算图的优势

咱们能够用一个函数 g(t) 表明经 t 步展开后的循环:

函数 g(t) 将所有的过去序列做为输入来生成当前状态,可是展开的循环架构容许咱们将 g(t) 分解为函数 f 的重复应用。所以,展开过程引入两个主要优势:

1. 不管序列的长度,学成的模型始终具备相同的输入大小,由于它指定的是从一种状态到另外一种状态的转移,而不是在可变长度的历史状态上操做
2. 咱们能够在每一个时间步使用相同参数的相同转移函数 f

这两个因素使得学习在全部时间步全部序列长度上操做单一的模型 f 是可能的,而不须要在全部可能时间步学习独立的模型 g(t)。

学习单一的共享模型容许泛化到训练集中未出现的序列长度,而且估计模型所需的训练样本远远少于不带参数共享的模型。

循环神经网络能够经过许多不一样的方式创建。就像几乎全部函数均可以被认为是前馈网络,本质上任何涉及循环的函数均可以被认为是一个循环神经网络

Relevant Link:

《深度学习》花书

 

4. 循环神经网络逻辑图结构

基于以前讨论的图展开参数共享的思想,能够设计各类循环神经网络。

读者朋友须要注意的是,循环神经网络不是特指必定具体的算法实现,循环神经网络是特指一整类具有某些特性的神经网络结构,注意要和TensorFlow/theano中的RNN实现类区分开来。

循环神经网络从大的分类来讲能够分为如下几种:

1. 从序列到序列的神经网络:即每一个时间步都有输出;
3. 从序列到向量的神经网络:即读取整个序列后产生单个输出,即整个循环网络能够压缩为一个拥有惟一输出的循环递归函数;
3. 从向量到序列的神经网络:输入单个向量,在每一个时间步都有输出;

在遵循以上设计模型的原则之下,RNN能够进行各类结构上的变种,使之具有相应新的能力和性能。

咱们学习RNN,就是要重点理解不一样结构之间的区别和原理,理解不一样的网络拓朴结构是如何影响信息流的传递和依赖。至于具体网络内部的激活函数是用tang仍是relu,其实倒还不是那么重要了。

0x1:经典RNN结构 - 时间步之间存在“隐藏神经元”循环链接

1. 逻辑流程图 - 将输入序列映射到等长的输出序列

下图是该循环神经网络的逻辑流程图:

计算循环网络(将 x 值的输入序列映射到输出值 o 的对应序列) 训练损失的计算图;
RNN输入到隐藏的链接由权重矩阵 U 参数化;
隐藏到隐藏的循环链接由权重矩阵 W 参数化以及隐藏到输出的链接由权重矩阵 V 参数化;
其中每一个节点如今与一个特定的时间实例相关联

任何图灵可计算的函数均可以经过这样一个有限维的循环网络计算,在这个意义上公式表明的循环神经网络是万能的。

RNN 通过若干时间步后读取输出,这与由图灵机所用的时间步是渐近线性的,与输入长度也是渐近线性的。

RNN 做为图灵机使用时,须要一个二进制序列做为输入,其输出必须离散化以提供二进制输出。利用单个有限大小的特定 RNN 计算全部函数是可能的。

RNN 能够经过激活和权重(由无限精度的有理数表示)来模拟无限堆栈。 

2. 一个具体的网络结构 - 指定特定的激活函数

再次强调,循环神经网络的结构和具体的激活函数和损失函数是不存在强关联的,网络能够选择任何激活函数

为了可以更好地公式化地描述上图网络结构,咱们指定特定的模型参数。

激活函数:双曲正切激活函数;
输出形式:假定输出是离散的,如用于预测词或字符的RNN。表示离散变量的常规方式是把输出 o 做为每一个离散变量可能值的非标准化对数几率;
损失函数。应用 softmax 函数后续处理后,得到标准化后几率的输出向量 yˆ;

RNN从特定的初始状态 h(0) 开始前向传播。从 t = 1 到 t = τ 的每一个时间步,咱们应用如下更新方程:

这个循环网络将一个输入序列映射到相同长度的输出序列。

与 x 序列配对的 y 的总损失就是全部时间步的损失之和。例如,L(t) 为给定的 x(1),...,x(t) 后y(t) 的负对数似然,则

其中,须要读取模型输出向量 yˆ(t) 中对应于 y(t) 的项。

0x2:导师驱动过程循环网络 - 时间步之间存在”目标值单元“和”隐藏神经元”链接的循环链接

1. 逻辑流程图 - 将输入序列映射到等长的输出序列

导师驱动过程循环神经网络,它仅在一个时间步的目标单元值下一个时间步的隐藏单元间存在循环链接。逻辑流程以下:

从本质上理解,这种导师驱动的循环神经网络,就是将多个单神经元感知机按照序列的方式串联起来,相邻神经元感知机之间存在2阶的依赖关系。

2. 导师驱动循环网络的优缺点

1)缺点

由于缺少隐藏到隐藏的循环链接,因此它不能模拟通用图灵机。本质缘由在于,隐藏神经元中保存和传递的是高阶维度特征,隐藏神经元之间循环链接使得这种高阶维度特征得以传播和记忆。

而目标值单元和隐藏神经元相连的网络结构,它要求目标值单元捕捉用于预测将来的关于过去的全部信息。

可是由于目标值单元(输出单元)明确地训练成匹配训练集的目标,它们不太能捕获关于过去输入历史的必要信息,除非用户知道如何描述系统的所有状态,并将它做为训练目标的一部分。

2)优势

反过来讲,消除隐藏到隐藏循环的优势在于,任何基于比较时刻 t 的预测和时刻 t 的训练目标的损失函数中的全部时间步都解耦了。所以训练能够并行化,即在各时刻 t 分别计算梯度。由于训练集提供输出的理想值,因此没有必要先计算前一时刻的输出。

3. 导师驱动过程训练(teacher forcing)

由输出反馈到模型而产生循环链接的模型可用导师驱动过程(teacher forcing) 进行训练。
训练模型时,导师驱动过程在时刻 t + 1 接收真实值 y(t) 做为输入。咱们能够经过检查两个时间步的序列得知这一点:

在这个例子中,同时给定迄今为止的 x 序列和来自训练集的前一 y 值,咱们可 以看到在时刻 t = 2 时,模型被训练为最大化 y(2) 的条件几率。

所以最大似然在训练时指定正确反馈,而不是将本身的输出反馈到模型。

咱们使用导师驱动过程的最初动机是为了在缺少隐藏神经元到隐藏神经元链接的模型中避免经过时间反向传播

0x3:时间步之间存在“隐藏神经元”循环链接,且网络只有惟一的单向量输出

1. 逻辑流程图 - 将输入序列映射为固定大小的向量

关于时间展开的循环神经网络,在序列结束时具备单个输出。

这样的网络能够用于归纳序列产生用于进一步处理的固定大小的表示。在结束处可能存在目标(如此处所示),或者经过更下游模块的反向传播来得到输出 o(t) 上的梯度。

0x4:基于上下文的RNN建模 - 隐藏神经元之间存在循环链接,且”目标值单元“和”隐藏神经元“之间存在链接

通常状况下,RNN 容许将图模型的观点扩展到不只表明 y 变量的联合分布也能表示给定 x 后 y 条件分布。

须要重点理解的一点是,输入序列 x 的方式的不一样,会形成RNN的效果和性能的很大差异。某种程度上甚至能够说,RNN网络中结构的一个调整,可能就是一个彻底不一样的新算法了,这也是深度神经网络强大而复杂的一面了。

1. 在每一个时间步将完整的 x序列 输入网络中 - 输入序列 x 和输出序列 y 不必定要等长 - 将固定大小的向量映射成一个序列

网络结构以下图所示:

输入 x 和每一个隐藏单元向量 h(t) 之间 的相互做用是经过新引入的权重矩阵 R 参数化的。乘积 x⊤R 在每一个时间步做为隐藏单元的一个额外输入。

咱们能够认为 x 的选择(肯定 x⊤R 的值),是有效地用于每一个隐藏单元的一个新偏置参数。权重与输入保持独立。

将固定长度的向量 x 映射到序列 Y 上每一个时间步的 RNN上。这类 RNN 适用于不少任务如图像标注, 其中单个图像做为模型的输入,而后产生描述图像的词序列。观察到的输出序列的每一个元素 y(t) 同时用做输入(对于当前时间步)和训练期间的目标(对于前一时间步)

笔者思考:这种结构能用于图注任务的原理很是的直观,图注的注解序列的每个词都应该和整张图片有关,因此须要在每一个时间步都输入完整的 x 序列,同时,图注注解序列的单词之间也存在依赖推导关系,由于 y(t) 须要传入下一个时间步

2. 将 x序列 依次输每一个时间步中 - 输入序列 x 和输出序列 y 等长 - 将输入序列映射为等长的输出序列

RNN 能够接收向量序列 x(t) 做为输入,而不是仅接收单个向量 x 做为输入。

接受向量序列的RNN的条件几率分布公式为:

结构图以下:
将可变长度的 x 值序列映射到相同长度的 y 值序列上分布的条件循环神经网络。此 RNN 包含从前一个输出到当前状态的链接。这些链接容许此RNN对给定 x 的序列后 相同长度的 y 序列上的任意分布建模

0x5:双向RNN

传统的前馈RNN网络都有一个 ‘‘因果’’ 结构,意味着在时刻 t 的状态只能从过去的序列x(1),...,x(t−1) 以及当前的输入x(t) 捕获信息。

然而,在许多应用中,咱们要输出的 y(t) 的预测可能依赖于整个输入序列。

例如,在语音识别中,因为协同发音,当前声音做为音素的正确解释可能取决于将来几个音素,甚至潜在的可能取决于将来的几个词,由于词与附近的词之间的存在语义依赖,若是当前的词有两种声学上合理的解释,咱们可能要在更远的将来(和过去)寻找信息区分它们。这在手写识别和许多其余序列到序列学习的任务中也是如此。

双向循环神经网络(或双向 RNN)为知足这种须要而被发明。他们在须要双向信息的应用中很是成功,如手写识别,,语音识别以及生物信息学。
顾名思义,双向RNN结合时间上从序列起点开始移动的RNN和另外一个时间上从序列末尾开始移动的RNN。下图展现了典型的双向 RNN

1. 逻辑流程图 - 将输入序列映射到等长的输出序列

其中 h(t) 表明经过时间向前移动的子 RNN 的状态,g(t) 表明经过时间向后移动的子 RNN 的状态。所以在每一个点 t,输出单元 o(t) 能够受益于输入 h(t) 中关于过去的相关概要以及输入 g(t) 中关于将来的相关概要

这容许输出单元 o(t) 可以计算同时依赖于过去和将来且对时刻 t 的输入值最敏感的表示,而没必要指定 t 周围固定大小的窗口。

0x6:基于编码 - 解码的序列到序列结构

这一小节,咱们将讨论RNN如何将一个输入序列映射到不等长的输出序列。这在许多场景中都有应用,如语音识别、机器翻译或问答,其中训练集的输入和输出序列的长度一般不相同。

咱们常常将RNN的输入称为“上下文”。咱们但愿产生此上下文的表示C。这个上下文C多是一个归纳输入序列 X = (x(1) , . . . , x(nx ) ) 的向量或者向量序列,即神经网络的隐层高维度向量。

实际上,输入长度和输出长度不一致的神经网络并不罕见,DNN和CNN中这种状况都很是常见(例如将图像输入获得手写数字输出),实现这一能力的核心思想就是增长 1 个及以上的隐层,对于RNN也是同样的,隐层起到信息压缩和解压缩的承上启下做用。

这种架构称为编码-解码序列到序列架构。以下图所示:

这个结构其实是由两个RNN结构拼接组成的。

(1) 编码器(encoder)或读取器(reader)或输入 (input) RNN 处理输入序列。编码器输出上下文C(一般是最终隐藏状态的简单函数),C表示输入序列的语义概要;

(2) 解码器(decoder)或写入器 (writer)或输出 (output) RNN 则以固定长度的向量(即上下文C)为条件产生输出序列 Y = (y(1),...,y(ny))。

在序列到序列的架构中,两个 RNN 共同训练以最大化 logP(y(1),...,y(ny) | x(1),...,x(nx))(训练集中全部 x 和 y 对的损失)。

编码器 RNN 的最后一个状态 hnx 一般被看成输入的表示 C 并做为解码器 RNN 的输入。 

0x7:LSTM

LSTM逻辑图以下所示:

LSTM 循环网络除了外部的 RNN 循环外,还具备内部的 “LSTM 细胞’’ 循环(自环),所以 LSTM 不是简单地向输入和循环单元的仿射变换以后施加一个逐元素的非线性。

与普通的循环网络相似,每一个单元有相同的输入和输出,但也有更多的参数和控制信息流动的门控单元系统

最重要的组成部分是状态单元 s(t),与以前讨论的渗漏单元有相似的线性自环。然而,此处自环的权重(或相关联的时间常数)由遗忘门 (forget gate) f(t) 控制。而渗漏单元须要设计者实现手工决定。一个是数据驱动,一个是经验驱动。

遗忘门函数 f(t) 由 sigmoid 单元将权重设置为 0 和 1 之间的值:

其中 x(t) 是当前输入向量,ht 是当前隐藏层向量,ht 包含全部 LSTM 细胞的输出。 bf , Uf , Wf 分别是偏置、输入权重和遗忘门的循环权重。

LSTM 细胞内部状态以以下方式更新:

其中 b, U, W 分别是 LSTM 细胞中的偏置、输入权重和遗忘门的循环权重。外部输入门 (external input gate) 单元 g(t) 以相似遗忘门(使用sigmoid得到一个 0 和 1 之
间的值)的方式更新,但有自身的参数:

LSTM 细胞的输出 h(t) 也能够由输出门 (output gate) q(t) 关闭(使用sigmoid单元做为门控):

其中 bo, Uo, Wo 分别是偏置、输入权重和遗忘门的循环权重。

LSTM 网络比简单的循环架构更易于学习长期依赖

笔者思考:LSTM从数学公式上并无什么特别的地方,这是相比于原始的RNN公式加入了一些额外的函数,使得联合优化过程更复杂了,固然也带来的额外的好处。LSTM的核心思想就是在隐状态的循环传递中插入了一个“门控函数”,该门控函数具有“放行”和“阻断”这两种能力,而具体是否要放行以及放行多少由输入和输出进行BP联合训练。
进一步扩展,咱们甚至能够将门控函数改成一个“信号放大函数”,使其成为一个具有新能力的RNN网络,全部的功能背后都是数学公式以及该公式具有的线性和非线性能力

0x8:GRU(门控循环单元)

GRU 与 LSTM 的主要区别是,单个门控单元同时控制遗忘因子和更新状态单元的决定。更新公式以下:

其中 u 表明 ”更新门”,r 表示 “复位门“。它们的值定义以下:

复位和更新门能独立地 ‘‘忽略’’ 状态向量的一部分。

1. 更新门像条件渗漏累积器同样,能够线性门控任意维度,从而选择将它复制(在 sigmoid 的一个极端)或彻底由新的 ‘‘目标状态’’ 值(朝向渗漏累积器的收敛方向)替换并彻底忽略它(在另外一个极端)。
2. 复位门控制当前状态中哪些部分用于计算下一个目标状态,在过去状态和将来状态之间引入了附加的非线性效应。

围绕这一主题能够设计更多的变种。例如复位门(或遗忘门)的输出能够在多个隐藏单元间共享。或者,全局门的乘积(覆盖一整组的单元,例如整一层)和一个局部门(每单元)可用于结合全局控制和局部控制。但无论怎样,读者朋友须要明白的是,这些本质上都是数学公式上的增长和变化,在核心架构上,GRU和咱们以前讨论的RNN单元公式是相似的。

 

5. 循环神经网络几率图结构

从几率模型的角度来看,咱们能够将深度神经网络的输出解释为一个几率分布,而且咱们一般使用与分布相关联的交叉熵来定义损失。

须要注意的是,是否将上一时间步的某种形式输出做为当前时间步的输入(便是否存在序列依赖),以及取多少步的历史时间步输出做为当前时间步的输入,会对网络模型的性能和效果形成很是大的变化,咱们这个小节来尝试讨论下这个话题。

0x1:序列时间步之间输入输出独立

将整个序列 y 的联合分布分解为一系列单步的几率预测是捕获关于整个序列完整联合分布的一种方法。

当咱们不把过去时间步的 y 值反馈给下一步做为预测的条件时,那么该有向图模型模型不包含任何从过去 y(i) 到当前 y(t) 的边。在这种状况下,输出 y 与给定的 x 序列是条件独立的。

朴素贝叶斯NB算法中的朴素贝叶斯假设本质上就属于这种状况。

0x2:序列时间步之间有限步(阶)输入输出依赖

许多几率图模型的目标是省略不存在强相互做用的边以实现统计和计算的效率

例如经典的Markov假设, 即图模型应该只包含从 {y(t−k), . . . , y(t−1)} 到 y(t) 的边(k阶马尔科夫),而不是包含整个过去历史的边。

然而,在一些状况下,咱们认为整个过去的输入会对序列的下一个元素有必定影响。当咱们认为 y(t) 的分布可能取决于遥远过去 (在某种程度) 的 y(i) 的值,且 没法经过 y(t−1) 捕获 y(i) 的影响时,RNN 将会颇有用。

笔者思考:在实际项目中,咱们对序列依赖的长度的需求是须要仔细思考的,并非全部状况下都须要针对超长序列提取模式记忆。例如在webshell检测场景中,咱们每每更关注”短程语法句式模式“,由于恶意代码的主题功能每每在5步以内就会完成,咱们须要捕获的也就是这些短程的序列模式。

0x3:历史时间步长序列输入输出依赖

RNN 被训练为可以根据以前的历史输入估计下一个序列元素 y(t) 的条件分布,条件几率公式以下:

能够看到,RNN遵循的是一种长序列依赖假设。

举一个简单的例子,让咱们考虑对标量随机变量序列 Y = {y(1),...,y(τ)} 建模的 RNN,也没有额外的输入 x(实际大多数状况是存在输入 x 的)。在时间步 t 的输入仅仅是时间步 t − 1 的输出:

这个例子中的 RNN 定义了关于 y 变量的有向图模型。咱们使用链式法则参数化这些观察值的联合分布:

其中当 t = 1 时竖杠右侧显然为空。所以,根据这样一个模型,一组值 {y(1),...,y(τ)} 的负对数似然为:

,其中,

”该RNN中每一时间步都参考了历史上全部历史时间步的输出“,这句话有点抽象很差理解,为了更好地讨论这句话的概念,咱们将计算图展开为彻底图:

咱们将RNN视为定义一个结构为彻底图的图模型,且可以表示任何一对 y 值之间的直接联系。即每个时间步之间都存在某种联系。这预示着 RNN 能对观测的联合分布提供很是有效的参数,以下图:

序列 y(1), y(2), . . . , y(t), . . . 的全链接图模型。给定先前的值,每一个过去的观察值 y(i) 可 以影响一些 y(t)(t > i) 的条件分布。

当序列中每一个元素的输入和参数的数目愈来愈多,根据此图直接参数化图模型多是很是低效的。RNN 能够经过高效的参数化(参数共享机制)得到相同的全链接。

可是全链接带来一个严重的问题,参数膨胀

假设咱们用表格表示法来表示离散值上任意的联合分布,即对每一个值可能的赋值分配一个单独条目的数组,该条目表示发生该赋值的几率。若是 y 能够取 k 个不一样的 值,表格表示法将有 O(kτ ) 个参数。

可是 RNN 因为使用参数共享机制,RNN 的参数数目为 O(1) 且是序列长度的函数。咱们能够调节 RNN 的参数数量来控制模型容量,但不用被迫与序列长度成比例。

下式展现了所述 RNN 经过循环应用相同的函数,以及在每一个时间步的相同参数 θ,有效地参数化的变量之间的长期联系:

同时在 RNN 图模型中引入状态变量,尽管它是输入的肯定性函数,但它有助于咱们得到很是高效的参数化。

序列中的每一个阶段(对于 h(t) 和 y(t) )使用相同的结构(每一个节点具备相同数量的输入),而且能够与其余阶段共享相同的参数。

在图模型中结合 h(t) 节点能够用做过去和将来之间的中间量,从而将它们解耦。遥远过去的变量 y(i) 能够经过其对 h 的影响来影响变量 y(t)。

 

6. 循环神经网络的梯度计算

循环神经网络中,关于各个参数计算这个损失函数的梯度是计算成本很高的操做。

经过将RNN的计算图展开后能够清楚地看到,梯度计算涉及执行一次前向传播,接着是由右到左的反向传播。运行时间是 O(τ),而且不能经过并行化来下降,由于前向传播图是固有循序的,每一个时间步只能一前一后地计算。前向传播中的各个状态必须保存,直到它们反向传播中被再次使用,所以内存代价也是 O(τ)。

应用于展开图且代价为 O(τ) 的反向传播算法称为经过时间反向传播(back-propagation through time, BPTT),隐藏单元之间存在循环的网络很是强大但训练代价也很大。

0x1:举例说明BPTT计算过程

以文章以前讨论的例子为例计算梯度:

计算图的节点包括参数 U, V, W, b 和 c,以及以 t 为索引的节点序列 x(t), h(t), o(t) 和 L(t)。

对于每个节点 N,咱们须要基于 N 后面的节点的梯度,递归地计算梯度 ∇NL。咱们从紧接着最终损失的节点开始往回递归:

在这个导数中,咱们假设输出 o(t) 做为 softmax 函数的参数,咱们能够从 softmax函数能够得到关于输出几率的向量 yˆ。咱们也假设损失是迄今为止给定了输入后的真实目标 y(t) 的负对数似然。对于全部 i, t,关于时间步 t 输出的梯度 ∇o(t) L 以下:

咱们从序列的末尾开始,反向进行计算。在最后的时间步 τ, h(τ) 只有 o(τ) 做为后续节点,所以这个梯度很简单:

而后,咱们能够从时刻 t = τ − 1 到 t = 1 反向迭代,经过时间反向传播梯度,注意h(t)(t < τ) 同时具备 o(t) 和 h(t+1) 两个后续节点。所以,它的梯度由下式计算:

其中 diag 1−(h(t+1))2 表示包含元素 1−(h(t+1))2 的对角矩阵。这是关于时刻 t+1 与隐藏单元 i 关联的双曲正切的Jacobian。

一旦得到了计算图内部节点的梯度,咱们就能够获得关于参数节点的梯度。

由于参数在许多时间步共享,咱们必须在表示这些变量的微积分操做时谨慎对待。咱们但愿使用 bprop 方法计算计算图中单一边对梯度的贡献。然而微积分中的 ∇Wf 算子,计算 W 对于 f 的贡献时将计算图中的全部边都考虑进去了。为了消除这种歧义,咱们定义只在 t 时刻使用的虚拟变量 W(t) 做为 W 的副本。而后,咱们可使用 ∇W(t) 表示权重在时间步 t 对梯度的贡献。

使用这个表示,计算节点内部参数的梯度能够由下式给出:

由于计算图中定义的损失的任何参数都不是训练数据 x(t) 的父节点,因此咱们不须要计算关于它的梯度。

 

7. 循环神经网络的具体应用

0x1:基于RNN+LSTM的模型自动编写古诗

1. 语料数据

一共四万多首古诗,每行一首诗。

2. 样本预处理

这里咱们采用one-hot的形式,基于当前的诗句文件统计出一个字典,这样诗句中的每一个字都能用向量来表示。固然,也能够采用emberding方式进行词向量嵌入。

# *-* coding:utf-8 *-*


puncs = [']', '[', '', '', '{', '}', '', '', '']


def preprocess_file(Config):
    # 语料文本内容
    files_content = ''
    with open(Config.poetry_file, 'r', encoding='utf-8') as f:
        for line in f:
            # 每行的末尾加上"]"符号表明一首诗结束
            for char in puncs:
                line = line.replace(char, "")
            files_content += line.strip() + "]"  

    # 统计整个预料的词频
    words = sorted(list(files_content))
    words.remove(']')
    counted_words = {}
    for word in words:
        if word in counted_words:
            counted_words[word] += 1
        else:
            counted_words[word] = 1

    # 去掉低频的字
    erase = []
    for key in counted_words:
        if counted_words[key] <= 2:
            erase.append(key)
    for key in erase:
        del counted_words[key]
    del counted_words[']']
    wordPairs = sorted(counted_words.items(), key=lambda x: -x[1])

    words, _ = zip(*wordPairs)
    # word到id的映射
    word2num = dict((c, i + 1) for i, c in enumerate(words))
    num2word = dict((i, c) for i, c in enumerate(words))
    word2numF = lambda x: word2num.get(x, 0)
    return word2numF, num2word, words, files_content

3. 生成序列数据

RNN是序列到序列的有监督模型,所以咱们须要定义每一个时间步的输入x,以及每一个时间步的输出目标值y。

咱们给模型学习的方法是,给定前六个字,生成第七个字,因此在后面生成训练数据的时候,会以6的跨度,1的步长截取文字,生成语料。

好比“我要吃香蕉”,如今以3的跨度生成训练数据就是("我要吃", “香”),("要吃香", "蕉")。跨度为6的句子中,先后每一个字都是有关联的。若是出现了]符号,说明]符号以前的语句和以后的语句是两首诗里面的内容,两首诗之间是没有关联关系的,因此咱们后面会舍弃掉包含]符号的训练数据。

def data_generator(self):
        '''生成器生成数据'''
        i = 0
        while 1:
            x = self.files_content[i: i + self.config.max_len]  # max_len跨度做为x
            y = self.files_content[i + self.config.max_len]     # max_len+1 的那个跟随词做为y

            puncs = [']', '[', '', '', '{', '}', '', '', '', ':']
            if len([j for j in puncs if j in x]) != 0:  # x中出现诗句中止符,丢弃该x
                i += 1
                continue
            if len([j for j in puncs if j in y]) != 0:  # y恰好是诗句中止符,丢弃该y
                i += 1
                continue

            y_vec = np.zeros(
                shape=(1, len(self.words)),
                dtype=np.bool
            )
            y_vec[0, self.word2numF(y)] = 1.0  # y是one-hot编码,对应出现的那个词为true,其余为false

            x_vec = np.zeros(
                shape=(1, self.config.max_len),
                dtype=np.int32
            )

            for t, char in enumerate(x):
                x_vec[0, t] = self.word2numF(char)
            yield x_vec, y_vec
            i += 1 

x表示输入,y表示输出,输入就是前六个字,输出即为第七个字。再将文字转换成向量的形式。

4. 构建模型

def build_model(self):
        '''创建模型'''

        # 输入的dimension
        input_tensor = Input(shape=(self.config.max_len,))
        embedd = Embedding(len(self.num2word) + 2, 300, input_length=self.config.max_len)(input_tensor)
        lstm = Bidirectional(GRU(128, return_sequences=True))(embedd)
        # dropout = Dropout(0.6)(lstm)
        # lstm = LSTM(256)(dropout)
        # dropout = Dropout(0.6)(lstm)
        flatten = Flatten()(lstm)
        dense = Dense(len(self.words), activation='softmax')(flatten)
        self.model = Model(inputs=input_tensor, outputs=dense)
        optimizer = Adam(lr=self.config.learning_rate)
        self.model.compile(loss='categorical_crossentropy', optimizer=optimizer, metrics=['accuracy'])

双向lstm以后用flattern和DNN进行压平和整合,最后softmax获得单个向量的输出。

5. 训练模型

 def train(self):
        '''训练模型'''
        number_of_epoch = len(self.words) // self.config.batch_size
 
        if not self.model:
            self.build_model()
 
        self.model.fit_generator(
            generator=self.data_generator(),
            verbose=True,
            steps_per_epoch=self.config.batch_size,
            epochs=number_of_epoch,
            callbacks=[
                keras.callbacks.ModelCheckpoint(self.config.weight_file, save_weights_only=False),
                LambdaCallback(on_epoch_end=self.generate_sample_result)
            ]
        )

6. 每轮epoch训练结果,进行一次成果展现

 def generate_sample_result(self, epoch, logs):
        '''训练过程当中,每一个epoch打印出当前的学习状况'''
        # if epoch % 5 != 0:
        #     return
        print("\n==================Epoch {}=====================".format(epoch))
        for diversity in [0.5, 1.0, 1.5]:
            print("------------Diversity {}--------------".format(diversity))
            start_index = random.randint(0, len(self.files_content) - self.config.max_len - 1)
            generated = ''
            sentence = self.files_content[start_index: start_index + self.config.max_len]   # 随机截取一段6词序列做为待预测x
            generated += sentence
            for i in range(20):     # 循环20次,即生成一句20个词的诗句
                x_pred = np.zeros((1, self.config.max_len))
                for t, char in enumerate(sentence[-6:]):
                    x_pred[0, t] = self.word2numF(char)

                preds = self.model.predict(x_pred, verbose=0)[0]    # 获得y预测结果
                print "preds: ", preds
                next_index = self.sample(preds, diversity)          # 从y中选择几率最大的词编码
                next_char = self.num2word[next_index]   # 翻译回可读汉字

                generated += next_char
                sentence = sentence + next_char
            print(sentence)

下图展现了训练初期和训练一段时间以后,RNN的诗句生成效果

训练一段时间后:

Relevant Link:

https://www.ioiogoo.cn/2018/02/01/%E7%94%A8keras%E5%AE%9E%E7%8E%B0rnnlstm%E7%9A%84%E6%A8%A1%E5%9E%8B%E8%87%AA%E5%8A%A8%E7%BC%96%E5%86%99%E5%8F%A4%E8%AF%97/
https://github.com/LittleHann/poetry_generator_Keras

0x2:基于LSTM生成城市名称

RNN具有记忆性,在通过大量训练后能够学习到时序数据的潜在规律,而且可使用这种规律随机生成新的序列。

1. 如何给RNN输入训练样本

RNN能够学习到数据中的时序规律,可是做为模型设计者,咱们须要明确地定义:该样本集中时序规律的形式是什么

例如笔者在项目中遇到的一些典型场景:

1. 你有一段时序向量数据,而且拥有对这个时序向量数据的一个 0/1 label,即二分类问题,这在安全攻防场景中很常见;
2. 你有一个预料库,该语料库中包含了各类句子。你但愿让RNN从中学习到隐藏的”句式、语法模式“。咱们知道,语言对话是由词/句/短语/段落组成的,我么能够采起”滑动窗口“的方式,逐段地将整个句子分红多个【X(可能长度为7), Y(可能长度为1)】的训练样本,经过让RNN学习 X序列 和紧随其后的 Y 字符的序列特征,等效地让RNN学会语料库中的”句式、语法模式“;
3. 同理,基于图像生成标注的道理也是相似的(同2);

2. 数据集

Abbeville
Abbotsford
Abbott
Abbottsburg
Abbottstown
Abbyville
Abell
Abercrombie
Aberdeen
Aberfoil
Abernant
Abernathy
Abeytas
Abie
Abilene
Abingdon
Abington
Abiquiu
Abita Springs
Abo
Aboite
Abraham
Abram
Abrams
Absarokee
Absecon
Academy
Accokeek
Accomac
Accord
Ace
Aceitunas
Acequia
Achille
Achilles
Ackerly
Ackerman
Ackley
Ackworth
Acme
Acomita Lake
Acra
Acree
Acton
Acworth
Acy
Ada
Adair
Adair Village
Adairsville
Adairville
Adams
Adams Center
Adams City
Adamstown
Adamsville
Adario
Addicks
Addie
Addieville
Addington
Addis
Addison
Addy
Addyston
Adel
Adelaide
Adelanto
Adelino
Adell
Adelphi
Adelphia
Aden
Adena
Adgateville
Adin
Adjuntas
Admire
Adna
Adona
Adrian
Advance
Adwolf
Ady
Aetna
Affton
Afton
Agar
Agate
Agate Beach
Agawam
Agency
Agnes
Agness
Agnew
Agnos
Agoura
Agra
Agricola
Agua Dulce

3. 训练代码

from __future__ import absolute_import, division

import os
from six import moves
import ssl

import tflearn
from tflearn.data_utils import *

path = "../data/US_Cities.txt"
maxlen = 20

file_lines = open(path, "r").read()
X, Y, char_idx = string_to_semi_redundant_sequences(file_lines, seq_maxlen=maxlen, redun_step=3)
print "X[0]", X[0]
print "len(X[0])", len(X[0])
print "Y[0]", Y[0]
print "char_idx", char_idx


g = tflearn.input_data(shape=[None, maxlen, len(char_idx)])
g = tflearn.lstm(g, 512, return_seq=True)
g = tflearn.dropout(g, 0.5)
g = tflearn.lstm(g, 512)
g = tflearn.dropout(g, 0.5)
g = tflearn.fully_connected(g, len(char_idx), activation='softmax')
g = tflearn.regression(g, optimizer='adam', loss='categorical_crossentropy',
                       learning_rate=0.001)

m = tflearn.SequenceGenerator(g, dictionary=char_idx,
                              seq_maxlen=maxlen,
                              clip_gradients=5.0,
                              checkpoint_path='model_us_cities')


for i in range(40):
    seed = random_sequence_from_string(file_lines, maxlen)
    m.fit(X, Y, validation_set=0.1, batch_size=128,
          n_epoch=1, run_id='us_cities')
    print("-- TESTING...")
    print("-- Test with temperature of 1.2 --")
    print(m.generate(30, temperature=1.2, seq_seed=seed))
    print("-- Test with temperature of 1.0 --")
    print(m.generate(30, temperature=1.0, seq_seed=seed))
    print("-- Test with temperature of 0.5 --")
    print(m.generate(30, temperature=0.5, seq_seed=seed))

4. 实验结果

0x3:基于LSTM生成JSP WEBSHELL样本

1. 样本集

咱们收集了131个大小在4096bytes内的JSP webshell文件,这批样本做为训练语料库。

须要特别注意的一点是,每一个文件之间理论上应该是一个独立的样本集,最合理的作法是单独从每一个文件中以ngram方式提取序列。

咱们这里为了简单起见,把全部文件concat到一个总体的字符串中,进行向量化,读者朋友在实际项目中要注意这点。

2. webshell词法模式提取原理

采集滑动窗口进行词模式提取,窗口越小,提取到的词模式特定空间就越大,描述能力就越强,相对的,训练难度也越大,举例说明:

<?php
    eval($_POST['op']);
?>
采用step_size = 2的滑动窗口进行词模式提取:
EOF< -> ?
<? -> p
?p -> h
... 
ev -> a
va -> l
al -> (
...
$_ -> P
..
?> -> EOF

3. 生成(预测)过程

RNN是一种sequence to sequence的神经网络,所以咱们须要给模型提供一个种子seed字符,做为启动字符,选择这个字符的原则也很简单,选择对应编程语言开头的第一个字母。

这里咱们简述过程

1. step_1: 输入 START<,prediect后进行softmax获得"?"
2. step_2: 在上一步的基础上,输入"<?",prediect后进行softmax获得"换行"或者"空格"
3. ...
4. 循环到直接网络输出EOF或者达到开发者设定的filesize
5. 最终获得的序列就是一个目标webshell序列

4. 实验代码

from __future__ import absolute_import, division

import os
from six import moves
import ssl

import tflearn
from tflearn.data_utils import *

DataDir = "../data/jsp_hash"
maxlen = 20
shelllen = 4096
step_size = 2

file_lines = ""
rootDir = DataDir
for file in os.listdir(rootDir):
    if file == '.DS_Store':
        continue
    print file
    path = os.path.join(rootDir, file)
    file_content = open(path, "r").read()
    file_lines += file_content

X, Y, char_idx = string_to_semi_redundant_sequences(file_lines, seq_maxlen=maxlen, redun_step=step_size)
print "X[0]", X[0]
print "len(X[0])", len(X[0])
print "Y[0]", Y[0]
print "char_idx", char_idx


g = tflearn.input_data(shape=[None, maxlen, len(char_idx)])
g = tflearn.lstm(g, 512, return_seq=True)
g = tflearn.dropout(g, 0.5)
g = tflearn.lstm(g, 512)
g = tflearn.dropout(g, 0.5)
g = tflearn.fully_connected(g, len(char_idx), activation='softmax')
g = tflearn.regression(g, optimizer='adam', loss='categorical_crossentropy',
                       learning_rate=0.001)

m = tflearn.SequenceGenerator(g, dictionary=char_idx,
                              seq_maxlen=maxlen,
                              clip_gradients=5.0,
                              checkpoint_path='model_us_cities')


for i in range(40):
    seed = random_sequence_from_string(file_lines, maxlen)
    m.fit(X, Y, validation_set=0.2, batch_size=128,
          n_epoch=5, run_id='webshell generate')
    print("-- GENERATING...")
    print("-- Test with temperature of 1.2 --")
    print(m.generate(shelllen, temperature=1.2, seq_seed=seed))
    print("-- Test with temperature of 1.0 --")
    print(m.generate(shelllen, temperature=1.0, seq_seed=seed))
    print("-- Test with temperature of 0.5 --")
    print(m.generate(shelllen, temperature=0.5, seq_seed=seed))

3. 实验结果

0x4:GENERATING IMAGE DESCRIPTIONS

Together with convolutional Neural Networks, RNNs have been used as part of a model to generate descriptions for unlabeled images. It’s quite amazing how well this seems to work. The combined model even aligns the generated words with features found in the images.

相关文章
相关标签/搜索