最难读的Theano代码

这份LSTM代码的作者,感觉和前面Tutorial代码作者不是同一个人。对于Theano、Python的手法使用得非常娴熟。

尤其是在两重并行设计上:

①LSTM各个门之间并行

②Mini-batch让多个句子并行

同时,在训练、预处理上使用了诸多技巧,相比之前的Tutorial,更接近一个完整的框架,所以导致代码阅读十分困难。

本文旨在梳理这份LSTM代码的脉络。

数据集:IMDB Large Movie Review Dataset

来源

该数据集是来自Stanford的一个爬虫数据集。

对IMDB每部电影的评论页面的每条评论进行爬虫,分为正面/负面两类情感标签。

相比于朴素贝叶斯用于垃圾邮件分类,显然,分析一段文字的情感难度比较大。

因为语义在各个词之间连锁着,有些喜欢玩梗的负面讽刺语义需要一个强力的Represention Extractor。

该数据集同时也在CS224D:Deep Learning for NLP    [Leture4]中演示,用于体现Pre-Training过后的词向量威力。

数据读取

原始数据集被Bengio组封装过,链接 http://www.iro.umontreal.ca/~lisa/deep/data/imdb.pkl

cPickle封装的格式如下:

train_set[0]  ---->  一个包含所有句子的二重列表,列表的每个元素也为一个列表,内容为:

[词索引1,词索引2,.....,词索引n],构成一个句子。熟悉文本数据的应该很清楚。

词索引之前建立了一个词库,实际使用的时候如果要对照索引,获取真实的词,则需要词库:~Link~

train_set[0][n]指的是第n个句子。

————————————————————————————————————————————

train_set[1]  ---->  一个一重列表,每个元素为每个句子的情感标签,0/1。

test_set格式相同。

————————————————————————————————————————————

本Tutorial使用的一些简单处理包括:maxLen句子词数剪枝、词库越界词剪枝、句子词数排序(不知道啥作用)

数据变形与预处理

这份代码的经典之处在于,让多个句子并行训练,构成一个mini-batch。

在RNN章节中,每次训练只是一个句子,所以输入就是一个向量,但mini-batch之后就是一个矩阵。

这个矩阵最大不同在于,xy轴是倒置的,文字方向在竖式伸展。

这么做的原因是由于theano.tensor.scan函数的工作机制。

scan函数的一旦sequence不为空,就进入序列循环模式,设sequence=[x],则

①Step1:取x[0]作为循环函数第一参数

②Step2:取x[1]作为循环函数第一参数

........

③StepN: 取x[N]作为循环函数第一参数

对应语言模型的序列学习算法,每个Step就相当于取一个句子的一个词。

竖式伸展,每次scan取的一排词,称为examples,数量等于batch_size。

横向batch并行,纵向序列时序伸展,这是mini-batch和序列学习的共同作用结果。

——————————————————————————————————————————

每个句子长度是不同的,为了便于并行矩阵计算,必须选定最长句子,最大化矩阵。句子词较少的,用0填充。

而在实际LSTM计算中,这个0填充则成了麻烦,因为你不能让标记为0的Padding参与递归网络计算,需要剔除。

这时候就需要Mask矩阵。来看LSTM.py中的实际代码:

$c = m\_[:, None] * c + (1. - m\_)[:, None] * c\_$

由于mask矩阵也在sequence中,所以m_被降成了1D,通过Numpy的第二维None扩充,可以和c([batch_size,dim_proj])

进行点乘(不是矩阵乘法)。

Mask矩阵的作用就是对于Padding,通过递推,直接滚到到上一个非0的状态,而不引入Padding。

源码解析

def get_minibatches_idx(n, minibatch_size, shuffle=False)

这部分设计对数据(句子)Shuffle随机排列。首先获取数据集所有句子数n,

