年度归档:2017年

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

第18章 CNN实例--人脸识别

广义的人脸识别实际包括构建人脸识别系统的一系列相关技术,包括人脸图像采集、人脸定位或检测、人脸识别预处理、身份确认以及身份查找等;而狭义的人脸识别特指通过人脸进行身份确认或者身份查找的技术或系统。 人脸识别是一项热门的计算机技术研究领域,它属于生物特征识别技术,是对生物体(一般特指人)本身的生物特征来区分生物体个体。本章主要内容如下:
1)先获取自己的头像,可以通过手机、电脑等拍摄;
2)下载别人的头像,具体网址详见下节;
3)利用dlib、opencv对人脸进行检测;
4)根据检测后的图片,利用卷积神经网络训练模型;
5)把新头像用模型进行识别,看模型是否能认出是你。

18.1 人脸识别简介

广义的人脸识别实际包括构建人脸识别系统的一系列相关技术,包括人脸图像采集、人脸定位、人脸识别预处理、身份确认以及身份查找等;而狭义的人脸识别特指通过人脸进行身份确认或者身份查找的技术或系统。
人脸识别是一项热门的计算机技术研究领域,它属于生物特征识别技术,是对生物体(一般特指人)本身的生物特征来区分生物体个体。生物特征识别技术所研究的生物特征包括脸、指纹、手掌纹、虹膜、视网膜、声音(语音)、体形、个人习惯(例如敲击键盘的力度和频率、签字)等,相应的识别技术就有人脸识别、指纹识别、掌纹识别、虹膜识别、视网膜识别、语音识别(用语音识别可以进行身份识别,也可以进行语音内容的识别,只有前者属于生物特征识别技术)、体形识别、键盘敲击识别、签字识别等。
人脸识别的优势在于其自然性和不被被测个体察觉的特点,容易被大家接受。
人脸识别的一般处理流程,如下图:


其中:
1)图像获取:可以通过摄像镜把人脸图像采集下来头或图片上传等方式
2)人脸检测:就是给定任意一张图片,找到其中是否存在一个或多个人脸,并返回图片中 每个人脸的位置、范围及特征等。如下图:

3)人脸定位:通过人脸来确定位置信息。
4)预处理:基于人脸检测结果,对图像进行处理,为后续的特征提取服务。系统获取到的人脸图像可能受到各种条件的限制或影响,需要对进行大小缩放、旋转、拉伸、灰度变换规范化及过滤等图像预处理。由于图像中存在很多干扰因素,如外部因素:清晰度、天气、角度、距离等;目标本身因素:胖瘦,假发、围巾、银镜、表情等。所以神经网络一般需要比较多的训练数据,才能从原始的特征中提炼出有意义的特征。如下图所示,如果数据少了,神经网络性能可能还不及传统机器学习。

5)特征提取:就是将人脸图像信息数字化,把人脸图像转换为一串数字。特征提取是一项重要内容,传统机器学习这部分往往要占据大部分时间和精力,有时虽然花去了时间,效果却不一定理想,好在深度学习很多都是自动获取特征,下图为传统机器学习与深度学习的一些异同,尤其是在提取特征方面。


6)人脸特征:找到人脸的一些关键特征或位置,如眼镜、嘴唇、鼻子、下巴等的位置,利用特征点间的欧氏距离、曲率和角度等提取特征分量,最终把相关的特征连接成一个长的特征向量。如下图显示人脸的一些特征点。

7)比对识别:通过模型回答两张人脸属于相同的人或指出一张新脸是人脸库中的谁的脸。
8)输出结果:对人脸库中的新图像进行身份认证,并给出是或否的结果。

人脸识别的应用非常广泛,主要有:
1)门禁系统:受安全保护的地区可以通过人脸识别辨识试图进入者的身份,比如监狱、看守所、小区、学校等。
2)摄像监视系统:在例如银行、机场、体育场、商场、超级市场等公共场所对人群进行监视,以达到身份识别的目的。例如在机场安装监视系统以防止恐怖分子登机。
3)网络应用:利用人脸识别辅助信用卡网络支付,以防止非信用卡的拥有者使用信用卡,社保支付防止冒领等。
4)学生考勤系统:香港及澳门的中、小学已开始将智能卡配合人脸识别来为学生进行每天的出席点名记录。
5)相机:新型的数码相机已内建人脸识别功能以辅助拍摄人物时对焦。
6)智能手机:解锁手机、识别使用者等。

18.2 导入数据

获取其他人脸图片集
需要收集一个其他人脸的图片集,只要不是自己的人脸都可以,可以在网上找到,这里我给出一个我用到的图片集:
网站地址:http://vis-www.cs.umass.edu/lfw/
图片集下载:http://vis-www.cs.umass.edu/lfw/lfw.tgz
先将下载的图片集,解压到项目目录下的lfw目录下,也可以自己指定目录(修改代码中的input_dir变量)
程序中使用的是dlib来识别人脸部分,也可以使用opencv来识别人脸,在实际使用过程中,dlib的识别效果比opencv的好,但opencv识别的速度会快很多,获取10000张人脸照片的情况下,dlib大约花费了1小时,而opencv的花费时间大概只有20分钟。opencv可能会识别一些奇怪的部分,所以综合考虑之后我使用了dlib来识别人脸。
1)导入需要的包,这里使用dlib库进行人脸识别。

import sys
import os
import cv2
import dlib

2)定义输入、输出目录,文件解压到当前目录./data/my_faces目录下。
#我的头像(可以用手机或电脑等拍摄,尽量清晰、尽量多,越多越好)上传到以下input_dir目录下,output_dir为检测以后的头像

input_dir = './data/face_recog/my_faces'
output_dir = './data/my_faces'
size = 64

3)判断输出目录是否存在,不存在,则创建。

if not os.path.exists(output_dir):
os.makedirs(output_dir)

18.3 预处理数据

接下来使用dlib来批量识别图片中的人脸部分,并对原图像进行预处理,并保存到指定目录下。
1)预处理我的头像

%matplotlib inline
index = 1
for (path, dirnames, filenames) in os.walk(input_dir):
for filename in filenames:
if filename.endswith('.jpg'):
print('Being processed picture %s' % index)
img_path = path+'/'+filename
# 从文件读取图片
img = cv2.imread(img_path)
# 转为灰度图片
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 使用detector进行人脸检测 dets为返回的结果
dets = detector(gray_img, 1)

#使用enumerate 函数遍历序列中的元素以及它们的下标
#下标i即为人脸序号
#left:人脸左边距离图片左边界的距离 ;right:人脸右边距离图片左边界的距离
#top:人脸上边距离图片上边界的距离 ;bottom:人脸下边距离图片上边界的距离
for i, d in enumerate(dets):
x1 = d.top() if d.top() > 0 else 0
y1 = d.bottom() if d.bottom() > 0 else 0
x2 = d.left() if d.left() > 0 else 0
y2 = d.right() if d.right() > 0 else 0
# img[y:y+h,x:x+w]
face = img[x1:y1,x2:y2]
# 调整图片的尺寸
face = cv2.resize(face, (size,size))
cv2.imshow('image',face)
# 保存图片
cv2.imwrite(output_dir+'/'+str(index)+'.jpg', face)
index += 1

key = cv2.waitKey(30) & 0xff
if key == 27:
sys.exit(0)

Being processed picture 109
Being processed picture 110
Being processed picture 111
Being processed picture 112
Being processed picture 113
Being processed picture 114
Being processed picture 115
这是处理后我的一张头像

2)用同样方法预处理别人的头像(我只选用别人部分头像)
#别人图片输入输出目录

input_dir = './data/face_recog/other_faces'
output_dir = './data/other_faces'
size = 64

3)判断输出目录是否存在,不存在,则创建。

if not os.path.exists(output_dir):
os.makedirs(output_dir)

4)预处理别人头像,同样调用本节的1)程序。

运行结果如下:
Being processed picture 264
Being processed picture 265
Being processed picture 266
Being processed picture 267
Being processed picture 268
Being processed picture 269
这是处理后别人的一张头像

以下是经预处理后的文件格式,各文件已标上序列号。

18.4 训练模型

有了训练数据之后,通过cnn来训练数据,就可以让她记住我的人脸特征,学习怎么认识我了。
1)导入需要的库

import tensorflow as tf
import cv2
import numpy as np
import os
import random
import sys
from sklearn.model_selection import train_test_split

2)定义预处理后图片(我的和别人的)所在目录

my_faces_path = './data/my_faces'
other_faces_path = './data/other_faces'
size = 64

3) 利用卷积循环网络开始训练,标注时我的表为[0,1],别人的标注为[1,0]

imgs = []
labs = []

#重新创建图形变量
tf.reset_default_graph()

def getPaddingSize(img):
h, w, _ = img.shape
top, bottom, left, right = (0,0,0,0)
longest = max(h, w)

if w < longest:
tmp = longest - w
# //表示整除符号
left = tmp // 2
right = tmp - left
elif h < longest: tmp = longest - h top = tmp // 2 bottom = tmp - top else: pass return top, bottom, left, right def readData(path , h=size, w=size): for filename in os.listdir(path): if filename.endswith('.jpg'): filename = path + '/' + filename img = cv2.imread(filename) top,bottom,left,right = getPaddingSize(img) # 将图片放大, 扩充图片边缘部分 img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=[0,0,0]) img = cv2.resize(img, (h, w)) imgs.append(img) labs.append(path) readData(my_faces_path) readData(other_faces_path) # 将图片数据与标签转换成数组 imgs = np.array(imgs) labs = np.array([[0,1] if lab == my_faces_path else [1,0] for lab in labs]) # 随机划分测试集与训练集 train_x,test_x,train_y,test_y = train_test_split(imgs, labs, test_size=0.05, random_state=random.randint(0,100)) # 参数:图片数据的总数,图片的高、宽、通道 train_x = train_x.reshape(train_x.shape[0], size, size, 3) test_x = test_x.reshape(test_x.shape[0], size, size, 3) # 将数据转换成小于1的数 train_x = train_x.astype('float32')/255.0 test_x = test_x.astype('float32')/255.0 print('train size:%s, test size:%s' % (len(train_x), len(test_x))) # 图片块,每次取100张图片 batch_size = 20 num_batch = len(train_x) // batch_size x = tf.placeholder(tf.float32, [None, size, size, 3]) y_ = tf.placeholder(tf.float32, [None, 2]) keep_prob_5 = tf.placeholder(tf.float32) keep_prob_75 = tf.placeholder(tf.float32) def weightVariable(shape): init = tf.random_normal(shape, stddev=0.01) return tf.Variable(init) def biasVariable(shape): init = tf.random_normal(shape) return tf.Variable(init) def conv2d(x, W): return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME') def maxPool(x): return tf.nn.max_pool(x, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME') def dropout(x, keep): return tf.nn.dropout(x, keep) def cnnLayer(): # 第一层 W1 = weightVariable([3,3,3,32]) # 卷积核大小(3,3), 输入通道(3), 输出通道(32) b1 = biasVariable([32]) # 卷积 conv1 = tf.nn.relu(conv2d(x, W1) + b1) # 池化 pool1 = maxPool(conv1) # 减少过拟合,随机让某些权重不更新 drop1 = dropout(pool1, keep_prob_5) # 第二层 W2 = weightVariable([3,3,32,64]) b2 = biasVariable([64]) conv2 = tf.nn.relu(conv2d(drop1, W2) + b2) pool2 = maxPool(conv2) drop2 = dropout(pool2, keep_prob_5) # 第三层 W3 = weightVariable([3,3,64,64]) b3 = biasVariable([64]) conv3 = tf.nn.relu(conv2d(drop2, W3) + b3) pool3 = maxPool(conv3) drop3 = dropout(pool3, keep_prob_5) # 全连接层 Wf = weightVariable([8*16*32, 512]) bf = biasVariable([512]) drop3_flat = tf.reshape(drop3, [-1, 8*16*32]) dense = tf.nn.relu(tf.matmul(drop3_flat, Wf) + bf) dropf = dropout(dense, keep_prob_75) # 输出层 Wout = weightVariable([512,2]) bout = weightVariable([2]) #out = tf.matmul(dropf, Wout) + bout out = tf.add(tf.matmul(dropf, Wout), bout) return out def cnnTrain(): out = cnnLayer() cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=out, labels=y_)) train_step = tf.train.AdamOptimizer(0.01).minimize(cross_entropy) # 比较标签是否相等,再求的所有数的平均值,tf.cast(强制转换类型) accuracy = tf.reduce_mean(tf.cast(tf.equal(tf.argmax(out, 1), tf.argmax(y_, 1)), tf.float32)) # 将loss与accuracy保存以供tensorboard使用 tf.summary.scalar('loss', cross_entropy) tf.summary.scalar('accuracy', accuracy) merged_summary_op = tf.summary.merge_all() # 数据保存器的初始化 saver = tf.train.Saver() with tf.Session() as sess: sess.run(tf.global_variables_initializer()) summary_writer = tf.summary.FileWriter('./tmp', graph=tf.get_default_graph()) for n in range(10): # 每次取128(batch_size)张图片 for i in range(num_batch): batch_x = train_x[i*batch_size : (i+1)*batch_size] batch_y = train_y[i*batch_size : (i+1)*batch_size] # 开始训练数据,同时训练三个变量,返回三个数据 _,loss,summary = sess.run([train_step, cross_entropy, merged_summary_op], feed_dict={x:batch_x,y_:batch_y, keep_prob_5:0.5,keep_prob_75:0.75}) summary_writer.add_summary(summary, n*num_batch+i) # 打印损失 print(n*num_batch+i, loss) if (n*num_batch+i) % 40 == 0: # 获取测试数据的准确率 acc = accuracy.eval({x:test_x, y_:test_y, keep_prob_5:1.0, keep_prob_75:1.0}) print(n*num_batch+i, acc) # 由于数据不多,这里设为准确率大于0.80时保存并退出 if acc > 0.8 and n > 2:
#saver.save(sess, './train_face_model/train_faces.model',global_step=n*num_batch+i)
saver.save(sess, './train_face_model/train_faces.model')
#sys.exit(0)
#print('accuracy less 0.80, exited!')

cnnTrain()

运行结果:
278 0.69154
279 0.068455
280 0.092965
280 1.0
281 0.189453
282 0.0440276
283 0.078829
284 0.32079
285 0.476557
286 0.193189
287 0.147238
288 0.2862
289 0.514215
290 0.0191329
291 0.0881194
292 0.337078
293 0.191775
294 0.054846
295 0.268961
296 0.1875
297 0.11575
298 0.175487
299 0.168204

18.5 测试模型

用训练得到的模型,测试我新拍摄的头像,看她是否认识我。
首先,把我的4张测试照片放在./data/face_recog/test_faces目录,然后,让模型来识别这些照片是否是我。

%matplotlib inline

input_dir='./data/face_recog/test_faces'
index=1

output = cnnLayer()
predict = tf.argmax(output, 1)

#先加载 meta graph并恢复权重变量
saver = tf.train.import_meta_graph('./train_face_model/train_faces.model.meta')
sess = tf.Session()

saver.restore(sess, tf.train.latest_checkpoint('./train_face_model/'))
#saver.restore(sess,tf.train.latest_checkpoint('./my_test_model/'))

def is_my_face(image):
sess.run(tf.global_variables_initializer())
res = sess.run(predict, feed_dict={x: [image/255.0], keep_prob_5:1.0, keep_prob_75: 1.0})
if res[0] == 1:
return True
else:
return False

#使用dlib自带的frontal_face_detector作为我们的特征提取器
detector = dlib.get_frontal_face_detector()

#cam = cv2.VideoCapture(0)

#while True:
#_, img = cam.read()
for (path, dirnames, filenames) in os.walk(input_dir):
for filename in filenames:
if filename.endswith('.jpg'):
print('Being processed picture %s' % index)
index+=1
img_path = path+'/'+filename
# 从文件读取图片
img = cv2.imread(img_path)
gray_image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
dets = detector(gray_image, 1)
if not len(dets):
print('Can`t get face.')
cv2.imshow('img', img)
key = cv2.waitKey(30) & 0xff
if key == 27:
sys.exit(0)
for i, d in enumerate(dets):
x1 = d.top() if d.top() > 0 else 0
y1 = d.bottom() if d.bottom() > 0 else 0
x2 = d.left() if d.left() > 0 else 0
y2 = d.right() if d.right() > 0 else 0
face = img[x1:y1,x2:y2]
# 调整图片的尺寸
face = cv2.resize(face, (size,size))
print('Is this my face? %s' % is_my_face(face))
cv2.rectangle(img, (x2,x1),(y2,y1), (255,0,0),3)
cv2.imshow('image',img)
key = cv2.waitKey(30) & 0xff
if key == 27:
sys.exit(0)

sess.close()

测试结果:
INFO:tensorflow:Restoring parameters from ./train_face_model/train_faces.model
Being processed picture 1
Is this my face? False
Being processed picture 2
Is this my face? True
Being processed picture 3
Is this my face? True
Being processed picture 4
Is this my face? True
通过识别我的脸来判断是否是我:

结果不错,4张照片,认出了3张。
因这次拍摄照片不多(不到200张),清晰度也不很好,有这个结果,感觉还不错,如果要想达到98%以上的精度,拍摄多一点照片是有效方法。
此外,本身算法还有很多优化空间。

第13 章 循环神经网络

13.1 循环神经网络简介

前面介绍卷积神经网络模型主要用于处理网格化数据,而且预先假设输入数据之间是互相独立的。但在很多实际应用中,数据之间是互相依赖的。比如,当我们在理解一句话意思时,孤立的理解这句话的每个词是不够的,我们需要处理这些词连接起来的整个序列;当我们思考问题时,我们都是根据以往的经验和知识,再结合当前的实际情况来综合考虑的。像处理这些有顺序有关的问题,如果用前馈神经网络(如卷积神经网络),会有很大的局限性。为解决这类问题,就诞生了时序神经网络(循环神经网络和递归神经网络)。
这章我们主要介绍循环神经网络,循环神经网络(Recurrent Neural Networks,简称为RNN)是目前非常流行的神经网络模型,在自然语言处理的很多任务中已经展示出卓越的效果。
循环神经网络的发展并不顺利,自从20世纪80年代以来,人们不断优化,出现了很多RNN的变种,由于受当时计算能力的限制,都没有得到广泛的应用。不过随着计算能力的不断提升,近年来情况大有改观。随着一些重要架构的出现,尤其在2006年提出的LSTM,RNN已经有非常强大的应用,能够很好地完成许多领域的序列任务,在语音识别、机器人翻译、人机对话、语音合成、视频处理等方面大显身手。
RNN有很多变种,我们先介绍一种简单的RNN网络,即Elman循环网络,然后介绍循环神经网络的几种有代表性的升级版,如LSTM、GRC、BiLSTM等。

13.2 Elman神经网络

Elman网络是 J. L. Elman于 1990年首先针对语音处理问题而提出来的, 它是一种典型的局部回归网络( global feed for ward l ocal recurrent)。Elman网络可以看作是一个具有局部记忆单元和局部反馈连接的前向神经网络。Elman网络具有与多层前向网络相似的多层结构。它的主要结构是前馈连接, 包括输入层、 隐含层、 输出层, 其连接权可以进行学习修正;反馈连接由一组“结构 ” 单元构成,用来记忆前一时刻的输出值, 其连接权值是固定的。

13.2.1 Elman结构

我们先回顾一下全连接的神经网络,如下图:

这种神经网络中,隐含层的值只取决于输入的x,而且隐含层的神经元之间是没有关联的。
我们看一简单RNN图形,如下图,它由输入层、一个隐藏层和一个输出层组成。

