reimplementation of SM model (#48) (#62)

* reimplementation of SM model

* features without normalization; parallel running of different modes

* minor fix
This commit is contained in:
rosequ
2017-10-03 15:02:34 -04:00
committed by Michael Tu
parent 6362239233
commit a471cea2c5
13 changed files with 929 additions and 0 deletions
+3
View File
@@ -5,3 +5,6 @@ __pycache__
.idea/
*idfsim
*.swp
trec_eval.9.0/
*.pt
text/
+5
View File
@@ -0,0 +1,5 @@
*pyc
*.pt
text/
trained_models/
trec_eval-8.0/trec_eval.dSYM
+78
View File
@@ -0,0 +1,78 @@
## SM model
#### References:
1. Aliaksei _S_everyn and Alessandro _M_oschitti. 2015. Learning to Rank Short Text Pairs with Convolutional Deep Neural
Networks. In Proceedings of the 38th International ACM SIGIR Conference on Research and Development in Information
Retrieval (SIGIR '15). ACM, New York, NY, USA, 373-382. DOI: http://dx.doi.org/10.1145/2766462.2767738
### Requirements
```
nltk==3.2.2
numpy==1.11.3
pytorch==0.1.12
```
The code uses torchtext for text processing. Set torchtext:
```bash
git clone https://github.com/pytorch/text.git
cd text
python setup.py install
```
We use `trec_eval` for evaluation:
```bash
cd eval
tar -xvf trec_eval.9.0.tar.gz
make
cd ..
```
Download the word2vec model from [here] (https://drive.google.com/file/d/0B2u_nClt6NbzUmhOZU55eEo4QWM/view?usp=sharing)
and copy it to the `data/` folder.
### Training the model
You can train the SM model for the 4 following configurations:
1. __random__ - the word embedddings are initialized randomly and are tuned during training
2. __static__ - the word embeddings are static (Severyn and Moschitti, SIGIR'15)
3. __non-static__ - the word embeddings are tuned during training
4. __multichannel__ - contains static and non-static channels for question and answer conv layers
To train on GPU 0 with static configuration:
```bash
python train.py --mode static --gpu 0
```
NB: pass `--no_cuda` to use CPU
The trained model will be save to:
```
saves/static_best_model.pt
```
### Testing the model
```
python main.py --trained_model saves/TREC/multichannel_best_model.pt
```
### Evaluation
The performance on TrecQA dataset:
### Best dev
Metric |rand |static|non-static|multichannel
-------|------|------|----------|------------
MAP |0.8096|0.8162|0.8387 | 0.8274
MRR |0.8560|0.8918|0.9058 | 0.8818
### Test
Metric |rand |static|non-static|multichannel
-------|-------|------|----------|------------
MAP |0.7441 |0.7524|0.7688 |0.7641
MRR |0.8172 |0.8012|0.8144 |0.8174
+28
View File
@@ -0,0 +1,28 @@
from argparse import ArgumentParser
def get_args():
parser = ArgumentParser(description="SM CNN")
parser.add_argument('--no_cuda', action='store_false', help='do not use cuda', dest='cuda')
parser.add_argument('--gpu', type=int, default=0) # Use -1 for CPU
parser.add_argument('--epochs', type=int, default=30)
parser.add_argument('--batch_size', type=int, default=64)
parser.add_argument('--mode', type=str, default='static')
parser.add_argument('--lr', type=float, default=1.0)
parser.add_argument('--seed', type=int, default=3435)
parser.add_argument('--dataset', type=str, default='TREC')
parser.add_argument('--resume_snapshot', type=str, default=None)
parser.add_argument('--dev_every', type=int, default=30)
parser.add_argument('--log_every', type=int, default=10)
parser.add_argument('--patience', type=int, default=50)
parser.add_argument('--save_path', type=str, default='saves')
parser.add_argument('--output_channel', type=int, default=100)
parser.add_argument('--filter_width', type=int, default=5)
parser.add_argument('--words_dim', type=int, default=50)
parser.add_argument('--dropout', type=float, default=0.5)
parser.add_argument('--epoch_decay', type=int, default=15)
parser.add_argument('--vector_cache', type=str, default='data/word2vec.trecqa.pt')
parser.add_argument('--trained_model', type=str, default="")
parser.add_argument('--weight_decay',type=float, default=1e-5)
args = parser.parse_args()
return args
+340
View File
@@ -0,0 +1,340 @@
#!/usr/bin/perl -w
# Graded relevance assessment script for the TREC 2010 Web track
# Evalution measures are written to standard output in CSV format.
#
# Currently reports only NDCG and ERR
# (see http://learningtorankchallenge.yahoo.com/instructions.php)
use constant LOGBASEDIV => log(2.0);
# gloals
my $QRELS;
my $VERSION = "version 1.3 (Mon Apr 29 20:50:24 EDT 2013)";
my $MAX_JUDGMENT = 4; # Maximum gain value allowed in qrels file.
my $K = 20; # Reporting depth for results.
my $USAGE = "usage: $0 [options] qrels run\n
options:\n
-c
Average over the complete set of topics in the relevance judgments
instead of the topics in the intersection of relevance judgments
and results.\n
-k value
Non-negative integer depth of ranking to evaluate in range [1,inf].
Default value is k=@{[($K)]}.\n
-baseline BASELINE_RUN_FILE
Baseline run to use for risk-sensitive evaluation\n
-riskAlpha value
Non-negative Risk sensitivity value to use when doing risk-sensitive
evaluation. A baseline must still be specified. By default 0.
The final weight to downside changes in performance is (1+value).\n";
use strict 'vars';
{ # main block to scope variables
if ($#ARGV >= 0 && ($ARGV[0] eq "-v" || $ARGV[0] eq "-version")) {
print "$0: $VERSION\n";
exit 0;
}
my $baselineRun = undef;
my $riskAlpha = 0;
my $cflag = 0;
while ($#ARGV != 1) # should probably replace this with perl's argument parsing
{
if ($#ARGV >= 0 && $ARGV[0] eq "-help") {
print "$USAGE\n";
exit 0;
}
elsif ($#ARGV >= 2 and ("-c" eq $ARGV[0]))
{
$cflag = 1;
shift @ARGV;
}
elsif ($#ARGV >= 3 and ("-k" eq $ARGV[0]))
{
$K = int($ARGV[1]);
die $USAGE if ($K < 1);
# print STDERR "k=$K\n";
shift @ARGV; shift @ARGV;
}
elsif ($#ARGV >= 3 and ("-baseline" eq $ARGV[0]))
{
$baselineRun = $ARGV[1];
shift @ARGV; shift @ARGV;
}
elsif ($#ARGV >= 3 and ("-riskAlpha" eq $ARGV[0]))
{
$riskAlpha = $ARGV[1];
die $USAGE if ($riskAlpha < 0.0);
shift @ARGV; shift @ARGV;
}
else
{
die $USAGE;
}
}
die $USAGE unless $#ARGV == 1;
$QRELS = $ARGV[0];
my $run = $ARGV[1];
# Read qrels file, check format, and sort
my @qrels = ();
my %seen = ();
open (QRELS,"<$QRELS") || die "$0: cannot open \"$QRELS\": $!\n";
while (<QRELS>) {
s/[\r\n]//g;
my ($topic, $zero, $docno, $judgment) = split (' ');
$topic =~ s/^.*\-//;
die "$0: format error on line $. of \"$QRELS\"\n"
unless
$topic =~ /^[0-9]+$/ && $zero == 0
&& $judgment =~ /^-?[0-9]+$/ && $judgment <= $MAX_JUDGMENT;
if ($judgment > 0) {
$qrels[$#qrels + 1]= "$topic $docno $judgment";
$seen{$topic} = 1;
}
}
close (QRELS);
@qrels = sort qrelsOrder (@qrels);
# Process qrels: store judgments and compute ideal gains
my $topicCurrent = -1;
my %ideal = ();
my @gain = ();
my %judgment = ();
for (my $i = 0; $i <= $#qrels; $i++) {
my ($topic, $docno, $judgment) = split (' ', $qrels[$i]);
if ($topic != $topicCurrent) {
if ($topicCurrent >= 0) {
$ideal{$topicCurrent} = &dcg($K, @gain);
$#gain = -1;
}
$topicCurrent = $topic;
}
next if $judgment < 0;
$judgment{"$topic:$docno"} = $gain[$#gain + 1] = $judgment;
}
if ($topicCurrent >= 0) {
$ideal{$topicCurrent} = &dcg($K, @gain);
$#gain = -1;
}
# process baseline if doing risk sensitive
my ($baseNDCGByTopic,$baseERRByTopic,$baserunname);
if (defined $baselineRun)
{
($baseNDCGByTopic,$baseERRByTopic,$baserunname) = processRun($baselineRun,0,\%seen,\%ideal,\%judgment,$cflag,0);
}
# process main run
processRun($run,1,\%seen,\%ideal,\%judgment,$cflag,defined($baselineRun),$riskAlpha,$baserunname,$baseNDCGByTopic,$baseERRByTopic);
exit 0;
} # end main block
# comparison function for qrels: by topic then judgment
sub qrelsOrder {
my ($topicA, $docnoA, $judgmentA) = split (' ', $a);
my ($topicB, $docnoB, $judgmentB) = split (' ', $b);
if ($topicA < $topicB) {
return -1;
} elsif ($topicA > $topicB) {
return 1;
} else {
return $judgmentB <=> $judgmentA;
}
}
# comparison function for runs: by topic then score then docno
sub runOrder {
my ($topicA, $docnoA, $scoreA) = split (' ', $a);
my ($topicB, $docnoB, $scoreB) = split (' ', $b);
if ($topicA < $topicB) {
return -1;
} elsif ($topicA > $topicB) {
return 1;
} elsif ($scoreA < $scoreB) {
return 1;
} elsif ($scoreA > $scoreB) {
return -1;
} elsif ($docnoA lt $docnoB) {
return 1;
} elsif ($docnoA gt $docnoB) {
return -1;
} else {
return 0;
}
}
# compute DCG over a sorted array of gain values, reporting at depth $k
sub dcg {
my ($k, @gain) = @_;
my ($i, $score) = (0, 0);
for ($i = 0; $i <= ($k <= $#gain ? $k - 1 : $#gain); $i++) {
$score += (2**$gain[$i] - 1)/(log ($i + 2)/ +LOGBASEDIV);
}
return $score;
}
# compute ERR over a sorted array of gain values, reporting at depth $k
sub err {
my ($k, @gain) = @_;
my ($i, $score, $decay, $r);
$score = 0.0;
$decay = 1.0;
for ($i = 0; $i <= ($k <= $#gain ? $k - 1 : $#gain); $i++) {
$r = (2**$gain[$i] - 1)/(2**$MAX_JUDGMENT);
$score += $r*$decay/($i + 1);
$decay *= (1 - $r);
}
return $score;
}
sub riskWeighted
{
my ($run,$base,$alpha) = @_;
if ($run < $base)
{
$run = (1+$alpha) * ($run - $base);
}
else
{
$run = $run - $base;
}
return $run;
}
# compute and report information for current topic
sub topicDone {
my ($printTopic, $runid, $topic, $pndcgTotal, $perrTotal, $ptopics, $pseen, $pideal,
$isRiskSensitive, $riskAlpha, $baseNDCG, $baseERR, @gain) = @_;
my($ndcg, $err) = (0, 0);
if (exists($$pseen{$topic}) and defined($$pseen{$topic}) and $$pseen{$topic}) {
$ndcg = &dcg($K, @gain)/$$pideal{$topic};
$err = &err ($K, @gain);
$ndcg = riskWeighted($ndcg,$baseNDCG,$riskAlpha) if ($isRiskSensitive);
$err = riskWeighted($err,$baseERR,$riskAlpha) if ($isRiskSensitive);
$$pndcgTotal += $ndcg;
$$perrTotal += $err;
$$ptopics++;
printf("$runid,$topic,%.5f,%.5f\n",$ndcg,$err) if ($printTopic);
return ($ndcg,$err);
}
}
sub processRun
{
my ($run,$printTopics,$pseen,$pideal,$pjudgment,$avgOverAllTopics,$isRiskSensitive,$riskAlpha,$baserunname,$baseNDCGByTopic,$baseERRByTopic) = @_;
my $ndcgByTopic = {()};
my $errByTopic = {()};
my $runid = "?????";
my @run = ();
# Read run rile, check format, and sort
open (RUN,"<$run") || die "$0: cannot open \"$run\": $!\n";
while (<RUN>) {
s/[\r\n]//g;
my ($topic, $q0, $docno, $rank, $score);
($topic, $q0, $docno, $rank, $score, $runid) = split (' ');
$topic =~ s/^.*\-//;
die "$0: format error on line $. of \"$run\"\n"
unless
$topic =~ /^[0-9]+$/ && $q0 eq "Q0" && $rank =~ /^[0-9]+$/ && $runid;
$run[$#run + 1] = "$topic $docno $score";
}
@run = sort runOrder (@run);
my %processed = ();
foreach my $topic (%$pseen)
{
$processed{$topic} = 0;
}
if ($isRiskSensitive)
{
$runid = sprintf("%s (rel to. %s, rs=1+a, a=%s)",$runid,$baserunname,$riskAlpha);
}
# Process runs: compute measures for each topic and average
my $ndcgTotal = 0;
my $errTotal = 0;
my $topics = 0;
print "runid,topic,ndcg\@$K,err\@$K\n" if ($printTopics);
my $topicCurrent = -1;
my @gain = ();
for (my $i = 0; $i <= $#run; $i++) {
my ($topic, $docno, $score) = split (' ', $run[$i]);
if ($topic != $topicCurrent) {
if ($topicCurrent >= 0) {
my ($baseNDCG,$baseERR) = 0;
if ($isRiskSensitive)
{
$baseNDCG = $$baseNDCGByTopic{$topicCurrent} if (exists($$baseNDCGByTopic{$topicCurrent}) and defined($$baseNDCGByTopic{$topicCurrent}));
$baseERR = $$baseERRByTopic{$topicCurrent} if (exists($$baseERRByTopic{$topicCurrent}) and defined($$baseERRByTopic{$topicCurrent}));
}
my ($ndcg,$err) = &topicDone ($printTopics, $runid, $topicCurrent, \$ndcgTotal, \$errTotal, \$topics,
$pseen, $pideal, $isRiskSensitive, $riskAlpha, $baseNDCG, $baseERR, @gain);
$$ndcgByTopic{$topicCurrent} = $ndcg;
$$errByTopic{$topicCurrent} = $err;
$processed{$topicCurrent} = 1;
$#gain = -1;
}
$topicCurrent = $topic;
}
my $j = $$pjudgment{"$topic:$docno"};
$j = 0 unless $j;
$gain[$#gain + 1] = $j;
}
if ($topicCurrent >= 0) {
my ($baseNDCG,$baseERR) = 0;
if ($isRiskSensitive)
{
$baseNDCG = $$baseNDCGByTopic{$topicCurrent} if (exists($$baseNDCGByTopic{$topicCurrent}) and defined($$baseNDCGByTopic{$topicCurrent}));
$baseERR = $$baseERRByTopic{$topicCurrent} if (exists($$baseERRByTopic{$topicCurrent}) and defined($$baseERRByTopic{$topicCurrent}));
}
my ($ndcg,$err) = &topicDone ($printTopics, $runid, $topicCurrent, \$ndcgTotal, \$errTotal, \$topics,
$pseen, $pideal, $isRiskSensitive, $riskAlpha, $baseNDCG, $baseERR, @gain);
$$ndcgByTopic{$topicCurrent} = $ndcg;
$$errByTopic{$topicCurrent} = $err;
$processed{$topicCurrent} = 1;
$#gain = -1;
}
my $numTopics = $topics; # $topics has the number in the run (at this point)
if ($avgOverAllTopics)
{
$numTopics = scalar(keys %$pseen); # we want denominator to change whenever flag is on but only need to compute differences for risk
if ($isRiskSensitive)
{ # need to process any topics that were missing from run
my ($baseNDCG,$baseERR) = 0;
my @gain = ();
foreach my $topicCurrent (sort {$a <=> $b} keys %processed)
{
next if ($processed{$topicCurrent});
$baseNDCG = $$baseNDCGByTopic{$topicCurrent} if (exists($$baseNDCGByTopic{$topicCurrent}) and defined($$baseNDCGByTopic{$topicCurrent}));
$baseERR = $$baseERRByTopic{$topicCurrent} if (exists($$baseERRByTopic{$topicCurrent}) and defined($$baseERRByTopic{$topicCurrent}));
my ($ndcg,$err) = &topicDone ($printTopics, $runid, $topicCurrent, \$ndcgTotal, \$errTotal, \$topics,
$pseen, $pideal, $isRiskSensitive, $riskAlpha, $baseNDCG, $baseERR, @gain);
}
}
}
my $ndcgAvg = $ndcgTotal;
my $errAvg = $errTotal;
if ($numTopics > 0)
{
$ndcgAvg /= $numTopics;
$errAvg /= $numTopics;
}
printf "$runid,amean,%.5f,%.5f\n",$ndcgAvg,$errAvg if ($printTopics);
return ($ndcgByTopic,$errByTopic,$runid);
close(RUN);
}
Binary file not shown.
+23
View File
@@ -0,0 +1,23 @@
import shlex
import subprocess
def evaluate(instances, valid, config):
sorted_instances = sorted(instances, key=lambda x: (x[0]))
with open('{}.{}.run.txt'.format(valid, config), 'w') as run, open('{}.{}.qrel.txt'.format(valid, config), 'w') as qrel:
i = 0
for instance in sorted_instances:
qid, predicted, score, gold = instance[0], instance[1], instance[2], instance[3]
# 32.1 0 1 0 0.13309887051582336 smmodel
run.write('{} 0 {} 0 {} sm_model\n'.format(qid, i, score))
qrel.write('{} 0 {} {}\n'.format(qid, i, gold))
i += 1
pargs = shlex.split("./eval/trec_eval.9.0/trec_eval -m map -m recip_rank {}.{}.qrel.txt {}.{}.run.txt"
.format(valid, config, valid, config))
p = subprocess.Popen(pargs, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
pout, perr = p.communicate()
lines = pout.split(b'\n')
map = float(lines[0].strip().split()[-1])
mrr = float(lines[1].strip().split()[-1])
return map, mrr
+98
View File
@@ -0,0 +1,98 @@
import numpy as np
import random
import logging
import torch
from torchtext import data
from args import get_args
from trec_dataset import TrecDataset
from evaluate import evaluate
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
args = get_args()
config = args
torch.manual_seed(args.seed)
if not args.cuda:
args.gpu = -1
if torch.cuda.is_available() and args.cuda:
logger.info("Note: You are using GPU for training")
torch.cuda.set_device(args.gpu)
torch.cuda.manual_seed(args.seed)
if torch.cuda.is_available() and not args.cuda:
logger.info("Warning: You have Cuda but do not use it. You are using CPU for training")
np.random.seed(args.seed)
random.seed(args.seed)
QID = data.Field(sequential=False)
QUESTION = data.Field(batch_first=True)
ANSWER = data.Field(batch_first=True)
LABEL = data.Field(sequential=False)
EXTERNAL = data.Field(sequential=False, tensor_type=torch.FloatTensor, batch_first=True, use_vocab=False,
preprocessing=data.Pipeline(lambda x: x.split()),
postprocessing=data.Pipeline(lambda x, train: [float(y) for y in x]))
train, dev, test = TrecDataset.splits(QID, QUESTION, ANSWER, EXTERNAL, LABEL)
QID.build_vocab(train, dev, test)
QUESTION.build_vocab(train, dev, test)
ANSWER.build_vocab(train, dev, test)
LABEL.build_vocab(train, dev, test)
train_iter = data.Iterator(train, batch_size=args.batch_size, device=args.gpu, train=True, repeat=False,
sort=False, shuffle=True)
dev_iter = data.Iterator(dev, batch_size=args.batch_size, device=args.gpu, train=False, repeat=False,
sort=False, shuffle=False)
test_iter = data.Iterator(test, batch_size=args.batch_size, device=args.gpu, train=False, repeat=False,
sort=False, shuffle=False)
config.target_class = len(LABEL.vocab)
config.questions_num = len(QUESTION.vocab)
config.answers_num = len(ANSWER.vocab)
print("Label dict:", LABEL.vocab.itos)
if args.cuda:
model = torch.load(args.trained_model, map_location=lambda storage, location: storage.cuda(args.gpu))
else:
model = torch.load(args.trained_model, map_location=lambda storage,location: storage)
index2label = np.array(LABEL.vocab.itos)
index2qid = np.array(QID.vocab.itos)
def predict(test_mode, dataset_iter):
model.eval()
dataset_iter.init_epoch()
instance = []
for dev_batch_idx, dev_batch in enumerate(dataset_iter):
qid_array = index2qid[np.transpose(dev_batch.qid.cpu().data.numpy())]
true_label_array = index2label[np.transpose(dev_batch.label.cpu().data.numpy())]
scores = model(dev_batch)
index_label = np.transpose(torch.max(scores, 1)[1].view(dev_batch.label.size()).cpu().data.numpy())
label_array = index2label[index_label]
score_array = scores[:, 2].cpu().data.numpy()
# print and write the result
for i in range(dev_batch.batch_size):
this_qid, predicted_label, score, gold_label = qid_array[i], label_array[i], score_array[i], \
true_label_array[i]
instance.append((this_qid, predicted_label, score, gold_label))
dev_map, dev_mrr = evaluate(instance, test_mode, config.mode)
print(dev_map, dev_mrr)
# Run the model on the dev set
predict('dev', dataset_iter=dev_iter)
# Run the model on the test set
predict('test', dataset_iter=test_iter)
+83
View File
@@ -0,0 +1,83 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
class SmPlusPlus(nn.Module):
def __init__(self, config):
super(SmPlusPlus, self).__init__()
output_channel = config.output_channel
questions_num = config.questions_num
answers_num = config.answers_num
words_dim = config.words_dim
filter_width = config.filter_width
self.mode = config.mode
n_classes = config.target_class
ext_feats_size = 4
if self.mode == 'multichannel':
input_channel = 2
else:
input_channel = 1
self.question_embed = nn.Embedding(questions_num, words_dim)
self.answer_embed = nn.Embedding(answers_num, words_dim)
self.static_question_embed = nn.Embedding(questions_num, words_dim)
self.nonstatic_question_embed = nn.Embedding(questions_num, words_dim)
self.static_answer_embed = nn.Embedding(answers_num, words_dim)
self.nonstatic_answer_embed = nn.Embedding(answers_num, words_dim)
self.static_question_embed.weight.requires_grad = False
self.static_answer_embed.weight.requires_grad = False
self.conv_q = nn.Conv2d(input_channel, output_channel, (filter_width, words_dim), padding=(filter_width - 1, 0))
self.conv_a = nn.Conv2d(input_channel, output_channel, (filter_width, words_dim), padding=(filter_width - 1, 0))
self.dropout = nn.Dropout(config.dropout)
n_hidden = 2 * output_channel + ext_feats_size
self.combined_feature_vector = nn.Linear(n_hidden, n_hidden)
self.hidden = nn.Linear(n_hidden, n_classes)
def forward(self, x):
x_question = x.question
x_answer = x.answer
x_ext = x.ext_feat
if self.mode == 'rand':
question = self.question_embed(x_question).unsqueeze(1)
answer = self.answer_embed(x_answer).unsqueeze(1) # (batch, sent_len, embed_dim)
x = [F.tanh(self.conv_q(question)).squeeze(3), F.tanh(self.conv_a(answer)).squeeze(3)]
x = [F.max_pool1d(i, i.size(2)).squeeze(2) for i in x] # max-over-time pooling
# actual SM model mode (Severyn & Moschitti, 2015)
elif self.mode == 'static':
question = self.static_question_embed(x_question).unsqueeze(1)
answer = self.static_answer_embed(x_answer).unsqueeze(1) # (batch, sent_len, embed_dim)
x = [F.tanh(self.conv_q(question)).squeeze(3), F.tanh(self.conv_a(answer)).squeeze(3)]
x = [F.max_pool1d(i, i.size(2)).squeeze(2) for i in x] # max-over-time pooling
elif self.mode == 'non-static':
question = self.nonstatic_question_embed(x_question).unsqueeze(1)
answer = self.nonstatic_answer_embed(x_answer).unsqueeze(1) # (batch, sent_len, embed_dim)
x = [F.tanh(self.conv_q(question)).squeeze(3), F.tanh(self.conv_a(answer)).squeeze(3)]
x = [F.max_pool1d(i, i.size(2)).squeeze(2) for i in x] # max-over-time pooling
elif self.mode == 'multichannel':
question_static = self.static_question_embed(x_question)
answer_static = self.static_answer_embed(x_answer)
question_nonstatic = self.nonstatic_question_embed(x_question)
answer_nonstatic = self.nonstatic_answer_embed(x_answer)
question = torch.stack([question_static, question_nonstatic], dim=1)
answer = torch.stack([answer_static, answer_nonstatic], dim=1)
x = [F.tanh(self.conv_q(question)).squeeze(3), F.tanh(self.conv_a(answer)).squeeze(3)]
x = [F.max_pool1d(i, i.size(2)).squeeze(2) for i in x] # max-over-time pooling
else:
print("Unsupported Mode")
exit()
# append external features and feed to fc
x.append(x_ext)
x = torch.cat(x, 1)
x = F.tanh(self.combined_feature_vector(x))
x = self.dropout(x)
x = self.hidden(x)
return x
+3
View File
@@ -0,0 +1,3 @@
nltk==3.2.1
numpy==1.11.3
pytorch==0.1.12
+214
View File
@@ -0,0 +1,214 @@
import time
import os
import numpy as np
import random
import logging
import torch
import torch.nn as nn
from torchtext import data
from args import get_args
from model import SmPlusPlus
from trec_dataset import TrecDataset
from evaluate import evaluate
args = get_args()
config = args
torch.manual_seed(args.seed)
def set_vectors(field, vector_path):
if os.path.isfile(vector_path):
stoi, vectors, dim = torch.load(vector_path)
field.vocab.vectors = torch.Tensor(len(field.vocab), dim)
for i, token in enumerate(field.vocab.itos):
wv_index = stoi.get(token, None)
if wv_index is not None:
field.vocab.vectors[i] = vectors[wv_index]
else:
# initialize <unk> with U(-0.25, 0.25) vectors
field.vocab.vectors[i] = torch.FloatTensor(dim).uniform_(-0.25, 0.25)
else:
print("Error: Need word embedding pt file")
print("Error: Need word embedding pt file")
exit(1)
return field
def regularize_loss(model, loss):
flattened_params = []
reg = args.weight_decay
for p in model.parameters():
f = p.data.clone()
flattened_params.append(f.view(-1))
fp = torch.cat(flattened_params)
loss = loss + 0.5 * reg * fp.norm() * fp.norm()
return loss
# Set default configuration in : args.py
args = get_args()
config = args
# Set random seed for reproducibility
torch.manual_seed(args.seed)
if not args.cuda:
args.gpu = -1
if torch.cuda.is_available() and args.cuda:
print("Note: You are using GPU for training")
torch.cuda.set_device(args.gpu)
torch.cuda.manual_seed(args.seed)
if torch.cuda.is_available() and not args.cuda:
print("You have Cuda but you're using CPU for training.")
np.random.seed(args.seed)
random.seed(args.seed)
QID = data.Field(sequential=False)
QUESTION = data.Field(batch_first=True)
ANSWER = data.Field(batch_first=True)
LABEL = data.Field(sequential=False)
EXTERNAL = data.Field(sequential=False, tensor_type=torch.FloatTensor, batch_first=True, use_vocab=False,
preprocessing=data.Pipeline(lambda x: x.split()),
postprocessing=data.Pipeline(lambda x, train: [float(y) for y in x]))
train, dev, test = TrecDataset.splits(QID, QUESTION, ANSWER, EXTERNAL, LABEL)
QID.build_vocab(train, dev, test)
QUESTION.build_vocab(train, dev, test)
ANSWER.build_vocab(train, dev, test)
LABEL.build_vocab(train, dev, test)
QUESTION = set_vectors(QUESTION, args.vector_cache)
ANSWER = set_vectors(ANSWER, args.vector_cache)
train_iter = data.Iterator(train, batch_size=args.batch_size, device=args.gpu, train=True, repeat=False,
sort=False, shuffle=True)
dev_iter = data.Iterator(dev, batch_size=args.batch_size, device=args.gpu, train=False, repeat=False,
sort=False, shuffle=False)
test_iter = data.Iterator(test, batch_size=args.batch_size, device=args.gpu, train=False, repeat=False,
sort=False, shuffle=False)
config.target_class = len(LABEL.vocab)
config.questions_num = len(QUESTION.vocab)
config.answers_num = len(ANSWER.vocab)
print("Dataset {} Mode {}".format(args.dataset, args.mode))
print("VOCAB num", len(QUESTION.vocab))
print("LABEL.target_class:", len(LABEL.vocab))
print("LABELS:", LABEL.vocab.itos)
print("Train instance", len(train))
print("Dev instance", len(dev))
print("Test instance", len(test))
if args.resume_snapshot:
if args.cuda:
model = torch.load(args.resume_snapshot, map_location=lambda storage, location: storage.cuda(args.gpu))
else:
model = torch.load(args.resume_snapshot, map_location=lambda storage, location: storage)
else:
model = SmPlusPlus(config)
model.static_question_embed.weight.data.copy_(QUESTION.vocab.vectors)
model.nonstatic_question_embed.weight.data.copy_(QUESTION.vocab.vectors)
model.static_answer_embed.weight.data.copy_(ANSWER.vocab.vectors)
model.nonstatic_answer_embed.weight.data.copy_(ANSWER.vocab.vectors)
if args.cuda:
model.cuda()
print("Shift model to GPU")
parameter = filter(lambda p: p.requires_grad, model.parameters())
# the SM model originally follows SGD but Adadelta is used here
optimizer = torch.optim.Adadelta(parameter, lr=args.lr, weight_decay=args.weight_decay)
criterion = nn.CrossEntropyLoss()
early_stop = False
best_dev_map = 0
iterations = 0
iters_not_improved = 0
epoch = 0
start = time.time()
header = ' Time Epoch Iteration Progress (%Epoch) Loss Dev/Loss Accuracy Dev/Accuracy'
dev_log_template = ' '.join('{:>6.0f},{:>5.0f},{:>9.0f},{:>5.0f}/{:<5.0f} {:>7.0f}%,{:>8.6f},{:8.6f},{:12.4f},{:12.4f}'.split(','))
log_template = ' '.join('{:>6.0f},{:>5.0f},{:>9.0f},{:>5.0f}/{:<5.0f} {:>7.0f}%,{:>8.6f},{},{:12.4f},{}'.split(','))
os.makedirs(args.save_path, exist_ok=True)
os.makedirs(os.path.join(args.save_path, args.dataset), exist_ok=True)
print(header)
index2label = np.array(LABEL.vocab.itos)
index2qid = np.array(QID.vocab.itos)
index2question = np.array(ANSWER.vocab.itos)
while True:
if early_stop:
print("Early Stopping. Epoch: {}, Best Dev Acc: {}".format(epoch, best_dev_map))
break
epoch += 1
train_iter.init_epoch()
n_correct, n_total = 0, 0
for batch_idx, batch in enumerate(train_iter):
iterations += 1
model.train(); optimizer.zero_grad()
scores = model(batch)
n_correct += (torch.max(scores, 1)[1].view(batch.label.size()).data == batch.label.data).sum()
n_total += batch.batch_size
train_acc = 100. * n_correct / n_total
loss = criterion(scores, batch.label)
loss = regularize_loss(model, loss)
loss.backward()
optimizer.step()
# Evaluate performance on validation set
if iterations % args.dev_every == 1:
# switch model into evaluation mode
model.eval()
dev_iter.init_epoch()
n_dev_correct = 0
dev_losses = []
instance = []
for dev_batch_idx, dev_batch in enumerate(dev_iter):
qid_array = index2qid[np.transpose(dev_batch.qid.cpu().data.numpy())]
true_label_array = index2label[np.transpose(dev_batch.label.cpu().data.numpy())]
scores = model(dev_batch)
n_dev_correct += (torch.max(scores, 1)[1].view(dev_batch.label.size()).data == dev_batch.label.data).sum()
dev_loss = criterion(scores, dev_batch.label)
dev_losses.append(dev_loss.data[0])
index_label = np.transpose(torch.max(scores, 1)[1].view(dev_batch.label.size()).cpu().data.numpy())
label_array = index2label[index_label]
# get the relevance scores
score_array = scores[:, 2].cpu().data.numpy()
for i in range(dev_batch.batch_size):
this_qid, predicted_label, score, gold_label = qid_array[i], label_array[i], score_array[i], true_label_array[i]
instance.append((this_qid, predicted_label, score, gold_label))
dev_map, dev_mrr = evaluate(instance, 'valid', config.mode)
print(dev_log_template.format(time.time() - start,
epoch, iterations, 1 + batch_idx, len(train_iter),
100. * (1 + batch_idx) / len(train_iter), loss.data[0],
sum(dev_losses) / len(dev_losses), train_acc, dev_map))
# Update validation results
if dev_map > best_dev_map:
iters_not_improved = 0
best_dev_map = dev_map
snapshot_path = os.path.join(args.save_path, args.dataset, args.mode+'_best_model.pt')
torch.save(model, snapshot_path)
else:
iters_not_improved += 1
if iters_not_improved >= args.patience:
early_stop = True
break
if iterations % args.log_every == 1:
# print progress message
print(log_template.format(time.time() - start,
epoch, iterations, 1 + batch_idx, len(train_iter),
100. * (1 + batch_idx) / len(train_iter), loss.data[0], ' ' * 8,
n_correct / n_total * 100, ' ' * 12))
+16
View File
@@ -0,0 +1,16 @@
from torchtext import data
import os
class TrecDataset(data.TabularDataset):
dirname = 'data'
@classmethod
def splits(cls, question_id, question_field, answer_field, external_field, label_field,
train='train.tsv', validation='dev.tsv', test='test.tsv'):
path = './data'
prefix_name = 'trecqa.'
return super(TrecDataset, cls).splits(
os.path.join(path, prefix_name), train, validation, test,
format='TSV', fields=[('qid', question_id), ('label', label_field), ('question', question_field),
('answer', answer_field), ('ext_feat', external_field)]
)
+38
View File
@@ -0,0 +1,38 @@
from tqdm import tqdm
import array
import torch
import numpy as np
from argparse import ArgumentParser
def convert(fname, vocab):
save_file = '{}.pt'.format(fname)
stoi, vectors, dim = [], array.array('d'), None
# TODO: fix by reading the .dimensions file
vocab_size, dim = 2470719, 50
W = np.memmap(fname, dtype=np.double, shape=(vocab_size, dim))
print("Loading vectors from {}".format(fname))
vectors = []
for line in tqdm(W, total=len(W)):
entry = line
vectors.extend(entry)
vectors = torch.Tensor(vectors).view(-1, dim)
with open(vocab) as f:
stoi = {word.strip():i for i, word in enumerate(f)}
print('saving vectors to', save_file)
torch.save((stoi, vectors, dim), save_file)
if __name__ == '__main__':
parser = ArgumentParser(description='create word embedding')
parser.add_argument('--input', type=str, required=True)
parser.add_argument('--vocab', type=str, required=True)
args = parser.parse_args()
convert(args.input, args.vocab)