cvpr2015,LSTM代码的链接为https://github.com/wojzaremba/lstm,作为一个初学Torch的小白,决定直接从代码入手,再根据Torch的说明包,与网上资料来进一步学习Torch。
不多说,直接上代码。
该代码分为三个lua文件,分别为base.lua,data.lua,main.lua。
先从base.lua
function g_disable_dropout(node)
if type(node) == "table" and node.__typename == nil then
for i = 1, #node do
node[i]:apply(g_disable_dropout)
end
return
end
if string.match(node.__typename, "Dropout") then
node.train = false
end
end
function g_enable_dropout(node)
if type(node) == "table" and node.__typename == nil then
for i = 1, #node do
node[i]:apply(g_enable_dropout)
end
return
end
if string.match(node.__typename, "Dropout") then
node.train = true
end
end
上面这两块代码,分别为dropout的取消,与dropout的设定,dropout是在深度学习中,防止过拟合的一种方式,给出其中一个解释链接,我也不是很懂,http://www.aiuxian.com/article/p-1870737.html
function g_cloneManyTimes(net, T)
local clones = {}
--创建克隆副本
local params, gradParams = net:parameters()
--得到原网络中的参数
local mem = torch.MemoryFile("w"):binary()
--将net网络存入文件中,于readObject()相对应
mem:writeObject(net)
for t = 1, T do
-- We need to use a new reader for each clone.
-- We don't want to use the pointers to already read objects.
--打开读文件
local reader = torch.MemoryFile(mem:storage(), "r"):binary()
--创建克隆副本
local clone = reader:readObject()
--关闭文件
reader:close()
--获得参数
local cloneParams, cloneGradParams = clone:parameters()
for i = 1, #params do
--复制参数
cloneParams[i]:set(params[i])
cloneGradParams[i]:set(gradParams[i])
end
--复制
clones[t] = clone
--获得可用内存空间
collectgarbage()
end
--关闭文件
mem:close()
--返回克隆体
return clones
end
这个代码块是用来,copy多份网络。
初始化GPU设置
function g_init_gpu(args)
local gpuidx = args
gpuidx = gpuidx[1] or 1
print(string.format("Using %s-th gpu", gpuidx))
--setDevice(ID)应用与multi-GPU模式,ID为GPU的编号
cutorch.setDevice(gpuidx)
g_make_deterministic(1)
end
function g_make_deterministic(seed)
--设置随机种子
torch.manualSeed(seed)
--设置GPUtorch随机种子
cutorch.manualSeed(seed)
--uniform(a,b)为返回一个a-b之间的随机数
torch.zeros(1, 1):cuda():uniform()
end
此函数为将to的内容用from来代替
function g_replace_table(to, from)
assert(#to == #from)
for i = 1, #to do
to[i]:copy(from[i])
end
end
此函数块为改变字符形式
function g_f3(f)
return string.format("%.3f", f)
end
function g_d(f)
--torch.round()为torch的去整函数
return string.format("%d", torch.round(f))
end
data.lua代码块
此代码块不做过多解释
local stringx = require('pl.stringx')
local file = require('pl.file')
--设置路径
local ptb_path = "./data/"
local vocab_idx = 0
local vocab_map = {}
此函数的作用为对数据作预处理
local function replicate(x_inp, batch_size)
--s为样本的容量
local s = x_inp:size(1)
--x是根据batch_size,确定样本组的数量,batch_size个样本一组
local x = torch.zeros(torch.floor(s / batch_size), batch_size)
for i = 1, batch_size do
--
local start = torch.round((i - 1) * s / batch_size) + 1
--x:size(1)为样本组的组数
local finish = start + x:size(1) - 1
--torch:sub()为取部分的函数,
--x_inp:sub(start,finish)意味取x_inp,start到finish数据,这是将数据从x_inp中复制到x中
x:sub(1, x:size(1), i, i):copy(x_inp:sub(start, finish))
end
return x
end
这个函数为加载数据函数,fname为文件名,由于pl.stringx,与pl.file这两个包暂时我还没查到,所以有些语句暂时还没理解
local function load_data(fname)
local data = file.read(fname)
data = stringx.replace(data, '\n', '' )
data = stringx.split(data)
print(string.format("Loading %s, size of data = %d", fname, #data))
local x = torch.zeros(#data)
for i = 1, #data do
if vocab_map[data[i]] == nil then
vocab_idx = vocab_idx + 1
vocab_map[data[i]] = vocab_idx
end
x[i] = vocab_map[data[i]]
end
return x
end
这个函数块,根据batch_size来预处理原始数据
local function traindataset(batch_size)
local x = load_data(ptb_path .. "ptb.train.txt")
--上个函数已经说明,大致化成x[num][batch_size],num为个数样本组的个数,batch_size为样本组的个数,每个单元为一个数据集。
x = replicate(x, batch_size)
return x
end
这个函数块同上面函数块,将test,valid数据进行预处理
local function testdataset(batch_size)
local x = load_data(ptb_path .. "ptb.test.txt")
--这句话稍微与之前有所不同,这里为什么采用这种方式来预处理数据,我暂时还不清楚
x = x:resize(x:size(1), 1):expand(x:size(1), batch_size)
return x
end
local function validdataset(batch_size)
local x = load_data(ptb_path .. "ptb.valid.txt")
x = replicate(x, batch_size)
return x
end
该包返回的数据
return {traindataset=traindataset,
testdataset=testdataset,
validdataset=validdataset}
main.lua
pcall(a)函数可以查看a语句有木有执行成功
--fbcunn,这个包这torch帮助文档中暂时没有找到
local ok,cunn = pcall(require, 'fbcunn')
if not ok then
--如果没有找到暂时先导入cunn这个包
ok,cunn = pcall(require,'cunn')
if ok then
--如果找到了,打印以下话
print("warning: fbcunn not found. Falling back to cunn")
--nn包中LookupTable.lua中的帮助文档于源代码https://github.com/torch/nn/blob/master/LookupTable.lua
LookupTable = nn.LookupTable
else
print("Could not find cunn or fbcunn. Either is required")
os.exit()
end
else
--得到GPU的属性
deviceParams = cutorch.getDeviceProperties(1)
--GPU的计算能力
cudaComputeCapability = deviceParams.major + deviceParams.minor/10
--同上面代码块
LookupTable = nn.LookupTable
end
--导入nngraph,base,data等工具包
require('nngraph')
require('base')
local ptb = require('data')
这个函数块为LSTM的训练参数
local params = {batch_size=20, --批处理数据的大小
seq_length=20, --序列长度
layers=2,
--参数的正则权重
decay=2,
rnn_size=200, --网络中隐藏层的大小
dropout=0, --dropout率,防止overfitting
init_weight=0.1,
lr=1, --
vocab_size=10000, --词向量的大小
max_epoch=4, --最大迭代次数
max_max_epoch=13,
max_grad_norm=5}
在GPU上运行
local function transfer_data(x) --GPU的转换器
return x:cuda()
end
local state_train, state_valid, state_test --三个数据集
local model = {} --真正最后的模型
local paramx, paramdx --训练参数
local state_train, state_valid, state_test --三个数据集
local model = {} --真正最后的模型
local paramx, paramdx --两种训练参数
lstm层
local function lstm(x, prev_c, prev_h) --lstm层
-- Calculate all four gates in one go
local i2h = nn.Linear(params.rnn_size, 4*params.rnn_size)(x) --x为输入新的输入向量,分别扩展成o层,f层,c层,i层
local h2h = nn.Linear(params.rnn_size, 4*params.rnn_size)(prev_h) --prev_h是输入向量,上个状态的输入,同x一样扩展成4层
local gates = nn.CAddTable()({i2h, h2h}) --两个向量相加,形成所有gate的未加非线性的状态
-- Reshape to (batch_size, n_gates, hid_size)
-- Then slize the n_gates dimension, i.e dimension 2
local reshaped_gates = nn.Reshape(4,params.rnn_size)(gates)
--将上面和在一起的gates,重新排列为4个不同的gate,得reshaped_gates
local sliced_gates = nn.SplitTable(2)(reshaped_gates)
--切分reshaped_gates的第二维度,扩展成一个Table{1,2,3,4}
--
-- Use select gate to fetch each gate and apply nonlinearity
-- 进行非线性处理
local in_gate = nn.Sigmoid()(nn.SelectTable(1)(sliced_gates))
local in_transform = nn.Tanh()(nn.SelectTable(2)(sliced_gates))
local forget_gate = nn.Sigmoid()(nn.SelectTable(3)(sliced_gates))
local out_gate = nn.Sigmoid()(nn.SelectTable(4)(sliced_gates))
-- 下个Cell单元的状态,公式不详细说了,给出一个LSTM简介http://blog.csdn.net/zdy0_2004/article/details/49977423
local next_c = nn.CAddTable()({
nn.CMulTable()({forget_gate, prev_c}),
nn.CMulTable()({in_gate, in_transform})
})
--下个hide层的状态
local next_h = nn.CMulTable()({out_gate, nn.Tanh()(next_c)})
--返回,下个cell层和下个hide层的状态,因为以后的网络可能还会用到
return next_c, next_h
end
创建最终的网络
local function create_network()
local x = nn.Identity()() --创建网络的输入借口,用来存放输入数据
local y = nn.Identity()() --目标向量的接口
local prev_s = nn.Identity()() --暂时认定为,先前输出的结果,应该为cell与hide的结合
local i = {[0] = LookupTable(params.vocab_size,
params.rnn_size)(x)} --是输入向量到,词向量空间的一个映射
local next_s = {} --认定为下一个结果,同样也是cell层与hide层的结合
local split = {prev_s:split(2 * params.layers)}
--2*params.layers乘二是因为prev_s包含cell层与hide,所以乘以二
--先认定params。layers为这个网络的层数,每一层都为LSTM
for layer_idx = 1, params.layers do
local prev_c = split[2 * layer_idx - 1]
local prev_h = split[2 * layer_idx]
--从prev_s中取出prev_c与prev_h
local dropped = nn.Dropout(params.dropout)(i[layer_idx - 1])
--得到dropped层
local next_c, next_h = lstm(dropped, prev_c, prev_h)
--经过lstm得到下一个next_c与next_h
--再将next_c与next_h插入到next_s
table.insert(next_s, next_c)
table.insert(next_s, next_h)
--导入输入
i[layer_idx] = next_h
end
local h2y = nn.Linear(params.rnn_size, params.vocab_size)
--设置隐藏层,最后一层与接口相连的一层
local dropped = nn.Dropout(params.dropout)(i[params.layers])
local pred = nn.LogSoftMax()(h2y(dropped))
local err = nn.ClassNLLCriterion()({pred, y})
local module = nn.gModule({x, y, prev_s},
{err, nn.Identity()(next_s)})
--从这里可以看出形成后的网络输入为x,y,prev_s向量,输出为err,next_s向量
module:getParameters():uniform(-params.init_weight, params.init_weight)
--初始化参数,将参数设定为[-params.init_weight,params.init_weight]之间
return transfer_data(module)
--将网络转化成GPU模式并返回
end
local function setup()
print("Creating a RNN LSTM network.")
--得到核心网络
local core_network = create_network()
--获得参数
paramx, paramdx = core_network:getParameters()
--创建最终模型model,s为储存LSTM中的prev_c与prev_h
--ds,start_s为整个序列公用的,暂未读懂干啥
model.s = {}
model.ds = {}
model.start_s = {}
--词向量序列的长度
for j = 0, params.seq_length do
model.s[j] = {}
for d = 1, 2 * params.layers do
--储存的是 s[j][d]是LSTM中每个层的prev_c与prev_h
model.s[j][d] = transfer_data(torch.zeros(params.batch_size, params.rnn_size))
end
end
for d = 1, 2 * params.layers do
--整个序列公用的start_s与ds
model.start_s[d] = transfer_data(torch.zeros(params.batch_size, params.rnn_size))
model.ds[d] = transfer_data(torch.zeros(params.batch_size, params.rnn_size))
end
--得到核心网络
model.core_network = core_network
--按照序列长度clone出相应长度的核心网络,完成整个rnn网路
model.rnns = g_cloneManyTimes(core_network, params.seq_length)
model.norm_dw = 0
--创建序列每个对应的误差
model.err = transfer_data(torch.zeros(params.seq_length))
end
重置状态,只重置start_s,与state_pos
local function reset_state(state)
--state.pos标明网络现在所训练序列的位置
state.pos = 1
if model ~= nil and model.start_s ~= nil then
for d = 1, 2 * params.layers do
model.start_s[d]:zero()
end
end
end
重置ds
local function reset_ds()
for d = 1, #model.ds do
model.ds[d]:zero()
end
end
网络的向前传播
local function fp(state)
--将第一号序列的起始向量定义为start_s,作为开始向量
g_replace_table(model.s[0], model.start_s)
--如果状态位置+序列长度大于整个state的尺寸就重新初state
--最后一组残余数据不参加训练
if state.pos + params.seq_length > state.data:size(1) then
reset_state(state)
end
for i = 1, params.seq_length do
--得到输入x,目标向量y,序列中的隐藏层(cell,hide)情况s
local x = state.data[state.pos]
local y = state.data[state.pos + 1]
--prev_c与prev_h
local s = model.s[i - 1]
--unpack它接受一个数组(table)作为参数,并默认从下标1开始返回数组的所有元素
--第i号core_network,向前传播,{X,Y,S}为输入向量
model.err[i], model.s[i] = unpack(model.rnns[i]:forward({x, y, s}))
--指向下一个词
state.pos = state.pos + 1
end
--将下次进行向前传播的start_s确定
g_replace_table(model.start_s, model.s[params.seq_length])
--返回误差的平均值
return model.err:mean()
end
反向传播
local function bp(state)
--初始paramdx与reset_ds
paramdx:zero()
reset_ds()
--由于是反向传播,所以出params.seq_length开始到第一个词
for i = params.seq_length, 1, -1 do
state.pos = state.pos - 1
--x为当前第i层的输入
local x = state.data[state.pos]
--y为当前层的目标输出
local y = state.data[state.pos + 1]
--s为prev_c与prev_h
local s = model.s[i - 1]
--初始derr
local derr = transfer_data(torch.ones(1))
--执行反向传播
local tmp = model.rnns[i]:backward({x, y, s},
{derr, model.ds})[3]
g_replace_table(model.ds, tmp)
cutorch.synchronize()
end
--重新复原state.pos
state.pos = state.pos + params.seq_length
--若norm_dw大于最大值,确定放缩因子,缩小梯度
model.norm_dw = paramdx:norm()
if model.norm_dw > params.max_grad_norm then
local shrink_factor = params.max_grad_norm / model.norm_dw
paramdx:mul(shrink_factor)
end
--加上梯度,完成梯度下降
paramx:add(paramdx:mul(-params.lr))
end
local function run_valid() --测试交叉检验集
reset_state(state_valid)
g_disable_dropout(model.rnns)
--取消dropout,重新刷新一遍dropout浇诘?
local len = (state_valid.data:size(1) - 1) / (params.seq_length)
--分为len个seq_length长度来训练
local perp = 0
--perp困惑度暂时不懂 附上链接 http://blog.sina.com.cn/s/blog_4c9dc2a10102vua9.html
,用来评判训练后的质量
for i = 1, len do
perp = perp + fp(state_valid)
end
print("Validation set perplexity : " .. g_f3(torch.exp(perp / len)))
--设定dropout
g_enable_dropout(model.rnns)
end
local function run_test() --跑测试集
reset_state(state_test)
--用于测试取消dropout
g_disable_dropout(model.rnns
local perp = 0 --设定困惑度
local len = state_test.data:size(1)
g_replace_table(model.s[0], model.start_s)
for i = 1, (len - 1) do
local x = state_test.data[i] --入口词向量
local y = state_test.data[i + 1] --目标词向量
--由于是测试阶段所以只用保存两个s值,为s[0]与s[1]
perp_tmp, model.s[1] = unpack(model.rnns[1]:forward({x, y, model.s[0]}))
perp = perp + perp_tmp[1]
g_replace_table(model.s[0], model.s[1])
end
print("Test set perplexity : " .. g_f3(torch.exp(perp / (len - 1))))
g_enable_dropout(model.rnns)
end
最后主函数代码
local function main()
g_init_gpu(arg)
--获得数据
state_train = {data=transfer_data(ptb.traindataset(params.batch_size))}
state_valid = {data=transfer_data(ptb.validdataset(params.batch_size))}
state_test = {data=transfer_data(ptb.testdataset(params.batch_size))}
print("Network parameters:")
print(params)
local states = {state_train, state_valid, state_test}
for _, state in pairs(states) do
reset_state(state)
end
setup()
local step = 0
local epoch = 0 --训练次数
local total_cases = 0
local beginning_time = torch.tic()
local start_time = torch.tic()
print("Starting training.")
--words_per_step是一次fp,dp所训练词的数量
local words_per_step = params.seq_length * params.batch_size
--epoch_size是训练样本组的数量,由data.lua可知数据的形式为[a,batch_size]
local epoch_size = torch.floor(state_train.data:size(1) / params.seq_length)
local perps
--进行每次迭代
while epoch < params.max_max_epoch do
--fp函数
local perp = fp(state_train) --得到结果困惑度
if perps == nil then
perps = torch.zeros(epoch_size):add(perp)
end
perps[step % epoch_size + 1] = perp
step = step + 1
--dp反向传播
bp(state_train)
total_cases = total_cases + params.seq_length * params.batch_size
--epoch表示当前正在进行的迭代
epoch = step / epoch_size
if step % torch.round(epoch_size / 10) == 10 then
local wps = torch.floor(total_cases / torch.toc(start_time))
local since_beginning = g_d(torch.toc(beginning_time) / 60)
print('epoch = ' .. g_f3(epoch) ..
', train perp. = ' .. g_f3(torch.exp(perps:mean())) ..
', wps = ' .. wps ..
', dw:norm() = ' .. g_f3(model.norm_dw) ..
', lr = ' .. g_f3(params.lr) ..
', since beginning = ' .. since_beginning .. ' mins.')
end
if step % epoch_size == 0 then
run_valid()
if epoch > params.max_epoch then
--降低学习率
params.lr = params.lr / params.decay
end
end
if step % 33 == 0 then
cutorch.synchronize()
collectgarbage()
end
end
run_test()
print("Training is over.")
end