Files
Castor/sm_cnn/external_features.py
gauravbaruah 9bd5b7bb2a Corpus idf (#23)
as part of sourcing-IDF-from-index and e2e experiments.
2017-05-07 22:28:35 -04:00

216 lines
8.5 KiB
Python

# module to compute various external features for the sm cnn model.
# TODO: add more external features like:
# word mover distance, cosine sim in tf.idf space, cosine sim in word embedding space
# overlap based on parts of speech: noun, verb, adj (POS tag)
# word embedding cosine sim based on part of speech: noun, verb, adj
import sys
import os
import shlex
import subprocess
import string
from collections import defaultdict
import numpy as np
import nltk
nltk.download('stopwords', quiet=True)
from nltk.stem.porter import PorterStemmer
from nltk.corpus import stopwords
def stopped(sentences):
"""
remove stop words from given sentences (questions|answers)
"""
stoplist = set(stopwords.words('english'))
#stoplist.update(set(string.punctuation))
def stop(sentence):
return ' '.join([word for word in sentence.split() if word not in stoplist])
return [stop(sentence) for sentence in sentences]
def stemmed(sentences):
"""
reduce sentence terms to stemmed representations
"""
stemmer = PorterStemmer()
def stem(sentence):
return ' '.join([stemmer.stem(word) for word in sentence.split()])
return [stem(sentence) for sentence in sentences]
def get_qadata_only_idf(all_data):
"""
returns idf weights computed over all question answer pairs in the dataset
"""
if not type(all_data) is list:
all_data = list(all_data)
term_idfs = defaultdict(float)
for doc in all_data:
for term in list(set(doc.split())):
term_idfs[term] += 1.0
N = len(all_data)
for term, n_t in term_idfs.items():
term_idfs[term] = np.log(N/(1+n_t))
return term_idfs
def get_source_corpus_idf(all_data, path_to_index):
"""
fetches idf weights from source corpus (disks1-5+aquaint|wikipedia) index, for all the qa pairs
"""
# first run maven to build ../idf_baseline/FetchTermIDF
maven_cmd = "mvn -f ../idf_baseline/pom.xml clean package appassembler:assemble"
pargs = shlex.split(maven_cmd)
p = subprocess.Popen(pargs, stdout=subprocess.PIPE, stderr=subprocess.PIPE, \
bufsize=1, universal_newlines=True)
pout, perr = p.communicate()
# if build failure, exit with message
if "BUILD FAILURE" in pout or "BUILD FAILURE" in perr:
print("\nERROR: Could not build ../idf_baseline/FetchTermIDF. Fix build errors before proceeding")
print("$ cd ../idf_baseline")
print("$ mvn clean package appassembler:assemble")
sys.exit(0)
if not type(all_data) is list:
all_data = list(all_data)
term_idfs = defaultdict(float)
all_terms = set([term for doc in all_data for term in doc.split()])
with open('dataset.vocab', 'w') as vf:
for term in list(all_terms):
print(term, file=vf)
fetchIDF_cmd = \
"sh ../idf_baseline/target/appassembler/bin/FetchTermIDF -index {} -vocabFile {}".\
format(path_to_index, 'dataset.vocab')
pargs = shlex.split(fetchIDF_cmd)
p = subprocess.Popen(pargs, stdout=subprocess.PIPE, stderr=subprocess.PIPE, \
bufsize=1, universal_newlines=True)
pout, perr = p.communicate()
lines = str(pout).split('\n')
for line in lines:
if not line:
continue
fields = line.strip().split("\t")
term, weight = fields[0], fields[-1]
term_idfs[term] = float(weight)
for line in str(perr).split('\n'):
print('Warning: '+line)
return term_idfs
def compute_overlap(questions, answers):
"""
returns simple overlap between document pairs
"""
overlap_scores = []
for q, a in zip(questions, answers):
q_terms = set(q.split())
a_terms = set(a.split())
common_terms = q_terms.intersection(a_terms)
overlap = float(len(common_terms))/(len(q_terms) + len(a_terms))
overlap_scores.append(overlap)
return np.array(overlap_scores)
def compute_idf_weighted_overlap(questions, answers, idf_weights):
"""
returns idf weighted overlap
"""
overlap_scores = []
for q, a in zip(questions, answers):
q_terms = set(q.split())
a_terms = set(a.split())
common_terms = q_terms.intersection(a_terms)
idf_weighted_overlap = np.sum([idf_weights[term] for term in list(common_terms)])
idf_weighted_overlap /= (len(q_terms) + len(a_terms))
overlap_scores.append(idf_weighted_overlap)
return np.array(overlap_scores)
def set_external_features_as_per_paper(trainer, corpus_index=None):
"""
computes external features as per the paper AND saves them into trainer
"""
all_questions, all_answers = [], []
for split in trainer.data_splits.keys():
questions, answers, labels, max_q_len, max_a_len, default_ext_feats = \
trainer.data_splits[split]
all_questions.extend(questions)
all_answers.extend(answers)
all_data = set(all_questions + all_answers)
print('corpus_index', corpus_index)
idf_weights = get_qadata_only_idf(list(all_data)) if not corpus_index else \
get_source_corpus_idf(list(all_data), corpus_index)
external_features = {}
# NOTE: expected external features as per paper are
# 1. overlap(q, a),
# 2. idf_overlap(q, a),
# 3. overlap(stopped(q), stopped(a)),
# 4. idf_over(stopped(q), stopped(a))
for split in trainer.data_splits.keys():
questions, answers, labels, max_q_len, max_a_len, default_ext_feats = \
trainer.data_splits[split]
overlap = compute_overlap(questions, answers)
idf_weighted_overlap = compute_idf_weighted_overlap(questions, answers, idf_weights)
overlap_no_stopwords =\
compute_overlap(stopped(questions), stopped(answers))
idf_weighted_overlap_no_stopwords =\
compute_idf_weighted_overlap(stopped(questions), stopped(answers), idf_weights)
ext_feats = [np.array(feats) for feats in zip(overlap, idf_weighted_overlap,\
overlap_no_stopwords, idf_weighted_overlap_no_stopwords)]
trainer.data_splits[split][-1] = ext_feats
external_features[split] = ext_feats
return external_features
def set_external_features_as_per_paper_and_stem(trainer, corpus_index=None):
"""
computes external features as per the paper but performs stemming before computing IDF.
features are saved into the trainer.data_splits
"""
all_questions, all_answers = [], []
for split in trainer.data_splits.keys():
questions, answers, labels, max_q_len, max_a_len, default_ext_feats = \
trainer.data_splits[split]
all_questions.extend(questions)
all_answers.extend(answers)
all_data = set(all_questions + all_answers)
# stem all words except stopwords to compute idf (required for feature number 2.)
stoplist = set(stopwords.words('english'))
stemmer = PorterStemmer()
def stem_non_stop_words(sentence):
return ' '.join([stemmer.stem(word) if word not in stoplist else word \
for word in sentence.split()])
all_but_stopwords_stemmed = [stem_non_stop_words(sentence) for sentence in list(all_data)]
idf_weights = get_qadata_only_idf(all_but_stopwords_stemmed) if not corpus_index else \
get_source_corpus_idf(all_but_stopwords_stemmed, corpus_index)
external_features = {}
for split in trainer.data_splits.keys():
questions, answers, labels, max_q_len, max_a_len, default_ext_feats = \
trainer.data_splits[split]
que_stem_all_but_stopwords = [stem_non_stop_words(que) for que in questions]
ans_stem_all_but_stopwords = [stem_non_stop_words(ans) for ans in answers]
overlap = compute_overlap(que_stem_all_but_stopwords, ans_stem_all_but_stopwords)
idf_weighted_overlap = compute_idf_weighted_overlap(que_stem_all_but_stopwords,\
ans_stem_all_but_stopwords, idf_weights)
que_stopped_stemmed = stemmed(stopped(questions))
ans_stopped_stemmed = stemmed(stopped(answers))
overlap_no_stopwords = compute_overlap(que_stopped_stemmed, ans_stopped_stemmed)
idf_weighted_overlap_no_stopwords =\
compute_idf_weighted_overlap(que_stopped_stemmed, ans_stopped_stemmed, idf_weights)
ext_feats = [np.array(feats) for feats in zip(overlap, idf_weighted_overlap,\
overlap_no_stopwords, idf_weighted_overlap_no_stopwords)]
trainer.data_splits[split][-1] = ext_feats
external_features[split] = ext_feats
return external_features