分类目录归档:人工智能


本章数据集下载

第6章Spark MLlib基础

传统的机器学习算法,由于技术和单机存储的限制,只能在少量数据上使用。一旦数据量过大,往往需要采用数据抽样的方法。但这种抽样很难保证不走样。近些年随着 HDFS 等分布式文件系统出现,存储海量数据已经成为可能。在全量数据上进行机器学习变得可能或必要,但由于MapReduce计算框架,虽然实现分布式计算,但中间结果需要存在到磁盘,这对这计算过程中需要多次迭代的机器学习,因为通常情况下机器学习算法参数学习的过程都是迭代计算的,不很理想。
Spark的出现,正好弥补了MapReduce的不足,它立足于内存计算,所以特别适合机器学习的迭代式计算。同时Spark提供了一个基于海量数据的分布式运算的机器学习库,同时提供了很多特征选取、特征转换等内嵌函数,大大降低了大家学习和使用Spark的门槛,对很多开发者只需对 Spark 有一定基础、了解机器学习算法的基本原理、以及相关参数的含义和作用,一般都可以通过都可以比较顺利地使用Spark进行基于大数据的机器学习。
Spark在机器学习方面有很多优势,本章主要Spark与机器学习相关的内容。
 Spark MLlib简介
 Spark MLlib架构
 常用的几种数据类型
 基础统计
 RDD、DataFrame及Dataset间的异同
 Spark MLlib常用算法

6.1Spark MLlib简介

MLlib是MLBase一部分,其中MLBase分为四部分:MLlib、MLI、ML Optimizer和MLRuntime。它们的结构如下图:

图6-2 MLBase四部分关系

6.2Spark MLlib架构

6.3数据类型

Spark MLlib的数据类型主要分为四种,下面将分别介绍。
1. 本地向量(Local vector)
其创建方式主要有以下几种:(以下使用Scala语言)

2. 标记点(Labeled point)
标记点是由一个本地向量(密集或稀疏)和一个标签(整数或浮点)组成,这个值的具体内容可以由用户指定。

从文件中直接获取标记点:

3. 本地矩阵(Local matrix)
由行索引、列索引、类型值组成,存放在单机中。

4. 分布式矩阵(Distributed matrix)

6.4 基础统计

6.4.1摘要统计

示例代码如下:

6.4.2相关性

目前Spark支持两种相关性(correlations)系数:皮尔森相关系数(pearson)和斯皮尔曼等级相关系数(spearman)。下面通过示例说明相关系统的如何计算。

6.4.3假设检验

假设检验(Hypothesis testing),Spark MLlib目前支持皮尔森卡方检测(Pearson’s chi-squared tests),包括适配度检测和独立性检测。适配度检测要求输入为Vector, 独立性检验要求输入是Matrix。
代码示例:

6.4.4随机数据生成

代码示例:

6.5 RDD、Dataframe和Dataset

目前,spark.mllib包中基于RDD的APIs已进入维护模式,以后将以spark.ml包中的基于DataFrame的API为主。

6.5.1RDD

RDD是Spark建立之初的核心API。它是一种有容错机制的特殊集合, RDD是不可变分布式弹性数据集,在Spark集群中可跨节点分区,以函数式编程操作集合的方式,进行各种并行操作,提供分布式low-level API来操作,包括transformation和action等。

6.5.2Dataset/DataFrame

DataFrame与RDD相同之处,都是不可变分布式弹性数据集。不同之处在于,DataFrame多了数据的结构信息,即schema,类似于传统数据库中的表。

6.5.3相互转换

RDD、DataFrame和Dataset间可以互相转换。

6.6小结

本章主要介绍了Spark MLlib的一些内容,包括MLlib的生态、架构等内容,同时介绍了Spark MLlib算法底层依赖的基础内容,如数据类型、基础统计等,最后简单介绍了RDD、DataFrame与Dataset间的异同等。后续章节我们将通过一些实例,说明如何把前几章介绍的一些方法应用到具体实例中。


本章数据集下载

第5章 模型选择和优化

本章主要介绍如何使用Spark ML提供的方法及自定义函数等方法来对模型进行调优。我们可以通过Spark ML内建的交叉验证、训练验证拆方法、网格参数等方法进行模型调优,当然也可以自定义函数进行模型优化。
本章主要内容包括:
 模型选择
 交叉验证
 训练验证拆分法
 自定义函数调优

5.1 模型选择

调优可以是对单个的Estimator,比如LogisticRegression,或者对包含多个算法、特征化和其他步骤的整个Pipline。用户可以一次性对整个Pipline进行调优,而不必对Pipline中的每一个元素进行单独的调优。
MLlib支持使用像交叉验证(CrossValidator)和训练验证拆分法(TrainValidationSplit)这样的工具进行模型选择(Model selection)。这些工具需要以下的组件:
 Estimator:用户调优的算法或Pipline
 ParamMap集合:提供参数选择,有时也叫作用户查找的“参数网格”
 Evaluator:衡量模型在测试数据上的拟合程度
在上层,这些模型选择工具的工作方式如下:
 将输入数据切分成训练数据集和测试数据集
 对于每一个(训练数据,测试数据)对,通过ParamMap集合进行迭代:对于每个ParamMap,使用它提供的参数对Estimator进行拟合,给出拟合模型,然后使用Evaluator来评估模型的性能。
 选择表现最好的参数集合生成的模型。

5.2交叉验证

下例使CrossValidator从整个网格的参数中选择合适的参数,而从自动选择最优模型。
在整个参数网格中进行交叉验证是比较耗时的。例如,在下面的例子中,参数网格有3个hashingTF.numFeatures值和2个lr.regParam值,CrossValidator使用2折切分数据。最终将有(3 * 2) * 2 = 12个不同的模型将被训练。在真实场景中,很可能使用更多的参数和进行更多折切分(k=3和k=10都很常见)。使用CrossValidator的代价可能会异常的高,当大数据集比较大时,需要慎重选择。不过采用交叉验证法,对比手动调优,还是有较大优势。
下面通过示例说明如何使用CrossValidator从整个网格的参数中选择合适的参数。
导入必要的包:

配置一个流水线,该流水线包含3个stages: tokenizer, hashingTF, and lr。

使用ParamGridBuilder构造一个parameter grid

流水线,嵌入到CrossValidator实例中,这样流水线的任务都可使用网格参数。
CrossValidator一般需要一个Estimator, 参数集及一个评估器Evaluator。
BinaryClassificationEvaluator缺省的评估指标为AUC(areaUnderROC)。

通过交叉验证模型, 并获取最优参数集,并测试模型

查看最佳模型中各参数值

5.3训练验证拆分法

像CrossValidator一样,TrainValidationSplit最终适合使用最好的ParamMap
和整个数据集的Estimator。

5.4自定义模型选择

the best model was trained with rank = 20 and lambda = 0.1, and numIter = 10, and its RMSE on the test set is 1.0059139723438042.

5.5小结

本章主要介绍了几种模型选择或调优的方法,我们可以从训练的数据集入手,可以从模型参数入手,当然也可把两者结合起来。实际上模型的优化还有很多其他方法,如使用不同的算法、集成算法等等。下一章我们将介绍Spark MLlib一些基础知识,包括Spark MLlib架构、原理、算法及算法依赖的一些库、向量和矩阵等相关内容。

第2章 概率与信息论

本章讨论概率论和信息论,概率论是用于表示不确定性陈述的数学框架,即它是对事物不确定性的度量。
在人工智能领域,我们主要以两种方式来使用概率论。首先,概率法则告诉我们AI系统应该如何推理,所以我们设计一些算法来计算或者近似由概率论导出的表达式。其次,我们可以用概率和统计从理论上分析我们提出的AI系统的行为。
计算机科学的许多分支处理的对象都是完全确定的实体,但机器学习却大量使用概率论。实际上如果你了解机器学习的工作原理你就会觉得这个很正常。因为机器学习大部分时候处理的都是不确定量或随机量。
概率论和信息论是众多科学学科和工程学科的基础,也是机器学习、深度学习的重要基础。
如果你对概率论和信息论很熟悉了,可以跳过这章。如果你觉得这些内容还不够,还想进一步了解相关知识,可以参考相关专业教材。

2.1为何要概率、信息论

机器学习、深度学习需要借助概率、信息论?
要回答这个问题,我觉得至少应该了解以下两个问题:
(1)概率、信息论的主要任务;
(2)机器学习、深度学习与概率、信息论有哪些因缘。
概率研究对象不是预习知道或确定的事情,而是预习不确定或随机事件。研究这些不确定或随机事件背后规律或规则。或许有人会说,这些不确定或随机事件有啥好研究?他们本来就不确定或随机的,飘忽不定、不可捉摸。表面上看起来确实如此,有句话说得好:偶然中有必然,必然中有偶然。就拿我们比较熟悉微积分来说吧,如果单看有限的几步,很多问题都杂乱无章,还难处理,但是一旦加上一个无穷大(∞)这个“照妖镜”,其背后规律立显、原来难处理的也好处理了。概率研究的对象也类似。如大数定律、各种分布等等。
信息论主要研究对一个信号包含信息的多少进行量化。它的基本思想是一个不太可能的事件居然发生了,其提供的信息量要比一个非常可能发生的事件更多。这个看起来,也好像与我们的直觉相矛盾。
说起机器学习、深度学习与概率、信息论的因缘可就多了:
(1)被建模系统内在的随机性。例如一个假想的纸牌游戏,在这个游戏中我们假设纸牌被真正混洗成了随机顺序。
(2不完全观测。即使是确定的系统,当我们不能观测到所有驱动系统行为的所有变量或因素时,该系统也会呈现随机性。
(3)不完全建模。例如,假设我们制作了一个机器人,它可以准确地观察周围每一个对象的位置。 在对这些对象将来的位置进行预测时,如果机器人采用的是离散化的空间,那么离散化的方法将使得机器人无法确定对象们的精确位置:因为每个对象都可能处于它被观测到的离散单元的任何一个角落。也就是说,当不完全建模时,我们不能明确的确定结果,这个时候的不确定,就需要借助概率来处理。
由此看来,概率、信息论很重要,机器学习、深度学习确实很需要它们。后续我们可以看到很多实例,见证概率、信息论在机器学习、深度学习中是如何发挥它们作用的。

2.2样本空间与随机变量

样本空间
样本空间是一个实验或随机试验所有可能结果的集合,而随机试验中的每个可能结果称为样本点。例如,如果抛掷一枚硬币,那么样本空间就是集合{正面,反面}。如果投掷一个骰子,那么样本空间就是 {1,2,3,4,5,6}。
随机变量
随机变量,顾名思义,就是“其值随机而定”的变量,一个随机试验有许多可能结果,到底出现哪个预先是不知道的,其结果只有等到试验完成后,才能确定。如掷骰子,掷出的点数X是一个随机变量,它可以取1,2,3,4,5,6中的任何一个,到底是哪一个,要等掷了骰子以后才知道。因此,随机变量又是试验结果的函数,它为每一个试验结果分配一个值。比如,在一次扔硬币事件中,如果把获得的背面的次数作为随机变量X,则X可以取两个值,分别是0和1。如果随机变量X的取值是有限的或者是可数无穷尽的值,如:

2.3概率分布

概率分布用来描述随机变量(含随机向量)在每一个可能取到的状态的可能性大小。概率分布的有不同方式,这取决于随机变量是离散的还是连续的。
对于随机变量X,其概率分布通常记为P(X=x),或X ~P(x),表示X服从概率分布P(x)。
概率分布描述了取单点值的可能性或概率,但在实际应用中,我们并不关心取某一值的概率,特别是对连续型随机变量,它在某点的概率都是0,这个后续章节将介绍。因此,我们通常比较关心随机变量落在某一区间的概率,为此,引入分布函数的概念。

2.3.1 离散型随机变量



例如,设X的概率分布由例1给出,则
F(2)=P(X≤2)=P(X=0)+P(X=l)=0.04+0.32=0.36
常见的离散随机变量的分布有:
(1)两点分布
若随机变量X只可能取0和1两个值,且它的分布列为P(X=1)=p,P(X = 0) = l − P 其中(0 < P < 1),则称X服从参数为p的两点分布,记作X~B(1, p)。其分布函数为

(2)二项分布
二项分布是最重要的离散概率分布之一,由瑞士数学家雅各布•伯努利(Jokab Bernoulli)所发展,一般用二项分布来计算概率的前提是,每次抽出样品后再放回去,并且只能有两种试验结果,比如黑球或红球,正品或次品等。二项分布指出,随机一次试验出现的概率如果为p,那么在n次试验中出现k次的概率为:

假设随机变量X满足二项分布,且知道n,p,k等参数,我们如何求出各种情况的概率值呢?方法比较多,这里介绍一种非常简单的方法,利用Python的scipy库可以非常简单,直接利用这个统计接口stats即可,具体如下:

(3)泊松(Poisson)分布
若随机变量X所有可能取值为0,1,2,…,它取各个值的概率为:

这里介绍了离散型随机变量的分布情况,如果X是连续型随机变量,其分布函数通常通过密度函数来描述,具体请看下一节

2.3.2 连续型随机变量

与离散型随机变量不同,连续型随机变量采用概率密度函数来描述变量的概率分布。如果一个函数f(x)是密度函数,满足以下三个性质,我们就称f(x)为概率密度函数。

图2-1 概率密度函数
对连续型随机变量在任意一点的概率处处为0。
假设有任意小的实数∆x,由于{X=x}⊂{x-∆x<X≤x},由式(2.1)分布函数的定义可得:

这个连续分布被称之为正态分布,或者高斯分布。其密度函数的曲线呈对称钟形,因此又被称之为钟形曲线,其中μ是平均值,σ是标准差(何为平均值、标准差后续我们会介绍)。正态分布是一种理想分布。
正态分布如何用Python实现呢?同样,我们可以借助其scipy库中stats来实现,非常方便。

正态分布的取值可以从负无穷到正无穷。这里我们为便于可视化,只取把X数据定义在[-6,6]之间,用stats.norm.pdf得到正态分布的概率密度函数。另外从图形可以看出,上面两图的均值u都是0,只是标准差(σ)不同,这就导致图像的离散程度不同,标准差大的更分散,个中原因,我们在介绍随机变量的数字特征时将进一步说明。

2.4边缘概率

对于多维随机变量,如二维随机变量(X,Y),假设其联合概率分布为F(x,y),我们经常遇到求其中一个随机变量的概率分布的情况。这种定义在子集上的概率分布称为边缘概率分布。
例如,假设有两个离散的随机变量X,Y,且知道P(X,Y),那么我们可以通过下面求和的方法,得到边缘概率P(X):

边缘概率如何计算呢?这里我们通过一个实例来说明。假设有两个离散型随机变量X,Y,其联合分布概率如下:
表1.1:X与Y的联合分布

如果我们要求P(Y=0)的边缘概率,根据式(2.7)可得:
P(Y=0)=P(X=1,Y=0)+P(X=2,Y=0)=0.05+0.28=0.33

2.5条件概率

上一节我们介绍了边缘概率,它是值多维随机变量一个子集(或分量)上的概率分布。对于含多个随机变量的事件中,经常遇到求某个事件在其它事件发生的概率,例如,在表1.1的分布中,假设我们要求当Y=0的条件下,求X=1的概率?这种概率叫作条件概率。条件概率如何求?我们先看一般情况。
设有两个随机变量X,Y,我们将把X=x,Y=y发生的条件概率记为P(Y=y|X=x),那么这个条件概率可以通过以下公式计算:

其中P(Y=0)是一个边缘概率,其值为:P(X=1,Y=0)+P(X=2,Y=0)=0.05+0.28=0.33
而P(X=1,Y=0)=0.05.故P(X=1|Y=0)=0.05/0.33=5/33

2.6条件概率的链式法则

条件概率的链式法则,又称为乘法法则,把式(2.10)变形,可得到条件概率的乘法法则:
P(X,Y)=P(X)xP(Y|X)                                                                                        (2.11)
根据式(2.11)可以推广到多维随机变量,如:
P(X,Y,Z)=P(Y,Z)xP(X|Y,Z)
而P(Y,Z)=P(Z)xP(Y|Z)
由此可得:P(X,Y,Z)=P(X|Y,Z)x P(Y|Z)xP(Z)                                               (2.12)
更多维的情况,以此类推。

2.7独立性及条件独立性

两个随机变量X,Y,如果它们的概率分布可以表示为两个因子的乘积,并一个因子只含x,另一个因子只含y,那么我们就称这两个随机变量互相独立。这句话可能不好理解,我们换一种方式的来表达。或许更好理解。
如果对 ∀x∈X,y∈Y,P(X=x,Y=y)=P(X=x)P(Y=y) 成立,那么随机变量X,Y互相独立。
在机器学习中,随机变量为互相独立的情况非常普遍,一旦互相独立,联合分布的计算就变得非常简单。
这是不带条件的随机变量的独立性定义,如果两个随机变量带有条件,如P(X,Y|Z),它的独立性如何定义呢?这个与上面的定义类似。具体定义如下:
如果对∀x∈X,y∈Y,z∈Z,P(X=x,Y=y|Z=z)=P(X=x|Z=z)P(Y=y|Z=z) 成立
那么随机变量X,Y在给定随机变量Z时是条件独立的。
为便于表达,如果随机变量X,Y互相独立,又可记为X⊥Y,如果随机变量X,Y在给定时互相独立,则可记为X⊥Y|Z。
以上主要介绍离散型随机变量的独立性和条件独立性,如果是连续型随机变量,我们只要把概率换成随机变量的密度函数即可。

2.8期望、方差、协方差

在机器学习、深度学习中经常需要分析随机变量的数据特征及随机变量间的关系等,对于这些指标的衡量在概率统计中有相关的内容,如用来衡量随机变量的取值大小的期望(Expectation)值或平均值、衡量随机变量数据离散程度的方差(Variance)、揭示随机向量间关系的协调方差(Convariance)等。
这些衡量指标的定义及公式就是本节主要内容。
首先我们看随机变量的数学期望的定义:
对离散型随机变量X,设其分布律为:


期望有一些重要性质,具体如下:
设a,b为一个常数,X和Y是两个随机变量。则有:
(1)E(a)=a
(2)E(aX)=aE(X)
(3)E(aX+bY)=aE(X)+bE(Y)                                                                      (2.19)
(4)当X和Y相互独立时,则有:
E(XY)=E(X)E(Y)                                                                                      (2.20)
数学期望也常称为均值,即随机变量取值的平均值之意,当然这个平均,是指以概率为权的加权平均。期望值可大致描述数据的大小,但无法描述数据的离散程度,这里我们介绍一种刻画随机变量在其中心位置附近离散程度的数字特征,即方差。如何定义方差?
假设随机向量X有均值E(X)=a。试验中,X取的值当然不一定恰好是a,可能会有所偏离。偏离的量X-a本身也是一个随机变量。如果我们用X-a来刻画随机变量X的离散程度,当然不能取X-a的均值,因E(X-a)=0 ,说明正负偏离抵消了,当然我们可以取|X-a|这样可以防止正负抵消的情况,但绝对值在实际运算时很不方便。人们就考虑另一种方法,先对X-a平方以便消去符号,然后再取平均得


方差的平方根被称为标准差。
对于多维随机向量,如二维随机向量(X,Y)如何刻画这些分量间的关系?显然均值、方差都无能为力。这里我们引入协方差的定义,我们知道方差是X-EX乘以X-EX的均值,如果我们把其中一个换成Y-EY,就得到E(X-EX)(Y-EY),其形式接近方差,又有X,Y两者的参与,由此得出协方差的定义,随机变量X,Y的协方差,记为:Cov(X,Y)
Cov(X,Y) = E(X-EX)(Y-EY)                                                                             (2.22)
协方差的另一种表达方式:
Cov(X,Y) = E(XY)-EX×EY                                                                               (2.23)
方差可以用来衡量随机变量与均值的偏离程度或随机变量取值的离散度,而协方差则可衡量随机变量间的相关性强度,如果X与Y独立,那么它们的协方差为0。注意反之,并不一定成立,独立性比协方差为0的条件更强。不过如果随机变量X、Y都是正态分布,此时独立和协方差为0是一个概念。
当协方差为正时,表示随机变量X、Y为正相关;如果协方差为负,表示随机变量X、Y为负相关。
为了更好的衡量随机变量间的相关性,我们一般使用相关系数来衡量,相关系数将每个变量的贡献进行归一化,使其只衡量变量的相关性而不受各变量尺寸大小的影响,相关系统的计算公式如下:

求随机变量的方差、协方差、相关系统等,使用Python的numpy相关的函数,如用numpy.var求方差,numpy.cov求协方差,使用numpy.corrcoef求相关系数,比较简单,这里就不展开来说。
在机器学习中多维随机向量,通常以矩阵的方式出现,所以求随机变量间的线性相关性,就转换为求矩阵中列或行的线性相关性。这里我们举一个简单实例,来说明如果分析向量间的线性相关性并可视化结果。这个例子中使用的随机向量(或特征值)共有三个,一个是气温(temp),一个体感温度(atemp),一个是标签(label)说明共享单车每日出租量,以下是这三个特征的部分数据:

这里使用Python中数据分析库pandas及画图库matplotlib 、sns等。

从以上图可以看出,特征temp与atemp是线性相关的,其分布接近正态分布。

2.9贝叶斯定理

贝叶斯定理是概率论中的一个定理,它跟随机变量的条件概率以及边缘概率分布有关。在有些关于概率的解释中,贝叶斯定理(贝叶斯公式)能够告知我们如何利用新证据修改已有的看法。这个名称来自于托马斯•贝叶斯。
通常,事件A在事件B(发生)的条件下的概率,与事件B在事件A(发生)的条件下的概率是不一样的;然而,这两者是有确定的关系的,贝叶斯定理就是这种关系的陈述。贝叶斯公式的一个用途在于通过已知的三个概率函数推出第四个。
贝叶斯公式为:

2.10信息论

信息论是应用数学的一个分支,主要研究的是对信号所含信息的多少进行量化。它的基本想法是一个不太可能的事件居然发生了,要比一个非常可能的事件发生能提供更多的信息。本节主要介绍度量信息的几种常用指标,如信息量、信息熵、条件熵、互信息、交叉熵等。

2.10.1 信息量

1948年克劳德•香农(Claude Shannon)发表的论文“通信的数学理论”是世界上首次将通讯过程建立了数学模型的论文,这篇论文和1949年发表的另一篇论文一起奠定了现代信息论的基础。信息量是信息论中度量信息多少的一个物理量。它从量上反应具有确定概率的事件发生时所传递的信息。香农把信息看作是“一种消除不确定性”的量,而概率正好是表示随机事件发生的可能性大小的一个量,因此,可以用概率来定量地描述信息。
在实际运用中,信息量常用概率的负对数来表示,即,。为此,可能有不少人会问,为何用对数,前面还要带上负号?
用对数表示是为了计算方便。因为直接用概率表示,在求多条信息总共包含的信息量时,要用乘法,而对数可以变求积为求和。另外,随机事件的概率总是小于1,而真实小于1的对数为负的,概率的对数之前冠以负号,其值便成为正数。所以通过消去不确定性,获取的信息量总是正的。

2.10.2 信息熵

信息熵(entropy)又简称为熵,是对随机变量不确定性的度量。熵的概念由鲁道夫•克劳修斯(Rudolf Clausius)于1850年提出,并应用在热力学中。1948年,克劳德•艾尔伍德•香农(Claude Elwood Shannon)第一次将熵的概念引入信息论中,因此它又称为香农熵。
用熵来评价整个随机变量X平均的信息量,而平均最好的量度就是随机变量的期望,即熵的定义如下:

我们利用Python具体实现以下概率p与H(X)的关系:

从这个图形可以看出,当概率为0或1时,H(X)为0,说明此时随机变量没有不确定性,当p=0.5时,随机变量的不确定性最大,即信息量最大。H(X)此时取最大值。

2.10.3 条件熵

设二维随机变量(X,Y),其联合概率分布为:

注意,这个条件熵,不是指随机变量X在给定某个数的情况下,另一个变量的熵是多少,变量的不确定性是多少?而是期望!因为条件熵中X也是一个变量,意思是在一个变量X的条件下(变量X的每个值都会取),另一个变量Y熵对X的期望。
条件熵比熵多了一些背景知识,按理说条件熵的不确定性小于熵的不确定,即H(Y|X)≤H(Y),事实也是如此,下面这个定理有力地说明了这一点。
定理:对二维随机变量(X,Y),条件熵H(Y|X)和信息熵H(Y)满足如下关系:
H(Y|X)≤H(Y)                                                                                                   (2.29)

2.10.4 互信息

互信息(mutual information)又称为信息增益,用来评价一个事件的出现对于另一个事件的出现所贡献的信息量。记为:
I(X,Y)=H(Y)-H(Y|X)                                                                               (2.30)
在决策树的特征选择中,信息增益为主要依据。在给定训练数据集D,假设数据集由n维特征构成,构建决策树时,一个核心问题就是选择哪个特征来划分数据集,使得划分后的纯度最大,一般而言,信息增益越大,意味着使用使用某属性a来划分所得“纯度提升”越大。因此,我们常用信息增益来进行决策树划分属性。
2.10.5 相对熵
相对熵(relative entropy),所谓相对,一般是在两个随机变量之间来说,又被称为KL散度(Kullback–Leibler divergence,KLD),这里我们假设 p(x) 和 q(x) 是 X 取值的两个概率分布,如p(x)表示X的真实分布,q(x)表示X的训练分布或预测分布。则 p 对 q 的相对熵为:

相对熵有些重要性质:
(1)相对熵不是传统意义上的距离,它没有对称性,即
KL(p(x)||q(x))≠KL(q(x)||p(x))
(2)当预测分布q(x)与真实分布p(x)完全相等时,相对熵为0;
(3)如果两个分别差异越大,那么相对熵也越大;反之,如果两个分布差异越小,相对熵也越小。
(4)相对熵满足非负性,即 KL(p(x)||q(x))≥0

2.10.5 交叉熵

交叉熵可在神经网络(机器学习)中作为损失函数,p表示真实标记的分布,q则为训练后的模型的预测标记分布,交叉熵损失函数可以衡量p与q的相似性。交叉熵作为损失函数还有一个好处是使用sigmoid函数在梯度下降时能避免均方误差损失函数学习速率降低的问题,因为学习速率可以被输出的误差所控制。

第1章 线性代数

线性代数是数学的一个重要分支,广泛应用于科学和工程领域。线性代数,特别是矩阵运算是很多机器学习算法,尤其是深度学习的基础。因此,我们先介绍一些必备的线性代数的知识。
在深度学习的图像处理中,1张图由28*28像素点构成,而这28*28就是一个矩阵;深度学习中神经网络中,权重一般都是矩阵,我们经常把权重矩阵W与输入X相乘,输入X一般是向量,这就涉及矩阵与向量相乘的问题。诸如此类,矩阵及矩阵运算在深度学习中非常普遍,当然也非常重要。

1.1标量、向量、矩阵和张量

在线性代数和机器学习中,通常会遇到以下4种类型的数据。
标量(scalar):
一个标量就是一个单独的数,一般用小写的的变量名称表示,如a,x等
向量(vector):

我们可以把向量看作空间中的点,每个元素是不同的坐标轴上的坐标。
向量可以这样表示,那我们如何用编程语言如python来实现呢?如何表示一个向量?如何获取向量中每个元素呢?请看如下示例:

打印结果如下:
5
1 2 4 8

说明这个向量,元素个数为5,向量中索引一般从0开始,如a[0]表示第一个元素1,a[1]
表示第一个元素2, a[2]表示第3个元素4,依次类推。这个从左到右的排列顺序,如果从右到左,我们可用负数来表示,如a[-1]表示第1个元素(注:从右到左),a[-2]表示第2个元素,依次类推。
矩阵(matrix)

我们如何用Python来表示或创建矩阵呢?如果我们希望获取其中某个元素,该如何实现呢?请看如下示例:

打印结果:
[[1 2 3]
[4 5 6]]
6
(2, 3)
1 2 5
[4 5 6]

矩阵我们可以用嵌套向量生成,和向量一样,在numpy中,矩阵的元素的下标索引也是从从开始的。

张量(tensor)
几何代数中定义的张量是基于向量和矩阵的推广,通俗一点理解的话,我们可以将标量视为零阶张量,向量视为一阶张量,那么矩阵就是二阶张量,当然,三阶的就称为三阶张量,以此类推。在机器学习、深度学习中经常遇到多维矩阵,如一张彩色图片就是一个三阶张量,三个维度分别是图片的高度、宽度和色彩数据。
张量(tensor)也是深度学习框架Tensorflow的重要概念。Tensorflow实际上有tensor(张量)+flow(流)构成。
同样我们可以用python来生成张量及获取其中某个元素或部分元素,请看示例:

打印结果如下:
[[[ 0 1 2 3]
[ 4 5 6 7]]

[[ 8 9 10 11]
[12 13 14 15]]]
16
(2, 2, 4)
0 1 5
[4 5 6 7]

转置(transpose)


numpy如何实现转置?很简单,利用张量的T属性即可,示例如下:

打印结果如下:
[[1 2 3]
[4 5 6]]
[[1 4]
[2 5]
[3 6]]

1.2矩阵和向量

矩阵加法和乘法是矩阵运算中最常用的操作之一,两个矩阵相加,需要它们的形状相同,则是对应元素的相加,如:C=A+B,其中。矩阵也可以和向量相加,只要它们的列数相同,相加的结果是矩阵每行与向量相加,这种隐式地复制向量b到很多位置的方式称为广播(broadcasting),以下我们通过一个代码示例来说明。

打印结果为:
[[11 22 33]
[14 25 36]]

两个矩阵相加,要求它们的形状相同,如果两个矩阵相乘,如A和B相乘,结果为矩阵C,矩阵A和B需要什么条件呢?条件比较简单,只要矩阵A的列数和矩阵B的行数相同即可。如果矩阵A的形状为mxn,矩阵B的形状为nxp,那么矩阵C的形状就是mxp,例如
C=AB,则它们的具体乘法操作定义为:

1.3特殊矩阵与向量

有些特殊类型的矩阵在机器学习或深度学习中经常遇到,如对角矩阵、对称矩阵、单位矩阵、正交矩阵、可逆矩阵等等。向量由正交向量。下面我们逐一进行简要说明。
(1)可逆矩阵

后续我们有更详细的讨论及代码实现。
(2)对角矩阵

从上面两个式子可以看到对角矩阵的简洁高效。
(3)对称矩阵
对称矩阵,对于任意一个n阶方阵A,若A满足: 成立, 则称方阵A为对称矩阵。

1.4线性相关性及向量空间

前面我们介绍了向量、矩阵等概念,接下来我们将介绍向量组、线性组合、线性相关性、秩等重要概念。
由多个同维度的列向量构成的集合称为向量组,矩阵可以可以看成是有行向量或列向量构成的向量组。
1)线性组合

