import os
from collections import defaultdict
import argparse
import json
import string
import random
import numpy as np
import torch
from torch.optim.lr_scheduler import LambdaLR
from torch.utils.data import DataLoader, Dataset
from transformers import RobertaTokenizer, AutoModel, AutoConfig, AutoModelWithLMHead
from scripts.triviaqa_utils import evaluation_utils
import pytorch_lightning as pl
from pytorch_lightning.logging import TestTubeLogger
from pytorch_lightning.callbacks import ModelCheckpoint
from pytorch_lightning.overrides.data_parallel import LightningDistributedDataParallel
from longformer.longformer import Longformer
from longformer.sliding_chunks import pad_to_window_size
class TriviaQADataset(Dataset):
"""
Largely based on
https://github.com/allenai/allennlp/blob/master/allennlp/data/dataset_readers/reading_comprehension/triviaqa.py
and
https://github.com/huggingface/transformers/blob/master/examples/run_squad.py
"""
def __init__(self, file_path, tokenizer, max_seq_len, max_doc_len, doc_stride,
max_num_answers, ignore_seq_with_no_answers, max_question_len):
assert os.path.isfile(file_path)
self.file_path = file_path
with open(self.file_path, "r", encoding='utf-8') as f:
print(f'reading file: {self.file_path}')
self.data_json = json.load(f)['data']
print(f'done reading file: {self.file_path}')
self.tokenizer = tokenizer
self.max_seq_len = max_seq_len
self.max_doc_len = max_doc_len
self.doc_stride = doc_stride
self.max_num_answers = max_num_answers
self.ignore_seq_with_no_answers = ignore_seq_with_no_answers
self.max_question_len = max_question_len
# A mapping from qid to an int, which can be synched across gpus using `torch.distributed`
if 'train' not in self.file_path: # only for the evaluation set
self.val_qid_string_to_int_map = \
{
self._get_qid(entry["paragraphs"][0]['qas'][0]['id']): index
for index, entry in enumerate(self.data_json)
}
else:
self.val_qid_string_to_int_map = None
def _normalize_text(self, text: str) -> str: # copied from the official triviaqa repo
return " ".join(
[
token
for token in text.lower().strip(self.STRIPPED_CHARACTERS).split()
if token not in self.IGNORED_TOKENS
]
)
IGNORED_TOKENS = {"a", "an", "the"}
STRIPPED_CHARACTERS = string.punctuation + "".join([u"‘", u"’", u"´", u"`", "_"])
def __len__(self):
return len(self.data_json)
def __getitem__(self, idx):
entry = self.data_json[idx]
tensors_list = self.one_example_to_tensors(entry, idx)
assert len(tensors_list) == 1
return tensors_list[0]
def one_example_to_tensors(self, example, idx):
def is_whitespace(c):
if c == " " or c == "\t" or c == "\r" or c == "\n" or ord(c) == 0x202F:
return True
return False
tensors_list = []
for paragraph in example["paragraphs"]:
paragraph_text = paragraph["context"]
doc_tokens = []
char_to_word_offset = []
prev_is_whitespace = True
for c in paragraph_text:
if is_whitespace(c):
prev_is_whitespace = True
else:
if prev_is_whitespace:
doc_tokens.append(c)
else:
doc_tokens[-1] += c
prev_is_whitespace = False
char_to_word_offset.append(len(doc_tokens) - 1)
for qa in paragraph["qas"]:
question_text = qa["question"]
start_position = None
end_position = None
orig_answer_text = None
answer_spans = []
for answer in qa["answers"]:
orig_answer_text = answer["text"]
answer_offset = answer["answer_start"]
answer_length = len(orig_answer_text)
try:
start_position = char_to_word_offset[answer_offset]
end_position = char_to_word_offset[answer_offset + answer_length - 1]
token_ids = self.tokenizer.encode(orig_answer_text)
except RuntimeError:
print(f'Reading example {idx} failed')
start_position = 0
end_position = 0
answer_spans.append({'start': start_position, 'end': end_position,
'text': orig_answer_text, 'token_ids': token_ids})
# ===== Given an example, convert it into tensors =============
query_tokens = self.tokenizer.tokenize(question_text)
query_tokens = query_tokens[:self.max_question_len]
tok_to_orig_index = []
orig_to_tok_index = []
all_doc_tokens = []
for (i, token) in enumerate(doc_tokens):
orig_to_tok_index.append(len(all_doc_tokens))
# hack: the line below should have been `self.tokenizer.tokenize(token')`
# but roberta tokenizer uses a different subword if the token is the beginning of the string
# or in the middle. So for all tokens other than the first, simulate that it is not the first
# token by prepending a period before tokenizing, then dropping the period afterwards
sub_tokens = self.tokenizer.tokenize(f'. {token}')[1:] if i > 0 else self.tokenizer.tokenize(token)
for sub_token in sub_tokens:
tok_to_orig_index.append(i)
all_doc_tokens.append(sub_token)
all_doc_tokens = all_doc_tokens[:self.max_doc_len]
# The -3 accounts for [CLS], [SEP] and [SEP]
max_tokens_per_doc_slice = self.max_seq_len - len(query_tokens) - 3
assert max_tokens_per_doc_slice > 0
if self.doc_stride < 0:
# negative doc_stride indicates no sliding window, but using first slice
self.doc_stride = -100 * len(all_doc_tokens) # large -ve value for the next loop to execute once
input_ids_list = []
input_mask_list = []
segment_ids_list = []
start_positions_list = []
end_positions_list = []
answer_token_ids_list = []
for slice_start in range(0, len(all_doc_tokens), max_tokens_per_doc_slice - self.doc_stride):
slice_end = min(slice_start + max_tokens_per_doc_slice, len(all_doc_tokens))
doc_slice_tokens = all_doc_tokens[slice_start:slice_end]
tokens = [self.tokenizer.cls_token] + query_tokens + [self.tokenizer.sep_token] \
+ doc_slice_tokens + [self.tokenizer.sep_token]
segment_ids = [0] * (len(query_tokens) + 2) + [1] * (len(doc_slice_tokens) + 1)
assert len(segment_ids) == len(tokens)
input_ids = self.tokenizer.convert_tokens_to_ids(tokens)
input_mask = [1] * len(input_ids)
if self.doc_stride >= 0: # no need to pad if document is not strided
# Zero-pad up to the sequence length.
padding_len = self.max_seq_len - len(input_ids)
input_ids.extend([self.tokenizer.pad_token_id] * padding_len)
input_mask.extend([0] * padding_len)
segment_ids.extend([0] * padding_len)
assert len(input_ids) == self.max_seq_len
assert len(input_mask) == self.max_seq_len
assert len(segment_ids) == self.max_seq_len
doc_offset = len(query_tokens) + 2 - slice_start
start_positions = []
end_positions = []
answer_token_ids = []
for answer_span in answer_spans:
start_position = answer_span['start']
end_position = answer_span['end']
tok_start_position_in_doc = orig_to_tok_index[start_position]
not_end_of_doc = int(end_position + 1 < len(orig_to_tok_index))
tok_end_position_in_doc = orig_to_tok_index[end_position + not_end_of_doc] - not_end_of_doc
if tok_start_position_in_doc < slice_start or tok_end_position_in_doc > slice_end:
# this answer is outside the current slice
continue
start_positions.append(tok_start_position_in_doc + doc_offset)
end_positions.append(tok_end_position_in_doc + doc_offset)
answer_token_ids.append(answer_span['token_ids'])
assert len(start_positions) == len(end_positions)
if self.ignore_seq_with_no_answers and len(start_positions) == 0:
continue
# answers from start_positions and end_positions if > self.max_num_answers
start_positions = start_positions[:self.max_num_answers]
end_positions = end_positions[:self.max_num_answers]
answer_token_ids = answer_token_ids[:self.max_num_answers]
# -1 padding up to self.max_num_answers
padding_len = self.max_num_answers - len(start_positions)
start_positions.extend([-1] * padding_len)
end_positions.extend([-1] * padding_len)
answer_token_ids.extend([[]] * padding_len)
# replace duplicate start/end positions with `-1` because duplicates can result into -ve loss values
found_start_positions = set()
found_end_positions = set()
found_answer_token_ids = set()
for i, (start_position, end_position, answer_tokens) in enumerate(
zip(start_positions, end_positions, answer_token_ids)
):
if start_position in found_start_positions:
start_positions[i] = -1
if end_position in found_end_positions:
end_positions[i] = -1
answer_tokens_as_str = ','.join([str(x) for x in answer_tokens])
if answer_tokens_as_str in found_answer_token_ids:
answer_token_ids[i] = []
found_start_positions.add(start_position)
found_end_positions.add(end_position)
found_answer_token_ids.add(answer_tokens_as_str)
input_ids_list.append(input_ids)
input_mask_list.append(input_mask)
segment_ids_list.append(segment_ids)
start_positions_list.append(start_positions)
end_positions_list.append(end_positions)
answer_token_ids_list.append(answer_token_ids)
# pad answers in answer_token_ids_list to the longest answer
max_answer_len = max([len(item) for sublist in answer_token_ids_list for item in sublist]) # flat list
if max_answer_len == 0:
max_answer_len = 2
for answers_of_one_slice in answer_token_ids_list:
for answer_tokens in answers_of_one_slice:
if len(answer_tokens) == 0:
# TODO: or ?
padding_len = max_answer_len - len(answer_tokens) - 2
answer_tokens.extend([self.tokenizer.bos_token_id, self.tokenizer.eos_token_id] +
([self.tokenizer.pad_token_id] * padding_len))
else:
padding_len = max_answer_len - len(answer_tokens)
answer_tokens.extend([self.tokenizer.pad_token_id] * padding_len)
tensors_list.append((torch.tensor(input_ids_list), torch.tensor(input_mask_list),
torch.tensor(segment_ids_list),
torch.tensor(start_positions_list), torch.tensor(end_positions_list),
torch.tensor(answer_token_ids_list),
self._get_qid(qa['id']), qa["aliases"])) # for eval
return tensors_list
def _get_qid(self, qid):
"""all input qids are formatted uniqueID__evidenceFile, but for wikipedia, qid = uniqueID,
and for web, qid = uniqueID__evidenceFile. This function takes care of this conversion.
"""
if 'wikipedia' in self.file_path:
# for evaluation on wikipedia, every question has one answer even if multiple evidence documents are given
return qid.split('--')[0]
elif 'web' in self.file_path:
# for evaluation on web, every question/document pair have an answer
return qid
elif 'sample' in self.file_path:
return qid
else:
raise RuntimeError('Unexpected filename')
@staticmethod
def collate_one_doc_and_lists(batch):
num_metadata_fields = 2 # qids and aliases
fields = [x for x in zip(*batch)]
stacked_fields = [torch.stack(field) for field in fields[:-num_metadata_fields]] # don't stack metadata fields
stacked_fields.extend(fields[-num_metadata_fields:]) # add them as lists not torch tensors
# always use batch_size=1 where each batch is one document
# will use grad_accum to increase effective batch size
assert len(batch) == 1
fields_with_batch_size_one = [f[0] for f in stacked_fields]
return fields_with_batch_size_one
class TriviaQA(pl.LightningModule):
def __init__(self, args):
super(TriviaQA, self).__init__()
self.args = args
self.hparams = args
self.tokenizer = RobertaTokenizer.from_pretrained('roberta-base')
self.tokenizer.model_max_length = self.args.max_seq_len
self.model = self.load_model()
self.num_labels = 2
if not self.args.seq2seq:
self.qa_outputs = torch.nn.Linear(self.model.config.hidden_size, self.num_labels)
self.train_dataloader_object = self.val_dataloader_object = self.test_dataloader_object = None
def load_model(self):
if 'longformer' in self.args.model_path:
model = Longformer.from_pretrained(self.args.model_path)
for layer in model.encoder.layer:
layer.attention.self.attention_mode = self.args.attention_mode
self.args.attention_window = layer.attention.self.attention_window
elif self.args.model_path in ['bart.large', 'bart.base']:
model = torch.hub.load('pytorch/fairseq', self.args.model_path)
model.config = model.args
model.config.hidden_size = model.config.decoder_output_dim
elif 'bart' in self.args.model_path and 'base' in self.args.model_path:
config = AutoConfig.from_pretrained(self.args.model_path)
config.encoder_attention_heads = 12
config.decoder_attention_heads = 12
config.attention_dropout = 0.1
if self.args.seq2seq:
model = AutoModelWithLMHead.from_pretrained(self.args.model_path, config=config)
else:
model = AutoModel.from_pretrained(self.args.model_path, config=config)
elif 'bart' in self.args.model_path and 'large' in self.args.model_path:
config = AutoConfig.from_pretrained(self.args.model_path)
config.attention_dropout = 0.1
config.gradient_checkpointing = True
if self.args.seq2seq:
model = AutoModelWithLMHead.from_pretrained(self.args.model_path, config=config)
else:
model = AutoModel.from_pretrained(self.args.model_path, config=config)
else:
model = AutoModel.from_pretrained(self.args.model_path)
print("Loaded model with config:")
print(model.config)
for p in model.parameters():
p.requires_grad_(True)
model.train()
return model
def forward(self, input_ids, attention_mask, segment_ids, start_positions, end_positions, answer_token_ids):
if 'longformer' in self.args.model_path:
question_end_index = self._get_question_end_index(input_ids)
# Each batch is one document, and each row of the batch is a chunck of the document.
# Make sure all rows have the same question length.
assert (question_end_index[0].float() == question_end_index.float().mean()).item()
# local attention everywhere
attention_mask = torch.ones(input_ids.shape, dtype=torch.long, device=input_ids.device)
# global attention for the question tokens
attention_mask[:, :question_end_index.item()] = 2
# sliding_chunks implemenation of selfattention requires that seqlen is multiple of window size
input_ids, attention_mask = pad_to_window_size(
input_ids, attention_mask, self.args.attention_window, self.tokenizer.pad_token_id)
sequence_output = self.model(
input_ids,
attention_mask=attention_mask)[0]
# The pretrained TriviaQA model wasn't trained with padding, so remove padding tokens
# before computing loss and decoding.
padding_len = input_ids[0].eq(self.tokenizer.pad_token_id).sum()
if padding_len > 0:
sequence_output = sequence_output[:, :-padding_len]
elif self.args.model_path in ['bart.large', 'bart.base']:
sequence_output = self.model.extract_features(input_ids)
else:
if self.args.seq2seq:
decoder_input_ids = answer_token_ids[:, 0, :-1].clone()
decoder_input_ids[decoder_input_ids == self.tokenizer.eos_token_id] = self.tokenizer.pad_token_id
decoder_attention_mask = (decoder_input_ids != self.tokenizer.pad_token_id)
labels = answer_token_ids[:, 0, 1:].contiguous()
labels[answer_token_ids[:, 0, 1:] == self.tokenizer.pad_token_id] = -100
outputs = self.model(
input_ids,
attention_mask=attention_mask,
decoder_input_ids=decoder_input_ids,
decoder_attention_mask=decoder_attention_mask,
labels=labels)
loss = outputs[0]
logit_scores = outputs[1].softmax(dim=2)[:, :, 0].sum(dim=1)
return [loss, logit_scores]
else:
sequence_output = self.model(input_ids, attention_mask=attention_mask)[0]
logits = self.qa_outputs(sequence_output)
start_logits, end_logits = logits.split(1, dim=-1)
start_logits = start_logits.squeeze(-1)
end_logits = end_logits.squeeze(-1)
outputs = (start_logits, end_logits,)
if start_positions is not None and end_positions is not None:
# If we are on multi-GPU, split add a dimension
if len(start_positions.size()) > 1:
start_positions = start_positions.squeeze(-1)
if len(end_positions.size()) > 1:
end_positions = end_positions.squeeze(-1)
if not self.args.regular_softmax_loss:
# loss function suggested in section 2.2 here https://arxiv.org/pdf/1710.10723.pdf
# NOTE: this returns sum of losses, not mean, so loss won't be normalized across different batch sizes
# but batch size is always 1, so this is not a problem
start_loss = self.or_softmax_cross_entropy_loss_one_doc(start_logits, start_positions, ignore_index=-1)
end_loss = self.or_softmax_cross_entropy_loss_one_doc(end_logits, end_positions, ignore_index=-1)
else:
loss_fct = torch.nn.CrossEntropyLoss(ignore_index=-1)
start_positions = start_positions[:, 0:1]
end_positions = end_positions[:, 0:1]
start_loss = loss_fct(start_logits, start_positions[:, 0])
end_loss = loss_fct(end_logits, end_positions[:, 0])
total_loss = (start_loss + end_loss) / 2
outputs = (total_loss,) + outputs
return outputs # (loss), start_logits, end_logits, (hidden_states), (attentions)
def or_softmax_cross_entropy_loss_one_doc(self, logits, target, ignore_index=-1, dim=-1):
"""loss function suggested in section 2.2 here https://arxiv.org/pdf/1710.10723.pdf"""
assert logits.ndim == 2
assert target.ndim == 2
assert logits.size(0) == target.size(0)
# with regular CrossEntropyLoss, the numerator is only one of the logits specified by the target
# here, the numerator is the sum of a few potential targets, where some of them is the correct answer
# compute a target mask
target_mask = target == ignore_index
# replaces ignore_index with 0, so `gather` will select logit at index 0 for the msked targets
masked_target = target * (1 - target_mask.long())
# gather logits
gathered_logits = logits.gather(dim=dim, index=masked_target)
# Apply the mask to gathered_logits. Use a mask of -inf because exp(-inf) = 0
gathered_logits[target_mask] = float('-inf')
# each batch is one example
gathered_logits = gathered_logits.view(1, -1)
logits = logits.view(1, -1)
# numerator = log(sum(exp(gathered logits)))
log_score = torch.logsumexp(gathered_logits, dim=dim, keepdim=False)
# denominator = log(sum(exp(logits)))
log_norm = torch.logsumexp(logits, dim=dim, keepdim=False)
# compute the loss
loss = -(log_score - log_norm)
# some of the examples might have a loss of `inf` when `target` is all `ignore_index`.
# remove those from the loss before computing the sum. Use sum instead of mean because
# it is easier to compute
return loss[~torch.isinf(loss)].sum()
def training_step(self, batch, batch_nb):
input_ids, input_mask, segment_ids, subword_starts, subword_ends, answer_token_ids, qids, aliases = batch
output = self.forward(input_ids, input_mask, segment_ids, subword_starts, subword_ends, answer_token_ids)
loss = output[0]
lr = loss.new_zeros(1) + self.trainer.optimizers[0].param_groups[0]['lr']
tensorboard_logs = {'train_loss': loss, 'lr': lr,
'input_size': input_ids.numel(),
'mem': torch.cuda.memory_allocated(input_ids.device) / 1024 ** 3}
return {'loss': loss, 'log': tensorboard_logs}
def validation_step(self, batch, batch_nb):
input_ids, input_mask, segment_ids, subword_starts, subword_ends, answer_token_ids, qids, aliases = batch
output = self.forward(input_ids, input_mask, segment_ids, subword_starts, subword_ends, answer_token_ids)
if self.args.seq2seq:
logit_scores = output[1]
answer_score_indices = logit_scores.sort().indices
generated_ids = self.model.generate(input_ids=input_ids, attention_mask=input_mask, use_cache=True,)
answer_text = ''
best_answer_score = 0
for i in answer_score_indices:
generated_answer_ids = generated_ids[answer_score_indices[i]]
generated_answer_ids[-1] = self.tokenizer.eos_token_id
index_of_eos_token = (generated_answer_ids == self.tokenizer.eos_token_id).nonzero()[0, 0].item()
generated_answer_ids = generated_answer_ids[1:index_of_eos_token]
answer_text = self.tokenizer.decode(generated_answer_ids)
if answer_text != '':
best_answer_score = logit_scores[answer_score_indices[i]]
break
f1_score = evaluation_utils.metric_max_over_ground_truths(evaluation_utils.f1_score, answer_text, aliases)
em_score = evaluation_utils.metric_max_over_ground_truths(evaluation_utils.exact_match_score, answer_text, aliases)
return {'vloss': output[0], 'vem': generated_answer_ids.new_zeros([1]).float(),
'qids': [qids], 'answer_scores': [best_answer_score],
'f1': [f1_score], 'em': [em_score]}
loss, start_logits, end_logits = output[:3]
answers = self.decode(input_ids, start_logits, end_logits)
# each batch is one document
answers = sorted(answers, key=lambda x: x['score'], reverse=True)[0:1]
qids = [qids]
aliases = [aliases]
f1_scores = [evaluation_utils.metric_max_over_ground_truths(evaluation_utils.f1_score, answer['text'],
aliase_list)
for answer, aliase_list in zip(answers, aliases)]
# TODO: if slow, skip em_scores, and use (f1_score == 1.0) instead
em_scores = [evaluation_utils.metric_max_over_ground_truths(evaluation_utils.exact_match_score, answer['text'],
aliase_list)
for answer, aliase_list in zip(answers, aliases)]
answer_scores = [answer['score'] for answer in answers] # start_logit + end_logit
assert len(answer_scores) == len(f1_scores) == len(em_scores) == len(qids) == len(aliases) == 1
# TODO: delete this metric
pred_subword_starts = start_logits.argmax(dim=1)
pred_subword_ends = end_logits.argmax(dim=1)
exact_match = (subword_ends[:, 0].squeeze(dim=-1) == pred_subword_ends).float() * \
(subword_starts[:, 0].squeeze(dim=-1) == pred_subword_starts).float()
return {'vloss': loss, 'vem': exact_match.mean(),
'qids': qids, 'answer_scores': answer_scores,
'f1': f1_scores, 'em': em_scores}
def _get_question_end_index(self, input_ids):
eos_token_indices = (input_ids == self.tokenizer.eos_token_id).nonzero()
assert eos_token_indices.ndim == 2
assert eos_token_indices.size(0) == 2 * input_ids.size(0)
assert eos_token_indices.size(1) == 2
return eos_token_indices.view(input_ids.size(0), 2, 2)[:, 0, 1]
def decode(self, input_ids, start_logits, end_logits):
# find beginning of document
question_end_index = self._get_question_end_index(input_ids)
# bsz x seqlen => bsz x n_best_size
start_logits_indices = start_logits.topk(k=self.args.n_best_size, dim=-1).indices
end_logits_indices = end_logits.topk(k=self.args.n_best_size, dim=-1).indices
answers = []
# This loop can't be vectorized, so loop over each example in the batch separetly
for i in range(start_logits_indices.size(0)): # bsz
potential_answers = []
for start_logit_index in start_logits_indices[i]: # n_best_size
for end_logit_index in end_logits_indices[i]: # n_best_size
if start_logit_index <= question_end_index[i]:
continue
if end_logit_index <= question_end_index[i]:
continue
if start_logit_index > end_logit_index:
continue
answer_len = end_logit_index - start_logit_index + 1
if answer_len > self.args.max_answer_length:
continue
potential_answers.append({'start': start_logit_index, 'end': end_logit_index,
'start_logit': start_logits[i][start_logit_index].item(),
'end_logit': end_logits[i][end_logit_index].item()})
sorted_answers = sorted(potential_answers, key=lambda x: (x['start_logit'] + x['end_logit']), reverse=True)
if len(sorted_answers) == 0:
answers.append({'text': 'NoAnswerFound', 'score': -1000000})
else:
answer = sorted_answers[0]
answer_token_ids = input_ids[i, answer['start']: answer['end'] + 1]
answer_tokens = self.tokenizer.convert_ids_to_tokens(answer_token_ids.tolist())
text = self.tokenizer.convert_tokens_to_string(answer_tokens)
score = answer['start_logit'] + answer['end_logit']
answers.append({'text': text, 'score': score})
return answers
def sync_list_across_gpus(self, list_to_sync, device, dtype):
l_tensor = torch.tensor(list_to_sync, device=device, dtype=dtype)
gather_l_tensor = [torch.ones_like(l_tensor) for _ in range(self.trainer.world_size)]
torch.distributed.all_gather(gather_l_tensor, l_tensor)
return torch.cat(gather_l_tensor).tolist()
def validation_end(self, outputs):
avg_loss = torch.stack([x['vloss'] for x in outputs]).mean()
avg_em = torch.stack([x['vem'] for x in outputs]).mean()
string_qids = [item for sublist in outputs for item in sublist['qids']]
int_qids = [self.val_dataloader_object.dataset.val_qid_string_to_int_map[qid] for qid in string_qids]
answer_scores = [item for sublist in outputs for item in sublist['answer_scores']]
f1_scores = [item for sublist in outputs for item in sublist['f1']]
em_scores = [item for sublist in outputs for item in sublist['em']]
print(f'before sync --> sizes: {len(int_qids)}, {len(answer_scores)}, {len(f1_scores)}, {len(em_scores)}')
if self.trainer.use_ddp:
torch.distributed.all_reduce(avg_loss, op=torch.distributed.ReduceOp.SUM)
avg_loss /= self.trainer.world_size
torch.distributed.all_reduce(avg_em, op=torch.distributed.ReduceOp.SUM)
avg_em /= self.trainer.world_size
int_qids = self.sync_list_across_gpus(int_qids, avg_loss.device, torch.int)
answer_scores = self.sync_list_across_gpus(answer_scores, avg_loss.device, torch.float)
f1_scores = self.sync_list_across_gpus(f1_scores, avg_loss.device, torch.float)
em_scores = self.sync_list_across_gpus(em_scores, avg_loss.device, torch.int)
print(f'after sync --> sizes: {len(int_qids)}, {len(answer_scores)}, {len(f1_scores)}, {len(em_scores)}')
# Because of having multiple documents per questions, some questions might have multiple corresponding answers
# Here, we only keep the answer with the highest answer_score
qa_with_duplicates = defaultdict(list)
for qid, answer_score, f1_score, em_score in zip(int_qids, answer_scores, f1_scores, em_scores):
qa_with_duplicates[qid].append({'answer_score': answer_score, 'f1': f1_score, 'em': em_score})
f1_scores = []
em_scores = []
for qid, answer_metrics in qa_with_duplicates.items():
top_answer = sorted(answer_metrics, key=lambda x: x['answer_score'], reverse=True)[0]
f1_scores.append(top_answer['f1'])
em_scores.append(top_answer['em'])
avg_val_f1 = sum(f1_scores) / len(f1_scores)
avg_val_em = sum(em_scores) / len(em_scores)
logs = {'val_loss': avg_loss, 'val_em': avg_em, 'avg_val_f1': avg_val_f1, 'avg_val_em': avg_val_em}
return {'avg_val_loss': avg_loss, 'log': logs, 'progress_bar': logs}
def test_step(self, batch, batch_nb):
input_ids, input_mask, segment_ids, subword_starts, subword_ends, answer_token_ids, qids, aliases = batch
output = self.forward(input_ids, input_mask, segment_ids, subword_starts, subword_ends, answer_token_ids)
if self.args.seq2seq:
raise NotImplemented
loss, start_logits, end_logits = output[:3]
answers = self.decode(input_ids, start_logits, end_logits)
# each batch is one document
answers = sorted(answers, key=lambda x: x['score'], reverse=True)[0:1]
qids = [qids]
assert len(answers) == len(qids)
return {'qids': qids, 'answers': answers}
def test_end(self, outputs):
qids = [item for sublist in outputs for item in sublist['qids']]
answers = [item for sublist in outputs for item in sublist['answers']]
qa_with_duplicates = defaultdict(list)
for qid, answer in zip(qids, answers):
qa_with_duplicates[qid].append({'answer_score': answer['score'], 'answer_text': answer['text'], })
qid_to_answer_text = {}
for qid, answer_metrics in qa_with_duplicates.items():
top_answer = sorted(answer_metrics, key=lambda x: x['answer_score'], reverse=True)[0]
qid_to_answer_text[qid] = top_answer['answer_text']
with open('predictions.json', 'w') as f:
json.dump(qid_to_answer_text, f)
return {'count': len(qid_to_answer_text)}
def configure_optimizers(self):
def lr_lambda(current_step):
if current_step < self.args.warmup:
return float(current_step) / float(max(1, self.args.warmup))
return max(0.0, float(self.args.steps - current_step) / float(max(1, self.args.steps - self.args.warmup)))
optimizer = torch.optim.Adam(self.parameters(), lr=self.args.lr)
scheduler = LambdaLR(optimizer, lr_lambda, last_epoch=-1)
return [optimizer], [{"scheduler": scheduler, "interval": "step"}]
@pl.data_loader
def train_dataloader(self):
if self.train_dataloader_object is not None:
return self.train_dataloader_object
dataset = TriviaQADataset(file_path=self.args.train_dataset, tokenizer=self.tokenizer,
max_seq_len=self.args.max_seq_len, max_doc_len=self.args.max_doc_len,
doc_stride=self.args.doc_stride,
max_num_answers=self.args.max_num_answers,
max_question_len=self.args.max_question_len,
ignore_seq_with_no_answers=self.args.ignore_seq_with_no_answers)
sampler = torch.utils.data.distributed.DistributedSampler(dataset, shuffle=True) if self.trainer.use_ddp else None
dl = DataLoader(dataset, batch_size=1, shuffle=(sampler is None),
num_workers=self.args.num_workers, sampler=sampler,
collate_fn=TriviaQADataset.collate_one_doc_and_lists)
self.train_dataloader_object = dl
return self.train_dataloader_object
@pl.data_loader
def val_dataloader(self):
if self.val_dataloader_object is not None:
return self.val_dataloader_object
dataset = TriviaQADataset(file_path=self.args.dev_dataset, tokenizer=self.tokenizer,
max_seq_len=self.args.max_seq_len, max_doc_len=self.args.max_doc_len,
doc_stride=self.args.doc_stride,
max_num_answers=self.args.max_num_answers,
max_question_len=self.args.max_question_len,
ignore_seq_with_no_answers=False) # evaluation data should keep all examples
sampler = torch.utils.data.distributed.DistributedSampler(dataset, shuffle=False) if self.trainer.use_ddp else None
dl = DataLoader(dataset, batch_size=1, shuffle=False,
num_workers=self.args.num_workers, sampler=sampler,
collate_fn=TriviaQADataset.collate_one_doc_and_lists)
self.val_dataloader_object = dl
return self.val_dataloader_object
@pl.data_loader
def test_dataloader(self):
if self.test_dataloader_object is not None:
return self.test_dataloader_object
dataset = TriviaQADataset(file_path=self.args.dev_dataset, tokenizer=self.tokenizer,
max_seq_len=self.args.max_seq_len, max_doc_len=self.args.max_doc_len,
doc_stride=self.args.doc_stride,
max_num_answers=self.args.max_num_answers,
max_question_len=self.args.max_question_len,
ignore_seq_with_no_answers=False) # evaluation data should keep all examples
dl = DataLoader(dataset, batch_size=1, shuffle=False,
num_workers=self.args.num_workers, sampler=None,
collate_fn=TriviaQADataset.collate_one_doc_and_lists)
self.test_dataloader_object = dl
return self.test_dataloader_object
def configure_ddp(self, model, device_ids):
model = LightningDistributedDataParallel(
model,
device_ids=device_ids,
find_unused_parameters=False
)
return model
@staticmethod
def add_model_specific_args(parser, root_dir):
parser.add_argument("--save_dir", type=str, default='triviaqa')
parser.add_argument("--save_prefix", type=str, required=True)
parser.add_argument("--train_dataset", type=str, required=False, help="Path to the training squad-format")
parser.add_argument("--dev_dataset", type=str, required=True, help="Path to the dev squad-format")
parser.add_argument("--batch_size", type=int, default=8, help="Batch size")
parser.add_argument("--gpus", type=int, default=1,
help="Number of gpus. 0 for CPU")
parser.add_argument("--warmup", type=int, default=200, help="Number of warmup steps")
parser.add_argument("--lr", type=float, default=0.0001, help="Maximum learning rate")
parser.add_argument("--val_every", type=float, default=0.5, help="Number of training steps between validations")
parser.add_argument("--val_percent_check", default=1.00, type=float, help='Percent of validation data used')
parser.add_argument("--num_workers", type=int, default=4, help="Number of data loader workers")
parser.add_argument("--seed", type=int, default=1234, help="Seed")
parser.add_argument("--epochs", type=int, default=30, help="Number of epochs")
parser.add_argument("--max_seq_len", type=int, default=4096,
help="Maximum length of seq passed to the transformer model")
parser.add_argument("--max_doc_len", type=int, default=4096,
help="Maximum number of wordpieces of the input document")
parser.add_argument("--max_num_answers", type=int, default=64,
help="Maximum number of answer spans per document (64 => 94%)")
parser.add_argument("--max_question_len", type=int, default=55,
help="Maximum length of the question")
parser.add_argument("--doc_stride", type=int, default=-1,
help="Overlap between document chunks. Use -1 to only use the first chunk")
parser.add_argument("--ignore_seq_with_no_answers", action='store_true',
help="each example should have at least one answer. Default is False")
parser.add_argument("--disable_checkpointing", action='store_true', help="No logging or checkpointing")
parser.add_argument("--n_best_size", type=int, default=20,
help="Number of answer candidates. Used at decoding time")
parser.add_argument("--max_answer_length", type=int, default=30,
help="maximum num of wordpieces/answer. Used at decoding time")
parser.add_argument("--regular_softmax_loss", action='store_true',
help="IF true, use regular softmax. Default is using ORed softmax loss")
parser.add_argument("--test", action='store_true', help="Test only, no training")
parser.add_argument("--model_path", type=str, required=True,
help="Path to the checkpoint directory")
parser.add_argument("--no_progress_bar", action='store_true', help="no progress bar. Good for printing")
parser.add_argument("--attention_mode", type=str, choices=['tvm', 'sliding_chunks'],
default='sliding_chunks', help='Which implementation of selfattention to use')
parser.add_argument("--fp32", action='store_true', help="default is fp16. Use --fp32 to switch to fp32")
parser.add_argument("--seq2seq", action='store_true', help="Use an answer generation model")
parser.add_argument("--resume_ckpt", type=str, help="Path of a checkpoint to resume from")
return parser
def main(args):
random.seed(args.seed)
np.random.seed(args.seed)
torch.manual_seed(args.seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(args.seed)
model = TriviaQA(args)
logger = TestTubeLogger(
save_dir=args.save_dir,
name=args.save_prefix,
version=0 # always use version=0
)
checkpoint_callback = ModelCheckpoint(
filepath=os.path.join(args.save_dir, args.save_prefix, "checkpoints"),
save_top_k=5,
verbose=True,
monitor='avg_val_loss',
# save_last=True,
mode='min',
period=-1,
prefix=''
)
print(args)
train_set_size = 110648 # hardcode dataset size. Needed to compute number of steps for the lr scheduler
args.steps = args.epochs * train_set_size / (args.batch_size * max(args.gpus, 1))
print(f'>>>>>>> #steps: {args.steps}, #epochs: {args.epochs}, batch_size: {args.batch_size * args.gpus} <<<<<<<')
trainer = pl.Trainer(gpus=args.gpus, distributed_backend='ddp' if args.gpus and args.gpus > 1 else None,
track_grad_norm=-1, max_epochs=args.epochs, early_stop_callback=None,
replace_sampler_ddp=False,
accumulate_grad_batches=args.batch_size,
val_check_interval=args.val_every,
num_sanity_val_steps=2,
# check_val_every_n_epoch=2,
val_percent_check=args.val_percent_check,
test_percent_check=args.val_percent_check,
logger=logger if not args.disable_checkpointing else False,
checkpoint_callback=checkpoint_callback if not args.disable_checkpointing else False,
show_progress_bar=not args.no_progress_bar,
use_amp=not args.fp32, amp_level='O2',
resume_from_checkpoint=args.resume_ckpt,
)
if not args.test:
trainer.fit(model)
trainer.test(model)
if __name__ == "__main__":
main_arg_parser = argparse.ArgumentParser(description="triviaQa")
parser = TriviaQA.add_model_specific_args(main_arg_parser, os.getcwd())
args = parser.parse_args()
main(args)