初次看到这个图,很多人会有点头晕,我们通常神经网络,一般是输入-->隐含-->输出,这里隐含层中突然冒出一个带方向的圈,而且上面还标有一个W。这个表示啥意思呢?
其实这个带箭头及W的圈就是循环网络的灵魂,它表示循环神经网络的隐藏层的值s不仅仅取决于当前这次的输入x,还取决于上一次隐藏层的值s。权重矩阵 W就是隐藏层上一次的值作为这一次的输入的权重。如果我们把上面的图展开(unfold),循环神经网络也可以画成下面这个样子:

 


输入与之前的状态合并为一个向量:


不像传统的深度神经网络,在不同的层使用不同的参数,循环神经网络在所有步骤中共享参数(U、V、W)。这个反映一个事实,我们在每一步上执行相同的任务,仅仅是输入不同。这个机制极大减少了我们需要学习的参数的数量;
上图在每一步都有输出,但是根据任务的不同,这个并不是必须的。例如,当预测一个句子的情感时,我们可能仅仅关注最后的输出,而不是每个词的情感。相似地,我们在每一步中可能也不需要输入。循环神经网络最大的特点就是隐层状态,它可以捕获一个序列的一些信息。

13.2.2 随时间反向传播(BPTT)

前面我们简单介绍RNN的结构,下面我们探讨一下如何找到最好的权值矩阵,或如何对权值矩阵进行优化。对前馈网络,最流行的优化方法是梯度下降法,优化权值矩阵通过反向传播法(BP),这些方法能在应用在RNN网络吗?当然能,只不过需要对RNN做一点小变化,只要把RNN沿时间轴展开,展开之后便可使用前馈网络一般的方式对RNN进行优化。这样,计算误差相对于各权值的梯度,便可对展开的RNN使用标准的BP方法,因这里涉及一个时间因素,故对RNN来说,这种算法称为随时间反向传播(Back-Propagation Through Time,BPTT)。

我们的优化目标是参数,计算出误差分别对参数U、W、V的梯度,然后用随机梯度下降学习好的参数。像我们汇总这些误差一样,我们也需要汇总一个训练样例在每个时间步(time step)的梯度:

 

 

我们可以看到每个时间步长对梯度的贡献。因为W在我们关心的输出的每个步骤中都使用了,所以我们需要将梯度从 网络中一直反向传播到 ,具体过程如下图:

请注意,这与我们在深度前馈神经网络中使用的标准反向传播算法完全相同。关键的区别在于我们汇总了 每个时间步的梯度。在传统的神经网络中,我们不共享层间参数,所以我们不需要汇总任何东西。不过,BPTT只是展开RNN标准反向传播的一个奇特名称。就像Backpropagation一样,我们也可以定义一个向后传递的δ向量,例如:

在代码中,一个朴素的BPTT实现如下所示:

def bptt(self, x, y):
T = len(y)
# 实现前向传导
o, s = self.forward_propagation(x)
# 初始化这些变量的梯度
dLdU = np.zeros(self.U.shape)
dLdV = np.zeros(self.V.shape)
dLdW = np.zeros(self.W.shape)
delta_o = o
delta_o[np.arange(len(y)), y] -= 1.
# For each output backwards...
for t in np.arange(T)[::-1]:
dLdV += np.outer(delta_o[t], s[t].T)
# Initial delta calculation: dL/dz
delta_t = self.V.T.dot(delta_o[t]) * (1 - (s[t] ** 2))
# Backpropagation through time (for at most self.bptt_truncate steps)
for bptt_step in np.arange(max(0, t-self.bptt_truncate), t+1)[::-1]:
# print "Backpropagation step t=%d bptt step=%d " % (t, bptt_step)
# Add to gradients at each previous step
dLdW += np.outer(delta_t, s[bptt_step-1])
dLdU[:,x[bptt_step]] += delta_t
# Update delta for next step dL/dz at t-1
delta_t = self.W.T.dot(delta_t) * (1 - s[bptt_step-1] ** 2)
return [dLdU, dLdV, dLdW]

从以上代码中易发现,为什么标准RNN比较难训练:序列(句子)可能很长,可能是20个字或更多,因此你需要通过很多层向后传播。在实践中,许多人通过截断方式来限制传播的步数。

13.2.3 梯度消失或爆炸

前面我们提到RNN在学习远程依赖方面存在的问题,这些远程依赖是相隔几个步骤的单词之间的相互作用。这是有问题的,因为英语句子的意思通常是由不太接近的词语来决定的:“头上戴假发的人进去了”。这句话实际上是关于一个人进去,而不是关于假发。但是简单的RNN不可能捕捉到这样的信息。为了理解为什么,让我们仔细看看我们上面计算的梯度:

事实上,上面的雅可比矩阵的2范数的上界为1(其证明大家可以参考相关文档)。而为 (或sigmoid)激活函数将所有值映射到-1和1之间的范围内,并且导数也小于1(而对sigmoid其导数小于1/4),可参考下图:

图13.1 tanh函数及其导数图形

图13.2 sigmoid函数及导数图形

从上图不难看出,tanh(或sigmoid)函数的两端导数逐渐趋于0。最后接近一条水平线。当这种情况出现时,我们就认为相应的神经元饱和了。
它们的梯度为0使得前面层的梯度也为0。矩阵中存在比较小的值,多个矩阵相乘会使梯度值以指数级速度下降,最终在几步后完全消失。比较远的时刻的梯度值为0,这些时刻的状态对学习过程没有帮助,导致你无法学习到长距离依赖。消失梯度问题不仅出现在RNN中,同样也出现在深度前向神经网中。只是RNN通常比较深(例子中深度和句子长度一致),使得这个问题更加普遍。
不难想到,依赖于我们的激活函数和网络参数,如果Jacobian矩阵中的值太大,会产生梯度爆炸而不是梯度消失问题。梯度消失比梯度爆炸受到了更多的关注有两方面的原因。其一,梯度爆炸容易发现,梯度值会变成NaN,导致程序崩溃。
其二,用预定义的阈值裁剪梯度可以简单有效的解决梯度爆炸问题。
梯度消失出现的时候不那么明显而且不好处理。
遇到梯度消失或爆炸是件比较麻烦的事,幸运的是,已经有一些方法解决了梯度消失或爆炸的问题。合适的初始化矩阵W可以减小梯度消失效应,正则化也能起作用。更好的方法是选择ReLU而不是sigmoid和tanh作为激活函数。ReLU的导数是常数值0或1,所以不可能会引起梯度消失。更通用的方案时采用长短项记忆(LSTM)或门限递归单元(GRU)结构。LSTM在1997年第一次提出,可能是目前在NLP上最普遍采用的模型。GRU,2014年第一次提出,是LSTM的简化版本。这两种RNN结构都是为了处理梯度消失问题而设计的,可以有效地学习到长距离依赖,我们会在本章后面部分进行介绍。

13.2.4 循环神经网络扩展

多年来,研究人员开发了更复杂的RNN来处理简单RNN模型的一些缺点。我们将在本章后面的更详细地介绍它们,但是我希望本节能够作为一个简要概述,以便您熟悉模型的分类。



深度(双向)RNN类似于双向RNN,只是在每个时间步有多个层。实际上这给了我们更高的学习能力(但是我们也需要大量的训练数据)。其结构如下:

13.2.5 RNN应用举例

接下来,我们介绍一下基于RNN语言模型。我们首先把词依次输入到循环神经网络中,每输入一个词,循环神经网络就输出截止到目前为止,下一个最可能的词。例如,当我们依次输入:
我 昨天 上学 迟到 了
神经网络的输出如下图所示:

其中,s和e是两个特殊的词,分别表示一个序列的开始和结束。
整个计算过程下图所示,第一个单词被转换成机器可读的向量。然后,RNN 逐个处理向量序列

在处理过程中,它将之前的隐状态传递给序列的下一个步骤。隐状态作为神经网络的记忆,保存着网络先前观察到的数据信息。

13.2.5 .1向量化

神经网络的输入和输出都是向量,为了让语言模型能够被神经网络处理,我们必须把词表达为向量的形式,这样神经网络才能处理它。
神经网络的输入是词,我们可以用下面的步骤对输入进行向量化:
1)建立一个包含所有词的词典,每个词在词典里面有一个唯一的编号。
2)任意一个词都可以用一个N维的one-hot向量来表示(也可以采用word2vec方式)。其中,N是词典中包含的词的个数。假设一个词在词典中的编号是i,v是表示这个词的向量,是向量的第j个元素,则:

上面这个公式的含义,可以用下面的图来直观的表示:

使用这种向量化方法,我们就得到了一个高维、稀疏的向量(稀疏是指绝大部分元素的值都是0)。处理这样的向量会导致我们的神经网络有很多的参数,带来庞大的计算量。因此,往往会需要使用一些降维方法,将高维的稀疏向量转变为低维的稠密向量。不过这个话题我们就不再这篇文章中讨论了。
语言模型要求的输出是下一个最可能的词,我们可以让循环神经网络计算计算词典中每个词是下一个词的概率,这样,概率最大的词就是下一个最可能的词。因此,神经网络的输出向量也是一个N维向量,向量中的每个元素对应着词典中相应的词是下一个词的概率。如下图所示:

13.2.5 .2 Softmax层

前面提到,语言模型是对下一个词出现的概率进行建模。那么,怎样让神经网络输出概率呢?方法就是用softmax层作为神经网络的输出层。
我们先来看一下softmax函数的定义:

这个公式看起来有点抽象,我们举一个例子来说明。Softmax层如下图所示:

从上图我们可以看到,softmax layer的输入是一个向量,输出也是一个向量,两个向量的维度是一样的(在这个例子里面是4)。输入向量x=[1 2 3 4]经过softmax层之后,经过上面的softmax函数计算,转变为输出向量y=[0.03 0.09 0.24 0.64]。计算过程为:

我们来看看输出向量y的特征:
1)每一项为取值为0-1之间的正数;
2)所有项的总和是1。
我们不难发现,这些特征和概率的特征是一样的,因此我们可以把它们看做是概率。对于语言模型来说,我们可以认为模型预测下一个词是词典中第一个词的概率是0.03,是词典中第二个词的概率是0.09,以此类推。

13.2.5 .3语言模型的训练

可以使用监督学习的方法对语言模型进行训练,首先,需要准备训练数据集。接下来,我们介绍怎样把语料。

我 昨天 上学 迟到 了

转换成语言模型的训练数据集。
首先,我们获取输入-标签对:

然后,使用前面介绍过的向量化方法,对输入x和标签y进行向量化。这里面有意思的是,对标签y进行向量化,其结果也是一个one-hot向量。例如,我们对标签『我』进行向量化,得到的向量中,只有第2019个元素的值是1,其他位置的元素的值都是0。它的含义就是下一个词是『我』的概率是1,是其它词的概率都是0。
最后,我们使用交叉熵误差函数作为优化目标,对模型进行优化。
在实际工程中,我们可以使用大量的语料来对模型进行训练,获取训练数据和训练的方法都是相同的。

13.2.5 .4交叉熵误差

一般来说,当神经网络的输出层是softmax层时,对应的误差函数E通常选择交叉熵误差函数,其定义如下:

当然我们可以选择其他函数作为我们的误差函数,比如最小平方误差函数(MSE)。不过对概率进行建模时,选择交叉熵误差函数更合理一些。

13.3 LSTM网络

前面我们介绍了循环神经网络的简单模型,这种模型有很多不足,尤其是层数稍多时出现梯度消失或爆炸的情况。为解决这些问题,研究人员苦苦探究,经过近8年的不懈努力,终于结出硕果,于1997年由Sepp Hochreiter和Jürgen Schmidhuber首次提出LSTM(Long Short Term Memory Network, LSTM),它成功的解决了原始循环神经网络的缺陷,成为当前最流行的RNN,在语音识别、图片描述、自然语言处理等许多领域中成功应用。2014年首次使用的GRU是LSTM的一个简单变体,它们拥有许多相同的属性。我们先看看LSTM,然后看看GRU是如何不同的。

13.3.1 LSTM网络

LSTM的思路比较简单。原始RNN的隐藏层只有一个状态,即h,它对于短期的输入非常敏感。那么,假如我们再增加一个状态,即c,让它来保存长期的状态,那么问题不就解决了么?如下图所示:

新增加的状态c,称为单元状态(cell state)。我们把上图按照时间维度展开:


LSTM的关键,就是怎样控制长期状态c。在这里,LSTM的思路是使用三个控制开关。第一个开关,负责控制继续保存长期状态c;第二个开关,负责控制把即时状态输入到长期状态c;第三个开关,负责控制是否把长期状态c作为当前的LSTM的输出。三个开关的作用如下图所示:

13.3.2 LSTM前向计算

前面描述的开关是怎样在算法中实现的呢?这就用到了门(gate)的概念。门实际上就是一层全连接层,它的输入是一个向量,输出是一个0到1之间的实数向量。假设W是门的权重向量,b是偏置项,那么门可以表示为:

门的使用,就是用门的输出向量按元素乘以我们需要控制的那个向量。因为门的输出是0到1之间的实数向量,那么,当门输出为0时,任何向量与之相乘都会得到0向量,这就相当于啥都不能通过;输出为1时,任何向量与之相乘都不会有任何改变,这就相当于啥都可以通过。因为σ(也就是sigmoid函数)的值域是(0,1),所以门的状态都是半开半闭的。
下图显示了遗忘门的计算过程:

 
下图为遗忘门的计算公式:

下图描述了输入门的计算过程:


下图演示了状态的计算过程:

下图表示输出门的过程及计算公式:

LSTM最终的输出,是由输出门和单元状态共同确定的:


下图表示LSTM最终输出的计算:

除了LSTM的四个门控制其状态外,LSTM还有一个固定权值为1的自连接,以及一个线性激活函数,因此,其局部偏导数始终为1。 在反向传播阶段,这个所谓的常量误差传输子(constant error carousel,CEC)能够在许多时间步中携带误差面不会发生梯度消失或梯度爆炸。

13.3.3 LSTM的训练

13.3.3.1 LSTM训练算法主要步骤


2)反向计算每个神经元的误差项值。与循环神经网络一样,LSTM误差项的反向传播也是包括两个方向:
一个是沿时间的反向传播,即从当前t时刻开始,计算每个时刻的误差项;
一个是将误差项向上一层传播。
3)根据相应的误差项,计算每个权重的梯度。
整个推导过程比较长,有兴趣的读者可参考这篇博文,这里只列出一些结果,过程就省略了。

13.3.3.2关于公式和符号的说明

为便于大家理解,预定义一些公式及运算符号等,这些公式或运算在推导一些结论时需要用到。

13.3.3.2误差项沿时间的反向传递

将误差项向前传递到任意k时刻的公式:

13.3.3.3将误差项传递到上一层

13.3.3.4权重梯度的计算

13.4 GRU网络

前面我们介绍了RNN的改进版LSTM,它有效克服了传统RNN的一些不足,事物总是向前发展的,LSTM也存在很多变体,在众多的LSTM变体中,GRU (Gated Recurrent Unit)也许是最成功的一种。它对LSTM做了很多简化,同时却保持着和LSTM相同的效果。因此,GRU最近变得越来越流行。
GRU对LSTM做了两个大改动:

GRU的前向计算公式为:

GRU的示意图:

13.5 Bi-LSTM网络

双向循环神经网络(Bidirectional Recurrent Neural Networks,Bi-RNN),Schuster、Paliwal,1997年首次提出,和LSTM同年。Bi-RNN,增加RNN可利用信息。普通MLP,数据长度有限制。RNN,可以处理不固定长度时序数据,无法利用历史输入未来信息。Bi-RNN,同时使用时序数据输入历史及未来数据,时序相反两个循环神经网络连接同一输出,输出层可以同时获取历史未来信息。
Language Modeling,不适合Bi-RNN,目标是通过前文预测下一单词,不能将下文信息传给模型。不过对分类问题,手写文字识别、机器翻译、蛋白结构预测等,采用Bi-RNN能提升模型效果。百度语音识别,通过Bi-RNN综合上下文语境,提升模型准确率。
双向循环神经网络(BRNN)的基本思想是提出每一个训练序列向前和向后分别是两个循环神经网络(RNN),而且这两个都连接着一个输出层。这个结构提供给输出层输入序列中每一个点的完整的过去和未来的上下文信息。下图展示的是一个沿着时间展开的双向循环神经网络。六个独特的权值在每一个时步被重复的利用,六个权值分别对应:输入到向前和向后隐含层(w1, w3),隐含层到隐含层自己(w2, w5),向前和向后隐含层到输出层(w4, w6)。值得注意的是:向前和向后隐含层之间没有信息流,这保证了展开图是非循环的。

13.6 一些优化方法

选择rmsprop优化算法
rmsprop优化算法背后的基本思想是根据先前梯度的和来调整学习率per-parameter。直观的 ,它意味着频繁出现的特征得到更小的学习率(因为它的梯度的和将会更大),稀有的特征得到更大的学习率; rmsprop的实现相当的简单;对于每个参数,有一个缓存变量。 在梯度下降时,我们更新参数和此变量;
cacheW = decay * cacheW + (1 - decay) * dW ** 2
W = W - learning_rate * dW / np.sqrt(cacheW + 1e-6)
衰退典型的被设置为0.9或0.95,1e-6 是为了避免0的出现;
加入嵌入层
使用例如word2vect和GloVe单词嵌入是一个流行的方法提高我们精度。代替使用one-hot vector来表达单词,使用word2vec或GloVe学习得到的携带语义的低维向量(形似的单词具有相似的向量),使用这些向量是训练前预处理的一种方式,直观的,能够告诉神经网络那些单词是相似的,以至于需要更少的学习语言;在你没有大量数据的时候,使用预训练向量很有效,因为它允许神经网络推广到没有见过的单词。我没有使用过预处理单词向量,但是加入一个嵌入层使它们更容易的插入进来;嵌入矩阵(E)就是一个查找表。第i列向量对应于我们的单词表中的第i个单词;
通过更新E进行单词的向量表示的学习更新;但是,这与我们的特殊任务相关,并不是可以下载使用大量文档寻训练的模型进行通用;
添加第二个GRU层
在神经网络中,加入第二层能够使我们的模型捕捉到更高水平相互作用;你能够加入额外的层;你将会发现在2-3层之后,结果会衰退,除非你拥有大量的数据,更多的层次不太可能造成大的差异,可能导致过拟合。

13.7 应用场景

上图中每一个矩形是一个向量,箭头则表示函数(比如矩阵相乘)。输入向量用红色标出,输出向量用蓝色标出,绿色的矩形是RNN的状态。从做到右:
(1)没有使用RNN的Vanilla模型,从固定大小的输入得到固定大小输出(比如图像分类)。

(2)序列输出(比如图片字幕,输入一张图片输出一段文字序列)。
(3)序列输入(比如情感分析,输入一段文字然后将它分类成积极或者消极情感)。
(4)序列输入和序列输出(比如机器翻译:一个RNN读取一条英文语句然后将它以法语形式输出)。
(5)同步序列输入输出(比如视频分类,对视频中每一帧打标签)。
我们注意到在每一个案例中,都没有对序列长度进行预先特定约束,因为递归变换(绿色部分)是固定的,而且我们可以多次使用。
正如你预想的那样,与使用固定计算步骤的固定网络相比,使用序列进行操作要更加强大,因此,这激起了我们建立对智能系统更大的兴趣。而且,我们可以从一小方面看出,RNNs将输入向量与状态向量用一个固定(但可以学习)函数绑定起来,从而用来产生一个新的状态向量。在编程层面,在运行一个程序时,可以用特定的输入和一些内部变量对其进行解释。从这个角度来看,RNNs本质上可以描述程序。事实上, RNNs是图灵完备的 ,即它们可以模拟任意程序(使用恰当的权值向量)。
13.8 LSTM实例之一(Python3实现)
前面我们介绍了LSTM的主要框架及原理,以及多种LSTM的改进模型,接下来我们通过一个实例来说明LSTM的具体实现。使用python3,后续我们将使用Tensorflow、Keras等实现。在下面的实现中,LSTMLayer的参数包括输入维度、输出维度、隐藏层维度,单元状态维度等于隐藏层维度。gate的激活函数为sigmoid函数,输出的激活函数为tanh。