3)向量组的秩


秩是一个重要概念,运行非常广泛,实际上矩阵我们可以看成是一个向量组。如果把矩阵看成是由所有行向量构成的向量组,这样矩阵的行秩就等于行向量组的秩;如果把矩阵看成是由所有列向量构成的向量组,这样矩阵的列秩就等于列向量组的秩。
矩阵的行秩与列秩相等,因此,把矩阵的行秩和列秩统称为矩阵的秩。

1.5范数

数有大小,向量也有大小,向量的大小我们通过范数(Norm)来衡量。范数在机器学习、深度学习中运行非常广泛,特别在限制模型复杂度、提升模型的泛化能力方面效果不错。
p范数的定义如下:


以上说了向量一种度量方式,即通过范数来度量向量或矩阵的大小,并有具体公式,在实际编程中如何计算向量的范数呢?这里我们还是以Python为例进行说明。

打印结果如下:
[ 0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9]
4.5
1.68819430161
0.9

看来利用python求向量的范数还是很方便的。

3.6特征值分解

许多数学对象可以分解成多个组成部分。特征分解就是使用最广的矩阵分解之一,即将矩阵分解成一组特征向量和特征值。本节讨论的矩阵都是方阵。
我们先介绍特征值、特征向量的概念。
设A是一个n阶方阵,如果存在实数⋋和n维的非零向量x,满足:
Ax=⋋x                                                     (3.13)
那么把数⋋称为方阵A的特征值,向量x称为矩阵A对应特征值⋋的特征向量。

注意,并不是所有方阵都能进行特征值分解,一个n阶方阵A能进行特征值分解的充分必要条件是它含有n个线性无关的特征向量。
这里我们介绍了给定一个方阵,如何求该方阵的特征向量和特征值的理论方法,这些方法如何用编程语言来实现呢?实现起来是否麻烦?有了Python的帮忙,实现起来非常简单,具体请看如下示例:

打印结果:
[-0.37228132 5.37228132]
[-0.37228132 5.37228132]
[[-0.82456484 -0.41597356]
[ 0.56576746 -0.90937671]]
【说明】
在numpy.linalg模块中:
eigvals() 计算矩阵的特征值
eig() 返回包含特征值和对应特征向量的元组

1.7奇异值分解

上节我们介绍了方阵的一种分解方式,如果矩阵不是方阵,是否能分解?如果能,该如何分解?这节我们介绍一种一般矩阵的分解方法,称为奇异值分解,这种方法应用非常广泛,如降维、推荐系统、数据压缩等等。
矩阵非常重要,所以其分解方法也非常重要,方法也比较多,除了特征分解法,还有一种分解矩阵的方法,被称为奇异值分解(SVD)。将矩阵分解为奇异向量和奇异值。通过奇异分解,我们会得到一些类似于特征分解的信息。然而,奇异分解有更广泛的应用。
每个实数矩阵都有一个奇异值分解,但不一定都有特征分解。例如,非方阵的矩阵没有特征分解,这时我们只能使用奇异值分解。
奇异分解与特征分解类似,只不过这回我们将矩阵A分解成三个矩阵的乘积:
                                                          (1.15)
假设A是一个mxn矩阵,那么U是一个mxm矩阵,D是一个mxn矩阵,V是一个nxn矩阵。这些矩阵每一个都拥有特殊的结构,其中U和V都是正交矩阵,D是对角矩阵(注意,D不一定是方阵)。对角矩阵D对角线上的元素被称为矩阵A的奇异值。矩阵U的列向量被称为左奇异向量,矩阵V 的列向量被称右奇异向量。
SVD最有用的一个性质可能是拓展矩阵求逆到非方矩阵上。
奇异值分解,看起来太复杂,但用python来实现,却非常简单,这得感谢哪些一直致力于Python广大贡献者们。具体请看如下示例:

打印结果:
[ 1.09824632e+01 8.79229347e+00 1.03974857e+00 1.18321522e-15
2.13044868e-32]
[[ 10.98246322 0. 0. ]
[ 0. 8.79229347 0. ]
[ 0. 0. 1.03974857]]

1.8迹运算

迹运算返回的是矩阵对角元素的和:

利用python的numpy对矩阵求迹同样方便。请看以下示例。

打印结果:
15
15
171
171

1.9实例:主成分分析

主成分分析(Principal Component Analysis,PCA), 是一种统计方法。通过正交变换将一组可能存在相关性的变量转换为一组线性不相关的变量,转换后的这组变量叫主成分。
在许多机器学习、深度学习的应用中,往往需要处理大量样本或大的矩阵,多变量大样本无疑会为研究和应用提供了丰富的信息,但也在一定程度上增加了数据采集的工作量,更重要的是在多数情况下,许多变量之间可能存在相关性,从而增加了问题分析的复杂性,同时对分析带来不便。如果分别对每个指标进行分析,分析往往是孤立的,而不是综合的。盲目减少指标会损失很多信息,容易产生错误的结论。
因此需要找到一个合理有效的方法,在减少需要分析的指标或维度的同时,尽量减少原指标包含信息的损失,以达到对所收集数据进行全面分析的目的。由于各变量间存在一定的相关关系,因此有可能用较少的综合指标分别综合存在于各变量中的各类信息。主成分分析就属于这类降维的方法。
如何实现以上目标呢?这里我们简要说明一下原理,然后使用Python来实现,至于详细的推导过程,大家可参考相关书籍或网上资料。


以下我们Python具体实现一个PCA实例。
以iris为数据集,该数据集可以通过load_iris自动下载。
1)iris数据集简介:
Iris数据集是常用的分类实验数据集,由Fisher, 1936收集整理。Iris也称鸢尾花卉数据集,是一类多重变量分析的数据集。数据集包含150个数据集,分为3类,每类50个数据,每个数据包含4个属性。可通过花萼长度,花萼宽度,花瓣长度,花瓣宽度4个属性预测鸢尾花卉属于(Setosa,Versicolour,Virginica)三个种类中的哪一类。
2)算法主要步骤如下:
(1)对向量X进行去中心化
(2)计算向量X的协方差矩阵,自由度可以选择0或者1
(3)计算协方差矩阵的特征值和特征向量
(4)选取最大的k个特征值及其特征向量
(5)用X与特征向量相乘
3)代码实现

4)我们看一下各特征值的贡献率

从这个图形显示,前2个特征值的方差贡献率超过96%,所以k取2是合理的。

1.10小结

本章主要介绍了线性代数中矩阵及向量有关概念及相关规则和运算,线性代数是理解深度学习所必须掌握的基础数学学科之一。另一门在机器学习中无处不在的重要数学学科是概率论,我们将在下一章介绍。

第22章 强化学习基础

22.1强化学习简介

强化学习是机器学习中的一种算法, 如下图,它是让计算机实现从一开始什么都不懂, 不像监督学习或无监督学习有大量的经验或输入数据, 它基本算自学成才。通过不断地尝试, 从错误或惩罚中学习, 最后找到规律, 学会了达到目的的方法. 这就是一个完整的强化学习过程。

强化学习已经在如游戏,机器人等领域中开花结果。各大科技公司,如中国的百度、阿里美国的谷歌、facebook、微软等更是将强化学习技术作为其重点发展的技术之一。可以说强化学习算法正在改变和影响着这个世界,掌握了这门技术就掌握了改变世界和影响世界的工具。
强化学习应用非常广泛,目前主要领域有:
游戏理论与多主体交互
机器人
电脑网络
车载导航
医学
工业物流
强化学习的原理大致如下图:

其中:
主体(agent)
是动作的行使者,例如配送货物的无人机,或者电子游戏中奔跑跳跃的超级马里奥。
状态(state)
是主体的处境,亦即一个特定的时间和地点、一项明确主体与工具、障碍、敌人或奖 品等其他重要事物的关系的配置。
动作(action)
其含义不难领会,但应当注意的是,主体需要在一系列潜在动作中进行选择。在电子 游戏中,这一系列动作可包括向左或向右跑、不同高度的跳跃、蹲下和站着不动。在 股票市场中,这一系列动作可包括购买、出售或持有一组证券及其衍生品中的任意一 种。无人飞行器的动作选项则包括三维空间中的许多不同的速度和加速度等。
奖励(reward)
是用于衡量主体的动作成功与否的反馈。例如,在电子游戏中,如果马里奥接触一枚 金币,他就能赢得分数。主体向环境发出以动作为形式的输出,而环境则返回主体的 新状态及奖励。
环境(environment)
其主体被“嵌入”并能够感知和行动的外部系统。

输入是:
State
为 Observation,例如迷宫的每一格是一个State
Actions
在每个状态下,有什么行动
Reward
进入每个状态时,能带来正面或负面的回报
输出就是:
Policy
在每个状态下,会选择哪个行动
具体来说就是:
State = 迷宫中Agent的位置,可以用一对座标表示,例如(1,3)
Action = 在迷宫中每一格,你可以行走的方向,例如:{上,下,左,右}
Reward = 当前的状态 (current state) 之下,迷宫中的一格可能有食物 (+1) ,也可能有怪兽 (-100)
Policy = 一个由状态 → 行动的函数,即: 函数对给定的每一个状态,都会给出一个行动。
增强学习的任务就是找到一个最优的策略Policy从而使Reward最多。
我们一开始并不知道最优的策略是什么,因此往往从随机的策略开始,使用随机的策略进行试验,就可以得到一系列的状态,动作和反馈:

这就是一系列的样本Sample。增强学习的算法就是需要根据这些样本来改进Policy,从而使得得到的样本中的Reward更好。

22.2强化学习常用算法

强化学习有多种算法,目前比较常用的算法, 有通过行为的价值来选取特定行为的方法, 如 Q-learning, Sarsa, 使用神经网络学习的 DQN(Deep Q Network), 以及DQN的后续算法,还有直接输出行为的 policy gradients等。限于篇幅,这里我们主要介绍几种具有代表性算法。

22.2.1 Q-Learning算法

利用 Q-Learning 训练计算机玩 Atari 游戏的时候,Q-Learning 曾引起了轰动。现在,Q-Learning 依然是一个有重大意义的概念。大多数现代的强化学习算法,都是 Q-Learning 的一些改进。
(1)其主流程大致如下:

(2)Q-learning的伪代码为:
初始化价值表Q(s,a)
观察当前状态 s
根据策略(比如贪心算法)选择一个动作a
执行这个动作,观察奖励r和新状态s
使用观察到的奖励和下一个状态可能的最大奖励来更新状态的价值。更新是基于下述公式和参数。
将状态设置为新状态,并重复该过程,直到达到终端状态。
(3)在Q-Learning中,函数Q(s,a)是代表机器人在状态s时采取动作a所得到的未来价值回报,可能有读者就有疑问了,当前我采取了动作a,我怎么知道我当前这个做法在未来的价值回报是多少?确实不能,如果我们仅仅知道当下的状态、动作,而不知道紧接下来的动作及其回报。
怎么求Q(s,a)是一个摆在我们面前的大问题,虽然我们无法确切的知道Q(s,a)是多少,但是我们可以在心里面假想这个Q(s,a)一定是存在的,一旦Q(s,a)存在,于是我们在每一个状态s时,只需采取满足Q(s,a)最大的那个动作a了。
如何计算Q(s,a)?我们可以回顾一下Bellman方程,即前后两个相邻的Q(s,a)必须满足的关系,简单说就是当前采取的行动的最大价值回报等于瞬时回报加上下一个状态的最大回报。顺此思路,Q-Learning的做法就是使用此Bellman方程不断逼近Q(s,a)。
Q-Learning最简单最直接的做法就是构建一个状态-动作表,该表中存储着每个状态采取每个动作时候的Q值。该做法的流程可以总结如下:
假设状态数目为A,动作数目为B
• 随机初始化数组Q[A, B]
• 定义或获取初始状态s
• 重复以下步骤直到数组Q收敛
(1) 选取一个动作a并且执行
(2) 接受回报R,同时转移到下一个状态s’
(3) 更新Q[s , a]的值
(4) 状态变为s’,并返回到步骤(1)
可以看到,使用Q[s,a]来进行Q-Learning的算法流程非常简单,不过上述算法中有以下几个值得思考的关键点:
第一,如何选取动作a?
如果仅仅是采取贪婪的算法,在每个状态s处挑选让此状态达到最大值的动作a,这种做法眼光太狭隘,只注重眼前利益,不顾长远回报,其使得expoitation达到了最大,exploration却最小,最终陷入局部最优。因此比较好的办法是在选取动作a的时候,既要注重expoitation,又要考虑exploration,可以采取e-greedy算法,设定一定的概率让机器人随机采取动作,其余时候贪婪做出决策。
第二,如何更新Q[s,a]的值?
前面说了,由Bellman方程可知,Q[s,a]=R+max(Q[s',:]),从梯度下降的角度看,这个新的Q[s,a]是在s状态下求出的最大未来回报,也是我们渴望逼近的值,但是我们在更新旧的Q[s,a]的时候,不能将此新的Q[s,a]直接替代旧的Q[s,a],因为这个新的Q[s,a]只是我们的一种估计,甚至是一种带噪声的估计。因此,不能直接使用新的Q[s,a]去取代旧的Q[s,a],而应该是让旧的Q[s,a]朝着新的Q[s,a]走一小步,用公式描述就是:
Q[s,a]=Q[s,a]+lr*(R+gamma*max(Q[s',:])-Q[s,a])
其中lr是学习率,gamma是折扣因子,之所以使用折扣因子是因为,环境可能是随机的,我们无法完全确保在下一次采取同样的动作还是会得到同样的回报值。折扣因子一般介于0和1之间。
第三,除了使用e-greedy算法选取动作,是否还可以使用其他技巧?
答案是可以的,比如,在每一个状态s时,当我们计算Q[s,:]的最大值时可以先随机添加一点高斯噪声到Q[s,:]上再求最大值,这样也可以起到了exploration的作用,这种方法的好处就是我们在鼓励exploration的同时也照顾了Q值,而不是纯粹的随机选取。我想,在训练的不同阶段,我们可以组合这两种技巧,比如在训练开始的时候,采取e-greedy算法,而一定周期以后,就采取添加噪声的算法。
Q-Learning算法是一种离策略算法(off-policy),即其Q函数更新不同于选取动作时遵循的策略,换句话说,Q函数更新时计算了下一个状态的最大价值,但是取那个最大值的时候的行动与当前策略是无关的。后面我们将介绍一种on-policy策略算法,即sarsa算法。
正如我之前所说,Q-Learning是一种off-policy的强化学习算法,即其Q表的更新不同于选取动作时所遵循的策略,换句化说,Q表在更新的时候计算了下一个状态的最大价值,但是取那个最大值的时候所对应的行动不依赖于当前策略。
用公式来表示Q-Learning中Q表的更新:
Q(St,At) = Q(St,At) + lr [R(t+1) + discount * max Q(St+1,a) – Q(St,At)]
根据以上公式,我们就可以写Q-Learning算法描述如下:

其代码如何实现?后面我们将结合具体实例来说明其详细代码实现。

22.2.2 Sarsa算法

Sarsa算法与Q-learning算法非常相似,所不同的就是更新Q值时,Sarca的现实值取R+γQ(S',A'),而不是R+γmax_a'Q(S',a'),而Q learning是使用R+γmax_a'Q(S',a')。
其算法逻辑为:

22.2.3 DQN算法

DQN(Deep Q Network) 由 DeepMind 在 2013 年在 NIPS 提出。它融合了神经网络和 Q learning 的方法。DQN 算法的主要做法是 Experience Replay,其将系统探索环境得到的数据储存起来,然后随机采样样本更新深度神经网络的参数。

Experience Replay 的动机是:
1)深度神经网络作为有监督学习模型,要求数据满足独立同分布。
2)但 Q Learning 算法得到的样本前后是有关系的。为了打破数据之间的关联性,Experience Replay 方法通过存储-采样的方法将这个关联性打破了。
DeepMind 在 2015 年初在 Nature 上发布了文章,引入了 Target Q 的概念,进一步打破数据关联性。Target Q 的概念是用旧的深度神经网络 w− 去得到目标值,下面是带有 Target Q 的 Q Learning 的优化目标。
J=min(r+γmaxa′Q(s′,a′,w−))−Q(s,a,w))2
Q-learning最早是根据一张Q-table,即各个状态动作的价值表来完成的,通过动作完成后的奖励不断迭代更新这张表,来完成学习过程。然而当状态过多或者离散时,这种方法自然会造成维度灾难,所以我们才要用一个神经网络来表达出这张表,也就是Q-Network。
如下图,把Q-table换为神经网络,利用神经网络参数替代Q-table。

通过前面两个卷积层,我们完全不需要费心去理解环境中的状态和动作奖励,只需要将状态参数一股脑输入就好。当然,我们的数据特征比较简单,无需进行池化处理,所以后面两层直接使用全连接即可。
主体算法如下:

22.3强化学习实例

22.3.1强化学习寻宝实例

这节我们用 tabular Q-learning 的方法实现一个小例子, 例子的环境是一个一维世界或一条线段上, 在世界的右边有宝藏, 探索者只要得到宝藏就将获得奖励, 然后以后就记住了得到宝藏的方法。其环境图像如下:

Q-learning 是一种记录行为值 (Q value) 的方法, 每种在一定状态的行为都会有一个值 Q(s, a), 就是说 行为 a 在 s 状态的值是 Q(s, a). s 在上面的探索者游戏中, 就是 o 所在的地点了. 而每一个地点探索者都能做出两个行为 left/right, 这就是探索者的所有可行的 a。
如果在某个地点 s1, 探索者计算了他能有的两个行为, a1/a2=left/right, 计算结果是 Q(s1, a1) > Q(s1, a2), 那么探索者就会选择 left 这个行为. 这就是 Q learning 的行为选择简单规则。

22.3.1.1算法的详细过程

 

22.3.1.2代码实现

这里我使用python3.6
1)导入必要的库

2)创建Q表
对于 tabular Q learning, 我们必须将所有的 Q values (行为值) 放在 q_table 中, 更新 q_table 也是在更新他的行为准则. q_table 的 index 是所有对应的 state (探索者位置), columns 是对应的 action (探索者行为).

格式如下:
# q_table:
"""
left right
0 0.0 0.0
1 0.0 0.0
2 0.0 0.0
3 0.0 0.0
4 0.0 0.0
5 0.0 0.0
"""

3)定义动作
接着定义探索者是如何挑选行为的. 这是我们引入 epsilon greedy 的概念. 因为在初始阶段, 随机的探索环境, 往往比固定的行为模式要好, 所以这也是累积经验的阶段, 我们希望探索者不会那么贪婪(greedy). 所以 EPSILON 就是用来控制贪婪程度的值. EPSILON 可以随着探索时间不断提升(越来越贪婪), 不过在这个例子中, 我们就固定成 EPSILON = 0.9, 90% 的时间是选择最优策略, 10% 的时间来探索.

4)获取环境反馈信息
做出行为后, 环境也要给我们的行为一个反馈, 反馈出下个 state (S_) 和 在上个 state (S) 做出 action (A) 所得到的 reward (R). 这里定义的规则就是, 只有当 o 移动到了 T, 探索者才会得到唯一的一个奖励, 奖励值 R=1, 其他情况都没有奖励。

5)更新环境

6)强化学习主循环

7)运行主程序

运行结果:
Episode 1: total_steps = 46
Episode 2: total_steps = 66
Episode 3: total_steps = 52
Episode 4: total_steps = 21
Episode 5: total_steps = 31
Episode 6: total_steps = 12
Episode 7: total_steps = 10
Episode 8: total_steps = 7
Episode 9: total_steps = 7
Episode 10: total_steps = 7
Episode 11: total_steps = 7
Episode 12: total_steps = 7
Episode 13: total_steps = 7
Episode 14: total_steps = 7
Episode 15: total_steps = 7

Q-table:

left right
0 1.324169e-07 0.000238
1 1.479568e-07 0.001645
2 6.925792e-07 0.009356
3 5.968673e-06 0.042724
4 6.631859e-05 0.152157
5 1.385100e-04 0.407920
6 8.100000e-04 0.794109
7 0.000000e+00 0.000000

从以上结果可以看出,为找到宝藏,前几次查询步骤比较多,但后来,她就越来越聪明了,后来从开始位置,只要7步了,可以说直奔宝藏了。

22.3.2强化学习立杆实例

这里我们用一个走迷宫例子来实现 RL方法,即Sarsa(state-action-reward-state_-action_). 我们从这一个简称可以了解到, Sarsa 的整个循环都将是在一个路径上, 也就是 on-policy, 下一个 state_, 和下一个 action_ 将会变成他真正采取的 action 和 state. 和 Qlearning 的不同之处就在这. Qlearning 的下个一个 state_ action_ 在算法更新的时候都还是不确定的 (off-policy). 而 Sarsa 的 state_, action_ 在这次算法更新的时候已经确定好了 (on-policy)。

整个算法还是一直不断更新 Q table 里的值, 然后再根据新的值来判断要在某个 state 采取怎样的 action. 不过与 Qlearning 不同之处:

1)Sarsa在当前 state 已经想好了 state 对应的 action, 而且想好了 下一个 state_ 和下一个 action_ ,但Qlearning 还没有想好下一个 action_。
2)Sarsa更新 Q(s,a) 的时候基于的是下一个 Q(s_, a_) ,而Qlearning 是基于 maxQ(s_)。

22.3.2.1算法的代码实现

【说明】
环境设计程序maze_env.py, 算法实现程序RL_brain.py,大家可以访问获取:
https://github.com/MorvanZhou/Reinforcement-learning-with-tensorflow/tree/master/contents/3_Sarsa_maze

22.3.3强化学习CartPole实例

在本次实战中,我们不选择Atari游戏,而使用OpenAI Gym中的传统增强学习任务之一CartPole作为练手的任务。游戏示意图如下:

