├── utils ├── __init__.py ├── external │ ├── __init__.py │ ├── dqn.py │ ├── reader.py │ ├── glad_evaluate.py │ ├── dialog_config_v2.py │ ├── glad_preprocess.py │ ├── bleu.py │ └── dialog_constants.py ├── internal │ ├── __init__.py │ ├── clock.py │ ├── vocabulary.py │ ├── ontology.py │ ├── arguments.py │ └── initialization.py ├── webtools │ ├── favicon.ico │ ├── style.css │ └── nlgWebsite.js ├── index.html └── survey.html ├── objects ├── __init__.py ├── blocks │ ├── __init__.py │ ├── readme.txt │ ├── basics.py │ ├── datasets.py │ └── attention.py ├── models │ ├── __init__.py │ ├── user_model.py │ ├── seq2seq.py │ ├── glad.py │ └── transformer.py ├── modules │ ├── __init__.py │ └── belief_tracker.py └── components.py ├── operators ├── __init__.py ├── learn │ ├── supervise.py │ ├── __init__.py │ └── builder.py ├── preprocess │ ├── __init__.py │ ├── preprocessor.py │ └── loader.py ├── evaluate │ ├── __init__.py │ ├── server.py │ ├── tester.py │ └── evaluator.py └── system.py ├── scripts ├── baseline │ ├── __init__.py │ ├── oracle_performance.xlsx │ └── tf_idf.py ├── 1_info_retrieval.py ├── _config.yml ├── example_seq2seq.xlsx ├── seq2seq_ex.txt ├── split_intent_sv.py ├── vocab_from_valueset.py ├── oracle.py ├── verify_frame.py ├── find_vocab_res.py ├── extract_cuisines.py ├── evaluation.py ├── retrieve_examples.py ├── car_data_io.py ├── match_feature_digits.py ├── accuracy.py ├── find_vocab_car.py ├── 3_advanced.py └── 2_neural_based.py ├── execute ├── server.sh ├── converse.sh ├── manage_policy.sh ├── track_intent.sh ├── generate_text.sh ├── attention.sh ├── basic.sh └── end_to_end.sh ├── .gitignore ├── run.py ├── environment.yml └── README.md /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /objects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /operators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/baseline/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/external/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/internal/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/1_info_retrieval.py: -------------------------------------------------------------------------------- 1 | stub 1 -------------------------------------------------------------------------------- /scripts/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /utils/webtools/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekchen14/task_oriented_dialog_agent/HEAD/utils/webtools/favicon.ico -------------------------------------------------------------------------------- /operators/learn/supervise.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | class Supervisor(object): 4 | def __init__(self, args): 5 | pass 6 | -------------------------------------------------------------------------------- /scripts/example_seq2seq.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekchen14/task_oriented_dialog_agent/HEAD/scripts/example_seq2seq.xlsx -------------------------------------------------------------------------------- /operators/preprocess/__init__.py: -------------------------------------------------------------------------------- 1 | from operators.preprocess.preprocessor import PreProcessor 2 | from operators.preprocess.loader import DataLoader -------------------------------------------------------------------------------- /objects/blocks/__init__.py: -------------------------------------------------------------------------------- 1 | from objects.blocks.attention import Attention, DoubleAttention 2 | from objects.blocks.datasets import Dataset, Dialogue, Turn 3 | -------------------------------------------------------------------------------- /scripts/baseline/oracle_performance.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/derekchen14/task_oriented_dialog_agent/HEAD/scripts/baseline/oracle_performance.xlsx -------------------------------------------------------------------------------- /scripts/seq2seq_ex.txt: -------------------------------------------------------------------------------- 1 | hi 2 | i want to book a french restaurant 3 | in new york city 4 | we have four people 5 | what do you recommend? 6 | i see. thank you. 7 | bye -------------------------------------------------------------------------------- /operators/learn/__init__.py: -------------------------------------------------------------------------------- 1 | from operators.learn.builder import Builder 2 | from operators.learn.learner import Learner 3 | from operators.learn.supervise import Supervisor -------------------------------------------------------------------------------- /operators/evaluate/__init__.py: -------------------------------------------------------------------------------- 1 | from operators.evaluate.evaluator import Evaluator 2 | from operators.evaluate.monitor import LossMonitor, RewardMonitor 3 | from operators.evaluate.tester import Tester -------------------------------------------------------------------------------- /execute/server.sh: -------------------------------------------------------------------------------- 1 | python run.py --task end_to_end --model ddq --max-turn 40 -e 10 \ 2 | --pool-size 5000 --prefix saver_ --suffix _15 --report-qual --verbose \ 3 | --epsilon 0.0 --hidden-dim 80 --seed 16 --dataset ddq/movies \ 4 | --early-stop success_rate --test-mode --user turk --seed 34 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | scr 3 | log 4 | .ipynb_checkpoints 5 | Makefile 6 | aws_config.json 7 | cocoa.egg-info 8 | *.pyc 9 | *.log 10 | *.pkl 11 | *.json 12 | *.png 13 | *.pdf 14 | cl*.sh 15 | aws_config.json 16 | *.DS_Store 17 | datasets 18 | *.pem 19 | results 20 | *.pt 21 | *.csv 22 | .idea 23 | *.docx 24 | -------------------------------------------------------------------------------- /objects/models/__init__.py: -------------------------------------------------------------------------------- 1 | from objects.models.transformer import Transformer, TransformerXL 2 | from objects.models.memory import EntNet, NPN #, ProStruct 3 | from objects.models.glad import GLAD 4 | from objects.models.seq2seq import Seq2Seq 5 | from objects.models.ddq import DQN, NLU, NLG 6 | from objects.models.policies import * 7 | 8 | -------------------------------------------------------------------------------- /utils/webtools/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: lightblue; 3 | } 4 | 5 | .chat { 6 | padding: 0.5em; 7 | font-family: Tahoma, Geneva, sans-serif; 8 | } 9 | .user { 10 | color:blue; 11 | } 12 | .agent { 13 | color:black; 14 | } 15 | .agent.dialogue { 16 | text-align: right; 17 | padding-right: 2em; 18 | } 19 | 20 | .removable { 21 | display: none; 22 | } 23 | 24 | .conversation { 25 | margin: 4em 0em; 26 | } -------------------------------------------------------------------------------- /objects/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from objects.modules.belief_tracker import RuleBeliefTracker, NeuralBeliefTracker 2 | from objects.modules.policy_manager import RulePolicyManager, NeuralPolicyManager 3 | from objects.modules.text_generator import RuleTextGenerator, NeuralTextGenerator 4 | from objects.modules.kb_operator import KBHelper 5 | from objects.modules.dialog_manager import DialogManager 6 | from objects.modules.user import RuleSimulator, NeuralSimulator, CommandLineUser 7 | from objects.modules.user import MechanicalTurkUser 8 | 9 | -------------------------------------------------------------------------------- /utils/external/dqn.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Jun 18, 2016 3 | 4 | @author: xiul 5 | ''' 6 | 7 | import numpy as np 8 | import math 9 | 10 | 11 | def initWeight(n,d): 12 | scale_factor = math.sqrt(float(6)/(n + d)) 13 | #scale_factor = 0.1 14 | return (np.random.rand(n,d)*2-1)*scale_factor 15 | 16 | """ for all k in d0, d0 += d1 . d's are dictionaries of key -> numpy array """ 17 | def mergeDicts(d0, d1): 18 | for k in d1: 19 | if k in d0: 20 | d0[k] += d1[k] 21 | else: 22 | d0[k] = d1[k] -------------------------------------------------------------------------------- /scripts/split_intent_sv.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pdb 3 | 4 | with open("label_vocab.json", "r") as f: 5 | vocab = json.load(f) 6 | 7 | intent = set() 8 | sv = set() 9 | for row in vocab["ordered_values"]: 10 | isv = row.split("(") 11 | intent.add(isv[0]) 12 | if len(isv) > 1: 13 | sv.add(isv[1][:-1]) # everything except closing ")" symbol 14 | else: 15 | sv.add("None") 16 | 17 | json.dump(list(intent), open("intent_vocab.json", "w") ) 18 | json.dump(list(sv), open("sv_vocab.json", "w") ) 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /execute/converse.sh: -------------------------------------------------------------------------------- 1 | python run.py --task end_to_end --model ddq --max-turn 40 --seed 35 \ 2 | --hidden-dim 80 --metrics success_rate avg_reward \ 3 | --user command --early-stop success_rate --dataset ddq/movies \ 4 | --prefix saver_ --suffix _15 --report-qual --verbose --test-mode 5 | 6 | # python run.py --task end_to_end --model ddq --max-turn 40 -e 7 \ 7 | # --epsilon 0.0 --hidden-dim 80 --batch-size 16 --seed 15 \ 8 | # --user simulate --early-stop success_rate --dataset ddq/movies \ 9 | # --prefix saver_ --suffix _15 --report-qual --verbose --debug --test-mode 10 | -------------------------------------------------------------------------------- /utils/external/reader.py: -------------------------------------------------------------------------------- 1 | """ 2 | @authors: xiul, t-zalipt, antoinne 3 | """ 4 | 5 | def get_glove_name(opt, type_="tokens", key="pt"): 6 | emb_type = getattr(opt, key) 7 | if type_ == "tokens": 8 | return "data/{}/tokens.{}{}.vocab".format(emb_type, emb_type, opt.iSize) 9 | else: 10 | return "data/{}/entities.{}{}.vocab".format(emb_type, emb_type, opt.eSize) 11 | 12 | 13 | def text_to_dict(path): 14 | """ Read in a text file as a dictionary 15 | where keys are text and values are indices (line numbers) """ 16 | 17 | slot_set = {} 18 | with open(path, 'r') as f: 19 | index = 0 20 | for line in f.readlines(): 21 | slot_set[line.strip('\n').strip('\r')] = index 22 | index += 1 23 | return slot_set -------------------------------------------------------------------------------- /scripts/vocab_from_valueset.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | 3 | vocab = set() 4 | 5 | value_set = pickle.load( open("value_set.p", "rb"), encoding="latin1") 6 | for slot, values in value_set.items(): 7 | vocab.add(slot) 8 | for val in values: 9 | val = val.replace(".", " ") 10 | val = val.replace(",", " ") 11 | val = val.replace("!", " ") 12 | val = val.replace("#", " ") 13 | val = val.replace("=", " ") 14 | val = val.replace("}", " ") 15 | val = val.replace("{", " ") 16 | for token in val.lower().split(): 17 | if len(token) <= 14: 18 | vocab.add(token) 19 | 20 | processed = list(vocab) 21 | print(len(processed)) 22 | 23 | print(processed[10:14]) 24 | print(processed[40:50]) 25 | print(processed[140:150]) 26 | 27 | import pdb 28 | pdb.set_trace() 29 | 30 | import json 31 | 32 | json.dump(processed, open("vocab.json", "w")) 33 | -------------------------------------------------------------------------------- /utils/internal/clock.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import time as tm 3 | import math 4 | from tqdm import tqdm as progress_bar 5 | 6 | def asMinutes(s): 7 | m = math.floor(s / 60) 8 | s -= m * 60 9 | return '%dm %ds' % (m, s) 10 | 11 | def timeSince(since, percent): 12 | now = tm.time() 13 | seconds_passed = now - since 14 | estimated_seconds = seconds_passed / (percent / 100) 15 | remaining_seconds = estimated_seconds - seconds_passed 16 | return '(%s remaining)' % (asMinutes(remaining_seconds)) 17 | 18 | def time_past(since): 19 | now = tm.time() 20 | minutes_passed = (now - since)/60.0 21 | print('{0:.2f} minutes '.format(minutes_passed)) 22 | 23 | def starting_checkpoint(epoch, epochs, use_cuda): 24 | if epoch == 0: 25 | if use_cuda: 26 | print("Starting to train on GPUs on epoch {}... ".format(epoch+1)) 27 | else: 28 | print("Start local CPU training on epoch {} ... ".format(epoch+1)) 29 | else: 30 | print("Continuing on epoch {} of {} ...".format(epoch+1, epochs)) 31 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import torch 3 | import sys 4 | from random import seed 5 | 6 | from utils.internal.arguments import solicit_args 7 | from operators.preprocess import DataLoader, PreProcessor 8 | from operators.learn import Builder, Learner 9 | from operators.evaluate import Evaluator 10 | from operators.system import SingleSystem, EndToEndSystem 11 | 12 | if __name__ == "__main__": 13 | args = solicit_args() 14 | # ------ BASIC SYSTEM SETUP ------ 15 | torch.manual_seed(args.seed) 16 | seed(args.seed) 17 | # ----- INITIALIZE OPERATORS ----- 18 | loader = DataLoader(args) 19 | builder = Builder(args, loader) 20 | processor = PreProcessor(args, loader) 21 | evaluator = Evaluator(args, processor) 22 | # -------- RUN THE SYSTEM -------- 23 | if args.task == "end_to_end": 24 | system = EndToEndSystem(args, loader, builder, processor, evaluator) 25 | else: 26 | system = SingleSystem(args, loader, builder, processor, evaluator) 27 | if not args.test_mode: 28 | system.run_main() 29 | system.monitor.logger.info(args) 30 | system.evaluate(args.test_mode) -------------------------------------------------------------------------------- /execute/manage_policy.sh: -------------------------------------------------------------------------------- 1 | CUDA_VISIBLE_DEVICES=0 python run.py --gpu=0 --task manage_policy --model ddq --max-turn 40 -e 500 \ 2 | --epsilon 0.0 --hidden-dim 80 --batch-size 16 --seed 14 --warm-start \ 3 | --user simulate --metrics success_rate avg_reward --dataset ddq/movies \ 4 | --learning-rate 1e-3 --optimizer rmsprop --weight-decay 0.0 \ 5 | --pool-size 5000 --prefix matches_snafu_ --suffix _14 --verbose 6 | # --early-stop success_rate --use-existing --report-qual 7 | 8 | # CUDA_VISIBLE_DEVICES=0 python run.py --gpu=0 --task manage_policy --model ddq --max-turn 40 -e 500 \ 9 | # --epsilon 0.0 --hidden-dim 80 --batch-size 16 --seed 15 --warm-start \ 10 | # --user simulate --metrics success_rate avg_reward --dataset ddq/movies \ 11 | # --learning-rate 1e-3 --optimizer rmsprop --weight-decay 0.0 \ 12 | # --pool-size 5000 --prefix matches_snafu_ --suffix _15 --verbose 13 | 14 | # CUDA_VISIBLE_DEVICES=0 python run.py --gpu=0 --task manage_policy --model ddq --max-turn 40 -e 500 \ 15 | # --epsilon 0.0 --hidden-dim 80 --batch-size 16 --seed 16 --warm-start \ 16 | # --user simulate --metrics success_rate avg_reward --dataset ddq/movies \ 17 | # --learning-rate 1e-3 --optimizer rmsprop --weight-decay 0.0 \ 18 | # --pool-size 5000 --prefix matches_snafu_ --suffix _16 --verbose 19 | 20 | 21 | # python run.py --task policy --model rulebased --report-quant -e 5 \ 22 | # --user simulate --metrics success_rate avg_reward --dataset ddq/movies \ 23 | # --batch-size 16 --verbose --debug 24 | -------------------------------------------------------------------------------- /utils/internal/vocabulary.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import pdb, sys 4 | 5 | class Vocabulary(object): 6 | def __init__(self, args, data_dir, kind=None): 7 | self.vocab = self.load_words(data_dir) 8 | labels = self.load_labels(data_dir, args.task) 9 | self.label_vocab = labels[kind] if kind is not None else labels 10 | 11 | def load_words(self, path): 12 | with open("{}/vocab.json".format(path), "r") as f: 13 | vocab = json.load(f) 14 | print("{} vocab loaded!".format(path)) 15 | return vocab 16 | def load_labels(self, path, task): 17 | label_path = "{}/{}.json".format(path, task) 18 | if os.path.exists(label_path): 19 | with open(label_path, "r") as f: 20 | labels = json.load(f) 21 | print("{} labels loaded!".format(label_path)) 22 | return labels 23 | else: 24 | return self.vocab 25 | 26 | def word_to_index(self, token): 27 | return self.vocab.index(token) 28 | def label_to_index(self, token): 29 | return self.label_vocab.index(token) 30 | 31 | def index_to_word(self, idx): 32 | return self.vocab[idx] 33 | def index_to_label(self, idx): 34 | return self.label_vocab[idx] 35 | 36 | def ulary_size(self): 37 | return len(self.vocab) 38 | def label_size(self): 39 | return len(self.label_vocab) 40 | 41 | ''' 42 | special_tokens = ["", "","","", ... , "", 43 | "UNK", "SOS", "EOS", "api_call","poi", "addr"] 44 | UNK_token = 15 45 | SOS_token = 16 46 | EOS_token = 17 47 | PHONE_token = 19 48 | POI_token = 19 49 | ADDR_token = 20 50 | ''' -------------------------------------------------------------------------------- /objects/models/user_model.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.optim as optim 4 | import torch.nn.functional as F 5 | 6 | from torch.autograd import Variable 7 | 8 | 9 | class SimulatorModel(nn.Module): 10 | def __init__(self, 11 | agent_action_size, 12 | hidden_size, 13 | state_size, 14 | user_action_size, 15 | reward_size=1, 16 | termination_size=1): 17 | super(SimulatorModel, self).__init__() 18 | 19 | self.linear_i2h = nn.Linear(state_size, hidden_size) 20 | self.agent_emb = nn.Embedding(agent_action_size, hidden_size) 21 | self.linear_h2r = nn.Linear(hidden_size, reward_size) 22 | self.linear_h2t = nn.Linear(hidden_size, termination_size) 23 | self.linear_h2a = nn.Linear(hidden_size, user_action_size) 24 | 25 | def forward(self, s, a): 26 | h_s = self.linear_i2h(s) 27 | h_a = self.agent_emb(a).squeeze(1) 28 | h = torch.tanh(h_s + h_a) 29 | 30 | reward = self.linear_h2r(h) 31 | term = self.linear_h2t(h) 32 | action = F.log_softmax(self.linear_h2a(h), 1) 33 | 34 | return reward, term, action 35 | 36 | def predict(self, s, a): 37 | h_s = self.linear_i2h(s) 38 | h_a = self.agent_emb(a).squeeze(1) 39 | h = torch.tanh(h_s + h_a) 40 | 41 | reward = self.linear_h2r(h) 42 | term = torch.sigmoid(self.linear_h2t(h)) 43 | action = F.log_softmax(self.linear_h2a(h), 1) 44 | 45 | return reward, term, action.argmax(1) 46 | -------------------------------------------------------------------------------- /utils/external/glad_evaluate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import logging 4 | from argparse import ArgumentParser, Namespace 5 | from utils import load_dataset, load_model 6 | 7 | 8 | if __name__ == '__main__': 9 | parser = ArgumentParser() 10 | parser.add_argument('dsave', help='save location of model') 11 | parser.add_argument('--split', help='split to evaluate on', default='dev') 12 | parser.add_argument('--gpu', type=int, help='gpu to use', default=None) 13 | parser.add_argument('--fout', help='optional save file to store the predictions') 14 | args = parser.parse_args() 15 | 16 | logging.basicConfig(level=logging.INFO) 17 | 18 | with open(os.path.join(args.dsave, 'config.json')) as f: 19 | args_save = Namespace(**json.load(f)) 20 | args_save.gpu = args.gpu 21 | pprint(args_save) 22 | 23 | dataset, ontology, vocab, Eword = load_dataset() 24 | 25 | model = load_model(args_save.model, args_save, ontology, vocab) 26 | model.load_best_save(directory=args.dsave) 27 | if args.gpu is not None: 28 | model.cuda(args.gpu) 29 | 30 | logging.info('Making predictions for {} dialogues and {} turns'.format(len(dataset[args.split]), len(list(dataset[args.split].iter_turns())))) 31 | preds = model.run_pred(dataset[args.split], args_save) 32 | dataset[args.split].evaluate_preds(preds) 33 | 34 | if args.fout: 35 | with open(args.fout, 'wt') as f: 36 | # predictions is a list of sets, need to convert to list of lists to make it JSON serializable 37 | json.dump([list(p) for p in preds], f, indent=2) 38 | -------------------------------------------------------------------------------- /execute/track_intent.sh: -------------------------------------------------------------------------------- 1 | # Unit test for original GLAD 2 | CUDA_VISIBLE_DEVICES=0 python run.py --gpu=0 --task track_intent \ 3 | --model glad --learning-rate 1e-3 --hidden-dim 200 --embedding-size 400 \ 4 | --epochs 50 --threshold 0.3 --optimizer adam --dataset woz2 \ 5 | --early-stop joint_goal --save-model \ 6 | --prefix apr30_ --suffix _unittest_14 --seed 14 --verbose 7 | 8 | CUDA_VISIBLE_DEVICES=0 python run.py --gpu=0 --task track_intent \ 9 | --model glad --learning-rate 1e-3 --hidden-dim 200 --embedding-size 400 \ 10 | --epochs 50 --threshold 0.3 --optimizer adam --dataset woz2 \ 11 | --early-stop joint_goal --save-model \ 12 | --prefix apr30_ --suffix _unittest_15 --seed 14 --verbose 13 | 14 | # Hyperparameter Tuning 15 | # CUDA_VISIBLE_DEVICES=1 python run.py --gpu=1 --task track_intent \ 16 | # --model glad --learning-rate 3e-4 --hidden-dim 200 --embedding-size 400 \ 17 | # --epochs 40 --threshold 0.3 --optimizer adam --dataset e2e/movies \ 18 | # --early-stop joint_goal --save-model \ 19 | # --prefix nlu_ --suffix _lr3e3 --seed 14 --verbose 20 | # CUDA_VISIBLE_DEVICES=0 python run.py --gpu=0 --task track_intent --dataset woz2 \ 21 | # --model glad --learning-rate 3e-4 --hidden-dim 200 --embedding-size 400 \ 22 | # --epochs 50 --threshold 0.3 --optimizer adam --early-stop success_rate \ 23 | # --prefix Apr_08_ --suffix _1 --seed 14 # --debug 24 | # CUDA_VISIBLE_DEVICES=0 python run.py --gpu=0 --task track_intent \ 25 | # --model glad --learning-rate 3e-4 --hidden-dim 200 --embedding-size 400 \ 26 | # --epochs 1 --threshold 0.3 --optimizer adam --dataset e2e/movies \ 27 | # --early-stop joint_goal --report-qual --test-mode \ 28 | # --prefix nlu_ --suffix _apr9 --seed 14 --verbose # --use-existing 29 | -------------------------------------------------------------------------------- /execute/generate_text.sh: -------------------------------------------------------------------------------- 1 | python 3_advanced.py --task-name challenge --model-type copy \ 2 | --report-results --results-path results/mile1a.csv \ 3 | --n-iters 30000 --decay-times 4 --optimizer Adam 4 | python 3_advanced.py --task-name challenge --model-type copy \ 5 | --report-results --results-path results/mile1b.csv \ 6 | --n-iters 30000 --decay-times 4 --optimizer Adam 7 | 8 | python 3_advanced.py --task-name challenge --model-type copy \ 9 | --report-results --results-path results/mile2a.csv \ 10 | --weight-decay 0.01 --drop-prob 0.1 --optimizer Adam 11 | python 3_advanced.py --task-name challenge --model-type copy \ 12 | --report-results --results-path results/mile2b.csv \ 13 | --weight-decay 0.01 --drop-prob 0.1 --optimizer Adam 14 | 15 | python 3_advanced.py --task-name challenge --model-type copy \ 16 | --report-results --results-path results/mile3a.csv \ 17 | --drop-prob 0.2 --optimizer Adam --attn-method dot 18 | python 3_advanced.py --task-name challenge --model-type copy \ 19 | --report-results --results-path results/mile3b.csv \ 20 | --drop-prob 0.2 --optimizer Adam --attn-method dot 21 | 22 | python 3_advanced.py --task-name challenge --model-type copy \ 23 | --report-results --results-path results/mile4a.csv \ 24 | --drop-prob 0.3 --optimizer Adam --attn-method luong 25 | python 3_advanced.py --task-name challenge --model-type copy \ 26 | --report-results --results-path results/mile4b.csv \ 27 | --drop-prob 0.3 --optimizer Adam --attn-method luong 28 | 29 | python 3_advanced.py --task-name challenge --model-type copy \ 30 | --report-results --results-path results/mile5a.csv \ 31 | --weight-decay 0.01 --drop-prob 0.2 --optimizer Adam 32 | python 3_advanced.py --task-name challenge --model-type copy \ 33 | --report-results --results-path results/mile5b.csv \ 34 | --weight-decay 0.01 --drop-prob 0.2 --optimizer Adam 35 | -------------------------------------------------------------------------------- /utils/internal/ontology.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import numpy as np 4 | import pickle as pkl 5 | 6 | class Ontology: 7 | 8 | def __init__(self, acts=None, slots=None, relations=None, values=None, num=None): 9 | self.acts = acts or [] 10 | self.slots = slots or [] 11 | self.relations = relations or [] 12 | self.values = values or {} 13 | self.num = num or {} 14 | 15 | self.feasible_agent_actions = None 16 | self.feasible_user_actions = None 17 | 18 | def __add__(self, another): 19 | new_acts = sorted(list(set(self.acts + another.acts))) 20 | new_slots = sorted(list(set(self.slots + another.slots))) 21 | new_relations = sorted(list(set(self.relations + another.relations))) 22 | new_values = {s: sorted(list(set( 23 | self.values.get(s, []) + another.values.get(s, [])))) for s in new_slots} 24 | return Ontology(new_acts, new_slots, new_relations, new_values) 25 | 26 | def __radd__(self, another): 27 | # reflective add, which flips the ordering of adding operations 28 | return self if another == 0 else self.__add__(another) 29 | 30 | def to_dict(self): 31 | return {'acts': self.acts, 'relations': self.relations, 32 | 'slots': self.slots, 'values': self.values } 33 | 34 | @classmethod 35 | def from_dict(cls, d): 36 | return cls(**d) 37 | 38 | @classmethod 39 | def from_path(cls, path): 40 | ont = json.load(open(os.path.join(path, "ontology.json"), "r")) 41 | 42 | data = {} 43 | data['acts'] = ont['dialogue_acts'] 44 | 45 | if 'slots' in ont.keys(): 46 | data['slots'] = ont['slots'] 47 | data['values'] = ont['values'] 48 | else: 49 | data['slots'] = list(ont['slot_values'].keys()) 50 | data['values'] = ont['slot_values'] 51 | 52 | if 'relations' in ont.keys(): 53 | data['relations'] = ont['relations'] 54 | if 'vectorized' in ont.keys(): 55 | data['num'] = ont['vectorized'] 56 | 57 | return cls(**data) 58 | -------------------------------------------------------------------------------- /objects/blocks/readme.txt: -------------------------------------------------------------------------------- 1 | I. Restaurant Domain: 2 | - reservation subdomain 3 | inform dialogue act 4 | inform(area=north), inform(area=east), inform(area=south) 5 | inform(price=cheap), inform(price=moderate), inform(price=expensive) 6 | inform(food=chinese), inform(food=italian), inform(food=japanese) 7 | question dialogue act 8 | question(address=the_missing_sock), question(address=il_casaro) 9 | question(phone=the_missing_sock), question(phone=il_casaro) 10 | question(rating=the_missing_sock), question(rating=il_casaro) 11 | request dialogue act 12 | request(reserve=the_missing_sock), request(reserve=il_casaro) 13 | request(call=the_missing_sock), request(call=il_casaro) 14 | - menu subdomain 15 | inform dialogue act 16 | inform(dish=kung_pao_chicken), inform(dish=pepperoni_pizza) 17 | inform(drink=lemonade), inform(drink=diet_soda), inform(drink=water) 18 | inform(appetizer=takoyaki), inform(appetizer=green_gem_salad) 19 | question dialogue act 20 | question(vegetarian=the_missing_sock), question(vegetarian=il_casaro) 21 | II. Transportation Domain: 22 | - traffic subdomain 23 | question dialogue act 24 | question(location=101_freeway), question(location=san_mateo) 25 | question(time=now), question(time=tomorrow), question(time=1700) 26 | inform dialogue act 27 | inform(time=night), inform(start_loc=current), inform(end_loc=san_jose) 28 | - weather subdomain 29 | question dialgoue act 30 | question(location=mountain_view), question(location=san_mateo) 31 | question(time=now), question(time=tomorrow), question(time=1700) 32 | III. Hotel Domain: 33 | - reservation subdomain 34 | - front desk subdomain 35 | IV. Calendar Domain: 36 | - meetings subdomain (for business) 37 | - event subdomain (for pleasure) 38 | V. Airport Domain: 39 | - reservation subdomain 40 | - airline subdomain 41 | VI. Internet Service Provider Domain: 42 | - sales subdomain 43 | - customer support subdomain 44 | - troubleshoot subdomain -------------------------------------------------------------------------------- /objects/models/seq2seq.py: -------------------------------------------------------------------------------- 1 | import random 2 | import numpy as np 3 | from torch import nn 4 | 5 | class Seq2Seq(nn.Module): 6 | def __init__(self, encoder, decoder): 7 | super(Seq2Seq, self).__init__() 8 | self.encoder = encoder.to(device) 9 | self.decoder = decoder.to(device) 10 | self.type = "seq2seq" 11 | self.arguments_size = decoder.arguments_size 12 | 13 | def flatten_parameters(self): 14 | self.encoder.rnn.flatten_parameters() 15 | self.decoder.rnn.flatten_parameters() 16 | 17 | def forward(self, sources, targets, enc_hidden, enc_length, dec_length, track): 18 | loss, predictions, visual, teach_ratio = track 19 | self.flatten_parameters() 20 | encoder_outputs, enc_hidden = self.encoder(sources, enc_hidden) 21 | 22 | dec_hidden = enc_hidden 23 | decoder_input = var([[vocab.SOS_token]], "long") 24 | decoder_context = var(torch.zeros(1, 1, decoder.hidden_size)) 25 | 26 | for di in range(dec_length): 27 | use_teacher_forcing = random.random() < teach_ratio 28 | if self.arguments_size == "large": 29 | dec_output, dec_context, dec_hidden, attn_weights = self.decoder( 30 | decoder_input, dec_context, dec_hidden, encoder_outputs, 31 | sources, targets, di, use_teacher_forcing) 32 | elif self.arguments_size == "medium": 33 | dec_output, dec_context, dec_hidden, attn_weights = self.decoder( 34 | decoder_input, dec_context, dec_hidden, encoder_outputs) 35 | elif self.arguments_size == "small": 36 | dec_output, dec_context = self.decoder(decoder_input, dec_context) 37 | attn_weights, visual = False, False 38 | 39 | # visual[:, di] = attn_weights.squeeze(0).squeeze(0).cpu().data 40 | loss += criterion(dec_output, targets[di]) 41 | 42 | if use_teacher_forcing: 43 | decoder_input = targets[di] 44 | else: # Use the predicted word as the next input 45 | topv, topi = dec_output.data.topk(1) 46 | ni = topi[0][0] 47 | predictions.append(ni) 48 | if ni == vocab.EOS_token: 49 | break 50 | decoder_input = var([[ni]], "long") 51 | 52 | return loss, predictions, visual -------------------------------------------------------------------------------- /execute/attention.sh: -------------------------------------------------------------------------------- 1 | python 3_advanced.py --task-name challenge --model-type transformer \ 2 | --optimizer Adam --n-layers 6 --n-iters 30000 --save-model \ 3 | --encoder-path results/enc_transformer_1a.pt --decoder-path results/dec_transformer_1a.pt \ 4 | --report-results --results-path results/transformer_1a.csv -lr 0.0158 5 | python 3_advanced.py --task-name challenge --model-type transformer \ 6 | --optimizer Adam --n-layers 6 --n-iters 30000 --save-model \ 7 | --encoder-path results/enc_transformer_1b.pt --decoder-path results/dec_transformer_1b.pt \ 8 | --report-results --results-path results/transformer_1b.csv -lr 0.0158 9 | 10 | python 3_advanced.py --task-name challenge --model-type transformer \ 11 | --optimizer Adam --n-layers 6 --n-iters 30000 --save-model \ 12 | --encoder-path results/enc_transformer_2a.pt --decoder-path results/dec_transformer_2a.pt \ 13 | --report-results --results-path results/transformer_2a.csv -lr 0.001 14 | python 3_advanced.py --task-name challenge --model-type transformer \ 15 | --optimizer Adam --n-layers 6 --n-iters 30000 --save-model \ 16 | --encoder-path results/enc_transformer_2b.pt --decoder-path results/dec_transformer_2b.pt \ 17 | --report-results --results-path results/transformer_2b.csv -lr 0.001 18 | 19 | # python 3_advanced.py --task-name challenge --model-type copy \ 20 | # --optimizer SGD --save-model --early-stopping 3.2 --weight-decay 0.01 \ 21 | # --encoder-path results/enc_copy_4a.pt --encoder-path results/dec_copy_4a.pt \ 22 | # --report-results --results-path results/copy_4a.csv -lr 0.003 23 | # python 3_advanced.py --task-name challenge --model-type copy \ 24 | # --optimizer SGD --save-model --early-stopping 3.2 --weight-decay 0.01 \ 25 | # --encoder-path results/enc_copy_4b.pt --encoder-path results/dec_copy_4b.pt \ 26 | # --report-results --results-path results/copy_4b.csv -lr 0.003 27 | # python 3_advanced.py --task-name challenge --model-type copy \ 28 | # --optimizer SGD --save-model --early-stopping 3.2 --weight-decay 0.01 \ 29 | # --encoder-path results/enc_copy_4c.pt --encoder-path results/dec_copy_4c.pt \ 30 | # --report-results --results-path results/copy_4c.csv -lr 0.003 31 | -------------------------------------------------------------------------------- /operators/evaluate/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from http.server import BaseHTTPRequestHandler, HTTPServer, SimpleHTTPRequestHandler 4 | from urllib.parse import parse_qs, parse_qsl, urlparse 5 | import os, re 6 | import json 7 | 8 | class ToyModel(object): 9 | def __init__(self): 10 | print("this is just a stub model for testing") 11 | 12 | def respond_to_turker(self, user_input): 13 | return "This is a server output!" 14 | 15 | class Handler(SimpleHTTPRequestHandler): 16 | def _set_headers(self): 17 | self.send_response(200) 18 | self.send_header('Access-Control-Allow-Origin', '*') 19 | self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 20 | self.send_header('Content-type', 'text/html') 21 | self.end_headers() 22 | 23 | def do_GET(self): 24 | print("path: ", self.path) 25 | if self.path == "/goal": 26 | self._set_headers() 27 | self.server.agent.initialize_episode(user_type='turk') 28 | new_goal = self.clean_raw_goal() 29 | self.wfile.write(bytes(new_goal, "UTF-8")) 30 | else: # includes index.html and survey.html 31 | self.path = self.server.wd+self.path 32 | super().do_GET() 33 | 34 | def do_POST(self): 35 | params = parse_qs(urlparse(self.path).query) 36 | # it's a list in case there's duplicates 37 | inputText = params["inputText"][0] 38 | raw_user_input = inputText.replace("|||","\n").strip() 39 | # print('raw_user_input', raw_user_input) 40 | response = self.server.agent.respond_to_turker(raw_user_input) 41 | # print('agent_response', response) 42 | self._set_headers() 43 | self.wfile.write(response.encode()) 44 | 45 | def clean_raw_goal(self): 46 | to_readable = { 47 | "starttime" : "Start time", 48 | "numberofpeople": "Number of people", 49 | "moviename": "Movie name" } 50 | 51 | raw_goal = self.server.agent.running_user.goal 52 | cleaned = "" 53 | for want, value in raw_goal['inform_slots'].items(): 54 | readable = to_readable[want] if want in to_readable.keys() else want.title() 55 | cleaned += readable + " is " 56 | cleaned += str(value).title() + ", " 57 | 58 | print(cleaned[:-2]) 59 | return cleaned[:-2] -------------------------------------------------------------------------------- /scripts/oracle.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | np.random.seed(11) 3 | 4 | def pause(): 5 | programPause = raw_input() 6 | 7 | def get_oracle_dialogs(lines, n=20): 8 | ''' 9 | :param lines: f.lines() 10 | :param n: number of dialogs to load 11 | :return: [[(u1, r1), (u2, r2)...], [(u1, r1), (u2, r2)...], ...] 12 | ui and ri are strings 13 | ''' 14 | data = [] 15 | dialogue = [] 16 | kb_dialogue = [] 17 | kb = [] 18 | read_dialog = 0 19 | for line in lines: 20 | if read_dialog == n: 21 | break 22 | 23 | if line != '\n' and line != lines[-1]: 24 | nid, line = line.split(' ', 1) 25 | nid = int(nid) 26 | line = line.decode('utf-8').strip() 27 | 28 | 29 | if len(line.split('\t')) == 1: 30 | kb_dialogue += (line.split('\t')) 31 | continue 32 | 33 | q, a = line.split('\t') 34 | dialogue.append((q, a)) 35 | else: 36 | data.append(dialogue) 37 | kb.append(kb_dialogue) 38 | read_dialog += 1 39 | dialogue = [] 40 | kb_dialogue = [] 41 | return data, kb 42 | 43 | 44 | def oracleExperiment(files, num_dialog_each_file, directory='datasets/restaurants/', randomize=False): 45 | ''' 46 | :param files: list of files you want to read from 47 | :param num_dialog_each_file: int 48 | :param directory: data directory 49 | :return: 50 | ''' 51 | if randomize is True: 52 | order = np.random.permutation(num_dialog_each_file * len(files)) 53 | else: 54 | order = range(num_dialog_each_file * len(files)) 55 | data = [] 56 | kbs = [] 57 | for file in files: 58 | path = directory + file 59 | with open(path) as f: 60 | datus, kb = get_oracle_dialogs(f.readlines(), n=num_dialog_each_file) 61 | data += datus 62 | kbs += kb 63 | 64 | for i in order: 65 | print "--------NEW CUSTOMER--------" 66 | print '--------KB--------' 67 | print kbs[i] 68 | print 'Starting conversation...' 69 | for turn in data[i]: 70 | print turn[0] 71 | pause() 72 | 73 | 74 | directory = 'datasets/restaurants/' 75 | files = ['dialog-babi-task5-full-dialogs-trn.txt', 'dialog-babi-task6-dstc2-trn.txt'] 76 | num_dialog_each_file = 2 77 | 78 | oracleExperiment(files, num_dialog_each_file, randomize=True) 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /utils/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | NLG Demo 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 21 | 31 |
32 |
33 |