13.8.1 LSTM实激活函数的实现

我们当然可以选择其他函数作为我们的误差函数,比如最小平方误差函数(MSE)。不过对概率进行建模时,选择交叉熵误差函数更make sense。

class SigmoidActivator(object):
def forward(self, weighted_input):
return 1.0 / (1.0 + np.exp(-weighted_input))
def backward(self, output):
return output * (1 - output)
class TanhActivator(object):
def forward(self, weighted_input):
return 2.0 / (1.0 + np.exp(-2 * weighted_input)) - 1.0
def backward(self, output):
return 1 - output * output

13.8.2 LSTM初始化

我们把LSTM的实现放在LstmLayer类中,根据LSTM前向计算和反向传播算法,我们需要初始化一系列矩阵和向量。这些矩阵和向量有两类用途,
一类是用于保存模型参数,例如:

另一类是保存各种中间计算结果,以便于反向传播算法使用,它们包括:

以及各个权重对应的梯度。
在构造函数的初始化中,只初始化了与forward计算相关的变量,与backward相关的变量没有初始化。

class LstmLayer(object):
def __init__(self, input_width, state_width,
learning_rate):
self.input_width = input_width
self.state_width = state_width
self.learning_rate = learning_rate
# 门的激活函数
self.gate_activator = SigmoidActivator()
# 输出的激活函数
self.output_activator = TanhActivator()
# 当前时刻初始化为t0
self.times = 0
# 各个时刻的单元状态向量c
self.c_list = self.init_state_vec()
# 各个时刻的输出向量h
self.h_list = self.init_state_vec()
# 各个时刻的遗忘门f
self.f_list = self.init_state_vec()
# 各个时刻的输入门i
self.i_list = self.init_state_vec()
# 各个时刻的输出门o
self.o_list = self.init_state_vec()
# 各个时刻的即时状态c~
self.ct_list = self.init_state_vec()
# 遗忘门权重矩阵Wfh, Wfx, 偏置项bf
self.Wfh, self.Wfx, self.bf = (
self.init_weight_mat())
# 输入门权重矩阵Wfh, Wfx, 偏置项bf
self.Wih, self.Wix, self.bi = (
self.init_weight_mat())
# 输出门权重矩阵Wfh, Wfx, 偏置项bf
self.Woh, self.Wox, self.bo = (
self.init_weight_mat())
# 单元状态权重矩阵Wfh, Wfx, 偏置项bf
self.Wch, self.Wcx, self.bc = (
self.init_weight_mat())
def init_state_vec(self):
'''
初始化保存状态的向量
'''

state_vec_list = []
state_vec_list.append(np.zeros(
(self.state_width, 1)))
return state_vec_list
def init_weight_mat(self):
'''
初始化权重矩阵
'''

Wh = np.random.uniform(-1e-4, 1e-4,
(self.state_width, self.state_width))
Wx = np.random.uniform(-1e-4, 1e-4,
(self.state_width, self.input_width))
b = np.zeros((self.state_width, 1))
return Wh, Wx, b

13.8.3前向计算的实现

forward方法实现了LSTM的前向计算:

def forward(self, x):
'''
根据式1-式6进行前向计算
'''

self.times += 1
# 遗忘门
fg = self.calc_gate(x, self.Wfx, self.Wfh,
self.bf, self.gate_activator)
self.f_list.append(fg)
# 输入门
ig = self.calc_gate(x, self.Wix, self.Wih,
self.bi, self.gate_activator)
self.i_list.append(ig)
# 输出门
og = self.calc_gate(x, self.Wox, self.Woh,
self.bo, self.gate_activator)
self.o_list.append(og)
# 即时状态
ct = self.calc_gate(x, self.Wcx, self.Wch,
self.bc, self.output_activator)
self.ct_list.append(ct)
# 单元状态
c = fg * self.c_list[self.times - 1] + ig * ct
self.c_list.append(c)
# 输出
h = og * self.output_activator.forward(c)
self.h_list.append(h)
def calc_gate(self, x, Wx, Wh, b, activator):
'''
计算门
'''

h = self.h_list[self.times - 1] # 上次的LSTM输出
net = np.dot(Wh, h) + np.dot(Wx, x) + b
gate = activator.forward(net)
return gate

从上面的代码我们可以看到,门的计算都是相同的算法,而门和c ̃_t的计算仅仅是激活函数不同。因此我们提出了calc_gate方法,这样减少了很多重复代码

13.8.4反向传播算法的实现

backward方法实现了LSTM的反向传播算法。需要注意的是,与backword相关的内部状态变量是在调用backward方法之后才初始化的。这种延迟初始化的一个好处是,如果LSTM只是用来推理,那么就不需要初始化这些变量,节省了很多内存。

def backward(self, x, delta_h, activator):
'''
实现LSTM训练算法
'''

self.calc_delta(delta_h, activator)
self.calc_gradient(x)

算法主要分成两个部分,一部分使计算误差项:

def calc_delta(self, delta_h, activator):
# 初始化各个时刻的误差项
self.delta_h_list = self.init_delta() # 输出误差项
self.delta_o_list = self.init_delta() # 输出门误差项
self.delta_i_list = self.init_delta() # 输入门误差项
self.delta_f_list = self.init_delta() # 遗忘门误差项
self.delta_ct_list = self.init_delta() # 即时输出误差项
# 保存从上一层传递下来的当前时刻的误差项
self.delta_h_list[-1] = delta_h
# 迭代计算每个时刻的误差项
for k in range(self.times, 0, -1):
self.calc_delta_k(k)
def init_delta(self):
'''
初始化误差项
'''

delta_list = []
for i in range(self.times + 1):
delta_list.append(np.zeros(
(self.state_width, 1)))
return delta_list
def calc_delta_k(self, k):
'''
根据k时刻的delta_h,计算k时刻的delta_f、
delta_i、delta_o、delta_ct,以及k-1时刻的delta_h
'''

# 获得k时刻前向计算的值
ig = self.i_list[k]
og = self.o_list[k]
fg = self.f_list[k]
ct = self.ct_list[k]
c = self.c_list[k]
c_prev = self.c_list[k-1]
tanh_c = self.output_activator.forward(c)
delta_k = self.delta_h_list[k]
# 根据式9计算delta_o
delta_o = (delta_k * tanh_c *
self.gate_activator.backward(og))
delta_f = (delta_k * og *
(1 - tanh_c * tanh_c) * c_prev *
self.gate_activator.backward(fg))
delta_i = (delta_k * og *
(1 - tanh_c * tanh_c) * ct *
self.gate_activator.backward(ig))
delta_ct = (delta_k * og *
(1 - tanh_c * tanh_c) * ig *
self.output_activator.backward(ct))
delta_h_prev = (
np.dot(delta_o.transpose(), self.Woh) +
np.dot(delta_i.transpose(), self.Wih) +
np.dot(delta_f.transpose(), self.Wfh) +
np.dot(delta_ct.transpose(), self.Wch)
).transpose()
# 保存全部delta值
self.delta_h_list[k-1] = delta_h_prev
self.delta_f_list[k] = delta_f
self.delta_i_list[k] = delta_i
self.delta_o_list[k] = delta_o
self.delta_ct_list[k] = delta_ct

另一部分是计算梯度:

def calc_gradient(self, x):
# 初始化遗忘门权重梯度矩阵和偏置项
self.Wfh_grad, self.Wfx_grad, self.bf_grad = (
self.init_weight_gradient_mat())
# 初始化输入门权重梯度矩阵和偏置项
self.Wih_grad, self.Wix_grad, self.bi_grad = (
self.init_weight_gradient_mat())
# 初始化输出门权重梯度矩阵和偏置项
self.Woh_grad, self.Wox_grad, self.bo_grad = (
self.init_weight_gradient_mat())
# 初始化单元状态权重梯度矩阵和偏置项
self.Wch_grad, self.Wcx_grad, self.bc_grad = (
self.init_weight_gradient_mat())
# 计算对上一次输出h的权重梯度
for t in range(self.times, 0, -1):
# 计算各个时刻的梯度
(Wfh_grad, bf_grad,
Wih_grad, bi_grad,
Woh_grad, bo_grad,
Wch_grad, bc_grad) = (
self.calc_gradient_t(t))
# 实际梯度是各时刻梯度之和
self.Wfh_grad += Wfh_grad
self.bf_grad += bf_grad
self.Wih_grad += Wih_grad
self.bi_grad += bi_grad
self.Woh_grad += Woh_grad
self.bo_grad += bo_grad
self.Wch_grad += Wch_grad
self.bc_grad += bc_grad
print( '-----%d-----' % t)
print(Wfh_grad)
print(self.Wfh_grad)
# 计算对本次输入x的权重梯度
xt = x.transpose()
self.Wfx_grad = np.dot(self.delta_f_list[-1], xt)
self.Wix_grad = np.dot(self.delta_i_list[-1], xt)
self.Wox_grad = np.dot(self.delta_o_list[-1], xt)
self.Wcx_grad = np.dot(self.delta_ct_list[-1], xt)
def init_weight_gradient_mat(self):
'''
初始化权重矩阵
'''

Wh_grad = np.zeros((self.state_width,
self.state_width))
Wx_grad = np.zeros((self.state_width,
self.input_width))
b_grad = np.zeros((self.state_width, 1))
return Wh_grad, Wx_grad, b_grad
def calc_gradient_t(self, t):
'''
计算每个时刻t权重的梯度
'''

h_prev = self.h_list[t-1].transpose()
Wfh_grad = np.dot(self.delta_f_list[t], h_prev)
bf_grad = self.delta_f_list[t]
Wih_grad = np.dot(self.delta_i_list[t], h_prev)
bi_grad = self.delta_f_list[t]
Woh_grad = np.dot(self.delta_o_list[t], h_prev)
bo_grad = self.delta_f_list[t]
Wch_grad = np.dot(self.delta_ct_list[t], h_prev)
bc_grad = self.delta_ct_list[t]
return Wfh_grad, bf_grad, Wih_grad, bi_grad, \
Woh_grad, bo_grad, Wch_grad, bc_grad

13.8.5梯度下降算法的实现

下面是用梯度下降算法来更新权重

def update(self):
'''
按照梯度下降,更新权重
'''

self.Wfh -= self.learning_rate * self.Whf_grad
self.Wfx -= self.learning_rate * self.Whx_grad
self.bf -= self.learning_rate * self.bf_grad
self.Wih -= self.learning_rate * self.Whi_grad
self.Wix -= self.learning_rate * self.Whi_grad
self.bi -= self.learning_rate * self.bi_grad
self.Woh -= self.learning_rate * self.Wof_grad
self.Wox -= self.learning_rate * self.Wox_grad
self.bo -= self.learning_rate * self.bo_grad
self.Wch -= self.learning_rate * self.Wcf_grad
self.Wcx -= self.learning_rate * self.Wcx_grad
self.bc -= self.learning_rate * self.bc_grad

13.8.6梯度检查的实现

和RecurrentLayer一样,为了支持梯度检查,我们需要支持重置内部状态。

def reset_state(self):
# 当前时刻初始化为t0
self.times = 0
# 各个时刻的单元状态向量c
self.c_list = self.init_state_vec()
# 各个时刻的输出向量h
self.h_list = self.init_state_vec()
# 各个时刻的遗忘门f
self.f_list = self.init_state_vec()
# 各个时刻的输入门i
self.i_list = self.init_state_vec()
# 各个时刻的输出门o
self.o_list = self.init_state_vec()
# 各个时刻的即时状态c~
self.ct_list = self.init_state_vec()

梯度检查的代码:

def data_set():
x = [np.array([[1], [2], [3]]),
np.array([[2], [3], [4]])]
d = np.array([[1], [2]])
return x, d
def gradient_check():
'''
梯度检查
'''

# 设计一个误差函数,取所有节点输出项之和
error_function = lambda o: o.sum()
lstm = LstmLayer(3, 2, 1e-3)
# 计算forward值
x, d = data_set()
lstm.forward(x[0])
lstm.forward(x[1])
# 求取sensitivity map
sensitivity_array = np.ones(lstm.h_list[-1].shape,
dtype=np.float64)
# 计算梯度
lstm.backward(x[1], sensitivity_array, IdentityActivator())
# 检查梯度
epsilon = 10e-4
for i in range(lstm.Wfh.shape[0]):
for j in range(lstm.Wfh.shape[1]):
lstm.Wfh[i,j] += epsilon
lstm.reset_state()
lstm.forward(x[0])
lstm.forward(x[1])
err1 = error_function(lstm.h_list[-1])
lstm.Wfh[i,j] -= 2*epsilon
lstm.reset_state()
lstm.forward(x[0])
lstm.forward(x[1])
err2 = error_function(lstm.h_list[-1])
expect_grad = (err1 - err2) / (2 * epsilon)
lstm.Wfh[i,j] += epsilon
print( 'weights(%d,%d): expected - actural %.4e - %.4e' % (
i, j, expect_grad, lstm.Wfh_grad[i,j]))
return lstm

13.8.7 运行

为便于运行,需要把一些函数,如forward、calc_gate、backward、calc_gradient等整合到LstmLayer类中
具体如下:

class LstmLayer(object):
def __init__(self, input_width, state_width,
learning_rate):
self.input_width = input_width
self.state_width = state_width
self.learning_rate = learning_rate
# 门的激活函数
self.gate_activator = SigmoidActivator()
# 输出的激活函数
self.output_activator = TanhActivator()
# 当前时刻初始化为t0
self.times = 0
# 各个时刻的单元状态向量c
self.c_list = self.init_state_vec()
# 各个时刻的输出向量h
self.h_list = self.init_state_vec()
# 各个时刻的遗忘门f
self.f_list = self.init_state_vec()
# 各个时刻的输入门i
self.i_list = self.init_state_vec()
# 各个时刻的输出门o
self.o_list = self.init_state_vec()
# 各个时刻的即时状态c~
self.ct_list = self.init_state_vec()
# 遗忘门权重矩阵Wfh, Wfx, 偏置项bf
self.Wfh, self.Wfx, self.bf = (
self.init_weight_mat())
# 输入门权重矩阵Wfh, Wfx, 偏置项bf
self.Wih, self.Wix, self.bi = (
self.init_weight_mat())
# 输出门权重矩阵Wfh, Wfx, 偏置项bf
self.Woh, self.Wox, self.bo = (
self.init_weight_mat())
# 单元状态权重矩阵Wfh, Wfx, 偏置项bf
self.Wch, self.Wcx, self.bc = (
self.init_weight_mat())
def init_state_vec(self):
'''
初始化保存状态的向量
'''

state_vec_list = []
state_vec_list.append(np.zeros(
(self.state_width, 1)))
return state_vec_list
def init_weight_mat(self):
'''
初始化权重矩阵
'''

Wh = np.random.uniform(-1e-4, 1e-4,
(self.state_width, self.state_width))
Wx = np.random.uniform(-1e-4, 1e-4,
(self.state_width, self.input_width))
b = np.zeros((self.state_width, 1))
return Wh, Wx, b
def forward(self, x):
'''
根据式1-式6进行前向计算
'''

self.times += 1
# 遗忘门
fg = self.calc_gate(x, self.Wfx, self.Wfh,
self.bf, self.gate_activator)
self.f_list.append(fg)
# 输入门
ig = self.calc_gate(x, self.Wix, self.Wih,
self.bi, self.gate_activator)
self.i_list.append(ig)
# 输出门
og = self.calc_gate(x, self.Wox, self.Woh,
self.bo, self.gate_activator)
self.o_list.append(og)
# 即时状态
ct = self.calc_gate(x, self.Wcx, self.Wch,
self.bc, self.output_activator)
self.ct_list.append(ct)
# 单元状态
c = fg * self.c_list[self.times - 1] + ig * ct
self.c_list.append(c)
# 输出
h = og * self.output_activator.forward(c)
self.h_list.append(h)
def calc_gate(self, x, Wx, Wh, b, activator):
'''
计算门
'''

h = self.h_list[self.times - 1] # 上次的LSTM输出
net = np.dot(Wh, h) + np.dot(Wx, x) + b
gate = activator.forward(net)
return gate
def backward(self, x, delta_h, activator):
'''
实现LSTM训练算法
'''

self.calc_delta(delta_h, activator)
self.calc_gradient(x)
def calc_delta(self, delta_h, activator):
# 初始化各个时刻的误差项
self.delta_h_list = self.init_delta() # 输出误差项
self.delta_o_list = self.init_delta() # 输出门误差项
self.delta_i_list = self.init_delta() # 输入门误差项
self.delta_f_list = self.init_delta() # 遗忘门误差项
self.delta_ct_list = self.init_delta() # 即时输出误差项
# 保存从上一层传递下来的当前时刻的误差项
self.delta_h_list[-1] = delta_h
# 迭代计算每个时刻的误差项
for k in range(self.times, 0, -1):
self.calc_delta_k(k)
def init_delta(self):
'''
初始化误差项
'''

delta_list = []
for i in range(self.times + 1):
delta_list.append(np.zeros(
(self.state_width, 1)))
return delta_list
def calc_delta_k(self, k):
'''
根据k时刻的delta_h,计算k时刻的delta_f、
delta_i、delta_o、delta_ct,以及k-1时刻的delta_h
'''

# 获得k时刻前向计算的值
ig = self.i_list[k]
og = self.o_list[k]
fg = self.f_list[k]
ct = self.ct_list[k]
c = self.c_list[k]
c_prev = self.c_list[k-1]
tanh_c = self.output_activator.forward(c)
delta_k = self.delta_h_list[k]
# 根据式9计算delta_o
delta_o = (delta_k * tanh_c *
self.gate_activator.backward(og))
delta_f = (delta_k * og *
(1 - tanh_c * tanh_c) * c_prev *
self.gate_activator.backward(fg))
delta_i = (delta_k * og *
(1 - tanh_c * tanh_c) * ct *
self.gate_activator.backward(ig))
delta_ct = (delta_k * og *
(1 - tanh_c * tanh_c) * ig *
self.output_activator.backward(ct))
delta_h_prev = (
np.dot(delta_o.transpose(), self.Woh) +
np.dot(delta_i.transpose(), self.Wih) +
np.dot(delta_f.transpose(), self.Wfh) +
np.dot(delta_ct.transpose(), self.Wch)
).transpose()
# 保存全部delta值
self.delta_h_list[k-1] = delta_h_prev
self.delta_f_list[k] = delta_f
self.delta_i_list[k] = delta_i
self.delta_o_list[k] = delta_o
self.delta_ct_list[k] = delta_ct
def calc_gradient(self, x):
# 初始化遗忘门权重梯度矩阵和偏置项
self.Wfh_grad, self.Wfx_grad, self.bf_grad = (
self.init_weight_gradient_mat())
# 初始化输入门权重梯度矩阵和偏置项
self.Wih_grad, self.Wix_grad, self.bi_grad = (
self.init_weight_gradient_mat())
# 初始化输出门权重梯度矩阵和偏置项
self.Woh_grad, self.Wox_grad, self.bo_grad = (
self.init_weight_gradient_mat())
# 初始化单元状态权重梯度矩阵和偏置项
self.Wch_grad, self.Wcx_grad, self.bc_grad = (
self.init_weight_gradient_mat())
# 计算对上一次输出h的权重梯度
for t in range(self.times, 0, -1):
# 计算各个时刻的梯度
(Wfh_grad, bf_grad,
Wih_grad, bi_grad,
Woh_grad, bo_grad,
Wch_grad, bc_grad) = (
self.calc_gradient_t(t))
# 实际梯度是各时刻梯度之和
self.Wfh_grad += Wfh_grad
self.bf_grad += bf_grad
self.Wih_grad += Wih_grad
self.bi_grad += bi_grad
self.Woh_grad += Woh_grad
self.bo_grad += bo_grad
self.Wch_grad += Wch_grad
self.bc_grad += bc_grad
print( '-----%d-----' % t)
print(Wfh_grad)
print(self.Wfh_grad)
# 计算对本次输入x的权重梯度
xt = x.transpose()
self.Wfx_grad = np.dot(self.delta_f_list[-1], xt)
self.Wix_grad = np.dot(self.delta_i_list[-1], xt)
self.Wox_grad = np.dot(self.delta_o_list[-1], xt)
self.Wcx_grad = np.dot(self.delta_ct_list[-1], xt)
def init_weight_gradient_mat(self):
'''
初始化权重矩阵
'''