游戏的基本要求就是控制下面的cart移动,使连接在上面的杆保持垂直不倒。这个任务简化到只有两个离散动作,要么向左用力,要么向右用力。而state状态就是这个杆的位置和速度。有关这个游戏环境的详细信息可参考:https://github.com/openai/gym/wiki/CartPole-v0
下面我们就要用DQN来解决这个问题。

22.3.3.1实现这个游戏需要的基础

1)熟悉Python编程,能够使用Python基本的语法
2)对Tensorflow有一定的了解,知道基本的使用
3)知道如何使用OpenAI Gym
4)了解基本的神经网络MLP
5)理解DQN算法

22.3.3.2实现算法

我们将要实现的是最基本的DQN,也就是NIPS 13版本的DQN:

面对CartPole问题,我们进一步简化:
1)无需预处理Preprocessing。也就是直接获取观察Observation作为状态state输入。
2)只使用最基本的MLP神经网络,而不使用卷积神经网络,更不用池化层等。

22.3.3.3导入需要的库

22.3.3.4编写主循环

这里我们按照至上而下的编程方式,先写主函数用来说明整个程序架构,然后再具体编写DQN算法实现。

22.3.3.5相关函数

接下来我们编写一个DQN的函数,DQN的一切都将封装在里面。在主函数中,我们只需调用

egreedy_action输入状态,输出动作,perceive 保存信息。
然后环境自己执行动作,输出新的状态:

然后整个过程就反复循环,一个episode结束,就执行一个。

22.3.3.6 DQN实现

1)编写基本DQN类的结构

DQN一个很重要的功能就是要能存储数据,然后在训练的时候minibatch出来。所以,我们需要构造一个存储机制。这里使用deque来实现。

2)初始化

这里要注意一点就是egreedy的epsilon是不断变小的,也就是随机性不断变小。怎么理解呢?就是一开始需要更多的探索,所以动作偏随机,慢慢的我们需要动作能够有效,因此减少随机。
3)创建Q网络
我们这里创建最基本的MLP,输入层为state_dim(其值为4)个节点、隐含层节点数设置为20,输出层节点数为action_dim(其值为2)

这里我们只用了一个隐层,然后使用relu非线性单元。另外,需注意state 输入的格式,因为使用minibatch,所以格式是[None,state_dim]
4)写perceive函数

这里action值与动作的对应关系为:0表示把cart往左推;1表示把cart往右推、为便于计算起见,对表示动作数据格式做了转换。使用了在神经网络中one hot key的形式,而在OpenAI Gym中则使用单值。什么意思呢?比如我们输出动作是1,那么对应的one hot形式就是[0,1],如果输出动作是0,那么one hot 形式就是[1,0]。这样做的目的是为了之后更好的进行计算。
在perceive中一个最主要的事情就是存储。然后根据情况进行train。这里我们要求只要存储的数据大于Batch的大小就开始训练。
5)编写action输出函数

输出有两种可能,一个是根据情况输出随机动作,一个是根据神经网络输出。由于神经网络输出的是每一个动作的Q值,因此我们选择最大的那个Q值对应的动作输出。
6)编写training method函数

这里的y_input就是target Q值。我们这里采用Adam优化器,其实随便选择一个必然SGD,RMSProp都是可以的。可能比较不好理解的就是Q值的计算。这里大家记住动作输入是one hot key的形式,因此将Q_value和action_input向量相乘得到的就是这个动作对应的Q_value。然后用reduce_sum将数据维度压成一维。
7)编写training函数

先进行minibatch工作,然后根据batch计算y_batch。最后用optimizer进行优化。
8)测试模型代码

测试中唯一的不同就是我们使用

来获取动作,也就是完全没有随机性,只根据神经网络来输出,没有探索,同时这里也就不再perceive输入信息来训练。

22.3.3.7 完整代码

修改了部分代码,主要是为适应新版本的要求。以下代码我的运行环境为:python3.6,tensorflow1.3,在ipython中运行。

运行结果:


episode: 0 Evaluation Average Reward: 74.7
episode: 100 Evaluation Average Reward: 95.8
episode: 200 Evaluation Average Reward: 67.0
episode: 300 Evaluation Average Reward: 14.1
episode: 400 Evaluation Average Reward: 9.8
episode: 500 Evaluation Average Reward: 9.7
episode: 600 Evaluation Average Reward: 11.3
episode: 700 Evaluation Average Reward: 11.6
episode: 800 Evaluation Average Reward: 11.3
episode: 900 Evaluation Average Reward: 12.2

参考了:
https://zhuanlan.zhihu.com/p/21477488

4.6 参数估计

参数估计(parameter estimation)是根据从总体中抽取的样本估计总体分布中包含的未知参数的方法。人们常常需要根据手中的数据,分析或推断数据反映的本质规律。即根据样本数据如何选择统计量去推断总体的分布或数字特征等。统计推断是数理统计研究的核心问题。所谓统计推断是指根据样本对总体分布或分布的数字特征等作出合理的推断。它是统计推断的一种基本形式,是数理统计学的一个重要分支。
参数估计有最大似然估计和EM算法,他们都是在已知样本数据下,如何确定未知参数的值,才能使样本值最大。而他们的区别是EM是带隐变量的似然估计,当有部分数据缺失或者无法观察到时,EM算法提供了一个高效的迭代程序用来计算这些数据的最大似然估计。

4.6.1极大似然估计

4.6.1.1极大似然估计简介

极大似然估计是一种参数估计的方法。
先验概率是 知因求果,后验概率是 知果求因,极大似然是 知果求最可能的原因。
即它的核心思想是:找到参数 θ的一个估计值,使得当前样本出现的可能性最大。

例如,当其他条件一样时,抽烟者患肺癌的概率是不抽烟者的 5 倍,那么当我们已知现在有个人是肺癌患者,问这个人是抽烟还是不抽烟?大多数人都会选择抽烟,因为这个答案是“最有可能”得到“肺癌”这样的结果。

4.6.1.2 为什么要有参数估计

当模型已定,但是参数未知时。
例如我们知道全国人民的身高服从正态分布,这样就可以通过采样,观察其结果,然后再用样本数据的结果推出正态分布的均值与方差的大概率值,就可以得到全国人民的身高分布的函数。

4.6.1.3为什么要用似然函数取最大

极大似然估计是频率学派最经典的方法之一,认为真实发生的结果的概率应该是最大的,那么相应的参数,也应该是能让这个状态发生的概率最大的参数。

4.6.1.4何为似然函数

统计学中,似然(likelihood)函数是一种关于统计模型参数的函数。给定输出x时,关于参数θ的似然函数L(θ|x)(在数值上)等于给定参数θ后变量X的概率:L(θ|x)=P(X=x|θ)。
如果有n个样本,似然函数为:
设总体X服从分布P(x;θ)(当X是连续型随机变量时为概率密度,当X为离散型随机变量时为概率分布),θ为待估参数,X1,X2,…Xn是来自于总体X的样本,x1,x2…xn为样本X1,X2,…Xn的一个观察值,则样本的联合分布(当X是连续型随机变量时为概率密度,当X为离散型随机变量时为概率分布) L(θ)=L(x1,x2,…,xn;θ)=ΠP(xi;θ)称为似然函数.

4.6.1.5极大似然估计的计算过程

(1)写出似然函数:

其中 x1,x2,..,xn 为样本,θ 为要估计的参数。

(2) 一般对似然函数取对数

为何要两边取对数?因为 f(xi|θ) 一般比较小,n 比较大,如果连乘容易造成浮点运算下溢。
(3) 求出使得对数似然函数取最大值的参数值
对对数似然函数求导,令导数为0,得出似然方程,
求解似然方程,得到的参数就是对概率模型中参数值的极大似然估计。
(4)示例
假如一个罐子里有黑白两种颜色的球,数目和比例都不知道。
假设进行一百次有放回地随机采样,每次取一个球,有七十次是白球。
问题是要求得罐中白球和黑球的比例?

假设罐中白球的比例是 p,那么黑球的比例就是 1-p。
第1步:定义似然函数:

第2步:对似然函数对数化

第3步:求似然方程
即对参数p求导,并令导数为0。

第4步 解方程
最后求得 p=0.7

4.6.2 EM算法

EM(Expectation Maximization) 算法是 Dempster,Laind,Rubin 于 1977 年提出的求参数极大似然估计的一种方法,它可以从非完整数据集中对参数进行 MLE 估计,是一种非常简单实用的学习算法。这种方法可以广泛地应用于处理缺损数据,截尾数据,带有噪声等所谓的不完全数据。
EM的主要原理可以用一个比较形象的比喻说法。比如说食堂的大师傅炒了一份菜,要等分成两份给两个人吃,显然没有必要拿来天平一点的精确的去称分量,最简单的办法是先随意的把菜分到两个碗中,然后观察是否一样多,把比较多的那一份取出一点放到另一个碗中,这个过程一直迭代地执行下去,直到大家看不出两个碗所容纳的菜有什么分量上的不同为止。
EM算法就是这样,假设我们估计知道A和B两个参数,在开始状态下二者都是未知的,并且知道了A的信息就可以得到B的信息,反过来知道了B也就得到了A。可以考虑首先赋予A某种初值,以此得到B的估计值,然后从B的当前值出发,重新估计A的取值,这个过程一直持续到收敛为止。

4.6.2.1简单回顾最大似然估计

极大似然估计,只是一种概率论在统计学的应用,它是参数估计的方法之一。说的是已知某个随机样本满足某种概率分布,但是其中具体的参数不清楚,参数估计就是通过若干次试验,观察其结果,利用结果推出参数的大概值。最大似然估计是建立在这样的思想上:已知某个参数能使这个样本出现的概率最大,我们当然不会再去选择其他小概率的样本,所以干脆就把这个参数作为估计的真实值。

4.6.2.2求最大似然函数估计值的一般步骤

(1)写出似然函数;
(2)对似然函数取对数,并整理;
(3)求导数,令导数为0,得到似然方程;
(4)解似然方程,得到的参数即为所求;

4.6.2.3生活中EM原型

在上面那个身高分布估计中。男生的身高服从高斯分布、女生身高也服从高斯分布,只是他们的参数可能不相同,那么通过抽取得到的那100个男生的身高和已知的其身高服从高斯分布,我们通过最大化其似然函数,就可以得到了对应高斯分布的参数θ=[u, ∂]T了。那么,对于我们学校的女生的身高分布也可以用同样的方法得到了。
上面这个例子,我们知道抽取的男生还是女生,即知道其分布(都是高斯分布,虽然不知分布的具体参数),现在假如我们从这200个人中,这200人,由于化妆的原因,我们无法确定这个人(的身高)是男生(的身高)还是女生(的身高)。也就是说你不知道抽取的那200个人里面的每一个人到底是从男生的那个身高分布里面抽取的,还是女生的那个身高分布抽取的。用数学的语言就是,抽取得到的每个样本都不知道是从哪个分布抽取的。
这个时候,对于每一个样本或者你抽取到的人,就有两个东西需要猜测或者估计的了,一是这个人是男的还是女的?二是男生和女生对应的身高的高斯分布的参数是多少?
只有当我们知道了哪些人属于同一个高斯分布的时候,我们才能够对这个分布的参数作出靠谱的预测,例如刚开始的最大似然所说的,但现在两种高斯分布的人混在一块了,我们又不知道哪些人属于第一个高斯分布,哪些属于第二个,所以就没法估计这两个分布的参数。反过来,只有当我们对这两个分布的参数作出了准确的估计的时候,才能知道到底哪些人属于第一个分布,那些人属于第二个分布。
如何这类互相依赖问题呢?这有点像先有鸡还是先有蛋的问题了。鸡说,没有我,谁把你生出来的啊。蛋不服,说,没有我,你从哪蹦出来啊。为了解决这个你依赖我,我依赖你的循环依赖问题,总得有一方要先打破僵局,说,不管了,我先随便整一个值出来,看你怎么变,然后我再根据你的变化调整我的变化,然后如此迭代着不断互相推导,最终就会收敛到一个解。这就是EM算法的基本思想了。
EM的意思是“Expectation Maximization”,在我们上面这个问题里面,我们是先随便猜一下男生(身高)的正态分布的参数:如均值和方差是多少。例如男生的均值是1米7,方差是0.1米(当然了,刚开始肯定没那么准),然后计算出每个人更可能属于第一个还是第二个正态分布中的(例如,这个人的身高是1米8,那很明显,他最大可能属于男生的那个分布),这个是属于Expectation一步。有了每个人的归属,或者说我们已经大概地按上面的方法将这200个人分为男生和女生两部分,我们就可以根据之前说的最大似然那样,通过这些被大概分为男生的n个人来重新估计第一个分布的参数,女生的那个分布同样方法重新估计。这个是Maximization。然后,当我们更新了这两个分布的时候,每一个属于这两个分布的概率又变了,那么我们就再需要调整E步……如此往复,直到参数基本不再发生变化为止。
这里把每个人(样本)的完整描述看做是三元组yi={xi,zi1,zi2},其中,xi是第i个样本的观测值,也就是对应的这个人的身高,是可以观测到的值。zi1和zi2表示男生和女生这两个高斯分布中哪个被用来产生值xi,就是说这两个值标记这个人到底是男生还是女生(的身高分布产生的)。这两个值我们是不知道的,是隐含变量。确切的说,zij在xi由第j个高斯分布产生时值为1,否则为0。例如一个样本的观测值为1.8,然后他来自男生的那个高斯分布,那么我们可以将这个样本表示为{1.8, 1, 0}。如果zi1和zi2的值已知,也就是说每个人我已经标记为男生或者女生了,那么我们就可以利用上面说的最大似然算法来估计他们各自高斯分布的参数。但是它们未知,因此我们只能用EM算法。
咱们现在不是因为那个恶心的隐含变量(抽取得到的每个样本都不知道是从哪个分布抽取的)使得本来简单的可以求解的问题变复杂了,求解不了吗。那怎么办呢?人类解决问题的思路都是想能否把复杂的问题简单化。好,那么现在把这个复杂的问题逆回来,我假设已经知道这个隐含变量了,哎,那么求解那个分布的参数是不是很容易了,直接按上面说的最大似然估计就好了。那你就问我了,这个隐含变量是未知的,你怎么就来一个假设说已知呢?你这种假设是没有根据的。呵呵,我知道,所以我们可以先给这个给分布弄一个初始值,然后求这个隐含变量的期望,当成是这个隐含变量的已知值,那么现在就可以用最大似然求解那个分布的参数了吧,那假设这个参数比之前的那个随机的参数要好,它更能表达真实的分布,那么我们再通过这个参数确定的分布去求这个隐含变量的期望,然后再最大化,得到另一个更优的参数,……迭代,就能得到一个皆大欢喜的结果了。
这时候你就不服了,说你老迭代迭代的,你咋知道新的参数的估计就比原来的好啊?为什么这种方法行得通呢?有没有失效的时候呢?什么时候失效呢?用到这个方法需要注意什么问题呢?呵呵,一下子抛出那么多问题,搞得我适应不过来了,不过这证明了你有很好的搞研究的潜质啊。呵呵,其实这些问题就是数学家需要解决的问题。在数学上是可以稳当的证明的或者得出结论的。那咱们用数学来把上面的问题重新描述下。(在这里可以知道,不管多么复杂或者简单的物理世界的思想,都需要通过数学工具进行建模抽象才得以使用并发挥其强大的作用,而且,这里面蕴含的数学往往能带给你更多想象不到的东西,这就是数学的精妙所在啊)。

4.6.2.4 EM算法推导

假设我们有一个样本集{x(1),…,x(m)},包含m个独立的样本。但每个样本i对应的类别z(i)是未知的(如,不知道它属于哪种分布),也即隐含变量。故我们需要估计概率模型p(x,z)的参数θ,但是由于里面包含隐含变量z,所以很难用最大似然求解,但如果z知道了,那我们就很容易求解了。
对于参数估计,我们本质上还是想获得一个使似然函数最大化的那个参数θ,现在与最大似然不同的只是似然函数式中多了一个未知的变量z,见下式(1)。也就是说我们的目标是找到适合的θ和z让L(θ)最大。那我们也许会想,你就是多了一个未知的变量而已啊,我也可以分别对未知的θ和z分别求偏导,再令其等于0,求解出来不也一样吗?

本质上我们是需要最大化(1)式(对(1)式,我们回忆下联合概率密度下某个变量的边缘概率密度函数的求解,注意这里z也是随机变量。对每一个样本i的所有可能类别z求等式右边的联合概率密度函数和,也就得到等式左边为随机变量x的边缘概率密度),也就是似然函数,但是可以看到里面有“和的对数”,求导后形式会非常复杂(自己可以想象下log(f1(x)+ f2(x)+ f3(x)+…)复合函数的求导),所以很难求解得到未知参数z和θ。那OK,我们可否对(1)式做一些改变呢?我们看(2)式,(2)式只是分子分母同乘以一个相等的函数,还是有“和的对数”啊,还是求解不了,那为什么要这么做呢?咱们先不管,看(3)式,发现(3)式变成了“对数的和”,那这样求导就容易了。我们注意点,还发现等号变成了不等号,为什么能这么变呢?这就是Jensen不等式的大显神威的地方。

4.6.2.5Jensen不等式

设f是定义域为实数的函数,如果对于所有的实数x。如果对于所有的实数x,f(x)的二次导数大于等于0,那么f是凸函数。当x是向量时,如果其hessian矩阵H是半正定的,那么f是凸函数。如果只大于0,不等于0,那么称f是严格凸函数。
Jensen不等式表述如下:
如果f是凸函数,X是随机变量,那么:E[f(X)]>=f(E[X])
特别地,如果f是严格凸函数,当且仅当X是常量时,上式取等号。
如果用图表示会很清晰:

图中,实线f是凸函数,X是随机变量,有0.5的概率是a,有0.5的概率是b。(就像掷硬币一样)。X的期望值就是a和b的中值了,图中可以看到E[f(X)]>=f(E[X])成立。
当f是(严格)凹函数当且仅当-f是(严格)凸函数。
Jensen不等式应用于凹函数时,不等号方向反向。
回到公式(2),因为f(x)=log x为凹函数(其二次导数为-1/x2<0)。(2)式中

的期望,(考虑到E(X)=∑x*p(x),f(X)是X的函数,则E(f(X))=∑f(x)*p(x)),又 ,所以就可以得到公式(3)的不等式了。

4.6.2.6 EM算法运算步骤4.6.2.7代码实现

4.6.2.7.1 双硬币模型

假设有两枚硬币A、B,以相同的概率随机选择一个硬币,进行如下的抛硬币实验:共做5次实验,每次实验独立的抛十次,结果如图中a所示,例如某次实验产生了H、T、T、T、H、H、T、H、T、H,H代表正面朝上。
假设试验数据记录员可能是实习生,业务不一定熟悉,造成a和b两种情况
a表示实习生记录了详细的试验数据,我们可以观测到试验数据中每次选择的是A还是B
b表示实习生忘了记录每次试验选择的是A还是B,我们无法观测实验数据中选择的硬币是哪个。问在两种情况下分别如何估计两个硬币正面出现的概率?

这是实习生a记录的情况,由于这里数据的背后参考模型已知(已分好类),因此用极大似然估计方法就能分别求出θ ̂_A和θ ̂_B的概率来。与上文第一节中的例子完全类似。


上图为实习生b记录的情况,令人遗憾的是数据的背后参考模型混淆在了一起,我们无法得知它们这几组实验数据是由A抛的还是由B抛的,因为这里隐含了一个该组数据是A还是B的分类问题。抛开数学对隐含变量的复杂求解过程,我们可以先给出一个思路来解决上述问题。
第一,既然我们不清楚是A类还是B类,但假定我们初始化了A类硬币抛正面的概率和B类硬币抛正面的概率,这两者的概率是随意定的,但由于两者概率可以存在差异,假设P(y=H;θA)>P(y=H;θB),那么一个明显的特征就是,由于能观察到10次硬币中有几次是成功的,我们可以基于这次观察,给出P(z=A|Y;θA,θB)的概率,上式的含义是可以根据两个参数的初值求出,在给定观察序列的情况下,它属于A类还是B类的概率。用公式可以表示为:

其中,z表示单个观察到的随机变量,此处z=A or B(属于分类问题),Y表示观察序列,即Y=(y1,y2,...,y10)T。由此,给定观察序列后,我们可以算出属于A类的概率和属于B类的概率,那么很显然CoinA 和CoinB 不再是属于你有我没有,你没有我有的敌对关系,因为我自己都还不是很清楚是不是A类,由此10个硬币,我们就根据概率进行一次平均分配咯,这样CoinA 和CoinB 在一次观察结果时,都能得到属于自己的那一份,非常的和谐。这一部分便是求期望的过程,即对于第一个观察序列中,10次抛硬币过程中5次为正面朝上,令yj=5,由此可以得到关于隐含变量的数学期望E(z)=0.45*5+0.55*5,其中0.45*5表示CoinA的分配; 0.55*5表示CoinB的分配。分配的份额根据z函数的分布给定,z函数的分布规则根据缺失的信息进行建模,解由初始参数求出。
因此分类问题,给出了每个CoinA 和CoinB 的分配额,有了所有观察值CoinA和CoinB的分配额,我们就可以单独的对CoinA和CoinB进行最大似然估计方法。求出来的新参数,再求z函数,求期望,求参数,如此迭代下去,直到收敛到某一结果。

4.6.2.7.2导入必要的库

4.6.2.7.2编写伯努利分布函数

在双硬币模型中,对某个种类硬币投掷10次中成功n次概率模型

符合伯努利分布

可视化伯努利分布

运行结果如下:

这是在p=0.2的情况下的伯努利分布函数,代回双硬币模型中去,当观察到10次实验中只有2次成功了,那么该θ参数便是0.2。因为只有当θ=0.2时,10次实验中出现成功次数为2次的概率最大

4.6.2.7.3 定义观察矩阵

由数据可得观察矩阵为

有实习生a记录的信息可知,实际每组观察数据属于A类,B类的隐藏状态为:

那么在观察数组中,属于A类的数组为:

运行结果为:
array([[1, 1, 1, 1, 0, 1, 1, 1, 1, 1],
[1, 0, 1, 1, 1, 1, 1, 0, 1, 1],
[0, 1, 1, 1, 0, 1, 1, 1, 0, 1]])
在所有属于A类的数组中,总的实验成功次数为:

运行结果为:24
所以说,针对属于A类的硬币,它的参数θ_A:

运行结果为:0.80000000000000004

同理,对于属于B类的硬币,它的参数为θ_B:

运行结果为:0.45000000000000001

4.6.2.7.4 EM算法步骤

(1)首先来看看,针对第一组观察数据,每一步的数据是如何求出的。
# 对于实验成功率为0.6的情况,10次中成功5次的概率

(2)单步EM算法,迭代一次算法实现步骤。

(3)迭代一次输出结果为:

运行结果为:[0.71301223540051617, 0.58133930831366265]

4.6.2.7.5 EM主循环

给定循环的两个终止条件:模型参数变化小于阈值;循环达到最大次数,就可以写出EM算法的主循环了。

最终结果为:

运行结果为:
[[0.79678875938310978, 0.51958393567528027], 14]
我们可以改变初值,试验初值对EM算法的影响。

运行结果为:
[[0.79678843908109542, 0.51957953211429142], 11]
EM算法对于参数的改变还是有一定的健壮性的。

最终实习生b的EM算法得出的结果,跟实习生a得出的参数还是非常相近的,但EM算法跟初始值的设置有着很大的关系,不信,修改[06,0.5]多尝试尝试。

运行结果为:
[[0.51958345063012845, 0.79678895444393927], 15]

参考:
http://blog.csdn.net/u014688145/article/details/53073266
http://blog.csdn.net/zouxy09/article/details/8537620
http://blog.csdn.net/lilynothing/article/details/64443563

第8章 一种全新的读取数据方式 Dataset API

8.1 Dataset API简介

Dataset API可以用简单复用的方式构建复杂的Input Pipeline。例如:一个图片模型的Pipeline可能会聚合在一个分布式文件系统中的多个文件,对每个图片进行随机扰动(random perturbations),接着将随机选中的图片合并到一个training batch中。一个文本模型的Pipeline可能涉及到:从原始文本数据中抽取特征,并通过一个转换(Transformation)将不同的长度序列batch在一起。Dataset API可以很方便地以不同的数据格式处理大量的数据,以及处理复杂的转换。
Dataset API是TensorFlow 1.3版本中引入的一个新的模块,主要用于数据读取,构建输入数据的pipeline等。之前,在TensorFlow中读取数据一般有两种方法:
 使用placeholder读内存中的数据
 使用queue读硬盘中的数据
Dataset API同时支持从内存和硬盘的读取,相比之前的两种方法在语法上更加简洁易懂。
此外,如果想要使用TensorFlow新出的Eager模式,就必须要使用Dataset API来读取数据。
Dataset API的导入,在TensorFlow 1.3中,Dataset API是放在contrib包中的:
tf.contrib.data.Dataset,而在TensorFlow 1.4中,Dataset API已经从contrib包中移除,变成了核心API的一员:tf.data.Dataset

8.2 Dataset API 架构

图1 Dataset API架构图

Dataset API引入了两个新的抽象类到Tensorflow中:

 tf.data.Dataset
表示一串元素(elements),其中每个元素包含了一或多个Tensor对象。例如:在一个图片pipeline中,一个元素可以是单个训练样本,它们带有一个表示图片数据的tensors和一个label组成的pair。有两种不同的方式创建一个dataset:
(1)创建一个source (例如:Dataset.from_tensor_slices()), 从一或多个tf.Tensor对象中构建一个dataset
(2)应用一个transformation(例如:Dataset.batch()),从一或多个tf.contrib.data.Dataset对象上构建一个dataset
 tf.data.Iterator
它提供了主要的方式来从一个dataset中抽取元素。通过Iterator.get_next() 返回的该操作会yields出Datasets中的下一个元素,作为输入pipeline和模型间的接口使用。最简单的iterator是一个“one-shot iterator”,它与一个指定的Dataset相关联,通过它来进行迭代。对于更复杂的使用,Iterator.initializer操作可以使用不同的datasets重新初始化(reinitialize)和参数化(parameterize)一个iterator ,例如,在同一个程序中通过training data和validation data迭代多次。
以下为生成Dataset的常用方法:
(1)tf.data.Dataset.from_tensor_slices()
利用tf.data.Dataset.from_tensor_slices()从一或多个tf.Tensor对象中构建一个dataset,
其tf.Tensor对象中包括数组、矩阵、字典、元组等,具体实例如下:

