LSTM代码初解析,Torch平台

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

你可能感兴趣的:(Torch)