Wh_grad = np.zeros((self.state_width,
self.state_width))
Wx_grad = np.zeros((self.state_width,
self.input_width))
b_grad = np.zeros((self.state_width, 1))
return Wh_grad, Wx_grad, b_grad
def calc_gradient_t(self, t):
'''
计算每个时刻t权重的梯度
'''

h_prev = self.h_list[t-1].transpose()
Wfh_grad = np.dot(self.delta_f_list[t], h_prev)
bf_grad = self.delta_f_list[t]
Wih_grad = np.dot(self.delta_i_list[t], h_prev)
bi_grad = self.delta_f_list[t]
Woh_grad = np.dot(self.delta_o_list[t], h_prev)
bo_grad = self.delta_f_list[t]
Wch_grad = np.dot(self.delta_ct_list[t], h_prev)
bc_grad = self.delta_ct_list[t]
return Wfh_grad, bf_grad, Wih_grad, bi_grad, \
Woh_grad, bo_grad, Wch_grad, bc_grad
def reset_state(self):
# 当前时刻初始化为t0
self.times = 0
# 各个时刻的单元状态向量c
self.c_list = self.init_state_vec()
# 各个时刻的输出向量h
self.h_list = self.init_state_vec()
# 各个时刻的遗忘门f
self.f_list = self.init_state_vec()
# 各个时刻的输入门i
self.i_list = self.init_state_vec()
# 各个时刻的输出门o
self.o_list = self.init_state_vec()
# 各个时刻的即时状态c~
self.ct_list = self.init_state_vec()

运行检查梯度函数
gradient_check()
运行结果如下:

-----2-----
[[ 3.54293058e-10 9.68056914e-11]
[ 9.68010040e-11 2.64495392e-11]]
[[ 3.54293058e-10 9.68056914e-11]
[ 9.68010040e-11 2.64495392e-11]]
-----1-----
[[ 0. 0.]
[ 0. 0.]]
[[ 3.54293058e-10 9.68056914e-11]
[ 9.68010040e-11 2.64495392e-11]]
weights(0,0): expected - actural 3.5432e-10 - 3.5429e-10
weights(0,1): expected - actural 9.6844e-11 - 9.6806e-11
weights(1,0): expected - actural 9.6801e-11 - 9.6801e-11
weights(1,1): expected - actural 2.6420e-11 - 2.6450e-11

参考:https://zybuluo.com/hanbingtao/note/541458

第5 章 用Sklearn预测客户流失

“流失率”是描述客户离开或停止支付产品或服务费率的业务术语。这在许多企业中是一个关键的数字,因为通常情况下,获取新客户的成本比保留现有成本(在某些情况下,贵5到20倍)。
因此,了解保持客户参与度是非常宝贵的,因为它是开发保留策略和推出旨在阻止客户流失的重要基础。因此,公司对开发更好的流失检测技术愈来愈有兴趣,导致许多人寻求数据挖掘和机器学习以获得新的和创造性的方法。

5.1 导入数据集

这里我们以一个长期的电信客户数据作为数据集,您可以点击这里下载数据集。
数据很简单。 每行代表一个预订的电话用户。 每列包含客户属性,例如电话号码,在一天中不同时间使用的通话分钟,服务产生的费用,生命周期帐户持续时间等。
使用pandas方便直接读取.csv里数据

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import json

from sklearn.cross_validation import KFold
from sklearn.preprocessing import StandardScaler
from sklearn.cross_validation import train_test_split
from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier as RF
%matplotlib inline

churn_df = pd.read_csv('./data/customer_loss/churn.csv')
col_names = churn_df.columns.tolist()

print(col_names)

to_show = col_names[:3] + col_names[-6:]
churn_df[to_show].head(4)

显示结果如下:

5.2 预处理数据集

删除不相关的列,并将字符串转换为布尔值(因为模型不处理“yes”和“no”非常好)。 其余的数字列保持不变。
将预测结果分离转化为0,1形式,把False转换为,把True转换为1。

churn_result = churn_df['Churn?']
y = np.where(churn_result == 'True.',1,0)
y[:20]
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0])

删除一些不必要的特征或字段,这里是从业务角度进行取舍。

to_drop = ['State','Area Code','Phone','Churn?','Unnamed: 21']
churn_feat_space = churn_df.drop(to_drop,axis=1)

将属性值yes,no等转化为boolean values,即False或True。numpy将把这些值转换为1或0.

yes_no_cols = ["Int'l Plan","VMail Plan"]
churn_feat_space[yes_no_cols] = churn_feat_space[yes_no_cols] == 'yes'

获取新的属性和属性值。

features = churn_feat_space.columns
X = churn_feat_space.as_matrix().astype(np.float)

对规模级别差距较大的数据,需要进行规范化处理。 例如:篮球队每场比赛得分的分数自然比他们的胜率要大几个数量级。 但这并不意味着后者的重要性低100倍,故需要进行标准化处理。
公式为:(X-mean)/std 计算时对每个属性/每列分别进行。
将数据按期属性(按列进行)减去其均值,并处以其方差。得到的结果是,对于每个属性/每列来说所有数据都聚集在-1到1附近,方差为1。这里直接使用sklearn库中数据预处理模块:StandardScaler

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X = scaler.fit_transform(X)

打印出数据数量和属性个数及分类label。

print("Feature space holds %d observations and %d features" % X.shape)
print("Unique target labels:", np.unique(y))
Feature space holds 3332 observations and 17 features
Unique target labels: [0 1]

5.3 使用交叉验证方法

模型的性能如何?我们可通过哪些方法来验证或检验一个模型的优劣?交叉验证是一种有效方法。此外,利用交叉验证尝试避免过拟合(对同一数据点进行训练和预测),同时仍然为每个观测数据集产生预测。
交叉验证(Cross Validation)的基本思想是把在某种意义下将原始数据(dataset)进行分组,一部分做为训练集(train set),另一部分做为验证集(validation set or test set),首先用训练集对分类器进行训练,再利用验证集来测试训练得到的模型(model),以此来做为评价分类器的性能指标。
另外这里顺便介绍一下使用scikit-learn库的常用算法,如下图,接下来我们将使用多种方法来构建模型,然后比较各种算法的性能。

from sklearn.cross_validation import KFold
def run_cv(X,y,clf_class,**kwargs):
# Construct a kfolds object
kf = KFold(len(y),n_folds=5,shuffle=True)
y_pred = y.copy()
# Iterate through folds
for train_index, test_index in kf:
X_train, X_test = X[train_index], X[test_index]
y_train = y[train_index]
# Initialize a classifier with key word arguments
clf = clf_class(**kwargs)
clf.fit(X_train,y_train)
y_pred[test_index] = clf.predict(X_test)
return y_pred

5.4 训练模型

比较三个相当独特的算法:支持向量机,集成方法,随机森林和k最近邻。 然后,显示各分类器预测正确率。

from sklearn.svm import SVC
from sklearn.ensemble import RandomForestClassifier as RF
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.linear_model import LogisticRegression as LR
from sklearn.ensemble import GradientBoostingClassifier as GBC
from sklearn.metrics import average_precision_score

def accuracy(y_true,y_pred):
# NumPy interpretes True and False as 1. and 0.
return np.mean(y_true == y_pred)

print( "Logistic Regression:")
print( "%.3f" % accuracy(y, run_cv(X,y,LR)))
print( "Gradient Boosting Classifier")
print( "%.3f" % accuracy(y, run_cv(X,y,GBC)))
print( "Support vector machines:")
print( "%.3f" % accuracy(y, run_cv(X,y,SVC)))
print( "Random forest:")
print( "%.3f" % accuracy(y, run_cv(X,y,RF)))
print( "K-nearest-neighbors:")
print( "%.3f" % accuracy(y, run_cv(X,y,KNN)))

Logistic Regression:
0.862
Gradient Boosting Classifier
0.951
Support vector machines:
0.922
Random forest:
0.942
K-nearest-neighbors:
0.893

可以看出Gradient Boosting、Random forest性能较好。

5.5 精确率和召回率

评估模型有很多方法,对分类问题,尤其是二分类问题通常采用准确率和召回率来衡量。这里我们将使用scikit-learn一个内置的函数来构造混淆矩阵。该矩阵反应模型的这些指标。混淆矩阵是一种可视化由分类器进行的预测的方式,并且仅仅是一个表格,其示出了对于特定类的预测的分布。 x轴表示每个观察的真实类别(如果客户流失或不流失),而y轴对应于模型预测的类别(如果我的分类器表示客户会流失或不流失)。这里补充一下有关混淆矩阵的有关定义,供大家参考。
二元分类的混淆矩阵形式如下:
True Positive(真正,TP):将正类预测为正类数;
True Negative(真负,TN):将负类预测为负类数;
False Positive(假正,FP):将负类预测为正类数误报 (Type I error);
False Negative(假负,FN):将正类预测为负类数→漏报 (Type II error)


混淆矩阵的缺点:
一些positive事件发生概率极小的不平衡数据集(imbalanced data),混淆矩阵可能效果不好。比如对信用卡交易是否异常做分类的情形,很可能1万笔交易中只有1笔交易是异常的。一个将所有交易都判定为正常的分类器,准确率是99.99%。这个数字虽然很高,但是没有任何现实意义。

from sklearn.metrics import confusion_matrix
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score

def draw_confusion_matrices(confusion_matricies,class_names):
class_names = class_names.tolist()
for cm in confusion_matrices:
classifier, cm = cm[0], cm[1]
print(cm)

fig = plt.figure()
ax = fig.add_subplot(111)
cax = ax.matshow(cm)
plt.title('Confusion matrix for %s' % classifier)
fig.colorbar(cax)
ax.set_xticklabels([''] + class_names)
ax.set_yticklabels([''] + class_names)
plt.xlabel('Predicted')
plt.ylabel('True')
plt.show()

y = np.array(y)
class_names = np.unique(y)

confusion_matrices = [
( "Support Vector Machines", confusion_matrix(y,run_cv(X,y,SVC)) ),
( "Random Forest", confusion_matrix(y,run_cv(X,y,RF)) ),
( "K-Nearest-Neighbors", confusion_matrix(y,run_cv(X,y,KNN)) ),
( "Gradient Boosting Classifier", confusion_matrix(y,run_cv(X,y,GBC)) ),
( "Logisitic Regression", confusion_matrix(y,run_cv(X,y,LR)) )
]

# Pyplot code not included to reduce clutter
# from churn_display import draw_confusion_matrices
%matplotlib inline

draw_confusion_matrices(confusion_matrices,class_names)

5.6 ROC & AUC

这里我们简单介绍一下这两个概念。
ROC曲线是根据一系列不同的二分类方式(分界值或决定阈),以真阳性率(灵敏度)为纵坐标,假阳性率(1-特异度)为横坐标绘制的曲线。传统的诊断试验评价方法有一个共同的特点,必须将试验结果分为两类,再进行统计分析。ROC曲线的评价方法与传统的评价方法不同,无须此限制,而是根据实际情况,允许有中间状态,可以把试验结果划分为多个有序分类,如正常、大致正常、可疑、大致异常和异常五个等级再进行统计分析。因此,ROC曲线评价方法适用的范围更为广泛
AUC值为ROC曲线所覆盖的区域面积,显然,AUC越大,分类器分类效果越好。

from sklearn.metrics import roc_curve, auc
from scipy import interp

def plot_roc(X, y, clf_class, **kwargs):
kf = KFold(len(y), n_folds=5, shuffle=True)
y_prob = np.zeros((len(y),2))
mean_tpr = 0.0
mean_fpr = np.linspace(0, 1, 100)
all_tpr = []
for i, (train_index, test_index) in enumerate(kf):
X_train, X_test = X[train_index], X[test_index]
y_train = y[train_index]
clf = clf_class(**kwargs)
clf.fit(X_train,y_train)
# Predict probabilities, not classes
y_prob[test_index] = clf.predict_proba(X_test)
fpr, tpr, thresholds = roc_curve(y[test_index], y_prob[test_index, 1])
mean_tpr += interp(mean_fpr, fpr, tpr)
mean_tpr[0] = 0.0
roc_auc = auc(fpr, tpr)
plt.plot(fpr, tpr, lw=1, label='ROC fold %d (area = %0.2f)' % (i, roc_auc))
mean_tpr /= len(kf)
mean_tpr[-1] = 1.0
mean_auc = auc(mean_fpr, mean_tpr)
plt.plot(mean_fpr, mean_tpr, 'k--',label='Mean ROC (area = %0.2f)' % mean_auc, lw=2)

plt.plot([0, 1], [0, 1], '--', color=(0.6, 0.6, 0.6), label='Random')
plt.xlim([-0.05, 1.05])
plt.ylim([-0.05, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver operating characteristic')
plt.legend(loc="lower right")
plt.show()

print("Support vector machines:")
print(plot_roc(X,y,SVC,probability=True))

print("Random forests:")
print(plot_roc(X,y,RF,n_estimators=18))

print("K-nearest-neighbors:")
print(plot_roc(X,y,KNN))

print("Gradient Boosting Classifier:")
print(plot_roc(X,y,GBC))

5.7 特征分析

我们想进一步分析,流失客户有哪些比较明显的特征。或哪些特征对客户流失影响比较大。

train_index,test_index = train_test_split(churn_df.index)

forest = RF()
forest_fit = forest.fit(X[train_index], y[train_index])
forest_predictions = forest_fit.predict(X[test_index])

importances = forest_fit.feature_importances_[:10]
std = np.std([tree.feature_importances_ for tree in forest.estimators_],
axis=0)
indices = np.argsort(importances)[::-1]

# Print the feature ranking
print("Feature ranking:")

for f in range(10):
#print("%d. %s (%f)" % (f + 1, features[f], importances[indices[f]]))
print("%d. %s (%f)" % (f + 1, features[f], importances[f]))
# Plot the feature importances of the forest
#import pylab as pl
plt.figure()
plt.title("Feature importances")
plt.bar(range(10), importances[indices], yerr=std[indices], color="r", align="center")
plt.xticks(range(10), indices)
plt.xlim([-1, 10])
plt.show()

Feature ranking:
1. Account Length (0.026546)
2. Int'l Plan (0.054757)
3. VMail Plan (0.020334)
4. VMail Message (0.041116)
5. Day Mins (0.164627)
6. Day Calls (0.026826)
7. Day Charge (0.120162)
8. Eve Mins (0.086248)
9. Eve Calls (0.022896)
10. Eve Charge (0.046945)

<img src="http://www.feiguyunai.com/wp-content/uploads/2017/11/3620ceddf163b3f60d0ba6544c5098ca.png" alt="" />
def run_prob_cv(X, y, clf_class, roc=False, **kwargs):
kf = KFold(len(y), n_folds=5, shuffle=True)
y_prob = np.zeros((len(y),2))
for train_index, test_index in kf:
X_train, X_test = X[train_index], X[test_index]
y_train = y[train_index]
clf = clf_class(**kwargs)
clf.fit(X_train,y_train)
# Predict probabilities, not classes
y_prob[test_index] = clf.predict_proba(X_test)
return y_prob
import warnings
warnings.filterwarnings('ignore')

# Use 10 estimators so predictions are all multiples of 0.1
pred_prob = run_prob_cv(X, y, RF, n_estimators=10)
pred_churn = pred_prob[:,1]
is_churn = y == 1

# Number of times a predicted probability is assigned to an observation
counts = pd.value_counts(pred_churn)
counts[:]

0.0 1782
0.1 666
0.2 276
0.3 123
0.9 89
0.4 85
0.7 73
0.8 65
1.0 63
0.6 61
0.5 49
dtype: int64

from collections import defaultdict
true_prob = defaultdict(float)

# calculate true probabilities
for prob in counts.index:
true_prob[prob] = np.mean(is_churn[pred_churn == prob])
true_prob = pd.Series(true_prob)

# pandas-fu
counts = pd.concat([counts,true_prob], axis=1).reset_index()
counts.columns = ['pred_prob', 'count', 'true_prob']
counts


我们可以看到,随机森林预测89个人将有0.9的可能性的流失,而在现实中,该群体具有大约0.98的流失率。

参考:
http://www.sohu.com/a/149724745_99906660
http://nbviewer.jupyter.org/github/donnemartin/data-science-ipython-notebooks/blob/master/analyses/churn.ipynb

第12章 TensorFlow卷积神经网络

12.1 卷积神经网络简介

卷积神经网路(Convolutional Neural Network, CNN)是一种前馈神经网络,对于CNN最早可以追溯到1986年BP算法的提出,1989年LeCun将其用到多层神经网络中,直到1998年LeCun提出LeNet-5模型,神经网络的雏形基本形成。在接下来近十年的时间里,卷积神经网络的相关研究处于低谷,原因有两个:一是研究人员意识到多层神经网络在进行BP训练时的计算量极其之大,当时的硬件计算能力完全不可能实现;二是包括SVM在内的浅层机器学习算法也渐渐开始暂露头脚。
2006年,Hinton终于一鸣惊人,在《科学》上发表文章,CNN再度觉醒,并取得长足发展。2012年,ImageNet大赛上CNN夺冠,2014年,谷歌研发出20层的VGG模型。同年,DeepFace、DeepID模型横空出世,直接将LFW数据库上的人脸识别、人脸认证的正确率刷到99.75%,几乎超越人类。
卷积神经网路由一个或多个卷积层和顶端的全连通层(对应经典的神经网路)组成,同时也包括关联权重和池化层(pooling layer)。这一结构使得卷积神经网路能够利用输入数据的二维结构。与其他深度学习结构相比,卷积神经网路在图像和语音识别方面能够给出更好的结果。这一模型也可以使用反向传播算法进行训练。相比较其他深度、前馈神经网路,卷积神经网路需要考量的参数更少,使之成为一种颇具吸引力的深度学习结构。

12.2卷积的定义

卷积现在可能是深度学习中最重要的概念。正是靠着卷积和卷积神经网络,深度学习才超越了几乎其他所有的机器学习方法。要理解卷积神经网络,首先需要了解卷积的含义,卷积的定义有点抽象,我们先给出一个数学上的定义,然后通过一个简单例子及其物理意义来帮助大家理解。
卷积(Convolution)是一种数学运算,它是通过两个函数f和g生成第三个函数的一种运算或数学操作。卷积的数据定义为:
其离散定义为:

通常把函数g称为输入函数,函数f称为滤波器(Filter),或卷积核(kernel),得到的结果h为特征图或特征映射(feature map)。
数学上的定义有点抽象,下面我通过几个示例来帮助大家理解。
1)小李定期存款示例
小李存入100元钱,年利率是4%,按本息计算(即将每一年所获利息加入本金,以计算下一年的利息),那么在五年之后他能拿到的钱数是:

以此类推,如果小李每年都往银行中存入新的100元钱,那么这个收益表格将是这样的:

在上式中,f(t)为小李的存钱函数,而g(t)为存入银行的每一笔钱的本息计算函数。在这里,小李最终得到的钱就是他的存钱函数和本息计算函数的卷积。
如果我们把这个公式推广到连续的情况,也就是说,小李在从0到x的这一段时间内,每时每刻都往银行里存钱,他的存钱函数为
f(t) (0≤t≤x)
而银行也对他存入的每一笔钱按本息公式计算收益:

通过上面这个例子,大家应该能够很清晰地记住卷积公式了。
如果我们将小李的存款函数视为一个信号发生(也就是激励)的过程,而将本息函数
g(x-t)
视为一个系统对信号的响应函数(也就是响应),那么二者的卷积
(f*g)(x)
就可以看做是在x时刻对系统进行观察,得到的观察结果(也就是输出)将是过去产生的所有信号经过系统的处理后得到的结果的叠加或加权平均,这也就是卷积的物理意义了。
参考了:https://www.zhihu.com/question/21686447
2)两个方形脉冲波的卷积示例
下图为两个方形脉冲波的卷积。其中函数"g"首先对τ=0反射,接着平移"t",成为 g(t-τ)。那么重叠部分的面积就相当于"t"处的卷积,其中横坐标代表待变量τ 以及新函数f*g的自变量"t"。
Convolution_of_box_signal_with_itself2
图1 两个方形脉冲的卷积