import tensorflow as tf
import numpy as np

arry1=np.array([1.0, 2.0, 3.0, 4.0, 5.0])
dataset = tf.data.Dataset.from_tensor_slices(arry1)
#生成实例
iterator = dataset.make_one_shot_iterator()
#从iterator里取出一个元素
one_element = iterator.get_next()
with tf.Session() as sess:
for i in range(len(arry1)):
print(sess.run(one_element))

运行结果为:1,2,3,4,5

(2)Dataset的转换(transformations)
支持任何结构的datasets当使用Dataset.map(),Dataset.flat_map(),以及Dataset.filter()
Dataset支持一类特殊的操作:Transformation。一个Dataset通过Transformation变成一个新的Dataset。通常我们可以通过Transformation完成数据变换,常用的Transformation有:
map()、flat_map()、filter()、filter()、shuffle()、repeat()、tf.py_func()等等。以下是一些简单示例:

import tensorflow as tf
import numpy as np

a1=np.array([1.0, 2.0, 3.0, 4.0, 5.0])
dataset = tf.data.Dataset.from_tensor_slices(a1)
dataset = dataset.map(lambda x: x * 2) # 2.0, 3.0, 4.0, 5.0, 6.0
iterator = dataset.make_one_shot_iterator()
#从iterator里取出一个元素
one_element = iterator.get_next()
with tf.Session() as sess:
for i in range(len(a1)):
print(sess.run(one_element))

flat_map()、filter()等的使用

#使用<code>Dataset.flat_map()</code>将每个文件转换为一个单独的嵌套数据集
#然后将它们的内容顺序连接成一个单一的“扁平”数据集
#跳过第一行(标题行)
#滤除以“#”开头的行(注释)
filenames = ["/var/data/file1.txt", "/var/data/file2.txt"]

dataset = tf.data.Dataset.from_tensor_slices(filenames)

dataset = dataset.flat_map(
lambda filename: (
tf.data.TextLineDataset(filename)
.skip(1)
.filter(lambda line: tf.not_equal(tf.substr(line, 0, 1), "#"))))

batch()、shuffle()、repeat()

filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)
dataset = dataset.shuffle(buffer_size=10000)
dataset = dataset.batch(32)
dataset = dataset.repeat(4)

(3)tf.data.TextLineDataset()
很多数据集是一个或多个文本文件。tf.contrib.data.TextLineDataset提供了一种简单的方式来提取这些文件的每一行。给定一个或多个文件名,TextLineDataset会对这些文件的每行生成一个值为字符串的元素。TextLineDataset也可以接受tf.Tensor作为filenames,所以你可以传递一个tf.placeholder(tf.string)作为参数。这个函数的输入是一个文件的列表,输出是一个dataset。dataset中的每一个元素就对应了文件中的一行。可以使用这个函数来读入CSV文件

filenames = ["/var/data/file1.txt", "/var/data/file2.txt"]
dataset = tf.data.TextLineDataset(filenames)

默认下,TextLineDataset生成每个文件中的每一行,这可能不是你所需要的,例如文件中有标题行,或包含注释。可以使用Dataset.skip()和Dataset.filter()来剔除这些行。为了对每个文件都各自应用这些变换,使用Dataset.flat_map()来对每个文件创建一个嵌套的Dataset。

filenames = ["/var/data/file1.txt", "/var/data/file2.txt"]
dataset = tf.contrib.data.Dataset.from_tensor_slices(filenames)
# Use <code>Dataset.flat_map()</code> to transform each file as a separate nested dataset,
# and then concatenate their contents sequentially into a single "flat" dataset.
# * Skip the first line (header row).
# * Filter out lines beginning with "#" (comments).
dataset = dataset.flat_map(
lambda filename: (
tf.contrib.data.TextLineDataset(filename)
.skip(1)
.filter(lambda line: tf.not_equal(tf.substr(line, 0, 1), "#"))))

(4)tf.data.FixedLengthRecordDataset():
这个函数的输入是一个文件的列表和一个record_bytes,之后dataset的每一个元素就是文件中固定字节数record_bytes的内容。通常用来读取以二进制形式保存的文件,如CIFAR10数据集就是这种形式。

(5)tf.data.TFRecordDataset():
TFRecord是一种面向记录的二进制文件,很多TensorFlow应用使用它作为训练数据。tf.contrib.data.TFRecordDataset能够使TFRecord文件作为输入管道的输入流。

filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
dataset = tf.data.TFRecordDataset(filenames)

传递给TFRecordDataset的参数filenames可以是字符串,字符串列表或tf.Tensor类型的字符串。因此,如果有两组文件分别作为训练和验证,可以使用tf.placeholder(tf.string)来表示文件名,使用适当的文件名来初始化一个迭代器。

filenames = tf.placeholder(tf.string, shape=[None])
dataset = tf.data.TFRecordDataset(filenames)
iterator = dataset.make_initializable_iterator()

training_filenames = ["/var/data/file1.tfrecord", "/var/data/file2.tfrecord"]
with tf.Session() as sess:
sess.run(iterator.initializer, feed_dict={filenames: training_filenames})
# Initialize <code>iterator</code> with validation data.
validation_filenames = ["/var/data/validation1.tfrecord", ...]
sess.run(iterator.initializer, feed_dict={filenames: validation_filenames})

8.3 使用Dataset Tensor实例

以上我们通过一个实例来介绍Dataset API的具体使用,实例内容用MNIST数据集为原数据,使用卷积神经网络,对手写0-9十个数字进行识别。
环境配置信息为:Python3.6,Tensorflow1.3,使用CPU
具体步骤如下:
 定义获取、预处理数据集的代码
 加载数据
 创建Dataset Tensor
 创建卷积神经网络
 训练及评估模型

8.3.1.导入需要的库

import os
import struct
import numpy as np
import tensorflow as tf

8.3.2.定义一个把标签变为热编码(one-hot)的函数

def dense_to_one_hot(labels_dense, num_classes=10):
"""Convert class labels from scalars to one-hot vectors."""
num_labels = labels_dense.shape[0]
index_offset = np.arange(num_labels) * num_classes
labels_one_hot = np.zeros((num_labels, num_classes))
labels_one_hot.flat[index_offset + labels_dense.ravel()] = 1
return labels_one_hot

8.3.3.定义加载数据函数

def load_mnist(path, kind='train'):
"""Load MNIST data from path"""
labels_path = os.path.join(path, '%s-labels-idx1-ubyte' % kind)
images_path = os.path.join(path, '%s-images-idx3-ubyte' % kind)

with open(labels_path, 'rb') as lbpath:
magic, n = struct.unpack('>II',lbpath.read(8))
labels = np.fromfile(lbpath, dtype=np.uint8)
labels=dense_to_one_hot(labels)

with open(images_path, 'rb') as imgpath:
magic, num, rows, cols = struct.unpack(">IIII",imgpath.read(16))
images = np.fromfile(imgpath, dtype=np.uint8).reshape(len(labels), 784)
#images = np.fromfile(imgpath, dtype=np.float32).reshape(len(labels), 784)

return images, labels

8.3.4 加载数据

import matplotlib.pyplot as plt
%matplotlib inline

X_train, y_train = load_mnist('./data/mnist/', kind='train')
print('Rows: %d, columns: %d' % (X_train.shape[0], X_train.shape[1]))
print('Rows: %d, columns: %d' % ( y_train.shape[0], y_train.shape[1]))

X_test, y_test = load_mnist('./data/mnist/', kind='t10k')
print('Rows: %d, columns: %d' % (X_test.shape[0], X_test.shape[1]))

运行结果:
Rows: 60000, columns: 784
Rows: 60000, columns: 10
Rows: 10000, columns: 784

8.3.5 定义参数

# Parameters
learning_rate = 0.001
num_steps = 2000
batch_size = 128
display_step = 100

# Network Parameters
n_input = 784 # MNIST data input (img shape: 28*28)
n_classes = 10 # MNIST total classes (0-9 digits)
dropout = 0.75 # Dropout, probability to keep units

8.3.6 创建Dataset Tensor

sess = tf.Session()

# Create a dataset tensor from the images and the labels
dataset = tf.contrib.data.Dataset.from_tensor_slices(
(X_train.astype(np.float32),y_train.astype(np.float32)))
# Create batches of data
dataset = dataset.batch(batch_size)
# Create an iterator, to go over the dataset
iterator = dataset.make_initializable_iterator()
# It is better to use 2 placeholders, to avoid to load all data into memory,
# and avoid the 2Gb restriction length of a tensor.
_data = tf.placeholder(tf.float32, [None, n_input])
_labels = tf.placeholder(tf.float32, [None, n_classes])
# Initialize the iterator
sess.run(iterator.initializer, feed_dict={_data: X_train.astype(np.float32),
_labels: y_train.astype(np.float32)})

# Neural Net Input
X, Y = iterator.get_next()

8.3.7 创建卷积神经网络模型

# Create model
def conv_net(x, n_classes, dropout, reuse, is_training):
# Define a scope for reusing the variables
with tf.variable_scope('ConvNet', reuse=reuse):
# MNIST data input is a 1-D vector of 784 features (28*28 pixels)
# Reshape to match picture format [Height x Width x Channel]
# Tensor input become 4-D: [Batch Size, Height, Width, Channel]
x = tf.reshape(x, shape=[-1, 28, 28, 1])

# Convolution Layer with 32 filters and a kernel size of 5
conv1 = tf.layers.conv2d(x, 32, 5, activation=tf.nn.relu)
# Max Pooling (down-sampling) with strides of 2 and kernel size of 2
conv1 = tf.layers.max_pooling2d(conv1, 2, 2)

# Convolution Layer with 32 filters and a kernel size of 5
conv2 = tf.layers.conv2d(conv1, 64, 3, activation=tf.nn.relu)
# Max Pooling (down-sampling) with strides of 2 and kernel size of 2
conv2 = tf.layers.max_pooling2d(conv2, 2, 2)

# Flatten the data to a 1-D vector for the fully connected layer
fc1 = tf.contrib.layers.flatten(conv2)

# Fully connected layer (in contrib folder for now)
fc1 = tf.layers.dense(fc1, 1024)
# Apply Dropout (if is_training is False, dropout is not applied)
fc1 = tf.layers.dropout(fc1, rate=dropout, training=is_training)

# Output layer, class prediction
out = tf.layers.dense(fc1, n_classes)
# Because 'softmax_cross_entropy_with_logits' already apply softmax,
# we only apply softmax to testing network
out = tf.nn.softmax(out) if not is_training else out

return out

8.3.8 训练及评估模型

# Because Dropout have different behavior at training and prediction time, we
# need to create 2 distinct computation graphs that share the same weights.

# Create a graph for training
logits_train = conv_net(X, n_classes, dropout, reuse=False, is_training=True)
# Create another graph for testing that reuse the same weights, but has
# different behavior for 'dropout' (not applied).
logits_test = conv_net(X, n_classes, dropout, reuse=True, is_training=False)

# Define loss and optimizer (with train logits, for dropout to take effect)
loss_op = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
logits=logits_train, labels=Y))
optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
train_op = optimizer.minimize(loss_op)

# Evaluate model (with test logits, for dropout to be disabled)
correct_pred = tf.equal(tf.argmax(logits_test, 1), tf.argmax(Y, 1))
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))

# Initialize the variables (i.e. assign their default value)
init = tf.global_variables_initializer()

sess.run(init)

for step in range(1, num_steps + 1):

try:
# Run optimization
sess.run(train_op)
except tf.errors.OutOfRangeError:
# Reload the iterator when it reaches the end of the dataset
sess.run(iterator.initializer,
feed_dict={_data: X_train.astype(np.float32),
_labels: y_train.astype(np.float32)})
sess.run(train_op)

if step % display_step == 0 or step == 1:
# Calculate batch loss and accuracy
# (note that this consume a new batch of data)
loss, acc = sess.run([loss_op, accuracy])
print("Step " + str(step) + ", Minibatch Loss= " + \
"{:.4f}".format(loss) + ", Training Accuracy= " + \
"{:.3f}".format(acc))

print("Optimization Finished!")

运行结果如下:
Step 1, Minibatch Loss= 182.4177, Training Accuracy= 0.258
Step 100, Minibatch Loss= 0.6034, Training Accuracy= 0.891
Step 200, Minibatch Loss= 0.4140, Training Accuracy= 0.930
Step 300, Minibatch Loss= 0.0813, Training Accuracy= 0.977
Step 400, Minibatch Loss= 0.1380, Training Accuracy= 0.969
Step 500, Minibatch Loss= 0.1193, Training Accuracy= 0.945
Step 600, Minibatch Loss= 0.3291, Training Accuracy= 0.953
Step 700, Minibatch Loss= 0.2158, Training Accuracy= 0.969
Step 800, Minibatch Loss= 0.1293, Training Accuracy= 0.977
Step 900, Minibatch Loss= 0.1323, Training Accuracy= 0.977
Step 1000, Minibatch Loss= 0.2017, Training Accuracy= 0.961
Step 1100, Minibatch Loss= 0.1555, Training Accuracy= 0.961
Step 1200, Minibatch Loss= 0.0744, Training Accuracy= 0.992
Step 1300, Minibatch Loss= 0.1331, Training Accuracy= 0.969
Step 1400, Minibatch Loss= 0.1279, Training Accuracy= 0.977
Step 1500, Minibatch Loss= 0.0733, Training Accuracy= 0.984
Step 1600, Minibatch Loss= 0.1529, Training Accuracy= 0.969
Step 1700, Minibatch Loss= 0.1223, Training Accuracy= 0.977
Step 1800, Minibatch Loss= 0.0503, Training Accuracy= 0.992
Step 1900, Minibatch Loss= 0.1077, Training Accuracy= 0.977
Step 2000, Minibatch Loss= 0.0344, Training Accuracy= 0.992
Optimization Finished!

参考博客:
https://www.leiphone.com/news/201711/zV7yM5W1dFrzs8W5.html
http://d0evi1.com/tensorflow/datasets/

第6章 安装TensorFlow

这里主要介绍基于Linux下的TensorFlow安装,TensorFlow的安装又分为CPU版和GPU版的。使用CPU的相对简单一些,无需安装GPU相关驱动及CUDA、cuDNN等。不过无论哪种安装,我们推荐使用Anaconda作为Python环境,因为这样可以避免大量的兼容性问题,而且使用其中的conda进行后续程序更新非常方便。这里使用Python3.6,TensorFlow为1.4。

6.1 TensorFlow CPU版的安装

TensorFlow的CPU版安装比较简单,可以利用编译好的版本或使用源码安装,推荐使用编译好的进行安装,如果用户环境比较特殊,如gcc高于6版本或不支持编译好的版本,才推荐采用源码安装。采用编译好的进行安装,具体步骤如下:
(1)从Anaconda的官网(https://www.anaconda.com/)下载Anaconda3的最新版本,如Anaconda3-5.0.1-Linux-x86_64.sh,建议使用3系列,3系列代表未来发展。 另外,下载时根据自己环境,选择操作系统、对应版本64位版本。
(2)在Anaconda所在目录,执行如下命令:

bash Anaconda3-5.0.1-Linux-x86_64.sh

(3)接下来根据会看到安装提示,直接按回车即可,然后,会提示选择安装路径,如果没有特殊要求,可以按回车使用默认路径,然后就开始安装。
(4)安装完成后,程序提示我们是否把anaconda3的binary路径加入到当前用户的.bashrc配置文件中,建议添加,添加以后,就可以使用python、ipython命令时自动使用Anaconda3的python3.6环境。
(5)使用conda进行安装

conda install tensorflow

(6)验证安装是否成功

6.2 TensorFlow GPU版的安装

TensorFlow的GPU版本安装相对步骤更多一些,这里采用一种比较简洁的方法。目前TensorFlow对CUDA支持比较好,所以要安装GPU版本的首先需要一块或多块GPU显卡,显卡一般采用NVIDIA显卡,AMD的显卡只能使用实验性支持的OpenCL,效果不是很好。
接下来我们需要安装:
 显卡驱动
 CUDA
 cuDNN
其中CUDA(Compute Unified Device Architecture),是英伟达公司推出的一种基于新的并行编程模型和指令集架构的通用计算架构,它能利用英伟达GPU的并行计算引擎,比CPU更高效的解决许多复杂计算任务。NVIDIA cuDNN是用于深度神经网络的GPU加速库。它强调性能、易用性和低内存开销。NVIDIA cuDNN可以集成到更高级别的机器学习框架中,其插入式设计可以让开发人员专注于设计和实现神经网络模型,而不是调整性能,同时还可以在GPU上实现高性能现代并行计算,目前大部分深度学习框架使用cuDNN来驱动GPU计算。以下为在ubuntu16.04版本上安装TensorFlow1.4版本的具体步骤。
(1)首先安装显卡驱动,首先看显卡信息。

lspci | grep -i vga

(2)查是否已安装驱动

lsmod | grep -i nvidia

(3)更新apt-get

sudo apt-get update

(4)安装一些依赖库

sudo apt-get install openjdk-8-jdk git python-dev python3-dev python-numpy python3-numpy build-essential python-pip python3-pip python-virtualenv swig python-wheel libcurl3-dev

(5)安装 nvidia 驱动

curl -O http://developer.download.nvidia.com/compute/cuda/repos/ubuntu1604/x86_64/cuda-repo-ubuntu1604_8.0.61-1_amd64.deb
sudo dpkg -i ./cuda-repo-ubuntu1604_8.0.61-1_amd64.deb
sudo apt-get update
sudo apt-get install cuda -y

(6)检查驱动安装是否成功

nvidia-smi

运行结果如下

说明驱动安装成功。

(7)安装cuda toolkit(在提示是否安装驱动时,选择 n,即不安装)

(8)安装cudnn

(9)把以下内容添加到~/.bashrc

export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/cuda/lib64:/usr/local/cuda/extras/CUPTI/lib64"
export CUDA_HOME=/usr/local/cuda

(10)使环境变量立即生效

source ~/.bashrc

(11)安装Anaconda
先从Anaconda官网下载最新版本,然后,在文件所在目录运行如下命令。

bash Anaconda3-5.0.1-Linux-x86_64.sh

Anaconda的详细安装可参考TensorFlow CPU版本的说明。
(12)使环境变量立即生效

source ~/.bashrc

(13)创建 conda 环境为安装tensorflow

conda create -n tensorflow
# press y a few times

(14)激活环境

source activate tensorflow

(15)安装tensorflow -GPU

pip install tensorflow-gpu

(16)验证安装是否成功

import tensorflow as tf

hello = tf.constant('Hello, TensorFlow!')
sess = tf.Session()
print(sess.run(hello))

6.3远程访问Jupyter Notebook

(1)生成配置文件

jupyter notebook --generate-config

(2)生成当前用户登录jupyter密码
打开ipython, 创建一个密文密码

In [1]: from notebook.auth import passwd
In [2]: passwd()
Enter password:
Verify password:

(3)修改配置文件

vim ~/.jupyter/jupyter_notebook_config.py

进行如下修改:

c.NotebookApp.ip='*' # 就是设置所有ip皆可访问
c.NotebookApp.password = u'sha:ce...刚才复制的那个密文'
c.NotebookApp.open_browser = False # 禁止自动打开浏览器
c.NotebookApp.port =8888 #这是缺省端口,也可指定其它端口

(4)启动jupyter notebook

#后台启动jupyter:不记日志:
nohup jupyter notebook >/dev/null 2>&1 &

然在浏览器上,输入IP:port,即可看到如下类似界面。

然后,我们就可以在浏览器进行开发调试Python或Tensorflow程序了。

6.4比较CPU与GPU 性能

(1)设备设为GPU

2.50004e+11

Shape: (10000, 10000) Device: /gpu:0
Time taken: 0:00:02.605461
(2)把设备改为CPU
把设备改为cpu,运行以上代码,其结果如下:
2.50199e+11

Shape: (10000, 10000) Device: /cpu:0
Time taken: 0:00:07.232871

这个实例运算较简单,但即使简单,GPU也是CPU的近3倍。GPU还是非常不错的。

6.5单GPU与多GPU 性能比较

这里比较使用一个GPU与使用多个(如两个)GPU的性能比较。

运行结果:
Single GPU computation time: 0:00:23.821055
Multi GPU computation time: 0:00:12.078067

第7 章 TensorFlow基础

7.1TensorFlow简介

TensorFlow是谷歌基于DistBelief进行研发的第二代人工智能学习系统,采用数据流图(data flow graphs),用于数值计算的开源软件库。节点(Nodes)在图中表示数学操作,图中的线(edges)则表示在节点间相互联系的多维数据数组,即Tensor(张量),而Flow(流)意味着基于数据流图的计算,TensorFlow为张量从流图的一端流动到另一端计算过程。TensorFlow不只局限于神经网络,其数据流式图支持非常自由的算法表达,当然也可以轻松实现深度学习以外的机器学习算法。事实上,只要可以将计算表示成计算图的形式,就可以使用TensorFlow。
TensorFlow可被用于语音识别或图像识别等多项机器深度学习领域,TensorFlow一大亮点是支持异构设备分布式计算,它能够在各个平台上自动运行模型,从手机、单个CPU / GPU到成百上千GPU卡组成的分布式系统。

7.2TensorFlow的安装

安装TensorFlow,因本环境的python2.7采用anaconda来安装,故这里采用conda管理工具来安装TensorFlow,目前conda缺省安装版本为TensorFlow 1.3。

conda install tensorflow

验证安装是否成功,可以通过导入tensorflow来检验。
启动ipython(或python)

import tensorflow as tf

7.3TensorFlow的发展

2015年11月9日谷歌开源了人工智能系统TensorFlow,同时成为2015年最受关注的开源项目之一。TensorFlow的开源大大降低了深度学习在各个行业中的应用难度。TensorFlow的近期里程碑事件主要有:
2016年04月:发布了分布式TensorFlow的0.8版本,把DeepMind模型迁移到TensorFlow;
2016年06月:TensorFlow v0.9发布,改进了移动设备的支持;
2016年11月:TensorFlow开源一周年;
2017年2月:TensorFlow v1.0发布,增加了Java、Go的API,以及专用的编译器和调试工具,同时TensorFlow 1.0引入了一个高级API,包含tf.layers,tf.metrics和tf.losses模块。还宣布增了一个新的tf.keras模块,它与另一个流行的高级神经网络库Keras完全兼容。
2017年4月:TensorFlow v1.1发布,为 Windows 添加 Java API 支,添加 tf.spectral 模块, Keras 2 API等;
2017年6月:TensorFlow v1.2发布,包括 API 的重要变化、contrib API的变化和Bug 修 复及其它改变等。
2017年7月:TensorFlow v1.3发布,tf.contrib.data.Dataset类、Tensorflow又在库中增加了 下列函数:DNNClassifier、DNNRegressor、LinearClassifer、LinearRegressor、 DNNLinearCombinedClassifier、DNNLinearCombinedRegressor。这些estimator 是tf.contrib.learn包的一部分。
2017年11月:TensorFlow v1.4发布,tf.keras、tf.data 现在是核心 TensorFlow API 的一部 分;添加 train_and_evaluate 用于简单的分布式 Estimator 处理。

7.4TensorFlow的特点

 高度的灵活性
TensorFlow 采用数据流图,用于数值计算的开源软件库。只要计算能表示为一个数据 流图,你就可以使用Tensorflow。
 真正的可移植性
Tensorflow 在CPU和GPU上运行,可以运行在台式机、服务器、云服务器、手机移动 设备、Docker容器里等等。
 将科研和产品联系在一起
过去如果要将科研中的机器学习想法用到产品中,需要大量的代码重写工作。Tensorflow 将改变这一点。使用Tensorflow可以让应用型研究者将想法迅速运用到产品中,也可以 让学术性研究者更直接地彼此分享代码,产品团队则用Tensorflow来训练和使用计算模 型,并直接提供给在线用户,从而提高科研产出率。
 自动求微分
基于梯度的机器学习算法会受益于Tensorflow自动求微分的能力。使用Tensorflow,只 需要定义预测模型的结构,将这个结构和目标函数(objective function)结合在一起,
并添加数据,Tensorflow将自动为你计算相关的微分导数。
 多语言支持
Tensorflow 有一个合理的c++使用界面,也有一个易用的python使用界面来构建和执 行你的graphs。你可以直接写python/c++程序,也可以用交互式的Ipython界面来用 Tensorflow尝试这些想法,也可以使用Go,Java,Lua,Javascript,或者是R等语言。
 性能最优化
如果你有一个32个CPU内核、4个GPU显卡的工作站,想要将你工作站的计算潜能 全发挥出来,由于Tensorflow 给予了线程、队列、异步操作等以最佳的支持,Tensorflow 让你可以将你手边硬件的计算潜能全部发挥出来。你可以自由地将Tensorflow图中的计 算元素分配到不同设备上,充分利用这些资源。
下表为TensorFlow的一些主要技术特征:

表7.1 TensorFlow的主要技术特征

7.5TensorFlow总体介绍

使用 TensorFlow, 你必须明白 TensorFlow:
 使用图 (graph) 来表示计算任务。
 在被称之为 会话 (Session) 的上下文 (context) 中执行图。
 使用 tensor 表示数据。
 通过 变量 (Variable) 维护状态。
 使用 feed 和 fetch 可以为任意的操作(arbitrary operation)赋值或者从其中获取数据。
一个 TensorFlow 图描述了计算的过程。为了进行计算, 图必须在会话里被启动. 会话将图的 op 分发到诸如 CPU 或 GPU 之类的设备 上, 同时提供执行 op 的方法. 这些方法执行后, 将产生的 tensor 返回。在 Python 语言中, 返回的 tensor 是 numpy ndarray 对象; 在 C 和 C++ 语言中, 返回的 tensor 是tensorflow::Tensor 实例。

7.6TensorFlow编程示例

实际上编写tensorflow可以总结为两步:
(1)组装一个graph;
(2)使用session去执行graph中的operation。
因此我们从 graph 与 session 说起。

7.6.1 graph与session

(1)计算图
Tensorflow 是基于计算图的框架,因此理解 graph 与 session 显得尤为重要。不过在讲解 graph 与 session 之前首先介绍下什么是计算图。假设我们有这样一个需要计算的表达式。该表达式包括了两个加法与一个乘法,为了更好讲述引入中间变量c与d。由此该表达式可以表示为:

图 7.1 数据流图
当需要计算e时就需要计算c与d,而计算c就需要依赖输入值a与b,计算d需要依赖输入值a与b。这样就形成了依赖关系。这种有向无环图就叫做计算图,因为对于图中的每一个节点其微分都很容易得出,因此应用链式法则求得一个复杂的表达式的导数就成为可能,所以它会应用在类似tensorflow这种需要应用反向传播算法的框架中。
(2)概念说明
下面是 graph , session , operation , tensor 四个概念的简介。
1)Tensor:类型化的多维数组,图的边;边(edge)对应于向操作(operation)传入和从operation传出的实际数值,通常以箭头表示。
2)Operation:执行计算的单元,图的节点,节点(node)通常以圆圈、椭圆和方框等来表示,代表了对数据所做的运算或某种操作,在上图中,“add”、“mul”为运算节点。
3)Graph:一张有边与点的图,其表示了需要进行计算的任务;
4)Session:称之为会话的上下文,用于执行图。