The system expects a properly written user intent of the form 34 | act(slot=value) such as inform(city=los angeles). Multiple intents can be included by joining them with a comma. Spaces will be removed. 35 |

36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 |
47 | 48 |
49 |
50 |
51 |
52 | 54 |
55 |
56 |
57 | 58 | -------------------------------------------------------------------------------- /scripts/verify_frame.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pdb 3 | 4 | split_types = ["test", "train"] 5 | for split in split_types: 6 | data = json.load( open("clean_woz_{}_1.json".format(split), "rb")) 7 | cleaned = [] 8 | counter = 0 9 | ex_id = 0 10 | 11 | for example in data: 12 | # cache user preference by selectively re-initializing for new examples 13 | if example['id'] != ex_id: 14 | frame = {} 15 | frame["food"] = "" 16 | frame["price"] = "" # cheap, moderate, expensive, dont_care 17 | frame["area"] = "" # north, south, east, west, centre, dont_care 18 | ex_id = example['id'] 19 | # initialize the frame / clear out the previous frame 20 | frame["open"] = False 21 | frame["request"] = [] # postcode, address, price, area, food, hours, phone 22 | frame["clarify"] = False 23 | frame["accept"] = "" # restaurant, response 24 | frame["reject"] = "" # continue, end 25 | frame["close"] = False 26 | # could add turn number, NER, did we greet first, user sentiment, etc. 27 | 28 | for isv in example['intents']: 29 | frame["open"] = True if (isv[0] == "open" or frame["open"]) else False 30 | frame["close"] = True if (isv[0] == "close" or frame["close"]) else False 31 | frame["clarify"] = True if (isv[1] == "clarify" or frame["clarify"]) else False 32 | 33 | if (isv[0] == "inform" and isv[1] == "food"): 34 | frame["food"] = isv[2] 35 | if (isv[0] == "inform" and isv[1] == "price"): 36 | frame["price"] = isv[2] 37 | if (isv[0] == "inform" and isv[1] == "area"): 38 | frame["area"] = isv[2] 39 | if (isv[0] == "accept" and frame["accept"] == ""): 40 | frame["accept"] = isv[2] 41 | if (isv[0] == "reject" and frame["accept"] == ""): 42 | frame["reject"] = isv[1] 43 | if (isv[0] == "request" and isv[1] == "question"): 44 | frame["request"].append(isv[2]) 45 | 46 | example['frame'] = frame.copy() 47 | cleaned.append(example) 48 | 49 | # if ex_id == 669: 50 | # print(cleaned[-3]['frame']['close']) 51 | # print(cleaned[-2]['frame']['close']) 52 | # print(cleaned[-1]['frame']['close']) 53 | 54 | json.dump(cleaned, open("clean_woz_{}_4.json".format(split), "w", encoding="utf8")) 55 | print("Done with {}, processed {} examples".format(split, len(cleaned))) 56 | 57 | -------------------------------------------------------------------------------- /scripts/find_vocab_res.py: -------------------------------------------------------------------------------- 1 | from nltk import word_tokenize 2 | import json 3 | 4 | vocab = set([]) 5 | fnames = [ "dialog-babi-task6-dstc2-dev.txt", 6 | "dialog-babi-task6-dstc2-tst.txt", 7 | "dialog-babi-task6-dstc2-trn.txt", 8 | "dialog-dstc-candidates.txt", 9 | "dialog-babi-task5-trn.txt", 10 | "dialog-babi-task4-trn.txt", 11 | "dialog-babi-task3-trn.txt", 12 | "dialog-babi-task2-trn.txt", 13 | "dialog-babi-task5-tst-OOV.txt", 14 | "dialog-babi-task4-tst-OOV.txt", 15 | "dialog-babi-task3-tst-OOV.txt", 16 | "dialog-babi-task2-tst-OOV.txt", 17 | ] 18 | 19 | ''' 20 | Checks if the token is a normal, valid word 21 | If the token is valid, then return True 22 | Otherwise, it is a special keywork, so return False 23 | ''' 24 | def valid(token): 25 | if token in ["", "api_call"]: 26 | return False 27 | if "address" in token and token.startswith("resto"): 28 | return False 29 | if "phone" in token and token.startswith("resto"): 30 | return False 31 | return True 32 | 33 | def pull_vocab(filename, vocab): 34 | with open(filename, "r") as f: 35 | for line in f: 36 | if line.strip() == "": 37 | continue 38 | nid, line = line.split(' ', 1) 39 | line = line.decode('utf-8').strip() 40 | if len(line.split('\t')) == 1: 41 | continue 42 | u, r = line.split('\t') 43 | for token in word_tokenize(u): 44 | if valid(token): 45 | vocab.add(token) 46 | for token in word_tokenize(r): 47 | # for token in u.split(): 48 | # if valid(token): 49 | # vocab.add(token) 50 | # for token in r.split(): 51 | if valid(token): 52 | vocab.add(token) 53 | print("Done with {0} now have {1} words".format(filename, len(vocab)) ) 54 | return vocab 55 | 56 | for filename in fnames: 57 | vocab = pull_vocab(filename, vocab) 58 | 59 | vocab = list(vocab) 60 | vocab.sort() 61 | 62 | special_tokens = ["", "","","","","","", 63 | "","","","","","","", 64 | "","UNK", "SOS", "EOS", "api_call","", ""] 65 | all_tokens = special_tokens + vocab 66 | 67 | print len(all_tokens) 68 | 69 | json.dump(all_tokens, open("res_vocab.json", "w")) 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /utils/webtools/nlgWebsite.js: -------------------------------------------------------------------------------- 1 | var inpF, hist; 2 | var inpFdefault = "Enter your text here."; 3 | 4 | window.onload = setup; 5 | 6 | function newConversation(){ 7 | // clear out the history and input field 8 | hist = document.getElementById("history"); 9 | hist.innerHTML = ""; 10 | inpF.value = inpFdefault; 11 | 12 | goal = document.getElementById("goal"); 13 | var xmlHttp = new XMLHttpRequest(); 14 | xmlHttp.onreadystatechange = function() { 15 | if (xmlHttp.readyState == 4 && xmlHttp.status == 200){ 16 | new_goal = xmlHttp.responseText; 17 | goal.innerHTML = new_goal; 18 | console.log(new_goal); 19 | } 20 | } 21 | xmlHttp.open("GET", window.location.href+"goal", true); 22 | xmlHttp.send(); 23 | } 24 | 25 | function getResponse(){ 26 | // Pushing prompt to the history 27 | var prompt = inpF.value; 28 | if (prompt === inpFdefault || prompt === "") 29 | return; // show warning message? 30 | hist = document.getElementById("history"); 31 | hist.innerHTML += `
${prompt}
`; 32 | inpF.value = ""; 33 | console.log(inpF.value); 34 | console.log(prompt); 35 | 36 | // Sending stuff to server (for now, no server) 37 | var nlgResponse = "Default Text"; 38 | var toSend = $.trim(prompt).replace(/\n/g,"|||"); 39 | // var toSend = $.trim(hist.innerText).replace(/\n/g,"|||"); 40 | var xmlHttp = new XMLHttpRequest(); 41 | xmlHttp.onreadystatechange = function() { 42 | if (xmlHttp.readyState == 4 && xmlHttp.status == 200){ 43 | console.log(xmlHttp.responseText); 44 | nlgResponse = xmlHttp.responseText; 45 | hist.innerHTML += `
${nlgResponse}
`; 46 | } 47 | } 48 | xmlHttp.open("POST", window.location.href+"?inputText="+toSend, true); 49 | xmlHttp.send(); 50 | } 51 | 52 | function setup(){ 53 | /*Input field aesthetics*/ 54 | inpF = document.getElementById("inputfield"); 55 | inpF.onfocus = function(){ 56 | if(inpF.value === inpFdefault){ 57 | inpF.style = "color: black;font-style: normal"; 58 | inpF.value = ""; 59 | } 60 | }; 61 | inpF.onblur = function(){ 62 | if (inpF.value === ""){ 63 | inpF.style = "color: grey;font-style: italic;"; 64 | inpF.value = inpFdefault; 65 | } 66 | }; 67 | inpF.onkeyup = function(e){ 68 | if (e.keyCode === 13 && !e.shiftKey){ 69 | getResponse(); 70 | } 71 | }; 72 | newConversation(); 73 | } -------------------------------------------------------------------------------- /utils/external/dialog_config_v2.py: -------------------------------------------------------------------------------- 1 | starting_dialogue_acts = ['open', 'request'] 2 | 3 | inform_slots = ['city', 'date', 'genre', 'moviename', 'numberofpeople', 'starttime', 'state', 'theater'] 4 | request_slots = ['other', 'theater', 'ticket', 'unknown'] 5 | 6 | # A Basic Set of Feasible actions to be Consdered By an RL agent 7 | feasible_agent_actions = [ 8 | {'dialogue_act':"greeting", 'inform_slots':{}, 'request_slots':{}}, 9 | {'dialogue_act': "confirm_question", 'inform_slots': {}, 'request_slots': {}}, 10 | {'dialogue_act': "confirm_answer", 'inform_slots': {}, 'request_slots': {}}, 11 | {'dialogue_act': "thanks", 'inform_slots': {}, 'request_slots': {}}, 12 | {'dialogue_act': "deny", 'inform_slots': {}, 'request_slots': {}}, 13 | ] 14 | # A Basic Set of Feasible actions to be Consdered By an User 15 | feasible_user_actions = [ 16 | {'dialogue_act': "thanks", 'inform_slots': {}, 'request_slots': {}}, 17 | {'dialogue_act': "deny", 'inform_slots': {}, 'request_slots': {}}, 18 | {'dialogue_act': "closing", 'inform_slots': {}, 'request_slots': {}}, 19 | {'dialogue_act': "confirm_answer", 'inform_slots': {}, 'request_slots': {}} 20 | ] 21 | 22 | for inf_slot in inform_slots: 23 | feasible_agent_actions.append({'dialogue_act': 'inform', 'inform_slots': {inf_slot: "PLACEHOLDER"}, 'request_slots': {}}) 24 | feasible_user_actions.append({'dialogue_act': 'inform', 'inform_slots': {inf_slot: "PLACEHOLDER"}, 'request_slots': {}}) 25 | for req_slot in request_slots: 26 | feasible_agent_actions.append({'dialogue_act': 'request', 'inform_slots': {}, 'request_slots': {req_slot: ""}}) 27 | feasible_user_actions.append({'dialogue_act': 'request', 'inform_slots': {}, 'request_slots': {req_slot: ""}}) 28 | feasible_agent_actions.append({'dialogue_act': 'inform', 'inform_slots': {"task": "complete"}, 'request_slots': {}}) 29 | 30 | 31 | # Dialog status 32 | FAILED_DIALOG = -1 33 | SUCCESS_DIALOG = 1 34 | NO_OUTCOME_YET = 0 35 | 36 | # Rewards 37 | SUCCESS_REWARD = 40 38 | FAILURE_REWARD = 0 39 | PER_TURN_REWARD = 0 40 | 41 | # Special Slot Values 42 | I_DO_NOT_CARE = "I do not care" 43 | NO_VALUE_MATCH = "NO VALUE MATCHES!!!" 44 | TICKET_AVAILABLE = 'Ticket Available' 45 | 46 | # Constraint Check 47 | CONSTRAINT_CHECK_FAILURE = 0 48 | CONSTRAINT_CHECK_SUCCESS = 1 49 | nlg_beam_size = 10 50 | 51 | # run_mode: 0 for dia-act; 1 for NL; 2 for no output; 3 for skip everything 52 | run_mode = 3 53 | auto_suggest = False # (or True) 54 | -------------------------------------------------------------------------------- /execute/basic.sh: -------------------------------------------------------------------------------- 1 | CUDA_VISIBLE_DEVICES=0 python run.py --gpu=0 --task end_to_end -e 500 \ 2 | --epsilon 0.0 --hidden-dim 100 --embedding-size 200 --use-old-nlu \ 3 | --batch-size 16 --seed 14 --warm-start --model ddq --max-turn 40 \ 4 | --user simulate --metrics success_rate avg_reward --dataset e2e/movies \ 5 | --learning-rate 1e-4 --optimizer rmsprop --weight-decay 0.0 \ 6 | --pool-size 5000 --prefix old_nlu_1e4_ --suffix _14 --verbose 7 | 8 | CUDA_VISIBLE_DEVICES=0 python run.py --gpu=0 --task end_to_end -e 500 \ 9 | --epsilon 0.0 --hidden-dim 100 --embedding-size 200 --use-old-nlu \ 10 | --batch-size 16 --seed 15 --warm-start --model ddq --max-turn 40 \ 11 | --user simulate --metrics success_rate avg_reward --dataset e2e/movies \ 12 | --learning-rate 1e-4 --optimizer rmsprop --weight-decay 0.0 \ 13 | --pool-size 5000 --prefix old_nlu_1e4_ --suffix _15 --verbose 14 | 15 | # CUDA_VISIBLE_DEVICES=1 python run.py --gpu=1 --task track_intent --dataset woz2 \ 16 | # --model glad --learning-rate 1e-3 --hidden-dim 200 --embedding-size 400 \ 17 | # --epochs 50 --threshold 0.3 --optimizer adam --save-model \ 18 | # --prefix Apr_09_ --suffix _1 --seed 14 --verbose # --debug 19 | 20 | # python run.py --task full_enumeration --dataset basicwoz --model basic \ 21 | # --prefix removeme_ -lr 0.001 --epochs 2 --optimizer rmsprop 22 | 23 | 24 | # CUDA_VISIBLE_DEVICES=1 python run.py --gpu=1 --task track_intent \ 25 | # --model glad --learning-rate 3e-3 --hidden-dim 200 --embedding-size 400 \ 26 | # --epochs 40 --threshold 0.3 --optimizer adam --dataset e2e/movies \ 27 | # --early-stop joint_goal --save-model \ 28 | # --prefix nlu_ --suffix _lr3e3 --seed 14 --verbose 29 | 30 | # CUDA_VISIBLE_DEVICES=1 python run.py --gpu=1 --task track_intent \ 31 | # --model glad --learning-rate 1e-2 --hidden-dim 200 --embedding-size 400 \ 32 | # --epochs 40 --threshold 0.3 --optimizer adam --dataset e2e/movies \ 33 | # --early-stop joint_goal --save-model \ 34 | # --prefix nlu_ --suffix _lr1e2 --seed 14 --verbose 35 | 36 | 37 | # Pre-existing model for evaluation, this save_dir has a working CPU version 38 | # python run.py --task track_intent --early-stop joint_goal --test-mode \ 39 | # --model glad --learning-rate 1e-3 --hidden-dim 200 --embedding-size 400 \ 40 | # --epochs 1 --threshold 0.3 --optimizer adam --dataset woz2 \ 41 | # --prefix Apr_08_ --suffix _14 --seed 14 --verbose --use-existing 42 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: research 2 | channels: 3 | - pytorch 4 | - defaults 5 | dependencies: 6 | - blas=1.0=mkl 7 | - ca-certificates=2019.1.23=0 8 | - certifi=2018.11.29=py36_0 9 | - cffi=1.11.5=py36he75722e_1 10 | - cycler=0.10.0=py36_0 11 | - dbus=1.13.6=h746ee38_0 12 | - expat=2.2.6=he6710b0_0 13 | - fontconfig=2.13.0=h9420a91_0 14 | - freetype=2.9.1=h8a8886c_1 15 | - glib=2.56.2=hd408876_0 16 | - gst-plugins-base=1.14.0=hbbd80ab_1 17 | - gstreamer=1.14.0=hb453b48_1 18 | - icu=58.2=h9c2bf20_1 19 | - intel-openmp=2019.1=144 20 | - jpeg=9b=h024ee3a_2 21 | - kiwisolver=1.0.1=py36hf484d3e_0 22 | - libedit=3.1.20170329=h6b74fdf_2 23 | - libffi=3.2.1=hd88cf55_4 24 | - libgcc-ng=8.2.0=hdf63c60_1 25 | - libgfortran-ng=7.3.0=hdf63c60_0 26 | - libpng=1.6.36=hbc83047_0 27 | - libstdcxx-ng=8.2.0=hdf63c60_1 28 | - libuuid=1.0.3=h1bed415_2 29 | - libxcb=1.13=h1bed415_1 30 | - libxml2=2.9.9=he19cac6_0 31 | - matplotlib=3.0.2=py36h5429711_0 32 | - mkl=2019.1=144 33 | - mkl_fft=1.0.10=py36ha843d7b_0 34 | - mkl_random=1.0.2=py36hd81dba3_0 35 | - ncurses=6.1=he6710b0_1 36 | - ninja=1.8.2=py36h6bb024c_1 37 | - nltk=3.4=py36_1 38 | - numpy=1.15.4=py36h7e9f1db_0 39 | - numpy-base=1.15.4=py36hde5b4d6_0 40 | - openssl=1.1.1a=h7b6447c_0 41 | - pandas=0.23.4=py36h04863e7_0 42 | - pcre=8.42=h439df22_0 43 | - pip=18.1=py36_0 44 | - pycparser=2.19=py36_0 45 | - pyparsing=2.3.1=py36_0 46 | - pyqt=5.9.2=py36h05f1152_2 47 | - python=3.6.8=h0371630_0 48 | - python-dateutil=2.7.5=py36_0 49 | - pytz=2018.7=py36_0 50 | - qt=5.9.7=h5867ecd_1 51 | - readline=7.0=h7b6447c_5 52 | - scipy=1.2.0=py36h7c811a0_0 53 | - setuptools=40.6.3=py36_0 54 | - sip=4.19.8=py36hf484d3e_0 55 | - six=1.12.0=py36_0 56 | - sqlite=3.26.0=h7b6447c_0 57 | - tk=8.6.8=hbc83047_0 58 | - tornado=5.1.1=py36h7b6447c_0 59 | - tqdm=4.28.1=py36h28b3542_0 60 | - wheel=0.32.3=py36_0 61 | - xz=5.2.4=h14c3975_4 62 | - zlib=1.2.11=h7b6447c_3 63 | - pytorch=1.0.0=py3.6_cuda9.0.176_cudnn7.4.1_1 64 | - pip: 65 | - apex==0.1 66 | - boto3==1.9.82 67 | - botocore==1.12.82 68 | - chardet==3.0.4 69 | - docutils==0.14 70 | - embeddings==0.0.6 71 | - idna==2.8 72 | - jmespath==0.9.3 73 | - pillow==5.4.1 74 | - pprint==0.1 75 | - protobuf==3.6.1 76 | - pytorch-pretrained-bert==0.4.0 77 | - requests==2.21.0 78 | - s3transfer==0.1.13 79 | - stanza==0.3 80 | - torch==1.0.0 81 | - torchvision==0.2.1 82 | - urllib3==1.24.1 83 | - vocab==0.0.4 84 | prefix: /home/dchen14/anaconda3/envs/research 85 | 86 | -------------------------------------------------------------------------------- /scripts/extract_cuisines.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | splits = ["tst", "dev", "trn"] 4 | total = [] 5 | # split = "trn" 6 | for split in splits: 7 | path_name = "datasets/restaurants/dialog-babi-task6-dstc2-{}.txt".format(split) 8 | with open(path_name, "r") as f: 9 | for line in f: 10 | # tokens = line.split() 11 | turn = line.split('\t') 12 | try: 13 | response = turn[1] # query = turn[0] 14 | # label = tokens[2] 15 | searchObj = re.search( r'no restaurant serving ([a-z]*) food', response) 16 | # if label == 'R_cuisine': 17 | if searchObj: 18 | # cuisine = tokens[3] 19 | cuisine = searchObj.group(1) 20 | total.append(cuisine) 21 | except(IndexError): 22 | continue 23 | # print set(total) 24 | total.sort() 25 | uniques = set(total) 26 | # print len(uniques) 27 | m = len(uniques) 28 | print "Finished processing set {0}, found {1} cuisines".format(split, m) 29 | 30 | 31 | def identify_matches(sentence): 32 | matches = [] 33 | for word in sentence: 34 | if index in match_phrases["cuisines"]: 35 | matches.append(0) 36 | elif index in match_phrases["locations"]: 37 | matches.append(1) 38 | elif index in match_phrases["prices"]: 39 | matches.append(2) 40 | elif index in match_phrases["ratings"]: 41 | matches.append(3) 42 | elif index in match_phrases["sizes"]: 43 | matches.append(4) 44 | elif index == PHONE_token: 45 | matches.append(5) 46 | elif index == ADDR_token: 47 | matches.append(6) 48 | else: # not a special match embedding 49 | matches.append(7) 50 | 51 | return matches 52 | ''' 53 | 54 | def add_match_feature(sentence, use_cuda): 55 | matches = identify_matches(sentence) 56 | encoding_suffix = [] 57 | for match in matches: 58 | thing = match_embeddings[match] 59 | encoding_suffix.extend(thing) 60 | # 1 = batch size / -1 = sentence length / 8 = size of match embedding 61 | reshaped = encoding_suffix.view(1,-1,8) 62 | # if use_cuda: reshaped.cuda() 63 | 64 | return sentence.extend(reshaped, axis=2) 65 | 66 | 67 | ''' 68 | # (10 choices, 91 in DTSC) 69 | cuisines = ["british", "cantonese", "french", "indian", "italian", 70 | "japanese", "korean", "spanish", "thai", "vietnamese"] 71 | # (10 choices, 5 in DTSC) 72 | locations = ["beijing", "seoul", "tokyo", "paris", "madrid", 73 | "hanoi", "bangkok", "rome", "london", "bombay"] 74 | prices = ["cheap", "moderate", "expensive"] 75 | ratings = [str(x) for x in range(1,9)] 76 | party_sizes = ["two", "four", "six", "eight"] 77 | -------------------------------------------------------------------------------- /scripts/evaluation.py: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | # Evaluation 3 | # ========== 4 | # 5 | # Evaluation is mostly the same as training, but there are no targets so 6 | # we simply feed the decoder's predictions back to itself for each step. 7 | # Every time it predicts a word we add it to the output string, and if it 8 | # predicts the EOS token we stop there. We also store the decoder's 9 | # attention outputs for display later. 10 | # 11 | 12 | def evaluate(encoder, decoder, sentence, max_length=MAX_LENGTH): 13 | input_variable = variableFromSentence(input_lang, sentence) 14 | input_length = input_variable.size()[0] 15 | encoder_hidden = encoder.initHidden() 16 | 17 | encoder_outputs = Variable(torch.zeros(max_length, encoder.hidden_size)) 18 | encoder_outputs = encoder_outputs.cuda() if use_cuda else encoder_outputs 19 | 20 | for ei in range(input_length): 21 | encoder_output, encoder_hidden = encoder(input_variable[ei], 22 | encoder_hidden) 23 | encoder_outputs[ei] = encoder_outputs[ei] + encoder_output[0][0] 24 | 25 | decoder_input = Variable(torch.LongTensor([[SOS_token]])) # SOS 26 | decoder_input = decoder_input.cuda() if use_cuda else decoder_input 27 | 28 | decoder_hidden = encoder_hidden 29 | 30 | decoded_words = [] 31 | decoder_attentions = torch.zeros(max_length, max_length) 32 | 33 | for di in range(max_length): 34 | decoder_output, decoder_hidden, decoder_attention = decoder( 35 | decoder_input, decoder_hidden, encoder_output, encoder_outputs) 36 | decoder_attentions[di] = decoder_attention.data 37 | topv, topi = decoder_output.data.topk(1) 38 | ni = topi[0][0] 39 | if ni == EOS_token: 40 | decoded_words.append('') 41 | break 42 | else: 43 | decoded_words.append(output_lang.index2word[ni]) 44 | decoder_input = Variable(torch.LongTensor([[ni]])) 45 | decoder_input = decoder_input.cuda() if use_cuda else decoder_input 46 | 47 | return decoded_words, decoder_attentions[:di + 1] 48 | 49 | 50 | ###################################################################### 51 | # We can evaluate random sentences from the training set and print out the 52 | # input, target, and output to make some subjective quality judgements: 53 | # 54 | 55 | def evaluateRandomly(encoder, decoder, n=10): 56 | for i in range(n): 57 | pair = random.choice(pairs) 58 | print('>', pair[0]) 59 | print('=', pair[1]) 60 | output_words, attentions = evaluate(encoder, decoder, pair[0]) 61 | output_sentence = ' '.join(output_words) 62 | print('<', output_sentence) 63 | print('') 64 | -------------------------------------------------------------------------------- /execute/end_to_end.sh: -------------------------------------------------------------------------------- 1 | CUDA_VISIBLE_DEVICES=1 python run.py --gpu=1 --task end_to_end -e 500 \ 2 | --epsilon 0.0 --hidden-dim 100 --embedding-size 200 --use-old-nlu \ 3 | --batch-size 16 --seed 14 --warm-start --model ddq --max-turn 40 \ 4 | --user simulate --metrics success_rate avg_reward --dataset e2e/movies \ 5 | --learning-rate 1e-3 --optimizer rmsprop --weight-decay 0.0 \ 6 | --pool-size 5000 --prefix their_nlu_ --suffix _14 --verbose 7 | 8 | CUDA_VISIBLE_DEVICES=1 python run.py --gpu=1 --task end_to_end -e 500 \ 9 | --epsilon 0.0 --hidden-dim 100 --embedding-size 200 --use-old-nlu \ 10 | --batch-size 16 --seed 15 --warm-start --model ddq --max-turn 40 \ 11 | --user simulate --metrics success_rate avg_reward --dataset e2e/movies \ 12 | --learning-rate 1e-3 --optimizer rmsprop --weight-decay 0.0 \ 13 | --pool-size 5000 --prefix their_nlu_ --suffix _15 --verbose 14 | # --early-stop success_rate --use-existing --report-qual 15 | 16 | # CUDA_VISIBLE_DEVICES=1 python run.py --gpu=1 --task end_to_end -e 600 \ 17 | # --epsilon 0.0 --hidden-dim 100 --embedding-size 200 \ 18 | # --batch-size 16 --seed 15 --warm-start --model ddq --max-turn 40 \ 19 | # --user simulate --metrics success_rate avg_reward --dataset e2e/movies \ 20 | # --learning-rate 1e-3 --optimizer adam --weight-decay 0.0 \ 21 | # --pool-size 5000 --prefix adam_ --suffix _15 --verbose 22 | 23 | 24 | # CUDA_VISIBLE_DEVICES=1 python run.py --gpu=1 --task end_to_end -e 600 \ 25 | # --epsilon 0.0 --hidden-dim 200 --embedding-size 400 \ 26 | # --batch-size 16 --seed 14 --warm-start --model ddq --max-turn 40 \ 27 | # --user simulate --metrics success_rate avg_reward --dataset e2e/movies \ 28 | # --learning-rate 1e-3 --optimizer rmsprop --weight-decay 0.0 \ 29 | # --pool-size 5000 --prefix large_embed_ --suffix _14 --verbose 30 | 31 | # CUDA_VISIBLE_DEVICES=1 python run.py --gpu=1 --task end_to_end -e 600 \ 32 | # --epsilon 0.0 --hidden-dim 200 --embedding-size 400 \ 33 | # --batch-size 16 --seed 15 --warm-start --model ddq --max-turn 40 \ 34 | # --user simulate --metrics success_rate avg_reward --dataset e2e/movies \ 35 | # --learning-rate 1e-3 --optimizer rmsprop --weight-decay 0.0 \ 36 | # --pool-size 5000 --prefix large_embed_ --suffix _15 --verbose 37 | 38 | # CUDA_VISIBLE_DEVICES=0 python run.py --gpu=0 --task track_intent \ 39 | # --model glad --learning-rate 3e-4 --hidden-dim 200 --embedding-size 400 \ 40 | # --epochs 40 --threshold 0.3 --optimizer adam --dataset e2e/movies \ 41 | # --early-stop joint_goal --save-model \ 42 | # --prefix nlu_ --suffix _lr1e4 --seed 14 --verbose -------------------------------------------------------------------------------- /operators/system.py: -------------------------------------------------------------------------------- 1 | from operators.learn import Learner 2 | from operators.evaluate import RewardMonitor, LossMonitor 3 | 4 | class SingleSystem(object): 5 | def __init__(self, args, loader, builder, processor, evaluator): 6 | self.args = args 7 | self.task = args.task 8 | self.evaluator = evaluator 9 | if self.args.task == 'track_intent': 10 | self.monitor = LossMonitor(args.metrics, args.threshold, args.early_stop) 11 | elif self.args.task == 'manage_policy': 12 | self.monitor = RewardMonitor(args.metrics, args.threshold) 13 | 14 | model = builder.get_model(processor, self.monitor) 15 | self.module = builder.configure_module(args, model) 16 | self.module.save_config(args, builder.dir) 17 | 18 | if args.use_existing or args.test_mode: 19 | self.monitor.restore_from_checkpoint(model) 20 | if not args.test_mode: 21 | self.learner = Learner(args, self.module, processor, self.monitor) 22 | 23 | def run_main(self): 24 | if self.args.task == 'track_intent': 25 | self.learner.supervise(self.args) 26 | elif self.args.task == 'manage_policy': 27 | self.learner.reinforce(self.args) 28 | 29 | def evaluate(self, test_mode): 30 | self.evaluator.module = self.module 31 | self.evaluator.monitor = self.monitor 32 | if self.args.test_mode: 33 | self.evaluator.run_test() 34 | self.evaluator.generate_report() 35 | 36 | class EndToEndSystem(object): 37 | def __init__(self, args, loader, builder, processor, evaluator): 38 | self.args = args 39 | self.metrics = args.metrics 40 | self.evaluator = evaluator 41 | self.monitor = RewardMonitor(args.metrics, args.threshold) 42 | 43 | nlu_type = 'nlu_model' if args.use_old_nlu else 'glad' 44 | bt_model = builder.get_model(processor, self.monitor, nlu_type) 45 | pm_model = builder.get_model(processor, self.monitor, "ddq") 46 | tg_model = builder.get_model(processor, self.monitor, "nlg_model") 47 | self.dialogue_agent = builder.create_agent(bt_model, pm_model, tg_model) 48 | 49 | if args.use_existing: 50 | self.monitor.restore_from_checkpoint(modules) 51 | if not args.test_mode: 52 | self.learner = Learner(args, self.dialogue_agent, processor, self.monitor) 53 | 54 | def run_main(self): 55 | self.learner.end_to_end(self.args) 56 | 57 | def evaluate(self, test_mode): 58 | self.evaluator.agent = self.dialogue_agent 59 | self.evaluator.monitor = self.monitor 60 | if test_mode: 61 | if self.args.user in ['command', 'simulate']: 62 | self.evaluator.start_talking(self.args.user, self.args.epochs) 63 | elif self.args.user == 'turk': 64 | import os 65 | root_dir = os.path.join(os.path.dirname(__file__), os.pardir) 66 | self.evaluator.start_server(root_dir) 67 | else: 68 | self.evaluator.generate_report() 69 | -------------------------------------------------------------------------------- /utils/survey.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | NLG Demo 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 18 |
19 |
20 |

As a last step, please complete this short three question survey describing your interacting with the agent.

21 |
22 |
23 |
24 |
25 |
    26 | 27 |
  • Do you think your partner demonstrated reasonable humanlike behavior? Were they coherent in what they are saying and did they understood what you said?

  • 28 |
    29 |

    Not at all

    30 | 31 |

    Definitely

    32 |
    33 | 34 | 35 |
  • How well was your partner able to understand you? Was it able to comprehend what you said?

  • 36 |
    37 |

    Not at all

    38 | 39 |

    Asked the right questions

    40 |
    45 | 46 | 47 |
  • Was your partner concise? Did your partner talk too much or repeat itself?

  • 48 |
49 |
50 |

Not at all

51 | 52 |

Talked too much

53 |
54 | 55 |
56 |

(optional) Please add any comments/feedback related to the dialogue below. (for e.g., comments on how we can improve this platform, why you made the above ratings, etc.)

57 |
58 | 59 |
60 |
61 | 62 | 63 | 64 |
65 |
66 |
67 | 68 | -------------------------------------------------------------------------------- /utils/external/glad_preprocess.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import json 4 | import logging 5 | import requests 6 | from tqdm import tqdm 7 | from vocab import Vocab 8 | from embeddings import GloveEmbedding, KazumaCharEmbedding 9 | from utils.external.glad_dataset import Dataset, Ontology 10 | 11 | 12 | root_dir = os.path.dirname(__file__) 13 | data_dir = os.path.join('datasets', 'woz2') 14 | 15 | 16 | draw = os.path.join(data_dir, 'raw') 17 | dann = os.path.join(data_dir, 'ann') 18 | 19 | splits = ['dev', 'train', 'test'] 20 | 21 | 22 | def download(url, to_file): 23 | r = requests.get(url, stream=True) 24 | with open(to_file, 'wb') as f: 25 | for chunk in r.iter_content(chunk_size=1024): 26 | if chunk: 27 | f.write(chunk) 28 | 29 | 30 | def missing_files(d, files): 31 | return not all([os.path.isfile(os.path.join(d, '{}.json'.format(s))) for s in files]) 32 | 33 | 34 | if __name__ == '__main__': 35 | if missing_files(draw, splits): 36 | if not os.path.isdir(draw): 37 | os.makedirs(draw) 38 | download('https://github.com/nmrksic/neural-belief-tracker/raw/master/data/woz/woz_train_en.json', os.path.join(draw, 'train.json')) 39 | download('https://github.com/nmrksic/neural-belief-tracker/raw/master/data/woz/woz_validate_en.json', os.path.join(draw, 'dev.json')) 40 | download('https://github.com/nmrksic/neural-belief-tracker/raw/master/data/woz/woz_test_en.json', os.path.join(draw, 'test.json')) 41 | 42 | if missing_files(dann, files=splits + ['ontology', 'vocab', 'emb']): 43 | if not os.path.isdir(dann): 44 | os.makedirs(dann) 45 | dataset = {} 46 | ontology = Ontology() 47 | vocab = Vocab() 48 | vocab.word2index(['', ''], train=True) 49 | for s in splits: 50 | fname = '{}.json'.format(s) 51 | logging.warn('Annotating {}'.format(s)) 52 | dataset[s] = Dataset.annotate_raw(os.path.join(draw, fname)) 53 | dataset[s].numericalize_(vocab) 54 | ontology = ontology + dataset[s].extract_ontology() 55 | with open(os.path.join(dann, fname), 'wt') as f: 56 | json.dump(dataset[s].to_dict(), f) 57 | ontology.numericalize_(vocab) 58 | with open(os.path.join(dann, 'ontology.json'), 'wt') as f: 59 | json.dump(ontology.to_dict(), f) 60 | with open(os.path.join(dann, 'vocab.json'), 'wt') as f: 61 | json.dump(vocab.to_dict(), f) 62 | 63 | logging.warn('Computing word embeddings') 64 | embeddings = [GloveEmbedding(), KazumaCharEmbedding()] 65 | E = [] 66 | for w in tqdm(vocab._index2word): 67 | e = [] 68 | for emb in embeddings: 69 | e += emb.emb(w, default='zero') 70 | E.append(e) 71 | with open(os.path.join(dann, 'emb.json'), 'wt') as f: 72 | json.dump(E, f) 73 | -------------------------------------------------------------------------------- /scripts/retrieve_examples.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This script retrieves answers to questions in PATH.txt. 3 | Pretrained models.pt can be loaded to answer the questions 4 | ''' 5 | from nltk import word_tokenize 6 | from utils.external.preprocessers import * 7 | from torch.autograd import Variable 8 | 9 | sys.path.append('.') 10 | 11 | task = "car" <-- or "res" 12 | PATH = 'scripts/seq2seq_ex.txt' 13 | 14 | def read_questions(path): 15 | ''' 16 | :param path: path of your questions. See 'scripts/seq2seq_ex.txt' for example. 17 | One question per line 18 | :return: list of tokenized questions 19 | ''' 20 | questions = [] 21 | with open(path) as f: 22 | lines = f.readlines() 23 | for line in lines: 24 | line = line.decode('utf-8').strip() 25 | q = word_tokenize(line) 26 | questions.append(q) 27 | return questions 28 | 29 | 30 | def q2idx(questions): 31 | ''' 32 | :param questions: list of tokens 33 | :return: convert tokens to index 34 | ''' 35 | results = [] 36 | for q in questions: 37 | question, wop = variable_from_sentence(q, [questions.index(q)+1]) 38 | results.append(question) 39 | return results 40 | 41 | 42 | 43 | def answers(input_variable, encoder, decoder): 44 | ''' 45 | :param input_variable: list of index 46 | :param encoder: pretrained encoder 47 | :param decoder: pretrained decoder 48 | :return: 49 | ''' 50 | max_length = 20 51 | 52 | encoder_hidden = encoder.initHidden() 53 | input_length = input_variable.size()[0] 54 | target_length = 25 55 | encoder_outputs = Variable(torch.zeros(max_length, encoder.hidden_size)) 56 | 57 | for ei in range(input_length): 58 | encoder_output, encoder_hidden = encoder(input_variable[ei], encoder_hidden) 59 | encoder_outputs[ei] = encoder_output[0][0] 60 | 61 | decoder_input = Variable(torch.LongTensor([[vocab.SOS_token]])) 62 | decoder_hidden = encoder_hidden 63 | 64 | 65 | decoder_outputs = [] 66 | for di in range(target_length): 67 | decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden) 68 | topv, topi = decoder_output.data.topk(1) 69 | ni = topi[0][0] 70 | 71 | decoder_outputs.append(ni) 72 | 73 | decoder_input = Variable(torch.LongTensor([[ni]])) 74 | if ni == vocab.EOS_token: 75 | break 76 | 77 | return decoder_outputs 78 | 79 | 80 | questions = read_questions(PATH) 81 | questions = q2idx(questions) 82 | 83 | # encoder = torch.load('encoder.pt') 84 | # decoder = torch.load('decoder.pt') 85 | 86 | encoder = torch.load('experiment_results/encoder_7500t3.pt') 87 | decoder = torch.load('experiment_results/decoder_7500t3.pt') 88 | 89 | encoder.eval() 90 | decoder.eval() 91 | 92 | print 'Model loaded!' 93 | 94 | 95 | for q in questions: 96 | a = answers(q, encoder, decoder) 97 | answer = [] 98 | for i in a: 99 | answer.append(vocab.index_to_word(i, task)) 100 | aSen = ' '.join(answer) 101 | print aSen 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /scripts/car_data_io.py: -------------------------------------------------------------------------------- 1 | import json 2 | from nltk import word_tokenize 3 | import pandas as pd 4 | 5 | def load_json_dataset(path): 6 | ''' 7 | Load the in-car dataset as it is 8 | :param path: path to train/validation/test.json 9 | :return: the json file 10 | ''' 11 | with open(path) as f: 12 | data = json.load(f) 13 | print path + ' file loaded!!' 14 | return data 15 | 16 | 17 | def look4str(u, df): 18 | a = df['addrs'].apply(lambda x: x in u) 19 | b = df['pois'].apply(lambda x: x in u) 20 | a = df[a]['addrs'].as_matrix() 21 | b = df[b]['pois'].as_matrix() 22 | 23 | if len(a) != 0: 24 | u = u.replace(a[0], 'addr') 25 | if len(b) != 0: 26 | u = u.replace(b[0], 'poi') 27 | return u 28 | 29 | 30 | def load_incar_data(data_json): 31 | ''' 32 | :param data_json: a json file loaded from .json 33 | :return: 34 | navigate/weather/schedule_data, three lists for the three tasks 35 | each list is a list of dialogues, and each dialogue is a list of turns [(u1, r1), (u2, r2)...] 36 | each utterance/response is a list of tokens 37 | ''' 38 | lookup = pd.read_csv('datasets/incar_addr_poi.csv') 39 | navigate_data = [] 40 | schedule_data = [] 41 | weather_data = [] 42 | kbs = [] 43 | 44 | uu = [] 45 | rr = [] 46 | 47 | for dialogue in data_json: 48 | dia = [] 49 | u = None 50 | r = None 51 | for turn in dialogue['dialogue']: 52 | if turn['turn'] == 'driver': 53 | u = turn['data']['utterance'] 54 | u = look4str(u, lookup) # Comment out this line to get raw dataset without replacement 55 | u = word_tokenize(u.lower()) 56 | 57 | uu.append(len(u)) 58 | if turn['turn'] == 'assistant': 59 | r = turn['data']['utterance'] 60 | r = look4str(r, lookup) # Comment out this line to get raw dataset without replacement 61 | r = word_tokenize(r.lower()) 62 | 63 | rr.append(len(r)) 64 | if len(r) == 95: 65 | print r 66 | dia.append((u, r)) 67 | 68 | if dialogue['scenario']['task']['intent'] == 'navigate': 69 | navigate_data.append(dia) 70 | elif dialogue['scenario']['task']['intent'] == 'schedule': 71 | schedule_data.append(dia) 72 | elif dialogue['scenario']['task']['intent'] == 'weather': 73 | weather_data.append(dia) 74 | else: 75 | print dialogue['scenario']['task']['intent'] 76 | 77 | kbs.append(dialogue['scenario']['kb']) 78 | print 'Loaded %i navigate data!'%len(navigate_data) 79 | print 'Loaded %i schedule data!'%len(schedule_data) 80 | print 'Loaded %i weather data!'%len(weather_data) 81 | 82 | print max(uu) 83 | print max(rr) 84 | 85 | return navigate_data, weather_data, schedule_data, kbs 86 | 87 | files = ['datasets/in_car/train.json','datasets/in_car/dev.json', 88 | 'datasets/in_car/test.json'] 89 | 90 | for file in files: 91 | data = load_json_dataset(file) 92 | navigates, weathers, schedules, _ = load_incar_data(data) -------------------------------------------------------------------------------- /scripts/baseline/tf_idf.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This is the tf-idf baseline for the chatbot. 3 | Candidates loaded from dialog-babi-task6-dstc2-candidates.txt 4 | Test data loaded from 'dialog-babi-task6-dstc2-tst.txt' 5 | 6 | ''' 7 | import os 8 | import sys 9 | sys.path.append('.') 10 | from sklearn.feature_extraction.text import TfidfVectorizer 11 | from sklearn.metrics.pairwise import cosine_similarity 12 | from utils.internal.data_io import parse_dialogue_QA, parse_candidates 13 | from scipy.sparse import dok_matrix 14 | import numpy as np 15 | 16 | DIRECTORY = 'datasets/restaurants' 17 | TEST_FILES = 'dialog-babi-task6-dstc2-tst.txt' 18 | RESPOND_CANDIDATE_FILE = 'dialog-babi-task6-dstc2-candidates.txt' 19 | 20 | # Step 0. Build the tfidf vectors 21 | MAX_FEATURES = 2000 22 | vectorizer = TfidfVectorizer(max_df=1.0, max_features=MAX_FEATURES, 23 | min_df=1, stop_words='english', 24 | use_idf=True) 25 | 26 | 27 | # Step 1. Generate the tf-idf weighted matrix for the candidates 28 | with open(os.path.join(DIRECTORY, RESPOND_CANDIDATE_FILE)) as f: 29 | candidates_tf_idf = vectorizer.fit_transform(f) 30 | # sparse matrix, [n_samples, n_features]. Tf-idf-weighted document-term matrix. 31 | 32 | # Put all candidates into a list (and generate a list of string for future lookup) 33 | with open(os.path.join(DIRECTORY, RESPOND_CANDIDATE_FILE)) as f: 34 | candidates = parse_candidates(f.readlines()) 35 | 36 | 37 | # Load the dialog in format of [Dialogue 1, Dialogue 2...] 38 | # Each dialog is in format of [(u1, u2, u3...), (c1, c2, c3...] 39 | with open(os.path.join(DIRECTORY, TEST_FILES)) as f: 40 | qa, _ = parse_dialogue_QA(f.readlines(), False) 41 | 42 | 43 | 44 | all_queries = [] 45 | total_queries = 0 46 | # Count total queries 47 | for dialog in qa: 48 | queries, reponses = dialog 49 | for query in queries: 50 | all_queries.append(query) 51 | total_queries += 1 52 | 53 | 54 | # Now let's load the test data into a sparse matrix by counting the frequency of each word 55 | # The weights have been already added to the candidates. Frequency vectors will work for the test data 56 | S = dok_matrix((total_queries, candidates_tf_idf.shape[1]), dtype=np.float32) 57 | 58 | for i in range(len(all_queries)): 59 | for word in all_queries[i].split(): 60 | if word in vectorizer.vocabulary_: 61 | idx = vectorizer.vocabulary_[word] 62 | S[i, idx] += 1 63 | 64 | # Calculate the cosine similarity between the test data and the candidates 65 | cosine_similarity_matrix = cosine_similarity(S, candidates_tf_idf) 66 | best_candidate_idx = np.argmax(cosine_similarity_matrix, axis=1) 67 | 68 | 69 | # Output the examples and calculate the accuracy per response 70 | match = 0 71 | curr_idx = -1 72 | for dialog in qa: 73 | queries, reponses = dialog 74 | for i in range(len(queries)): 75 | curr_idx += 1 76 | print 'Questions: ', queries[i] 77 | print 'True response: ', reponses[i] 78 | print 'TF-IDF response: ', candidates[best_candidate_idx[curr_idx]][0] 79 | if candidates[best_candidate_idx[curr_idx]][0].split() == reponses[i].split(): 80 | match += 1 81 | 82 | 83 | print match * 1.0/len(all_queries) 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /scripts/match_feature_digits.py: -------------------------------------------------------------------------------- 1 | import json 2 | import torch 3 | import utils.internal.vocabulary as vocab 4 | 5 | # Run this file from the root folder, not from /scripts 6 | vocab = json.load( open("datasets/res_vocab.json", "r") ) 7 | 8 | # short list include 10, full list has 24 cuisines, missing has 51 cuisines 9 | # cuisines = ["british", "cantonese", "french", "indian", "italian", 10 | # "japanese", "korean", "spanish", "thai", "vietnamese"] 11 | cuisines = ['portuguese', 'mexican', 'chinese', 'mediterranean', 'japanese', 12 | 'spanish', 'vietnamese', 'gastropub', 'north_american', 'indian', 13 | 'asian_oriental', 'international', 'korean', 'european', 'bistro', 14 | 'french', 'fusion', 'thai', 'lebanese', 'seafood', 'turkish', 15 | 'british', 'african', 'italian'] 16 | missing = ['irish', 'singaporean', 'bistro', 'german', 'cantonese', 17 | 'australasian', 'kosher', 'moroccan', 'crossover', 'tuscan', 18 | 'persian', 'halal', 'scottish', 'polish', 'corsica', 19 | 'swedish', 'traditional', 'unusual', 'caribbean', 'romanian', 20 | 'creative', 'brazilian', 'jamaican', 'australian', 'christmas', 21 | 'belgian', 'danish', 'indonesian', 'austrian', 'hungarian', 22 | 'welsh', 'panasian', 'catalan', 'fusion', 'polynesian', 'russian', 23 | 'world', 'afghan', 'canapes', 'basque', 'cuban', 'vegetarian', 24 | 'malaysian', 'scandinavian', 'venetian', 'greek', 'steakhouse', 25 | 'english', 'swiss', 'barbeque'] 26 | locations = ["beijing", "seoul", "tokyo", "paris", "madrid", 27 | "hanoi", "bangkok", "rome", "london", "bombay"] 28 | prices = ["cheap", "moderate", "expensive"] 29 | sizes = ["two", "four", "six", "eight"] # party size 30 | PHONE_token = "" 31 | ADDR_token = "" 32 | 33 | vocab_size = len(vocab) 34 | print("vocab_size: {}".format(vocab_size) ) 35 | match_features = torch.zeros((vocab_size, 8)) 36 | 37 | for idx, token in enumerate(vocab): 38 | if token in cuisines: 39 | match_features[idx, 0] = 1 40 | if token in missing: 41 | match_features[idx, 1] = 1 42 | if token in locations: 43 | match_features[idx, 2] = 1 44 | if token in prices: 45 | match_features[idx, 3] = 1 46 | if token in sizes: 47 | match_features[idx, 4] = 1 48 | if token == PHONE_token: 49 | match_features[idx, 5] = 1 50 | if token == ADDR_token: 51 | match_features[idx, 6] = 1 52 | else: 53 | match_features[idx, 7] = 1 54 | 55 | torch.save(match_features, 'datasets/restaurants/match_features.pt') 56 | 57 | ''' 58 | # ratings = [str(x) for x in range(1,9)] # star rating 59 | 60 | json.dump(phrases, open("phrases.json", "w")) 61 | phrases = {"cuisines":[], "missing":[], "locations":[], "prices":[], "sizes":[]} 62 | digit = res_vocab.index(token) 63 | phrases["sizes"].append(digit) 64 | print("{0}: {1}".format(token, digit) ) 65 | 66 | def add_match_feature(sentence, use_cuda): 67 | matches = identify_matches(sentence) 68 | encoding_suffix = [] 69 | for match in matches: 70 | thing = match_embeddings[match] 71 | encoding_suffix.extend(thing) 72 | reshaped = encoding_suffix.view(1,-1,8) 73 | # if use_cuda: reshaped.cuda() 74 | 75 | return sentence.extend(reshaped, axis=2) 76 | ''' 77 | -------------------------------------------------------------------------------- /objects/blocks/basics.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | from torch import optim 4 | import torch.nn.functional as F 5 | 6 | from objects.components import var, device 7 | from objects.blocks.base import BaseBeliefTracker 8 | from utils.external.reader import get_glove_name 9 | 10 | class BasicClassifer(BaseBeliefTracker): 11 | def __init__(self, encoder, ff_network, args): 12 | super().__init__(args) 13 | self.encoder = encoder.to(device) 14 | self.decoder = ff_network.to(device) 15 | self.model_type = "basic" 16 | 17 | def forward(self, sources, hidden): 18 | self.encoder.rnn.flatten_parameters() 19 | encoder_outputs, hidden = self.encoder(sources, hidden) 20 | return self.decoder(encoder_outputs[0]) 21 | 22 | class PretrainedEmbeddingsModel(nn.Module): 23 | """ 24 | Base class that allows pretrained embedding to be loaded from 25 | a vocabulary. Assumes the wrapper class will initialize the embeddings 26 | """ 27 | def load_embeddings(self, vocab): 28 | if self.opt.pt != "none": 29 | # Get glove vector file name 30 | name = get_glove_name(self.opt, "tokens") 31 | print("Loading embeddings from {}".format(name)) 32 | 33 | # Load glove vectors 34 | with open(name, "r") as f: 35 | token_words = pickle.load(f) 36 | 37 | # Assign glove vectors to correct word in vocab 38 | for i, word in vocab.iteritems(): 39 | 40 | # If vocab word is meaning less, set embedding to 0 41 | if word in ["", "", "", ""]: 42 | self.embeddings.weight.data[i].zero_() 43 | continue 44 | 45 | if self.is_cuda: 46 | vec = torch.cuda.FloatTensor(token_words[word]) 47 | else: 48 | vec = torch.FloatTensor(token_words[word]) 49 | 50 | # Set embedding in embedding module 51 | self.embeddings.weight.data[i] = vec 52 | 53 | class WeightedBOW(PretrainedEmbeddingsModel): 54 | """ 55 | Indexes a set of word embeddings for a sequence of words it receives 56 | as input. Weighs each of these embeddings by a learned mask 57 | and returns the sum 58 | 59 | Initialization Args: 60 | opt.vSize: number of embeddings to initialize 61 | opt.hSize: size of embeddings to initialize 62 | opt.dpt: dropout probability after the embedding layer (default = 0) 63 | max_size: maximum number of masking functions 64 | 65 | Input: 66 | input: 3-dimensional tensor of batch_size x seq_len x embed_size 67 | 68 | Output: 69 | 2-dimensional tensor of size batch_size x embed_size 70 | 71 | """ 72 | def __init__(self, opt, max_size): 73 | super(WeightedBOW, self).__init__() 74 | self.embeddings = nn.Embedding(opt.vSize, opt.hSize, padding_idx=0) 75 | 76 | self.weights = nn.Parameter( 77 | torch.FloatTensor(max_size, opt.hSize).fill_(1)) 78 | 79 | self.dropout = nn.Dropout(opt.dpt) 80 | 81 | self.is_cuda = False 82 | self.max_size = max_size 83 | self.opt = opt 84 | 85 | def forward(self, input): 86 | batch_size = input.size(0) 87 | length = input.size(1) 88 | 89 | embed = self.embeddings(input) 90 | dropped = self.dropout(embed) 91 | 92 | # Multiply dropped embeddings by mask weights 93 | w_dropped = dropped * self.weights[:length, :].unsqueeze(0).repeat( 94 | batch_size, 1, 1) 95 | 96 | return torch.sum(w_dropped, 1).view(batch_size, self.opt.hSize), None 97 | 98 | def cuda(self, device_id): 99 | super(WeightedBOW, self).cuda(device_id) 100 | self.weights.cuda(device_id) 101 | self.is_cuda = True -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Framework for training and evaluating dialogue agents 2 | 3 | To get started, you should create your own "datasets" and "results" directories and download the appropriate data. Training has worked before with Woz2, DSTC2, Stanford In-Car dataset, Maluuba Frames Corpus, MultiWoz, FB bAbI Dialog, and Microsoft E2E Dialogue Challenge. Although a handful of these now are deprecated. Next, there are execution shell scripts in the `execute` folder which can be run for various purposes. 4 | 5 | Much of the code is in flux, so please feel free to make pull requests or leave suggestions! 6 | 7 | ### Major Concepts 8 | Every experiment or trial is considered a system. Systems have different versions and the system in production should always be the latest stable version. 9 | Each system is made up of objects and operators. 10 | * Objects - the building blocks of the system. They are the things that get acted on and transformed. Typical examples are the Dataset and Model. The dataset is often made up of Dialogue objects, which are in turn made up of Utterance objects. End-to-End Models are often made up of Intent Tracker, Knowledge Base Operator, Policy Manager and Text Generator modules. 11 | - blocks: these are the most basic building blocks that most other items import 12 | - models: these hold some of the more typical models seen in papers, such as a Transformer or Seq2Seq 13 | - modules: these wrap around a model and offer semantic meaning, such as a Intent Tracker wrapped around a GLAD model 14 | * Operators - the main actors in the system. They are the things that take action and transform objects. These items are divided by the phase of operation. 15 | - preprocessing: loading and pre-processing data 16 | - learning: trainining models 17 | - evaluation: running qualitative, quantitative, and visual evaluation (ie. plottting) 18 | * Session - a session is a special type of object since there is a time component involved. A system might spin up two sessions to interact with each other, such as a agent session and user session. These sessions might last indefinitely, and a system might have multiple concurrent sessions running at once. (To be developed) 19 | 20 | #### Belief Tracking 21 | Arguably the most important task of a dialogue agent is to understand the user intent. Intents are broken down into five pieces: 22 | 1. domain 23 | 2. dialogue act 24 | 3. slot 25 | 4. relation 26 | 5. value 27 | 28 | The vast majority of systems assume that the domain and sub-domain are given and consequently are ignored during prediction, instead focusing on only 'act(slot=value)'. (See Victor's GLAD model and Hannah's xIntent from Event2Mind) 29 | 30 | #### Policy Management 31 | The policy manager is formalized as a POMDP where the latent state is the underlying user intent. The actual dialogue state is also composed of other information such as dialogue context, turn count and complete semantic frame. Agent actions are defined by the ontology for any given domain. Clarification is studied by expanding the question types to include: 32 | 1. conventional clarification request - what did you want? I understood almost nothing 33 | 2. partial clarification requests - what was the area you mentioned? I understood one piece of information 34 | 3. confirmation through mention of alternatives - did you say the north part of town? I only misunderstood one piece of information 35 | 4. reformulation of information - so basically you want asian food, right? I understood almost everything 36 | 37 | In conjunction with the policy manager is the user simulator which makes training an RL agent feasible (Xiujin's D3Q stuff). 38 | 39 | #### Text Generation 40 | Currently, template based mechanisms or straight-forward LSTM decoders are used for response generation. This will be expanded in the future to include ability to add in diversity, persona (Jiwei's papers) and other desirable traits based on Grice's maxims (Ari, Jan, Max and others) 41 | 42 | ### Order of Execution 43 | 1. Utils should always load first since they are used everywhere 44 | 2. Objects will load next since they are shared across Operators 45 | 3. Operator modules are found in Preprocess, Learn and Evaluate 46 | 4. Run.py is the true starting point, it will import Operators to perform tasks 47 | 5. The system will be chosen which will perform the actual operations 48 | -------------------------------------------------------------------------------- /objects/models/glad.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | from torch import optim 4 | import torch.nn.functional as F 5 | 6 | from objects.blocks.attention import Attention 7 | from objects.components import device 8 | import pdb 9 | 10 | # the GlobalLocalModel model described in https://arxiv.org/abs/1805.09655. 11 | class GLAD(nn.Module): 12 | def __init__(self, args, ontology, vocab, Eword, GLADEncoder): 13 | super().__init__() 14 | self.optimizer = None 15 | self.model_type = "glad" 16 | self.attend = Attention() 17 | 18 | self.demb = args.embedding_size # aka embedding dimension 19 | self.dhid = args.hidden_dim # aka hidden state dimension 20 | dropout = {key: args.drop_prob for key in ["emb", "local", "global"]} 21 | self.drop_rate = args.drop_prob 22 | 23 | self.vocab = vocab 24 | self.ontology = ontology 25 | if args.pretrained: 26 | self.embedding = nn.Embedding.from_pretrained(torch.FloatTensor(Eword)) 27 | else: 28 | self.embedding = nn.Embedding(len(vocab), self.demb) # (num_embeddings, embedding_dim) 29 | 30 | self.utt_encoder = GLADEncoder(self.demb, self.dhid, ontology.slots, dropout) 31 | self.act_encoder = GLADEncoder(self.demb, self.dhid, ontology.slots, dropout) 32 | self.ont_encoder = GLADEncoder(self.demb, self.dhid, ontology.slots, dropout) 33 | self.utt_scorer = nn.Linear(2 * self.dhid, 1) 34 | self.score_weight = nn.Parameter(torch.Tensor([0.5])) 35 | 36 | def forward(self, batch): 37 | # convert to variables and look up embeddings 38 | eos = self.vocab.word2index('') 39 | utterance, utterance_len = self.pad([e.num['utterance'] for e in batch], self.embedding, pad=eos) 40 | acts = [self.pad(e.num['agent_actions'], self.embedding, pad=eos) for e in batch] 41 | ontology = {s: self.pad(v, self.embedding, pad=eos) for s, v in self.ontology.num.items()} 42 | 43 | ys = {} 44 | for s in self.ontology.slots: 45 | # for each slot, compute the scores for each value 46 | H_utt, c_utt = self.utt_encoder(utterance, utterance_len, slot=s) 47 | # H_utt: torch.Size([50, 30, 400]) batch_size x seq_len x embed_dim 48 | # c_utt: torch.Size([50, 400]) 49 | _, C_acts = list(zip(*[self.act_encoder(a, a_len, slot=s) for a, a_len in acts])) 50 | _, C_vals = self.ont_encoder(ontology[s][0], ontology[s][1], slot=s) 51 | # C_acts is list of length 50, a single c_act is size([1, 400]) 52 | # C_vals is list of length 7, a single c_val is size([400]) 53 | 54 | # compute the utterance score 55 | y_utts = [] 56 | q_utts = [] 57 | for i, c_val in enumerate(C_vals): 58 | q_utt = self.attend(H_utt, c_val.unsqueeze(0).expand( 59 | len(batch), *c_val.size()), lengths=utterance_len) 60 | q_utts.append(q_utt) # torch.Size([50, 400]) 61 | y_utts = self.utt_scorer(torch.stack(q_utts, dim=1)).squeeze(2) 62 | 63 | # compute the previous action score 64 | q_acts = [] 65 | for j, C_act in enumerate(C_acts): 66 | q_act = self.attend(C_act.unsqueeze(0), c_utt[j].unsqueeze(0), [C_act.size(0)]) 67 | q_acts.append(q_act) # torch.Size([1, 400]) 68 | # (50x7) = (50, 400) x (400, 7) 69 | y_acts = torch.cat(q_acts, dim=0).mm(C_vals.transpose(0, 1)) 70 | 71 | # combine the scores 72 | # y_acts: torch.Size([50, 7]) batch size, num of values for slot 's' 73 | # y_utts: torch.Size([50, 7]) for slot==area, there are 7 values 74 | ys[s] = torch.sigmoid(y_utts + self.score_weight * y_acts) 75 | # ys[s] = torch.sigmoid(c_utt.mm(C_vals.transpose(0, 1))) 76 | 77 | if self.training: 78 | # create label variable and compute loss 79 | labels = {s: [len(self.ontology.values[s]) * [0] for i in range(len(batch))] for s in self.ontology.slots} 80 | for i, example in enumerate(batch): 81 | for s, v in example.user_intent: 82 | labels[s][i][self.ontology.values[s].index(v)] = 1 83 | labels = {s: torch.Tensor(m).to(device) for s, m in labels.items()} 84 | 85 | loss = 0 86 | for s in self.ontology.slots: 87 | loss += self.criterion(ys[s], labels[s]) 88 | else: 89 | loss = torch.Tensor([0]).to(device) 90 | return loss, {s: v.data.tolist() for s, v in ys.items()} 91 | 92 | def pad(self, seqs, emb, pad=0): 93 | lens = [len(s) for s in seqs] 94 | max_len = max(lens) 95 | padded = torch.LongTensor([s + (max_len-l) * [pad] for s, l in zip(seqs, lens)]) 96 | # return emb(padded.to(device)), lens 97 | out = F.dropout(emb(padded.to(device)), self.drop_rate) 98 | return out, lens 99 | -------------------------------------------------------------------------------- /scripts/accuracy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals, division 3 | from utils.internal.arguments import solicit_args 4 | from io import open 5 | import unicodedata 6 | import string 7 | import re 8 | import random 9 | import json 10 | import sys 11 | import time as tm 12 | 13 | import torch 14 | import torch.nn as nn 15 | from torch.autograd import Variable 16 | 17 | import utils.internal.data_io as data_io 18 | from utils.external.clock import * 19 | from utils.external.preprocessers import * 20 | from utils.internal.bleu import BLEU 21 | 22 | use_cuda = torch.cuda.is_available() 23 | MAX_LENGTH = 8 24 | 25 | def evaluate(input_variable, target_variable, encoder, decoder, max_length, 26 | task, show_results): 27 | encoder_hidden = encoder.initHidden() 28 | 29 | input_length = input_variable.size()[0] 30 | target_length = target_variable.size()[0] 31 | encoder_outputs = Variable(torch.zeros(max_length, encoder.hidden_size)) 32 | encoder_outputs = encoder_outputs.cuda() if use_cuda else encoder_outputs 33 | 34 | for ei in range(min(max_length, input_length)): 35 | encoder_output, encoder_hidden = encoder(input_variable[ei], encoder_hidden) 36 | encoder_outputs[ei] = encoder_output[0][0] 37 | 38 | decoder_input = Variable(torch.LongTensor([[vocab.SOS_token]])) 39 | decoder_input = decoder_input.cuda() if use_cuda else decoder_input 40 | decoder_hidden = encoder_hidden 41 | 42 | predicts = [] 43 | for di in range(target_length): 44 | decoder_output, decoder_hidden, attn_weights = decoder(decoder_input, decoder_hidden, \ 45 | encoder_output, encoder_outputs) 46 | 47 | # decoder_input, decoder_hidden, last_enc_hidden_state, encoder_outputs) To be used later for attention 48 | topv, topi = decoder_output.data.topk(1) 49 | ni = topi[0][0] 50 | decoder_input = Variable(torch.LongTensor([[ni]])) 51 | decoder_input = decoder_input.cuda() if use_cuda else decoder_input 52 | predicts.append(ni) 53 | 54 | queries = input_variable.data.tolist() 55 | targets = target_variable.data.tolist() 56 | 57 | predict_tokens = [vocab.index_to_word(x, task) for x in predicts] 58 | target_tokens = [vocab.index_to_word(y[0], task) for y in targets] 59 | bleu_score = BLEU.compute(predict_tokens, target_tokens) 60 | 61 | if show_results: 62 | qry_words = " ".join([vocab.index_to_word(z[0], task) for z in queries]) 63 | print("user query: {}".format(qry_words) ) 64 | pred_words = " ".join(predict_tokens) 65 | print("predic: {}".format(pred_words) ) 66 | tar_words = " ".join(target_tokens) 67 | print("target: {}".format(tar_words) ) 68 | 69 | turn_success = [pred == tar[0] for pred, tar in zip(predicts, targets)] 70 | return all(turn_success), bleu_score 71 | 72 | if __name__ == "__main__": 73 | args = solicit_args() 74 | task = 'car' if args.task_name in ['navigate', 'schedule', 'weather'] else 'res' 75 | val_data, val_candidates, _ = data_io.load_dataset(args.task_name, "dev", False) 76 | max_length = 30 77 | val_variables = collect_dialogues(val_data, task=task)[0:2000] 78 | encoder = torch.load('results/bid_best_en.pt') 79 | decoder = torch.load('results/bid_best_de.pt') 80 | 81 | # single_dialog_success and single_turn_success are scalars with values 0 or 1 82 | # dialog_success and turn_success are lists that hold "single success" 83 | # overall_dialog and overall_turn are lists that hold accuracy totals 84 | # per_dialog_accuracy and per_turn_accuracy are floats between 0% to 100% 85 | overall_dialog = [] 86 | overall_turn = [] 87 | overall_bleu = [] 88 | dialog_success = [] 89 | 90 | show_results = False 91 | for input_var, output_var in val_variables: 92 | first_token = input_var[0].data[0] 93 | # first token is a turn counter, so when it equals 1, that means we are in a new dialogue 94 | if first_token == 1 and len(dialog_success) > 1: 95 | single_dialog_success = all(dialog_success) 96 | overall_dialog.append(single_dialog_success) 97 | dialog_success = [] 98 | if random.random() < 0.01: 99 | show_results = True 100 | print "New Dialogue ------------------" 101 | else: 102 | show_results = False 103 | 104 | single_turn_success, single_bleu_score = evaluate(input_var, output_var, 105 | encoder, decoder, max_length, task, show_results) 106 | dialog_success.append(single_turn_success) 107 | overall_turn.append(single_turn_success) 108 | overall_bleu.append(single_bleu_score) 109 | 110 | averaged_bleu = 100 * float(sum(overall_bleu)) / len(overall_bleu) 111 | print("BLEU Score: {:.2f}".format(averaged_bleu) ) 112 | per_turn_accuracy = 100 * float(sum(overall_turn)) / len(overall_turn) 113 | print("Per Turn Accuracy: {:.2f}%".format(per_turn_accuracy) ) 114 | per_dialog_accuracy = 100 * float(sum(overall_dialog)) / len(overall_dialog) 115 | print("Per Dialog Accuracy: {:.2f}%".format(per_dialog_accuracy) ) 116 | -------------------------------------------------------------------------------- /operators/evaluate/tester.py: -------------------------------------------------------------------------------- 1 | import pdb, sys, os 2 | import torch 3 | from tqdm import tqdm as progress_bar 4 | from objects.components import var, run_inference 5 | import numpy as np 6 | 7 | # Used for testing, as opposed to training or validation 8 | class Tester(object): 9 | def __init__(self, args, processor): 10 | self.test_data = processor.test_data 11 | 12 | model_path = "results/{0}_{1}.pt".format(args.model_name, args.suffix) 13 | if os.path.exists(model_path): 14 | self.model = torch.load(model_path) 15 | self.model.eval() 16 | else: 17 | raise FileNotFoundError('Please train and save a dialogue model first.') 18 | 19 | def test(self, *metrics): 20 | predictions, targets = [], [] 21 | for test_pair in self.test_data: #progress_bar(self.test_data): 22 | test_input, test_output = test_pair 23 | _, pred, _ = run_inference(self.model, test_input, \ 24 | test_output, criterion=None, teach_ratio=0) 25 | targets.append(test_output.tolist()[0]) 26 | predictions.append(pred.item()) 27 | 28 | classes = list(set(targets)) 29 | 30 | for metric in metrics: 31 | getattr(self, metric)(classes, predictions, targets) 32 | sys.exit() 33 | 34 | def just_loss(self): 35 | criterion = NegLL_Loss() 36 | rate_of_success = [] 37 | rate_of_loss = [] 38 | for iteration, test_pair in enumerate(self.test_data): 39 | if iteration % 31 == 0: 40 | test_input, test_output = test_pair 41 | loss, _, success = validate(test_input, test_output, encoder, decoder, criterion, task) 42 | if success: 43 | rate_of_success.append(1) 44 | else: 45 | rate_of_success.append(0) 46 | 47 | rate_of_loss.append(loss) 48 | ros = np.average(rate_of_success) 49 | rol = np.average(rate_of_loss) 50 | print("Loss: {} and Success: {:.3f}".format(rol, ros)) 51 | 52 | def accuracy(self, task): 53 | batch_test_loss, batch_bleu, batch_success = [], [], [] 54 | bleu_scores, accuracy = [], [] 55 | 56 | for test_pair in progress_bar(self.test_data): 57 | test_input, test_output = test_pair 58 | loss, predictions, visual = run_inference(self.model, test_input, \ 59 | test_output, criterion=NLLLoss(), teach_ratio=0) 60 | 61 | targets = test_output.data.tolist() 62 | predicted_tokens = [vocab.index_to_word(x, task) for x in predictions] 63 | target_tokens = [vocab.index_to_word(z[0], task) for z in targets] 64 | 65 | test_loss = loss.data[0] / test_output.size()[0] 66 | bleu_score = BLEU.compute(predicted_tokens, target_tokens) 67 | turn_success = all([pred == tar[0] for pred, tar in zip(predictions, targets)]) 68 | 69 | batch_test_loss.append(test_loss) 70 | batch_bleu.append(bleu_score) 71 | batch_success.append(turn_success) 72 | 73 | return batch_processing(batch_test_loss, batch_bleu, batch_success) 74 | 75 | def macro_f1(self, classes, predictions, targets): 76 | total_f1 = [] 77 | for cls in progress_bar(classes): 78 | 79 | true_positive, false_positive, false_negative, true_negative = 0,0,0,0 80 | for pred, tar in zip(predictions, targets): 81 | if pred == cls and tar == cls: 82 | true_positive += 1 83 | elif pred == cls and tar != cls: 84 | false_positive += 1 85 | elif pred != cls and tar == cls: 86 | false_negative += 1 87 | elif pred != cls and tar != cls: 88 | true_negative += 1 89 | 90 | f1 = Tester.single_f1(true_positive, false_positive, false_negative) 91 | if f1 >= 0: 92 | total_f1.append(f1) 93 | 94 | macro = np.average(total_f1) 95 | print("Macro average is {:.3f}".format(macro)) 96 | return macro 97 | 98 | def micro_f1(self, classes, predictions, targets): 99 | true_positive, false_positive, false_negative, true_negative = 0,0,0,0 100 | 101 | for cls in progress_bar(classes): 102 | for pred, tar in zip(predictions, targets): 103 | if pred == cls and tar == cls: 104 | true_positive += 1 105 | elif pred == cls and tar != cls: 106 | false_positive += 1 107 | elif pred != cls and tar == cls: 108 | false_negative += 1 109 | elif pred != cls and tar != cls: 110 | true_negative += 1 111 | 112 | micro = Tester.single_f1(true_positive, false_positive, false_negative) 113 | print("Micro average is {:.3f}".format(micro)) 114 | return micro 115 | 116 | @staticmethod 117 | def single_f1(true_positive, false_positive, false_negative): 118 | precision_total = max(1, true_positive + false_positive) 119 | precision = true_positive / precision_total 120 | recall_total = max(1, true_positive + false_negative) 121 | recall = true_positive / recall_total 122 | if (true_positive + false_positive + false_negative == 0): 123 | return -1 124 | elif (precision + recall == 0): 125 | return 0 126 | else: 127 | return 2 * ((precision * recall) / (precision + recall)) 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /scripts/find_vocab_car.py: -------------------------------------------------------------------------------- 1 | from nltk import word_tokenize 2 | import json 3 | import pandas as pd 4 | 5 | files = ['datasets/in_car/train.json','datasets/in_car/dev.json', 6 | 'datasets/in_car/test.json'] 7 | entity = 'datasets/in_car/entities.json' 8 | 9 | 10 | def load_json_dataset(path): 11 | ''' 12 | Load the in-car dataset as it is 13 | :param path: path to train/validation/test.json 14 | :return: the json file 15 | ''' 16 | with open(path) as f: 17 | data = json.load(f) 18 | print path + ' file loaded!!' 19 | return data 20 | 21 | 22 | def load_incar_data(data_json): 23 | ''' 24 | :param data_json: a json file loaded from .json 25 | :return: 26 | navigate/weather/schedule_data, three lists for the three tasks 27 | each list is a list of dialogues, and each dialogue is a list of turns [(u1, r1), (u2, r2)...] 28 | each utterance/response is a list of tokens 29 | ''' 30 | navigate_data = [] 31 | schedule_data = [] 32 | weather_data = [] 33 | kbs = [] 34 | 35 | for dialogue in data_json: 36 | dia = [] 37 | u = None 38 | r = None 39 | for turn in dialogue['dialogue']: 40 | if turn['turn'] == 'driver': 41 | u = turn['data']['utterance'] 42 | 43 | if turn['turn'] == 'assistant': 44 | r = turn['data']['utterance'] 45 | dia.append((u, r)) 46 | u = None 47 | r = None 48 | 49 | if dialogue['scenario']['task']['intent'] == 'navigate': 50 | navigate_data.append(dia) 51 | elif dialogue['scenario']['task']['intent'] == 'schedule': 52 | schedule_data.append(dia) 53 | elif dialogue['scenario']['task']['intent'] == 'weather': 54 | weather_data.append(dia) 55 | else: 56 | print dialogue['scenario']['task']['intent'] 57 | kbs.append(dialogue['scenario']['kb']) 58 | 59 | print 'Loaded %i navigate data!'%len(navigate_data) 60 | print 'Loaded %i schedule data!'%len(schedule_data) 61 | print 'Loaded %i weather data!'%len(weather_data) 62 | 63 | return navigate_data, weather_data, schedule_data, kbs 64 | 65 | 66 | # Step 1. Get addresses and pois 67 | def save_addr_pois(): 68 | addrs = set() 69 | poiSet = set() 70 | 71 | data = load_json_dataset(entity) 72 | pois = data['poi'] 73 | 74 | for poi in pois: 75 | addrs.add(poi['address']) 76 | poiSet.add(poi['poi']) 77 | 78 | df = pd.DataFrame(data={'addrs':list(addrs), 'pois':list(poiSet)}) 79 | df.to_csv('datasets/incar_addr_poi.csv') 80 | 81 | 82 | def look4str(u, df): 83 | a = df['addrs'].apply(lambda x: x in u) 84 | b = df['pois'].apply(lambda x: x in u) 85 | a = df[a]['addrs'].as_matrix() 86 | b = df[b]['pois'].as_matrix() 87 | 88 | if len(a) != 0: 89 | u = u.replace(a[0], 'addr') 90 | if len(b) != 0: 91 | u = u.replace(b[0], 'poi') 92 | return u 93 | 94 | 95 | def dump_vocab(): 96 | lookup = pd.read_csv('datasets/incar_addr_poi.csv') 97 | 98 | vocab = set() 99 | specials = ['100.', '20.'] 100 | for key in specials: 101 | vocab.add(key) 102 | 103 | for file in files: 104 | print 'Now processing ' + file 105 | data = load_json_dataset(file) 106 | navigates, weathers, schedules, _ = load_incar_data(data) 107 | 108 | for navigate in navigates: 109 | for u, r in navigate: 110 | if u: 111 | u = look4str(u, lookup) 112 | for token in word_tokenize(u): 113 | vocab.add(token.lower()) 114 | 115 | r = look4str(r, lookup) 116 | for token in word_tokenize(r): 117 | vocab.add(token.lower()) 118 | 119 | print 'Finished navigations... len(vocab) = ', len(vocab) 120 | 121 | for weather in weathers: 122 | for u, r in weather: 123 | if u: 124 | for token in word_tokenize(u.lower()): 125 | vocab.add(token.lower()) 126 | for token in word_tokenize(r): 127 | vocab.add(token.lower()) 128 | 129 | print 'Finished weathers... len(vocab) = ', len(vocab) 130 | 131 | for schedule in schedules: 132 | for u, r in schedule: 133 | if u: 134 | for token in word_tokenize(u): 135 | vocab.add(token.lower()) 136 | for token in word_tokenize(r): 137 | vocab.add(token.lower()) 138 | 139 | 140 | print 'Done with ' + file + 'now, the vocab has %i words...'%len(vocab) 141 | 142 | vocab = list(vocab) 143 | vocab.sort() 144 | special_tokens = ["", "","","","","","", 145 | "","","","","","","", 146 | "","UNK", "SOS", "EOS", "api_call","poi", "addr"] 147 | all_tokens = special_tokens + vocab 148 | 149 | print len(all_tokens) 150 | 151 | json.dump(all_tokens, open("car_vocab.json", "w")) 152 | 153 | dump_vocab() -------------------------------------------------------------------------------- /objects/models/transformer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from torch import nn 3 | 4 | class Transformer(nn.Module): 5 | def __init__(self, vocab_size, hidden_size, n_layers, masked=False, max_len=30): 6 | super(Transformer, self).__init__() 7 | self.hidden_size = hidden_size 8 | self.scale_factor = math.sqrt(hidden_size) 9 | self.num_attention_heads = 8 # hardcoded since it won't change 10 | self.num_layers = n_layers # defaults to 6 to follow the paper 11 | self.positions = positional_encoding(hidden_size, max_len+1) 12 | 13 | self.dropout = nn.Dropout(0.2) 14 | self.masked = masked 15 | 16 | for head_idx in range(self.num_attention_heads): 17 | for vector_type in ['query', 'key', 'value']: 18 | head_name = "{0}_head_{1}".format(vector_type, head_idx) 19 | mask_name = "{0}_mask_{1}".format(vector_type, head_idx) 20 | head_in = self.hidden_size 21 | head_out = int(self.hidden_size / self.num_attention_heads) 22 | setattr(self, head_name, nn.Linear(head_in, head_out)) 23 | if masked: 24 | setattr(self, mask_name, nn.Linear(head_in, head_out)) 25 | 26 | self.pw_ffn_1 = nn.Linear(self.hidden_size, hidden_size) 27 | self.pw_ffn_2 = nn.Linear(self.hidden_size, hidden_size) 28 | try: 29 | self.layernorm = nn.LayerNorm(hidden_size, affine=False) 30 | except(AttributeError): 31 | self.layernorm = nn.BatchNorm1d(hidden_size, affine=False) 32 | 33 | def forward(self, inputs, encoder_outputs=None, di=None): 34 | # inputs will be seq_len, batch_size, hidden dim. However, our batch_size 35 | # is always one so we squeeze it out to keep calculations simpler 36 | position_emb = torch.tensor(self.positions[:len(inputs), :], requires_grad=False) 37 | # if batch_size > 1, self.positions[:len(inputs), :1, :inputs.size(2)].expand_as(inputs) 38 | transformer_input = inputs.squeeze() + position_emb 39 | k_v_input = self.dropout(transformer_input) 40 | 41 | for layer_idx in range(self.num_layers): 42 | if layer_idx > 0: 43 | transformer_input = self.dropout(transformer_output) 44 | 45 | if self.masked: 46 | masked_input = self.apply_mask(transformer_input, di) 47 | k_v_input = encoder_outputs 48 | 49 | mask_attn_heads = [] 50 | for j in range(self.num_attention_heads): 51 | Q = getattr(self, "query_mask_{}".format(j))(masked_input) 52 | K = getattr(self, "key_mask_{}".format(j))(k_v_input) 53 | V = getattr(self, "value_mask_{}".format(j))(k_v_input) 54 | mask_attn_heads.append(self.scaled_dot_product_attention(Q, K, V)) 55 | residual_connection = masked_input + torch.cat(mask_attn_heads, dim=1) 56 | masked_output = self.layernorm(residual_connection) 57 | transformer_input = self.dropout(masked_output) 58 | 59 | attn_heads = [] # don't create a new variable since it messes with the graph 60 | for idx in range(self.num_attention_heads): 61 | Q = getattr(self, "query_head_{}".format(idx))(transformer_input) 62 | K = getattr(self, "key_head_{}".format(idx))(k_v_input) 63 | V = getattr(self, "value_head_{}".format(idx))(k_v_input) 64 | attn_heads.append(self.scaled_dot_product_attention(Q, K, V)) 65 | residual_connection = transformer_input + torch.cat(attn_heads, dim=1) 66 | multihead_output = self.layernorm(residual_connection) 67 | 68 | pw_ffn_output = self.positionwise_ffn(multihead_output) 69 | transformer_output = self.layernorm(multihead_output + pw_ffn_output) 70 | 71 | return transformer_output 72 | 73 | def apply_mask(self, decoder_inputs, decoder_idx): 74 | mask = var(torch.zeros(( decoder_inputs.shape )), "variable") 75 | mask[:decoder_idx+1, :] = 1 76 | return decoder_inputs * mask 77 | # updated code for dealing with batch_size >1; lengths is a list, 78 | # where each element is an integer representing the number of tokens 79 | # for each sentence in the batch 80 | # batch_size = lengths.numel() 81 | # max_len = max_len or lengths.max() 82 | # return (torch.arange(0, max_len) 83 | # .type_as(lengths) 84 | # .repeat(batch_size, 1) 85 | # .lt(lengths.unsqueeze(1))) # less than 86 | 87 | def positionwise_ffn(self, multihead_output): 88 | ffn_1_output = F.relu(self.pw_ffn_1(multihead_output)) 89 | ffn_2_output = self.pw_ffn_2(ffn_1_output) 90 | return ffn_2_output 91 | 92 | def scaled_dot_product_attention(self, Q, K, V): 93 | # K should be seq_len, hidden_dim / 8 to start with, but will be transposed 94 | scaled_matmul = Q.matmul(K.transpose(0,1)) / self.scale_factor # batch_size x seq_len 95 | # (batch_size, hidden_dim) x (hidden_dim, seq_len) / broadcast integer 96 | attn_weights = F.softmax(scaled_matmul, dim=1) 97 | attn_context = attn_weights.matmul(V) # batch_size, hidden_dim 98 | return attn_context 99 | 100 | def positional_encoding(dim, max_len=5000): 101 | # Implementation based on "Attention Is All You Need" 102 | pe = torch.arange(0, max_len).unsqueeze(1).expand(max_len, dim) 103 | div_term = 1 / torch.pow(10000, torch.arange(0, dim * 2, 2) / dim) 104 | pe = pe * div_term.expand_as(pe) 105 | pe[:, 0::2] = torch.sin(pe[:, 0::2]) 106 | pe[:, 1::2] = torch.cos(pe[:, 1::2]) 107 | return pe # .unsqueeze(1) 108 | 109 | class TransformerXL(nn.Module): 110 | ''' 111 | Able to handle long-term recurrent connections 112 | ''' 113 | 114 | def __init__(self, vocab_size): 115 | pass -------------------------------------------------------------------------------- /objects/components.py: -------------------------------------------------------------------------------- 1 | from torch.nn.utils import clip_grad_norm_ 2 | from torch.nn.utils import rnn as rnn_utils 3 | from torch.nn import NLLLoss, parameter 4 | 5 | import utils.internal.vocabulary as vocab 6 | import utils.internal.initialization as data_io 7 | from utils.external.bleu import BLEU 8 | 9 | import torch 10 | import numpy as np 11 | import os, pdb, sys 12 | import re 13 | from tqdm import tqdm as progress_bar 14 | 15 | use_cuda = torch.cuda.is_available() 16 | device = torch.device("cuda" if use_cuda else "cpu") 17 | 18 | def var(data, dtype="float"): 19 | if dtype == "float": 20 | result = torch.Tensor(data) 21 | elif dtype == "long": 22 | result = torch.LongTensor(data) 23 | elif dtype == "variable": 24 | result = data 25 | return result.to(device) 26 | 27 | def clip_gradient(model, clip): 28 | if clip is None: return 29 | try: 30 | clip_grad_norm_(model.encoder.parameters(), clip) 31 | clip_grad_norm_(model.decoder.parameters(), clip) 32 | except(AttributeError): 33 | pass 34 | 35 | def get_saves(directory, early_stop): 36 | files = [f for f in os.listdir(directory) if f.endswith('.pt')] 37 | scores = [] 38 | for fname in files: 39 | re_str = r'dev_{}=([0-9\.]+)'.format(early_stop) 40 | dev_acc = re.findall(re_str, fname) 41 | if dev_acc: 42 | score = float(dev_acc[0].strip('.')) 43 | scores.append((score, os.path.join(directory, fname))) 44 | if not scores: 45 | print("Files found:", files) 46 | print("Search regex:", re_str) 47 | raise Exception('No checkpoints found in {}!'.format(directory)) 48 | scores.sort(key=lambda tup: tup[0], reverse=True) 49 | return scores 50 | 51 | def run_inference(model, batch): 52 | if model.model_type in ["basic", "dual", "multi"]: 53 | return basic_inference(model, sources, targets) 54 | elif model.model_type == "transformer": 55 | return transformer_inference(model, sources, targets) 56 | elif model.model_type == "glad": 57 | return model.forward(batch) 58 | else: 59 | assert(model.type == "seq2seq") 60 | 61 | sources = var(sources, dtype="variable") 62 | targets = var(targets, dtype="variable") 63 | enc_hidden = model.encoder.initHidden() 64 | enc_length = sources.shape[0] 65 | dec_length = targets.shape[0] 66 | 67 | loss = 0 68 | predictions = [] 69 | visual = torch.zeros(enc_length, dec_length) 70 | track = loss, predictions, visual, teach_ratio 71 | 72 | return model(sources, targets, enc_hidden, enc_length, dec_length, track) 73 | 74 | def basic_inference(model, batch): 75 | hidden = model.encoder.initHidden() 76 | output = model(sources, hidden) 77 | topv, topi = output.data.topk(2) 78 | pred = topi[0] # returns full list of predictions as a tensor 79 | # pred = topi[0][0] # instead to select just the first 80 | loss = model.criterion(output, targets) 81 | return loss, pred 82 | 83 | def transformer_inference(model, sources, targets, criterion): 84 | loss = 0 85 | predictions = [] 86 | encoder_outputs = model.encoder(var(sources, dtype="variable")) 87 | decoder_start = var([[vocab.SOS_token]], "list") 88 | decoder_tokens = var(targets, dtype="variable") 89 | decoder_inputs = torch.cat([decoder_start, decoder_tokens], dim=0) 90 | 91 | for di in range(targets.size()[0]): 92 | decoder_output = model.decoder(decoder_inputs, encoder_outputs, di) 93 | # we need to index into the output now since output is (seq_len, vocab) 94 | loss += criterion(decoder_output[di].view(1,-1), decoder_tokens[di]) 95 | 96 | topv, topi = decoder_output[di].data.topk(1) 97 | pdb.set_trace() 98 | ni = topi[0][0] 99 | predictions.append(ni) 100 | if ni == vocab.EOS_token: 101 | break 102 | 103 | return loss, predictions, None 104 | 105 | def run_rnn(rnn, inputs, lens): 106 | # sort by lens, argsort gives smallest to largest, [::-1] gives largest to smallest 107 | order = np.argsort(lens)[::-1].tolist() 108 | reindexed = inputs.index_select(0, inputs.data.new(order).long()) 109 | reindexed_lens = [lens[i] for i in order] 110 | packed = rnn_utils.pack_padded_sequence(reindexed, reindexed_lens, batch_first=True) 111 | # print("packed: {}".format(packed.shape)) 112 | outputs, _ = rnn(packed) 113 | # print("outputs: {}".format(outputs.shape)) 114 | # pdb.set_trace() 115 | padded, _ = rnn_utils.pad_packed_sequence(outputs, batch_first=True, padding_value=0.) 116 | reverse_order = np.argsort(order).tolist() 117 | recovered = padded.index_select(0, inputs.data.new(reverse_order).long()) 118 | # reindexed_lens = [lens[i] for i in order] 119 | # recovered_lens = [reindexed_lens[i] for i in reverse_order] 120 | # assert recovered_lens == lens 121 | return recovered 122 | 123 | def unique_identifier(summary, epoch, early_stop_metric): 124 | uid = 'epoch={epoch},train_{key}={train:.4f},dev_{key}={dev:.4f}.pt'.format( 125 | epoch=epoch, key=early_stop_metric, 126 | train=summary.get(f'train_{early_stop_metric}', 0.0), 127 | dev=summary[f'best_{early_stop_metric}'] ) 128 | return uid 129 | 130 | def show_dialogues(val_data, encoder, decoder, task): 131 | encoder.eval() 132 | decoder.eval() 133 | dialogues = data_io.select_consecutive_pairs(val_data, 5) 134 | 135 | for i, dialog in enumerate(dialogues): 136 | print("Dialogue Sample {} ------------".format(i)) 137 | for j, turn in enumerate(dialog): 138 | input_variable, output_variable = turn 139 | _, predictions, _ = run_inference(encoder, decoder, input_variable, \ 140 | output_variable, criterion=NLLLoss(), teach_ratio=0) 141 | sources = input_variable.data.tolist() 142 | targets = output_variable.data.tolist() 143 | 144 | source_tokens = [vocab.index_to_word(s[0], task) for s in sources] 145 | target_tokens = [vocab.index_to_word(t[0], task) for t in targets] 146 | pred_tokens = [vocab.index_to_word(p, task) for p in predictions] 147 | 148 | source = " ".join(source_tokens[:-1]) # Remove the 149 | target = " ".join(target_tokens[:-1]) 150 | pred = " ".join(pred_tokens[:-1]) 151 | print("User Query: {0}".format(source)) 152 | print("Target Response: {0}".format(target)) 153 | print("Predicted Response: {0}".format(pred)) 154 | print('') 155 | 156 | def match_embedding(vocab_size, hidden_size): 157 | match_tensor = torch.load('datasets/restaurants/match_features.pt') 158 | embed = torch.nn.Embedding(vocab_size, hidden_size) 159 | # Extract just the tensor inside the Embedding 160 | embed_tensor = embed.weight.data 161 | extended_tensor = torch.cat([embed_tensor, match_tensor], dim=1) 162 | # Set the weight of original embedding matrix with the new Parameter 163 | embed.weight = torch.nn.parameter.Parameter(extended_tensor) 164 | return embed 165 | -------------------------------------------------------------------------------- /operators/preprocess/preprocessor.py: -------------------------------------------------------------------------------- 1 | from objects.components import var 2 | from utils.internal.initialization import pickle_io 3 | import os, pdb 4 | 5 | class PreProcessor(object): 6 | def __init__(self, args, loader): 7 | self.loader = loader 8 | self.task = args.task 9 | self.datasets = loader.datasets 10 | self.vocab = loader.vocab 11 | self.ontology = loader.ontology 12 | self.max_turn = args.max_turn 13 | self.use_old_nlu = args.use_old_nlu 14 | 15 | if self.task in ['manage_policy', 'track_intent', 'end_to_end']: 16 | pass 17 | elif args.test_mode: 18 | self.prepare_examples("test", args.context) 19 | elif args.debug: 20 | self.load_debug_examples(loader.debug_dir) 21 | else: # normal training mode 22 | self.prepare_examples("train", args.context) 23 | self.prepare_examples("val", args.context) 24 | self.make_cache(loader.debug_dir) 25 | 26 | def input_output_cardinality(self): 27 | """ get the input size and output size to set model attributes""" 28 | if self.task == 'track_intent': 29 | return "self.vocab.ulary_size()", "self.vocab.label_size()" 30 | elif self.use_old_nlu: 31 | act_cardinality = 2 * len(self.loader.ontology.old_acts) 32 | slot_cardinality = 7 * len(self.loader.ontology.old_slots) 33 | other_count = 3 + self.max_turn + 5 # where does 5 come from? 34 | num_intents = act_cardinality + slot_cardinality + other_count 35 | num_actions = len(self.ontology.feasible_agent_actions) 36 | return num_intents, num_actions # (273, 30) 37 | elif self.task == 'manage_policy': 38 | act_cardinality = 2 * len(self.loader.ontology.acts) 39 | slot_cardinality = 7 * len(self.loader.ontology.slots) 40 | other_count = 3 + self.max_turn + 5 # where does 5 come from? 41 | num_intents = act_cardinality + slot_cardinality + other_count 42 | num_actions = len(self.ontology.feasible_agent_actions) 43 | return num_intents, num_actions # (273, 30) 44 | elif self.task == 'generate_text': 45 | num_actions = len(self.ontology.feasible_agent_actions) 46 | return num_actions, self.vocab.ulary_size() 47 | elif self.task == 'end_to_end': 48 | num_beliefs = sum([len(vals) for vals in self.ontology.values.values()]) 49 | # where 8 is the number of possible inform slots for the frame-rep 50 | act_len = len(self.ontology.old_acts) 51 | slot_len = len(self.ontology.old_slots) 52 | # slots is 10, -2 is removing act & request, 1 is adding taskcomplete 53 | inform_len = len(self.ontology.slots) - 2 + 1 54 | num_beliefs += (slot_len * 2) + act_len + inform_len 55 | # num_beliefs += len(ont['slots'])*3 + len(ont['acts']) 56 | num_beliefs += self.max_turn + 5 + 1 57 | num_actions = len(self.ontology.feasible_agent_actions) 58 | 59 | return num_beliefs, num_actions # (904, 30) # currently 10, 22? 60 | 61 | def prepare_examples(self, split, use_context): 62 | dataset = self.datasets[split] 63 | 64 | variables = [] 65 | for example in dataset: 66 | input_var = self.prepare_input(example["input_source"], use_context) 67 | output_var, double = self.prepare_output(example["output_target"]) 68 | if double: 69 | variables.append((input_var, output_var[0])) # append extra example 70 | output_var = output_var[1] # set output to be the second label 71 | variables.append((input_var, output_var)) 72 | 73 | self.datasets[split] = variables 74 | 75 | def prepare_input(self, source, use_context): 76 | tokens = [self.vocab.word_to_index(word) for word in source] 77 | return var(tokens, "long") 78 | 79 | def prepare_output(self, target): 80 | target = self._prepare_target(target) 81 | if len(target) == 1: 82 | target_index = self.vocab.label_to_index(target[0]) 83 | output_var = var([target_index], "long") 84 | return output_var, False 85 | elif len(target) == 2: 86 | target_indexes = [self.vocab.label_to_index(t) for t in target] 87 | output_vars = [var([ti], "long") for ti in target_indexes] 88 | return output_vars, True 89 | 90 | def load_debug_examples(self, debug_dir): 91 | data_path = os.path.join(debug_dir, "cache.pkl") 92 | debug_data = pickle_io(data_path, "load") 93 | self.train_data = debug_data["train"] 94 | self.val_data = debug_data["val"] 95 | 96 | def make_cache(self, debug_dir): 97 | data_path = os.path.join(debug_dir, "cache.pkl") 98 | if not os.path.exists(data_path): 99 | cache = { "train": self.train_data[0:14], "val": self.val_data[0:7] } 100 | pickle_io(data_path, "save", cache) 101 | print("{} saved successfully!".format(data_path)) 102 | 103 | def dialog_to_variable(self, dialog, dataset): 104 | # example is list of tuples, where each tuple is utterance and response 105 | # [(u1, r1), (u2, r2)...] 106 | dialog_pairs = [] 107 | for t_idx, turn in enumerate(dialog): 108 | utterance, wop = variable_from_sentence(turn[0], [t_idx+1], dataset) 109 | response, dop = variable_from_sentence(turn[1], [], dataset) 110 | dialog_pairs.append((utterance, response)) 111 | 112 | return dialog_pairs 113 | 114 | def _prepare_target(self, target): 115 | if len(target) == 1: 116 | act, slot, value = target[0] 117 | return ["{}={}".format(slot, value)] 118 | else: 119 | intents = [self._single_prep([label])[0] for label in target] 120 | if self.task == "full_enumeration": 121 | return ["+".join(intents)] 122 | else: 123 | return intents 124 | 125 | def _multi_prep(self, intents): 126 | if self.task == "slot": 127 | return [slot for act, slot, value in intents] 128 | elif self.task == "value": 129 | return [value for act, slot, value in intents] 130 | elif self.task in ["area", "food", "price", "request"]: 131 | targets = [] 132 | for act, slot, value in intents: 133 | if self.task == slot or self.task == act: 134 | targets.append(value) 135 | else: 136 | targets.append("") 137 | return targets 138 | 139 | ''' 140 | tokens = [] 141 | if "turn" in source.keys(): 142 | # vocab is designed so that the first 14 tokens are turn indicators 143 | tokens.append(source["turn"]) 144 | if use_context: 145 | for word in source["context"].split(): 146 | tokens.append(var(vocab.word_to_index(word, dataset), "long")) 147 | tokens.append(var(vocab.SOS_token, "long")) 148 | 149 | for word in source["utterance"].split(): 150 | tokens.append(vocab.word_to_index(word, dataset)) 151 | return var(tokens, "long") 152 | 153 | For DSTC2, format is 154 | Example is dict with keys 155 | {"input_source": [utterance, context, id]} 156 | {"output_target": [list of labels]} 157 | where each label is (high level intent, low level intent, slot, value) 158 | ''' 159 | -------------------------------------------------------------------------------- /objects/blocks/datasets.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections import defaultdict 3 | import numpy as np 4 | from tqdm import tqdm 5 | from stanza.nlp.corenlp import CoreNLPClient 6 | from pprint import pprint 7 | from utils.internal.ontology import Ontology 8 | 9 | client = None 10 | 11 | def annotate(sent): 12 | global client 13 | if client is None: 14 | client = CoreNLPClient(default_annotators='ssplit,tokenize'.split(',')) 15 | words = [] 16 | for sent in client.annotate(sent).sentences: 17 | for tok in sent: 18 | words.append(tok.word) 19 | return words 20 | 21 | class Intent: 22 | 23 | def __init__(self, domain, subdomain, act, slot, value): 24 | self.domain = domain # restaurant, airline, banking 25 | self.subdomain = subdomain # reservation, traffic, event 26 | self.act = act # accept/reject, open/close, request/inform, 27 | # question/answer, acknow/confuse 28 | self.slot = slot # time, location, price, rating, name 29 | self.value = value # north, yes/no, cheap, 5, more, italian 30 | 31 | @classmethod 32 | def from_dict(cls, d): 33 | return cls(**d) 34 | 35 | 36 | class Turn: 37 | 38 | def __init__(self, turn_id, utterance, user_intent, belief_state, agent_actions, agent_utterance, num=None): 39 | self.id = turn_id 40 | self.utterance = utterance 41 | self.user_intent = user_intent 42 | self.belief_state = belief_state 43 | self.agent_actions = agent_actions 44 | self.agent_utterance = agent_utterance 45 | self.num = num or {} 46 | 47 | def to_dict(self): 48 | return {'turn_id': self.id, 'utterance': self.utterance, 'user_intent': self.user_intent, 'belief_state': self.belief_state, 'agent_actions': self.agent_actions, 'agent_utterance': self.agent_utterance, 'num': self.num} 49 | 50 | @classmethod 51 | def from_dict(cls, d): 52 | return cls(**d) 53 | 54 | @classmethod 55 | def annotate_raw(cls, raw): 56 | agent_actions = [] 57 | for a in raw['agent_actions']: 58 | if isinstance(a, list): 59 | s, v = a 60 | agent_actions.append(['inform'] + s.split() + ['='] + v.split()) 61 | else: 62 | agent_actions.append(['request'] + a.split()) 63 | # NOTE: fix inconsistencies in data label 64 | fix = {'centre': 'center', 'areas': 'area', 'phone number': 'number'} 65 | return cls( 66 | turn_id=raw['turn_idx'], 67 | utterance=annotate(raw['utterance']), 68 | agent_actions=agent_actions, 69 | user_intent=[[fix.get(s.strip(), s.strip()), fix.get(v.strip(), v.strip())] for s, v in raw['user_intent']], 70 | belief_state=raw['belief_state'], 71 | agent_utterance=raw['agent_utterance'], 72 | ) 73 | 74 | def numericalize_(self, vocab): 75 | self.num['utterance'] = vocab.word2index([''] + [w.lower() for w in self.utterance + ['']], train=True) 76 | self.num['agent_actions'] = [vocab.word2index([''] + [w.lower() for w in a] + [''], train=True) for a in self.agent_actions + [['']]] 77 | 78 | 79 | class Dialogue: 80 | 81 | def __init__(self, dialogue_id, turns): 82 | self.id = dialogue_id 83 | self.turns = turns 84 | 85 | def __len__(self): 86 | return len(self.turns) 87 | 88 | def to_dict(self): 89 | return {'dialogue_id': self.id, 'turns': [t.to_dict() for t in self.turns]} 90 | 91 | @classmethod 92 | def from_dict(cls, d): 93 | return cls(d['dialogue_id'], [Turn.from_dict(t) for t in d['turns']]) 94 | 95 | @classmethod 96 | def annotate_raw(cls, raw): 97 | return cls(raw['dialogue_idx'], [Turn.annotate_raw(t) for t in raw['dialogue']]) 98 | 99 | 100 | class Dataset: 101 | 102 | def __init__(self, dialogues): 103 | self.dialogues = dialogues 104 | 105 | def __len__(self): 106 | return len(self.dialogues) 107 | 108 | def iter_turns(self): 109 | for d in self.dialogues: 110 | for t in d.turns: 111 | yield t 112 | 113 | def to_dict(self): 114 | return [d.to_dict() for d in self.dialogues] 115 | 116 | @classmethod 117 | def from_dict(cls, dialogues): 118 | return cls([Dialogue.from_dict(dd) for dd in dialogues]) 119 | 120 | @classmethod 121 | def annotate_raw(cls, fname): 122 | with open(fname) as f: 123 | data = json.load(f) 124 | return cls([Dialogue.annotate_raw(d) for d in tqdm(data)]) 125 | 126 | def numericalize_(self, vocab): 127 | for t in self.iter_turns(): 128 | t.numericalize_(vocab) 129 | 130 | def extract_ontology(self): 131 | slots = set() 132 | values = defaultdict(set) 133 | for t in self.iter_turns(): 134 | for s, v in t.user_intent: 135 | slots.add(s.lower()) 136 | values[s].add(v.lower()) 137 | ont_slots = sorted(list(slots)) 138 | ont_values = {k: sorted(list(v)) for k, v in values.items()} 139 | return Ontology(slots=ont_slots, values=ont_values) 140 | 141 | def batch(self, batch_size, shuffle=False): 142 | turns = list(self.iter_turns()) 143 | if shuffle: 144 | np.random.shuffle(turns) 145 | for i in tqdm(range(0, len(turns), batch_size)): 146 | yield turns[i:i+batch_size] 147 | 148 | def evaluate_preds(self, preds): 149 | request = [] 150 | inform = [] 151 | joint_goal = [] 152 | fix = {'centre': 'center', 'areas': 'area', 'phone number': 'number'} 153 | i = 0 154 | for d in self.dialogues: 155 | pred_state = {} 156 | for t in d.turns: 157 | gold_request = set([(s, v) for s, v in t.user_intent if s == 'request']) 158 | gold_inform = set([(s, v) for s, v in t.user_intent if s != 'request']) 159 | pred_request = set([(s, v) for s, v in preds[i] if s == 'request']) 160 | pred_inform = set([(s, v) for s, v in preds[i] if s != 'request']) 161 | request.append(gold_request == pred_request) 162 | inform.append(gold_inform == pred_inform) 163 | 164 | gold_recovered = set() 165 | pred_recovered = set() 166 | for s, v in pred_inform: 167 | pred_state[s] = v 168 | for b in t.belief_state: 169 | for s, v in b['slots']: 170 | if b['act'] != 'request': 171 | gold_recovered.add((b['act'], fix.get(s.strip(), s.strip()), 172 | fix.get(v.strip(), v.strip()))) 173 | for s, v in pred_state.items(): 174 | pred_recovered.add(('inform', s, v)) 175 | joint_goal.append(gold_recovered == pred_recovered) 176 | i += 1 177 | result = {'turn_inform': np.mean(inform), 178 | 'turn_request': np.mean(request), 179 | 'joint_goal': np.mean(joint_goal)} 180 | # pprint(result) 181 | return result 182 | 183 | def record_preds(self, preds, to_file): 184 | data = self.to_dict() 185 | i = 0 186 | for dialogue in data: 187 | for t in dialogue['turns']: 188 | t['pred'] = sorted(list(preds[i])) 189 | i += 1 190 | with open(to_file, 'wt') as f: 191 | json.dump(data, f) 192 | -------------------------------------------------------------------------------- /objects/blocks/attention.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import torch 3 | import torch.nn as nn 4 | import torch.nn.functional as F 5 | 6 | class Attention(nn.Module): 7 | """ 8 | Attend to a sequence of tensors using an attention distribution 9 | Input: 10 | Sequence: usually the hidden states of an RNN as a matrix 11 | shape = batch_size x seq_len x hidden_dim 12 | Condition: usually the vector of the current decoder state 13 | shape = batch_size x hidden_dim 14 | Lengths: list of integers, where each integer is the number of tokens 15 | in each sequence, the length of list should equal batch_size 16 | 17 | weights: attention distribution 18 | do_reduce: indicator variable indicating whether we compute 19 | the weighted average of the attended vectors OR 20 | just return them scaled by their attention weight 21 | 22 | Output: 23 | attention distribution over hidden states 24 | activations for each hidden state 25 | 26 | """ 27 | def __init__(self, hidden=None, method='dot', act='tanh'): 28 | super(Attention, self).__init__() 29 | act_options = { "softmax": nn.Softmax(dim=1), 30 | "sigmoid": nn.Sigmoid(), 31 | "tanh": nn.Tanh() } 32 | 33 | if method == 'linear': # h(Wh) 34 | self.W_a = nn.Linear(hidden, hidden) 35 | elif method == 'concat': # v_a tanh(W[h_i;h_j]) 36 | self.activation = act_options[act] 37 | self.W_a = nn.Linear(2 * hidden, hidden) 38 | self.v_a = nn.Linear(hidden, 1, bias=False) 39 | elif method == 'double': # v_a tanh(W[h_i;h_j]) 40 | self.W_a = nn.Linear(2 * hidden, hidden) 41 | self.v_a = nn.Linear(hidden, 1, bias=False) 42 | elif method == 'self': 43 | self.W_a = nn.Linear(hidden, 1) 44 | self.drop = nn.Dropout(act) # semi hack to use the param 45 | # if attention method is 'dot' no extra matrix is needed 46 | self.attn_method = method 47 | 48 | def forward(self, sequence, condition, lengths=None): 49 | """ 50 | Compute context weights for a given type of scoring mechanism 51 | return the scores along with the weights 52 | """ 53 | if lengths is None: 54 | self.masking = False 55 | else: 56 | assert len(lengths) == sequence.shape[0] # equal to batch size 57 | self.lengths = lengths 58 | self.masking = True 59 | 60 | scores = self.score(sequence, condition) 61 | # Normalize scores into weights --> batch_size x seq_len 62 | self.scores = F.softmax(scores, dim=1) 63 | # scores unsqueezed is batch_size x 1 x seq_len 64 | # sequence shape is batch_size x seq_len x hidden_dim 65 | weights = torch.bmm(self.scores.unsqueeze(1), sequence).squeeze(1) 66 | # this is equivalent to doing a dot product and summing 67 | # weights = scores.unsqueeze(2).expand_as(sequence).mul(sequence).sum(1) 68 | 69 | return weights 70 | 71 | def score(self, sequence, condition): 72 | """ 73 | Calculate activation score over the sequence using the condition. 74 | Output shape = batch_size x seq_len 75 | """ 76 | if self.attn_method == 'self': # W h 77 | batch_size, seq_len, hidden_dim = sequence.size() 78 | # start out with some drop out 79 | sequence = self.drop(sequence) 80 | # batch_size, seq_len, hidden_dim --> batch_size x seq_len, 1 81 | reshaped = sequence.contiguous().view(-1, hidden_dim) 82 | # batch_size x seq_len, 1 --> batch_size, seq_len 83 | scores = self.W_a(reshaped).view(batch_size, seq_len) 84 | 85 | elif self.attn_method == 'linear': # p(Wq) 86 | # after affine, sequence keeps the same batch_size x seq_len x hidden_dim 87 | # after unsqueeze, condition shape becomes batch_size x hidden_dim x 1 88 | product = torch.bmm(self.W_a(sequence), condition.unsqueeze(2)) 89 | # final score shape is just batch_size x seq_len as expected 90 | scores = product.squeeze(2) 91 | 92 | elif self.attn_method == 'concat': # v_a tanh(W[h_i;h_j]) 93 | # batch_size x hidden_dim --> batch_size x seq_len x hidden_dim 94 | expanded = condition.unsqueeze(1).expand_as(sequence) 95 | # joined shape becomes batch_size x seq_len x (2 * hidden_dim) 96 | # Note that W_a[h_i; h_j] is the same as W_1a(h_i) + W_2a(h_j) since 97 | # W_a is just (W_1a concat W_2a) (nx2n) = [(nxn);(nxn)] 98 | joined = torch.cat([sequence, expanded], dim=2) 99 | # logit is now brought back to original batch_size x seq_len x hidden_dim 100 | logit = self.activation(self.W_a(joined)) 101 | # v_a brings to batch_size x seq_len x 1, and squeeze completes the job 102 | scores = self.v_a(logit).squeeze(2) 103 | 104 | elif self.attn_method == 'dot': 105 | # batch_size x hidden_dim --> batch_size x seq_len x hidden_dim 106 | expanded = condition.unsqueeze(1).expand_as(sequence) 107 | # final scores shape is batch_size x seq_len 108 | scores = expanded.mul(sequence).sum(2) 109 | 110 | if self.masking: 111 | max_len = max(self.lengths) 112 | for i, l in enumerate(self.lengths): 113 | if l < max_len: 114 | scores.data[i, l:] = -np.inf 115 | return scores 116 | 117 | 118 | class DoubleAttention(nn.Module): 119 | """ 120 | Compute a double dot product attention. While the typical attention 121 | takes in a 3-dim sequence and 2-dim condition this module takes in two 122 | inputs that are both 3-dim and a hidden state as the 2-dim condition 123 | 124 | Initialization Args: 125 | act: which non-linearity to use to compute attention 126 | (sigmoid for independent attention, softmax for joint attention) 127 | 128 | Input: 129 | hidden: vector used to compute attentions which acts as a gate 130 | shape is batch_size x hidden_dim 131 | slots: 3-dimensional tensor of batch_size x num_items x hidden_dim 132 | keys: 3-dimensional tensor of batch_size x num_items x hidden_dim 133 | Note that the slot and key shapes are inter-changeable 134 | 135 | Output: 136 | weights: weighted context representing the chosen slots 137 | normalized: attention distribution over memory slots 138 | scores: raw activation scores based on hidden states 139 | """ 140 | def __init__(self, act="softmax"): 141 | super(DoubleDotAttention, self).__init__() 142 | if act == "softmax": 143 | self.activation = nn.Softmax(dim=1) 144 | elif act == "sigmoid": 145 | self.activation = nn.Sigmoid() 146 | 147 | def forward(hidden, slots, keys): 148 | scores = self.score(hidden, slots, keys) 149 | activated = self.activation(scores, dim=1) 150 | weights = torch.bmm(activated.unsqueeze(1), slots).squeeze(1) 151 | return weights, activated, scores 152 | 153 | def score(self, hidden, input_1, input_2): 154 | hidden_ = hidden.unsqueeze(1).expand_as(input_1) 155 | logit_1 = (input_1 * hidden_).sum(2) 156 | logit_2 = (input_2 * hidden_).sum(2) 157 | return logit_1 + logit_2 158 | 159 | -------------------------------------------------------------------------------- /utils/internal/arguments.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser 2 | 3 | def solicit_args(): 4 | parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) 5 | parser.add_argument('--seed', help='Random seed', type=int, default=14) 6 | parser.add_argument('-t', '--task', default='default', type=str, 7 | help='overall configuration of operations and objects in system', ) 8 | parser.add_argument('-d', '--dataset', choices=['babi', 'woz2', 'dstc2', 9 | 'e2e/movies', 'ddq/movies', 'e2e/restaurants'], default='woz2', 10 | help='Choose the data to train on, defines labels', ) 11 | parser.add_argument('-m', '--model', default='match', choices=['rulebased', \ 12 | 'basic', 'attention', 'transformer', 'ddq', 'bilstm', 'glad'], 13 | help='Choose the model type used',) 14 | parser.add_argument('--use-old-nlu', default=False, action='store_true', 15 | help='Use the old nlg model for the Belief Tracker module') 16 | 17 | parser.add_argument('--debug', default=False, action='store_true', 18 | help='whether or not to go into debug mode, which is faster') 19 | parser.add_argument('-v', '--verbose', default=False, action='store_true', 20 | help='whether or not to have verbose prints') 21 | parser.add_argument('--test-mode', default=False, action='store_true', 22 | help='if true, then we are in test phase instead of train/val') 23 | parser.add_argument('--use-existing', default=False, action='store_true', 24 | help='continue training a model rather than creating a new one') 25 | parser.add_argument('--gpu', type=int, help='which GPU to use') 26 | 27 | # --------- TUNING OPTIONS ------------- 28 | parser.add_argument('--context', default=False, action='store_true', 29 | help='if true, then include context as part of training input') 30 | parser.add_argument('--attn-method', default='luong', type=str, 31 | help='type of attention', choices=['luong', 'dot', 'vinyals']) 32 | parser.add_argument('--optimizer', default='sgd', type=str, 33 | help='Optimizer to use. Choose from sgd, rmsprop, adam') 34 | 35 | # ------ PARAMETER OPTIMIZATION -------- 36 | param_group = parser.add_argument_group(title='hyperparameters') 37 | param_group.add_argument('-lr', '--learning-rate', default=0.01, type=float, 38 | help='Learning rate alpha for weight updates') 39 | param_group.add_argument('--hidden-dim', default=256, type=int, 40 | help='Number of hidden units, size of hidden dimension') 41 | param_group.add_argument('--embedding-size', default=300, type=int, 42 | help='Word embedding size usually from pretrained') 43 | param_group.add_argument('--drop-prob', default=0.2, type=float, 44 | help='probability of dropping a node, opposite of keep prob') 45 | param_group.add_argument('--teacher-forcing', default=0.6, type=float, 46 | help='teacher forcing ratio, 0 means no teacher forcing') 47 | param_group.add_argument('-reg', '--weight-decay', default=0.003, type=float, 48 | help='weight_decay to regularize the weights') 49 | param_group.add_argument('--num-layers', default=1, type=int, 50 | help='Number of layers in each LSTM') 51 | param_group.add_argument('--batch-size', default=50, type=int, 52 | help='batch size for training') 53 | param_group.add_argument('-e', '--epochs', default=14, type=int, 54 | help='Number of epochs or episodes to train') 55 | param_group.add_argument('--decay-times', default=3, type=int, 56 | help='total lr decay times') 57 | 58 | # --------- LIMITS AND THRESHOLDS ----------- 59 | parser.add_argument('--max-turn', default=20, type=int, 60 | help='max allowed turns in dialogue before declaring failure') 61 | parser.add_argument('--max-seq-len', default=15, type=int, 62 | help='max number of tokens allowed to generate or to use as input') 63 | parser.add_argument('--threshold', default=0.8, type=float, 64 | help='minimum confidence level to keep, minimum success rate for \ 65 | experience replay, minimum loss value we are willing to accept \ 66 | for early stopping (with -1 to turn off), or other threshold') 67 | parser.add_argument('--early-stop', default='joint_goal', type=str, 68 | help='slot to report metrics on, used by monitor') 69 | 70 | # --------- REINFORCEMENT LEARNING ---------- 71 | parser.add_argument('--discount-rate', default=0.9, type=float, 72 | help='discount rate for value, commonly known as gamma') 73 | parser.add_argument('--pool-size', default=1000, type=int, 74 | help='number of examples to hold in experience replay pool') 75 | parser.add_argument('--epsilon', default=0.1, type=float, 76 | help='Amount to start looking around in epsilon-greedy exploration') 77 | parser.add_argument('--warm-start', default=False, action='store_true', 78 | help='when true, agent has warm start phase for training') 79 | parser.add_argument('--user', default='simulate', type=str, 80 | help='type of user to talk to', choices=['simulate', 'command', 'turk']) 81 | parser.add_argument('--belief', default='discrete', type=str, 82 | help='type of belief state representation', 83 | choices=['discrete', 'memory', 'distributed']) 84 | 85 | # -------- MODEL CHECKPOINTS ---------------- 86 | parser.add_argument('--save-model', default=False, action='store_true', 87 | help='when true, save model weights in a checkpoint') 88 | parser.add_argument('--pretrained', default=False, action='store_true', 89 | help='when true, use pretrained word embeddings from data directory') 90 | parser.add_argument('--prefix', default='', type=str, 91 | help='prepended string to distinguish among trials, usually date') 92 | parser.add_argument('--suffix', default='', type=str, 93 | help='appended string to distinguish among trials, usually count') 94 | 95 | # -------- REPORTING RESULTS ---------------- 96 | parser.add_argument('--report-visual', default=False, action='store_true', 97 | help='when true, plot the train and val loss graph to file') 98 | parser.add_argument('--report-qual', default=False, action='store_true', 99 | help='when true, 50 random samples of inputs and outputs will be \ 100 | printed out in human readable form for qualitative evaluation') 101 | parser.add_argument('--report-quant', default=False, action='store_true', 102 | help='when true, the values selected by the metrics argument will \ 103 | be calculated, displayed and stored into a results.log file') 104 | parser.add_argument('--metrics', nargs='+', default=['accuracy'], 105 | choices=['bleu', 'rouge', 'meteor', 'accuracy', 'val_loss', \ 106 | 'macro_f1', 'micro_f1', 'avg_reward', 'avg_turn', 'success_rate'], 107 | help='list of evaluation metrics, each metric is a single float') 108 | 109 | return parser.parse_args() -------------------------------------------------------------------------------- /operators/preprocess/loader.py: -------------------------------------------------------------------------------- 1 | import os, pdb, sys 2 | import json 3 | import pickle as pkl 4 | import torch 5 | 6 | from vocab import Vocab 7 | from utils.external import dialog_constants 8 | from utils.internal.vocabulary import Vocabulary 9 | from utils.external.reader import text_to_dict, get_glove_name 10 | from objects.blocks import Dataset 11 | from utils.internal.ontology import Ontology 12 | 13 | # Used for loading data, to be fed into the PreProcessor 14 | class DataLoader(object): 15 | def __init__(self, args): 16 | self.data_dir = os.path.join('datasets', args.dataset) 17 | self.clean_dir = os.path.join(self.data_dir, 'clean') 18 | self.debug_dir = os.path.join(self.data_dir, 'debug') 19 | self.task = args.task 20 | 21 | if args.pretrained: 22 | self.embeddings = json.load(self.path('embeddings.json')) 23 | 24 | self.ontology = self.augment_with_actions(Ontology.from_path(self.data_dir)) 25 | if self.task == 'track_intent': 26 | self.vocab = Vocab.from_dict(self.json_data('vocab')) 27 | elif self.task == 'manage_policy': 28 | self.kb = self.json_data('kb') 29 | # self.kb = self.pickle_data('knowledge_base') 30 | self.vocab = Vocabulary(args, self.data_dir) 31 | self.ontology.slots = self.text_data('slot_set') # force special slots 32 | elif self.task == 'end_to_end': 33 | self.kb = self.json_data('kb') 34 | self.goals = self.json_data('goals') 35 | self.vocab = Vocab.from_dict(self.json_data('vocab')) 36 | self.ontology.old_acts = self.text_data('act_set') 37 | self.ontology.old_slots = self.text_data('slot_set') 38 | 39 | self.load_datasets() 40 | 41 | def path(self, filename, kind=None): 42 | if kind is None: 43 | return open(os.path.join(self.data_dir, filename)) 44 | else: 45 | return open(os.path.join(self.data_dir, filename), kind) 46 | 47 | def load_datasets(self): 48 | self.datasets = {} 49 | for split in ['train', 'val', 'test']: 50 | data_path = os.path.join(self.clean_dir, '{}.json'.format(split)) 51 | with open(data_path, 'r') as f: 52 | dataset = json.load(f) 53 | if self.task == 'track_intent': 54 | dataset = Dataset.from_dict(dataset) 55 | self.datasets[split] = dataset 56 | print("{} loaded with {} items!".format(data_path, len(dataset))) 57 | 58 | def load_entity_embeddings(self, vocab): 59 | # made for EntNet 60 | if self.opt.pt != "none": 61 | name = get_glove_name(self.opt, "entities", "entpt") 62 | print("Loading entity embeddings from {}".format(name)) 63 | entity_words = pkl.load( open(name, "r") ) 64 | 65 | for i, word in vocab.iteritems(): 66 | if word in ["", "", "", ""]: 67 | self.key_init.weight.data[i].zero_() 68 | continue 69 | vec = tensor(entity_words[word]).to(device) 70 | self.key_init.weight.data[i] = vec 71 | 72 | def restore_checkpoint(self, filepath): 73 | model_checkpoint = torch.load(filepath) 74 | print('Loaded model from {}'.format(filepath)) 75 | return model_checkpoint 76 | 77 | def json_data(self, filename): 78 | file_path = self.path(filename + '.json', 'r') 79 | return json.load(file_path) 80 | 81 | def pickle_data(self, filename, directory=None): 82 | if directory is None: 83 | file_path = self.path(filename + '.pkl', 'rb') 84 | return pkl.load(file_path, encoding='latin1') 85 | else: 86 | file_path = os.path.join(directory, filename + '.pkl') 87 | return pkl.load(open(file_path, 'rb'), encoding='latin1') 88 | 89 | def text_data(self, filename): 90 | full_set = {} 91 | with self.path(filename + '.txt', 'r') as f: 92 | index = 0 93 | for line in f.readlines(): 94 | full_set[line.strip('\n').strip('\r')] = index 95 | index += 1 96 | return full_set 97 | 98 | def augment_with_actions(self, ontology): 99 | agent_actions = [] 100 | user_actions = [] 101 | 102 | # force the order of slots so acts and requests can be deleted later 103 | inform_set = [x for x in ontology.slots if x not in ['act', 'request']] 104 | inform_set += ['taskcomplete'] 105 | ontology.inform_set = inform_set 106 | 107 | agent_acts = ['greeting', 'confirm_question', 'confirm_answer', 'thanks', 'deny'] 108 | user_acts = ['thanks', 'deny', 'closing', 'confirm_answer'] 109 | # if self.task == "end_to_end": 110 | # agent_acts.append('clarify') 111 | 112 | for act in agent_acts: 113 | action = {'dialogue_act':act, 'inform_slots':{}, 'request_slots':{}} 114 | agent_actions.append(action) 115 | for act in user_acts: 116 | action = {'dialogue_act':act, 'inform_slots':{}, 'request_slots':{}} 117 | user_actions.append(action) 118 | 119 | if 'request' in ontology.slots: 120 | valid_slots = [x for x in ontology.slots if x not in ['act', 'request']] 121 | for slot in valid_slots: 122 | req_action = {'dialogue_act': 'request', 'inform_slots': {}, 'request_slots': {slot: "UNK"}} 123 | agent_actions.append(req_action) 124 | inf_action = {'dialogue_act': 'inform', 'inform_slots': {slot: "PLACEHOLDER"}, 'request_slots': {}} 125 | agent_actions.append(inf_action) 126 | agent_actions.append({'dialogue_act': 'inform', 'inform_slots': {'taskcomplete': 'PLACEHOLDER'}, 'request_slots': {}}) 127 | else: 128 | for req_slot in dialog_constants.sys_request_slots: 129 | action = {'dialogue_act': 'request', 'inform_slots': {}, 'request_slots': {req_slot: "UNK"}} 130 | agent_actions.append(action) 131 | for inf_slot in dialog_constants.sys_inform_slots: 132 | action = {'dialogue_act': 'inform', 'inform_slots': {inf_slot: "PLACEHOLDER"}, 'request_slots': {}} 133 | agent_actions.append(action) 134 | ontology.feasible_agent_actions = agent_actions 135 | 136 | for inf_slot in dialog_constants.sys_inform_slots_for_user: 137 | action = {'dialogue_act': 'inform', 'inform_slots': {inf_slot: "PLACEHOLDER"}, 'request_slots': {}} 138 | user_actions.append(action) 139 | for req_slot in dialog_constants.sys_request_slots_for_user: 140 | action = {'dialogue_act': 'request', 'inform_slots': {}, 'request_slots': {req_slot: "UNK"}} 141 | user_actions.append(action) 142 | user_actions.append({'dialogue_act': 'inform', 'inform_slots': {}, 'request_slots': {}}) 143 | user_actions.append({'dialogue_act': 'inform', 'inform_slots': {}, 'request_slots': {'ticket': 'UNK'}}) 144 | ontology.feasible_user_actions = user_actions 145 | 146 | return ontology 147 | 148 | 149 | """ 150 | def set_categories(self): 151 | self.multitask = True 152 | if self.task == "end_to_end": 153 | self.categories = ["intent_tracker", "kb_lookup", "policy_manager", "text_generator"] 154 | elif self.task == "clarification": 155 | self.categories = ["belief_tracker", "policy_manager", "user_simulator"] 156 | elif self.task == "dual": 157 | self.categories = ["slot", "value"] 158 | elif self.task == "per_slot": 159 | self.categories = ["area", "food", "price", "request"] 160 | else: 161 | self.categories = self.task 162 | self.multitask = False 163 | """ 164 | -------------------------------------------------------------------------------- /operators/evaluate/evaluator.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | matplotlib.use('agg') 3 | import matplotlib.pyplot as plt 4 | import matplotlib.ticker as ticker 5 | import random 6 | import pandas as pd 7 | import os, pdb, sys 8 | 9 | from utils.external.bleu import BLEU 10 | import utils.internal.initialization as data_io 11 | from objects.components import var, run_inference 12 | from operators.evaluate.server import HTTPServer, Handler, ToyModel 13 | 14 | class Evaluator(object): 15 | def __init__(self, args, processor): 16 | self.config = args 17 | self.task = args.task 18 | self.metrics = args.metrics 19 | self.method = args.attn_method 20 | self.verbose = args.verbose 21 | self.batch_size = args.batch_size 22 | 23 | self.module = None 24 | self.monitor = None 25 | 26 | self.save_dir = os.path.join("results", args.task, args.dataset) 27 | self.vocab = processor.vocab 28 | self.data = processor.datasets['test'] if args.test_mode else processor.datasets['val'] 29 | self.ontology = processor.ontology 30 | 31 | def run_test(self): 32 | output = self.module.run_glad_inference(self.data) 33 | 34 | def generate_report(self): 35 | if self.config.report_visual: 36 | self.visual_report() 37 | if self.config.report_qual: 38 | self.qualitative_report() 39 | if self.config.report_quant: 40 | self.monitor.summarize_results(verbose=False) 41 | # if self.task == "glad": 42 | # self.model.quant_report(self.data, self.config) 43 | self.quantitative_report() 44 | 45 | def qualitative_report(self): 46 | print("starting qualitative_report") 47 | self.module.model.eval() 48 | samples = next(self.data.batch(self.batch_size, shuffle=True)) 49 | loss, scores = self.module.model.forward(samples) 50 | predictions = self.module.extract_predictions(scores) 51 | vals = self.ontology.values 52 | lines = self.module.qual_report(samples, predictions, scores, vals) 53 | 54 | save_dir = self.save_dir if self.module is None else self.module.dir 55 | qual_report_path = os.path.join(save_dir, "qual.txt") 56 | with open(qual_report_path, "w") as file: 57 | for line in lines: 58 | file.write(line + '\n') 59 | print('Qualitative examples saved to {}'.format(qual_report_path)) 60 | 61 | """ Quantitative evalution of model performance, for per step loss 62 | please view the logging output in the results folder instead """ 63 | def quantitative_report(self): 64 | datarows = {} 65 | extras = ["save_model", "prefix", "suffix", "verbose", "gpu", "context", \ 66 | "metrics", "report_visual", "report_qual", "report_quant"] 67 | for param, value in vars(self.config).items(): 68 | if param not in extras: 69 | datarows[param] = value 70 | 71 | for metric in self.metrics: 72 | datarows[metric] = getattr(self.monitor, metric) 73 | 74 | quant = pd.DataFrame(data=datarows, index=["Attributes"]) 75 | self.save_csv_report(quant.T, "quant") 76 | 77 | def visual_report(self, vis_count): 78 | self.model.eval() 79 | dialogues = data_io.select_consecutive_pairs(self.data, vis_count) 80 | train_s, train_l = self.monitor.train_steps, self.monitor.train_losses 81 | val_s, val_l = self.monitor.val_steps, self.monitor.val_losses 82 | 83 | visualizations = [] 84 | for dialog in dialogues: 85 | for turn in dialog: 86 | input_variable, output_variable = turn 87 | _, responses, visual = run_inference(self.model, input_variable, \ 88 | output_variable, criterion=NLLLoss(), teach_ratio=0) 89 | queries = input_variable.data.tolist() 90 | query_tokens = [self.vocab.index_to_word(q[0]) for q in queries] 91 | response_tokens = [self.vocab.index_to_word(r) for r in responses] 92 | 93 | visualizations.append((visual, query_tokens, response_tokens)) 94 | self.show_save_attention(visualizations) 95 | 96 | def save_csv_report(self, report, filename): 97 | report_path = "{}/{}.csv".format(self.save_dir, filename) 98 | report.to_csv(report_path, index=True) 99 | print('{} report complete, saved to {}!'.format(filename, report_path)) 100 | 101 | def plot(title): 102 | tracker = getattr(system.learner, '{}_tracker'.format(self.tasks[0])) 103 | xs = tracker.train_epochs # tracker.val_epochs 104 | ys = tracker.train_losses # tracker.val_losses 105 | 106 | xlabel = "Iterations" 107 | ylabel = "Loss" 108 | assert len(xs) == len(ys) 109 | for i in range(len(xs)): 110 | plt.plot(xs[i], ys[i]) 111 | 112 | plt.title(title) 113 | plt.xlabel(xlabel) 114 | plt.ylabel(ylabel) 115 | plt.legend(["Train", "Validation"]) 116 | plt.show() 117 | 118 | def show_save_attention(self, visualizations): 119 | for i, viz in enumerate(self.visualizations): 120 | visual, query_tokens, response_tokens = viz 121 | visual[-1,:] = 0 122 | # Set up figure with colorbar 123 | fig = plt.figure() 124 | ax = fig.add_subplot(111) 125 | cax = ax.matshow(visual.numpy(), cmap='bone') 126 | fig.colorbar(cax) 127 | # Set up axes 128 | ax.set_yticklabels([''] + query_tokens) 129 | ax.set_xticklabels([''] + response_tokens, rotation=90) 130 | ax.xaxis.set_major_locator(ticker.MultipleLocator(1)) 131 | ax.yaxis.set_major_locator(ticker.MultipleLocator(1)) 132 | # Set up labels 133 | plt.ylabel("User Query") 134 | plt.xlabel("Agent Response") 135 | # pdb.set_trace() 136 | plt.savefig("results/visualize_{0}{1}.png".format(self.method, i)) 137 | if self.verbose: 138 | plt.show() 139 | plt.close() 140 | 141 | def start_talking(self, user_type, num_epochs): 142 | self.episode_counter = 0 143 | self.max_episodes = num_epochs 144 | 145 | while not self.done_talking(user_type): 146 | turn_count = 0 147 | self.agent.initialize_episode(user_type) 148 | self.agent.start_conversation(user_type) 149 | 150 | episode_over = False 151 | while not episode_over: 152 | episode_over, reward = self.agent.next(record_user_data=False) 153 | turn_count += 1 154 | 155 | if episode_over: 156 | final_reward = reward - turn_count # lose reward for every turn taken 157 | result = "succeeded :)" if final_reward > 0 else "failed :(" 158 | print(f"this dialogue {result}") 159 | print("---" * 16) 160 | 161 | def done_talking(self, user_type): 162 | if self.episode_counter == 0: 163 | self.episode_counter += 1 164 | return False 165 | if user_type == 'simulate': 166 | if self.episode_counter > self.max_episodes: 167 | return True 168 | else: 169 | self.episode_counter += 1 170 | return False 171 | 172 | done = input("Are you done talking? ") 173 | if done in ["yes", "Yes", "y"]: 174 | print("It was good talking to you, have a nice day!") 175 | return True 176 | elif done in ["no", "No", "n"]: 177 | return False 178 | 179 | def start_server(self, root_dir): 180 | """ Server will run on port 1414 from index.html 181 | It will chat with the user until user decides to end. Then the 182 | user will be redirected to survey page, which stores records in CSV. 183 | For more details see operators/evaluate/server.py """ 184 | ip_address = ('0.0.0.0', 1414) 185 | httpd = HTTPServer(ip_address, Handler) 186 | httpd.agent = self.agent 187 | httpd.wd = os.path.relpath(os.path.join(root_dir, 'utils')) 188 | print("Listening at", ip_address) 189 | httpd.serve_forever() 190 | 191 | -------------------------------------------------------------------------------- /utils/external/bleu.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Natural Language Toolkit: BLEU 3 | # 4 | # Copyright (C) 2001-2013 NLTK Project 5 | # Authors: Chin Yee Lee, Hengfeng Li, Ruxin Hou, Calvin Tanujaya Lim 6 | # URL: 7 | # For license information, see LICENSE.TXT 8 | 9 | from __future__ import division 10 | 11 | import math 12 | 13 | from nltk import word_tokenize 14 | from collections import Counter 15 | from nltk.util import ngrams 16 | 17 | class BLEU(object): 18 | 19 | # weights are for unigram, bigram, trigram ... 5-gram respectively 20 | @staticmethod 21 | def compute(candidate, references, weights=[0.2, 0.2, 0.2, 0.2, 0.2]): 22 | candidate = [c.lower() for c in candidate] 23 | references = [[r.lower() for r in reference] for reference in references] 24 | 25 | p_ns = (BLEU.modified_precision(candidate, references, i) for i, _ in enumerate(weights, start=1)) 26 | s = math.fsum(w * math.log(p_n) for w, p_n in zip(weights, p_ns) if p_n) 27 | 28 | bp = BLEU.brevity_penalty(candidate, references) / 2.0 29 | return bp * math.exp(s) 30 | 31 | @staticmethod 32 | def modified_precision(candidate, references, n): 33 | counts = Counter(ngrams(candidate, n)) 34 | if not counts: 35 | return 0 36 | 37 | max_counts = {} 38 | for reference in references: 39 | reference_counts = Counter(ngrams(reference, n)) 40 | for ngram in counts: 41 | max_counts[ngram] = max(max_counts.get(ngram, 0), reference_counts[ngram]) 42 | # this is part where we do the "modifcation" 43 | clipped_counts = dict((ngram, min(count, max_counts[ngram])) for ngram, count in counts.items()) 44 | 45 | return sum(clipped_counts.values()) / sum(counts.values()) 46 | 47 | @staticmethod 48 | def brevity_penalty(candidate, references): 49 | c = len(candidate) 50 | r = min(abs(len(r) - c) for r in references) 51 | 52 | if c > r: 53 | return 1 54 | else: 55 | return math.exp(1 - r / c) 56 | 57 | 58 | """ 59 | Consider an example: 60 | 61 | > weights = [0.25, 0.25, 0.25, 0.25] 62 | > candidate1 = ['It', 'is', 'a', 'guide', 'to', 'action', 'which', 63 | ... 'ensures', 'that', 'the', 'military', 'always', 64 | ... 'obeys', 'the', 'commands', 'of', 'the', 'party'] 65 | 66 | > candidate2 = ['It', 'is', 'to', 'insure', 'the', 'troops', 67 | ... 'forever', 'hearing', 'the', 'activity', 'guidebook', 68 | ... 'that', 'party', 'direct'] 69 | 70 | > reference1 = ['It', 'is', 'a', 'guide', 'to', 'action', 'that', 71 | ... 'ensures', 'that', 'the', 'military', 'will', 'forever', 72 | ... 'heed', 'Party', 'commands'] 73 | 74 | > reference2 = ['It', 'is', 'the', 'guiding', 'principle', 'which', 75 | ... 'guarantees', 'the', 'military', 'forces', 'always', 76 | ... 'being', 'under', 'the', 'command', 'of', 'the', 77 | ... 'Party'] 78 | 79 | > reference3 = ['It', 'is', 'the', 'practical', 'guide', 'for', 'the', 80 | ... 'army', 'always', 'to', 'heed', 'the', 'directions', 81 | ... 'of', 'the', 'party'] 82 | 83 | The BLEU method mainly consists of two parts: 84 | 85 | Part 1 - modified n-gram precision 86 | 87 | The normal precision method may lead to some wrong translations with 88 | high-precision, e.g., the translation, in which a word of reference 89 | repeats several times, has very high precision. So in the modified 90 | n-gram precision, a reference word will be considered exhausted after 91 | a matching candidate word is identified. 92 | 93 | Unigrams: 94 | 95 | > BLEU.modified_precision( 96 | ... candidate1, 97 | ... [reference1, reference2, reference3], 98 | ... n=1, 99 | ... ) 100 | 0.94... 101 | 102 | > BLEU.modified_precision( 103 | ... candidate2, 104 | ... [reference1, reference2, reference3], 105 | ... n=1, 106 | ... ) 107 | 0.57... 108 | 109 | Bigrams: 110 | 111 | > BLEU.modified_precision( 112 | ... candidate1, 113 | ... [reference1, reference2, reference3], 114 | ... n=2, 115 | ... ) 116 | 0.58... 117 | 118 | > BLEU.modified_precision( 119 | ... candidate2, 120 | ... [reference1, reference2, reference3], 121 | ... n=2, 122 | ... ) 123 | 0.07... 124 | 125 | 126 | Part 2 - brevity penalty 127 | 128 | As the modified n-gram precision still has the problem from the short 129 | length sentence, brevity penalty is used to modify the overall BLEU 130 | score according to length. 131 | 132 | > BLEU.compute(candidate1, [reference1, reference2, reference3], weights) 133 | 0.504... 134 | 135 | > BLEU.compute(candidate2, [reference1, reference2, reference3], weights) 136 | 0.457... 137 | 138 | 2. Test with two corpus that one is a reference and another is 139 | an output from translation system: 140 | 141 | > weights = [0.25, 0.25, 0.25, 0.25] 142 | > ref_file = open('newstest2012-ref.en') # doctest: +SKIP 143 | > candidate_file = open('newstest2012.fr-en.cmu-avenue') # doctest: +SKIP 144 | 145 | > total = 0.0 146 | > count = 0 147 | 148 | > for candi_raw in candidate_file: # doctest: +SKIP 149 | ... ref_raw = ref_file.readline() 150 | ... ref_tokens = word_tokenize(ref_raw) 151 | ... candi_tokens = word_tokenize(candi_raw) 152 | ... total = BLEU.compute(candi_tokens, [ref_tokens], weights) 153 | ... count += 1 154 | 155 | > total / count # doctest: +SKIP 156 | 2.787504437460048e-05 157 | 158 | [1] Papineni, Kishore, et al. "BLEU: a method for automatic evaluation of 159 | machine translation." Proceedings of the 40th annual meeting on 160 | association for computational linguistics. Association for Computational 161 | Linguistics, 2002. 162 | 163 | -------------------------------------------------------------------------------------------------- 164 | Calculate modified ngram precision. 165 | 166 | > BLEU.modified_precision( 167 | ... 'the the the the the the the'.split(), 168 | ... ['the cat is on the mat'.split(), 'there is a cat on the mat'.split()], 169 | ... n=1, 170 | ... ) 171 | 0.28... 172 | 173 | > BLEU.modified_precision( 174 | ... 'the the the the the the the'.split(), 175 | ... ['the cat is on the mat'.split(), 'there is a cat on the mat'.split()], 176 | ... n=2, 177 | ... ) 178 | 0.0 179 | 180 | > BLEU.modified_precision( 181 | ... 'of the'.split(), 182 | ... [ 183 | ... 'It is a guide to action that ensures that the military will forever heed Party commands.'.split(), 184 | ... 'It is the guiding principle which guarantees the military forces always being under the command of the Party.'.split(), 185 | ... 'It is the practical guide for the army always to heed the directions of the party'.split(), 186 | ... ], 187 | ... n=1, 188 | ... ) 189 | 1.0 190 | 191 | > BLEU.modified_precision( 192 | ... 'of the'.split(), 193 | ... [ 194 | ... 'It is a guide to action that ensures that the military will forever heed Party commands.'.split(), 195 | ... 'It is the guiding principle which guarantees the military forces always being under the command of the Party.'.split(), 196 | ... 'It is the practical guide for the army always to heed the directions of the party'.split(), 197 | ... ], 198 | ... n=2, 199 | ... ) 200 | 1.0 201 | 202 | """ -------------------------------------------------------------------------------- /scripts/3_advanced.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals, division 3 | from utils.internal.arguments import solicit_args 4 | from io import open 5 | import unicodedata 6 | import string 7 | import re 8 | import random 9 | import json 10 | import pdb, sys 11 | import time as tm 12 | import pickle 13 | 14 | import torch 15 | from torch import optim 16 | from torch.nn import NLLLoss as NegLL_Loss 17 | from torch.optim.lr_scheduler import StepLR as StepLR 18 | 19 | import utils.internal.data_io as data_io 20 | import utils.internal.evaluate as evaluate 21 | from utils.internal.bleu import BLEU 22 | from utils.external.clock import * 23 | from utils.external.preprocessers import * 24 | from model.components import * 25 | 26 | use_cuda = torch.cuda.is_available() 27 | MAX_LENGTH = 8 28 | 29 | def train(input_variable, target_variable, encoder, decoder, 30 | encoder_optimizer, decoder_optimizer, criterion, teach_ratio): 31 | encoder.train() # affects the performance of dropout 32 | decoder.train() 33 | encoder_optimizer.zero_grad() 34 | decoder_optimizer.zero_grad() 35 | 36 | loss, _, _ = run_inference(encoder, decoder, input_variable, target_variable, \ 37 | criterion, teach_ratio) 38 | loss.backward() 39 | clip_gradient([encoder, decoder], clip=10) 40 | encoder_optimizer.step() 41 | decoder_optimizer.step() 42 | 43 | return loss.data[0] / target_variable.size()[0] 44 | 45 | def validate(input_variable, target_variable, encoder, decoder, criterion, task): 46 | encoder.eval() # affects the performance of dropout 47 | decoder.eval() 48 | 49 | loss, predictions, visual = run_inference(encoder, decoder, input_variable, \ 50 | target_variable, criterion, teach_ratio=0) 51 | 52 | queries = input_variable.data.tolist() 53 | targets = target_variable.data.tolist() 54 | predicted_tokens = [vocab.index_to_word(x, task) for x in predictions] 55 | query_tokens = [vocab.index_to_word(y[0], task) for y in queries] 56 | target_tokens = [vocab.index_to_word(z[0], task) for z in targets] 57 | 58 | avg_loss = loss.data[0] / target_variable.size()[0] 59 | bleu_score = BLEU.compute(predicted_tokens, target_tokens) 60 | turn_success = [pred == tar[0] for pred, tar in zip(predictions, targets)] 61 | 62 | return avg_loss, bleu_score, all(turn_success) 63 | 64 | def track_progress(args, encoder, decoder, verbose, debug, train_data, val_data, 65 | task, n_iters=1400, teacher_forcing=0.0, weight_decay=0.0): 66 | start = tm.time() 67 | bleu_scores, accuracy = [], [] 68 | learner = LossTracker(args.early_stopping) 69 | 70 | v_iters = len(val_data) 71 | n_iters = 600 if debug else n_iters 72 | print_every, plot_every, val_every = print_frequency(verbose, debug) 73 | print_loss_total = 0 # Reset every print_every 74 | plot_loss_total = 0 # Reset every plot_every 75 | 76 | if use_cuda: 77 | encoder = encoder.cuda() 78 | decoder = decoder.cuda() 79 | 80 | enc_optimizer, dec_optimizer = init_optimizers(args.optimizer, weight_decay, 81 | encoder.parameters(), decoder.parameters(), args.learning_rate) 82 | 83 | # training_pairs = [random.choice(train_data) for i in range(n_iters)] 84 | # validation_pairs = [random.choice(val_data) for j in range(v_iters)] 85 | criterion = NegLL_Loss() 86 | enc_scheduler = StepLR(enc_optimizer, step_size=n_iters/(args.decay_times+1), gamma=0.2) 87 | dec_scheduler = StepLR(dec_optimizer, step_size=n_iters/(args.decay_times+1), gamma=0.2) 88 | 89 | for iteration in range(n_iters): 90 | enc_scheduler.step() 91 | dec_scheduler.step() 92 | 93 | training_pair = train_data[iteration] # training_pairs[iter - 1] 94 | input_variable = training_pair[0] 95 | output_variable = training_pair[1] 96 | 97 | starting_checkpoint(iteration) 98 | loss = train(input_variable, output_variable, encoder, decoder, \ 99 | enc_optimizer, dec_optimizer, criterion, teach_ratio=teacher_forcing) 100 | print_loss_total += loss 101 | plot_loss_total += loss 102 | 103 | if iteration > 0 and iteration % print_every == 0: 104 | learner.train_steps.append(iteration + 1) 105 | print_loss_avg = print_loss_total / print_every 106 | print_loss_total = 0 # reset the print loss 107 | print('{1:3.1f}% complete {2}, Train Loss: {0:.4f}'.format(print_loss_avg, 108 | ((iteration + 1)/ n_iters * 100.0), timeSince(start, (iteration + 1)/ n_iters))) 109 | learner.update_loss(print_loss_avg, "train") 110 | 111 | if iteration > 0 and iteration % val_every == 0: 112 | learner.val_steps.append(iteration + 1) 113 | batch_val_loss, batch_bleu, batch_success = [], [], [] 114 | for j in range(v_iters): 115 | val_pair = validation_pairs[j] 116 | val_input = val_pair[0] 117 | val_output = val_pair[1] 118 | val_loss, bleu_score, turn_success = validate(val_input, \ 119 | val_output, encoder, decoder, criterion, task) 120 | batch_val_loss.append(val_loss) 121 | batch_bleu.append(bleu_score) 122 | batch_success.append(turn_success) 123 | 124 | avg_val_loss, avg_bleu, avg_success = evaluate.batch_processing( 125 | batch_val_loss, batch_bleu, batch_success) 126 | learner.update_loss(avg_val_loss, "val") 127 | bleu_scores.append(avg_bleu) 128 | accuracy.append(avg_success) 129 | if learner.should_early_stop(): 130 | print("Early stopped at val epoch {}".format(learner.val_epoch)) 131 | learner.completed_training = False 132 | break 133 | 134 | time_past(start) 135 | return learner, bleu_scores, accuracy 136 | 137 | if __name__ == "__main__": 138 | # ---- PARSE ARGS ----- 139 | args = solicit_args() 140 | task = args.task_name 141 | # ----- LOAD DATA ----- 142 | if args.debug: 143 | debug_data = pickle.load( open( "datasets/debug_data.pkl", "rb" ) ) 144 | train_variables, val_variables, max_length = debug_data 145 | if args.test_mode: 146 | test_data, candidates, max_length = data_io.load_dataset(args.task_name, "dev", args.debug) 147 | test_variables = collect_dialogues(test_data, task=task) 148 | 149 | encoder = torch.load("results/enc_vanilla_1b.pt") 150 | decoder = torch.load("results/dec_vanilla_1b.pt") 151 | show_dialogues(test_variables, encoder, decoder, task) 152 | grab_attention(val_data, encoder, decoder, task, 3) 153 | evaluate.show_save_attention(visualizations, args.attn_method, args.verbose) 154 | results = test_mode_run(test_variables, encoder, decoder, task) 155 | print("Done with visualizing.") 156 | else: 157 | train_data, max_length = data_io.load_dataset(args.task_name, "train", args.debug) 158 | train_variables = prepare_examples(train_data, args.context, task=task) 159 | val_data, _ = data_io.load_dataset(args.task_name, "dev", args.debug) 160 | val_variables = prepare_examples(val_data, args.context, task=task) 161 | # ---- BUILD MODEL ---- 162 | print("Running model {0}_{1}".format(args.model_path, args.suffix)) 163 | encoder, decoder = choose_model(args.model_type, vocab.ulary_size(task), 164 | args.hidden_size, args.attn_method, args.n_layers, args.drop_prob, max_length) 165 | # ---- TRAIN MODEL ---- 166 | results = track_progress(args, encoder, decoder, args.verbose, args.debug, 167 | train_variables, val_variables, task, n_iters=args.n_iters, 168 | teacher_forcing=args.teacher_forcing, weight_decay=args.weight_decay) 169 | # --- MANAGE RESULTS --- 170 | if args.save_model and results[0].completed_training: 171 | torch.save(encoder, "results/enc_{0}_{1}.pt".format(args.model_path, args.suffix)) 172 | torch.save(decoder, "results/dec_{0}_{1}.pt".format(args.model_path, args.suffix)) 173 | print('Model saved at results/model_{}!'.format(args.model_path)) 174 | if args.report_results and results[0].completed_training: 175 | evaluate.create_report(results, args, args.suffix) 176 | if args.plot_results and results[0].completed_training: 177 | evaluate.plot([strain, sval], [ltrain, lval], 'Training curve', 'Iterations', 'Loss') 178 | if args.visualize > 0: 179 | visualizations = grab_attention(val_variables, encoder, decoder, task, args.visualize) 180 | evaluate.show_save_attention(visualizations, args.attn_method, args.verbose) 181 | -------------------------------------------------------------------------------- /utils/external/dialog_constants.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on May 17, 2016 3 | 4 | @author: xiul, t-zalipt 5 | ''' 6 | 7 | sys_inform_slots_for_user = ['city', 'closing', 'date', 'distanceconstraints', 'greeting', 'moviename', 8 | 'numberofpeople', 'taskcomplete', 'price', 'starttime', 'state', 'theater', 9 | 'theater_chain', 'video_format', 'zip'] 10 | 11 | sys_request_slots = ['moviename', 'theater', 'starttime', 'date', 'numberofpeople', 'state', 'city', 'zip', 12 | 'distanceconstraints', 'video_format', 'theater_chain', 'price'] 13 | sys_inform_slots = ['moviename', 'theater', 'starttime', 'date', 'state', 'city', 'zip', 14 | 'distanceconstraints', 'video_format', 'theater_chain', 'price', 'taskcomplete', 'ticket'] 15 | # 16 | # sys_request_slots = ['moviename', 'theater', 'starttime', 'date', 'numberofpeople', 'genre', 'state', 'city', 'zip', 'critic_rating', 'mpaa_rating', 'distanceconstraints', 'video_format', 'theater_chain', 'price', 'actor', 'description', 'numberofkids'] 17 | # sys_inform_slots = ['moviename', 'theater', 'starttime', 'date', 'genre', 'state', 'city', 'zip', 'critic_rating', 'mpaa_rating', 'distanceconstraints', 'video_format', 'theater_chain', 'price', 'actor', 'description', 'numberofkids', 'taskcomplete', 'ticket'] 18 | # 19 | starting_dialogue_acts = ['request'] 20 | # start_dia_acts = { 21 | # 'greeting':[], 22 | # 'request': ['moviename', 'starttime', 'theater', 'city', 'state', 'date', 'ticket', 'numberofpeople'] 23 | # } 24 | 25 | # sys_request_slots = ['moviename', 'theater', 'starttime', 'date', 'numberofpeople', 'genre', 'state', 'city', 'zip', 26 | # 'critic_rating', 'mpaa_rating', 'distanceconstraints', 'video_format', 'theater_chain', 'price', 27 | # 'actor', 'description', 'other', 'numberofkids'] 28 | # sys_inform_slots = ['moviename', 'theater', 'starttime', 'date', 'genre', 'state', 'city', 'zip', 'critic_rating', 29 | # 'mpaa_rating', 'distanceconstraints', 'video_format', 'theater_chain', 'price', 'actor', 30 | # 'description', 'other', 'numberofkids', 'taskcomplete', 'ticket'] 31 | # 32 | # start_dia_acts = { 33 | # # 'greeting':[], 34 | # 'request': ['moviename', 'starttime', 'theater', 'city', 'state', 'date', 'genre', 'ticket', 'numberofpeople'] 35 | # } 36 | 37 | ################################################################################ 38 | # Dialog status 39 | ################################################################################ 40 | FAILED_DIALOG = -1 41 | SUCCESS_DIALOG = 1 42 | NO_OUTCOME_YET = 0 43 | 44 | # Rewards 45 | SUCCESS_REWARD = 40 46 | FAILURE_REWARD = 0 47 | PER_TURN_REWARD = 0 48 | 49 | ################################################################################ 50 | # Special Slot Values 51 | ################################################################################ 52 | I_DO_NOT_CARE = "I do not care" 53 | I_DO_NOT_KNOW = "I do not know" 54 | NO_VALUE_MATCH = "NO VALUE MATCHES!!!" 55 | TICKET_AVAILABLE = 'Ticket Available' 56 | 57 | ################################################################################ 58 | # Constraint Check 59 | ################################################################################ 60 | CONSTRAINT_CHECK_FAILURE = 0 61 | CONSTRAINT_CHECK_SUCCESS = 1 62 | 63 | ################################################################################ 64 | # NLG Beam Search 65 | ################################################################################ 66 | nlg_beam_size = 10 67 | 68 | ################################################################################ 69 | # run_mode: 0 for dia-act; 1 for NL; 2 for no output; 3 for skip everything 70 | ################################################################################ 71 | run_mode = 3 72 | auto_suggest = False # (or True) 73 | 74 | ################################################################################ 75 | # A Basic Set of Feasible actions to be Consdered By an RL agent 76 | ################################################################################ 77 | feasible_actions = [ 78 | ############################################################################ 79 | # greeting actions 80 | ############################################################################ 81 | # {'dialogue_act':"greeting", 'inform_slots':{}, 'request_slots':{}}, 82 | ############################################################################ 83 | # confirm_question actions 84 | ############################################################################ 85 | {'dialogue_act': "confirm_question", 'inform_slots': {}, 'request_slots': {}}, 86 | ############################################################################ 87 | # confirm_answer actions 88 | ############################################################################ 89 | {'dialogue_act': "confirm_answer", 'inform_slots': {}, 'request_slots': {}}, 90 | ############################################################################ 91 | # thanks actions 92 | ############################################################################ 93 | {'dialogue_act': "thanks", 'inform_slots': {}, 'request_slots': {}}, 94 | ############################################################################ 95 | # deny actions 96 | ############################################################################ 97 | {'dialogue_act': "deny", 'inform_slots': {}, 'request_slots': {}}, 98 | ] 99 | 100 | ############################################################################ 101 | # Adding the inform actions 102 | ############################################################################ 103 | 104 | 105 | sys_inform_slots_for_user = ['city', 'closing', 'date', 'distanceconstraints', 'greeting', 'moviename', 106 | 'numberofpeople', 'taskcomplete', 'price', 'starttime', 'state', 'theater', 107 | 'theater_chain', 'video_format', 'zip', 'description','numberofkids','genre'] 108 | 109 | sys_request_slots_for_user = ['city', 'date', 'moviename', 'numberofpeople', 'starttime', 'state', 'theater', 110 | 'theater_chain', 'video_format', 'zip', 'ticket'] 111 | 112 | for slot in sys_inform_slots: 113 | feasible_actions.append({'dialogue_act': 'inform', 'inform_slots': {slot: "PLACEHOLDER"}, 'request_slots': {}}) 114 | 115 | ############################################################################ 116 | # Adding the request actions 117 | ############################################################################ 118 | for slot in sys_request_slots: 119 | feasible_actions.append({'dialogue_act': 'request', 'inform_slots': {}, 'request_slots': {slot: "UNK"}}) 120 | 121 | feasible_actions_users = [ 122 | {'dialogue_act': "thanks", 'inform_slots': {}, 'request_slots': {}}, 123 | {'dialogue_act': "deny", 'inform_slots': {}, 'request_slots': {}}, 124 | {'dialogue_act': "closing", 'inform_slots': {}, 'request_slots': {}}, 125 | {'dialogue_act': "confirm_answer", 'inform_slots': {}, 'request_slots': {}} 126 | ] 127 | 128 | # for slot in sys_inform_slots_for_user: 129 | for slot in sys_inform_slots_for_user: 130 | feasible_actions_users.append({'dialogue_act': 'inform', 'inform_slots': {slot: "PLACEHOLDER"}, 'request_slots': {}}) 131 | 132 | feasible_actions_users.append( 133 | {'dialogue_act': 'inform', 'inform_slots': {'numberofpeople': "PLACEHOLDER"}, 'request_slots': {}}) 134 | 135 | ############################################################################ 136 | # Adding the request actions 137 | ############################################################################ 138 | for slot in sys_request_slots_for_user: 139 | feasible_actions_users.append({'dialogue_act': 'request', 'inform_slots': {}, 'request_slots': {slot: "UNK"}}) 140 | 141 | feasible_actions_users.append({'dialogue_act': 'inform', 'inform_slots': {}, 'request_slots': {}}) 142 | 143 | lexicon = {} 144 | lexicon['act_mapper'] = {'greeting': 'open', 'deny': 'reject', 'confirm_question': 'inform', 145 | 'inform': 'skip', 'thanks': 'accept', 'welcome': 'close', 'closing': 'close', 146 | 'request': 'skip', 'confirm_answer': 'accept', 'not_sure': 'reject'} 147 | lexicon['slot_mapper'] = {} 148 | lexicon['val_mapper'] = { I_DO_NOT_CARE: 'any', I_DO_NOT_KNOW: 'any', 149 | 'one': '1', 'two': '2', 'three': '3', 'four': '4', 'five': '5', 150 | 'six': '6', 'seven': '7', 'eight': '8', 'nine': '9' } 151 | -------------------------------------------------------------------------------- /utils/internal/initialization.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import random 3 | import numpy as np 4 | from scipy.stats import truncnorm 5 | import math 6 | import json 7 | import pandas as pd 8 | import pickle as pkl 9 | # from nltk import word_tokenize 10 | 11 | def pickle_io(filename, process, data=None): 12 | if process == "load": 13 | return pkl.load( open( filename, "rb" ) ) 14 | elif process == "save": 15 | pkl.dump(data, open( filename, "wb") ) 16 | 17 | def parse_dialogue(lines, tokenizer=True): 18 | ''' 19 | lines: f.readline(), which is actually a list of lines in txt 20 | Return [[(u1, r1), (u2, r2)...], [(u1, r1), (u2, r2)...], ...] 21 | ''' 22 | data = [] 23 | dialogue = [] 24 | kb_dialogue = [] 25 | kb = [] 26 | for line in lines: 27 | if line != '\n' and line != lines[-1]: 28 | nid, line = line.split(' ', 1) 29 | nid = int(nid) 30 | line = line.strip() # line.decode('utf-8').strip() for python 2.7 31 | 32 | if len(line.split('\t')) == 1: 33 | kb_dialogue.append(line.split('\t')) 34 | continue 35 | 36 | u, r = line.split('\t') 37 | if tokenizer is True: 38 | u = word_tokenize(u) 39 | r = word_tokenize(r) 40 | u = fix_silence(u) if len(u)>2 else u 41 | dialogue.append((u, r)) 42 | else: 43 | data.append(dialogue) 44 | kb.append(kb_dialogue) 45 | dialogue = [] 46 | kb_dialogue = [] 47 | return data, kb 48 | 49 | def fix_silence(utterance): 50 | ''' 51 | Special keyword is being split into three tokens 52 | So we revert that back to one token as required 53 | ''' 54 | if (utterance[0] == "<") and (utterance[1] == "SILENCE"): 55 | utterance[0] = "" 56 | utterance.pop(2) 57 | utterance.pop(1) 58 | 59 | return utterance 60 | 61 | def parse_candidates(lines): 62 | ''' 63 | :param lines: f.readlines() 64 | :return: list of all candidates ["hello", "A is a good restaurant"] 65 | ''' 66 | candidates = [] 67 | for line in lines: 68 | nid, line = line.split(' ', 1) 69 | line = line.decode('utf-8').strip() 70 | candidates.append(line.split('\t')) 71 | return candidates 72 | 73 | 74 | def parse_dialogue_QA(lines, tokenizer=True): 75 | ''' 76 | lines: f.readline(), which is actually a list of lines in txt 77 | Return [[(u1, u2, u3), (c1, c2, c3)], [(u1, u2, u3), (c1, c2, c3)], ...] 78 | ''' 79 | data = [] 80 | dialogue = [] 81 | kb_dialogue = [] 82 | kb = [] 83 | u = [] 84 | c = [] 85 | for line in lines: 86 | if line != '\n' and line != lines[-1]: 87 | nid, line = line.split(' ', 1) 88 | nid = int(nid) 89 | line = line.decode('utf-8').strip() 90 | 91 | if len(line.split('\t')) == 1: 92 | kb_dialogue.append(line.split('\t')) 93 | continue 94 | 95 | q, a = line.split('\t') 96 | if tokenizer is True: 97 | q = tokenize(q) 98 | a = tokenize(a) 99 | u.append(q) 100 | c.append(a) 101 | else: 102 | dialogue.append(tuple(u)) 103 | dialogue.append(tuple(c)) 104 | data.append(dialogue) 105 | 106 | kb.append(kb_dialogue) 107 | dialogue = [] 108 | u = [] 109 | c = [] 110 | kb_dialogue = [] 111 | 112 | return data, kb 113 | 114 | def read_restaurant_data(filename, restaraunt_prefix = "datasets/babi/"): 115 | ''' 116 | :param filename: 'directory/file.txt' 117 | :return:[ 118 | [(u1, r1), (u2, r2)...] 119 | , [(u1, r1), (u2, r2)...], ...] 120 | the data is a list of training example, each example consists of one dialog 121 | for each dialog, there are a number of turns 122 | each turn is made up of a tuple of (u_i, r_i) for up to N turns 123 | where ui is utterance from the customer 124 | where ri is a response from an agent 125 | each ui or ri, is a list of strings, for up to M tokens 126 | each token is usually a word or punctuation 127 | 128 | kb: the knowledge base in the format of 129 | [u'saint_johns_chop_house R_post_code saint_johns_chop_house_post_code', 130 | u'saint_johns_chop_house R_phone saint_johns_chop_house_phone', 131 | u'saint_johns_chop_house R_address saint_johns_chop_house_address', 132 | u'saint_johns_chop_house R_price moderate'] 133 | ''' 134 | restaurant_path = restaraunt_prefix + filename 135 | with open(restaurant_path) as f: 136 | # max_length = None 137 | data, kb = parse_dialogue(f.readlines()) 138 | return data, kb 139 | 140 | def select_consecutive_pairs(data, count): 141 | import pdb 142 | random_location = (random.random()/2.0) + 0.2 # random number from 0.2 to 0.7 143 | random_index = int(round(random_location * len(data))) 144 | random_query = data[random_index][0] # [0] is query, [1] is response 145 | turn_index = int(random_query[0].cpu().data.numpy()[0]) 146 | start_index = (random_index - (turn_index - 1)) 147 | 148 | dialogues = [] 149 | dialog = [] 150 | while len(dialogues) < count: 151 | turn_pair = data[start_index] 152 | # check if we reached the end of the dialog 153 | turn_count = int(turn_pair[0][0].data.cpu().numpy()[0]) 154 | if turn_count == 1 and (len(dialog) > 0): 155 | dialogues.append(dialog) 156 | dialog = [] 157 | dialog.append(turn_pair) 158 | start_index += 1 159 | return dialogues 160 | 161 | def spit_fire(dialogues): 162 | words = [] 163 | for dialog in dialogues: 164 | for turn in dialog: 165 | for utterance in turn: 166 | for token in utterance: 167 | token_index = int(token.data.numpy()[0]) 168 | words.append(vocab.index_to_word(token_index, "res")) 169 | print(words) 170 | 171 | def look4str(u, df): 172 | a = df['addrs'].apply(lambda x: x in u) 173 | b = df['pois'].apply(lambda x: x in u) 174 | a = df[a]['addrs'].as_matrix() 175 | b = df[b]['pois'].as_matrix() 176 | 177 | if len(a) != 0: 178 | u = u.replace(a[0], 'addr') 179 | if len(b) != 0: 180 | u = u.replace(b[0], 'poi') 181 | return u 182 | 183 | 184 | def load_incar_data(data_json): 185 | ''' 186 | :param data_json: a json file loaded from .json 187 | :return: 188 | navigate/weather/schedule_data, three lists for the three tasks 189 | each list is a list of dialogues, and each dialogue is a list of turns [(u1, r1), (u2, r2)...] 190 | each utterance/response is a list of tokens 191 | ''' 192 | lookup = pd.read_csv('datasets/in_car/incar_addr_poi.csv') 193 | navigate_data = [] 194 | schedule_data = [] 195 | weather_data = [] 196 | kbs = [] 197 | 198 | for dialogue in data_json: 199 | dia = [] 200 | u = None 201 | r = None 202 | for turn in dialogue['dialogue']: 203 | if turn['turn'] == 'driver': 204 | u = turn['data']['utterance'] 205 | u = look4str(u, lookup) 206 | u = word_tokenize(u.lower()) 207 | if turn['turn'] == 'assistant': 208 | r = turn['data']['utterance'] 209 | r = look4str(r, lookup) 210 | r = word_tokenize(r.lower()) 211 | dia.append((u, r)) 212 | 213 | if dialogue['scenario']['task']['intent'] == 'navigate': 214 | navigate_data.append(dia) 215 | elif dialogue['scenario']['task']['intent'] == 'schedule': 216 | schedule_data.append(dia) 217 | elif dialogue['scenario']['task']['intent'] == 'weather': 218 | weather_data.append(dia) 219 | else: 220 | print(dialogue['scenario']['task']['intent']) 221 | 222 | kbs.append(dialogue['scenario']['kb']) 223 | print('Loaded %i navigate data!'%len(navigate_data)) 224 | print('Loaded %i schedule data!'%len(schedule_data)) 225 | print('Loaded %i weather data!'%len(weather_data)) 226 | 227 | return navigate_data, weather_data, schedule_data, kbs 228 | 229 | 230 | def init_glove_words(name='6B', dim=100): 231 | ''' 232 | :param name: which glove you want to load 233 | :param dim: dimension of word vectors 234 | :return: the glove object in pytorch 235 | ''' 236 | import torchtext.vocab as vocab 237 | glove = vocab.GloVe(name='6B', dim=100) 238 | print('Loaded {} words'.format(len(glove.itos))) 239 | word_embeddings = glove 240 | return word_embeddings 241 | 242 | def init_normal_words(vocab_size=1229, dim=100): 243 | # add EOS and SOS and UNK? 244 | mean = 0 245 | stddev = 1.0/math.sqrt(2 * math.pi) 246 | # each word embedding is a column vector 247 | word_embeddings = truncnorm.rvs(a=mean, b=stddev, size=[dim, vocab_size]) 248 | return word_embeddings 249 | 250 | -------------------------------------------------------------------------------- /objects/modules/belief_tracker.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import os, pdb, sys # set_trace 3 | 4 | from torch import nn 5 | from nltk import word_tokenize 6 | from objects.blocks.base import BaseBeliefTracker 7 | from objects.blocks import Turn 8 | 9 | ''' 10 | class BaseBeliefTracker(object): 11 | def __init__(self, data): 12 | self.data = data 13 | self.learning_method = "supervised" # or "rulebased" # or "reinforce" 14 | 15 | def learn(self): 16 | raise NotImplementedError 17 | 18 | def predict(self): 19 | a belief tracker predicts user intents: 20 | input - user utterance, previous embedded context, memory 21 | output - a binary value for each possible slot in the ontology 22 | raise NotImplementedError 23 | ''' 24 | 25 | 26 | class RuleBeliefTracker(BaseBeliefTracker): 27 | def __init__(self, *args): 28 | super().__init__(args) 29 | 30 | def learn(self): 31 | print("rule-based belief tracker has no training") 32 | 33 | def predict(self, examples, batch_size=1): 34 | if batch_size > 1: # then examples is a list 35 | return [self.predict_one(exp) for exp in examples] 36 | else: # examples is a single item 37 | self.predict_one(examples) 38 | 39 | def predict_one(self, example): 40 | input_text 41 | 42 | 43 | class NeuralBeliefTracker(BaseBeliefTracker): 44 | def __init__(self, args, model): 45 | super().__init__(args, model) 46 | self.vocab = None 47 | 48 | def extract_beliefs(self, scores, threshold=0.01): 49 | batch_size = len(list(scores.values())[0]) 50 | predictions = [set() for i in range(batch_size)] 51 | ont = self.model.ontology 52 | for s in ont.slots: 53 | # idx is an index of the batch size 54 | for idx, p in enumerate(scores[s]): 55 | triggered = [(s, v, p_v) for v, p_v in zip(ont.values[s], p) if p_v > threshold] 56 | if batch_size == 1: # grab all triggered values, along with the score 57 | for belief in triggered: 58 | predictions[idx].add(belief) 59 | elif s == 'request': 60 | # we can have multiple requests predictions 61 | predictions[idx] |= set([(s, v) for s, v, p_v in triggered]) 62 | elif triggered: 63 | # only extract the top inform prediction 64 | sort = sorted(triggered, key=lambda tup: tup[-1], reverse=True) 65 | predictions[idx].add((sort[0][0], sort[0][1])) 66 | return predictions 67 | 68 | def extract_predictions(self, scores, threshold=0.5): 69 | batch_size = len(list(scores.values())[0]) 70 | predictions = [set() for i in range(batch_size)] 71 | ont = self.model.ontology 72 | for s in ont.slots: 73 | # idx is an index of the batch size 74 | for idx, p in enumerate(scores[s]): 75 | triggered = [(s, v, p_v) for v, p_v in zip(ont.values[s], p) if p_v > threshold] 76 | if batch_size == 1: # grab all triggered values, along with the score 77 | for belief in triggered: 78 | predictions[idx].add(belief) 79 | elif s == 'request': 80 | # we can have multiple requests predictions 81 | predictions[idx] |= set([(s, v) for s, v, p_v in triggered]) 82 | elif triggered: 83 | # only extract the top inform prediction 84 | sort = sorted(triggered, key=lambda tup: tup[-1], reverse=True) 85 | predictions[idx].add((sort[0][0], sort[0][1])) 86 | return predictions 87 | 88 | def run_glad_inference(self, data): 89 | self.model.eval() 90 | predictions = [] 91 | for batch in data.batch(self.batch_size): 92 | loss, scores = self.model(batch) 93 | predictions += self.extract_predictions(scores) 94 | return predictions 95 | 96 | def classify_intent(self, utterance, agent_action=None): 97 | """ input utterance is a string and agent action is a dict 98 | with 'slot_action' as key. For output, user_beliefs is 99 | a dict with dialogue act, inform_slots and request slots 100 | """ 101 | exp = {'turn_id': 14, 'utterance': '', 'user_intent': {}, 'num': {}, 102 | 'belief_state': {}, 'agent_actions': {}, 'agent_utterance': {}} 103 | 104 | tokens = utterance.replace(':', ' ').split() 105 | cleaned = [token.rstrip(',').rstrip('.') for token in tokens] 106 | exp['utterance'] = [''] + cleaned + [''] 107 | 108 | exp['num']['utterance'] = [self.w2i(word) for word in exp['utterance']] 109 | exp['num']['agent_actions'] = self.scrub(agent_action) 110 | 111 | turn = Turn.from_dict(exp) 112 | _, scores = self.model([turn]) 113 | predictions = self.extract_beliefs(scores) 114 | 115 | user_beliefs = {'dialogue_act': 'inform'} 116 | assert len(self.model.ontology.slots) == 10 117 | 118 | for slot_type in self.model.ontology.slots: 119 | user_beliefs[f'{slot_type}_slots'] = [] 120 | for slot, value, score in predictions[0]: 121 | user_beliefs[f'{slot}_slots'].append((value,score)) 122 | if slot == 'request' and score > 0.5: 123 | user_beliefs['dialogue_act'] = 'request' 124 | 125 | return user_beliefs 126 | 127 | 128 | def scrub(self, agent_action): 129 | 130 | def vectorize(text): 131 | return [self.w2i(word) for word in text.split()] 132 | extracted = [] 133 | 134 | if agent_action is None: 135 | text = f' = ' 136 | extracted.append(vectorize(text)) 137 | return extracted 138 | 139 | for slot, val in agent_action['slot_action']['inform_slots'].items(): 140 | text = f' {slot} = {val} ' 141 | extracted.append(vectorize(text)) 142 | for slot, val in agent_action['slot_action']['request_slots'].items(): 143 | text = f' request = {slot} ' 144 | extracted.append(vectorize(text)) 145 | 146 | dialogue_act = agent_action['slot_action']['dialogue_act'] 147 | if dialogue_act != 'request' and dialogue_act != 'inform': 148 | text = f' act = {dialogue_act} ' 149 | extracted.append(vectorize(text)) 150 | 151 | return extracted 152 | 153 | """ 154 | { 155 | "current_slots": { 156 | "inform_slots": { 157 | "numberofpeople": "3", 158 | "moviename": "risen" 159 | }, 160 | "request_slots": { 161 | "starttime": "UNK" 162 | }, 163 | "act_slots": {}, 164 | "proposed_slots": {}, 165 | "agent_request_slots": {} 166 | }, 167 | "kb_results_dict": { 168 | "numberofpeople": 0, 169 | "moviename": 12, 170 | "matching_all_constraints": 0 171 | }, 172 | "turn_count": 1, 173 | "history": [ 174 | { 175 | "turn_count": 0, 176 | "speaker": "user", 177 | "request_slots": { 178 | "starttime": "UNK" 179 | }, 180 | "inform_slots": { 181 | "numberofpeople": "3", 182 | "moviename": "risen" 183 | }, 184 | "dialogue_act": "request" 185 | } 186 | ], 187 | "user_action": { 188 | "turn_count": 0, 189 | "speaker": "user", 190 | "request_slots": { 191 | "starttime": "UNK" 192 | }, 193 | "inform_slots": { 194 | "numberofpeople": "3", 195 | "moviename": "risen" 196 | }, 197 | "dialogue_act": "request" 198 | }, 199 | "agent_action": null 200 | } 201 | 202 | 203 | { 204 | "current_slots": { 205 | "inform_slots": { 206 | "numberofpeople": "3", 207 | "moviename": "risen" 208 | }, 209 | "request_slots": { 210 | "starttime": "UNK" 211 | }, 212 | "act_slots": {}, 213 | "proposed_slots": {}, 214 | "agent_request_slots": {} 215 | }, 216 | "kb_results_dict": { 217 | "numberofpeople": 0, 218 | "moviename": 12, 219 | "matching_all_constraints": 0 220 | }, 221 | "turn_count": 1, 222 | "history": [ 223 | { 224 | "turn_count": 0, 225 | "speaker": "user", 226 | "request_slots": { 227 | "starttime": "UNK" 228 | }, 229 | "inform_slots": { 230 | "numberofpeople": "3", 231 | "moviename": "risen" 232 | }, 233 | "dialogue_act": "request" 234 | } 235 | ], 236 | "user_action": { 237 | "turn_count": 0, 238 | "speaker": "user", 239 | "request_slots": { 240 | "starttime": "UNK" 241 | }, 242 | "inform_slots": { 243 | "numberofpeople": "3", 244 | "moviename": "risen" 245 | }, 246 | "dialogue_act": "request" 247 | }, 248 | "agent_action": null 249 | } 250 | 251 | { 252 | "dialogue_act": "inform", 253 | "inform_slots": { 254 | "moviename": "london has fallen" 255 | }, 256 | "request_slots": {}, 257 | "turn_count": 2, 258 | "nl": "I want to watch london has fallen." 259 | } 260 | """ -------------------------------------------------------------------------------- /scripts/2_neural_based.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals, division 3 | from io import open 4 | import unicodedata 5 | import string 6 | import re 7 | import random 8 | import json 9 | import sys 10 | import time as tm 11 | import pandas as pd 12 | 13 | import torch 14 | import torch.nn as nn 15 | from torch.autograd import Variable 16 | from torch import optim 17 | 18 | import utils.internal.data_io as data_io 19 | import utils.internal.vocabulary as vocab 20 | import utils.internal.display as display 21 | from utils.internal.arguments import solicit_args 22 | from utils.external.clock import * 23 | from utils.external.preprocessers import * 24 | 25 | from model.encoders import Bid_GRU_Encoder, GRU_Encoder # LSTM_Encoder, RNN_Encoder 26 | from model.decoders import Bid_GRU_Decoder, GRU_Decoder # RNN_Attn_Decoder 27 | 28 | use_cuda = torch.cuda.is_available() 29 | MAX_LENGTH = 8 30 | n_layers = 1 31 | 32 | def train(input_variable, target_variable, encoder, decoder, \ 33 | encoder_optimizer, decoder_optimizer, criterion, max_length): 34 | encoder.train() 35 | decoder.train() 36 | 37 | encoder_hidden = encoder.initHidden() 38 | 39 | encoder_optimizer.zero_grad() 40 | decoder_optimizer.zero_grad() 41 | input_length = input_variable.size()[0] 42 | target_length = target_variable.size()[0] 43 | encoder_outputs = Variable(torch.zeros(max_length, encoder.hidden_size)) 44 | encoder_outputs = encoder_outputs.cuda() if use_cuda else encoder_outputs 45 | 46 | loss = 0 47 | 48 | for ei in range(min(max_length, input_length)): 49 | encoder_output, encoder_hidden = encoder(input_variable[ei], encoder_hidden) 50 | encoder_outputs[ei] = encoder_output[0][0] 51 | 52 | # encoder's last hidden state is the decoder's intial hidden state 53 | # last_enc_hidden_state = encoder_hidden[-1] 54 | decoder_input = Variable(torch.LongTensor([[vocab.SOS_token]])) 55 | decoder_input = decoder_input.cuda() if use_cuda else decoder_input 56 | # encoder's last hidden state is the decoder's intial hidden state 57 | decoder_hidden = encoder_hidden 58 | 59 | for di in range(target_length): 60 | decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden) 61 | topv, topi = decoder_output.data.topk(1) 62 | ni = topi[0][0] # the index of the top predicted word 63 | # the [0][0] is simply to extract a scalar from a 1x1x1 3d tensor 64 | decoder_input = Variable(torch.LongTensor([[ni]])) 65 | decoder_input = decoder_input.cuda() if use_cuda else decoder_input 66 | loss += criterion(decoder_output, target_variable[di]) 67 | if ni == vocab.EOS_token: 68 | break 69 | 70 | loss.backward() 71 | 72 | encoder_optimizer.step() 73 | decoder_optimizer.step() 74 | 75 | return loss.data[0] / target_length 76 | 77 | def validate(input_variable, target_variable, encoder, decoder, criterion, max_length): 78 | encoder.eval() 79 | decoder.eval() 80 | 81 | encoder_hidden = encoder.initHidden() 82 | 83 | input_length = input_variable.size()[0] 84 | target_length = target_variable.size()[0] 85 | encoder_outputs = Variable(torch.zeros(max_length, encoder.hidden_size)) 86 | encoder_outputs = encoder_outputs.cuda() if use_cuda else encoder_outputs 87 | loss = 0 88 | 89 | for ei in range(min(max_length, input_length)): 90 | encoder_output, encoder_hidden = encoder(input_variable[ei], encoder_hidden) 91 | encoder_outputs[ei] = encoder_output[0][0] 92 | 93 | decoder_input = Variable(torch.LongTensor([[vocab.SOS_token]])) 94 | decoder_input = decoder_input.cuda() if use_cuda else decoder_input 95 | # encoder's last hidden state is the decoder's intial hidden state 96 | decoder_hidden = encoder_hidden 97 | 98 | for di in range(target_length): 99 | decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden) 100 | # decoder_input, decoder_hidden, last_enc_hidden_state, encoder_outputs) To be used later for attention 101 | topv, topi = decoder_output.data.topk(1) 102 | ni = topi[0][0] 103 | decoder_input = Variable(torch.LongTensor([[ni]])) 104 | decoder_input = decoder_input.cuda() if use_cuda else decoder_input 105 | 106 | loss += criterion(decoder_output, target_variable[di]) #############???? 107 | if ni == vocab.EOS_token: 108 | break 109 | 110 | return loss.data[0] / target_length 111 | 112 | def track_progress(encoder, decoder, train_data, val_data, task, max_length=8, \ 113 | n_iters=75000, print_every=5000, plot_every=100, val_every=150, \ 114 | learning_rate=0.01, ): 115 | start = tm.time() 116 | plot_losses_train = [] 117 | plot_losses_validation = [] 118 | plot_steps_train = [] 119 | plot_steps_validation = [] 120 | 121 | v_iters = len(val_data) if task == 'car' else int(len(val_data)/500) - 1 122 | print_loss_total = 0 # Reset every print_every 123 | plot_loss_total = 0 # Reset every plot_every 124 | 125 | if use_cuda: 126 | encoder = encoder.cuda() 127 | decoder = decoder.cuda() 128 | 129 | if args.optimizer == 'SGD': 130 | encoder_optimizer = optim.SGD(encoder.parameters(), lr=learning_rate) 131 | decoder_optimizer = optim.SGD(decoder.parameters(), lr=learning_rate) 132 | 133 | elif args.optimizer == 'Adam': 134 | encoder_optimizer = optim.Adam(encoder.parameters(), lr=learning_rate * 0.01) 135 | decoder_optimizer = optim.Adam(decoder.parameters(), lr=learning_rate * 0.01) 136 | else: 137 | encoder_optimizer = optim.RMSprop(encoder.parameters(), lr=learning_rate) 138 | decoder_optimizer = optim.RMSprop(decoder.parameters(), lr=learning_rate) 139 | 140 | training_pairs = [random.choice(train_data) for i in xrange(n_iters)] 141 | validation_pairs = [random.choice(val_data) for j in xrange(v_iters)] 142 | criterion = nn.NLLLoss() 143 | 144 | for iter in range(1, n_iters + 1): 145 | training_pair = training_pairs[iter - 1] 146 | input_variable = training_pair[0] 147 | 148 | output_variable = training_pair[1] 149 | 150 | loss = train(input_variable, output_variable, encoder, decoder, \ 151 | encoder_optimizer, decoder_optimizer, criterion, max_length) 152 | print_loss_total += loss 153 | plot_loss_total += loss 154 | 155 | if iter % print_every == 0: 156 | print_loss_avg = print_loss_total / print_every 157 | print_loss_total = 0 158 | print('%d%% complete %s, Train Loss: %.4f' % ((iter / n_iters * 100), 159 | timeSince(start, iter / n_iters), print_loss_avg)) 160 | # plot_losses_train.append(print_loss_avg) 161 | plot_losses_train.append(print_loss_avg) 162 | plot_steps_train.append(iter) 163 | 164 | if iter % val_every == 0: 165 | plot_steps_validation.append(iter) 166 | validation_losses = [] 167 | for iter in range(1, v_iters + 1): 168 | validation_pair = validation_pairs[iter - 1] 169 | validation_input = validation_pair[0] 170 | validation_output = validation_pair[1] 171 | val_loss = validate(validation_input, validation_output, encoder, decoder, criterion, max_length) 172 | validation_losses.append(val_loss) 173 | print('Validation loss = ', sum(validation_losses) * 1.0 / len(validation_losses)) 174 | plot_losses_validation.append(sum(validation_losses) * 1.0 / len(validation_losses)) 175 | 176 | return plot_losses_train, plot_losses_validation, plot_steps_train, plot_steps_validation 177 | 178 | if __name__ == "__main__": 179 | # ---- PARSE ARGS ----- 180 | args = solicit_args() 181 | task = task_simplification(args.task_name) 182 | # ----- LOAD DATA ----- 183 | train_data, candidates, max_length = data_io.load_dataset(args.task_name, \ 184 | "trn", args.debug) 185 | train_variables = collect_dialogues(train_data, task) 186 | val_data, val_candidates, _ = data_io.load_dataset(args.task_name, "dev") 187 | val_variables = collect_dialogues(val_data, task) 188 | # ---- BUILD MODEL ---- 189 | encoder = Bid_GRU_Encoder(vocab.ulary_size(task), args.hidden_size, use_cuda) 190 | decoder = Bid_GRU_Decoder(vocab.ulary_size(task), args.hidden_size, use_cuda, n_layers) 191 | # ---- TRAIN MODEL ---- 192 | ltrain, lval, strain, sval = track_progress(encoder, decoder, train_variables, 193 | val_variables, task, max_length, n_iters=args.n_iters, print_every=150) 194 | # --- MANAGE RESULTS --- 195 | if args.save_results: 196 | torch.save(encoder, args.encoder_path) 197 | torch.save(decoder, args.decoder_path) 198 | print('Model saved!') 199 | errors = pd.DataFrame(data={'train_steps':strain, 'valid_steps':sval, 'train_error': ltrain, 'validation_error':lval}) 200 | errors.to_csv(args.error_path, index=False) 201 | print('Error saved!') 202 | if args.plot_results: 203 | display.plot([strain, sval], [ltrain, lval], 'Training curve', 'Iterations', 'Loss') 204 | 205 | 206 | -------------------------------------------------------------------------------- /operators/learn/builder.py: -------------------------------------------------------------------------------- 1 | import os, pdb, sys # set_trace 2 | import random 3 | import torch 4 | import torch.nn as nn 5 | from torch.nn.parameter import Parameter 6 | 7 | from objects.components import device, get_saves 8 | from objects.models import encoders as enc 9 | from objects.models import decoders as dec 10 | from objects.models import * 11 | from objects.modules import * 12 | 13 | class Builder(object): 14 | def __init__(self, args, loader): 15 | self.args = args 16 | self.use_existing = args.use_existing 17 | self.model_type = args.model 18 | self.dhid = args.hidden_dim 19 | self.test_mode = args.test_mode 20 | self.use_old_nlu = args.use_old_nlu 21 | 22 | self.loader = loader 23 | self.data_dir = loader.data_dir 24 | self.embeddings = loader.embeddings if args.pretrained else None 25 | 26 | def get_model(self, processor, monitor, model_type=None): 27 | self.prepare_directory() 28 | model = self.create_model(processor, model_type) 29 | monitor.build_logger(self.dir) 30 | 31 | model_str = "model" if model_type is None else model_type 32 | if self.test_mode: 33 | print("Loading {} at {} for testing".format(model_str, self.dir)) 34 | model = self.load_best_model(self.dir, model, model_type) 35 | elif self.use_existing: 36 | print("Resuming {} at {} for training".format(model_str, self.dir)) 37 | model = self.load_best_model(self.dir, model, model_type) 38 | else: 39 | print("Building {} at {}".format(model_str, self.dir)) 40 | 41 | model.save_dir = self.dir 42 | return model 43 | 44 | def add_loss_function(self, model, function_type): 45 | if function_type == "negative_log_likelihood": 46 | model.criterion = nn.NLLLoss() 47 | elif function_type == "binary_cross_entropy": 48 | model.criterion = nn.BCELoss() 49 | else: 50 | raise(ValueError, "loss function is not valid") 51 | return model 52 | 53 | def prepare_directory(self): 54 | model_path = self.args.prefix + self.args.model + self.args.suffix 55 | self.dir = os.path.join("results", self.args.task, self.args.dataset, model_path) 56 | if not os.path.exists(self.dir): 57 | os.makedirs(self.dir) 58 | print("Created directory at {}".format(self.dir)) 59 | 60 | def load_best_model(self, directory, model, model_type): 61 | scores_and_files = get_saves(directory, self.args.early_stop) 62 | if scores_and_files: 63 | assert scores_and_files, 'no saves exist at {}'.format(directory) 64 | score, filepath = scores_and_files[0] 65 | elif model.module_type == 'intent_tracker': 66 | model.load_nlu_model('nlu_1468447442') 67 | return model 68 | elif model.module_type == 'text_generator': 69 | model.load_nlg_model('nlg_1468202263') 70 | return model 71 | else: 72 | filename = "epoch=12_success=25.4042_recall@two=41.3395" 73 | filepath = os.path.join(self.save_dir, filename) 74 | 75 | checkpoint = self.loader.restore_checkpoint(filepath) 76 | model.load_state_dict(checkpoint['model']) 77 | model.existing_checkpoint = checkpoint 78 | model.train() if self.use_existing else model.eval() 79 | 80 | return model 81 | 82 | def create_model(self, processor, model_type): 83 | input_size, output_size = processor.input_output_cardinality() 84 | if model_type is None: 85 | model_type = self.model_type 86 | 87 | if model_type == "basic": 88 | encoder = enc.RNN_Encoder(input_size, self.args) 89 | ff_network = dec.FF_Network(self.dhid, output_size, model_type) 90 | model = BasicClassifer(encoder, ff_network, self.args) 91 | model = self.add_loss_function(model, "negative_log_likelihood") 92 | if model_type in ["bilstm", "dual", "per_slot"]: 93 | encoder = enc.BiLSTM_Encoder(input_size, self.embeddings, self.args) 94 | ff_network = dec.FF_Network(self.dhid, output_size, model_type) 95 | model = BasicClassifer(encoder, ff_network, self.args) 96 | model = self.add_loss_function(model, "negative_log_likelihood") 97 | elif model_type == "rulebased": 98 | model = HackPolicy(processor.ontology) 99 | mmodel_type = model_type 100 | elif model_type == "copy": 101 | encoder = enc.Match_Encoder(input_size, self.dhid) 102 | decoder = dec.Copy_Without_Attn_Decoder(output_size, self.args) 103 | zeros_tensor = torch.zeros(input_size, max_length=25) 104 | copy_tensor = [zeros_tensor, encoder.embedding.weight.data] 105 | decoder.embedding.weight = Parameter(torch.cat(copy_tensor, dim=1)) 106 | model = Seq2Seq(encoder, decoder) 107 | elif model_type == "combined": 108 | encoder = enc.Match_Encoder(input_size, self.dhid) 109 | decoder = dec.Copy_Decoder(output_size, self.args) 110 | zeros_tensor = torch.zeros(input_size, max_length=25) 111 | copy_tensor = [zeros_tensor, encoder.embedding.weight.data] 112 | decoder.embedding.weight = Parameter(torch.cat(copy_tensor, dim=1)) 113 | model = Seq2Seq(encoder, decoder) 114 | elif model_type == "transformer": 115 | encoder = enc.Transformer_Encoder(input_size, self.args) 116 | decoder = dec.Transformer_Decoder(output_size, self.args) 117 | model = Seq2Seq(encoder, decoder) 118 | elif model_type == "glad": 119 | model = GLAD(self.args, self.loader.ontology, 120 | self.loader.vocab, self.embeddings, enc.GLAD_Encoder) 121 | model = self.add_loss_function(model, "binary_cross_entropy") 122 | model.module_type = 'belief_tracker' 123 | elif model_type == 'ddq': 124 | model = DQN(input_size, self.dhid, output_size) 125 | model.module_type = 'policy_manager' 126 | return model.to(torch.device('cpu')) 127 | elif model_type == "attention": 128 | encoder = enc.GRU_Encoder(input_size, self.args) 129 | decoder = dec.Attn_Decoder(output_size, self.args) 130 | model = Seq2Seq(encoder, decoder) 131 | model.module_type = 'text_generator' 132 | elif model_type == 'nlg_model': 133 | model = NLG(self.loader, 'results/end_to_end/ddq/movies/') 134 | model.load_nlg_model('nlg_1468202263') 135 | model.load_natural_langauge_templates('nl_templates') 136 | model.module_type = 'text_generator' 137 | return model 138 | elif model_type == 'nlu_model': 139 | model = NLU(self.loader, 'results/end_to_end/ddq/movies/') 140 | model.load_nlu_model('nlu_1468447442') 141 | model.module_type = 'intent_tracker' 142 | return model 143 | 144 | return model.to(device) 145 | 146 | def configure_module(self, args, model): 147 | if model.module_type == 'belief_tracker': 148 | module = NeuralBeliefTracker(args, model) 149 | 150 | elif model.module_type == 'policy_manager': 151 | kb, ontology = self.loader.kb, self.loader.ontology 152 | if args.model == 'rulebased': 153 | module = RulebasedPolicyManager(args, model, kb, ontology) 154 | results_dir = os.path.join('results', args.dataset, 'models') 155 | module.user.text_generator = RuleTextGenerator.from_pretrained(args) 156 | module.user.text_generator.set_templates(args.dataset) 157 | elif args.model == 'ddq': 158 | pm_model = NeuralPolicyManager(args, model) 159 | goal_set = self.loader.json_data('goal_set_v2') 160 | 161 | user_sim = RuleSimulator(args, ontology, goal_set) 162 | world_sim = NeuralSimulator(args, ontology, goal_set) 163 | real_user = CommandLineUser(args, ontology, goal_set) 164 | turk_user = MechanicalTurkUser(args, ontology, goal_set) 165 | users = (user_sim, world_sim, real_user, turk_user) 166 | 167 | results_dir = os.path.join("results", self.args.task, self.args.dataset) 168 | nlu_model = NLU(self.loader, results_dir) 169 | nlu_model.load_nlu_model('nlu_1468447442') 170 | 171 | nlg_model = NLG(self.loader, results_dir) 172 | nlg_model.load_nlg_model('nlg_1468202263') 173 | nlg_model.load_natural_langauge_templates('dia_act_nl_pairs.v6') 174 | 175 | user_sim.nlu_model = nlu_model 176 | world_sim.nlu_model = nlu_model 177 | user_sim.nlg_model = nlg_model 178 | world_sim.nlg_model = nlg_model 179 | 180 | pm_model.configure_settings(device, world_sim, ontology, kb) 181 | module = DialogManager(args, pm_model, users, ontology, kb) 182 | 183 | elif model.module_type == 'text_generator': 184 | module = model 185 | 186 | module.dir = self.dir 187 | return module 188 | 189 | def create_agent(self, bt_model, pm_model, tg_model): 190 | kb, goals = self.loader.kb, self.loader.goals 191 | ontology = self.loader.ontology 192 | 193 | user_sim = RuleSimulator(self.args, ontology, goals) 194 | world_sim = NeuralSimulator(self.args, ontology, goals) 195 | users = (user_sim, world_sim) 196 | 197 | if self.use_old_nlu: 198 | belief_tracker = bt_model 199 | else: 200 | belief_tracker = NeuralBeliefTracker(self.args, bt_model) 201 | 202 | policy_manager = NeuralPolicyManager(self.args, pm_model) 203 | text_generator = NeuralTextGenerator(self.args, tg_model) 204 | 205 | user_sim.nlu_model = belief_tracker 206 | world_sim.nlu_model = belief_tracker 207 | user_sim.nlg_model = text_generator 208 | world_sim.nlg_model = text_generator 209 | 210 | policy_manager.configure_settings(device, world_sim, ontology, kb) 211 | 212 | return DialogManager(self.args, policy_manager, users, ontology, kb) 213 | --------------------------------------------------------------------------------