下图示方形脉冲波和指数衰退的脉冲波的卷积(后者可能出现于RC电路中),同样地重叠部分面积就相当于"t"处的卷积。注意到因为"g"是对称的,所以在这两张图中,反射并不会改变它的形状。

Convolution_of_spiky_function
图2 一个方形与指数衰退的脉冲波的卷积

该图取自:
https://zh.wikipedia.org/wiki/%E5%8D%B7%E7%A7%AF

12.3卷积运算

上节我们通过一些简单示例介绍了卷积的定义,卷积本质上是一个线性运算,因此卷积操作也被称为线性滤波。卷积如何运算?为提供一个直观印象,我们先看一个简单的二维空间卷积运算示例:

这个二维空间的卷积运算对两个输入张量(即输入和卷积核)进行卷积,并把结果输出。
接下来再看一个二维卷积详细运算过程,以便于大家对卷积运算有个更全面的了解。
saddle_point_evaluation_optimizers

图3 卷积运算
该图取自:https://www.zybuluo.com/hanbingtao/note/485480
在以上计算中,输入为一个5x5的矩阵,移动窗口为3x3矩阵,又称为卷积矩阵或卷积核,这个矩阵值为共享参数,移动步长或步幅(stride)为1,当然这个值可以大于1,如2或3等。小窗口移动过程中,其中的值是保持不变的:

图4 卷积核
这就是参数共享的说法,这些参数就决定一个卷积核,每个卷积核能够提取某一部分的特征。一般情况下,只有一个卷积核是不够的,在实际处理中,往往通过多个卷积核来提取多重特征,每一个卷积核与原始输入数据执行卷积操作后得到一个特征映射,如下图:

上图中,最右边,特征映射矩阵中的4,具体计算公式如下:

上图中,最右边特征映射矩阵中的3,具体计算公式如下:

其他以此类推,最后输出结果为:

图5 输出结果
以上介绍了一个输入,通过一个卷积核后,得到一个特征映射。如果输入有多个,卷积核也有多个情况又如何呢?另外,步幅也可以是大于1的整数,如为2,如果原来的输入为5*5的矩阵,卷积核还是3*3的矩阵,那么卷积核小窗口按步幅2从左到右,从上到下移动过程中,可能导致卷积核窗口部分在输入矩阵之外,如下图:

当卷积窗口在3这个位置时,它只覆盖输入矩阵的一列,这个时候该如何处理呢?为防止信息丢失,我们采用边界外面补0(Zero padding)策略。下面的动画显示了包含两个卷积核的卷积层的计算。我们可以看到7*7*3输入,经过两个3*3*3的卷积(步幅为2),得到了3*3*2的输出。另外我们也会看到下图的Zero padding是1,也就是在输入元素的周围补了一圈0。Zero padding对于图像边缘部分的特征提取是很有帮助的,可以防止信息丢失。

图6 多个输入及卷积核的卷积计算

以上就是卷积层的计算方法。这里面体现了局部连接和权值共享:每层神经元只和上一层部分神经元相连(卷积计算规则),且卷积核的权值对于上一层所有神经元都是一样的。对于包含两个3*3*3的卷积核的卷积层来说,其参数数量仅有(3*3*3+1)*2=56个,且参数数量与上一层神经元个数无关。与全连接神经网络相比,其参数数量大大减少了。

这是通常的卷积运算,此外,还有多种卷积运算,如空洞卷积、分离卷积等等,这里我们介绍一下空洞卷积(atrous convolutions),空洞卷积又称为扩张卷积(dilated convolutions),向卷积层引入了一个称为 “扩张率(dilation rate)”的新参数,该参数定义了卷积核处理数据时各值的间距。

卷积核为3、扩张率为2和无边界扩充的二维空洞卷积
一个扩张率为2的3×3卷积核,感受野与5×5的卷积核相同,而且仅需要9个参数。你可以把它想象成一个5×5的卷积核,每隔一行或一列删除一行或一列。
在相同的计算条件下,空洞卷积提供了更大的感受野。空洞卷积经常用在实时图像分割中。当网络层需要较大的感受野,但计算资源有限而无法提高卷积核数量或大小时,可以考虑空洞卷积。

卷积核,从这个名字可以看出它的重要性,它是整个卷积过程的核心。比较简单的卷积核或过滤器有Horizontalfilter、Verticalfilter、Sobel filter等。这些过滤器能够检测图像的水平边缘、垂直边缘、增强图片中心区域权重等。

12.4 卷积网络结构

卷积神经网络到底啥样?为获取一个感性认识,我们先看一下卷积神经网络的示意图。

图7 卷积神经网络
由图7可知,一个卷积神经网络由若干卷积层(Convolution)、池化层(Pooling)、全连接层(fully connected)组成。
从图7我们可以发现卷积神经网络的层结构和全连接神经网络的层结构有很大不同。全连接神经网络每层的神经元是按照一维排列的,也就是排成一条线的样子;而卷积神经网络每层的神经元是按照三维排列的,也就是排成一个长方体的样子,有宽度、高度和深度。
对于图7展示的神经网络,我们看到输入层的宽度和高度对应于输入图像的宽度和高度,而它的深度为1。接着,第一个卷积层对这幅图像进行了卷积操作,得到了三个特征映射。这里的"3"可能是让很多初学者迷惑的地方,实际上,就是这个卷积层包含三个卷积核,也就是三套参数(或共享参数),每个卷积核都可以把原始输入图像卷积得到一个特征映射,三个卷积核就可以得到三个特征映射。至于一个卷积层可以有多少个卷积核,那是可以自由设定的。也就是说,卷积层的卷积核个数也是一个超参数。我们可以把特征映射可以看做是通过卷积变换提取到的图像特征,三个卷积核就对原始图像提取出三组不同的特征,也就是得到了三个特征映射,也称做三个通道(channel)。
继续观察图7,在第一个卷积层之后,Pooling层对三个特征映射做了下采样,得到了三个更小的特征映射。接着,是第二个卷积层,它有5个卷积核。每个卷积核都把前面下采样之后的3个**特征映射卷积在一起,得到一个新的特征映射。这样,5个卷积核就得到了5个特征映射。接着,是第二个Pooling,继续对5个特征映射进行下采样,得到了5个更小的特征映射。
图7所示网络的最后两层是全连接层。第一个全连接层的每个神经元,和上一层5个特征映射中的每个神经元相连,第二个全连接层(也就是输出层)的每个神经元,则和第一个全连接层的每个神经元相连,这样得到了整个网络的输出。
至此,我们对卷积神经网络有了最基本的感性认识。接下来,我们将介绍卷积神经网络中各种层的计算和训练。

12.4.1卷积层

我们知道一般神经网络没有卷积层,深度学习中的卷积层能解决哪些问题?
神经网络在处理图像时数据量太大,例如一个输入1000*1000像素的图片(一百万像素,现在已经不能算大图了),输入层有1000*1000=100万节点。假设第一个隐藏层有100个节点(这个数量并不多),那么仅这一层就有(1000*1000+1)*100=1亿参数,这实在是太多了!神经网络训练本来就慢,这么多数据是不合理的。我们看卷积如何解决这个问题。
主要从三个方面:
局部连接
这个是最容易想到的,每个神经元不再和上一层的所有神经元相连,而只和一小部分神经元相连。这样就减少了很多参数。
权值共享
一组连接可以共享同一个权重,而不是每个连接有一个不同的权重,这样又减少了很多参数。
池化或下采样
可以使用Pooling来减少每层的样本数,进一步减少参数数量,同时还可以提升模型的鲁棒性。
对于图像识别任务来说,卷积神经网络通过尽可能保留重要的参数,去掉大量不重要的参数,来达到更好的学习效果
卷积核对图片进行卷积操作就得到的卷积层的输出,卷积核的大小是人为设定的,比如可以设置成3×3或其他尺寸,卷积核的个数也是人为设定,太多太少都不合适,卷积核的内容是训练的时候学习得来的,学习方法一般也是梯度下降法。
由上节内容可知,卷积操作,其实就是卷积核在输入的二维数据面上每次移动一个步长,进行乘法运算再除以卷积核的元素个数。
我们通常会使用多层卷积层来得到更深层次的特征图。如下:

12.4.2激活函数

为增强各层的表现力,各层的输出往往使用激活函数来生成特征映射或特征图,并以此来加入非线性因素的,因为线性模型的表达力不够。
在具体处理图像的时候,如何处理呢?我们知道在神经网络中,对于图像,我们主要采用了卷积的方式来处理,也就是对每个像素点赋予一个权值,这个操作显然就是线性的。但是样本不一定是线性可分的,为了解决这个问题,我们可以进行线性变化,或者我们引入非线性因素,解决线性模型所不能解决的问题。
在选择激活函数时应该满足注意哪些方面呢?
首先,我们知道神经网络的数学基础是处处可微的,所以选取的激活函数要能保证数据输入与输出也是可微的,运算特征是不断进行循环计算,所以在每代循环过程中,每个神经元的值也是在不断变化的。 这就导致了tanh特征相差明显时的效果会很好,在循环过程中会不断扩大特征效果显示出来,但有时,在特征相差比较复杂或是相差不是特别大时,需要更细微的分类判断的时候,sigmoid效果就好了。
其次,sigmoid 和 tanh作为激活函数的话,一定要注意一定要对 input 进行归一话,否则激活后的值都会进入平坦区,使隐层的输出全部趋同,但是 ReLU 并不需要输入归一化来防止它们达到饱和。
此外,对稀疏矩阵,也就是大多数为0的稀疏矩阵来表示。这个特性矩阵比较适合采用于Relu激活函数,Relu就是取的max(0,x),因为神经网络是不断反复计算,实际上变成了它在尝试不断试探如何用一个大多数为0的矩阵来尝试表达数据特征,结果因为稀疏特性的存在,反而这种方法变得运算得又快效果又好了。所以我们可以看到目前大部分的卷积神经网络中,基本上都是采用了ReLU 函数。
常用的激活函数
激活函数应该具有的性质:
(1)非线性。线性激活层对于深层神经网络没有作用,因为其作用以后仍然是输入的各种线性变换。。
(2)连续可微。梯度下降法的要求。
(3)范围最好不饱和,当有饱和的区间段时,若系统优化进入到该段,梯度近似为0,网络的学习就会停止。
(4)单调性,当激活函数是单调时,单层神经网络的误差函数是凸的,好优化。
(5)在原点处近似线性,这样当权值初始化为接近0的随机值时,网络可以学习的较快,不用可以调节网络的初始值。
目前常用的激活函数都只拥有上述性质的部分,没有一个拥有全部。
下面介绍几种常用的激活函数。

目前已被淘汰 ,其主要缺点:
饱和时梯度值非常小。由于BP算法反向传播的时候后层的梯度是以乘性方式传递到前层,因此当层数比较多的时候,传到前层的梯度就会非常小,网络权值得不到有效的更新,即梯度消失。如果该层的权值初始化使得f(x) 处于饱和状态时,网络基本上权值无法更新。
Tanh函数
Tanh和Sigmoid是有异曲同工之妙的,它的图形如上图右所示,不同的是它把实值得输入压缩到-1~1的范围,因此它基本是0均值的,也就解决了上述Sigmoid缺点中的第二个,所以实际中tanh会比sigmoid更常用。但是它还是存在梯度饱和的问题。Tanh是sigmoid的变形:

 

Alex在2012年提出的一种新的激活函数。该函数的提出很大程度的解决了BP算法在优化深层神经网络时的梯度耗散问题 。
优点:
(1) x>0 时,梯度恒为1,无梯度耗散问题,收敛快;
(2)增大了网络的稀疏性。当x (3)运算量很小;
缺点:
如果后层的某一个梯度特别大,导致W更新以后变得特别大,导致该层的输入<0,输出为0,这时该层就会‘die’,没有更新。当学习率比较大时可能会有40%的神经元都会在训练开始就‘die’,因此需要对学习率进行一个好的设置。
由优缺点可知max(0,x) 函数为一个双刃剑,既可以形成网络的稀疏性,也可能造成有很多永远处于‘die’的神经元,需要tradeoff。

改善了ReLU的死亡特性,但是也同时损失了一部分稀疏性,且增加了一个超参数,目前来说其好处不太明确


根据一定的概率(可配置)将输出设置为0,当引入少量随机性将有助于训练时,这个层有很好的表现,它是防止过拟合的一种策略,Dropout在训练的时候有,但在测试的时候是不能有Dropout的,毕竟只有在训练学习的时候才才担心过拟合。
简单讲,Dropout就是在按梯度下降法求网络参数的时候,每一次迭代都随机删掉一些隐含神经元,注意删掉之后会再次放回,下次迭代继续在所有隐含神经元存在的情况下随机删除。
解决过拟合的方法还有在代价函数添加正则项的方法,L1是添加绝对值,L2是添加二次方差。这两个正则项在数学上是可以证明的,能防止参数w过大。
那Dropout为什么有助于防止过拟合呢?可以简单地这样解释,运用了dropout的训练过程,相当于训练了很多个只有半数隐层单元的神经网络(后面简称为“半数网络”),每一个这样的半数网络,都可以给出一个分类结果,这些结果有的是正确的,有的是错误的。随着训练的进行,大部分半数网络都可以给出正确的分类结果,那么少数的错误分类结果就不会对最终结果造成大的影响。实际上dropout不一定非要在全连接层隐含层,卷积似乎也可以dropout。
如何选择激活函数?
通常来说,很少会把各种激活函数串起来在一个网络中使用的。
如果使用 ReLU,那么一定要小心设置 learning rate,而且要注意不要让你的网络出现很多 “dead” 神经元,如果这个问题不好解决,那么可以试试 Leaky ReLU、PReLU 或者 Maxout.
最好不要用 sigmoid,可以试试 tanh,不过可以预期它的效果会比不上 ReLU 和 Maxout。
实际使用的时候最常用的还是ReLU函数,注意学习率的设置以及死亡节点所占的比例即可。

12.4.3池化层(Pooling)

池化(Pooling)又称为下采样,通过卷积层获得了图像的特征之后,理论上我们可以直接使用这些特征训练分类器(如softmax),但是这样做将面临巨大的计算量的挑战,而且容易产生过拟合的现象。为了进一步降低网络训练参数及模型的过拟合程度,对卷积层进行池化/采样(Pooling)处理。池化/采样的方式通常有以下两种:
最大池化(Max Pooling: 选择Pooling窗口中的最大值作为采样值;
均值池化(Mean Pooling): 将Pooling窗口中的所有值相加取平均,以平均值作为采样值
高斯池化:借鉴高斯模糊的方法。不常用。
图像经过池化后,得到的是一系列的特征图,而多层感知器接受的输入是一个向量。因此需要将这些特征图中的像素依次取出,排列成一个向量。各种池化方法可用如下图来表示:

12.4.4归一化层

对于归一化我们应该不陌生,在线性回归和逻辑回归中经常使用,而且很有效。因为输入层的输入值的大小变化不剧烈,那么输入也不会。但是,对于一个可能有很多层的深度学习模型来说,情况可能会比较复杂。
举个例子,随着第一层和第二层的参数在训练时不断变化,第三层所使用的激活函数的输入值可能由于乘法效应而变得极大或极小,例如和第一层所使用的激活函数的输入值不在一个数量级上。这种在训练时可能出现的情况会造成模型训练的不稳定性。例如,给定一个学习率,某次参数迭代后,目标函数值会剧烈变化或甚至升高。数学的解释是,如果把目标函数 f根据参数 w迭代进行泰勒展开,有关学习率 η 的高阶项的系数可能由于数量级的原因(通常由于层数多)而不容忽略。然而常用的低阶优化算法(如梯度下降)对于不断降低目标函数的有效性通常基于一个基本假设:在以上泰勒展开中把有关学习率的高阶项通通忽略不计。
为了应对上述这种情况,Sergey Ioffe和Christian Szegedy在2015年提出了批量归一化(Batch Normalization, BN)的方法。简而言之,在训练时给定一个批量输入,批量归一化试图对深度学习模型的某一层所使用的激活函数的输入进行归一化:使批量呈标准正态分布(均值为0,标准差为1)。
批量归一化通常应用于输入层或任意中间层

12.4.5全连接层

全连接层(fully connected layers,FC)在整个卷积神经网络中起到“分类器”的作用。如果说卷积层、池化层和激活函数层等操作是将原始数据映射到隐层特征空间的话,全连接层则起到将学到的“分布式特征表示”映射到样本标记空间的作用。在实际使用中,全连接层可由卷积操作实现:对前层是全连接的全连接层可以转化为卷积核为1x1的卷积;而前层是卷积层的全连接层可以转化为卷积核为hxw的全局卷积,h和w分别为前层卷积结果的高和宽。
目前由于全连接层参数冗余(仅全连接层参数就可占整个网络参数80%左右),近期一些性能优异的网络模型如ResNet和GoogLeNet等均用全局平均池化(global average pooling,GAP)取代FC来融合学到的深度特征,最后仍用softmax等损失函数作为网络目标函数来指导学习过程。需要指出的是,用GAP替代FC的网络通常有较好的预测性能。
近期的研究(In Defense of Fully Connected Layers in Visual Representation Transfer)发现,FC可在模型表示能力迁移过程中充当“防火墙”的作用。具体来讲,假设在ImageNet上预训练得到的模型为 ,则ImageNet可视为源域(迁移学习中的source domain)。微调(fine tuning)是深度学习领域最常用的迁移学习技术。针对微调,若目标域(target domain)中的图像与源域中图像差异巨大(如相比ImageNet,目标域图像不是物体为中心的图像,而是风景照,见下图),不含FC的网络微调后的结果要差于含FC的网络。因此FC可视作模型表示能力的“防火墙”,特别是在源域与目标域差异较大的情况下,FC可保持较大的模型capacity从而保证模型表示能力的迁移。

12.4.6几种经典的CNN

这里介绍几种经典的CNN,如LeNet5、AlexNet、VGGNet、Google Inception Net、ResNet等。

12.4.6.1 LeNet5

LeNet5 诞生于 1994 年,是最早的卷积神经网络之一,并且推动了深度学习领域的发展。自从 1988 年开始,在许多次成功的迭代后,这项由 Yann LeCun 完成的开拓性成果被命名为 LeNet5(参见:Gradient-Based Learning Applied to Document Recognition)。

图1

LeNet5 的架构基于这样的观点:(尤其是)图像的特征分布在整张图像上,以及带有可学习参数的卷积是一种用少量参数在多个位置上提取相似特征的有效方式。在那时候,没有 GPU 帮助训练,甚至 CPU 的速度也很慢。因此,能够保存参数以及计算过程是一个关键进展。这和将每个像素用作一个大型多层神经网络的单独输入相反。LeNet5 阐述了那些像素不应该被使用在第一层,因为图像具有很强的空间相关性,而使用图像中独立的像素作为不同的输入特征则利用不到这些相关性。
LeNet5特征能够总结为如下几点:
1)卷积神经网络使用三个层作为一个系列: 卷积,池化,非线性
2) 使用卷积提取空间特征
3)使用映射到空间均值下采样(subsample)
4)双曲线(tanh)或S型(sigmoid)形式的非线性
5)多层神经网络(MLP)作为最后的分类器
6)层与层之间的稀疏连接矩阵避免大的计算成本
总体看来,这个网络是最近大量神经网络架构的起点,并且也给这个领域带来了许多灵感。