Graph仅仅定义了所有 operation 与 tensor 流向,没有进行任何计算。而session根据 graph 的定义分配资源,计算 operation,得出结果。既然是图就会有点与边,在图计算中 operation 就是点而 tensor 就是边。Operation 可以是加减乘除等数学运算,也可以是各种各样的优化算法。每个 operation 都会有零个或多个输入,零个或多个输出。 tensor 就是其输入与输出,其可以表示一维二维多维向量或者常量。而且除了Variables指向的 tensor 外,所有的 tensor 在流入下一个节点后都不再保存。
(3)举例
下面首先定义一个数据流图(其实没有必要,tensorflow会默认定义一个),并做一些计算。

import tensorflow as tf
graph = tf.Graph()
with graph.as_default():
a = tf.Variable(3,name='input_a')
b = tf.Variable(5,name='input_b')
d = tf.add(a,b,name='add_d')
initialize = tf.global_variables_initializer()

这段代码,首先导入tensorflow,定义一个graph类,并在这张图上定义了foo与bar的两个变量,并给予初始值,最后对这个值求和,并初始化所有变量。其中,Variable是定义变量并赋予初值。让我们看下d。后面是输出,可以看到并没有输出实际的结果,由此可见在定义图的时候其实没有进行任何实际的计算。

print(d)

运行结果为:ensor("add_d:0", shape=(), dtype=int32)

下面定义一个session,并进行真正的计算。

with tf.Session(graph=graph) as sess:
sess.run(initialize)
res = sess.run(d)
print(res)

运行结果为:8
这段代码中,定义了session,并在session中执行了真正的初始化,并且求得d的值并打印出来。可以看到,在session中产生了真正的计算,得出值为8。

7.6.2 Tensor

Tensorflow的张量(Tensor)有rank,shape,data types的概念,下面来分别讲解。
(1)rank
张量(Tensor)为n维矩阵,而Rank一般是指张量的维度,即0D(0维)张量称为标量;1D(1维)张量等价于向量;2D(2维)等价于矩阵;对于更高维数的张量,可称为N维张量或N阶张量。有了这一概念,便可以之前的示例数据流图进行修改,使其变为使用张量:

图 7.2 张量数据流图

按下列方式,修改之前的代码。将分离的节点a、b替换为统一的输入节点a,而a为1D张量。当然运算符需要调整,由add改为reduce_sum,具体如下:

a= tf.Variable([5,3],name='input_a')
c=tf.reduce_sum(a,name='sum_c')

(2)shape
Shape指tensor每个维度数据的个数,可以用python的list/tuple表示。下图表示了rank,shape的关系。

表7.2 Rank与shape的对应关系

(3)data type
Data type,是指单个数据的类型。常用DT_FLOAT,也就是32位的浮点数。下图表示了所有的types。

表7.3 数据类型

7.6.3 Variables

1)介绍
TensorFlow中的变量在使用前需要被初始化,在模型训练中或训练完成后可以保存或恢复这些变量值。下面介绍如何创建变量,初始化变量,保存变量,恢复变量以及共享变量。当训练模型时,需要使用Variables保存与更新参数。Variables会保存在内存当中,所有tensor一旦拥有Variables的指向就不会在session中丢失。其必须明确的初始化而且可以通过Saver保存到磁盘上。Variables可以通过Variables初始化。

weights = tf.Variable(tf.random_normal([784, 200], stddev=0.35),name="weights")
biases = tf.Variable(tf.zeros([200]), name="biases")

其中,tf.random_normal是随机生成一个正态分布的tensor,其shape是第一个参数,stddev是其标准差。tf.zeros是生成一个全零的tensor。之后将这个tensor的值赋值给Variable。
以下我们通过一个实例来详细说明:
(1)创建模型的权重及偏置

import tensorflow as tf

weights = tf.Variable(tf.random_normal([784, 200], stddev=0.35), name="weights")
biases = tf.Variable(tf.zeros([200]), name="biases")

(2)初始化变量
实际在其初始化过程中做了很多的操作,比如初始化空间,赋初值(等价于tf.assign),并把Variable添加到graph中等操作。注意在计算前需要初始化所有的Variable。一般会在定义graph时定义global_variables_initializer,其会在session运算时初始化所有变量。
直接调用global_variables_initializer会初始化所有的Variable,如果仅想初始化部分Variable可以调用

tf.variables_initializer
init_op = tf.global_variables_initializer()
sess=tf.Session()
sess.run(init_op)

(3) 保存模型变量
保存模型由三个文件组成model.data,model.index,model.meta

saver = tf.train.Saver()
saver.save(sess, './tmp/model/',global_step=100)

运行结果:
'./tmp/model/-100'

(4)恢复模型变量
#先加载 meta graph并恢复权重变量

saver = tf.train.import_meta_graph('./tmp/model/-100.meta')
saver.restore(sess,tf.train.latest_checkpoint('./tmp/model/'))

(5)查看恢复后的变量

print(sess.run('biases:0'))

运行结果:
[ 0. 0. 0. 0. 0. 0. 0. 0. 0. 0................... 0.]

2) 共享模型变量
在复杂的深度学习模型中,存在大量的模型变量,并且期望能够一次性地初始化这些变量。TensorFlow提供了tf.variable_scope和tf.get_variable两个API,实现了共享模型变量。tf.get_variable(, , ):表示创建或返回指定名称的模型变量,其中name表示变量名称,shape表示变量的维度信息,initializer表示变量的初始化方法。tf.variable_scope():表示变量所在的命名空间,其中scope_name表示命名空间的名称。共享模型变量使用示例如下:

#定义卷积神经网络运算规则,其中weights和biases为共享变量

def conv_relu(input, kernel_shape, bias_shape):
# 创建变量"weights"
weights = tf.get_variable("weights", kernel_shape, initializer=tf.random_normal_initializer())

# 创建变量 "biases"
biases = tf.get_variable("biases", bias_shape, initializer=tf.constant_initializer(0.0))
conv = tf.nn.conv2d(input, weights, strides=[1, 1, 1, 1], padding='SAME')
return tf.nn.relu(conv + biases)

#定义卷积层,conv1和conv2为变量命名空间
with tf.variable_scope("conv1"):
# 创建变量 "conv1/weights", "conv1/biases".
relu1 = conv_relu(input_images, [5, 5, 32, 32], [32])
with tf.variable_scope("conv2"):
# 创建变量 "conv2/weights", "conv2/biases".
relu1 = conv_relu(relu1, [5, 5, 32, 32], [32])

3)Variables与constant的区别
值得注意的是Variables与constant的区别。Constant一般是常量,可以被赋值给Variables,constant保存在graph中,如果graph重复载入那么constant也会重复载入,其非常浪费资源,如非必要尽量不使用其保存大量数据。而Variables在每个session中都是单独保存的,甚至可以单独存在一个参数服务器上。可以通过代码观察到constant实际是保存在graph中,具体如下。

const = tf.constant(1.0,name="constant")
print(tf.get_default_graph().as_graph_def())

运行结果:
node {
name: "Const"
op: "Const"
attr {
key: "dtype"
value {
type: DT_FLOAT
}
}
attr {
key: "value"
value {
tensor {
dtype: DT_FLOAT
tensor_shape {
}
float_val: 1.0
}
}
}
}
4)variables与get_variables的区别
(1)语法格式:
 tf.Variable的参数列表为:

tf.Variable(name=None, initial_value, validate_shape=True, trainable=True, collections=None)

返回一个由initial_value创建的变量
 tf.get_variable的参数列表为:

tf.get_variable(name, shape=None, initializer=None, dtype=tf.float32, trainable=True, collections=None)

如果已存在参数定义相同的变量,就返回已存在的变量,否则创建由参数定义的新变量。

(2)使用tf.Variable时,如果检测到命名冲突,系统会自己处理。使用tf.get_variable()时,系统不会处理冲突,而会报错。

import tensorflow as tf
w_1 = tf.Variable(3,name="w_1")
w_2 = tf.Variable(1,name="w_1")
print( w_1.name)
print( w_2.name)

打印结果:
w_1:0
w_1_1:0

import tensorflow as tf

w_1 = tf.get_variable(name="w_1",initializer=1)
w_2 = tf.get_variable(name="w_1",initializer=2)

报错:
alueError Traceback (most recent call last)
in ()
2
3 w_1 = tf.get_variable(name="w_1",initializer=1)
----> 4 w_2 = tf.get_variable(name="w_1",initializer=2)
(3)两者的本质区别
tf.get_variable创建变量时,会进行变量检查,当设置为共享变量时(通过scope.reuse_variables()或tf.get_variable_scope().reuse_variables()),检查到第二个拥有相同名字的变量,就返回已创建的相同的变量;如果没有设置共享变量,则会报[ValueError: Variable varx alreadly exists, disallowed.]的错误。而tf.Variable()创建变量时,name属性值允许重复,检查到相同名字的变量时,由自动别名机制创建不同的变量。

import tensorflow as tf

tf.reset_default_graph()

with tf.variable_scope("scope1"):
w1 = tf.get_variable("w1", shape=[])
w2 = tf.Variable(0.0, name="w2")
with tf.variable_scope("scope1", reuse=True):
w1_p = tf.get_variable("w1", shape=[])
w2_p = tf.Variable(1.0, name="w2")

print(w1 is w1_p, w2 is w2_p)
print(w1_p,w2_p)

打印结果:
True False
<tf.Variable 'scope1/w1:0' shape=() dtype=float32_ref> <tf.Variable 'scope1_1/w2:0' shape=() dtype=float32_ref>

由此,不难明白官网上说的参数复用的真面目了。由于tf.Variable() 每次都在创建新对象,所有reuse=True 和它并没有什么关系。对于get_variable(),来说,如果已经创建的变量对象,就把那个对象返回,如果没有创建变量对象的话,就创建一个新的。

5)命名
另外一个值得注意的地方是尽量每一个变量都明确的命名,这样易于管理命令空间,而且在导入模型的时候不会造成不同模型之间的命名冲突,这样就可以在一张graph中容纳很多个模型。
6)Variables支持很多数学运算,具体可参考以下表格。

表7.4 常用运算

7.6.4 placeholders与feed_dict

当我们定义一张graph时,有时候并不知道需要计算的值,比如模型的输入数据,其只有在训练与预测时才会有值。这时就需要placeholder与feed_dict的帮助。
定义一个placeholder,可以使用tf.placeholder(dtype,shape=None,name=None)函数。

a = tf.placeholder(tf.int32,shape=[1],name='input_a')
b = tf.constant(2,name='input_b')
d = tf.add(a,b,name='add_d')
with tf.Session() as sess:
print(sess.run(d))

在上面的代码中,会抛出错误(InvalidArgumentError (see above for traceback): You must feed a value for placeholder tensor 'input_a_1' with dtype int32 and shape [1]),因为计算d需要a的具体值,而在代码中并没有给出。这时候需要将实际值赋给a。最后一行修改如下:

print(sess.run(d,{a:[5]}))

其中最后的dict就是一个feed_dict,一般会使用python读入一些值后传入,当使用minbatch的情况下,每次输入的值都不同。

7.6.5 constant 、variable及 placeholder的异同

(1)constant
constant()是一个函数,作用是创建一个常量tensor,其格式为:

tf.constant(value,dtype=None,shape=None,name=’Const’,verify_shape=False)

其中各参数说明如下:
value: 一个dtype类型(如果指定了)的常量值(列表)。要注意的是,要是value是一个列表的话,那么列表的长度不能够超过形状参数指定的大小(如果指定了)。要是列表长度小于指定的,那么多余的由列表的最后一个元素来填充。
dtype: 返回tensor的类型
shape: 返回的tensor形状。
name: tensor的名字
verify_shape: Boolean that enables verification of a shape of values。
示例:

import tensorflow as tf

#build graph
a=tf.constant(1.,name="a")
b=tf.constant(1.,shape=[2,2],name="b")

#construct session
sess=tf.Session()

#run in session
result_a=sess.run([a,b])
print("result_a:",result_a[0])
print("result_b:",result_a[1])

运行结果如下:
result_a: 1.0
result_b: [[ 1. 1.] [ 1. 1.]]

(2)variable
通过Variable()构造一个变量(variable),创建一个新的变量,初始值为initial_value构造函数需要初始值,初始值为initial_value,初始值可以是一个任何类型任何形状的Tensor。初始值的形状和类型决定了这个变量的形状和类型。构造之后,这个变量的形状和类型就固定了,他的值可以通过assign()函数来改变。如果你想要在之后改变变量的形状,你就需要assign()函数同时变量的validate_shape=False。和任何的Tensor一样,通过Variable()创造的变量能够作为图中其他操作的输入使用。
创建variable 后,其值会一直保存到程序运行结束,而一般的tensor张量在tensorflow运行过程中只是在计算图中流过,并不会保存下来。
因此varibale主要用来保存tensorflow构建的一些结构中的参数,这些参数才不会随着运算的消失而消失,才能最终得到一个模型。
比如神经网络中的权重和bias等,在训练过后,总是希望这些参数能够保存下来,而不是直接就消失了,所以这个时候要用到Variable。
注意,所有和varible有关的操作在计算的时候都要使用session会话来控制,包括计算,打印等等。
其具体格式为:

tf.Variable(initial_value=None, trainable=True, collections=None, validate_shape=True, caching_device=None, name=None, variable_def=None, dtype=None, expected_shape=None, import_scope=None, constraint=None)

各参数说明:
initial_value: 一个Tensor类型或者是能够转化为Tensor的python对象类型。它是这个变量的初始值。这个初始值必须指定形状信息,不然后面的参数validate_shape需要设置为false。当然也能够传入一个无参数可调用并且返回制定初始值的对象,在这种情况下,dtype必须指定。
trainable: 如果为True(默认也为Ture),这个变量就会被添加到图的集合GraphKeys.TRAINABLE_VARIABLES.中去 ,这个collection被作为优化器类的默认列表。
collections:图的collection 键列表,新的变量被添加到这些collection中去。默认是[GraphKeys.GLOBAL_VARIABLES].
validate_shape: 如果是False的话,就允许变量能够被一个形状未知的值初始化,默认是True,表示必须知道形状。
caching_device: 可选,描述设备的字符串,表示哪个设备用来为读取缓存。默认是变量的device,
name: 可选,变量的名称
variable_def: VariableDef protocol buffer. If not None, recreates the Variable object with its contents. variable_def and the other arguments are mutually exclusive.
dtype: 如果被设置,初始化的值就会按照这里的类型来定。
expected_shape: TensorShape类型.要是设置了,那么初始的值会是这种形状
import_scope: Optional string. Name scope to add to the Variable. Only used when initializing from protocol buffer.
示例:

import numpy as np
import tensorflow as tf

#create a Variable
w=tf.Variable(initial_value=[[1,2],[3,4]],dtype=tf.float32)
x=tf.Variable(initial_value=[[1,1],[1,1]],dtype=tf.float32)
x=x.assign(x*2)
print(x)
y=tf.matmul(w,x)
z=tf.sigmoid(y)
print(z)
init=tf.global_variables_initializer()

with tf.Session() as session:
session.run(init)
z=session.run(z)
print(z)

运行结果:
Tensor("Assign_1:0", shape=(2, 2), dtype=float32_ref)
Tensor("Sigmoid_2:0", shape=(2, 2), dtype=float32)
[[ 0.99752742 0.99752742]
[ 0.99999917 0.99999917]]
(3)placeholder
placeholder的作用可以理解为占个位置,我并不知道这里将会是什么值,但是知道类型和形状等等一些信息,先把这些信息填进去占个位置,然后以后用feed的方式来把这些数据“填”进去。返回的就是一个用来用来处理feeding一个值的tensor。
那么feed的时候一般就会在你之后session的run()方法中用到feed_dict这个参数了。这个参数的内容就是你要“喂”给那个placeholder的内容。

它是tensorflow中又一保存数据的利器,它在使用的时候和前面的variable不同的是在session运行阶段,需要给placeholder提供数据,利用feed_dict的字典结构给placeholdr变量“喂数据”。
其一般格式:

tf.placeholder(dtype, shape=None, name=None)

参数说明:
dtype: 将要被fed的元素类型
shape:(可选) 将要被fed的tensor的形状,要是不指定的话,你能够fed进任何形状的tensor。
name:(可选)这个操作的名字
示例:

运行结果:
Tensor("MatMul_9:0", shape=(2, 2), dtype=float32)
[[ 0.2707203 0.68843865]
[ 0.42275223 0.73435611]]

7.6.6 常用概念

从上例我们可以看到TensorFlow有不少概念,这些概念或名称有些我们在其它系统中看到或使用过,但在TensorFlow架构中,其用途很多不一样,现把主要的一些概念总结如下,便于大家参考。

表 7.5 TensorFlow常用概念

7.6.7 实例:利用梯度下降--预测曲线

根据函数生成数据,利用梯度下降法,画一条模拟曲线,逼近原数据的分布。

运行结果:
a= 1.16647 b= 2.48966

7.7 TensorFlow实现一个神经元

【环境说明】TensorFlow1.3,Python3.6,Jupyter

7.7.1 定义变量

#导入tensorflow
import tensorflow as tf
#定义一个图
graph = tf.get_default_graph()
#定义一个常数
input_value = tf.constant(1.0)

我们来看一下input_value的结果,就会发现这个是一个无维度的32位浮点张量:就是一个数字
input_value
<tf.Tensor 'Const:0' shape=() dtype=float32>

这个结果并没有说明这个数字是多少?不像Python变量一样,定义后立即可以看到其值,对于TensorFlow定义的这个常数,为了执行input_value这句话,并给出这个数字的值,我们需要创造一个“会话”(session)。让图里的计算在其中执行并明确地要执行input_value并给出结果(会话会默认地去找那个默认图)

sess = tf.Session()
sess.run(input_value)

运行结果为:1
“执行”一个常量可能会让人觉得有点怪。但是这与在Python里执行一个表达式类似。这就是TensorFlow管理它自己的对象空间(计算图)和它自己的执行方式。

7.7.2 定义一个神经元

接下来我们定义一个神经元,让该神经元学习一个简单1到0的函数,即输入为1,输出为0这样一个函数。假设我们有一个训练集,输入为1,输出为0.8(正确输出是0)。
假设我们要预测的函数为:y=w*x,其中w为权重,x为输入,y为输出或预测值。他们间的关系如下图所示:

图7.3 单个神经元

现在已经有了一个会话,其中有一个简单的图。下面让我们构建仅有一个参数的神经元,或者叫权重。通常即使是简单的神经元也都会有偏置项和非一致的启动函数或激活函数,但这里我们先不管这些。
神经元的权重不应该是常量,我们会期望这个值能改变,从而学习训练数据里的输入和输出。这里我们定义权重是一个TensorFlow的变量,并给它一个初值0.8。

weight = tf.Variable(0.8)
#让权重与输入相乘,得到输出
output_value = weight * input_value

怎么才能看到乘积是多少?我们必须“运行”这个output_value运算。但是这个运算依赖于一个变量:权重。我们告诉TensorFlow这个权重的初始值是0.8,但在这个会话里,这个值还没有被设置。tf.global_variables_initializer()函数生成了一个运算,来初始化所有的变量(我们的情况是只有一个变量)。随后我们就可以运行这个运算了。

init=tf.global_variables_initializer()
sess.run(init)

tf.global_variables_initializer()的结果会包括现在图里所有变量的初始化器。所以如果你后续加入了新的变量,你就需要再次使用tf.global_variables_initializer()。一个旧的init是不会包括新的变量的。
现在我们已经准备好运行output_value运算了。

sess.run(output_value)

运算结果为:0.80000001
0.8 * 1.0是一个32位的浮点数,而32位浮点数一般不会是0.8。0.80000001是系统可以获得的一个近似值。

7.7.3 TensorBoard可视化你的图

到目前为止,我们的图是很简单的,但是能看到她的图形表现形式也是很好的。我们用TensorBoard来生成这个图形。TensorBoard读取存在每个运算里面的名字字段,这和Python里的变量名是很不一样的。我们可以使用这些TensorFlow的名字,并转成更方便的Python变量名。这里tf.multiply和我前面使用*来做乘运算是等价的,但这个操作可以让我们设置运算的名字。

x = tf.constant(1.0, name='input')
w = tf.Variable(0.8, name='weight')
y = tf.multiply(w, x, name='output')

TensorBoard是通过查看一个TensorFlow会话创建的输出的目录来工作的。我们可以先用一个summary.FileWriter来写这个输出。如果我们只是创建一个图的输出,它就将图写出来。
构建summary.FileWriter的第一个参数是一个输出目录的名字。如果此目录不存在,则在构建summary.FileWriter时会被建出来。

summary_write = tf.summary.FileWriter('./log/test', sess.graph)

现在我们可以通过命令行来启动TensorBoard了。

$ tensorboard --logdir="./log/test"

TensorBoard会运行一个本地的Web应用,端口6006(6006是goog这个次倒过的对应)。在你本机的浏览器里登陆IP:6006/#graphs,你就可以看到在TensorFlow里面创建的图,类似于图7.4

图7.4 在TensorBoard里可视化的一个最简单的TensorFlow的神经元

7.7.4训练神经元

我们已经有了一个神经元,但如何才能让它学习?假定我们让输入为1,而正确的输出应该是0。也就是说我们有了一个仅有一条记录且记录只有一个特征(值为1)和一个结果(值为0)的训练数据集。我们现在希望这个神经元能学习这个1->0的函数。
目前的这个系统是输入1而输出0.8。但不是我们想要的。我们需要一个方法来测量系统误差是多少。我们把对误差的测量称为“损失”,并把损失最小化设定为系统的目标。损失是可以为负值的,而对负值进行最小化是毫无意思的。所以我们用实际输出和期望输出之差的平方来作为损失的测量值。

y_ = tf.constant(0.0)
loss = (y - y_)**2

对此,现有的图还不能做什么事情。所以我们需要一个优化器。这里我们使用梯度下降优化器来基于损失值的导数去更新权重。这个优化器采用一个学习率来调整每一步更新的大小。这里我们设为0.025。

optim = tf.train.GradientDescentOptimizer(learning_rate=0.025)

这个优化器很聪明。它自动地运行,并在整个网络里恰当地设定梯度,完成后向的学习过程。让我们看看我们的简单例子里的梯度是什么样子的。

grads_and_vars = optim.compute_gradients(loss)

那么compute_gradients可能会返回(None,v),即部分变量没有对应的梯度,在下一步的时候NoneType会导致错误。因此,需要将有梯度的变量提取出来,记为grads_vars。

grads_vars = [v for (g,v) in grads_and_vars if g is not None]

之后,对grads_vars再一次计算梯度,得到了gradient。

gradient = optim.compute_gradients(loss, grads_vars)
sess.run(tf.global_variables_initializer())
sess.run(gradient)

运行结果为:[(1.6, 0.80000001)]

为什么梯度值是1.6?我们的损失函数是错误的平方,因此它的导数就是这个错误乘2。现在系统的输出是0.8而不是0,所以这个错误就是0.8,乘2就是1.6。优化器是对的!
对于更复杂的系统,TensorFlow可以自动地计算并应用这些梯度值。
让我们运用这个梯度来完成反向传播。

sess.run(tf.global_variables_initializer())
sess.run(optim.apply_gradients(grads_and_vars))
sess.run(w)

运行结果为:0.75999999,约为0.76
现在权重减少了0.04,这是因为优化器减去了梯度乘以学习比例(1.6*0.025)。权重向着正确的方向在变化。
其实我们不必像这样调用优化器。我们可以形成一个运算,自动地计算和使用梯度:train_step。

train_step = tf.train.GradientDescentOptimizer(0.025).minimize(loss)
for i in range(100):
sess.run(train_step)
sess.run(y)

运算结果为:0.0047364226
通过100次运行训练步骤后,权重和输出值已经非常接近0了。这个神经元已经学会了!

7.7.5可视化训练过程

在TensorBoard里显示训练过程的分析,你可能对训练过程中发生了什么感兴趣,比如我们想知道每次训练步骤后,系统都是怎么去预测输出的。为此,我们可以在训练循环里面打印输出值。

sess.run(tf.global_variables_initializer())
for i in range(100):
print('before step {}, y is {}'.format(i, sess.run(y)))
sess.run(train_step)

运行结果:
before step 0, y is 0.800000011920929
before step 1, y is 0.7599999904632568
before step 2, y is 0.722000002861023
before step 3, y is 0.6858999729156494
....................................................................
before step 95, y is 0.006121140904724598
before step 96, y is 0.005815084092319012
before step 97, y is 0.005524329841136932
before step 98, y is 0.005248113535344601
before step 99, y is 0.004985707812011242
这种方法可行,但是有些问题。看懂一串数字是比较难的,能用一个图来展示就好了。仅仅就这一个需要观察的值,就有很多输出要看。而且我们希望能观察多个值。如果能用一个一致统一的方法来记录所有值就好了。
幸运的是,上面我们用来可视化图的工具也有我们需要的这个功能。
我们通过加入能总结图自己状态的运算来提交给计算图。这里我们会创建一个运算,它能报告y的当前值,即神经元的输出。

summary_y=tf.summary.scalar('./output', y)

当你运行一个总结运算,它会返回给一个protocal buffer文本的字符串。用summary.FileWriter可以把这个字符串写入一个日志目录。

summary_writer = tf.summary.FileWriter('./log/simple_stats', sess.graph)
sess.run(tf.global_variables_initializer())
for i in range(100):
summary_str = sess.run(summary_y)
summary_writer.add_summary(summary_str, i)
sess.run(train_step)

