戳simple-recurrent-network.lua下载源码。
Torch的重要作者之一,Nicolas Leonard对Torch中的nn库进行扩展,发布了rnn库。这个库包含RNN, LSTM, GRU, BRNN, BLSTM等能够处理时序和记忆的网络。
本文以其中Simple Recurrent Network源码为例,讲解Torch中使用RNN1的基本方式。x是自然数序列。
这篇代码实现的功能是:输入一个序列x,能够输出一个序列y。
在真实应用中,输入输出序列可能是不同语种的一句话。这里做了极大简化:y=x+1 mod 10。
为了使用RNN,需要包含rnn库:
require 'rnn'
首先设定关键参数。RNN的核心是隐变量 h ,记录了系统当前时刻的状态。
batchSize = 8
rho = 5 -- sequence length
hiddenSize = 7 -- 隐变量维度
nIndex = 10 -- 输出分类数量
lr = 0.1 --学习率
下面可以建立rnn库中的Recurrent类型(参看Recurrent.lua)建立核心模块r
:
local r = nn.Recurrent(
hiddenSize, -- start
nn.LookupTable(nIndex, hiddenSize), -- input
nn.Linear(hiddenSize, hiddenSize), -- feedback
nn.Sigmoid(), -- transfer
rho -- rho
)
创建Recurrent的四个参数意义如下:
start
- 指明隐变量维度。或者,指明从input
到transfer
模块之间的操作。
input
- 指明从输入到隐变量的操作。此处是个nn库中的查找表2。
feedback
- 从前一时刻transfer
之前到当前transfer
函数之前的操作,和input
结果逐元素相加。此处是个全连接层。
transfer
- 非线性函数,可以取ReLU, Sigmoid等。
rho
- 反向传播的步数。最多只向前考虑这么多步骤。
参考源码中的updateOutput
函数,给出了输入和输出的关系:
输入 x 是1-10之间的整数。
核心模块r
输出的是隐变量 h ,还需要做一点后处理,才能输出各类得分。
这里用一个全连层把7维隐变量变成10类数字得分,最后加一个SoftMax调整为概率:
local rnn = nn.Sequential()
:add(r)
:add(nn.Linear(hiddenSize, nIndex))
:add(nn.LogSoftMax())
用Recursor
把rnn
包起来,声明这是一个循环网络:
rnn = nn.Recursor(rnn, rho)
最后加上negative log likelihood误差函数:
criterion = nn.ClassNLLCriterion()
这个误差函数的输入为长度nIndex的概率x
,真值为一个标量类号class
。误差为:loss(x, class) = -x[class]。
下面来准备一些原始数据。
sequence是个长度1000的DoubleTensor,重复1,2…10,1,2..10。
sequence_ = torch.LongTensor():range(1,10) -- 1到10,列向量
sequence = torch.LongTensor(100,10):copy(sequence_:view(1,10):expand(100,10)) -- view函数把列向量变成行向量,sequence为100*10的矩阵,每行相同
sequence:resize(100*10) -- one long sequence of 1,2,3...,10,1,2,3...10...
offset包含batchSize个偏移量,取值范围在1-1000之间。
offsets = {}
for i=1,batchSize do
table.insert(offsets, math.ceil(math.random()*sequence:size(1)))
end
offsets = torch.LongTensor(offsets)
在每一次训练迭代中,从sequence中创建输入inputs
和真值targets
:
local inputs, targets = {}, {}
for step=1,rho do -- 设定每一行
-- a batch of inputs
inputs[step] = sequence:index(1, offsets)
-- incement indices
offsets:add(1)
for j=1,batchSize do
if offsets[j] > sequence:size(1) then
offsets[j] = 1
end
end
targets[step] = sequence:index(1, offsets)
end
这里的inputs
是一个尺寸为rho*batchSize的数据(左),它的每一列是一个自然数序列,每一行对应这批数据中的一步。
targest
(右)结构类似,但是比输入后错一位。
每一步输入,都对应着一步输出。
首先清理参数和前次迭代记忆:
rnn:zeroGradParameters()
rnn:forget() -- forget all past time-steps
rho
表示反向传播的步数,也是训练过程中能够考虑的序列长度。分rho
步,送入batch中当前步骤的一行数据:
local outputs, err = {}, 0
for step=1,rho do
outputs[step] = rnn:forward(inputs[step])
err = err + criterion:forward(outputs[step], targets[step])
end
inputs[step]
为一个长度为batchSize的数组,表示各序列当前步骤类标。
outputs[step]
为一个长度为batchSize*nIndex的数组,每一列表示该序列在当前步骤属于每一类的概率。
同样要分rho
步执行,但要注意:inputs
和targets
数据按照倒序执行:
local gradOutputs, gradInputs = {}, {}
for step=rho,1,-1 do -- reverse order of forward calls
gradOutputs[step] = criterion:backward(outputs[step], targets[step])
gradInputs[step] = rnn:backward(inputs[step], gradOutputs[step])
end
本次迭代结束之前,利用反向传播结果调整系统参数:
rnn:updateParameters(lr)
iteration = iteration + 1
我们可以看到,RNN网络的记忆长度由rho
控制。如果需要记忆较远的状态,需要反复执行很多步前向和后向算法。