12.4.6.2 AlexNet

2012年,Hinton的学生Alex Krizhevsky提出了深度卷积神经网络模型AlexNet,它可以算是LeNet的一种更深更宽的版本。AlexNet中包含了几个比较新的技术点,也首次在CNN中成功应用了ReLU、Dropout和LRN等Trick。同时AlexNet也使用了GPU进行运算加速,作者开源了他们在GPU上训练卷积神经网络的CUDA代码。AlexNet包含了6亿3000万个连接,6000万个参数和65万个神经元,拥有5个卷积层,其中3个卷积层后面连接了最大池化层,最后还有3个全连接层。AlexNet以显著的优势赢得了竞争激烈的ILSVRC 2012比赛,top-5的错误率降低至了16.4%,相比第二名的成绩26.2%错误率有了巨大的提升。AlexNet可以说是神经网络在低谷期后的第一次发声,确立了深度学习(深度卷积网络)在计算机视觉的统治地位,同时也推动了深度学习在语音识别、自然语言处理、强化学习等领域的拓展。
AlexNet将LeNet的思想发扬光大,把CNN的基本原理应用到了很深很宽的网络中。AlexNet主要使用到的新技术点如下。
(1)成功使用ReLU作为CNN的激活函数,并验证其效果在较深的网络超过了Sigmoid,成功解决了Sigmoid在网络较深时的梯度弥散问题。虽然ReLU激活函数在很久之前就被提出了,但是直到AlexNet的出现才将其发扬光大。
(2)训练时使用Dropout随机忽略一部分神经元,以避免模型过拟合。Dropout虽有单独的论文论述,但是AlexNet将其实用化,通过实践证实了它的效果。在AlexNet中主要是最后几个全连接层使用了Dropout。
(3)在CNN中使用重叠的最大池化。此前CNN中普遍使用平均池化,AlexNet全部使用最大池化,避免平均池化的模糊化效果。并且AlexNet中提出让步长比池化核的尺寸小,这样池化层的输出之间会有重叠和覆盖,提升了特征的丰富性。
(4)提出了LRN层,对局部神经元的活动创建竞争机制,使得其中响应比较大的值变得相对更大,并抑制其他反馈较小的神经元,增强了模型的泛化能力。
(5)使用CUDA加速深度卷积网络的训练,利用GPU强大的并行计算能力,处理神经网络训练时大量的矩阵运算。AlexNet使用了两块GTX 580 GPU进行训练,单个GTX 580只有3GB显存,这限制了可训练的网络的最大规模。因此作者将AlexNet分布在两个GPU上,在每个GPU的显存中储存一半的神经元的参数。因为GPU之间通信方便,可以互相访问显存,而不需要通过主机内存,所以同时使用多块GPU也是非常高效的。同时,AlexNet的设计让GPU之间的通信只在网络的某些层进行,控制了通信的性能损耗。
(6)数据增强,随机地从256 x 256的原始图像中截取224 x 224大小的区域(以及水平翻转的镜像),相当于增加了倍的数据量。如果没有数据增强,仅靠原始的数据量,参数众多的CNN会陷入过拟合中,使用了数据增强后可以大大减轻过拟合,提升泛化能力。进行预测时,则是取图片的四个角加中间共5个位置,并进行左右翻转,一共获得10张图片,对他们进行预测并对10次结果求均值。同时,AlexNet论文中提到了会对图像的RGB数据进行PCA处理,并对主成分做一个标准差为0.1的高斯扰动,增加一些噪声,这个Trick可以让错误率再下降1%。
整个AlexNet有8个需要训练参数的层(不包括池化层和LRN层),前5层为卷积层,后3层为全连接层,如图4所示。AlexNet最后一层是有1000类输出的Softmax层用作分类。 LRN层出现在第1个及第2个卷积层后,而最大池化层出现在两个LRN层及最后一个卷积层后。ReLU激活函数则应用在这8层每一层的后面。因为AlexNet训练时使用了两块GPU,因此这个结构图中不少组件都被拆为了两部分。现在我们GPU的显存可以放下全部模型参数,因此只考虑一块GPU的情况即可。

图4
AlexNet每层的超参数如图5所示。其中输入的图片尺寸为224x224,第一个卷积层使用了较大的卷积核尺寸11x11,步长为4,有96个卷积核;紧接着一个LRN层;然后是一个3 x 3的最大池化层,步长为2。这之后的卷积核尺寸都比较小,都是5 x 5或者3 x 3的大小,并且步长都为1,即会扫描全图所有像素;而最大池化层依然保持为3 x 3,并且步长为2。我们可以发现一个比较有意思的现象,在前几个卷积层,虽然计算量很大,但参数量很小,都在1M左右甚至更小,只占AlexNet总参数量的很小一部分。这就是卷积层有用的地方,可以通过较小的参数量提取有效的特征。而如果前几层直接使用全连接层,那么参数量和计算量将成为天文数字。虽然每一个卷积层占整个网络的参数量的1%都不到,但是如果去掉任何一个卷积层,都会使网络的分类性能大幅地下降。

图5

12.4.6.3 VGGNet

VGGNet是牛津大学计算机视觉组(Visual Geometry Group)和Google DeepMind公司的研究员一起研发的的深度卷积神经网络。VGGNet探索了卷积神经网络的深度与其性能之间的关系,通过反复堆叠3x3的小型卷积核和2x2的最大池化层,VGGNet成功地构筑了16~19层深的卷积神经网络。VGGNet相比之前state-of-the-art的网络结构,错误率大幅下降,并取得了ILSVRC 2014比赛分类项目的第2名和定位项目的第1名。同时VGGNet的拓展性很强,迁移到其他图片数据上的泛化性非常好。VGGNet的结构非常简洁,整个网络都使用了同样大小的卷积核尺寸(3x3)和最大池化尺寸(2x2)。到目前为止,VGGNet依然经常被用来提取图像特征。VGGNet训练后的模型参数在其官方网站上开源了,可用来在domain specific的图像分类任务上进行再训练(相当于提供了非常好的初始化权重),因此被用在了很多地方。
VGGNet论文中全部使用了3x3的卷积核和2x2的池化核,通过不断加深网络结构来提升性能。图6所示为VGGNet各级别的网络结构图,图7所示为每一级别的参数量,从11层的网络一直到19层的网络都有详尽的性能测试。虽然从A到E每一级网络逐渐变深,但是网络的参数量并没有增长很多,这是因为参数量主要都消耗在最后3个全连接层。前面的卷积部分虽然很深,但是消耗的参数量不大,不过训练比较耗时的部分依然是卷积,因其计算量比较大。这其中的D、E也就是我们常说的VGGNet-16和VGGNet-19。C很有意思,相比B多了几个1x1的卷积层,1x1卷积的意义主要在于线性变换,而输入通道数和输出通道数不变,没有发生降维。


图6

图7
VGGNet拥有5段卷积,每一段内有2~3个卷积层,同时每段尾部会连接一个最大池化层用来缩小图片尺寸。每段内的卷积核数量一样,越靠后的段的卷积核数量越多:64 – 128 – 256 – 512 – 512。其中经常出现多个完全一样的3x3的卷积层堆叠在一起的情况,这其实是非常有用的设计。如图8所示,两个3x3的卷积层串联相当于1个5x5的卷积层,即一个像素会跟周围5x5的像素产生关联,可以说感受野大小为5x5。而3个3x3的卷积层串联的效果则相当于1个7x7的卷积层。除此之外,3个串联的3x3的卷积层,拥有比1个7x7的卷积层更少的参数量,只有后者的。最重要的是,3个3x3的卷积层拥有比1个7x7的卷积层更多的非线性变换(前者可以使用三次ReLU激活函数,而后者只有一次),使得CNN对特征的学习能力更强。

图8

VGGNet在训练时有一个小技巧,先训练级别A的简单网络,再复用A网络的权重来初始化后面的几个复杂模型,这样训练收敛的速度更快。在预测时,VGG采用Multi-Scale的方法,将图像scale到一个尺寸Q,并将图片输入卷积网络计算。然后在最后一个卷积层使用滑窗的方式进行分类预测,将不同窗口的分类结果平均,再将不同尺寸Q的结果平均得到最后结果,这样可提高图片数据的利用率并提升预测准确率。同时在训练中,VGGNet还使用了Multi-Scale的方法做数据增强,将原始图像缩放到不同尺寸S,然后再随机裁切224x224的图片,这样能增加很多数据量,对于防止模型过拟合有很不错的效果。实践中,作者令S在[256,512]这个区间内取值,使用Multi-Scale获得多个版本的数据,并将多个版本的数据合在一起进行训练。图9所示为VGGNet使用Multi-Scale训练时得到的结果,可以看到D和E都可以达到7.5%的错误率。最终提交到ILSVRC 2014的版本是仅使用Single-Scale的6个不同等级的网络与Multi-Scale的D网络的融合,达到了7.3%的错误率。不过比赛结束后作者发现只融合Multi-Scale的D和E可以达到更好的效果,错误率达到7.0%,再使用其他优化策略最终错误率可达到6.8%左右,非常接近同年的冠军Google Inceptin Net。同时,作者在对比各级网络时总结出了以下几个观点。
(1)LRN层作用不大。
(2)越深的网络效果越好。
(3)1x1的卷积也是很有效的,但是没有3x3的卷积好,大一些的卷积核可以学习更大的空间特征。

图9

12.4.6.4Google Inception Net

Google Inception Net首次出现在ILSVRC 2014的比赛中(和VGGNet同年),就以较大优势取得了第一名。那届比赛中的Inception Net通常被称为Inception V1,它最大的特点是控制了计算量和参数量的同时,获得了非常好的分类性能——top-5错误率6.67%,只有AlexNet的一半不到。Inception V1有22层深,比AlexNet的8层或者VGGNet的19层还要更深。但其计算量只有15亿次浮点运算,同时只有500万的参数量,仅为AlexNet参数量(6000万)的1/12,却可以达到远胜于AlexNet的准确率,可以说是非常优秀并且非常实用的模型。Inception V1降低参数量的目的有两点,第一,参数越多模型越庞大,需要供模型学习的数据量就越大,而目前高质量的数据非常昂贵;第二,参数越多,耗费的计算资源也会更大。Inception V1参数少但效果好的原因除了模型层数更深、表达能力更强外,还有两点:一是去除了最后的全连接层,用全局平均池化层(即将图片尺寸变为1x1)来取代它。全连接层几乎占据了AlexNet或VGGNet中90%的参数量,而且会引起过拟合,去除全连接层后模型训练更快并且减轻了过拟合。用全局平均池化层取代全连接层的做法借鉴了Network In Network(以下简称NIN)论文。二是Inception V1中精心设计的Inception Module提高了参数的利用效率,其结构如图10所示。这一部分也借鉴了NIN的思想,形象的解释就是Inception Module本身如同大网络中的一个小网络,其结构可以反复堆叠在一起形成大网络。不过Inception V1比NIN更进一步的是增加了分支网络,NIN则主要是级联的卷积层和MLPConv层。一般来说卷积层要提升表达能力,主要依靠增加输出通道数,但副作用是计算量增大和过拟合。每一个输出通道对应一个滤波器,同一个滤波器共享参数,只能提取一类特征,因此一个输出通道只能做一种特征处理。而NIN中的MLPConv则拥有更强大的能力,允许在输出通道之间组合信息,因此效果明显。可以说,MLPConv基本等效于普通卷积层后再连接1x1的卷积和ReLU激活函数。

我们再来看Inception Module的基本结构,其中有4个分支:第一个分支对输入进行1x1的卷积,这其实也是NIN中提出的一个重要结构。1x1的卷积是一个非常优秀的结构,它可以跨通道组织信息,提高网络的表达能力,同时可以对输出通道升维和降维。可以看到Inception Module的4个分支都用到了1x1卷积,来进行低成本(计算量比3x3小很多)的跨通道的特征变换。第二个分支先使用了1x1卷积,然后连接3x3卷积,相当于进行了两次特征变换。第三个分支类似,先是1x1的卷积,然后连接5x5卷积。最后一个分支则是3x3最大池化后直接使用1x1卷积。我们可以发现,有的分支只使用1x1卷积,有的分支使用了其他尺寸的卷积时也会再使用1x1卷积,这是因为1x1卷积的性价比很高,用很小的计算量就能增加一层特征变换和非线性化。Inception Module的4个分支在最后通过一个聚合操作合并(在输出通道数这个维度上聚合)。Inception Module中包含了3种不同尺寸的卷积和1个最大池化,增加了网络对不同尺度的适应性,这一部分和Multi-Scale的思想类似。早期计算机视觉的研究中,受灵长类神经视觉系统的启发,Serre使用不同尺寸的Gabor滤波器处理不同尺寸的图片,Inception V1借鉴了这种思想。Inception V1的论文中指出,Inception Module可以让网络的深度和宽度高效率地扩充,提升准确率且不致于过拟合。

图10
人脑神经元的连接是稀疏的,因此研究者认为大型神经网络的合理的连接方式应该也是稀疏的。稀疏结构是非常适合神经网络的一种结构,尤其是对非常大型、非常深的神经网络,可以减轻过拟合并降低计算量,例如卷积神经网络就是稀疏的连接。Inception Net的主要目标就是找到最优的稀疏结构单元(即Inception Module),论文中提到其稀疏结构基于Hebbian原理,这里简单解释一下Hebbian原理:神经反射活动的持续与重复会导致神经元连接稳定性的持久提升,当两个神经元细胞A和B距离很近,并且A参与了对B重复、持续的兴奋,那么某些代谢变化会导致A将作为能使B兴奋的细胞。总结一下即“一起发射的神经元会连在一起”(Cells that fire together, wire together),学习过程中的刺激会使神经元间的突触强度增加。受Hebbian原理启发,另一篇文章Provable Bounds for Learning Some Deep Representations提出,如果数据集的概率分布可以被一个很大很稀疏的神经网络所表达,那么构筑这个网络的最佳方法是逐层构筑网络:将上一层高度相关(correlated)的节点聚类,并将聚类出来的每一个小簇(cluster)连接到一起,如图11所示。这个相关性高的节点应该被连接在一起的结论,即是从神经网络的角度对Hebbian原理有效性的证明。

图11
因此一个“好”的稀疏结构,应该是符合Hebbian原理的,我们应该把相关性高的一簇神经元节点连接在一起。在普通的数据集中,这可能需要对神经元节点聚类,但是在图片数据中,天然的就是临近区域的数据相关性高,因此相邻的像素点被卷积操作连接在一起。而我们可能有多个卷积核,在同一空间位置但在不同通道的卷积核的输出结果相关性极高。因此,一个1x1的卷积就可以很自然地把这些相关性很高的、在同一个空间位置但是不同通道的特征连接在一起,这就是为什么1x1卷积这么频繁地被应用到Inception Net中的原因。1x1卷积所连接的节点的相关性是最高的,而稍微大一点尺寸的卷积,比如3x3、5x5的卷积所连接的节点相关性也很高,因此也可以适当地使用一些大尺寸的卷积,增加多样性(diversity)。最后Inception Module通过4个分支中不同尺寸的1x1、3x3、5x5等小型卷积将相关性很高的节点连接在一起,就完成了其设计初衷,构建出了很高效的符合Hebbian原理的稀疏结构。

在Inception Module中,通常1x1卷积的比例(输出通道数占比)最高,3x3卷积和5x5卷积稍低。而在整个网络中,会有多个堆叠的Inception Module,我们希望靠后的Inception Module可以捕捉更高阶的抽象特征,因此靠后的Inception Module的卷积的空间集中度应该逐渐降低,这样可以捕获更大面积的特征。因此,越靠后的Inception Module中,3x3和5x5这两个大面积的卷积核的占比(输出通道数)应该更多。
Inception Net有22层深,除了最后一层的输出,其中间节点的分类效果也很好。因此在Inception Net中,还使用到了辅助分类节点(auxiliary classifiers),即将中间某一层的输出用作分类,并按一个较小的权重(0.3)加到最终分类结果中。这样相当于做了模型融合,同时给网络增加了反向传播的梯度信号,也提供了额外的正则化,对于整个Inception Net的训练很有裨益。
当年的Inception V1还是跑在TensorFlow的前辈DistBelief上的,并且只运行在CPU上。当时使用了异步的SGD训练,学习速率每迭代8个epoch降低4%。同时,Inception V1也使用了Multi-Scale、Multi-Crop等数据增强方法,并在不同的采样数据上训练了7个模型进行融合,得到了最后的ILSVRC 2014的比赛成绩——top-5错误率6.67%。
同时,Google Inception Net还是一个大家族,包括:
— 2014年9月的论文Going Deeper with Convolutions提出的Inception V1(top-5错误率6.67%)。
— 2015年2月的论文Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate提出的Inception V2(top-5错误率4.8%)。
— 2015年12月的论文Rethinking the Inception Architecture for Computer Vision提出的Inception V3(top-5错误率3.5%)。
— 2016年2月的论文Inception-v4, Inception-ResNet and the Impact of Residual Connections on Learning提出的Inception V4(top-5错误率3.08%)。

12.4.6.5 ResNet

2015 年 12 月又出现了新的变革,这和 Inception V3 出现的时间一样。ResNet 有着简单的思路:供给两个连续卷积层的输出,并分流(bypassing)输入进入下一层(参见论文:Deep Residual Learning for Image Recognition)。

图12
这和之前的一些旧思路类似。但 ResNet 中,它们分流两个层并被应用于更大的规模。在 2 层后分流是一个关键直觉,因为分流一个层并未给出更多的改进。通过 2 层可能认为是一个小型分类器,或者一个 Network-In-Network。
这是第一次网络层数超过一百,甚至还能训练出 1000 层的网络。
有大量网络层的 ResNet 开始使用类似于 Inception 瓶颈层的网络层:

图13
这种层通过首先是由带有更小输出(通常是输入的 1/4)的 1×1 卷积较少特征的数量,然后使用一个 3×3 的层,再使用 1×1 的层处理更大量的特征。类似于 Inception 模块,这样做能保证计算量低,同时提供丰富的特征结合。
ResNet 在输入上使用相对简单的初始层:一个带有两个池的 7×7 卷基层。可以把这个与更复杂、更少直觉性的 Inception V3、V4 做下对比。
ResNet 也使用一个池化层加上 softmax 作为最后的分类器。
关于 ResNet 的其他洞见每天都有发生:
ResNet 可被认为既是平行模块又是连续模块,把输入输出(inout)视为在许多模块中并行,同时每个模块的输出又是连续连接的。
ResNet 也可被视为并行模块或连续模块的多种组合(参见论文:Residual Networks are Exponential Ensembles of Relatively Shallow Networks)。
已经发现 ResNet 通常在 20-30 层的网络块上以并行的方式运行。而不是连续流过整个网络长度。
当 ResNet 像 RNN 一样把输出反馈给输入时,该网络可被视为更好的生物上可信的皮质模型(参见论文:Bridging the Gaps Between Residual Learning, Recurrent Neural Networks and Visual Cortex)。