在运行命令 tensorboard --logdir="./log/simple_stats"后,你就可以在IP:6006里面看到一个可交互的图形(如图7.5所示)。

图7.5 TensorBoard里的可视化图,显示了一个神经元的输出和训练循环次数的关系。

7.7.6小结

下面是代码的完全版。它相当的小。但每个小部分都显示了有用且可理解的TensorflowFlow的功能。

我们这里所演示的例子甚至比非常简单。能看到这样具体的例子可以帮助理解,还可以从简单的砖头开始使用并扩展构建更为复杂的系统。
如果你想继续实践TensorFlow,可以从构建更有趣的神经元开始,或许可以使用不同的激活函数。你也可以用更有趣的数据来训练。继续添加更多的神经元,或者更多的层级。你可以查看更复杂的预制的模型,或学习TensorFlow的教程与如何使用它手册。去学吧!
参考:https://www.oreilly.com/learning/hello-tensorflow

7.8TensorFlow常用函数

TensorFlow函数有很多,有不同类别的,如数据类型转换、变量定义、激活函数、卷积函数等等,这里我们选择一些常用或本章后续用到的一些函数,供大家参考。

表7.6 TensorFlow常用函数

7.9TensorFlow的运行原理

TensorFlow有一个重要组件client,即客户端,此外,还有master、worker,这些有点类似Spark的结构。它通过Session的接口与master及多个worker相连,其中每一个worker可以与多个硬件设备(device)相连,比如CPU或GPU,并负责管理这些硬件。而master则负责管理所有worker按流程执行计算图。
TensorFlow有单机模式和分布式模式两种实现,其中单机指client、master、worker全部在一台机器上的同一个进程中;分布式的版本允许client、master、worker在不同机器的不同进程中,同时由集群调度系统统一管理各项任务。下图(图7.6)所示为单机版和分布式版本的实现原理图。


图7.6 TensorFlow 单机版本和分布式运行原理

7.10TensorFlow系统架构

图7.7 是TensorFlow的系统架构,从底向上分为设备管理和通信层、数据操作层、图计算层、API接口层、应用层。其中设备管理和通信层、数据操作层、图计算层是TensorFlow的核心层。

图7.7 TensorFlow 系统架构

 底层设备通信层负责网络通信和设备管理
设备管理可以实现TensorFlow设备异构的特性,支持CPU、GPU、Mobile等不同设备。 网络通信依赖gRPC通信协议实现不同设备间的数据传输和更新。
 第二层为数据操作层实现
这些OP以Tensor为处理对象,依赖网络通信和设备内存分配,实现了各种Tensor操 作或计算。OP不仅包含MatMul等计算操作,还包含Queue等非计算操作。
 第三层是图计算层(Graph),包含本地计算流图和分布式计算流图的实现
Graph模块包含Graph的创建、编译、优化和执行等部分,Graph中每个节点都是OP 类型表示。
 第四层是API接口层
Tensor C API是对TensorFlow功能模块的接口封装,便于其他语言平台调用。
 第四层以上是应用层
不同编程语言在应用层通过API接口层调用TensorFlow核心功能实现相关应用。

第17章 自己动手做一个聊天机器人

【环境说明】
使用Python3.6的Jupyter,TensorFlow1.3+, tensorflow的embedding_attention_seq2seq,使用LSTM神经网络,采用AdamOptimizer优化器、jieba分词等。

17.1 聊天机器人简介

现在很多公司把技术重点放在人机对话上,通过人机对话,控制各种家用电器、控制机器人、控制汽车等等。如,苹果的Siri、微软的小冰、百度的度秘、亚马逊的蓝色音箱等等。这种智能聊天机器人将给企业带来强大的竞争优势。
智能聊天机器人的发展经历了3代不同的技术:
1)第一代,基于逻辑判断,如if then;else then等;
2)第二代,基于检索库,如给定一个问题,然后从检索库中找到与之匹配度最高的答案;
3)第三代,基于深度学习,采用seq2seq+Attention模型,经过大量数据的训练和学习,得到一个模型,通过这个模型,输入数据产生相应的输出。接下来我们通过一个简单实例来介绍seq2seq+Attention模型的架构及原理。

17.2 聊天机器人的原理

目前聊天机器人一般采用带注意力(Attention)的模型,但之前一般采用Seq2Seq模型,这种模型有哪些不足?需要引入Attention Model(简称为 AM)呢?我们先来看一下Seq2Seq模型的架构,如下图:

图1 ncoder-Decoder 架构
这是一个典型的编码器-解码器(Encoder-Decoder)框架。我们该如何理解这个框架呢?
从左到右,我们可以这么直观地去理解:从左到右,看作适合处理由一个句子(或篇章)生成另外一个句子(或篇章)的通用处理模型。假设这句子对为<X,Y>,我们的目标是给定输入句子X,期待通过Encoder-Decoder框架来生成目标句子Y。X和Y可以是同一种语言,也可以是两种不同的语言。而X和Y分别由各自的单词序列构成:
每个都依次这么产生,那么看起来就是整个系统根据输入句子X生成了目标句子Y。
Encoder-Decoder是个非常通用的计算框架,至于Encoder和Decoder具体使用什么模型是由我们自己定的,常见的比如CNN/RNN/BiRNN/GRU/LSTM/Deep LSTM等,而且变化组合非常多。
Encoder-Decoder模型应用非常广泛,其应用场景非常多,比如对于机器翻译来说,<X,Y>就是对应不同语言的句子,比如X是英语句子,Y是对应的中文句子翻译。再比如对于文本摘要来说,X就是一篇文章,Y就是对应的摘要;再比如对于对话机器人来说,X就是某人的一句话,Y就是对话机器人的应答;再比如……总之,太多了。
这个框架有一个缺点,就是生成的句子中每个词采用的中间语言编码是相同的,都是C,具体看如下表达式。这种框架,在句子表较短时,性能还可以,但句子稍长一些,生成的句子就不尽如人意了。如何解决这一不足呢?

解铃还须系铃人,既然问题出在C上,所以我们需要在C上做文章。我们引入一个Attention机制,可以有效解决这个问题。

17.3 带注意力的框架


这就是为何说这个模型没有体现出注意力的缘由。这类似于你看到眼前的画面,但是没有注意焦点一样。
如果拿机器翻译(输入英文输出中文)来解释这个分心模型的Encoder-Decoder框架更好理解,比如:

输入英文句子:Tom chase Jerry,Encoder-Decoder框架逐步生成中文单词:“汤姆”,“追逐”,“杰瑞”

在翻译“杰瑞”这个中文单词的时候,分心模型里面的每个英文单词对于翻译目标单词“杰瑞”贡献是相同的,很明显这里不太合理,显然“Jerry”对于翻译成“杰瑞”更重要,但是分心模型是无法体现这一点的,这就是为何说它没有引入注意力的原因。
没有引入注意力的模型在输入句子比较短的时候估计问题不大,但是如果输入句子比较长,此时所有语义完全通过一个中间语义向量来表示,单词自身的信息已经消失,可想而知会丢失很多细节信息,这也是为何要引入注意力模型的重要原因。
上面的例子中,如果引入AM(Attention Model)模型的话,应该在翻译“杰瑞”的时候,体现出英文单词对于翻译当前中文单词不同的影响程度,比如给出类似下面一个概率分布值:

(Tom,0.3(Chase,0.2)(Jerry,0.5)

每个英文单词的概率代表了翻译当前单词“杰瑞”时,注意力分配模型分配给不同英文单词的注意力大小。这对于正确翻译目标语单词肯定是有帮助的,因为引入了新的信息。同理,目标句子中的每个单词都应该学会其对应的源语句子中单词的注意力分配概率信息。这意味着在生成每个单词Yi的时候,原先都是相同的中间语义表示C会替换成根据当前生成单词而不断变化的Ci。理解AM模型的关键就是这里,即由固定的中间语义表示C换成了根据当前输出单词来调整成加入注意力模型的变化的Ci。增加了AM模型的Encoder-Decoder框架理解起来如图2所示。

图2 引入AM(Attention Model)模型的Encoder-Decoder框架
即生成目标句子单词的过程成了下面的形式:

而每个Ci可能对应着不同的源语句子单词的注意力分配概率分布,比如对于上面的英汉翻译来说,其对应的信息可能如下:

其中,f2函数代表Encoder对输入英文单词的某种变换函数,比如如果Encoder是用的RNN模型的话,这个f2函数的结果往往是某个时刻输入xi后隐层节点的状态值;g代表Encoder根据单词的中间表示合成整个句子中间语义表示的变换函数,一般的做法中,g函数就是对构成元素加权求和,也就是常常在论文里看到的下列公式:

图3  的生成过程
这里还有一个问题:生成目标句子某个单词,比如“汤姆”的时候,你怎么知道AM模型所需要的输入句子单词注意力分配概率分布值呢?就是说“汤姆”对应的概率分布:

(Tom,0.6(Chase,0.2)(Jerry,0.2

如何得到的呢?
为了便于说明,我们假设对图1的非AM模型的Encoder-Decoder框架进行细化,Encoder采用RNN模型,Decoder也采用RNN模型,这是比较常见的一种模型配置,则图1的图转换为下图:

图4 RNN作为具体模型的Encoder-Decoder框架
那么用下图可以较为便捷地说明注意力分配概率分布值的通用计算过程:

图5 AM注意力分配概率计算

相当于在原来的模型上,又加了一个单层DNN(特指全连接)网络,当前输出词Yi针对某一个输入词j的注意力权重由当前的隐层Hi,以及输入词j的隐层状态(hj)共同决定;函数F(hj,Hi)在不同论文里可能会采取不同的方法,然后函数F的输出经过Softmax进行归一化就得到得到一个0-1的注意力分配概率分布数值。
如图5所示:当输出单词为“汤姆”时刻对应的输入句子单词的对齐概率。绝大多数AM模型都是采取上述的计算框架来计算注意力分配概率分布信息,区别只是在F的定义上可能有所不同。yi值的生成可参考下图6:

上述内容就是论文里面常常提到的Soft Attention Model的基本思想,你能在文献里面看到的大多数AM模型基本就是这个模型,区别很可能只是把这个模型用来解决不同的应用问题。那么怎么理解AM模型的物理含义呢?一般文献里会把AM模型看作是单词对齐模型,这是非常有道理的。目标句子生成的每个单词对应输入句子单词的概率分布可以理解为输入句子单词和这个目标生成单词的对齐概率,这在机器翻译语境下是非常直观的:传统的统计机器翻译一般在做的过程中会专门有一个短语对齐的步骤,而注意力模型其实起的是相同的作用。在其他应用里面把AM模型理解成输入句子和目标句子单词之间的对齐概率也是很顺畅的想法。
当然,从概念上理解的话,把AM模型理解成影响力模型也是合理的,就是说生成目标单词的时候,输入句子每个单词对于生成这个单词有多大的影响程度。这种想法也是比较好理解AM模型物理意义的一种思维方式。

图7是论文“A Neural Attention Model for Sentence Summarization”中,Rush用AM模型来做生成式摘要给出的一个AM的一个非常直观的例子。

图7 句子生成式摘要例子

这个例子中,Encoder-Decoder框架的输入句子是:“russian defense minister ivanov called sunday for the creation of a joint front for combating global terrorism”。对应图中纵坐标的句子。系统生成的摘要句子是:“russia calls for joint front against terrorism”,对应图中横坐标的句子。可以看出模型已经把句子主体部分正确地抽出来了。矩阵中每一列代表生成的目标单词对应输入句子每个单词的AM分配概率,颜色越深代表分配到的概率越大。这个例子对于直观理解AM是很有帮助作用的。

17.4 用TensorFlow的seq2seq+Attention制作你自己的聊天机器人

上节我们介绍了带AM的seq2seq模型的框架及原理,这一节我们将利用tensorflow实行一个非常简单的功能,两个奇数序列样本,输出也是奇数序列,以此建立一个简单encode-decode模型,这里我们将使用TensorFlow提供的强大API,不过要真正利用好tensorflow必须理解好它的重要接口及其所有参数,所以第一步我们找到这次要使用的最关键的接口embedding_attention_seq2seq。

embedding_attention_seq2seq(
encoder_inputs,
decoder_inputs,
cell,
num_encoder_symbols,
num_decoder_symbols,
embedding_size,
num_heads=1,
output_projection=None,
feed_previous=False,
dtype=None,
scope=None,
initial_state_attention=False
)

在这个接口函数中有很多参数,这些参数的具体含义是啥?如何使用这些参数?这些问题我们在下节将详细说明。

17.4.1 接口参数说明

为了更好理解这个接口,我们参考上节的图6,以下我们就上节这个接口函数各参数进行详细说明:
1)encoder_inputs
参数encoder_inputs是一个list,list中每一项是1D(1维)的Tensor,这个Tensor的shape是[batch_size],Tensor中每一项是一个整数,类似这样:

[array([0, 0, 0, 0], dtype=int32),
array([0, 0, 0, 0], dtype=int32),
array([8, 3, 5, 3], dtype=int32),
array([7, 8, 2, 1], dtype=int32),
array([6, 2, 10, 9], dtype=int32)]

其中5个array,表示一句话的长度是5个词,每个array里有4个数,表示batch是4,也就是一共4个样本。那么可以看出第一个样本是[[0],[0],[8],[7],[6]],第二个样本是[[0],[0],[3],[8],[2]],以此类推。这里的数字是用来区分不同词的一个id,一般通过统计得出,一个id表示一个词
2)decoder_inputs
同理,参数decoder_inputs也是和encoder_inputs一样结构,不赘述。
3)cell
参数cell是tf.nn.rnn_cell.RNNCell类型的循环神经网络单元,可以用tf.contrib.rnn.BasicLSTMCell、tf.contrib.rnn.GRUCell等。
4)num_encoder_symbols
参数num_encoder_symbols是一个整数,表示encoder_inputs中的整数词id的数目。
5)num_decoder_symbols
同理num_decoder_symbols表示decoder_inputs中整数词id的数目。
6)embedding_size
参数embedding_size表示在内部做word embedding(如通过word2vec把各单词转换为向量)时转成几维向量,需要和RNNCell的size大小相等。
7)num_heads
参数num_heads表示在attention_states中的抽头数量,一般取1。
8)output_projection
参数output_projection是一个(W, B)结构的元组(tuple),W是shape为[output_size x num_decoder_symbols]的权重(weight)矩阵,B是shape为[num_decoder_symbols]的偏置向量,那么每个RNNCell的输出经过WX+B就可以映射成num_decoder_symbols维的向量,这个向量里的值表示的是任意一个decoder_symbol的可能性,也就是softmax的输出值。
9)feed_previous
参数feed_previous表示decoder_inputs是我们直接提供训练数据的输入,还是用前一个RNNCell的输出映射出来的,如果feed_previous为True,那么就是用前一个RNNCell的输出,并经过WX+B映射而成。
10)dtype
参数dtype是RNN状态数据的类型,默认是tf.float32。
11)scope
scope是子图的命名,默认是“embedding_attention_seq2seq”
12)initial_state_attention
initial_state_attention表示是否初始化attentions,默认为否,表示全都初始化为0
函数的返回值是一个(outputs, state)结构的tuple,其中outputs是一个长度为句子长度(词数,与上面encoder_inputs的list长度一样)的list,list中每一项是一个2D(二维)的tf.float32类型的Tensor,第一维度是样本数,比如4个样本则有四组Tensor,每个Tensor长度是embedding_size,像下面的样子:

[
array([
[-0.02027004, -0.017872 , -0.00233014, -0.0437047 , 0.00083584,
0.01339234, 0.02355197, 0.02923143],
[-0.02027004, -0.017872 , -0.00233014, -0.0437047 , 0.00083584,
0.01339234, 0.02355197, 0.02923143],
[-0.02027004, -0.017872 , -0.00233014, -0.0437047 , 0.00083584,
0.01339234, 0.02355197, 0.02923143],
[-0.02027004, -0.017872 , -0.00233014, -0.0437047 , 0.00083584,
0.01339234, 0.02355197, 0.02923143]
],dtype=float32),
array([
......
],dtype=float32),
array([
......
],dtype=float32),
array([
......
],dtype=float32),
array([
......
],dtype=float32),
]

其实这个outputs可以描述为5*4*8个浮点数,5是句子长度,4是样本数,8是词向量维数。

下面再看返回的state,它是num_layers个LSTMStateTuple组成的大tuple,这里num_layers是初始化cell时的参数,表示神经网络单元有几层,一个由2层LSTM神经元组成的encoder-decoder多层循环神经网络是像下面这样的网络结构:

图8 由2层LSTM神经元组成的encoder-decoder多层循环神经网络
encoder_inputs输入encoder的第一层LSTM神经元,这个神经元的output传给第二层LSTM神经元,第二层的output再传给Attention层,而encoder的第一层输出的state则传给decoder第一层的LSTM神经元,依次类推,如图8所示。

回过头来再看LSTM State Tuple这个结构,它是由两个Tensor组成的tuple,第一个tensor命名为c,由4个8维向量组成(4是batch, 8是state_size也就是词向量维度), 第二个tensor命名为h,同样由4个8维向量组成。
这里的c和h如下所示:

c是传给下一个时序的存储数据,h是隐藏层的输出,这里的计算公式是:

在tensorflow代码里也有对应的实现:

concat = _linear([inputs, h], 4 * self._num_units, True)
i, j, f, o = array_ops.split(value=concat, num_or_size_splits=4, axis=1)
new_c = (c * sigmoid(f + self._forget_bias) + sigmoid(i) * self._activation(j))
new_h = self._activation(new_c) * sigmoid(o)

事实上,如果我们直接使用embedding_attention_seq2seq来做训练,返回的state一般是用不到的。

17.4.2 用数字样本训练seq2seq模型

17.4.2.1预定义seq2seq模型

我们以1、3、5、7、9……奇数序列为例来构造样本,比如两个样本是[[1,3,5],[7,9,11]]和[[3,5,7],[9,11,13]],相当于两个<X,Y>对:

train_set = [[[1, 3, 5], [7, 9, 11]], [[3, 5, 7], [9, 11, 13]]]

输入:[1, 3, 5],输出为:[7, 9, 11]
输入:[3, 5, 7],输出为:[9, 11, 13]
从这两个样本可以看出,输出的每个数据是对应输入加6得到。
为了我们能够满足不同长度的序列,需要让我们训练的序列比样本的序列长度要长一些,比如我们设置为5,即

input_seq_len = 5
output_seq_len = 5

因为样本长度小于训练序列的长度,所以我们用0来填充,即:

PAD_ID = 0

那么我们的第一个样本的encoder_input就是:

encoder_input_0 = [PAD_ID] * (input_seq_len - len(train_set[0][0])) + train_set[0][0]

结果为:[0, 0, 1, 3, 5]
那么我们的第二个样本的encoder_input就是:

encoder_input_1 = [PAD_ID] * (input_seq_len - len(train_set[1][0])) + train_set[1][0]

结果为:[0, 0, 3, 5, 7]

ecoder_input我们需要用一个GO_ID来作为起始,再输入样本序列,最后再用PAD_ID来填充,即:

GO_ID = 1
decoder_input_0 = [GO_ID] + train_set[0][1]+[PAD_ID] * (output_seq_len - len(train_set[0][1]) - 1)
decoder_input_1 = [GO_ID] + train_set[1][1]+[PAD_ID] * (output_seq_len - len(train_set[1][1]) - 1)
print(decoder_input_0,decoder_input_1)

输出结果为:[1, 7, 9, 11, 0] [1, 9, 11, 13, 0]
为了把输入转成上面讲到的embedding_attention_seq2seq中的输入参数encoder_inputs和decoder_inputs的格式,我们进行如下转换即可:

import numpy as np
encoder_inputs = []
decoder_inputs = []
for length_idx in range(input_seq_len):
encoder_inputs.append(np.array([encoder_input_0[length_idx],
encoder_input_1[length_idx]], dtype=np.int32))
for length_idx in range(output_seq_len):
decoder_inputs.append(np.array([decoder_input_0[length_idx],
decoder_input_1[length_idx]], dtype=np.int32))

运算结果如下:
encoder_inputs为:
[array([0, 0], dtype=int32),
array([0, 0], dtype=int32),
array([1, 3], dtype=int32),
array([3, 5], dtype=int32),
array([5, 7], dtype=int32)]

decoder_inputs为:
[array([1, 1], dtype=int32),
array([7, 9], dtype=int32),
array([ 9, 11], dtype=int32),
array([11, 13], dtype=int32),
array([0, 0], dtype=int32)]

太好了,第一步大功告成,我们把这部分独立出一个函数,整理代码如下:

import numpy as np

# 输入序列长度
input_seq_len = 5
# 输出序列长度
output_seq_len = 5
# 空值填充0
PAD_ID = 0
# 输出序列起始标记
GO_ID = 1

def get_samples():
"""构造样本数据

:return:
encoder_inputs: [array([0, 0], dtype=int32),
array([0, 0], dtype=int32),
array([1, 3], dtype=int32),
array([3, 5], dtype=int32),
array([5, 7], dtype=int32)]
decoder_inputs: [array([1, 1], dtype=int32),
array([7, 9], dtype=int32),
array([ 9, 11], dtype=int32),
array([11, 13], dtype=int32),
array([0, 0], dtype=int32)]
"""

train_set = [[[1, 3, 5], [7, 9, 11]], [[3, 5, 7], [9, 11, 13]]]
encoder_input_0 = [PAD_ID] * (input_seq_len - len(train_set[0][0]))+ train_set[0][0]
encoder_input_1 = [PAD_ID] * (input_seq_len - len(train_set[1][0]))+ train_set[1][0]
decoder_input_0 = [GO_ID] + train_set[0][1]+ [PAD_ID] * (output_seq_len - len(train_set[0][1]) - 1)
decoder_input_1 = [GO_ID] + train_set[1][1]+ [PAD_ID] * (output_seq_len - len(train_set[1][1]) - 1)

encoder_inputs = []
decoder_inputs = []
for length_idx in range(input_seq_len):
encoder_inputs.append(np.array([encoder_input_0[length_idx],
encoder_input_1[length_idx]], dtype=np.int32))
for length_idx in range(output_seq_len):
decoder_inputs.append(np.array([decoder_input_0[length_idx],
decoder_input_1[length_idx]], dtype=np.int32))
return encoder_inputs, decoder_inputs

完成上面这部分之后,我们开始构造模型,我们了解了tensorflow的运行过程是先构造图,再加入数据计算的,所以我们构建模型的过程实际上就是构建一张图。具体步骤如下:
1)创建encoder_inputs和decoder_inputs的placeholder(占位符):

import tensorflow as tf
encoder_inputs = []
decoder_inputs = []
for i in range(input_seq_len):
encoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="encoder{0}".format(i)))
for i in range(output_seq_len):
decoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="decoder{0}".format(i)))

2)创建一个记忆单元数目为size=8的LSTM神经元结构:

size = 8
cell = tf.contrib.rnn.BasicLSTMCell(size)

我们假设我们要训练的奇数序列最大数值是输入最大为10,输出最大为16,那么

num_encoder_symbols = 10
num_decoder_symbols = 16

3)把参数传入embedding_attention_seq2seq获取output

from tensorflow.contrib.legacy_seq2seq.python.ops import seq2seq
outputs, _ = seq2seq.embedding_attention_seq2seq(
encoder_inputs,
decoder_inputs[:output_seq_len],
cell,
num_encoder_symbols=num_encoder_symbols,
num_decoder_symbols=num_decoder_symbols,
embedding_size=size,
output_projection=None,
feed_previous=False,
dtype=tf.float32)

4)为了说明之后的操作,我们先把这部分运行一下,看看输出的output是个什么样的数据,我们先把上面的构建模型部分放到一个单独的函数里,如下:

def get_model():
"""构造模型
"""

encoder_inputs = []
decoder_inputs = []
for i in range(input_seq_len):
encoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="encoder{0}".format(i)))
for i in range(output_seq_len):
decoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="decoder{0}".format(i)))

cell = tf.contrib.rnn.BasicLSTMCell(size)

# 这里输出的状态我们不需要
outputs, _ = seq2seq.embedding_attention_seq2seq(
encoder_inputs,
decoder_inputs,
cell,
num_encoder_symbols=num_encoder_symbols,
num_decoder_symbols=num_decoder_symbols,
embedding_size=size,
output_projection=None,
feed_previous=False,
dtype=tf.float32)
return encoder_inputs, decoder_inputs, outputs

5)构造运行时的session,并填入样本数据:

tf.reset_default_graph()
with tf.Session() as sess:
sample_encoder_inputs, sample_decoder_inputs = get_samples()
encoder_inputs, decoder_inputs, outputs = get_model()
input_feed = {}
for l in range(input_seq_len):
input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
for l in range(output_seq_len):
input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]

sess.run(tf.global_variables_initializer())
outputs = sess.run(outputs, input_feed)
print(outputs)