对所有句子,按照batch_size,划分出 (n//batch_size)+1个列表。

拼在一起,构成一个二重列表。

返回一个zip(batch索引,batch内容),用于运行。

每份batch是一个列表,包含句子的索引。后续会根据句子的索引,拼出一个小量的x,

进过prepare_data处理过之后,方能使用。

def dropout_layer(state_before, use_noise, trng)

50%的Dropout,开了之后千万不要加Weght_Decay,两种一叠加,惩罚太重了。

Dropout的实现,利用了Tensor.switch(Bool,Ture Operation,False Operation)来动态实现。

传入的use_noise,在训练时置为1,这时候Dropout为动态概率屏蔽。

反之在测试时,置为0,这时候Dropout为平均网络。

具体原理见Hinton关于Dropout的论文:

Dropout: A Simple Way to Prevent Neural Networks from Overfitting

def init_params(options)

这部分主要是初始化全部参数。分为两个部分:

①初始化Embedding参数、Softmax输出层参数

②初始化LSTM参数,在下面的 def param_init_lstm()函数中。

①中参数莫名其妙使用了[0,0.01]的随机值初始化,不知道为什么不用负值。

像Word2Vec的初始化,可能更好些:

$ Init \, \sim \, Rand(\frac{-0.5}{Emb\_Dim},\frac{0.5}{Emb\_Dim}) $

Emb大小为$[VocabSize,Emb\_Dim]$,Softmax参数大小为$[Emb\_Dim,2]$

def param_init_lstm(options, params, prefix='lstm')

这部分用于初始化LSTM参数,W阵、U阵。

LSTM的初始化值很特殊,先用[0,1]随机数生成矩阵,然后对随机矩阵进行SVD奇异值分解。

取正交基矩阵来初始化,即 ortho_weight,原理不明,没有找到相关文献。

————————————————————————————————————————

值得注意的就是numpy.concatenate函数的使用,它锁定了AXIS=1轴(横向,列,第二维)

将Input、Forget、Output三态门与Cell的RNN核的相同操作$Wx+Uh^{'}$合并,并行计算。

如果$x$是$[1000,100]$,即$dim\_proj=100$, 那么$W$大小是$[100,100*4]$。

一次计算,有$Wx$的大小是$[1000,400]$,四个部分在矩阵中被并行计算了。

矩阵计算和FOR循环在串行算法下速度是差不多的,但是在并行算法下,矩阵同时计算比先后串行计算快不少。

def init_tparams(params)

这个函数意义就是一键将Numpy标准的params全部转为Theano.shared标准。

替代大量的Theano.shared(......)

启用tparams作为Model的正式params,而原来的params废弃。

def lstm_layer(tparams, state_below, options, prefix='lstm', mask=None)

LSTM的计算核心。

首先得注意参数state_below,这是个3D矩阵,$[n\_Step,BatchSize,Emb\_Dim]$

在scan函数的Sequence里,每步循环,都会降解第一维n_Step,得到一个Emb矩阵,作为输入$X\_$

计算过程:

①用 $[state\_below] \cdot [lstm\_W]$。

这步很奇妙,这是一个3D矩阵与2D矩阵的乘法,由于每个Step,都需要做$Wx$

所以无须每一个Step做一次$Wx$,而是把所有Step的$Wx$预计算好了,并行量很大。

②进入scan函数过程,每一个Step:

I、将前一时序$h'$与4倍化$U$阵并行计算,并加上4倍化$Wx$的预计算

II、分离计算,按照LSTM结构定义,分别计算$Input Gate$、$Forget Gate$、$Outout Gate$

$\tilde{Cell}$、$Cell$、$h$

③返回rval[0],即h矩阵。注意,scan函数的输出结果会增加1D。

每个Step里,h结果是一个2D矩阵,$[BatchSize,Emb\_Dim]$

而rval[0]是一个3D矩阵,$[n\_Step,BatchSize,Emb\_Dim]$。

后续会对第一维$n\_Step$进行Mean Pooling,之后才能降解成用于Softmax的2D输入。

def sgd|adadelta|rmsprop(lr, tparams, grads, x, mask, y, cost)

这是三个可选梯度更新算法。由于作者想要保持格式一致,所以AdaDelta和RMSProp写的有点啰嗦。

AdaDelta详见我的 自适应学习率调整:AdaDelta

至于Hinton在2014 Winter的公开课提出的RMSProp,没有找到具体的数学推导,不好解释。

应该是由AdaDelta改良过来的,代码很费解。由可视化结果来看,RMSProp在某些特殊情况下,比AdaDelta要稳定。

官方代码里默认用的是AdaDelta,毕竟有Matthew D. Zeiler的论文详细推导与说明。

AdaDelta和RMSProp都是模拟二阶梯度更新,所以可以和Learning Rate这个恶心的超参说Bye~Bye了。值得把玩。

def build_model(tparams, options)

该部分是联合LSTM和Softmax,构成完整Theano.function的重要部分。

①首先是定义几个Tensor量,$x$、$y$、$mask$。

②接着,从tparams['Wemb']中取出Words*Sentences数量的词向量,并且变形为3D矩阵。

x.flatten()的使用非常巧妙,它将词矩阵拆成1D列表,然后按顺序取出词向量,然后再按顺序变形成3D形态。

体现了Python和Numpy的强大之处。

③得到3D的输入state_below之后,配合mask,经过LSTM,得到一个3D的h矩阵proj。

④对3D的h矩阵,各个时序进行Mean Pooling,得到2D矩阵,有点像Dropout的平均网络。

⑤Dropout处理

⑥Softmax、构建prob、pred、cost,都是老面孔了。

特别的是,这里有一个offset,防止prob爆0,造成log溢出。碰到这种情况可能不大。

def pred_probs|pred_error()

用于Cross-Validation计算Print信息。也就是Debug Mode...

def train_lstm()

训练过程。Theano代码块中最冗长、最臃肿的部分。

①options=locals().copy()是python中的一个小trick。

它能将函数中所有参数按爬虫下来,保存为一个词典,方便访问。

②load_data。定义在imdb.py中,获取train、vaild、test三个数据集

让人费解的是,既然这里要对test做shuffle,又何必在load_data里把test排序呢?

③初始化params,并且build_model,返回theano.function的pred、prob、cost。

④WeightDecay。有Dropout之后,必要性不大。

⑤利用cost,得到grad。利用tparams、grad,得到theano.function的update。

这里代码很啰嗦,如cost完全没有必要通过theano.function转化出来,保持Tensor状态是可以带入T.grad

而前面就没有这么写。代码风格和前面的章节截然不同。

至此,准备工作完毕,进入mini-batch执行阶段。

——————————————————————————————————————————————

①首先获取vaild和test的zip化minibatch。

每轮zip返回一个二元组(batch_idx,batch_content_list),idx实际并不用。

list是指当前batch中所有句子的idx。然后,对这些离散的idx,拼出实际的1D$x$。

经过imdb.py下的prepare_data,得到2D的$x$,作为Word Embedding的预备输入。

②进入max_epochs循环阶段:

包括early stopping优化,这部分与之前章节大致相同。

——————————————————————————————————————————————

至此,这份源码算是解析完了。

Theano:LSTM源码解析的更多相关文章

  1. [源码解析] 深度学习流水线并行Gpipe(1)---流水线基本实现

    [源码解析] 深度学习流水线并行Gpipe(1)---流水线基本实现 目录 [源码解析] 深度学习流水线并行Gpipe(1)---流水线基本实现 0x00 摘要 0x01 概述 1.1 什么是GPip ...

  2. [源码解析] 深度学习流水线并行 GPipe(3) ----重计算

    [源码解析] 深度学习流水线并行 GPipe(3) ----重计算 目录 [源码解析] 深度学习流水线并行 GPipe(3) ----重计算 0x00 摘要 0x01 概述 1.1 前文回顾 1.2 ...

  3. [源码解析] 深度学习流水线并行之PopeDream(1)--- Profile阶段

    [源码解析] 深度学习流水线并行之PopeDream(1)--- Profile阶段 目录 [源码解析] 深度学习流水线并行之PopeDream(1)--- Profile阶段 0x00 摘要 0x0 ...

  4. [源码解析] 深度学习流水线并行 PipeDream(2)--- 计算分区

    [源码解析] 深度学习流水线并行 PipeDream(2)--- 计算分区 目录 [源码解析] 深度学习流水线并行 PipeDream(2)--- 计算分区 0x00 摘要 0x01 前言 1.1 P ...

  5. [源码解析] 深度学习流水线并行 PipeDream(3)--- 转换模型

    [源码解析] 深度学习流水线并行 PipeDream(3)--- 转换模型 目录 [源码解析] 深度学习流水线并行 PipeDream(3)--- 转换模型 0x00 摘要 0x01 前言 1.1 改 ...

  6. [源码解析] 深度学习流水线并行 PipeDream(4)--- 运行时引擎

    [源码解析] 深度学习流水线并行 PipeDream(4)--- 运行时引擎 目录 [源码解析] 深度学习流水线并行 PipeDream(4)--- 运行时引擎 0x00 摘要 0x01 前言 1.1 ...

  7. [源码解析] PyTorch 分布式(14) --使用 Distributed Autograd 和 Distributed Optimizer

    [源码解析] PyTorch 分布式(14) --使用 Distributed Autograd 和 Distributed Optimizer 目录 [源码解析] PyTorch 分布式(14) - ...

  8. 【原】Android热更新开源项目Tinker源码解析系列之三:so热更新

    本系列将从以下三个方面对Tinker进行源码解析: Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Android热更新开源项目Tinker源码解析系列之二:资源文件热更新 A ...

  9. 【原】Android热更新开源项目Tinker源码解析系列之一:Dex热更新

    [原]Android热更新开源项目Tinker源码解析系列之一:Dex热更新 Tinker是微信的第一个开源项目,主要用于安卓应用bug的热修复和功能的迭代. Tinker github地址:http ...

随机推荐

  1. eclipse中SSH三大框架环境搭建<二>

    通过上一篇博客我们可以轻松搭建strtus2的环境,接下来由我来继续介绍spring的环境搭建以及spring注入的简单使用 相关链接:eclipse中SSH三大k框架环境搭建<一> ec ...

  2. 第二轮冲刺-Runner站立会议09

    今天:将日历界面和主程序结合起来 明天:查看整个项目,调试是否有问题

  3. HTML补充

    补充一: 让DIV 自动居中 width:980px;margin:0 auto; 补充二: 内敛标签: inline #块级标签转换成内联标签. block #变成块级. inline-block ...

  4. thwen 缓动框架

    描述 目前提供一个方法 ele 元素对象 obj 操作 duration 时间 effect 缓动选择 thwenMove(option) 框架支持以下缓动策略 -指数衰减的正弦曲线缓动 -圆形曲线的 ...

  5. VC调试闪退解决办法

    在VC2010调试或执行EXE文件时,程序运行结束后自动退出了,想看到打印 可以采用几种方法: 1.按ctrl+F5只执行不调试 2.在cmd中手动调用 而不是直接点 3.加入getchar  #in ...

  6. java基本知识小记(1)

    1.Java中的值传递 值传递意味着对于传给方法的每个参数都会制作一份副本然后将副本而不是原始值传递给方法并且通过参数的名进行引用. 注意:虽然传值机制对于所有参数类型都适用,但是他对对象类型的作用与 ...

  7. linux 最近使用的命令

    vi 文件名   >>  按 i 键,进行编辑,编辑完,按ESC键,再按:q代表不保存退出,按:wq代表保存退出. ps aux|grep java >>  搜索目前启动的ja ...

  8. redis该如何分区-译文(原创)

    写在最前,最近一直在研究redis的使用,包括redis应用场景.性能优化.可行性.这是看到redis官网中一个链接,主要是讲解redis数据分区的,既然是官方推荐的,那我就翻译一下,与大家共享. P ...

  9. HTML页面meta标签内容详解

    所有的浏览器都支持meta标签,用于提供页面相关信息,信息都是以名(http-equiv和name标示)/值(content标示)对的形式现实. 属性content,用于定义http-equiv(定义 ...

  10. 【ZJOI2008】 树的统计 count

    Description 一 棵树上有n个节点,编号分别为1到n,每个节点都有一个权值w.我们将以下面的形式来要求你对这棵树完成一些操作: I. CHANGE u t : 把结点u的权值改为t II. ...