mirror of
https://github.com/wassname/Castor.git
synced 2026-10-29 12:40:22 +08:00
+ renamed sm_model + faster bridge by obtaining the IDF scores of a term directly from the Java server
This commit is contained in:
1 parent
7d0a78d1a7
commit
212aa6fb93
27 files changed
+194
-210
No files matched your search
@@ -3,5 +3,5 @@ from setuptools import setup
|
||||
setup(name='castor',
|
||||
version='1.0.0',
|
||||
description='models for question answering',
|
||||
packages=['sm_model'],
|
||||
packages=['sm_cnn'],
|
||||
)
|
||||
File renamed without changes.
@@ -28,16 +28,16 @@ git clone https://github.com/castorini/Castor.git
|
||||
This should generate:
|
||||
```
|
||||
├── Castor
|
||||
│ ├── castorini_smmodel_bridge.py
|
||||
│ ├── README.md
|
||||
│ └── sm_model/
|
||||
│ ├── idf_baseline
|
||||
│ ├── kim_cnn
|
||||
│ └── sm_cnn
|
||||
├── data
|
||||
│ ├── README.md
|
||||
│ ├── TrecQA/
|
||||
│ └── word2vec/
|
||||
└── models
|
||||
├── README.md
|
||||
└── sm_model/
|
||||
└── sm_cnn/
|
||||
```
|
||||
|
||||
2. Preprocess data
|
||||
@@ -57,7 +57,7 @@ python3 build_vocab.py
|
||||
Make trec_eval
|
||||
|
||||
```
|
||||
cd Castor/sm_model/
|
||||
cd Castor/sm_cnn/
|
||||
cd trec_eval-8.0
|
||||
make clean && make
|
||||
cd ..
|
||||
@@ -65,9 +65,9 @@ cd ..
|
||||
|
||||
To train the S&M model on TrecQA
|
||||
```
|
||||
python main.py ../../model/sm_model/sm_model.train-all
|
||||
python main.py ../../models/sm_model/sm_model.TrecQA.TRAIN-ALL.2017-04-02.castor
|
||||
```
|
||||
The final model will be saved to ```../../model/sm_model/sm_model.train-all```
|
||||
The final model will be saved to ```../../models/sm_model/sm_model.TrecQA.TRAIN-ALL.2017-04-02.castor```
|
||||
|
||||
_NOTE:_ On first run, the program will create a memory-mapped cache for word e mbeddings (943MB) in ``data/word2vec``.
|
||||
The cache allows for faster loading of data in future runs.
|
||||
File renamed without changes.
@@ -0,0 +1,186 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import Counter
|
||||
import argparse
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from nltk.tokenize import TreebankWordTokenizer
|
||||
from torch.autograd import Variable
|
||||
from py4j.java_gateway import JavaGateway
|
||||
|
||||
from sm_cnn import model
|
||||
from sm_cnn.external_features import compute_overlap, compute_idf_weighted_overlap, stopped
|
||||
|
||||
sys.modules['model'] = model
|
||||
|
||||
|
||||
class SMModelBridge(object):
|
||||
|
||||
def __init__(self, model_file, word_embeddings_cache_file, index_path):
|
||||
# init torch random seeds
|
||||
torch.manual_seed(1234)
|
||||
np.random.seed(1234)
|
||||
|
||||
# load model
|
||||
self.model = model.QAModel.load(model_file)
|
||||
# load vectors
|
||||
self.vec_dim = self._preload_cached_embeddings(word_embeddings_cache_file)
|
||||
self.unk_term_vec = np.random.uniform(-0.25, 0.25, self.vec_dim)
|
||||
|
||||
self.index = index_path
|
||||
|
||||
|
||||
def _preload_cached_embeddings(self, cache_file):
|
||||
|
||||
with open(cache_file + '.dimensions') as d:
|
||||
vocab_size, vec_dim = [int(e) for e in d.read().strip().split()]
|
||||
|
||||
self.W = np.memmap(cache_file, dtype=np.double, shape=(vocab_size, vec_dim))
|
||||
|
||||
with open(cache_file + '.vocab') as f:
|
||||
w2v_vocab_list = map(str.strip, f.readlines())
|
||||
|
||||
self.vocab_dict = {w:k for k, w in enumerate(w2v_vocab_list)}
|
||||
return vec_dim
|
||||
|
||||
|
||||
def parse(self, sentence):
|
||||
s_toks = TreebankWordTokenizer().tokenize(sentence)
|
||||
s_str = ' '.join(s_toks).lower()
|
||||
return s_str
|
||||
|
||||
|
||||
def make_input_matrix(self, sentence):
|
||||
terms = sentence.strip().split()
|
||||
# word_embeddings = torch.zeros(max_len, vec_dim).type(torch.DoubleTensor)
|
||||
word_embeddings = torch.zeros(len(terms), self.vec_dim).type(torch.DoubleTensor)
|
||||
for i in range(len(terms)):
|
||||
word = terms[i]
|
||||
if word not in self.vocab_dict:
|
||||
emb = torch.from_numpy(self.unk_term_vec)
|
||||
else:
|
||||
emb = torch.from_numpy(self.W[self.vocab_dict[word]])
|
||||
word_embeddings[i] = emb
|
||||
input_tensor = torch.zeros(1, self.vec_dim, len(terms))
|
||||
input_tensor[0] = torch.transpose(word_embeddings, 0, 1)
|
||||
return input_tensor
|
||||
|
||||
|
||||
def get_tensorized_inputs(self, batch_ques, batch_sents, batch_ext_feats):
|
||||
assert(1 == len(batch_ques))
|
||||
tensorized_inputs = []
|
||||
for i in range(len(batch_ques)):
|
||||
xq = Variable(self.make_input_matrix(batch_ques[i]))
|
||||
xs = Variable(self.make_input_matrix(batch_sents[i]))
|
||||
ext_feats = Variable(torch.FloatTensor(batch_ext_feats[i]))
|
||||
ext_feats = torch.unsqueeze(ext_feats, 0)
|
||||
tensorized_inputs.append((xq, xs, ext_feats))
|
||||
return tensorized_inputs
|
||||
|
||||
def rerank_candidate_answers(self, question, answers, idf_json):
|
||||
# run through the model
|
||||
scores_sentences = []
|
||||
question = self.parse(question)
|
||||
term_idfs = json.loads(idf_json)
|
||||
term_idfs = dict((k, float(v)) for k, v in term_idfs.items())
|
||||
|
||||
for answer in answers:
|
||||
answer = self.parse(answer)
|
||||
|
||||
overlap = compute_overlap([question], [answer])
|
||||
idf_weighted_overlap = compute_idf_weighted_overlap([question], [answer], term_idfs)
|
||||
overlap_no_stopwords =\
|
||||
compute_overlap(stopped([question]), stopped([answer]))
|
||||
idf_weighted_overlap_no_stopwords =\
|
||||
compute_idf_weighted_overlap(stopped([question]), stopped([answer]), term_idfs)
|
||||
ext_feats = [np.array(feats) for feats in zip(overlap, idf_weighted_overlap,\
|
||||
overlap_no_stopwords, idf_weighted_overlap_no_stopwords)]
|
||||
|
||||
xq, xa, x_ext_feats = self.get_tensorized_inputs([question], [answer], \
|
||||
ext_feats)[0]
|
||||
pred = self.model(xq, xa, x_ext_feats)
|
||||
pred = torch.exp(pred)
|
||||
scores_sentences.append((pred.data.squeeze()[1], answer))
|
||||
|
||||
return scores_sentences
|
||||
|
||||
def get_term_idf_json_list(index_path, sent_list):
|
||||
gateway = JavaGateway()
|
||||
index = gateway.jvm.java.lang.String(index_path)
|
||||
pyserini = gateway.jvm.io.anserini.py4j.PyseriniEntryPoint()
|
||||
pyserini.initializeWithIndex(index_path)
|
||||
java_list = gateway.jvm.java.util.ArrayList()
|
||||
|
||||
for l in sent_list:
|
||||
java_list.add(l)
|
||||
|
||||
json_object = pyserini.getTermIdfJSONs(java_list)
|
||||
return json_object
|
||||
|
||||
if __name__ == "__main__":
|
||||
ap = argparse.ArgumentParser(description="Bridge Demo. Produces scores in trec_eval format",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
ap.add_argument('model', help="the path to the saved model file")
|
||||
ap.add_argument('--word-embeddings-cache', help="the embeddings 'cache' file",\
|
||||
default='../data/word2vec/aquaint+wiki.txt.gz.ndim=50.cache')
|
||||
ap.add_argument('index_path', help="the path to the source corpus index")
|
||||
# ap.add_argument('--paper-ext-feats', action="store_true", \
|
||||
# help="external features as per the paper")
|
||||
ap.add_argument('--dataset-folder', help="the QA dataset folder {TrecQA|WikiQA}",
|
||||
default='../data/TrecQA/')
|
||||
|
||||
args = ap.parse_args()
|
||||
|
||||
smmodel = SMModelBridge(
|
||||
args.model,
|
||||
args.word_embeddings_cache,
|
||||
args.index_path
|
||||
)
|
||||
|
||||
train_set, dev_set, test_set = 'train', 'dev', 'test'
|
||||
if 'TrecQA' in args.dataset_folder:
|
||||
train_set, dev_set, test_set = 'train-all', 'raw-dev', 'raw-test'
|
||||
|
||||
|
||||
|
||||
for split in [dev_set, test_set]:
|
||||
outfile = open('bridge.{}.scores'.format(split), 'w')
|
||||
|
||||
questions = [q.strip() for q in \
|
||||
open(os.path.join(args.dataset_folder, split, 'a.toks')).readlines()]
|
||||
answers = [q.strip() for q in \
|
||||
open(os.path.join(args.dataset_folder, split, 'b.toks')).readlines()]
|
||||
labels = [q.strip() for q in \
|
||||
open(os.path.join(args.dataset_folder, split, 'sim.txt')).readlines()]
|
||||
qids = [q.strip() for q in \
|
||||
open(os.path.join(args.dataset_folder, split, 'id.txt')).readlines()]
|
||||
|
||||
qid_question = dict(zip(qids, questions))
|
||||
q_counts = Counter(questions)
|
||||
|
||||
answers_offset = 0
|
||||
docid_counter = 0
|
||||
|
||||
all_questions_answers = questions + answers
|
||||
idf_json = get_term_idf_json_list(args.index_path, all_questions_answers)
|
||||
|
||||
for qid, question in sorted(qid_question.items(), key=lambda x: float(x[0])):
|
||||
num_answers = q_counts[question]
|
||||
q_answers = answers[answers_offset: answers_offset + num_answers]
|
||||
answers_offset += num_answers
|
||||
sentence_scores = smmodel.rerank_candidate_answers(question, q_answers, idf_json)
|
||||
|
||||
for score, sentence in sentence_scores:
|
||||
print('{} Q0 {} 0 {} sm_cnn_bridge.{}.run'.format(
|
||||
qid,
|
||||
docid_counter,
|
||||
score,
|
||||
os.path.basename(args.dataset_folder)
|
||||
), file=outfile)
|
||||
docid_counter += 1
|
||||
if 'WikiQA' in args.dataset_folder:
|
||||
docid_counter = 0
|
||||
|
||||
outfile.close()
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,202 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import pickle
|
||||
import string
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from nltk.tokenize import TreebankWordTokenizer
|
||||
from torch.autograd import Variable
|
||||
|
||||
from sm_model import model
|
||||
|
||||
sys.modules['model'] = model
|
||||
|
||||
|
||||
class SMModelBridge(object):
|
||||
|
||||
def __init__(self, model_file, word_embeddings_cache_file, stopwords_file, word2dfs_file):
|
||||
# init torch random seeds
|
||||
torch.manual_seed(1234)
|
||||
np.random.seed(1234)
|
||||
|
||||
# load model
|
||||
self.model = model.QAModel.load(model_file)
|
||||
# load vectors
|
||||
self.vec_dim = self._preload_cached_embeddings(word_embeddings_cache_file)
|
||||
self.unk_term_vec = np.random.uniform(-0.25, 0.25, self.vec_dim)
|
||||
|
||||
# stopwords
|
||||
self.stoplist = set([line.strip() for line in open(stopwords_file)])
|
||||
|
||||
# word dfs
|
||||
if os.path.isfile(word2dfs_file):
|
||||
with open(word2dfs_file, "rb") as w2dfin:
|
||||
self.word2dfs = pickle.load(w2dfin)
|
||||
|
||||
|
||||
def _preload_cached_embeddings(self, cache_file):
|
||||
|
||||
with open(cache_file + '.dimensions') as d:
|
||||
vocab_size, vec_dim = [int(e) for e in d.read().strip().split()]
|
||||
|
||||
self.W = np.memmap(cache_file, dtype=np.double, shape=(vocab_size, vec_dim))
|
||||
|
||||
with open(cache_file + '.vocab') as f:
|
||||
w2v_vocab_list = map(str.strip, f.readlines())
|
||||
|
||||
self.vocab_dict = {w:k for k, w in enumerate(w2v_vocab_list)}
|
||||
return vec_dim
|
||||
|
||||
|
||||
def parser(self, q, a):
|
||||
q_toks = TreebankWordTokenizer().tokenize(q)
|
||||
q_str = ' '.join(q_toks).lower()
|
||||
a_list = []
|
||||
for ans in a:
|
||||
ans_toks = TreebankWordTokenizer().tokenize(ans)
|
||||
a_str = ' '.join(ans_toks).lower()
|
||||
a_list.append(a_str)
|
||||
return q_str, a_list
|
||||
|
||||
|
||||
def compute_overlap_features(self, q_str, a_list, word2df=None, stoplist=None):
|
||||
word2df = word2df if word2df else {}
|
||||
stoplist = stoplist if stoplist else set()
|
||||
feats_overlap = []
|
||||
for a in a_list:
|
||||
question = q_str.split()
|
||||
answer = a.split()
|
||||
# q_set = set(question)
|
||||
# a_set = set(answer)
|
||||
q_set = set([q for q in question if q not in stoplist])
|
||||
a_set = set([a for a in answer if a not in stoplist])
|
||||
word_overlap = q_set.intersection(a_set)
|
||||
# overlap = float(len(word_overlap)) / (len(q_set) * len(a_set) + 1e-8)
|
||||
if len(q_set) == 0 and len(a_set) == 0:
|
||||
overlap = 0
|
||||
else:
|
||||
overlap = float(len(word_overlap)) / (len(q_set) + len(a_set))
|
||||
|
||||
# q_set = set([q for q in question if q not in stoplist])
|
||||
# a_set = set([a for a in answer if a not in stoplist])
|
||||
word_overlap = q_set.intersection(a_set)
|
||||
df_overlap = 0.0
|
||||
for w in word_overlap:
|
||||
df_overlap += word2df[w]
|
||||
|
||||
if len(q_set) == 0 and len(a_set) == 0:
|
||||
df_overlap = 0
|
||||
else:
|
||||
df_overlap /= (len(q_set) + len(a_set))
|
||||
|
||||
feats_overlap.append(np.array([overlap, df_overlap]))
|
||||
return np.array(feats_overlap)
|
||||
|
||||
|
||||
def make_input_matrix(self, sentence):
|
||||
terms = sentence.strip().split()
|
||||
# word_embeddings = torch.zeros(max_len, vec_dim).type(torch.DoubleTensor)
|
||||
word_embeddings = torch.zeros(len(terms), self.vec_dim).type(torch.DoubleTensor)
|
||||
for i in range(len(terms)):
|
||||
word = terms[i]
|
||||
if word not in self.vocab_dict:
|
||||
emb = torch.from_numpy(self.unk_term_vec)
|
||||
else:
|
||||
emb = torch.from_numpy(self.W[self.vocab_dict[word]])
|
||||
word_embeddings[i] = emb
|
||||
input_tensor = torch.zeros(1, self.vec_dim, len(terms))
|
||||
input_tensor[0] = torch.transpose(word_embeddings, 0, 1)
|
||||
return input_tensor
|
||||
|
||||
|
||||
def get_tensorized_inputs(self, batch_ques, batch_sents, batch_ext_feats):
|
||||
assert(1 == len(batch_ques))
|
||||
tensorized_inputs = []
|
||||
for i in range(len(batch_ques)):
|
||||
xq = Variable(self.make_input_matrix(batch_ques[i]))
|
||||
xs = Variable(self.make_input_matrix(batch_sents[i]))
|
||||
ext_feats = Variable(torch.FloatTensor(batch_ext_feats[i]))
|
||||
ext_feats = torch.unsqueeze(ext_feats, 0)
|
||||
tensorized_inputs.append((xq, xs, ext_feats))
|
||||
return tensorized_inputs
|
||||
|
||||
|
||||
def rerank_candidate_answers(self, question, answers):
|
||||
# tokenize
|
||||
q_str, a_list = self.parser(question, answers)
|
||||
|
||||
# calculate overlap features
|
||||
overlap_feats = self.compute_overlap_features(q_str, a_list, \
|
||||
stoplist=None, word2df=self.word2dfs)
|
||||
overlap_feats_stoplist = self.compute_overlap_features(q_str, a_list, \
|
||||
stoplist=self.stoplist, word2df=self.word2dfs)
|
||||
overlap_feats_vec = np.hstack([overlap_feats, overlap_feats_stoplist])
|
||||
|
||||
# run through the model
|
||||
scores_sentences = []
|
||||
for i in range(len(a_list)):
|
||||
xq, xa, x_ext_feats = self.get_tensorized_inputs([q_str], [a_list[i]], \
|
||||
[overlap_feats_vec[i]])[0]
|
||||
pred = self.model(xq, xa, x_ext_feats)
|
||||
pred = torch.exp(pred)
|
||||
scores_sentences.append((pred.data.squeeze()[1], a_list[i]))
|
||||
|
||||
return scores_sentences
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
ap = argparse.ArgumentParser(description="Bridge Demo. Produces scores in trec_eval format")
|
||||
ap.add_argument('model')
|
||||
ap.add_argument('--word_embeddings_cache', default='../data/word2vec/aquaint+wiki.txt.gz.ndim=50.cache')
|
||||
ap.add_argument('--stopwords_file', default='../data/TrecQA/stopwords.txt')
|
||||
ap.add_argument('--wordDF_file', default='../data/TrecQA/word2dfs.p')
|
||||
ap.add_argument('--no_ext_feats', action="store_true", help="This argument has no effect because the model saves its members")
|
||||
ap.add_argument('--use_pre_ext_feats', action="store_true", help="use the precomputed external overlap features")
|
||||
ap.add_argument('--data_folder', default='../data/TrecQA/')
|
||||
ap.add_argument('dataset', choices=['train-all', 'raw-test', 'raw-dev', 'train'])
|
||||
ap.add_argument('out_scorefile', help='file in trec_eval format')
|
||||
ap.add_argument('--out_qrels', help='will also output qrels trec_eval format')
|
||||
|
||||
args = ap.parse_args()
|
||||
|
||||
smmodel = SMModelBridge(
|
||||
#'../models/sm_model/sm_model.TrecQA.TRAIN-ALL.2017-04-02.castor',
|
||||
args.model,
|
||||
args.word_embeddings_cache,
|
||||
args.stopwords_file,
|
||||
args.wordDF_file)
|
||||
|
||||
# if args.no_ext_feats:
|
||||
# smmodel.model.no_ext_feats = True
|
||||
|
||||
|
||||
allque = [q.strip() for q in open(os.path.join('../data/TrecQA/', args.dataset+'/a.toks')).readlines()]
|
||||
allans = [a.strip() for a in open(os.path.join('../data/TrecQA/', args.dataset+'/b.toks')).readlines()]
|
||||
labels = [y.strip() for y in open(os.path.join('../data/TrecQA/', args.dataset+'/sim.txt')).readlines()]
|
||||
qids = [id.strip() for id in open(os.path.join('../data/TrecQA/', args.dataset+'/id.txt')).readlines()]
|
||||
|
||||
pre_ext_feats = None
|
||||
if args.use_pre_ext_feats:
|
||||
pre_ext_feats = [ [float(e) for e in x.split() ] for x in open(os.path.join('../data/TrecQA/', args.dataset+'/overlap_feats.txt')).readlines()]
|
||||
|
||||
scoref = open(args.out_scorefile, 'w')
|
||||
if args.out_qrels:
|
||||
qrelf = open(args.out_qrels, 'w')
|
||||
|
||||
for i in range(len(allque)):
|
||||
question = allque[i]
|
||||
answers = [allans[i]]
|
||||
ext_feats = None
|
||||
if args.use_pre_ext_feats:
|
||||
ext_feats = [pre_ext_feats[i]]
|
||||
ss = smmodel.rerank_candidate_answers(question, answers, ext_feats)
|
||||
# print('Question:', question)
|
||||
for score, sentence in ss:
|
||||
#print(score, '\t', sentence)
|
||||
#print('{}\t{}'.format(labels[i], score))
|
||||
print('{} {} {} {} {} {}'.format(qids[i], '0', i, 0, score, 'sm_model.'+args.dataset), file=scoref)
|
||||
if args.out_qrels:
|
||||
print('{} {} {} {}'.format(qids[i], '0', i, labels[i]), file=qrelf)
|
||||
Reference in new issue
Block a user