输出结果为:
[array([[ 0.02211747, 0.02335669, 0.20193386, 0.13999327, -0.08593997,
0.22940636, -0.01699461, -0.13609734, 0.0205682 , 0.04478336,
-0.06487859, 0.12278311, 0.25824267, -0.11856561, 0.00924652,
-0.05683991],
[-0.07845577, -0.02809871, 0.18017495, 0.0934044 , 0.03219789,
0.07012151, 0.03865771, 0.00123758, 0.07511085, 0.01999556,
-0.02662061, 0.10624584, 0.20883667, -0.02273149, 0.00469023,
-0.05817863]], dtype=float32),
array([[ 0.0059285 , 0.01392498, 0.14229313, 0.07545042, 0.00946952,
0.18953349, 0.00642931, -0.15386952, 0.01010698, 0.01384872,
-0.00726009, 0.13510276, 0.16494165, -0.08263297, 0.01707343,
-0.03762008],
[-0.07017908, -0.04930218, 0.14285906, 0.05723355, 0.06988001,
0.04076997, 0.05316475, -0.0261047 , 0.06291211, 0.01883025,
0.0061325 , 0.12620246, 0.16149819, -0.01529151, 0.050455 ,
-0.03946743]], dtype=float32),
array([[-0.00194316, -0.01631381, 0.1203991 , 0.06203739, 0.03219883,
0.14352044, 0.03928373, -0.14056513, 0.00291529, 0.01727359,
0.01229408, 0.15131217, 0.14297011, -0.0548197 , 0.05326709,
-0.02463808],
[-0.04386059, -0.05562492, 0.10245109, 0.0426993 , 0.09483594,
0.03656092, 0.0556438 , -0.07965589, 0.04327345, 0.01180707,
0.01022344, 0.14290853, 0.1213765 , -0.02787331, 0.10306676,
0.00820766]], dtype=float32),
array([[ 0.01521008, -0.03012705, 0.09070239, 0.05803963, 0.05991809,
0.12920979, 0.06138738, -0.17233956, -0.01331626, 0.01376779,
0.01249342, 0.1820184 , 0.11310001, -0.04977838, 0.10077294,
0.0205139 ],
[-0.04125331, -0.06516436, 0.11274034, 0.04927413, 0.07295317,
0.03652341, 0.03664432, -0.05592719, 0.06112453, 0.02948697,
0.02171964, 0.13186185, 0.1393885 , -0.00982405, 0.09507501,
-0.03440713]], dtype=float32),
array([[-0.0036198 , -0.03023063, 0.08077027, 0.01246183, 0.09018619,
0.10092171, 0.06540067, -0.13675798, -0.0074318 , 0.00661841,
0.0476986 , 0.1658352 , 0.08331098, -0.02784023, 0.07896069,
0.00924372],
[-0.05942168, -0.05818485, 0.09542555, -0.00622959, 0.10408131,
0.01689048, 0.03596107, -0.03097581, 0.06668746, 0.01272842,
0.05121479, 0.11581403, 0.10105841, 0.00028773, 0.07818841,
-0.03244215]], dtype=float32)]
我们看到这里输出的outputs是由5个array组成的list(5是序列长度),每个array由两个size是16的list组成(2表示2个样本,16表示输出符号有16个)

这里的outputs实际上应该对应seq2seq的输出,也就是下图中的W、X、Y、Z、EOS,也就是decoder_inputs[1:],也就是我们样本里的[7,9,11]和[9,11,13]

但是我们的decoder_inputs的结构是这样的:

[array([1, 1], dtype=int32), array([ 7, 29], dtype=int32), array([ 9, 31], dtype=int32), array([11, 33], dtype=int32), array([0, 0], dtype=int32)]

与这里的outputs稍有不同,所以不是直接的对应关系,那么到底是什么关系呢?我们先来看一个损失函数的说明:

sequence_loss(
logits,
targets,
weights,
average_across_timesteps=True,
average_across_batch=True,
softmax_loss_function=None,
name=None
)

这个函数的原理可以看成为如下公式(损失函数,目标词语的平均负对数概率最小):

其中logits是一个由多个2D的shape为[batch * num_decoder_symbols]的Tensor组成的list,我们这里batch就是2,num_decoder_symbols就是16,这里组成list的Tensor的个数是output_seq_len,所以我们刚才得到的outputs刚好符合。
其中targets是一个和logits一样长度(output_seq_len)的list,list里每一项是一个整数组成的1D的Tensor,每个Tensor的shape是[batch],数据类型是tf.int32,这刚好和我们的decoder_inputs[1:]也就是刚才说的W、X、Y、Z、EOS结构一样。
其中weights是一个和targets结构一样,只是数据类型是tf.float32。
所以这个函数就是用来计算加权交叉熵损失的,这里面的weights我们需要初始化他的占位符,如下:

target_weights = []
target_weights.append(tf.placeholder(tf.float32, shape=[None],
name="weight{0}".format(i)))

那么我们计算得出的损失值就是:

targets = [decoder_inputs[i + 1] for i in range(len(decoder_inputs) - 1)]
print(len(targets))
#loss = seq2seq.sequence_loss(outputs, targets, target_weights)

看到这里,其实我们遇到一个问题,这里的targets长度(为4)比decoder_inputs少了一个,为了让长度保持一致,需要我们对前面decoder_inputs的初始化做个调整,把长度加1。

那么问题来了,这里我们多了一个target_weights这个placeholder,那么我们用什么数据来填充这个占位符呢?因为我们要计算的是加权交叉熵损失,也就是对于有意义的数权重大,无意义的权重小,所以我们把targets中有值的赋值为1,没值的赋值为0,所有代码整理后如下:

# coding:utf-8
import numpy as np
import tensorflow as tf
from tensorflow.contrib.legacy_seq2seq.python.ops import seq2seq

# 输入序列长度
input_seq_len = 5
# 输出序列长度
output_seq_len = 5
# 空值填充0
PAD_ID = 0
# 输出序列起始标记
GO_ID = 1
# LSTM神经元size
size = 8
# 最大输入符号数
num_encoder_symbols = 10
# 最大输出符号数
num_decoder_symbols = 16

def get_samples():
"""构造样本数据

:return:
encoder_inputs: [array([0, 0], dtype=int32),
array([0, 0], dtype=int32),
array([1, 3], dtype=int32),
array([3, 5], dtype=int32),
array([5, 7], dtype=int32)]
decoder_inputs: [array([1, 1], dtype=int32),
array([7, 9], dtype=int32),
array([ 9, 11], dtype=int32),
array([11, 13], dtype=int32),
array([0, 0], dtype=int32)]
"""

train_set = [[[1, 3, 5], [7, 9, 11]], [[3, 5, 7], [9, 11, 13]]]
encoder_input_0 = [PAD_ID] * (input_seq_len - len(train_set[0][0]))+ train_set[0][0]
encoder_input_1 = [PAD_ID] * (input_seq_len - len(train_set[1][0]))+ train_set[1][0]
decoder_input_0 = [GO_ID] + train_set[0][1]+ [PAD_ID] * (output_seq_len - len(train_set[0][1]) - 1)
decoder_input_1 = [GO_ID] + train_set[1][1]+ [PAD_ID] * (output_seq_len - len(train_set[1][1]) - 1)

encoder_inputs = []
decoder_inputs = []
target_weights = []
for length_idx in range(input_seq_len):
encoder_inputs.append(np.array([encoder_input_0[length_idx],
encoder_input_1[length_idx]], dtype=np.int32))
for length_idx in range(output_seq_len):
decoder_inputs.append(np.array([decoder_input_0[length_idx],
decoder_input_1[length_idx]], dtype=np.int32))
target_weights.append(np.array([
0.0 if length_idx == output_seq_len - 1
or decoder_input_0[length_idx] == PAD_ID else 1.0,
0.0 if length_idx == output_seq_len - 1
or decoder_input_1[length_idx] == PAD_ID else 1.0,
], dtype=np.float32))
return encoder_inputs, decoder_inputs, target_weights

def get_model():
"""构造模型
"""

encoder_inputs = []
decoder_inputs = []
target_weights = []
for i in range(input_seq_len):
encoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="encoder{0}".format(i)))
for i in range(output_seq_len + 1):
decoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="decoder{0}".format(i)))
for i in range(output_seq_len):
target_weights.append(tf.placeholder(tf.float32, shape=[None],
name="weight{0}".format(i)))

# decoder_inputs左移一个时序作为targets
targets = [decoder_inputs[i + 1] for i in range(output_seq_len)]

cell = tf.contrib.rnn.BasicLSTMCell(size)

# 这里输出的状态我们不需要
outputs, _ = seq2seq.embedding_attention_seq2seq(
encoder_inputs,
decoder_inputs[:output_seq_len],
cell,
num_encoder_symbols=num_encoder_symbols,
num_decoder_symbols=num_decoder_symbols,
embedding_size=size,
output_projection=None,
feed_previous=False,
dtype=tf.float32)

# 计算加权交叉熵损失
loss = seq2seq.sequence_loss(outputs, targets, target_weights)
return encoder_inputs, decoder_inputs, target_weights, outputs, loss

def main():
with tf.Session() as sess:
sample_encoder_inputs, sample_decoder_inputs, sample_target_weights= get_samples()
encoder_inputs, decoder_inputs, target_weights, outputs, loss = get_model()

input_feed = {}
for l in range(input_seq_len):
input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
for l in range(output_seq_len):
input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
input_feed[target_weights[l].name] = sample_target_weights[l]
input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

sess.run(tf.global_variables_initializer())
loss = sess.run(loss, input_feed)
print(loss)

if __name__ == "__main__":
tf.reset_default_graph() ##清除默认图的堆栈,并设置全局图为默认图。
main()

运行结果为:2.81352
到这里远远没有结束,我们的旅程才刚刚开始,下面是怎么训练这个模型,

17.4.2.2训练模型

1)首先我们需要经过多轮计算让这里的loss变得很小,这就需要运用梯度下降来更新参数,我们先来看一下tensorflow提供给我们的梯度下降的类:
Class GradientDescentOptimizer的构造方法如下:

__init__(
learning_rate,
use_locking=False,
name='GradientDescent'
)

其中关键就是第一个参数:学习率learning_rate
他的另外一个方法是计算梯度

compute_gradients(
loss,
var_list=None,
gate_gradients=GATE_OP
aggregation_method=None,
colocate_gradients_with_ops=False,
grad_loss=None
)

其中关键参数loss就是传入的误差值,他的返回值是(gradient, variable)组成的list。
再看另外一个方法是更新参数:

apply_gradients(
grads_and_vars,
global_step=None,
name=None
)

其中grads_and_vars就是compute_gradients的返回值。
那么根据loss计算梯度并更新参数的方法如下:

learning_rate = 0.1
opt = tf.train.GradientDescentOptimizer(learning_rate)
update = opt.apply_gradients(opt.compute_gradients(loss))

所以,我们把get_model()增加以上三行,具体如下:

def get_model():
"""构造模型
"""

encoder_inputs = []
decoder_inputs = []
target_weights = []
for i in range(input_seq_len):
encoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="encoder{0}".format(i)))
for i in range(output_seq_len + 1):
decoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="decoder{0}".format(i)))
for i in range(output_seq_len):
target_weights.append(tf.placeholder(tf.float32, shape=[None],
name="weight{0}".format(i)))

# decoder_inputs左移一个时序作为targets
targets = [decoder_inputs[i + 1] for i in range(output_seq_len)]

cell = tf.contrib.rnn.BasicLSTMCell(size)

# 这里输出的状态我们不需要
outputs, _ = seq2seq.embedding_attention_seq2seq(
encoder_inputs,
decoder_inputs[:output_seq_len],
cell,
num_encoder_symbols=num_encoder_symbols,
num_decoder_symbols=num_decoder_symbols,
embedding_size=size,
output_projection=None,
feed_previous=False,
dtype=tf.float32)

# 计算加权交叉熵损失
loss = seq2seq.sequence_loss(outputs, targets, target_weights)
learning_rate = 0.1
opt = tf.train.GradientDescentOptimizer(learning_rate)
update = opt.apply_gradients(opt.compute_gradients(loss))
return encoder_inputs, decoder_inputs, target_weights, outputs, loss,update

我们对main函数增加个循环迭代,具体如下:

def main():
with tf.Session() as sess:
sample_encoder_inputs, sample_decoder_inputs, sample_target_weights= get_samples()
encoder_inputs, decoder_inputs, target_weights, outputs, loss, update= get_model()

input_feed = {}
for l in range(input_seq_len):
input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
for l in range(output_seq_len):
input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
input_feed[target_weights[l].name] = sample_target_weights[l]
input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

sess.run(tf.global_variables_initializer())
while True:
[loss_ret, _] = sess.run([loss, update], input_feed)
print(loss_ret)

修改后,完整程序如下:

import numpy as np
import tensorflow as tf
from tensorflow.contrib.legacy_seq2seq.python.ops import seq2seq

# 输入序列长度
input_seq_len = 5
# 输出序列长度
output_seq_len = 5
# 空值填充0
PAD_ID = 0
# 输出序列起始标记
GO_ID = 1
# LSTM神经元size
size = 8
# 最大输入符号数
num_encoder_symbols = 10
# 最大输出符号数
num_decoder_symbols = 16

def get_samples():
"""构造样本数据

:return:
encoder_inputs: [array([0, 0], dtype=int32),
array([0, 0], dtype=int32),
array([1, 3], dtype=int32),
array([3, 5], dtype=int32),
array([5, 7], dtype=int32)]
decoder_inputs: [array([1, 1], dtype=int32),
array([7, 9], dtype=int32),
array([ 9, 11], dtype=int32),
array([11, 13], dtype=int32),
array([0, 0], dtype=int32)]
"""

train_set = [[[1, 3, 5], [7, 9, 11]], [[3, 5, 7], [9, 11, 13]]]
encoder_input_0 = [PAD_ID] * (input_seq_len - len(train_set[0][0]))+ train_set[0][0]
encoder_input_1 = [PAD_ID] * (input_seq_len - len(train_set[1][0]))+ train_set[1][0]
decoder_input_0 = [GO_ID] + train_set[0][1]+ [PAD_ID] * (output_seq_len - len(train_set[0][1]) - 1)
decoder_input_1 = [GO_ID] + train_set[1][1]+ [PAD_ID] * (output_seq_len - len(train_set[1][1]) - 1)

encoder_inputs = []
decoder_inputs = []
target_weights = []
for length_idx in range(input_seq_len):
encoder_inputs.append(np.array([encoder_input_0[length_idx],
encoder_input_1[length_idx]], dtype=np.int32))
for length_idx in range(output_seq_len):
decoder_inputs.append(np.array([decoder_input_0[length_idx],
decoder_input_1[length_idx]], dtype=np.int32))
target_weights.append(np.array([
0.0 if length_idx == output_seq_len - 1
or decoder_input_0[length_idx] == PAD_ID else 1.0,
0.0 if length_idx == output_seq_len - 1
or decoder_input_1[length_idx] == PAD_ID else 1.0,
], dtype=np.float32))
return encoder_inputs, decoder_inputs, target_weights

def get_model():
"""构造模型
"""

encoder_inputs = []
decoder_inputs = []
target_weights = []
for i in range(input_seq_len):
encoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="encoder{0}".format(i)))
for i in range(output_seq_len + 1):
decoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="decoder{0}".format(i)))
for i in range(output_seq_len):
target_weights.append(tf.placeholder(tf.float32, shape=[None],
name="weight{0}".format(i)))

# decoder_inputs左移一个时序作为targets
targets = [decoder_inputs[i + 1] for i in range(output_seq_len)]

cell = tf.contrib.rnn.BasicLSTMCell(size)

# 这里输出的状态我们不需要
outputs, _ = seq2seq.embedding_attention_seq2seq(
encoder_inputs,
decoder_inputs[:output_seq_len],
cell,
num_encoder_symbols=num_encoder_symbols,
num_decoder_symbols=num_decoder_symbols,
embedding_size=size,
output_projection=None,
feed_previous=False,
dtype=tf.float32)

# 计算加权交叉熵损失
loss = seq2seq.sequence_loss(outputs, targets, target_weights)
learning_rate = 0.1
opt = tf.train.GradientDescentOptimizer(learning_rate)
update = opt.apply_gradients(opt.compute_gradients(loss))
return encoder_inputs, decoder_inputs, target_weights, outputs, loss,update

def main():
with tf.Session() as sess:
sample_encoder_inputs, sample_decoder_inputs, sample_target_weights= get_samples()
encoder_inputs, decoder_inputs, target_weights, outputs, loss, update= get_model()

input_feed = {}
for l in range(input_seq_len):
input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
for l in range(output_seq_len):
input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
input_feed[target_weights[l].name] = sample_target_weights[l]
input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

sess.run(tf.global_variables_initializer())
#循环2000次
for step in range(2000):
[loss_ret, _] = sess.run([loss, update], input_feed)
if step%100 ==0:
print(loss_ret)

if __name__ == "__main__":
tf.reset_default_graph()
main()

再次运行代码后,我们看到loss_ret唰唰的收敛,如下:
2.84531
0.870862
0.297892
0.0739714
0.0334021
0.0200548
0.0137981
0.0102763
0.0080602
0.0065568
0.00548026
0.00467716
0.00405862
0.00356976
0.00317507
0.00285076
0.00258015
0.00235146
0.00215595
0.0019872
看来我们的训练结果还不错,接下来就是实现预测的逻辑了,就是我们只输入样本的encoder_input,看能不能自动预测出decoder_input。

17.4.2.3测试模型

1)在对新数据进行预测之前,我们需要把训练好的模型保存起来,以便重新启动做预测时能够加载:

def get_model():
...
saver = tf.train.Saver(tf.global_variables())
return ..., saver

在训练结束后执行

saver.save(sess, './model/chatbot/demo')

这样模型会存储到./model目录下以demo开头的一些文件中,之后我们要加载时就先调用:

saver.restore(sess, './model/chatbot/demo')

2)因为我们在做预测的时候,原则上不能有decoder_inputs输入了,所以在执行时的decoder_inputs就要取前一个时序的输出,这时候embedding_attention_seq2seq的feed_previous参数就派上用场了,这个参数的含义就是:若为True则decoder里每一步输入都用前一步的输出来填充,如下图:

所以,我们的get_model需要传递参数来区分训练和预测可通过不同的feed_previous配置,另外,考虑到预测时main函数也是不同的,索性我们分开两个函数来分别做train和predict,整理好的一份完整代码如下(为了更好理解,完整代码和上面稍有出入,请以这份代码为准):

import numpy as np
import tensorflow as tf
import os
import sys
from tensorflow.contrib.legacy_seq2seq.python.ops import seq2seq

output_dir='./model/chatbot/demo'
if not os.path.exists(output_dir):
os.makedirs(output_dir)

# 输入序列长度
input_seq_len = 5
# 输出序列长度
output_seq_len = 5
# 空值填充0
PAD_ID = 0
# 输出序列起始标记
GO_ID = 1
# 结尾标记
EOS_ID = 2
# LSTM神经元size
size = 8
# 最大输入符号数
num_encoder_symbols = 10
# 最大输出符号数
num_decoder_symbols = 16
# 学习率
learning_rate = 0.1

def get_samples():
"""构造样本数据

:return:
encoder_inputs: [array([0, 0], dtype=int32),
array([0, 0], dtype=int32),
array([1, 3], dtype=int32),
array([3, 5], dtype=int32),
array([5, 7], dtype=int32)]
decoder_inputs: [array([1, 1], dtype=int32),
array([7, 9], dtype=int32),
array([ 9, 11], dtype=int32),
array([11, 13], dtype=int32),
array([0, 0], dtype=int32)]
"""

train_set = [[[1, 3, 5], [7, 9, 11]], [[3, 5, 7], [9, 11, 13]]]
encoder_input_0 = [PAD_ID] * (input_seq_len - len(train_set[0][0]))+ train_set[0][0]
encoder_input_1 = [PAD_ID] * (input_seq_len - len(train_set[1][0]))+ train_set[1][0]
decoder_input_0 = [GO_ID] + train_set[0][1]+ [EOS_ID] * (output_seq_len - len(train_set[0][1]) - 1)
decoder_input_1 = [GO_ID] + train_set[1][1]+ [EOS_ID] * (output_seq_len - len(train_set[1][1]) - 1)

encoder_inputs = []
decoder_inputs = []
target_weights = []
for length_idx in range(input_seq_len):
encoder_inputs.append(np.array([encoder_input_0[length_idx],
encoder_input_1[length_idx]], dtype=np.int32))
for length_idx in range(output_seq_len):
decoder_inputs.append(np.array([decoder_input_0[length_idx],
decoder_input_1[length_idx]], dtype=np.int32))
target_weights.append(np.array([
0.0 if length_idx == output_seq_len - 1
or decoder_input_0[length_idx] == PAD_ID else 1.0,
0.0 if length_idx == output_seq_len - 1
or decoder_input_1[length_idx] == PAD_ID else 1.0,
], dtype=np.float32))
return encoder_inputs, decoder_inputs, target_weights

def get_model(feed_previous=False):
"""构造模型
"""

encoder_inputs = []
decoder_inputs = []
target_weights = []
for i in range(input_seq_len):
encoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="encoder{0}".format(i)))
for i in range(output_seq_len + 1):
decoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="decoder{0}".format(i)))
for i in range(output_seq_len):
target_weights.append(tf.placeholder(tf.float32, shape=[None],
name="weight{0}".format(i)))

# decoder_inputs左移一个时序作为targets
targets = [decoder_inputs[i + 1] for i in range(output_seq_len)]

cell = tf.contrib.rnn.BasicLSTMCell(size)

# 这里输出的状态我们不需要
outputs, _ = seq2seq.embedding_attention_seq2seq(
encoder_inputs,
decoder_inputs[:output_seq_len],
cell,
num_encoder_symbols=num_encoder_symbols,
num_decoder_symbols=num_decoder_symbols,
embedding_size=size,
output_projection=None,
feed_previous=feed_previous,
dtype=tf.float32)

# 计算加权交叉熵损失
loss = seq2seq.sequence_loss(outputs, targets, target_weights)
# 梯度下降优化器
opt = tf.train.GradientDescentOptimizer(learning_rate)
# 优化目标:让loss最小化
update = opt.apply_gradients(opt.compute_gradients(loss))
# 模型持久化
saver = tf.train.Saver(tf.global_variables())
return encoder_inputs, decoder_inputs, target_weights,outputs, loss, update, saver, targets
#return encoder_inputs, decoder_inputs, target_weights, outputs, loss,update

def train():
with tf.Session() as sess:
sample_encoder_inputs, sample_decoder_inputs, sample_target_weights= get_samples()
encoder_inputs, decoder_inputs, target_weights, outputs, loss, update,saver, targets= get_model()

input_feed = {}
for l in range(input_seq_len):
input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
for l in range(output_seq_len):
input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
input_feed[target_weights[l].name] = sample_target_weights[l]
input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

sess.run(tf.global_variables_initializer())
for i in range(2000):
[loss_ret, _] = sess.run([loss, update], input_feed)
if i %100 == 0:
print( 'step=', i, 'loss=', loss_ret)

# 模型持久化
saver.save(sess, output_dir)

def predict():
"""
预测过程
"""

with tf.Session() as sess:
sample_encoder_inputs, sample_decoder_inputs, sample_target_weights= get_samples()
encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver, targets= get_model(feed_previous=True)
# 从文件恢复模型
saver.restore(sess, output_dir)

input_feed = {}
for l in range(input_seq_len):
input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
for l in range(output_seq_len):
input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
input_feed[target_weights[l].name] = sample_target_weights[l]
input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

# 预测输出
outputs = sess.run(outputs, input_feed)
# 一共试验样本有2个,所以分别遍历
for sample_index in range(2):
# 因为输出数据每一个是num_decoder_symbols维的
# 因此找到数值最大的那个就是预测的id,就是这里的argmax函数的功能
outputs_seq = [int(np.argmax(logit[sample_index], axis=0)) for logit in outputs]
# 如果是结尾符,那么后面的语句就不输出了
if EOS_ID in outputs_seq:
outputs_seq = outputs_seq[:outputs_seq.index(EOS_ID)]
outputs_seq = [str(v) for v in outputs_seq]
print(" ".join(outputs_seq))

if __name__ == "__main__":
tf.reset_default_graph()
train()
tf.reset_default_graph()
predict()

运行结果:
step= 0 loss= 2.81223
step= 100 loss= 1.31877
step= 200 loss= 0.459723
step= 300 loss= 0.144857
step= 400 loss= 0.0428859
step= 500 loss= 0.0206691
step= 600 loss= 0.0127222
step= 700 loss= 0.00889739
step= 800 loss= 0.00671659
step= 900 loss= 0.00533214
step= 1000 loss= 0.0043858
step= 1100 loss= 0.00370342
step= 1200 loss= 0.00319082
step= 1300 loss= 0.00279343
step= 1400 loss= 0.00247735
step= 1500 loss= 0.00222061
step= 1600 loss= 0.00200841
step= 1700 loss= 0.00183036
step= 1800 loss= 0.00167907
step= 1900 loss= 0.00154906
INFO:tensorflow:Restoring parameters from ./model/chatbot/demo
7 9 11
9 11 13

至此,我们算有了小小的成就了。
比较仔细的人会发现,在做预测的时候依然是按照完整的encoder_inputs和decoder_inputs计算的,那么怎么能证明模型不是直接使用了decoder_inputs来预测出的输出呢?那么我们来继续改进predict,让我们可以手工输入一串数字(只有encoder部分),看看模型能不能预测出输出

17.4.2.4优化模型

首先我们实现一个从输入空格分隔的数字id串,转成预测用的encoder、decoder、target_weight的函数。

def seq_to_encoder(input_seq):
"""从输入空格分隔的数字id串,转成预测用的encoder、decoder、target_weight等
"""

input_seq_array = [int(v) for v in input_seq.split()]
encoder_input = [PAD_ID] * (input_seq_len - len(input_seq_array)) + input_seq_array
decoder_input = [GO_ID] + [PAD_ID] * (output_seq_len - 1)
encoder_inputs = [np.array([v], dtype=np.int32) for v in encoder_input]
decoder_inputs = [np.array([v], dtype=np.int32) for v in decoder_input]
target_weights = [np.array([1.0], dtype=np.float32)] * output_seq_len
return encoder_inputs, decoder_inputs, target_weights

然后我们改写predict函数如下:

def predict():
"""
预测过程
"""

with tf.Session() as sess:
encoder_inputs, decoder_inputs, target_weights,outputs, loss, update, saver, targets=get_model(feed_previous=True)
#encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver= get_model(feed_previous=True)
saver.restore(sess, output_dir)
sys.stdout.write("> ")
#sys.stdout.flush()
#input_seq = sys.stdin.readline()
input_seq="5 7 9"
while input_seq:
input_seq = input_seq.strip()
sample_encoder_inputs, sample_decoder_inputs, sample_target_weights= seq_to_encoder(input_seq)

input_feed = {}
for l in range(input_seq_len):
input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
for l in range(output_seq_len):
input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
input_feed[target_weights[l].name] = sample_target_weights[l]
input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

# 预测输出
outputs_seq = sess.run(outputs, input_feed)
# 因为输出数据每一个是num_decoder_symbols维的
# 因此找到数值最大的那个就是预测的id,就是这里的argmax函数的功能
outputs_seq = [int(np.argmax(logit[0], axis=0)) for logit in outputs_seq]
# 如果是结尾符,那么后面的语句就不输出了
if EOS_ID in outputs_seq:
outputs_seq = outputs_seq[:outputs_seq.index(EOS_ID)]
outputs_seq = [str(v) for v in outputs_seq]
print( " ".join(outputs_seq))