12.4.6.6 CNN发展趋势

以上,我们简单回顾了卷积神经网络的历史,下图所示大致勾勒出最近几十年卷积神经网络的发展方向。
Perceptron(感知机)于1957年由Frank Resenblatt提出,而Perceptron不仅是卷积网络,也是神经网络的始祖。Neocognitron(神经认知机)是一种多层级的神经网络,由日本科学家Kunihiko Fukushima于20世纪80年代提出,具有一定程度的视觉认知的功能,并直接启发了后来的卷积神经网络。LeNet-5由CNN之父Yann LeCun于1997年提出,首次提出了多层级联的卷积结构,可对手写数字进行有效识别。

图14

可以看到前面这三次关于卷积神经网络的技术突破,间隔时间非常长,需要十余年甚至更久才出现一次理论创新。而后于2012年,Hinton的学生Alex依靠8层深的卷积神经网络一举获得了ILSVRC 2012比赛的冠军,瞬间点燃了卷积神经网络研究的热潮。AlexNet成功应用了ReLU激活函数、Dropout、最大覆盖池化、LRN层、GPU加速等新技术,并启发了后续更多的技术创新,卷积神经网络的研究从此进入快车道。
在AlexNet之后,我们可以将卷积神经网络的发展分为两类,一类是网络结构上的改进调整(图14左侧分支),另一类是网络深度的增加(图14右侧分支)。
2013年,颜水成教授的Network in Network工作首次发表,优化了卷积神经网络的结构,并推广了1x1的卷积结构。在改进卷积网络结构的工作中,后继者还有2014年的Google Inception Net V1,提出了Inception Module这个可以反复堆叠的高效的卷积网络结构,并获得了当年ILSVRC比赛的冠军。2015年初的Inception V2提出了Batch Normalization,大大加速了训练过程,并提升了网络性能。2015年年末的Inception V3则继续优化了网络结构,提出了Factorization in Small Convolutions的思想,分解大尺寸卷积为多个小卷积乃至一维卷积。
而另一条分支上,许多研究工作则致力于加深网络层数,2014年,ILSVRC比赛的亚军VGGNet全程使用3x3的卷积,成功训练了深达19层的网络,当年的季军MSRA-Net也使用了非常深的网络。2015年,微软的ResNet成功训练了152层深的网络,一举拿下了当年ILSVRC比赛的冠军,top-5错误率降低至3.46%。其后又更新了ResNet V2,增加了Batch Normalization,并去除了激活层而使用Identity Mapping或Preactivation,进一步提升了网络性能。此后,Inception ResNet V2融合了Inception Net优良的网络结构,和ResNet训练极深网络的残差学习模块,集两个方向之长,取得了更好的分类效果。
我们可以看到,自AlexNet于2012年提出后,深度学习领域的研究发展极其迅速,基本上每年甚至每几个月都会出现新一代的技术。新的技术往往伴随着新的网络结构,更深的网络的训练方法等,并在图像识别等领域不断创造新的准确率记录。至今,ILSVRC比赛和卷积神经网络的研究依然处于高速发展期,CNN的技术日新月异。当然其中不可忽视的推动力是,我们拥有了更快的GPU计算资源用以实验,以及非常方便的开源工具(比如TensorFlow)可以让研究人员快速地进行探索和尝试。在以前,研究人员如果没有像Alex那样高超的编程实力能自己实现cuda-convnet,可能都没办法设计CNN或者快速地进行实验。现在有了TensorFlow,研究人员和开发人员都可以简单而快速地设计神经网络结构并进行研究、测试、部署乃至实用。

参考:http://blog.csdn.net/app_12062011/article/details/62886113

12.5 卷积网络TensorFlow实例

在梯度下降和最优化部分我们用传统的神经网络在MNIST数据集上得到了90%左右的测试准确率。这个结果其实并不太理想。
在本章中,我们将使用卷积神经网络来得到一个准确率更高的模型,接近99%。卷积神经网络使用共享的卷积核对图像进行卷积操作,以提取图像深层特征。这些深层特征然后组合成特征向量输入全连接的神经网络中,再使用类似传统神经网络的方法进行分类。

12.5.1 网络架构图

输入为原始的28x28的图像,它首先进入第一个拥有16个5x5卷积核的卷积层,得到16张28x28的卷积后的图像,再进入降采样层(图中省略)最终得到16张14x14的图像(可称为16个通道)。为了保证卷积前后图像的像素不变,在卷积过后,对图像边框采取补零的操作(在TensorFlow中的conv2d的padding参数为’SAME’,如果不采取补零而是缩小像素值,padding参数值设置为’VALID’)。降采样层使用max pooling操作,将2x2的像素块取最大值合并为一个像素点,这个操作会将图像缩小1倍。
对于得到的16通道的14x14图像,进入第二个拥有36个卷积核的卷积层,得到36张14x14的卷积后图像,再进入降采样层得到36张7x7的图像。在这里包括了一些隐含的操作,对于16张原始图像,每一张图像使用36个卷积核卷积,应该得到16x36张新的图像,但是为了减少模型的参数量,降低复杂度,卷积层对每个卷积核得到的16张图像相加,最后得到36张卷积后图像。
经过两层卷积后,将36张7x7的图像展平,得到一个7x7x36的向量,输入到一个128维的全连接层,最后输入到10维的softmx层进行分类。
这里层数较多,但关键是卷积层,下节我们重点介绍一下卷积层的运算逻辑。

12.5.2卷积层

卷积层使用多个卷积核作用于同一幅图像,以得到多个卷积后的图像。如下图所示:

对于原始的图像7,使用一个5x5的卷积核,从左到右从上到下滑动。滑动的过程称为stride,卷积核分别从上到下,从左到右,步长一般设定为1或2。对卷积核覆盖的区域于卷积核进行点乘操作得到一个值作为该区域的中心点的像素。在上图中,红色代表这部分的像素对原始图像存在一个正的影响,而蓝色表示负的影响,在这个样例中卷积核似乎在识别图像中的横线部分,因为从结果看来7的那一横具有更强烈的反应。
此外,对于每一个卷积层的输出,一般会经过一个relu层或激活层,以保证全部的像素值都为正(因为所有为负的像素值都被设定为0),同时增强模型的泛化能力。

12.5.3导入需要的包

12.5.4 定义网络参数

12.5.5 导入数据

TensorFlow在样例教程中已经做了下载并导入MNIST数字手写体识别数据集的实现,可以直接使用。但运行时经常发生断网或无法下载的情况,所以这里我们采用先从http://yann.lecun.com/exdb/mnist/下载,然后把下载的4个文件放在本地当前运行目录的data/mnist目录下,具体实现请看如下代码。其中load_mnist函数请参考梯度下降及优化部分。

12.5.6 把标签转换为one-hot格式

从以上结果我们可以看出,目前标签值是0-9之间的数字,在机器学习或深度学习中,为提高分类的性能,一般会把类别转换one-hot的格式,这种格式把每个数字或类别转换为一个长度为类别总数的向量,一行只有一个1其余都是0。这里类别总数为10(共有10个不同的数字),7这个数字one-hot后就变成:[0. 0. 0. 0. 0. 0. 0. 1. 0. 0. ],具体实现如下:

在one-hot编码中,只有对应类别的那个位置为1,其余都为0,我们可以使用以下代码将其转换为真实类别:

12.5.7 数据维度

在MNIST数据集中,原始的28*28像素的黑白图片被展平为784维的向量。

为使得网络结构更加清晰,在这里对这些固定维度做如下定义:

12.5.8打印部分样例图片

12.5.9 CNN的Tensorflow实现

TensorFlow使用计算图模型来构建神经网络。其主要流程是先建立好整个网络的计算图模型,然后再导入数据进行计算。
一个TensorFlow计算图包含以下几个部分:
Placeholder: 占位符,用来读取用户输入与输出;
Variable: 模型的变量,也称为参数,在计算过程中逐步优化;
Model: 使用的神经网络模型,也可以使用一些简单的计算;
Cost Function: 代价函数,也称损失函数,如何计算模型的误差;
Optimizer: 优化器,使用哪种优化策略来降低损失。

12.5.9.1 创建变量

卷积神经网络中有两类变量,权重和偏置项。以下为初始化这两种变量的函数,其中对权重参数采用随机生成其符合正态分布的随机值,对偏置项初始化为常量0.05。

12.5.9.2 创建卷积

这个函数创建了一个卷积层。输入为4维的tensor,维度如下:
图像数量
图像高度
图像宽度
通道数
输出同样是一个4维的tensor,维度如下:
图像数量,与输入相同
图像高度,如果使用2x2 pooling,高宽都除以2
图像宽度,同上
由卷积层生成的通道数

12.5.9.3 展平操作

一个卷积层的输出为4维度的tensor。我们需要在卷积层后添加一个全连接层,首先得将4为的tensor展平为2维的tensor,这样才能直接输入到全连接层。

12.5.9.4 创建全连接层

12.5.9.5 定义占位符

占位符(Placeholder)为输入与输出占据位置,这些输入输出一般在不同的轮次都会有所变化。由于TensorFlow先构图再计算,所以需要使用占位符为输入和输出预留位置。

12.5.9.6 定义卷积层1

输入为(?, 28, 28, 1)的图像,其中?为图像数量。可以看到,第一个卷积层的输入为(?, 14, 14, 16)的tensor,即14x14像素的16通道图像。

12.5.9.7 定义卷积层2

12.5.9.8 定义展平层

展平层将第二个卷积层展平为二维tensor。

12.5.9.9 定义全连接层1

输出为(?, 10)的二维tensor,意在判定输入图像属于哪一类, 注意该层未使用relu,因为将要输入到后续的softmax中。

12.5.9.10 预测类别

第二个全连接层估计输入的图像属于某一类别的程度,这个估计有些粗糙,需要添加一个softmax层归一化为概率表示。

12.5.9.12 选择优化方法

使用自适应的梯度下降优化法,Adam。

12.5.9.13 模型性能度量

12.5.10.2执行优化的帮助函数

定义批处理函数,该函数在优化算法中被调用。

定义优化算法,调用批处理函数、Adam优化器等,并增加部分状态输出的代码。

12.5.10.3执行优化的帮助函数

12.5.10.4显示性能的帮助函数

用来输出测试准确率的的函数。计算所有图像的分类需要一定的时间,因此我们在上面定义的一些函数中重用了分类结果。这个函数会占据大量的内存,所以将测试集分成了多个小的批次。如果你的机器内存太小,你可以尝试减小batch_size。

可以看到,测试的准确率极低,但是函数的功能正常。

12.5.10.5执行一轮优化后的性能

用时: 0:00:14
测试集准确率: 80.1% (8010 / 10000)

可以看到,执行100轮迭代后,性能存在大幅度提升。

12.5.10.7 1000轮优化后的性能

迭代轮次: 101, 训练准确率: 76.6%
迭代轮次: 201, 训练准确率: 76.6%
迭代轮次: 301, 训练准确率: 93.8%
迭代轮次: 401, 训练准确率: 87.5%
迭代轮次: 501, 训练准确率: 90.6%
迭代轮次: 601, 训练准确率: 96.9%
迭代轮次: 701, 训练准确率: 93.8%
迭代轮次: 801, 训练准确率: 98.4%
迭代轮次: 901, 训练准确率: 96.9%
用时: 0:02:07
测试集准确率: 95.6% (9563 / 10000)
Example errors:

可以发现,测试集的准确率为95.6%,已经比传统的90.9%要高。输出的部分错误样例显示,部分形状相似的数字仍然难以区分。

12.5.10.8 10000轮优化后的性能

迭代轮次: 8701, 训练准确率: 100.0%
迭代轮次: 8801, 训练准确率: 100.0%
迭代轮次: 8901, 训练准确率: 100.0%
迭代轮次: 9001, 训练准确率: 100.0%
迭代轮次: 9101, 训练准确率: 98.4%
迭代轮次: 9201, 训练准确率: 100.0%
迭代轮次: 9301, 训练准确率: 95.3%
迭代轮次: 9401, 训练准确率: 100.0%
迭代轮次: 9501, 训练准确率: 100.0%
迭代轮次: 9601, 训练准确率: 100.0%
迭代轮次: 9701, 训练准确率: 100.0%
迭代轮次: 9801, 训练准确率: 100.0%
迭代轮次: 9901, 训练准确率: 100.0%
用时: 0:21:04
测试集准确率: 98.3% (9833 / 10000)
Example errors:

Confusion Matrix:
[[ 977 0 2 0 0 0 0 1 0 0]
[ 0 1129 5 0 0 0 0 1 0 0]
[ 0 1 1028 0 1 0 0 2 0 0]
[ 0 0 4 996 0 9 0 0 0 1]
[ 0 1 2 0 973 0 0 0 0 6]
[ 1 0 0 4 0 886 1 0 0 0]
[ 6 2 2 1 3 8 935 0 1 0]
[ 1 2 14 2 0 0 0 1002 2 5]
[ 12 0 17 5 5 9 1 2 914 9]
[ 2 0 2 2 3 6 0 1 0 993]]

经过10000轮迭代后,测试集的准确率达到了98.3%的准确率。在分错的样本中,部分用肉眼也难以分辨。而混淆矩阵表明绝大部分的样本都分类正确。这是一个非常好的模型。

12.5.11权重和层的可视化

为了更好的理解卷积神经网络为何能识别手写体数字,我来来可视化部分权重和层输出。

12.5.11.1卷积权重可视化

12.5.11.2卷积层输出可视化

12.5.11.3打印输入图像

12.5.11.4显示卷积层1的权重


以上就是16个卷积核在第一个通道的权重情况。其中红色为正的权重,蓝色为负的权重。在这里我们很难判别这些权重是如何起作用的。
将image1输入卷积层1,得到使用不同卷积后得到的图像,这些图像的棱角更加分明,而且在不同的边的突出情况也不同:


将image2输入卷积层1,得到如下图像,在不同部位的突出情况不同

12.5.11.5显示卷积层2的权重

现在输出第二个卷积层的权重。

由于卷积层1有16个输出通道,这意味着卷积层2有16个输入通道,每个通道的输入又对应36个输出通道,因此总共有16x36个通道的卷积核。我们先输出第一个通道的卷积核


这些权重相对与卷积层1的权重更加抽象,无法用语言来解释。接下来输出第二个通道的卷积核。


可以说明,不同输入通道对应的卷积核是不同的。将image1在卷积层1的输出再次输入卷积层2,得到如下输出:
[cceN_python theme="blackboard"]
plot_conv_layer(layer=layer_conv1, image=image2)


所输出的图像达到了一个更高的层次,卷积核试图提取一些边缘化的特征,这些特征对于同类图像的变化并不敏感。

在运行完整个计算图后,需要将它关闭,否则将一直占用资源:

参考:https://gaussic.github.io/2017/08/14/tensorflow-cnn/

5.1 梯度下降与最优化

梯度下降及最优化是机器学习、深度学习中关键技术之一,也是核心内容之一。梯度下降法是基本、经典方法,现在很多深度学习仍然使用,当然不是简单使用,而是做了很多优化。
本节就是从基础梯度下降方法定义开始,由浅入深、由简单到复杂,从训练样本数、学习率、梯度等多方面对算法进行优化。为便于大家更好理解,为此我们提供了很多实例代码、主要公式推导、图形等。

5.1.1梯度下降法简介

梯度下降法是一种致力于找到函数极值点的算法,在机器学习中,我们一般通过这种方法获取模型参数,从而求得目标函数或损失函数的极值。
问题描述:


随着迭代步数的增加,逐渐逼近极值点,可参考下图:

5.1.2训练集数据大小对优化的影响

根据每一次迭代所使用的数据集范围的不同,可以把梯度下降算法区分为批量梯度下降(Batch Gradient Descent, BGD)、随机梯度下降(Stochastic Gradient Descent, SGD)和小批量梯度下降(Mini-Batch Gradient Descent, MBGD)。
1)、BGD 是梯度下降算法最原始的形式, 其特点是每次更新参数 时, 都使用整个训练集的数据。当数据集比较大时,速度将非常慢,同时,它不能以在线的方式更新模型,当有新元素加入时,需要对全量数据进行更新,效率较低,因此,当前的梯度下降法一般不采用这种方法。
2)、SGD是对BGD的改进,SGD每次更新,只考虑一个样本数据,因此,它的速度比较快,一般要远远BGD,尤其是它能在线更新参数。不过由于单个样本会出现相似或重复的情况,因此,数据更新会出现冗余;另外,因单个样本之间的数据差异会比较大,因此,训练时可能造成每次迭代的损失函数会出现较大的波动。
示例代码:

for i in range(epochs):
np.random.shuffle(data)
for example in data:
params_grad = evaluate_gradient(loss_function,example,params)
params = params - learning_rate * params_grad

随机梯度下降最大的缺点在于每次更新可能并不会按照正确的方向进行,因此可以带来优化波动(扰动),如下图

3)、MBGD每次参数更新时,由m个样本数据构成,这种优化方法结合了BGD与SGD两者的优点,同时克服了两者的不足。m的取值一般较小,一般在[10,500]之间。
示例代码:

for i in range(epochs):
np.random.shuffle(data)
for batch in get_batches(data, batch_size=50):
params_grad = evaluate_gradient(loss_function,batch,params)
params = params - learning_rate * params_grad

5.1.3步长对优化的影响

在利用梯度下降法求目标函数极值时,学习速率(即探索步长)这个参数非常重要,太小可能导致迭代慢,太大了有可能跳过极值点。如何调整搜索的步长、如何加快收敛速度以及如何防止搜索时发生震荡却是一门值得深究的学问。接下来本文将分析第一个问题:学习速率的大小对搜索过程的影响。以下通过一个实例来具体说明,为简便起见,这里目标函数为一个一元二次函数:

1)、定义函数:

import numpy as np
import matplotlib.flot as plt

# 目标函数:y=x^2
def func(x):
return np.square(x)

# 目标函数一阶导数:dy/dx=2*x
def dfunc(x):
return 2 * x

2)、编写梯度下降法函数

def GD(x_start, df, epochs, lr):
"""
梯度下降法。给定起始点与目标函数的一阶导函数,求在epochs次迭代中x的更新值
:param x_start: x的起始点
:param df: 目标函数的一阶导函数
:param epochs: 迭代周期
:param lr: 学习速率
:return: x在每次迭代后的位置(包括起始点),长度为epochs+1
"""

xs = np.zeros(epochs+1)
x = x_start
xs[0] = x
for i in range(epochs):
dx = df(x)
# v表示x要改变的幅度
v = - dx * lr
x += v
xs[i+1] = x
return xs

3)、根据以上梯度的定义,假设起始搜索点(即:x_0)为-5,迭代周期(或迭代次数)为5,学习速率为0.3,测试代码如下:

def demo_GD():
x_start = -5
epochs = 5
lr = 0.3
x = GD(x_start, dfunc, epochs, lr=lr)
print x

运行demo0_GD()的结果如下:
[-5. -2. -0.8 -0.32 -0.128 -0.0512]

4)、继续修改一下demo_GD()函数,可视化梯度下降法的搜索的整个过程。

ef demo0_GD():
"""演示如何使用梯度下降法GD()"""
line_x = np.linspace(-5, 5, 100)
line_y = func(line_x)

x_start = -5
epochs = 5

