Github-jcjohnson/torch-rnn代码详解
zoerywzhou@gmail.com
http://www.cnblogs.com/swje/
作者:Zhouwan
2016-3-18
声明
1)本文仅供学术交流,非商用。所以每一部分具体的参考资料并没有详细对应。如果某部分不小心侵犯了大家的利益,还望海涵,并联系博主删除。
2)本人才疏学浅,整理总结的时候难免出错,还望各位前辈不吝指正,谢谢。
请联系:zoerywzhou@gmail.com 或13813017783@163.com
本研究课题系本人本科毕业论文,具体学习计划见http://www.cnblogs.com/swje/p/5068069.html,
源文件及参考文献如下
torch-rnn代码@Github:https://github.com/jcjohnson/torch-rnn
学习体会
1、torch-rnn 提供了一个高性能、可再用的RNN和LSTM模块,使用这些模块对字符级别的语言建模和char-rnn是类似的。RNN和LSTM模块仅仅依赖于torch和nn,所以可以很容易地整合到现有的项目中。相比于char-rnn,torch-rnn的速度快了1.9倍,并且节约了七倍的内存。
- 数据预处理:在训练前,需要用脚本scripts/preprocess.py对数据进行预处理,这将会生成一个包含数据的预处理版本的HDF5 文件和 JSON 文件。 比如生成了
my_data.h5
和my_data.json
两个文件。
- 训练模型:预处理之后,需要用脚本train.lua 来训练模型。这一步是最慢的。
可以运行下面的代码来训练:
th train.lua -input_h5 my_data.h5 -input_json my_data.json
以上代码将会读取存储在my_data.h5
和 my_data.json两个文件中的数据,
运行一段时间后,将会生成检查点文件checkpoint,文件命名类似cv/checkpoint_1000.t7。
你可以通过参数设置改变RNN模型类型、隐藏层大小和 RNN层数,选择使用CUDA在GPU模式下运行或在CPU模式下运行,也可以选择是否使用OpenCL,还有其他参数设置,参考这里。
- 从模型中抽样:训练完一个模型之后,你可以通过使用脚本sample.lua从文本中抽样来生成新的文本。运行以下代码:th sample.lua -checkpoint cv/checkpoint_10000.t7 -length 2000,将会从前一步载入训练好的检查点集 ,从中抽取2000个字符,并将结果打印到控制台上。
你可以通过参数设置,选择使用CUDA在GPU模式下运行或者在CPU模式下运行,也可以选择是否使用OpenCL,还有其他参数设置,参考这里。
4、为了用基准问题测试torch-rnn和char-rnn,我们对莎士比亚散文集训练LSTM语言模型,RNN层数和RNN大小分别设置为1、2、3层和64、128、256和512,并将minibatch大小设为50,序列长度设为50,dropout设为0。对于同一大小RNN模型的两次实验中,在前100次训练迭代过程中,我们记录下向前和向后传播的时间和GPU的内存使用情况,并使用这些测量值来计算平均时间和内存使用情况。所有的基准测试程序数值都是在一个配置有 Intel i7-4790k CPU, 32 GB 主存和带有一个 Titan X GPU的机器上运行的。
从实验结果出可以看出,torch-rnn在任何模型大小下都比char-rnn运行速度快,小模型的加速比更大一些。对于有128个隐藏单元的单层LSMT来说,加速了1.9倍;对于较大的模型,我们达到大约1.4倍的加速。
从节约GPU内存方面来看,torch-rnn在所有的模型大小上都胜过char-rnn,但是,对于较大一些的模型节约内存更多一些,例如:对于有512个隐藏单元的模型来说,torch-rnn比char-rnn少用了七倍内存。
一句话总结:torch-rnn相比于char-rnn,更节约内存,模型越大越节约;且训练时间(前向传播&反向传播的时间)更短,模型越小加速越快!
模块分析
1、VanillaRNN:
rnn = nn.VanillaRNN(D, H)
VanillaRNN 是 torch nn.Module的一个子类,用双曲正切函数实施一个vanilla 递归神经网络。它将一个D维输入向量的序列转换为一个H维隐藏状态向量的序列;在长度为T的序列、大小为N的minibatch的模型上运行,在每一次前向传播中,序列长度和minibatch的大小可以改变。
暂且忽略minibatch,vanilla RNN使用下面的递归关系式 从前一个隐藏状态 h[t - 1](of shape (
H,)
) 和当前的输入向量 x[t](of shape (
D,)
) 来计算下一个隐藏层的状态向量 h[t]:
h[t] = tanh(Wh h[t- 1] + Wx x[t] + b)
其中, Wx是一个连接输入层和隐藏层的矩阵,
Wh是一个连接隐藏层和隐藏层的矩阵,b 是一个偏项。其中权重Wx 和Wh存储在大小为
(D + H, H)
的张量rnn.weight 中,而偏差项 b 存储在为H维的张量 rnn.bias中。
你可以用两种不同的方式来使用 VanillaRNN
实例:
h = rnn:forward({h0, x}) grad_h0, grad_x = unpack(rnn:backward({h0, x}, grad_h)) h = rnn:forward(x) grad_x = rnn:backward(x, grad_h)
h0
是隐藏层初始状态,大小为(N,H), x
是输入向量序列,大小为(N, T, D)
。在每个时间步长,输出 h 是隐藏状态的序列,
大小是 (N, T, H)
。在一些应用中,比如图像字幕,隐藏层的初始状态可能是一些其他网络计算出来的输出结果。
默认情况下,如果前向传播中没有提供 h0 ,那么隐藏层的初始状态就设为0。这种方法可能对情感分析等应用有帮助,这类应用往往需要用一个RNN处理很多独立的序列。
如果没有提供h0,且实例变量rnn.remember_states的值被设置为真,那么首次调用rnn:forward时将会将隐藏层的初始状态设为0;在随后调用rnn:forward时,从先前的调用得到的最后一个隐藏状态将被作为下一个隐藏层的初始状态。这种方法常用在语言模型中,我们想用很长的序列(可能无限长)训练网络,并且使用沿时间截断反向传播的方法(truncated back-propagation through time)计算梯度。通过调用rnn:resetStates()使模型忘记它的隐藏状态,然后下次调用rnn:forward时会将h0的值初始化为0。
这种方法在unit test for VanillaRNN.lua中有所运用。
作为一个实现,我们直接执行 :backward 以同时计算关于输入的梯度并积累关于权重的梯度,因为这两个操作涉及很多相同的计算。我们将 :updateGradInput
和:accGradparameters重写进调用 :backward中,然后直接调用:backward而不是先调用:updateGradInput
再调用:accGradparameters,
这样就避免了计算两次同样的事情。
文件VanillaRNN.lua是独立的,除了torch 和 nn,对其他模块没有依赖性。
2、LSTM:
lstm = nn.LSTM(D, H)
LSTM( Long Short-Term Memory的简称)是一个别致的递归神经网络类型,比vanilla RNNs常用的多。类似于上面提到的vanilla RNNs的性质,LSTM是一个执行LSTM的torch nn.Module 的子类。它将一个D维输入向量的序列转换为一个H维隐藏状态向量的序列;在长度为T的序列、大小为N的minibatch的模型上运行,在每一次前向传播中,序列长度和minibatch的大小可以改变。
LSTM和vanilla RNN的不同之处在于,在每一个时间步长上它都跟踪隐藏状态和cell 状态。暂且忽略minibatch,vanilla RNN使用下面的递归关系式 从前一个隐藏状态 h[t - 1](of shape (
H,)
) 、前一个cell状态 c[t-1] 和当前的输入向量 x[t](of shape (
D,)
) 来计算下一个隐藏状态向量 h[t]和cell状态向量 c[t]:
ai[t] = Wxi x[t] + Whi h[t - 1] + bi # Matrix / vector multiplication af[t] = Wxf x[t] + Whf h[t - 1] + bf # Matrix / vector multiplication ao[t] = Wxo x[t] + Who h[t - 1] + bo # Matrix / vector multiplication ag[t] = Wxg x[t] + Whg h[t - 1] + bg # Matrix / vector multiplication i[t] = sigmoid(ai[t]) # Input gate f[t] = sigmoid(af[t]) # Forget gate o[t] = sigmoid(ao[t]) # Output gate g[t] = tanh(ag[t]) # Proposed update c[t] = f[t] * c[t - 1] + i[t] * g[t] # Elementwise multiplication of vectors h[t] = o[t] * tanh(c[t]) # Elementwise multiplication of vectors
输入层到隐藏层的矩阵 Wxi
, Wxf
, Wxo和
Wxg
以及隐藏层到隐藏层的矩阵 Whi
, Whf
, Who和
Whg
都被存储在大小为(D + H, 4 * H)的单个张量lstm.weight
中,偏差向量 bi
, bf
, bo
和 bg
被存储在大小为(4 * H,)的单个张量 lstm.bias
中。
你可以用三种不同的方式来使用 LSTM
实例:
h = lstm:forward({c0, h0, x}) grad_c0, grad_h0, grad_x = unpack(lstm:backward({c0, h0, x}, grad_h)) h = lstm:forward({h0, x}) grad_h0, grad_x = unpack(lstm:backward({h0, x}, grad_h)) h = lstm:forward(x) grad_x = lstm:backward(x, grad_h)
在所有情况下,c0
是初始cell 状态,大小为(N,H), h0
是初始隐藏状态,大小为(N,H),x 是输入向量序列,大小为(N, T, D)
, h 是输出隐藏状态的序列,
大小是 (N, T, H)
。
如果没有提供初始cell状态或者初始隐藏状态,那么它们的初始状态就默认设为0。
如果没有提供初始 cell 状态或者初始隐藏状态,且实例变量 lstm.remember_states 的值被设置为真,那么首次调用 lstm:forward 时将会将隐藏层的初始状态和cell 状态设为0;在随后调用lstm:forward 中,将会把隐藏状态和cell状态的初始值设为前一次调用时得到的最终的隐藏层和cell的状态,这和 VanillaRNN 很类似
。你可以通过调用lstm:resetStates() 重置它们的cell 状态和隐藏状态,然后下次调用 lstm:forward时会将初始隐藏状态和cell 状态设为 0。
这种方法在unit test for LSTM.lua中有所运用。
作为一个实现,我们直接执行 :backward 以同时计算关于输入的梯度并积累关于权重的梯度,因为这两个操作涉及很多相同的计算。我们将 :updateGradInput
和 :accGradparameters 重写进调用 :backward
中,然后直接调用:backward而不是先调用:updateGradInput
再调用:accGradparameters,
这样就避免了计算两次同样的事情。
文件LSTM.lua 是独立的,除了torch 和 nn,对其他模块没有依赖性。
3、LanguageModel module:
torch-rnn提供了一个LanguageModel module,用于对字符级的语言建模。
model = nn.LanguageModel(kwargs)
LanguageModel 使用以上模块 通过dropout 正则化来实现多层递归神经网络语言模型。因为 LSTM
和 VanillaRNN
是 nn.Module
子类,我们可以通过在容器 nn.Sequential
中简单地堆积多个实例来实现多层递归神经网络。
kwargs
是一张表,包含以下关键词:
-
idx_to_token
: 一张给定语言模型词汇的表,将整型ids 映射为 字符串tokensmodel_type
: "lstm" 或 "rnn"wordvec_size
: 单词向量嵌入的维度rnn_size
: RNN的隐藏状态大小num_layers
: 使用的RNN层数dropout
: 介于0和1之间的数字,指定经过每个RNN层之后的dropout长度