sys.stdout.write(">")
sys.stdout.flush()
input_seq = sys.stdin.readline()
#input_seq="1 3 5"

重新执行predict如下:

tf.reset_default_graph()
predict()

运行结果:
INFO:tensorflow:Restoring parameters from ./model/chatbot/demo
> 9 11 13
>
这个结果不错,输入5 7 9 预测为9 11 13。

那么我们如果输入一个新的测试样本会怎么样呢?他能不能预测出我们是在推导奇数序列呢?
当我们输入7 9 11的时候发现他报错了,原因是我们设置了num_encoder_symbols = 10,而11无法表达了,所以我们为了训练一个强大的模型,我们修改参数并增加样本,如下:

# 最大输入符号数
num_encoder_symbols = 32
# 最大输出符号数
num_decoder_symbols = 32
……
train_set = [
[[5, 7, 9], [11, 13, 15, EOS_ID]],
[[7, 9, 11], [13, 15, 17, EOS_ID]],
[[15, 17, 19], [21, 23, 25, EOS_ID]]
]

我们把迭代次数扩大到2000次
修改参数,添加函数,修改predict函数,最后综合在一起,如下:

import numpy as np
import tensorflow as tf
import os
import sys
from tensorflow.contrib.legacy_seq2seq.python.ops import seq2seq

output_dir='./model/chatbot/demo'
if not os.path.exists(output_dir):
os.makedirs(output_dir)

# 输入序列长度
input_seq_len = 5
# 输出序列长度
output_seq_len = 5
# 空值填充0
PAD_ID = 0
# 输出序列起始标记
GO_ID = 1
# 结尾标记
EOS_ID = 2
# LSTM神经元size
size = 8
# 最大输入符号数
#num_encoder_symbols = 10
num_encoder_symbols = 32
# 最大输出符号数
#num_decoder_symbols = 16
num_decoder_symbols = 32
# 学习率
learning_rate = 0.1

def seq_to_encoder(input_seq):
"""从输入空格分隔的数字id串,转成预测用的encoder、decoder、target_weight等
"""

input_seq_array = [int(v) for v in input_seq.split()]
encoder_input = [PAD_ID] * (input_seq_len - len(input_seq_array)) + input_seq_array
decoder_input = [GO_ID] + [PAD_ID] * (output_seq_len - 1)
encoder_inputs = [np.array([v], dtype=np.int32) for v in encoder_input]
decoder_inputs = [np.array([v], dtype=np.int32) for v in decoder_input]
target_weights = [np.array([1.0], dtype=np.float32)] * output_seq_len
return encoder_inputs, decoder_inputs, target_weights

def get_samples():
"""构造样本数据

:return:
encoder_inputs: [array([0, 0], dtype=int32),
array([0, 0], dtype=int32),
array([1, 3], dtype=int32),
array([3, 5], dtype=int32),
array([5, 7], dtype=int32)]
decoder_inputs: [array([1, 1], dtype=int32),
array([7, 9], dtype=int32),
array([ 9, 11], dtype=int32),
array([11, 13], dtype=int32),
array([0, 0], dtype=int32)]
"""

#train_set = [[[1, 3, 5], [7, 9, 11, EOS_ID]], [[3, 5, 7], [9, 11, 13, EOS_ID]]]
train_set = [[[5, 7, 9], [11, 13, 15, EOS_ID]],
[[7, 9, 11], [13, 15, 17, EOS_ID]],
[[15, 17, 19], [21, 23, 25, EOS_ID]]]
encoder_input_0 = [PAD_ID] * (input_seq_len - len(train_set[0][0]))+ train_set[0][0]
encoder_input_1 = [PAD_ID] * (input_seq_len - len(train_set[1][0]))+ train_set[1][0]
decoder_input_0 = [GO_ID] + train_set[0][1]+ [EOS_ID] * (output_seq_len - len(train_set[0][1]) - 2)
decoder_input_1 = [GO_ID] + train_set[1][1]+ [EOS_ID] * (output_seq_len - len(train_set[1][1]) - 2)

encoder_inputs = []
decoder_inputs = []
target_weights = []
for length_idx in range(input_seq_len):
encoder_inputs.append(np.array([encoder_input_0[length_idx],
encoder_input_1[length_idx]], dtype=np.int32))
for length_idx in range(output_seq_len):
decoder_inputs.append(np.array([decoder_input_0[length_idx],
decoder_input_1[length_idx]], dtype=np.int32))
target_weights.append(np.array([
0.0 if length_idx == output_seq_len - 1
or decoder_input_0[length_idx] == PAD_ID else 1.0,
0.0 if length_idx == output_seq_len - 1
or decoder_input_1[length_idx] == PAD_ID else 1.0,
], dtype=np.float32))
return encoder_inputs, decoder_inputs, target_weights

def get_model(feed_previous=False):
"""构造模型
"""

encoder_inputs = []
decoder_inputs = []
target_weights = []
for i in range(input_seq_len):
encoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="encoder{0}".format(i)))
for i in range(output_seq_len + 1):
decoder_inputs.append(tf.placeholder(tf.int32, shape=[None],
name="decoder{0}".format(i)))
for i in range(output_seq_len):
target_weights.append(tf.placeholder(tf.float32, shape=[None],
name="weight{0}".format(i)))

# decoder_inputs左移一个时序作为targets
targets = [decoder_inputs[i + 1] for i in range(output_seq_len)]

cell = tf.contrib.rnn.BasicLSTMCell(size)

# 这里输出的状态我们不需要
outputs, _ = seq2seq.embedding_attention_seq2seq(
encoder_inputs,
decoder_inputs[:output_seq_len],
cell,
num_encoder_symbols=num_encoder_symbols,
num_decoder_symbols=num_decoder_symbols,
embedding_size=size,
output_projection=None,
feed_previous=feed_previous,
dtype=tf.float32)

# 计算加权交叉熵损失
loss = seq2seq.sequence_loss(outputs, targets, target_weights)
# 梯度下降优化器
opt = tf.train.GradientDescentOptimizer(learning_rate)
# 优化目标:让loss最小化
update = opt.apply_gradients(opt.compute_gradients(loss))
# 模型持久化
saver = tf.train.Saver(tf.global_variables())
return encoder_inputs, decoder_inputs, target_weights,outputs, loss, update, saver, targets
#return encoder_inputs, decoder_inputs, target_weights, outputs, loss,update

def train():
with tf.Session() as sess:
sample_encoder_inputs, sample_decoder_inputs, sample_target_weights= get_samples()
encoder_inputs, decoder_inputs, target_weights, outputs, loss, update,saver, targets= get_model()

input_feed = {}
for l in range(input_seq_len):
input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
for l in range(output_seq_len):
input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
input_feed[target_weights[l].name] = sample_target_weights[l]
input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

sess.run(tf.global_variables_initializer())
for i in range(2000):
[loss_ret, _] = sess.run([loss, update], input_feed)
if i %100 == 0:
print( 'step=', i, 'loss=', loss_ret)

# 模型持久化
saver.save(sess, output_dir)

def predict():
"""
预测过程
"""

with tf.Session() as sess:
encoder_inputs, decoder_inputs, target_weights,outputs, loss, update, saver, targets=get_model(feed_previous=True)
#encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver= get_model(feed_previous=True)
saver.restore(sess, output_dir)
sys.stdout.write("> ")
#sys.stdout.flush()
#input_seq = sys.stdin.readline()
input_seq="9 11 13"
while input_seq:
input_seq = input_seq.strip()
sample_encoder_inputs, sample_decoder_inputs, sample_target_weights= seq_to_encoder(input_seq)

input_feed = {}
for l in range(input_seq_len):
input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
for l in range(output_seq_len):
input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
input_feed[target_weights[l].name] = sample_target_weights[l]
input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

# 预测输出
outputs_seq = sess.run(outputs, input_feed)
# 因为输出数据每一个是num_decoder_symbols维的
# 因此找到数值最大的那个就是预测的id,就是这里的argmax函数的功能
outputs_seq = [int(np.argmax(logit[0], axis=0)) for logit in outputs_seq]
# 如果是结尾符,那么后面的语句就不输出了
if EOS_ID in outputs_seq:
outputs_seq = outputs_seq[:outputs_seq.index(EOS_ID)]
outputs_seq = [str(v) for v in outputs_seq]
print( " ".join(outputs_seq))

sys.stdout.write(">")
sys.stdout.flush()
input_seq = sys.stdin.readline()
#input_seq="1 3 5"

if __name__ == "__main__":
tf.reset_default_graph()
train()
tf.reset_default_graph()
predict()

运行结果为:
step= 0 loss= 3.47917
step= 100 loss= 1.15968
step= 200 loss= 0.280037
step= 300 loss= 0.0783488
step= 400 loss= 0.0339385
step= 500 loss= 0.0198116
step= 600 loss= 0.0133718
step= 700 loss= 0.00981527
step= 800 loss= 0.00761077
step= 900 loss= 0.00613428
step= 1000 loss= 0.00508853
step= 1100 loss= 0.00431588
step= 1200 loss= 0.00372594
step= 1300 loss= 0.00326322
step= 1400 loss= 0.00289231
step= 1500 loss= 0.00258943
step= 1600 loss= 0.00233817
step= 1700 loss= 0.00212704
step= 1800 loss= 0.00194738
step= 1900 loss= 0.00179307
INFO:tensorflow:Restoring parameters from ./model/chatbot/demo
> 13 15
输入:9 11 13 预测为:13 15
说明修改参数后,可以输入大于10的数,而且预测结果为13 15 效果还不错。

17.4.3 用文本数据训练seq2seq模型

到现在,我们依然在玩的只是数字的游戏,怎么样才能和中文对话扯上关系呢?很简单,在训练时把中文词汇转成id号,在预测时,把预测到的id转成中文就可以了
1)首先我们需要对中文分词,为此创建建一个WordToken类,其中load函数负责加载样本,并生成word2id_dict和id2word_dict词典,word2id函数负责将词汇转成id,id2word负责将id转成词汇:

import sys
import jieba

class WordToken(object):
def __init__(self):
# 最小起始id号, 保留的用于表示特殊标记
self.START_ID = 4
self.word2id_dict = {}
self.id2word_dict = {}

def load_file_list(self, file_list, min_freq):
"""
加载样本文件列表,全部切词后统计词频,按词频由高到低排序后顺次编号
并存到self.word2id_dict和self.id2word_dict中
"""

words_count = {}
for file in file_list:
with open(file, 'r') as file_object:
for line in file_object.readlines():
line = line.strip()
seg_list = jieba.cut(line)
for str in seg_list:
if str in words_count:
words_count[str] = words_count[str] + 1
else:
words_count[str] = 1

sorted_list = [[v[1], v[0]] for v in words_count.items()]
sorted_list.sort(reverse=True)
for index, item in enumerate(sorted_list):
word = item[1]
if item[0] < min_freq: break self.word2id_dict[word] = self.START_ID + index self.id2word_dict[self.START_ID + index] = word return index def word2id(self, word): if not isinstance(word, unicode): print("Exception: error word not unicode") sys.exit(1) if word in self.word2id_dict: return self.word2id_dict[word] else: return None def id2word(self, id): id = int(id) if id in self.id2word_dict: return self.id2word_dict[id] else: return None 2)定义获取训练集的函数get_train_set,如下: def get_train_set(): global num_encoder_symbols, num_decoder_symbols train_set = [] with open('./data/chatbot/question.txt', 'r') as question_file: with open('./data/chatbot/answer.txt', 'r') as answer_file: while True: question = question_file.readline() answer = answer_file.readline() if question and answer: question = question.strip() answer = answer.strip() question_id_list = get_id_list_from(question) answer_id_list = get_id_list_from(answer) answer_id_list.append(EOS_ID) train_set.append([question_id_list, answer_id_list]) else: break return train_set

3)定义获取句子id的函数 get_id_list_from

 def get_id_list_from(sentence): sentence_id_list = [] seg_list = jieba.cut(sentence) for str in seg_list: id = wordToken.word2id(str) if id: sentence_id_list.append(wordToken.word2id(str)) return sentence_id_list

4)导入文件,并自动获取num_encoder_symbols、num_decoder_symbols

 #import word_token import jieba wordToken = WordToken() # 放在全局的位置,为了动态算出num_encoder_symbols和num_decoder_symbols max_token_id = wordToken.load_file_list(['./data/chatbot/question.txt', './data/chatbot/answer.txt'],10) num_encoder_symbols = max_token_id + 5 num_decoder_symbols = max_token_id + 5

结果显示num_encoder_symbols、num_decoder_symbols均为90 5)重新修改预测编码

 def predict(): """ 预测过程 """ with tf.Session() as sess: encoder_inputs, decoder_inputs, target_weights,outputs, loss, update, saver, targets= get_model(feed_previous=True) saver.restore(sess, output_dir) sys.stdout.write("> ")
sys.stdout.flush()
input_seq = sys.stdin.readline()
while input_seq:
input_seq = input_seq.strip()
input_id_list = get_id_list_from(input_seq)
if (len(input_id_list)):
sample_encoder_inputs, sample_decoder_inputs, sample_target_weights= seq_to_encoder(' '.join([str(v) for v in input_id_list]))

input_feed = {}
for l in range(input_seq_len):
input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
for l in range(output_seq_len):
input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
input_feed[target_weights[l].name] = sample_target_weights[l]
input_feed[decoder_inputs[output_seq_len].name]= np.zeros([2], dtype=np.int32)

# 预测输出
outputs_seq = sess.run(outputs, input_feed)
# 因为输出数据每一个是num_decoder_symbols维的
# 因此找到数值最大的那个就是预测的id,就是这里的argmax函数的功能
outputs_seq = [int(np.argmax(logit[0], axis=0)) for logit in outputs_seq]
# 如果是结尾符,那么后面的语句就不输出了
if EOS_ID in outputs_seq:
outputs_seq = outputs_seq[:outputs_seq.index(EOS_ID)]
outputs_seq = [wordToken.id2word(v) for v in outputs_seq]
print( " ".join(outputs_seq))
else:
print("WARN:词汇不在服务区")

sys.stdout.write("> ")
sys.stdout.flush()
input_seq = sys.stdin.readline()

6)优化学习率参数
我们发现模型收敛的非常慢,因为我们设置的学习率是0.1,我们希望首先学习率大一些,每当下一步的loss和上一步相比反弹(反而增大)的时候我们再尝试降低学习率,方法如下,首先我们不再直接用learning_rate,而是初始化一个学习率:

init_learning_rate = 1

然后在get_model中创建一个变量,并用init_learning_rate初始化:

learning_rate = tf.Variable(float(init_learning_rate), trainable=False, dtype=tf.float32)

之后再创建一个操作,目的是再适当的时候把学习率打9折:

learning_rate_decay_op = learning_rate.assign(learning_rate * 0.9)

7)整合后的程序
其中对学习率、优化算法等进行调整,调整后的loss循环10000次后,loss= 0.758005 。

import sys
import numpy as np
import tensorflow as tf
from tensorflow.contrib.legacy_seq2seq.python.ops import seq2seq
import jieba
import random

# 输入序列长度
input_seq_len = 5
# 输出序列长度
output_seq_len = 5
# 空值填充0
PAD_ID = 0
# 输出序列起始标记
GO_ID = 1
# 结尾标记
EOS_ID = 2
# LSTM神经元size
size = 8
# 初始学习率
init_learning_rate = 0.001
# 在样本中出现频率超过这个值才会进入词表
min_freq = 10

wordToken = WordToken()

output_dir='./model/chatbot/demo'
if not os.path.exists(output_dir):
os.makedirs(output_dir)

# 放在全局的位置,为了动态算出num_encoder_symbols和num_decoder_symbols
max_token_id = wordToken.load_file_list(['./data/chatbot/question.txt', './data/chatbot/answer.txt'], min_freq)
num_encoder_symbols = max_token_id + 5
num_decoder_symbols = max_token_id + 5

def get_id_list_from(sentence):
sentence_id_list = []
seg_list = jieba.cut(sentence)
for str in seg_list:
id = wordToken.word2id(str)
if id:
sentence_id_list.append(wordToken.word2id(str))
return sentence_id_list

def get_train_set():
global num_encoder_symbols, num_decoder_symbols
train_set = []
with open('./data/chatbot/question.txt', 'r') as question_file:
with open('./data/chatbot/answer.txt', 'r') as answer_file:
while True:
question = question_file.readline()
answer = answer_file.readline()
if question and answer:
question = question.strip()
answer = answer.strip()

question_id_list = get_id_list_from(question)
answer_id_list = get_id_list_from(answer)
if len(question_id_list) > 0 and len(answer_id_list) > 0:
answer_id_list.append(EOS_ID)
train_set.append([question_id_list, answer_id_list])
else:
break
return train_set

def get_samples(train_set, batch_num):
"""构造样本数据
:return:
encoder_inputs: [array([0, 0], dtype=int32), array([0, 0], dtype=int32), array([5, 5], dtype=int32),
array([7, 7], dtype=int32), array([9, 9], dtype=int32)]
decoder_inputs: [array([1, 1], dtype=int32), array([11, 11], dtype=int32), array([13, 13], dtype=int32),
array([15, 15], dtype=int32), array([2, 2], dtype=int32)]
"""

# train_set = [[[5, 7, 9], [11, 13, 15, EOS_ID]], [[7, 9, 11], [13, 15, 17, EOS_ID]], [[15, 17, 19], [21, 23, 25, EOS_ID]]]
raw_encoder_input = []
raw_decoder_input = []
if batch_num >= len(train_set):
batch_train_set = train_set
else:
random_start = random.randint(0, len(train_set)-batch_num)
batch_train_set = train_set[random_start:random_start+batch_num]
for sample in batch_train_set:
raw_encoder_input.append([PAD_ID] * (input_seq_len - len(sample[0])) + sample[0])
raw_decoder_input.append([GO_ID] + sample[1] + [PAD_ID] * (output_seq_len - len(sample[1]) - 1))

encoder_inputs = []
decoder_inputs = []
target_weights = []

for length_idx in range(input_seq_len):
encoder_inputs.append(np.array([encoder_input[length_idx] for encoder_input in raw_encoder_input], dtype=np.int32))
for length_idx in range(output_seq_len):
decoder_inputs.append(np.array([decoder_input[length_idx] for decoder_input in raw_decoder_input], dtype=np.int32))
target_weights.append(np.array([
0.0 if length_idx == output_seq_len - 1 or decoder_input[length_idx] == PAD_ID else 1.0 for decoder_input in raw_decoder_input
], dtype=np.float32))
return encoder_inputs, decoder_inputs, target_weights

def seq_to_encoder(input_seq):
"""从输入空格分隔的数字id串,转成预测用的encoder、decoder、target_weight等
"""

input_seq_array = [int(v) for v in input_seq.split()]
encoder_input = [PAD_ID] * (input_seq_len - len(input_seq_array)) + input_seq_array
decoder_input = [GO_ID] + [PAD_ID] * (output_seq_len - 1)
encoder_inputs = [np.array([v], dtype=np.int32) for v in encoder_input]
decoder_inputs = [np.array([v], dtype=np.int32) for v in decoder_input]
target_weights = [np.array([1.0], dtype=np.float32)] * output_seq_len
return encoder_inputs, decoder_inputs, target_weights

def get_model(feed_previous=False):
"""构造模型
"""


learning_rate = tf.Variable(float(init_learning_rate), trainable=False, dtype=tf.float32)
learning_rate_decay_op = learning_rate.assign(learning_rate * 0.9)

encoder_inputs = []
decoder_inputs = []
target_weights = []
for i in range(input_seq_len):
encoder_inputs.append(tf.placeholder(tf.int32, shape=[None], name="encoder{0}".format(i)))
for i in range(output_seq_len + 1):
decoder_inputs.append(tf.placeholder(tf.int32, shape=[None], name="decoder{0}".format(i)))
for i in range(output_seq_len):
target_weights.append(tf.placeholder(tf.float32, shape=[None], name="weight{0}".format(i)))

# decoder_inputs左移一个时序作为targets
targets = [decoder_inputs[i + 1] for i in range(output_seq_len)]

cell = tf.contrib.rnn.BasicLSTMCell(size)

# 这里输出的状态我们不需要
outputs, _ = seq2seq.embedding_attention_seq2seq(
encoder_inputs,
decoder_inputs[:output_seq_len],
cell,
num_encoder_symbols=num_encoder_symbols,
num_decoder_symbols=num_decoder_symbols,
embedding_size=size,
output_projection=None,
feed_previous=feed_previous,
dtype=tf.float32)

# 计算加权交叉熵损失
loss = seq2seq.sequence_loss(outputs, targets, target_weights)
# 梯度下降优化器
#opt = tf.train.GradientDescentOptimizer(learning_rate)
opt = tf.train.AdamOptimizer(learning_rate=learning_rate)
# 优化目标:让loss最小化
update = opt.apply_gradients(opt.compute_gradients(loss))
# 模型持久化
saver = tf.train.Saver(tf.global_variables())

return encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver, learning_rate_decay_op, learning_rate

def train():
"""
训练过程
"""

# train_set = [[[5, 7, 9], [11, 13, 15, EOS_ID]], [[7, 9, 11], [13, 15, 17, EOS_ID]],
# [[15, 17, 19], [21, 23, 25, EOS_ID]]]
train_set = get_train_set()
with tf.Session() as sess:

encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver, learning_rate_decay_op, learning_rate = get_model()

# 全部变量初始化
sess.run(tf.global_variables_initializer())

# 训练很多次迭代,每隔10次打印一次loss,可以看情况直接ctrl+c停止
previous_losses = []
for step in range(10000):
sample_encoder_inputs, sample_decoder_inputs, sample_target_weights = get_samples(train_set, 1000)
input_feed = {}
for l in range(input_seq_len):
input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
for l in range(output_seq_len):
input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
input_feed[target_weights[l].name] = sample_target_weights[l]
input_feed[decoder_inputs[output_seq_len].name] = np.zeros([len(sample_decoder_inputs[0])], dtype=np.int32)
[loss_ret, _] = sess.run([loss, update], input_feed)
if step % 100 == 0:
print( 'step=', step, 'loss=', loss_ret, 'learning_rate=', learning_rate.eval())

if len(previous_losses) > 5 and loss_ret > max(previous_losses[-5:]):
sess.run(learning_rate_decay_op)
previous_losses.append(loss_ret)

# 模型持久化
saver.save(sess, output_dir)

def predict():
"""
预测过程
"""

with tf.Session() as sess:
encoder_inputs, decoder_inputs, target_weights, outputs, loss, update, saver, learning_rate_decay_op, learning_rate = get_model(feed_previous=True)
saver.restore(sess, output_dir)
sys.stdout.write("> ")
sys.stdout.flush()
#input_seq = sys.stdin.readline()
input_seq=input()
while input_seq:
input_seq = input_seq.strip()
input_id_list = get_id_list_from(input_seq)
if (len(input_id_list)):
sample_encoder_inputs, sample_decoder_inputs, sample_target_weights = seq_to_encoder(' '.join([str(v) for v in input_id_list]))

input_feed = {}
for l in range(input_seq_len):
input_feed[encoder_inputs[l].name] = sample_encoder_inputs[l]
for l in range(output_seq_len):
input_feed[decoder_inputs[l].name] = sample_decoder_inputs[l]
input_feed[target_weights[l].name] = sample_target_weights[l]
input_feed[decoder_inputs[output_seq_len].name] = np.zeros([2], dtype=np.int32)

# 预测输出
outputs_seq = sess.run(outputs, input_feed)
# 因为输出数据每一个是num_decoder_symbols维的,因此找到数值最大的那个就是预测的id,就是这里的argmax函数的功能
outputs_seq = [int(np.argmax(logit[0], axis=0)) for logit in outputs_seq]
# 如果是结尾符,那么后面的语句就不输出了
if EOS_ID in outputs_seq:
outputs_seq = outputs_seq[:outputs_seq.index(EOS_ID)]
outputs_seq = [wordToken.id2word(v) for v in outputs_seq]
print(" ".join(outputs_seq))
else:
print("WARN:词汇不在服务区")

sys.stdout.write("> ")
sys.stdout.flush()
input_seq = input()

if __name__ == "__main__":
tf.reset_default_graph()
train()
tf.reset_default_graph()
predict()

迭代次数、loss与学习率间的运行结果如下:
step= 0 loss= 4.48504 learning_rate= 0.001
step= 100 loss= 3.33924 learning_rate= 0.001
step= 200 loss= 2.90026 learning_rate= 0.001
step= 300 loss= 2.65296 learning_rate= 0.001
step= 400 loss= 2.48741 learning_rate= 0.001
step= 500 loss= 2.32334 learning_rate= 0.001
.............................................
step= 9000 loss= 0.765676 learning_rate= 0.000729
step= 9100 loss= 0.758894 learning_rate= 0.000729
step= 9200 loss= 0.75713 learning_rate= 0.000729
step= 9300 loss= 0.758076 learning_rate= 0.000729
step= 9400 loss= 0.752497 learning_rate= 0.000729
step= 9500 loss= 0.770046 learning_rate= 0.000729
step= 9600 loss= 0.749504 learning_rate= 0.0006561
step= 9700 loss= 0.747211 learning_rate= 0.0006561
step= 9800 loss= 0.747742 learning_rate= 0.0006561
step= 9900 loss= 0.758005 learning_rate= 0.0006561
测试部分结果如下:

> 你说
我 想 你
> 哈哈
什么
> 爱你
= 。 =
> 你吃了吗
吃 了
> 喜欢你
我 喜欢 !
> 工作
WARN:词汇不在服务区
> 去上海
你 啊 !
> 再见
WARN:词汇不在服务区
>

17.4.4 小结

本文从理论到实践讲解了怎么一步一步实现一个自动聊天机器人模型,并基于1000条样本,用了20分钟左右训练了一个聊天模型,试验效果比较好,核心逻辑是调用了tensorflow的embedding_attention_seq2seq,也就是带注意力的seq2seq模型,其中神经网络单元是LSTM。
由于语料有限,设备有限,只验证了小规模样本,如果大家想做一个更好的聊天系统,可以下载更多对话内容。

参考:
http://blog.csdn.net/malefactor/article/details/50550211
http://www.shareditor.com/blogshow?blogId=136【内含数据文件、及代码等】
https://www.geekhub.cn/a/2214.html

How Does Attention Work in Encoder-Decoder Recurrent Neural Networks

How to Develop an Encoder-Decoder Model with Attention in Keras