--[[ Training, adapted from https://github.com/harvardnlp/seq2seq-attn/blob/master/train.lua --]] torch.setheaptracking(true) require 'nn' require 'nngraph' require 'cudnn' require 'optim' require 'paths' package.path = package.path .. ';src/?.lua' .. ';src/data/?.lua' .. ';src/utils/?.lua' .. ';src/model/?.lua' require 'model' require 'data_gen' require 'logging' cmd = torch.CmdLine() -- Input and Output cmd:text('') cmd:text('**Input and Output**') cmd:text('') cmd:option('-data_base_dir', '/n/rush_lab/data/image_data/formula_images_crop_pad_down', [[The base directory of the image path in data-path. If the image path in data-path is absolute path, set it to /]]) cmd:option('-data_path', '/n/rush_lab/data/image_data/im2latex_train_large_filter.lst', [[The path containing data file names and labels. Format per line: image_path characters]]) cmd:option('-label_path', '/n/rush_lab/data/image_data/im2latex_formulas.norm4.final.lst', [[The path containing data file names and labels. Format per line: image_path characters]]) cmd:option('-val_data_path', '/n/rush_lab/data/image_data/im2latex_validate_large_filter.lst', [[The path containing validate data file names and labels. Format per line: image_path characters]]) cmd:option('-model_dir', 'model', [[The directory for saving and loading model parameters (structure is not stored)]]) cmd:option('-log_path', 'log.txt', [[The path to put log]]) cmd:option('-output_dir', 'results', [[The path to put visualization results if visualize is set to True]]) -- Display cmd:option('-steps_per_checkpoint', 100, [[Checkpointing (print perplexity, save model) per how many steps]]) cmd:option('-num_batches_val', math.huge, [[Number of batches to evaluate.]]) cmd:option('-beam_size', 1, [[Beam size.]]) cmd:option('-use_dictionary', false, [[Use dictionary during decoding or not.]]) cmd:option('-allow_digit_prefix', false, [[During decoding, allow arbitary digits before word.]]) cmd:option('-dictionary_path', '/n/rush_lab/data/image_data/train_dictionary.txt', [[The path containing dictionary. Format per line: word]]) -- Optimization cmd:text('') cmd:text('**Optimization**') cmd:text('') cmd:option('-num_epochs', 15, [[The number of whole data passes]]) cmd:option('-batch_size', 1, [[Batch size]]) cmd:option('-learning_rate', 0.1, [[Initial learning rate]]) cmd:option('-learning_rate_min', 0.00001, [[Initial learning rate]]) cmd:option('-lr_decay', 0.5, [[Decay learning rate by this much if (i) perplexity does not decrease on the validation set or (ii) epoch has gone past the start_decay_at_limit]]) cmd:option('-start_decay_at', 999, [[Start decay after this epoch]]) -- Network cmd:option('-dropout', 0.0, [[Dropout probability]]) -- does support dropout now!!! cmd:option('-target_embedding_size', 80, [[Embedding dimension for each target]]) cmd:option('-input_feed', false, [[Whether or not use LSTM attention decoder cell]]) cmd:option('-encoder_num_hidden', 256, [[Number of hidden units in encoder cell]]) cmd:option('-encoder_num_layers', 1, [[Number of hidden layers in encoder cell]]) -- does not support >1 now!!! cmd:option('-decoder_num_layers', 1, [[Number of hidden units in decoder cell]]) cmd:option('-vocab_file', '', [[Vocabulary file. A token per line.]]) -- Other cmd:option('-phase', 'test', [[train or test]]) cmd:option('-gpu_id', 1, [[Which gpu to use. <=0 means use CPU]]) cmd:option('-load_model', false, [[Load model from model-dir or not]]) cmd:option('-visualize', false, [[Print results or not]]) cmd:option('-seed', 910820, [[Load model from model-dir or not]]) cmd:option('-max_num_tokens', 150, [[Maximum number of output tokens]]) -- when evaluate, this is the cut-off length. cmd:option('-max_image_width', 500, [[Maximum length of input feature sequence along width direction]]) --800/2/2/2 cmd:option('-max_image_height', 160, [[Maximum length of input feature sequence along width direction]]) --80 / (2*2*2) cmd:option('-prealloc', false, [[Use memory preallocation and sharing between cloned encoder/decoders]]) opt = cmd:parse(arg) torch.manualSeed(opt.seed) math.randomseed(opt.seed) function train(model, phase, batch_size, num_epochs, train_data, val_data, model_dir, steps_per_checkpoint, num_batches_val, beam_size, visualize, output_dir, trie, learning_rate_init, lr_decay, start_decay_at) local loss = 0 local num_seen = 0 local num_samples = 0 local num_nonzeros = 0 local accuracy = 0 local forward_only local learning_rate = model.optim_state.learningRate or learning_rate_init learning_rate = math.max(learning_rate, opt.learning_rate_min) model.optim_state.learningRate = learning_rate logging:info(string.format('Lr: %f', learning_rate)) if phase == 'train' then forward_only = false elseif phase == 'test' then if visualize then model:vis(output_dir) end forward_only = true num_epochs = 1 model.global_step = 0 else assert(false, 'phase must be either train or test') end local prev_loss = nil local val_losses = {} for epoch = 1, num_epochs do if not forward_only then train_data:shuffle() end if epoch >= start_decay_at and learning_rate > opt.learning_rate_min then learning_rate = learning_rate*lr_decay model.optim_state.learningRate = math.max(learning_rate, opt.learning_rate_min) logging:info(string.format('Decay lr, current lr: %f', learning_rate)) end while true do train_batch = train_data:nextBatch(batch_size) if train_batch == nil then break end local real_batch_size = train_batch[1]:size()[1] local step_loss, stats = model:step(train_batch, forward_only, beam_size, trie) logging:info(string.format('%f', math.exp(step_loss/stats[1]))) num_seen = num_seen + 1 num_samples = num_samples + real_batch_size num_nonzeros = num_nonzeros + stats[1] if forward_only then --print (train_batch[1]:size(4)) accuracy = accuracy + stats[2] loss = loss + step_loss --print (stats[2]/real_batch_size) else loss = loss + step_loss end --print (loss/num_seen) model.global_step = model.global_step + 1 if model.global_step % steps_per_checkpoint == 0 then if forward_only then logging:info(string.format('Number of samples %d - Accuracy = %f', num_samples, accuracy/num_samples)) else logging:info(string.format('Step %d - training perplexity = %f', model.global_step, math.exp(loss/num_nonzeros))) logging:info('Saving model') local model_path = paths.concat(model_dir, string.format('model-%d', model.global_step)) local final_model_path_tmp = paths.concat(model_dir, '.final-model.tmp') local final_model_path = paths.concat(model_dir, 'final-model') if model.global_step % 1000 ~= 0 then model_path = final_model_path end model:save(model_path) logging:info(string.format('Model saved to %s', model_path)) os.execute(string.format('cp %s %s', model_path, final_model_path_tmp)) os.execute(string.format('mv %s %s', final_model_path_tmp, final_model_path)) num_seen = 0 num_nonzeros = 0 loss = 0 accuracy = 0 collectgarbage() end end end -- while true if not forward_only then -- Evaluate on val data logging:info(string.format('Evaluating model on %s batches of validation data', num_batches_val)) local val_loss = 0 local val_num_samples = 0 local val_num_nonzeros = 0 local val_accuracy = 0 local b = 1 while b <= num_batches_val do val_batch = val_data:nextBatch(batch_size) if val_batch == nil then if num_batches_val < math.huge then val_data:shuffle() else break end else local real_batch_size = val_batch[1]:size()[1] b = b+1 local step_loss, stats = model:step(val_batch, true, beam_size, trie) val_loss = val_loss + step_loss val_num_samples = val_num_samples + real_batch_size val_num_nonzeros = val_num_nonzeros + stats[1] val_accuracy = val_accuracy + stats[2] end end val_losses[epoch] = val_loss logging:info(string.format('Epoch: %d Step %d - Val Accuracy = %f Perp = %f', epoch, model.global_step, val_accuracy/val_num_samples, math.exp(val_loss/val_num_nonzeros))) if val_losses[epoch-1] ~= nil and val_losses[epoch] > val_losses[epoch-1] and learning_rate > opt.learning_rate_min then learning_rate = learning_rate*lr_decay model.optim_state.learningRate = math.max(learning_rate, opt.learning_rate_min) logging:info(string.format('Decay lr, current lr: %f', learning_rate)) end prev_loss = loss logging:info('Saving model') local model_path = paths.concat(model_dir, string.format('model-%d', model.global_step)) local final_model_path_tmp = paths.concat(model_dir, '.final-model.tmp') local final_model_path = paths.concat(model_dir, 'final-model') model:save(model_path) logging:info(string.format('Model saved to %s', model_path)) os.execute(string.format('cp %s %s', model_path, final_model_path_tmp)) os.execute(string.format('mv %s %s', final_model_path_tmp, final_model_path)) else logging:info(string.format('Epoch: %d Number of samples %d - Accuracy = %f Perp = %f', epoch, num_samples, accuracy/num_samples, math.exp(loss/num_nonzeros))) end end -- for epoch end function main() logging = logger(opt.log_path) logging:info('Command Line Arguments:') logging:info(table.concat(arg, ' ')) logging:info('End Command Line Arguments') local phase= opt.phase local batch_size = opt.batch_size local num_epochs = opt.num_epochs local model_dir = opt.model_dir local load_model = opt.load_model local steps_per_checkpoint = opt.steps_per_checkpoint local num_batches_val = opt.num_batches_val local beam_size = opt.beam_size local gpu_id = opt.gpu_id local seed = opt.seed local visualize = opt.visualize local output_dir = opt.output_dir opt.max_decoder_l = opt.max_num_tokens+1 opt.max_encoder_l_w = math.floor(opt.max_image_width / 8.0) opt.max_encoder_l_h = math.floor(opt.max_image_height / 8.0) if gpu_id > 0 then logging:info(string.format('Using CUDA on GPU %d', gpu_id)) require 'cutorch' require 'cunn' cutorch.setDevice(gpu_id) cutorch.manualSeed(seed) use_cuda = true else logging:info(string.format('Using CPU')) use_cuda = false end -- Build model logging:info('Building model') local model = Model() local final_model = paths.concat(model_dir, 'final-model') if load_model and paths.filep(final_model) then logging:info(string.format('Loading model from %s', final_model)) model:load(final_model, opt) else logging:info('Creating model with fresh parameters') logging:info(string.format('Loading vocab from %s', opt.vocab_file)) -- Load Vocab id2vocab = tds.Hash() local file, err = io.open(opt.vocab_file, "r") if err then logging:info(string.format('ERROR: vocab file %s does not exist!', opt.vocab_file)) os.exit() end for line in file:lines() do local vocab = trim(line) if string.len(vocab) == 0 then vocab = ' ' end id2vocab[#id2vocab+1] = vocab end opt.target_vocab_size = #id2vocab+4 model:create(opt) end if not paths.dirp(model_dir) then paths.mkdir(model_dir) end if visualize then if not paths.dirp(output_dir) then paths.mkdir(output_dir) end end -- Load data logging:info(string.format('Data base dir %s', opt.data_base_dir)) logging:info(string.format('Load training data from %s', opt.data_path)) local train_data = DataGen(opt.data_base_dir, opt.data_path, opt.label_path, 10.0, opt.max_encoder_l_h, opt.max_encoder_l_w, opt.max_decoder_l) logging:info(string.format('Training data loaded from %s', opt.data_path)) local val_data if phase == 'train' then logging:info(string.format('Load validation data from %s', opt.val_data_path)) val_data = DataGen(opt.data_base_dir, opt.val_data_path, opt.label_path, 10.0, opt.max_encoder_l_h, opt.max_encoder_l_w, opt.max_decoder_l) logging:info(string.format('Validation data loaded from %s', opt.val_data_path)) end local trie = nil if opt.use_dictionary then logging:info(string.format('Load dictionary from %s', opt.dictionary_path)) trie = loadDictionary(opt.dictionary_path, opt.allow_digit_prefix) end train(model, phase, batch_size, num_epochs, train_data, val_data, model_dir, steps_per_checkpoint, num_batches_val, beam_size, visualize, output_dir, trie, opt.learning_rate, opt.lr_decay, opt.start_decay_at) logging:shutdown() model:shutdown() end main()