lr = 0.3
x = GD(x_start, dfunc, epochs, lr=lr)

color = 'r'
plt.plot(line_x, line_y, c='b')
plt.plot(x, func(x), c=color, label='lr={}'.format(lr))
plt.scatter(x, func(x), c=color, )
plt.legend()
plt.show()

运行函数demo0_GD(),得到下图:

5)、看了lr=0.3时,效果不错,假设选用其它学习速率,情况如何呢?以下我们分别设置学习速率为0.1、0.3与0.9,比较一下各个学习速率的迭代过程,并把整个迭代过程进行可视化。

def demo1_GD_lr():
# 函数图像
line_x = np.linspace(-5, 5, 100)
line_y = func(line_x)
plt.figure('Gradient Desent: Learning Rate')

x_start = -5
epochs = 5

lr = [0.1, 0.3, 0.9]

color = ['r', 'g', 'y']
size = np.ones(epochs+1) * 10
size[-1] = 70
for i in range(len(lr)):
x = GD(x_start, dfunc, epochs, lr=lr[i])
plt.subplot(1, 3, i+1)
plt.plot(line_x, line_y, c='b')
plt.plot(x, func(x), c=color[i], label='lr={}'.format(lr[i]))
plt.scatter(x, func(x), c=color[i])
plt.legend()
plt.show()

运行函数demo1_GD_lr()的结果如下:

由此可以看出,学习速率大小对梯度下降法的搜索过程起着非常大的影响,太小,搜索速度比较慢;太大又可能跳过极值点。要选中一个恰当的学习速率往往要花费不少时间。是否有方法能避免或解决这个问题呢?使我们不必花费太多时间在选择学习速率这个参数上,又能获取同样、甚至更好的效果呢?
有的,我们可以通过衰减因子、引入动量、自动调整学习速率的方法(又称为自适应梯度策略)等方法就可。
(3)衰减因子
当学习速率较大时,容易在搜索过程中发生震荡,而发生震荡的根本原因无非就是搜索的步长迈的太大了。
如果能够让 lr 随着迭代周期不断衰减变小,那么搜索时迈的步长就能不断减少以减缓震荡。学习速率衰减因子由此诞生。如何使lr 随着迭代周期不断衰减变小?我们只要把循环次数加入参数中即可:
lr_i = lr_start * 1.0 / (1.0 + decay * i)
上面的公式即为学习速率衰减公式,其中 lr_i 为第 i 次迭代时的学习速率, lr_start 为原始学习速率, decay 为 一个介于[0.0, 1.0]的小数。
decay 越小,学习速率衰减地越慢,当 decay = 0 时,学习速率保持不变。
decay 越大,学习速率衰减地越快,当 decay = 1 时,学习速率衰减最快。
以下我们通过一个实例来说明,如果通过衰减因子避免因学习速率过大,导致搜索震荡问题。
还是以函数为例,这次引入衰减因子decay 来改变学习速率。
(1)、先定义函数及导数,确定衰减因子与学习速率lr的关系

lr = [0.1, 0.3, 0.9, 0.99]
decay = [0.0, 0.01, 0.5, 0.9]

import numpy as np
import matplotlib.pyplot as plt

# 目标函数:y=x^2
def func(x):
return np.square(x)

# 目标函数一阶导数:dy/dx=2*x
def dfunc(x):
return 2 * x

def GD_decay(x_start, df, epochs, lr, decay):
xs = np.zeros(epochs+1)
x = x_start
xs[0] = x
v = 0
for i in range(epochs):
dx = df(x)
# 学习速率衰减
lr_i = lr * 1.0 / (1.0 + decay * i)
# v表示x要改变的幅度
v = - dx * lr_i
x += v
xs[i+1] = x
return xs

(2)、利用新的学习速率来进行优化

def demo3_GD_decay():
line_x = np.linspace(-5, 5, 100)
line_y = func(line_x)
plt.figure('Gradient Desent: Decay')

x_start = -5
epochs = 10

lr = [0.1, 0.3, 0.9, 0.99]
decay = [0.0, 0.01, 0.5, 0.9]

color = ['k', 'r', 'g', 'y']

plt.figure(figsize=(14,10))
row = len(lr)
col = len(decay)
size = np.ones(epochs + 1) * 10
size[-1] = 70
for i in range(row):
for j in range(col):
x = GD_decay(x_start, dfunc, epochs, lr=lr[i], decay=decay[j])
plt.subplot(row, col, i * col + j + 1)
plt.plot(line_x, line_y, c='b')
plt.plot(x, func(x), c=color[i], label='lr={}, de={}'.format(lr[i], decay[j]))
plt.scatter(x, func(x), c=color[i], s=size)
plt.legend(loc=0)
plt.show()

demo3_GD_decay()

运行结果如下:

在所有行中均可以看出,decay越大,学习速率衰减地越快。在第三行与第四行可看到,decay确实能够对震荡起到减缓的作用。
(3)、比较多种衰减因子,对学习速率的影响。
假设起始学习速率为1.0,decay为[0.0, 0.001, 0.1, 0.5, 0.9, 0.99],迭代周期为300

ef demo4_how_to_chose_decay():
lr = 1.0
iterations = np.arange(300)

decay = [0.0, 0.001, 0.1, 0.5, 0.9, 0.99]
for i in range(len(decay)):
decay_lr = lr * (1.0 / (1.0 + decay[i] * iterations))
plt.plot(iterations, decay_lr, label='decay={}'.format(decay[i]))

plt.ylim([0, 1.1])
plt.legend(loc='best')
plt.show()

demo4_how_to_chose_decay()

运行结果如下:

可以看到,当decay为0.1时,50次迭代后学习速率已从1.0急剧降低到了0.2。如果decay设置得太大,则可能会收敛到一个不是极值的位置。
由上可知,小批量梯度下降法选择合适的learning rate比较困难,也很难保证良好地收敛,此外,对神经网络最优化非凸的罚函数时,另一个通常面临的挑战,是如何避免目标函数被困在局部最小值中。Dauphin 及其他人认为,这个困难并不来自于局部最小值,而是来自于「鞍点」,也就是在一个方向上斜率是正的、在一个方向上斜率是负的点。这些鞍点通常由一些函数值相同的面环绕,它们在各个方向的梯度值都为 0,所以 SGD 很难从这些鞍点中脱开。是否有更好的方法能解决这些问题或挑战?下面我们介绍引入动量(momentum)的优化方法。

5.1.4 动量对梯度下降法的影响

通过前面学习速率的实例,我们知道学习速率较小时,收敛到极值的速度较慢。
学习速率较大时,容易在搜索过程中发生震荡,如下图。

在这种情况下,SGD 在陡谷的周围震荡,向局部极值处缓慢地前进。优化方法除与学习率有关外,还与梯度方法有关,这里我们可以通过引入一个动量方法,来避免这个比较费时问题。动量(momentum)是模拟物理里动量的概念,积累之前的动量来替代真正的梯度。动量在物理学上定义为质量乘以速度,这里我们不妨假设单位质量,因此速度向量就可以看成为动量。一个物体在运动时具有惯性,把这个思想运用到梯度下降计算中,增加算法的收敛速度和稳定性,如下图:

从这个图形可以看出:
当本次梯度下降- lr*dx 的方向与上次更新量的方向相同时,上次的更新量能够对本次的搜索起到一个正向加速的作用。
当本次梯度下降- lr*dx的方向与上次更新量的方向相反时,上次的更新量能够对本次的搜索起到一个减速的作用。
使用动量的梯度下降法的Python代码如下:

import numpy as np
import matplotlib.pyplot as plt

# 目标函数:y=x^2
def func(x):
return np.square(x)

# 目标函数一阶导数:dy/dx=2*x
def dfunc(x):
return 2 * x

def GD_momentum(x_start, df, epochs, lr, mu):
"""
带有冲量的梯度下降法。
:param x_start: x的起始点
:param df: 目标函数的一阶导函数
:param epochs: 迭代周期
:param lr: 学习速率
:param mu: 动量
:return: x在每次迭代后的位置(包括起始点),长度为epochs+1
"""

xs = np.zeros(epochs+1)
x = x_start
xs[0] = x
v = 0
for i in range(epochs):
dx = df(x)
# v表示x要改变的幅度
v = - dx * lr + mu * v
x += v
xs[i+1] = x
return xs

为了查看mu大小对不同学习速率的影响,此处设置学习速率为lr = [0.01, 0.1, 0.6, 0.9],冲量依次为mu = [0.0, 0.1, 0.5, 0.9],起始位置为x_start = -5,迭代周期为6。测试以及绘图代码如下:

def demo2_GD_momentum():
line_x = np.linspace(-5, 5, 100)
line_y = func(line_x)
plt.figure('Gradient Desent: Learning Rate, Momentum')

x_start = -5
epochs = 6

lr = [0.01, 0.1, 0.6, 0.9]
mu = [0.0, 0.1, 0.5, 0.9]

color = ['k', 'r', 'g', 'y']

row = len(lr)
col = len(mu)
size = np.ones(epochs+1) * 10
size[-1] = 70
for i in range(row):
for j in range(col):
x = GD_momentum(x_start, dfunc, epochs, lr=lr[i], mu=mu[j])
plt.subplot(row, col, i * col + j + 1)
plt.plot(line_x, line_y, c='b')
plt.plot(x, func(x), c=color[i], label='lr={}, mu={}'.format(lr[i], mu[j]))
plt.scatter(x, func(x), c=color[i], s=size)
plt.legend(loc=0)
plt.show()

运行结果如下:

从上图不难以下几点:
(1)从第一行可看出:在学习率较小的时候,适当的momentum能够起到一个加速收敛速度的作用。
(2)从第四行可看出:在学习率较大的时候,适当的momentum能够起到一个减小收敛时震荡幅度的作用。
从上述两点来看,momentum确实能够解决收敛慢或震荡的两个问题。

然而在第二行与第三行的最后一列图片中也发现了一个问题,当momentum较大时,原本能够正确收敛的时候却因为刹不住车跑过头了。那么怎么继续解决这个新出现的问题呢?下面我们介绍一种改进动量方法。

5.1.5 改进的动量更新策略

 

NAG算法相对于Momentum多了一个本次梯度相对上次梯度的变化量,这个变化量本质上是对目标函数二阶导的近似。由于利用了二阶导的信息,NAG算法才会比Momentum具有更快的收敛速度.
NAG的python代码具体实现:

import numpy as np
import matplotlib.pyplot as plt
def f(x):
return x[0] * x[0] + 50 * x[1] * x[1]
def g(x):
return np.array([2 * x[0], 100 * x[1]])
xi = np.linspace(-200,200,1000)
yi = np.linspace(-100,100,1000)
X,Y = np.meshgrid(xi, yi)
Z = X * X + 50 * Y * Y

%matplotlib inline
def contour(X,Y,Z, arr = None):
plt.figure(figsize=(15,7))
xx = X.flatten()
yy = Y.flatten()
zz = Z.flatten()
plt.contour(X, Y, Z, colors='black')
plt.plot(0,0,marker='*')
if arr is not None:
arr = np.array(arr)
for i in range(len(arr) - 1):
plt.plot(arr[i:i+2,0],arr[i:i+2,1])

contour(X,Y,Z)

def nesterov(x_start, step, g, discount = 0.7):
x = np.array(x_start, dtype='float64')
passing_dot = [x.copy()]
pre_grad = np.zeros_like(x)
for i in range(50):
x_future = x - step * discount * pre_grad
grad = g(x_future)
pre_grad = pre_grad * discount + grad
x -= pre_grad * step
passing_dot.append(x.copy())
#print '[ Epoch {0} ] grad = {1}, x = {2}'.format(i, grad, x)
if abs(sum(grad)) < 1e-6:
break;
return x, passing_dot

start_point = [150,75]
step = 0.012
discount = 0.9
res2, x_arr2 = nesterov(start_point, step, g, discount)
contour(X,Y,Z, x_arr2)


参考了:
http://www.jianshu.com/p/58b3fe300ecb
https://github.com/WarBean/zhihuzhuanlan/blob/master/Momentum_Nesterov.ipynb

5.1.6 自适应梯度策略

通过以上实例的分析我们了解到,虽然梯度下降算法效果很好,并且广泛使用,但同时其也存在一些挑战与问题需要解决:
1)选择一个合理的学习速率很难。
如果学习速率过小,则会导致收敛速度很慢。如果学习速率过大,那么其会阻碍收敛,即在极值点附近会振荡。
2)学习速率调整也不易
学习速率调整(又称学习速率调度),试图在每次更新过程中,改变学习速率,一般使用某种事先设定的策略或者在每次迭代中衰减一个较小的阈值。无论哪种调整方法,都需要事先进行固定设置,这边便无法自适应每次学习的数据集特点。
3)固定学习速率易导致算法卡在鞍点。
前面的策略都是针对迭代方向进行优化,学习速率为固定值,即所有参数共享相同的学习速率,学习速率在每一个方向上的大小固定,很容易造成算法被卡在鞍点的位置。

图 5.3 鞍点
这是实现上图的python代码

from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
plot_args = {'rstride': 1, 'cstride': 1, 'cmap':"Blues_r",
'linewidth': 0.4, 'antialiased': True,
'vmin': -1, 'vmax': 1}

x, y = np.mgrid[-1:1:31j, -1:1:31j]
z = x**2 - y**2

ax.plot_surface(x, y, z, **plot_args)
ax.plot([0], [0], [0], 'ro')
ax.view_init(azim=-60, elev=30)
ax.set_xlim(-1, 1)
ax.set_ylim(-1, 1)
ax.set_zlim(-1, 1)
plt.xticks([-1, -0.5, 0, 0.5, 1],
[r"$-1$", r"$-1/2$", r"$0$", r"$1/2$", r"$1$"])
plt.yticks([-1, -0.5, 0, 0.5, 1],
[r"$-1$", r"$-1/2$", r"$0$", r"$1/2$", r"$1$"])
ax.set_zticks([-1, -0.5, 0, 0.5, 1])
ax.set_zticklabels([r"$-1$", r"$-1/2$", r"$0$", r"$1/2$", r"$1$"])
ax.w_xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
ax.w_yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
ax.w_zaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
plt.savefig("Saddle_point.svg", bbox_inches="tight", transparent=True)
plt.show()

4)对于非凸目标函数,容易陷入那些次优的局部极值点中。
如何解决这些问题?这里我们介绍几种自适应方法来解决此类问题。

5.1.6.1 AdaGrad自适应梯度策略

如图5.3 所示,假设鞍点为a,由于学习速率不变,容易在鞍点处来回震荡,无法找到最优点。下面我们介绍学习速率随迭代数的变化而变化的自适应梯度策略,简称为AdaGrad。AdaGrad是一种自适应的调整学习速率的优化方法。
假设每个模型参数使用相同的学习速率η,而Adagrad在每一个更新步骤中对于每一个模型参数使用不同的学习速率lr,设第t次更新步骤中,目标函数的参数梯度为
AdaGrad算法主要步骤如下:
lr为全局学习速率参数
初始参数为θ
小参数δ
初始化梯度累积变量 γ=0

由上面算法的伪代码可知,
1)、随着迭代时间越长,累积梯度r越大,从而学习速率随着时间就减小,在接近目标值时,不会因为学习速率过大而越过极值点;
2)、不同参数之间学习速率是不同,因此,与前面固定学习速率相比,不容易在鞍点卡住;
3)、如果梯度累积参数r比较小,则学习速率会比较大,所以参数迭代的步长就会比较大;相反,如果梯度累积参数比较大,则学习速率会比较小,所以迭代的步长会比较小。

5.1.6.2RMSprop自适应梯度策略

经验上,RMSProp已被证明是一种有效而且实用的深度学习网络优化算法,这也是深度学习中经常采用的方法之一。

5.1.6.3 Adam自适应梯度策略

Adam(Adaptive Moment Estimation)本质上是带有动量项的RMSprop,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。Adam的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。
Adam是另一种学习速率自适应的深度神经网络方法,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习速率。Adam的优点主要在于经过偏置校正后,每一次迭代学习速率都有个确定范围,使得参数比较平稳。公式如下:


特点:
结合了Adagrad善于处理稀疏梯度和RMSprop善于处理非平稳目标的优点
对内存需求较小
为不同的参数计算不同的自适应学习速率
也适用于大多非凸优化 - 适用于大数据集和高维空间

5.1.7 各种算法可视化

如下的两个动画(图像版权:Alec Radford)给了我们关于各种优化算法在优化过程中行为的直观说明。

contours_evaluation_optimizers
图5.4
在图 5.4 中,我们可以看到,在罚函数的等高线图中,优化器的位置随时间的变化情况。注意到,Adagrad、 Adadelta 及 RMSprop 法几乎立刻就找到了正确前进方向并以相似的速度很快收敛。而动量法和 NAG 法,则找错了方向,如图所示,让小球沿着梯度下降的方向前进。但 NAG 法能够很快改正它的方向向最小指出前进,因为他能够往前看并对前面的情况做出响应。
saddle_point_evaluation_optimizers

图5.5

图 5.5 展现了各算法在鞍点附近的表现。如上面所说,这对对于 SGD 法、动量法及 NAG 法制造了一个难题。他们很难打破」对称性「带来的壁垒,尽管最后两者设法逃脱了鞍点。而 Adagrad 法、RMSprop 法及 Adadelta 法都能快速的沿着负斜率的方向前进。

5.1.8 如何选择优化方法

在一般机器学习中很多会用 SGD,SGD 虽然能达到极小值,但是比其它算法用的时间长,而且可能会被困在鞍点。
如果需要更快的收敛,或者是训练更深更复杂的神经网络,需要用一种自适应的算法。
最后两张动图从直观上展现了算法的优化过程。第一张图为不同算法在损失平面等高线上随时间的变化情况,第二张图为不同算法在鞍点处的行为比较。
我们该如何选择优化器呢?如果数据为稀疏的,而且采用深度学习,一般需要考虑使用自适用方法,如 Adagrad, RMSprop, Adam等。当然RMSprop, Adam 在很多情况下的效果是相似的。Adam在 RMSprop 的基础上加了bias-correction 和 momentum,随着梯度变的稀疏,Adam 比 RMSprop 效果会好。整体来讲,Adam 是最好的选择。
总的来说,RMSprop 法是一种基于 Adagrad 法的拓展,他从根本上解决学习率骤缩的问题。而 Adam 法,则基于 RMSprop 法添加了偏差修正项和动量项。在我们地讨论范围中,RMSprop、Adam 法都是非常相似地算法,在相似地情况下都能做的很好。Kingma 及其他人展示了他们的偏差修正项帮助 Adam 法,在最优化过程快要结束、梯度变得越发稀疏的时候,表现略微优于 RMSprop 法。总的来说,Adam 也许是总体来说最好的选择。
有趣的是,很多最新的论文,都直接使用了(不带动量项的)Vanilla SGD 法,配合一个简单的学习率(退火)列表。如论文所示,这些 SGD 最终都能帮助他们找到一个最小值,但会花费远多于上述方法的时间。并且这些方法非常依赖于鲁棒的初始化值及退火列表。因此,如果你非常在你的模型能快速收敛,或是你需要训练一个深度或复杂模型,你可能需要选择上述的适应性模型。
就一般经验而言:
对于稀疏数据,尽量使用学习率可自适应的优化方法,不用手动调节,而且最好采用默认值
SGD通常训练时间更长,但是在好的初始化和学习率调度方案的情况下,结果更可靠
如果在意更快的收敛,并且需要训练较深较复杂的网络时,推荐使用学习率自适应的优化方法。
RMSprop,Adam是比较相近的算法,在相似的情况下表现差不多。
在想使用带动量的RMSprop,或者Adam的地方,大多可以使用Nadam取得更好的效果。